Cypress
Cypress
Definition: An end-to-end testing tool that runs tests directly inside the browser, in the same run loop as the application under test, instead of remote-controlling the browser from an outside process.
How It Works
- Cypress ships as two cooperating pieces: a Node.js process (the test runner) and the browser itself, which it drives directly rather than through a remote WebDriver protocol.
- Test code executes inside the browser, sharing the same event loop as the app. Cypress hooks into the DOM, the network layer, timers, and the
windowobject. - Commands like
cy.get(),cy.click(),cy.type()don’t run immediately. They’re queued and executed one at a time by an internal command queue, each with automatic retry-until-timeout built in. - Because Cypress sits in the network layer,
cy.intercept()can stub, spy on, or rewrite any request or response without a real backend and without a proxy hop. - Assertions retry automatically:
cy.get('.item').should('have.length', 3)doesn’t fail on the first check, it polls until the assertion passes or the default 4-second timeout elapses. - The Node process handles what the browser sandbox can’t: reading fixture files, seeding a database via
cy.task(), taking screenshots and video. - Tests run in a real, visible browser during development (
cypress open) or headlessly in CI (cypress run). - Component testing mode mounts individual React, Vue, or Svelte components in a real browser, using the same command API as full e2e tests, without spinning up the whole app.
- Cypress Cloud (formerly Dashboard) records runs, parallelizes specs across CI machines, and flags flaky tests by re-running failures automatically.
Under the Hood
Contrast with Selenium: a Selenium client library sends HTTP commands to a separate driver process, which relays them to the browser over the W3C WebDriver protocol, one network round trip per action. Cypress skips that: test code and app share a process, so there’s no serialization step, and Cypress can hook into real browser events instead of guessing when a page is “ready.”
Given a login form with an async API call on submit Step:
cy.visit('/login')
cy.get('[data-cy=email]').type('user@example.com')
cy.get('[data-cy=password]').type('secret123')
cy.intercept('POST', '/api/login').as('loginRequest')
cy.get('[data-cy=submit]').click()
cy.wait('@loginRequest').its('response.statusCode').should('eq', 200)
cy.get('[data-cy=welcome]').should('contain', 'Welcome back')
Answer: Cypress queues each command, waits for the intercepted /api/login call to resolve, asserts on its status code, then retries .should('contain', ...) until the welcome banner renders. No manual sleep is needed anywhere in this chain.
Given a list that populates asynchronously
Step: cy.get('.item').should('have.length', 5)
Answer: Cypress re-queries the DOM and re-checks the length on every retry cycle until it matches or the timeout hits, so the test doesn’t care whether the list took 50ms or 3 seconds to fill in.
This is different from a one-shot assertion library: the retry lives inside .should() itself, not in a wrapper waitFor() call the test author has to remember to add.
Why It Matters
- The in-browser model plus automatic retrying removes most of the arbitrary
sleep()calls that make Selenium suites flaky. - Time-travel debugging: every command’s DOM snapshot is stored, so a failed test can be replayed step by step in the Cypress UI instead of re-run with scattered print statements.
- Network stubbing via
cy.intercept()lets teams test frontend behavior (loading states, error handling) without depending on a real, possibly flaky, backend. - The fast local feedback loop makes it practical for developers, not just QA engineers, to write and run e2e tests during regular development.
- Because the same command API works for component tests and full e2e tests, teams don’t need a second tool just to test isolated UI pieces.
Common Pitfalls
- Using
cy.wait(3000)instead of waiting on a specific request or assertion. Fixed sleeps are exactly the flakiness Cypress exists to avoid. - Treating
cy.get()like a synchronous jQuery call and trying to store its result in a plain variable. Cypress commands are queued and async; use.then()to work with resolved values. - Selecting elements by CSS classes or visible text that change with styling, instead of stable
data-cy/data-testidattributes. This breaks tests on unrelated UI tweaks. - Testing implementation details (component state, internal function calls) instead of what a user actually sees and does.
- Assuming every app works with Cypress’s single-origin model without checking. Cross-origin navigation needs
cy.origin()and extra setup. - Not resetting state (database, local storage, cookies) between tests, causing order-dependent tests that fail only when run in a different sequence.
- Chaining too many UI steps into one giant test instead of using
cy.session()or API calls to set up state, which makes failures slow to isolate and tests slow to run.
Comparison
| Cypress | Playwright | Selenium | |
|---|---|---|---|
| Architecture | Runs inside the browser, same process as the app | Node process drives browsers via dev tools protocols | Client sends commands to a separate WebDriver process |
| Browser support | Chromium family, Firefox, WebKit (experimental) | Chromium, Firefox, WebKit | Chromium, Firefox, Safari, Edge |
| Multi-tab / multi-origin | Limited, needs cy.origin() | Native support | Native support |
| Waiting model | Automatic retry-until-timeout | Automatic retry-until-timeout | Manual/explicit waits by default |
| Language support | JavaScript/TypeScript only | JS/TS, Python, Java, .NET | JS, Python, Java, C#, Ruby, and more |
| Best fit | Frontend-heavy teams wanting fast dev feedback | Teams needing true cross-browser coverage | Legacy stacks, broadest language/browser matrix |
| Debugging | Time-travel snapshots in the interactive runner | Trace viewer with DOM snapshots per step | Screenshots plus external logging, no built-in replay |
Example
// cypress/e2e/checkout.cy.js
describe('Checkout flow', () => {
beforeEach(() => {
cy.intercept('GET', '/api/cart', { fixture: 'cart.json' }).as('getCart')
cy.visit('/cart')
cy.wait('@getCart')
})
it('completes checkout with a valid card', () => {
cy.get('[data-cy=checkout-button]').click()
cy.get('[data-cy=card-number]').type('4242424242424242')
cy.get('[data-cy=expiry]').type('12/28')
cy.get('[data-cy=cvc]').type('123')
cy.intercept('POST', '/api/orders').as('placeOrder')
cy.get('[data-cy=place-order]').click()
cy.wait('@placeOrder').its('response.statusCode').should('eq', 201)
cy.url().should('include', '/order-confirmation')
cy.get('[data-cy=order-status]').should('contain', 'Confirmed')
})
})
This stubs the cart response with a fixture, drives the real UI through checkout, and verifies both the network call and the resulting page state, without touching a real payment processor.
Related Terms
- Playwright — the closest modern alternative, with true multi-browser support
- Selenium — the older, out-of-process automation model Cypress’s architecture reacts against
- Jest — often paired with Cypress for unit tests underneath the e2e layer
- Test Pyramid and TDD — where e2e tests like Cypress’s sit relative to unit and integration tests
- REST API — what
cy.intercept()stubs and asserts against - CI-CD — where headless Cypress runs typically live
- JUnit — the xUnit-style pattern that inspired
describe/itstructuring in most JS test tools