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.