Our Pick Playwright — Multi-browser support including Firefox and Safari, faster parallel execution, no trade-offs on same-origin policy, and Microsoft's active development make Playwright the stronger choice for comprehensive E2E testing.
Playwright vs Cypress

import ComparisonTable from ’../../components/ComparisonTable.astro’;

End-to-end testing has consolidated around two frameworks: Playwright (Microsoft) and Cypress. Both provide modern developer experiences — the differences matter at scale.

Quick Verdict

Choose Playwright if: You need cross-browser testing (Firefox, Safari, WebKit), want faster parallel test execution, or are testing applications with cross-origin iframes.

Choose Cypress if: You want the best debugging experience with time-travel and visual replay, are already invested in the Cypress ecosystem, or prefer its more opinionated API.


Feature Comparison

<ComparisonTable headers={[“Feature”, “Playwright”, “Cypress”]} rows={[ [“Browser support”, “Chromium, Firefox, WebKit/Safari”, “Chrome, Firefox, Edge (no Safari)”], [“Parallel execution”, “Native (free)”, “Paid (Cypress Cloud)”], [“Cross-origin”, “Yes”, “Limited”], [“Language support”, “JS/TS, Python, Java, C#”, “JS/TS only”], [“Time-travel debugging”, “Trace viewer”, “Built-in replays”], [“Mobile emulation”, “Excellent”, “Limited”], [“Component testing”, “Yes (experimental)”, “Yes (mature)”], [“Installation”, “npm install only”, “Electron + npm”], [“Speed”, “Faster (parallel)”, “Slower (sequential default)”], [“Learning curve”, “Medium”, “Low”], ]} />


Playwright Tests

import { test, expect } from '@playwright/test';

test.describe('E-commerce checkout', () => {
  test('complete purchase flow', async ({ page }) => {
    await page.goto('https://shop.example.com');

    // Add product to cart
    await page.getByRole('button', { name: 'Add to Cart' }).first().click();
    await expect(page.getByTestId('cart-count')).toHaveText('1');

    // Navigate to checkout
    await page.getByRole('link', { name: 'Checkout' }).click();

    // Fill shipping form
    await page.getByLabel('First name').fill('Alice');
    await page.getByLabel('Last name').fill('Johnson');
    await page.getByLabel('Email').fill('[email protected]');
    await page.getByLabel('Address').fill('123 Main St');
    await page.getByLabel('City').fill('San Francisco');
    await page.getByLabel('ZIP code').fill('94105');

    // Payment
    const cardFrame = page.frameLocator('[data-testid="card-frame"]');
    await cardFrame.getByLabel('Card number').fill('4242424242424242');
    await cardFrame.getByLabel('Expiry').fill('12/28');
    await cardFrame.getByLabel('CVC').fill('123');

    // Submit
    await page.getByRole('button', { name: 'Place Order' }).click();
    await expect(page.getByText('Order confirmed')).toBeVisible({ timeout: 10000 });
  });

  test('handles out-of-stock gracefully', async ({ page }) => {
    await page.goto('https://shop.example.com/product/out-of-stock-item');
    await expect(page.getByRole('button', { name: 'Add to Cart' })).toBeDisabled();
    await expect(page.getByText('Out of stock')).toBeVisible();
  });
});

Playwright configuration:

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 4 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
    { name: 'mobile-chrome', use: { ...devices['Pixel 5'] } },
    { name: 'mobile-safari', use: { ...devices['iPhone 13'] } },
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});

Cypress Tests

describe('E-commerce checkout', () => {
  it('completes purchase flow', () => {
    cy.visit('https://shop.example.com');

    // Add product to cart
    cy.get('[data-cy=add-to-cart]').first().click();
    cy.get('[data-cy=cart-count]').should('have.text', '1');

    // Navigate to checkout
    cy.get('[data-cy=checkout-link]').click();

    // Fill shipping form
    cy.get('[data-cy=first-name]').type('Alice');
    cy.get('[data-cy=last-name]').type('Johnson');
    cy.get('[data-cy=email]').type('[email protected]');

    // Cypress can intercept and mock API calls
    cy.intercept('POST', '/api/orders', { fixture: 'order-success.json' }).as('createOrder');

    cy.get('[data-cy=place-order]').click();
    cy.wait('@createOrder');
    cy.contains('Order confirmed').should('be.visible');
  });
});

Cypress’s strength — network interception:

// Stubbing API calls is elegant in Cypress
cy.intercept('GET', '/api/products*', { fixture: 'products.json' }).as('getProducts');
cy.visit('/products');
cy.wait('@getProducts');

// Verify specific request was made
cy.get('@getProducts').its('request.url').should('include', '?category=electronics');

Debugging: Where Cypress Leads

Cypress’s time-travel debugging is genuinely excellent:

  • Every command snapshot is stored
  • Click any step to see the DOM state at that point
  • Console output tied to each command
  • Automatic screenshots on failure

Playwright’s Trace Viewer is powerful but requires opening a separate viewer tool:

# Run with trace
npx playwright test --trace on

# Open trace viewer
npx playwright show-trace trace.zip

Parallel Execution

Playwright: Parallel by default, free.

# Run all tests in parallel across 4 workers
npx playwright test --workers 4

# Run across all configured browsers in parallel
npx playwright test --project chromium --project firefox --project webkit

Cypress: Sequential by default. Parallelism requires Cypress Cloud (paid):

# Free: sequential
npx cypress run

# Paid (Cypress Cloud): parallel
npx cypress run --record --parallel --ci-build-id $BUILD_ID

For large test suites, this is a meaningful cost difference.


Cross-Origin and iframes

Playwright: Handles cross-origin iframes natively.

// Playwright can interact with cross-origin iframes
const stripeFrame = page.frameLocator('[data-testid="stripe-frame"]');
await stripeFrame.getByLabel('Card number').fill('4242424242424242');

Cypress: Same-origin policy limitations. Cross-origin iframes require workarounds or cy.origin() (added in Cypress 9.6, but limited).

This is a real limitation for testing payment forms, embedded maps, or any third-party widget.


Bottom Line

Playwright for new projects — free parallelism, true cross-browser testing (including Safari/WebKit), and no same-origin limitations make it the more powerful and complete tool. Cypress remains excellent for teams that value its developer experience, network interception API, and time-travel debugging — and are willing to pay for parallel execution. Both are mature, well-maintained frameworks; pick based on your browser requirements and team preferences.