Skip to main content
← All Articles · · 11 min read

Playwright vs Cypress vs Selenium: E2E Testing Framework Comparison 2026

Compare Playwright, Cypress, and Selenium for end-to-end testing. Speed, browser support, DX, CI integration, and which framework to choose in 2026.

testingplaywrightcypressseleniume2efrontend

Choosing an end-to-end testing framework in 2026 should be straightforward, but the options have multiplied faster than the opinions. Playwright, Cypress, and Selenium each claim to be the right answer—and depending on your stack and team size, each of them might be correct.

This guide skips the marketing copy and focuses on what actually matters: how they handle async operations, how fast they run in CI, how much setup they require, and where each one breaks down in real projects.


What E2E Testing Actually Tests

Before comparing frameworks, it’s worth being precise about what end-to-end testing does and doesn’t cover.

E2E tests simulate real user behavior through a real browser. They click buttons, fill forms, navigate pages, and verify outcomes. Unlike unit tests or API tests, they exercise the full application stack—frontend JavaScript, network requests, backend services, and database state.

The downside is cost: E2E tests are slow, flaky, and require maintenance. A test suite that takes 2 minutes locally can take 20 minutes in CI. A selector that works today breaks when a designer renames a CSS class.

The frameworks covered here—Playwright, Cypress, and Selenium WebDriver—differ significantly in how they handle these tradeoffs. Understanding those differences is more useful than reading feature checklists.


Playwright

Playwright is Microsoft’s open-source browser automation library. Released in 2020, it has grown quickly and overtaken Cypress in GitHub stars and many developer surveys by 2025.

Architecture

Playwright communicates with browsers through the DevTools Protocol (for Chromium) and equivalent protocols for Firefox and WebKit. It runs out-of-process, meaning your test code and browser run in separate processes. This avoids a class of timing issues that plague in-process approaches.

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

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  ],
});

Auto-Waiting

Playwright’s most important feature is auto-waiting. Every interaction waits for the target element to be actionable before proceeding—visible, stable, not covered by another element, and able to receive events.

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

test('user can submit a form', async ({ page }) => {
  await page.goto('/contact');

  // Playwright waits for the input to be actionable
  await page.fill('[name="email"]', '[email protected]');
  await page.fill('[name="message"]', 'Hello, world');
  await page.click('button[type="submit"]');

  // Waits for the success message to appear
  await expect(page.locator('.success-message')).toBeVisible();
});

There are no manual waitFor calls in this test. Playwright handles the timing. In practice, this eliminates a large category of flaky tests.

Parallel Execution

Playwright runs tests in parallel by default across multiple worker processes. Each worker gets its own browser context, which is isolated but shares a running browser instance. This keeps overhead low while maintaining test isolation.

A suite of 200 tests that runs in 8 minutes sequentially can run in 90 seconds with 8 workers on a reasonable CI machine.

Tracing and Debugging

When tests fail in CI, Playwright generates trace files that include:

  • A timeline of all test actions
  • Network requests and responses
  • Console logs and errors
  • Screenshots at each step
  • DOM snapshots

You can view these in the Playwright Trace Viewer, a web UI that makes CI failures debuggable without having to reproduce them locally.

# View a trace file
npx playwright show-trace trace.zip

Cross-Browser Support

Playwright supports Chromium, Firefox, and WebKit (the engine behind Safari) out of the box. It bundles its own browser binaries rather than relying on system installations.

# Install browsers
npx playwright install

# Install only specific browsers
npx playwright install chromium firefox

This means your tests run against the same browser versions everywhere—locally, in CI, and across developer machines.

API Testing Built In

Playwright includes an APIRequestContext for API testing alongside browser tests:

test('API and UI agree on order count', async ({ page, request }) => {
  // Check API directly
  const response = await request.get('/api/orders');
  const orders = await response.json();

  // Verify UI matches
  await page.goto('/dashboard');
  await expect(page.locator('.order-count')).toHaveText(
    orders.length.toString()
  );
});

This is useful for setting up state via API before a UI test, or verifying backend state after a UI action.


Cypress

Cypress was the dominant choice for E2E testing from roughly 2018 to 2023. It pioneered a developer-friendly testing experience and time-travel debugging.

Architecture

