Skip to content
End To End Tester

Cypress

In-browser test execution, automatic retry-ability and time-travel debugging — what Cypress's architecture buys, and the constraints that come with it.

2 min read · updated 19 September 2026

Cypress runs inside the browser, in the same run loop as your application. That single architectural decision explains everything people love and everything they find limiting about it.

#What the architecture buys

Retry-ability everywhere. Every assertion automatically retries until it passes or times out. There is no equivalent of a stale element and no need to think about waiting.

javascript
// No awaits, no waits. The queue handles ordering; assertions retry.
it('adds an item to the cart', () => {
  cy.visit('/products/field-notes');

  cy.findByRole('button', { name: 'Add to cart' }).click();

  cy.findByRole('status').should('have.text', '1 item in cart');
  cy.findByRole('link', { name: 'Checkout' }).should('be.enabled');
});

Time-travel debugging. The Cypress runner keeps a DOM snapshot for every command. Hover over any step afterwards and the application appears exactly as it was at that moment. For diagnosing a failure this is genuinely excellent and remains a real differentiator.

Direct access to the application. Because the test shares a process with the app, you can reach in:

javascript
// Seed state through the app's own store rather than clicking through a form.
cy.window().its('store').invoke('dispatch', { type: 'cart/add', payload: item });

// Stub a network call at the browser level and wait on it by name.
cy.intercept('POST', '/api/orders', { statusCode: 402, body: { error: 'declined' } })
  .as('placeOrder');

cy.findByRole('button', { name: /^Pay/ }).click();
cy.wait('@placeOrder');
cy.findByRole('alert').should('contain.text', 'declined');

cy.intercept plus cy.wait('@alias') is the cleanest network-driven waiting model of the three major tools.

#What the architecture costs

No multi-tab or multi-origin freedom. Cypress lives in the page. A second tab, a native dialog, or an OAuth redirect to another origin all require workarounds — cy.origin() handles the last of these, and it is still more friction than the alternatives.

No real cross-browser parity. Chromium-family browsers and Firefox are supported; WebKit support is experimental. If Safari matters, Playwright or Selenium with a device cloud is the answer.

Parallelisation is a product. Splitting a suite across machines with load balancing is a Cypress Cloud feature. You can shard by hand, but the polished version is paid — which is the main reason large suites migrate.

The command queue surprises people. cy.get() does not return an element; it enqueues a command. This is fine until you try to mix it with ordinary JavaScript:

javascript
// Does not work — text is a Chainable, not a string.
const text = cy.get('h1').invoke('text');
if (text === 'Hello') { /* never true */ }

// Works — stay inside the chain.
cy.get('h1').invoke('text').then((text) => {
  expect(text).to.equal('Hello');
});

#Component testing

Cypress component testing mounts a single component in a real browser with your real bundler config. Unlike jsdom-based Testing Library setups, layout, CSS and real event dispatch all work.

javascript
// cypress/component/PriceTag.cy.tsx
import { PriceTag } from '../../src/PriceTag';

it('shows a strikethrough original price when discounted', () => {
  cy.mount(<PriceTag cents={1_200} wasCents={1_500} />);

  cy.findByText('£12.00').should('be.visible');
  cy.findByText('£15.00').should('have.css', 'text-decoration-line', 'line-through');
});

That last assertion — a computed CSS property — is one no jsdom test can make, and it is the strongest argument for browser-based component testing.

#Configuration worth having

javascript
// cypress.config.ts
export default defineConfig({
  e2e: {
    baseUrl: 'http://127.0.0.1:3000',
    retries: { runMode: 1, openMode: 0 },   // one retry in CI, none locally
    video: false,                            // large, and the snapshots are better
    screenshotOnRunFailure: true,
    experimentalRunAllSpecs: true
  },
  // A per-spec browser restart is the default and it is slow; keep specs
  // few and large rather than many and small.
  numTestsKeptInMemory: 20
});

Install @testing-library/cypress and prefer findByRole over cy.get with a CSS selector — the argument in DOM testing applies identically here.

#Choosing between the three

Cypress Playwright Selenium
Debugging experience best very good (trace) basic
Cross-browser Chromium, Firefox all three engines all, plus devices
Parallel across machines paid free Grid
Multi-tab / multi-origin limited full full
Language support JS/TS only JS/TS, Python, .NET, Java everything
Component testing real browser real browser no

Cypress is a strong choice for a front-end team working in JavaScript on a Chromium-first product, where the developer experience is worth more than the scale ceiling. Beyond a few hundred specs, or where Safari matters, the constraints start to bite.

Common questions

Can Cypress test multiple browser tabs?
Not directly. Cypress runs inside the browser alongside your application, which means it cannot drive a second tab or a separate window. The usual workaround is to assert the target URL and then visit it in the same tab, which covers most real cases.
Is Cypress slower than Playwright?
Generally yes at scale, mainly because parallelisation across machines is a paid Cloud feature and the per-spec startup cost is higher. For a small to medium suite on one machine the difference is not usually what decides it.
Why does Cypress not need awaits?
Commands are enqueued rather than executed immediately, and the queue runs asynchronously. That makes the syntax clean and means you cannot mix Cypress commands with ordinary async code the way you might expect — a common source of confusion for people arriving from Playwright.

Runnable samples for this page

last test results ↗

Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.

Was this page useful?