Cypress runs inside the browser alongside your application code, sharing the same JavaScript runtime. This is the opposite of Playwright’s out-of-process design.

The in-browser architecture gives Cypress direct access to your application’s JavaScript objects, network requests (via cy.intercept), and state. But it also means Cypress can only run in one browser at a time, and the shared event loop can cause subtle timing issues.

// cypress.config.js
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    viewportWidth: 1280,
    viewportHeight: 720,
    video: true,
    screenshotOnRunFailure: true,
  },
})

Command Chaining and Retries

Cypress uses a command queue with built-in retry logic. When you write:

cy.get('.submit-button').click()
cy.contains('Success').should('be.visible')

Cypress queues these commands and retries assertions until they pass or a timeout is exceeded. This is Cypress’s version of auto-waiting—it handles most timing issues without explicit waits.

The tradeoff is the chained command style, which is opinionated and can feel unfamiliar coming from other testing tools.

Time Travel Debugging

Cypress’s strongest differentiator is its interactive test runner. During development, you can run tests in a browser window and see:

  • Each command as it executes
  • DOM snapshots at every step (hover to “time travel” back to that point)
  • Network requests intercepted and modified
  • Console output

This makes writing and debugging tests significantly faster than working with CI-only tools.

cy.intercept('GET', '/api/users', { fixture: 'users.json' }).as('getUsers')
cy.visit('/dashboard')
cy.wait('@getUsers')
cy.get('[data-cy="user-list"]').should('have.length', 3)

Network Interception

cy.intercept is Cypress’s network stub layer. You can intercept any request and return fixtures, modify responses, or simulate errors:

// Simulate a server error
cy.intercept('POST', '/api/checkout', { statusCode: 500 }).as('checkoutFail')
cy.get('[data-cy="checkout-button"]').click()
cy.wait('@checkoutFail')
cy.get('.error-banner').should('contain', 'Payment failed')

This capability is useful for testing error states that are hard to reproduce against a real backend.

Limitations

Cypress has real limitations worth knowing before you commit to it:

Single tab only. Cypress cannot handle multiple browser tabs. Applications that open new windows for OAuth flows, payment processors, or document previews require workarounds or stubbing.

No native iframe support. Testing content inside iframes requires plugins or workarounds.

No multi-browser parallel runs. Cypress runs tests in one browser at a time per runner instance. Cross-browser coverage requires separate CI jobs.

In-process limitations. The shared JavaScript runtime occasionally causes tests to interfere with application state in unexpected ways.


Selenium WebDriver

Selenium WebDriver is the oldest of the three, dating to 2004. It remains the most widely used E2E framework in absolute terms—largely because it has been the default in enterprise Java and Python shops for two decades.

Architecture

Selenium communicates with browsers through a WebDriver protocol that every major browser vendor implements. Your test code sends HTTP commands to a driver process (ChromeDriver, GeckoDriver, etc.) which translates them into browser actions.

# Python example with Selenium
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

driver = webdriver.Chrome()
driver.get("http://localhost:3000")

wait = WebDriverWait(driver, 10)
email_input = wait.until(
    EC.element_to_be_clickable((By.NAME, "email"))
)
email_input.send_keys("[email protected]")
driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()

success = wait.until(
    EC.visibility_of_element_located((By.CLASS_NAME, "success-message"))
)
assert "Thank you" in success.text
driver.quit()

Where Selenium Still Wins

Selenium has advantages in specific contexts:

Language flexibility. Selenium has official bindings for Java, Python, C#, Ruby, JavaScript, and Kotlin. If your team’s testing expertise is in Python or Java, Selenium lets you use that expertise without switching languages.

Browser compatibility. Selenium supports any browser with a WebDriver implementation, including Internet Explorer, Edge, and mobile browsers via Appium.

Grid infrastructure. Selenium Grid distributes tests across multiple machines and browsers. If you have existing Selenium Grid infrastructure, migrating to a newer framework requires significant work.

Mature ecosystem. Twenty years of usage means extensive documentation, Stack Overflow answers, and third-party integrations.

Where Selenium Falls Short

Selenium’s age shows in its developer experience:

No built-in auto-waiting. Selenium requires explicit waits for nearly every async operation. Forgetting them causes flaky tests. The WebDriverWait pattern works but is verbose compared to Playwright’s implicit handling.

Higher flakiness baseline. The HTTP-based WebDriver protocol introduces latency that can cause race conditions. Tests that pass locally fail in CI because timing is slightly different.

No trace viewer. When Selenium tests fail in CI, debugging requires screenshots, logs, and guesswork. There is no equivalent to Playwright’s trace viewer or Cypress’s time-travel debugging.

Setup complexity. Installing and maintaining browser drivers (ChromeDriver must match your Chrome version) adds operational overhead. Playwright and Cypress bundle their own browsers.


Side-by-Side Comparison

FeaturePlaywrightCypressSelenium
Auto-waitingYes (built-in)Yes (retry-based)No (manual waits)
Multi-browserChrome, Firefox, SafariChrome, Firefox, EdgeAny WebDriver browser
Parallel testsYes (workers)Yes (dashboard, paid)Yes (Grid)
Language supportJS/TS, Python, Java, C#JS/TS onlyJS, Python, Java, C#, Ruby
Trace/debugTrace ViewerTime TravelScreenshots only
Setup complexityLowLowMedium-High
CI performanceExcellentGoodVariable
Multiple tabsYesNoYes
iframesYesLimitedYes
API testingBuilt inPluginExternal
LicenseApache 2.0MIT (open core)Apache 2.0
Mobile testingEmulationEmulationVia Appium

Performance in CI

Raw speed matters in CI. Slow test suites delay deployments and reduce how often developers run tests.

For a benchmark of 300 tests across a typical React application:

Playwright (8 workers): ~4 minutes Playwright (1 worker, sequential): ~18 minutes Cypress (1 runner): ~22 minutes Cypress (4 runners, parallelization): ~7 minutes (requires Cypress Cloud subscription) Selenium (ChromeDriver): ~25–35 minutes depending on explicit wait configuration

These numbers vary significantly based on test design, application complexity, and CI machine specs. The takeaway is that Playwright’s built-in parallel execution gives it a substantial advantage without additional infrastructure.

Cypress parallelization requires Cypress Cloud, which has pricing based on test runs. For high-volume test suites, this cost is worth calculating.


CI/CD Integration

All three frameworks work with any CI system, but the integration experience differs.

Playwright in GitHub Actions

# .github/workflows/playwright.yml
name: Playwright Tests
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - name: Install dependencies
        run: npm ci
      - name: Install Playwright Browsers
        run: npx playwright install --with-deps
      - name: Run Playwright tests
        run: npx playwright test
      - uses: actions/upload-artifact@v4
        if: ${{ !cancelled() }}
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

Cypress in GitHub Actions

# .github/workflows/cypress.yml
name: Cypress Tests
on: [push]

jobs:
  cypress-run:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Cypress run
        uses: cypress-io/github-action@v6
        with:
          build: npm run build
          start: npm start
          wait-on: 'http://localhost:3000'

The cypress-io/github-action simplifies Cypress CI setup significantly—it handles starting your server and waiting for it to be ready.


Choosing the Right Framework

The frameworks aren’t equal across all contexts. Here is guidance based on common team situations.

Choose Playwright if:

  • You are starting a new project and have no existing framework investment
  • Your application uses multiple browser tabs, complex OAuth flows, or cross-domain navigation
  • You need cross-browser coverage including Safari/WebKit
  • CI speed is a priority and you want parallel execution without additional cost
  • Your team works in TypeScript primarily

Playwright is the best default choice for new projects in 2026. It has the strongest momentum, the most complete feature set, and handles the cases that cause problems in other frameworks.

Choose Cypress if:

  • Your team values interactive debugging over CI speed
  • You are adding E2E tests to an existing project where developers are unfamiliar with testing
  • Your application doesn’t require multiple tabs or iframe interaction
  • You want the best test-writing experience during development

Cypress’s time-travel debugging is genuinely useful when onboarding developers to testing. The visual feedback loop makes it easier to understand what tests are doing and why they fail.

Choose Selenium if:

  • You have existing Selenium test infrastructure and a large test suite
  • Your team’s expertise is in Java, Python, or C# and you don’t want to introduce JavaScript tooling
  • You need to test in browsers that don’t have Playwright or Cypress support
  • You’re testing mobile applications using Appium (which extends the WebDriver protocol)

Selenium is rarely the right choice for new projects, but the cost of migrating a large existing suite is real. If you have hundreds of passing Selenium tests, the right move is usually to stop writing new Selenium tests and write new tests in Playwright—not to rewrite everything at once.


Migrating from Selenium or Cypress to Playwright

If you decide to migrate, a gradual approach works better than a full rewrite.

From Selenium to Playwright

  1. Install Playwright alongside Selenium: npm install -D @playwright/test
  2. Run both suites in CI temporarily
  3. Write all new tests in Playwright
  4. Gradually convert high-priority or frequently-failing Selenium tests
  5. Remove Selenium when coverage is equivalent

The conversion is mostly mechanical: replace driver.findElement(By.CSS_SELECTOR, sel) with page.locator(sel), replace explicit waits with Playwright’s built-in auto-waiting, and replace WebDriverWait assertions with Playwright’s expect API.

From Cypress to Playwright

The concepts are similar enough that the conversion is mostly syntactic:

CypressPlaywright
cy.visit('/path')await page.goto('/path')
cy.get('.selector')page.locator('.selector')
cy.click()await locator.click()
cy.type('text')await locator.fill('text')
cy.contains('text')page.getByText('text')
cy.intercept(...)await page.route(...)
should('be.visible')await expect(locator).toBeVisible()

The biggest mental shift is from Cypress’s synchronous-looking command queue to Playwright’s explicit async/await. Playwright tests are real async JavaScript—no special command queuing to understand.


Testing Best Practices That Apply to All Three

Framework choice matters less than how you write tests.

Use data attributes for selectors. Avoid selecting by CSS class, text content, or element position. Use data-testid attributes that exist only for testing:

<button data-testid="submit-order">Place Order</button>
// Playwright
await page.getByTestId('submit-order').click();

// Cypress
cy.get('[data-testid="submit-order"]').click();

Keep tests independent. Each test should set up and tear down its own state. Tests that depend on execution order fail unpredictably and are hard to debug.

Test behavior, not implementation. Test what the user sees and does, not internal component state. Tests that assert on class names or internal function calls break with every refactor.

Use fixtures for complex state. If a test requires 20 database rows to be set up, use a fixture or seed script rather than UI actions. Creating state through the UI is slow and creates cascading failures when the UI changes.

Limit what E2E tests cover. E2E tests are expensive. Test the critical user paths—login, purchase, core workflows. Leave edge cases and error conditions to unit and integration tests.


Connecting E2E Tests to Load Testing

E2E tests verify correctness under normal conditions. If you need to know how your application behaves under load, you need a different class of tool.

k6 and Artillery are open-source load testing frameworks that simulate concurrent users against your API or frontend. Running load tests alongside E2E tests in CI gives you both correctness and performance signal.

A typical CI pipeline might run:

  1. Unit tests (fastest, broadest coverage)
  2. API integration tests (moderate speed, covers backend contracts)
  3. E2E tests with Playwright (slowest, covers critical user flows)
  4. Load tests with k6 (periodic, not on every PR)

For a broader view of testing frameworks and API testing strategies, see our API Testing Best Practices guide and CI/CD integration guide with GitHub Actions.


Summary

Playwright is the strongest choice for new E2E testing projects in 2026. It handles the full range of modern web application requirements—multiple browsers, multiple tabs, parallel execution, and built-in API testing—with minimal configuration and excellent CI performance.

Cypress remains a compelling option when developer experience during test writing is the priority. Its interactive runner and time-travel debugging make testing more accessible for teams new to E2E testing.

Selenium is the right choice when you have an existing investment to protect or need to test in environments that Playwright and Cypress don’t support.

The best framework is the one your team will actually maintain. A well-written Cypress suite beats a poorly-maintained Playwright suite every time.

Free Download

50 CLI Commands That Save Hours Every Week

The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.

  • Git mastery (10 cmds)
  • File & search wizardry
  • Docker & DevOps tricks
  • Process & network tools
  • Productivity boosters
  • macOS & Linux install guide

No spam. 1 email/week max. Unsubscribe anytime.

Found this guide useful? Check out our free developer tools.