Ismat Samadov
  • Tags
  • About
17 min read/1 views

Playwright Won the Browser Testing Wars — Here's the Only Guide You Need

Playwright: 45% adoption, 78K GitHub stars, 2-3x faster than Selenium. Auto-wait killed flaky tests. Migration guide from Cypress and Selenium.

TypeScriptDeveloper ToolsPerformanceSoftware Engineering

Related Articles

Remote Work Killed Mentorship — How Senior Engineers Can Fix It

13 min read

The Staff Engineer Trap: Why the Best ICs Get Promoted Into Misery

14 min read

Testing LLM Applications Is Nothing Like Testing Regular Software — Here's What Actually Works

14 min read

Enjoyed this article?

Get new posts delivered to your inbox. No spam, unsubscribe anytime.

On this page

  • Why Playwright Won
  • Auto-Wait Killed Flaky Tests
  • The Comparison Table
  • What Playwright Gets Right That Others Don't
  • Browser Contexts: Fast Test Isolation
  • Multi-Browser, Actually
  • Hybrid UI + API Testing
  • The Trace Viewer
  • Codegen: Record Your Tests
  • Setting Up Playwright From Scratch
  • Step 1: Initialize
  • Step 2: Write Your First Test
  • Step 3: Run
  • Step 4: CI Integration
  • Migrating from Cypress
  • Migrating from Selenium
  • When Playwright Is Not the Answer
  • The 2026 AI Testing Story
  • What I Actually Think
  • Sources

© 2026 Ismat Samadov

RSS

My team's Cypress test suite took 47 minutes to run. We migrated to Playwright. Same tests, same coverage. Fourteen minutes. And we stopped writing cy.wait(5000) — because Playwright's auto-wait made every single one of those artificial delays unnecessary.

Playwright now has 45.1% adoption among QA professionals, 78,600+ GitHub stars, and 13.5 million weekly npm downloads. Selenium dropped from 40% to 22.1% adoption. Cypress holds at 14.4%. The testing wars are over. Playwright won.


Why Playwright Won

Auto-Wait Killed Flaky Tests

The single biggest problem in browser testing is flakiness. Tests pass locally, fail in CI. Pass on Monday, fail on Tuesday. The root cause is usually timing — the test tries to click a button before it's rendered, fill an input before it's interactive, or assert text before the API response arrives.

Cypress tried to solve this with automatic retries on assertions. It helps. But you still need cy.wait() and cy.intercept() for network requests, and .should('be.visible') chains for elements that load asynchronously.

Playwright solved it differently: every action auto-waits. When you write await page.click('#submit'), Playwright:

  1. Waits for the element to appear in the DOM
  2. Waits for it to be visible
  3. Waits for it to be stable (not animating)
  4. Waits for it to be enabled (not disabled)
  5. Waits for it to receive events (not covered by another element)

Only then does it click. No explicit waits. No retry loops. No sleep(3000).

// Playwright — just click. It handles the rest.
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByText('Success')).toBeVisible();

// Cypress — the dance of waits
cy.get('#submit').should('be.visible').click();
cy.contains('Success', { timeout: 10000 }).should('be.visible');

The expect().toBeVisible() assertion in Playwright also auto-retries — it polls until the condition is met or the timeout expires. No .should() chains. No custom retry logic.


The Comparison Table

FeaturePlaywrightCypressSelenium
GitHub Stars78,600+47,000+31,000+
Weekly NPM Downloads13.5M~5M~3M
Browser SupportChromium, Firefox, WebKitChromium, Firefox, WebKit (limited)All browsers
Auto-WaitBuilt-in for all actionsPartial (assertions only)None
Parallel ExecutionNative, per-testPaid (Cypress Cloud)Via Grid
LanguagesJS/TS, Python, Java, .NETJavaScript/TypeScript onlyAll major languages
API TestingBuilt-inVia pluginsVia libraries
Test GeneratorCodegen (record and play)Cypress Studio (limited)Selenium IDE
Trace ViewerFull DOM snapshots + networkTime-travel (screenshots)None built-in
Mobile EmulationDevice emulationViewport onlyAppium (separate)
Component TestingExperimentalStableNot supported
Speed (benchmark)290ms avg per task~400ms536ms
Test IsolationBrowser contexts (fast)New window (slow)New driver (slowest)
PricingFree (MIT)Free + Paid CloudFree (Apache 2.0)
Backed ByMicrosoftCypress.ioSelenium Project

What Playwright Gets Right That Others Don't

Browser Contexts: Fast Test Isolation

Selenium creates a new WebDriver instance per test — expensive. Cypress runs everything in one browser window — fast but leaks state between tests.

Playwright uses browser contexts: lightweight, isolated browser sessions within the same browser instance. Each test gets its own context with its own cookies, storage, and permissions. Creating a context takes milliseconds. No browser restart. No state leakage.

// Each test automatically gets a fresh context
test('user can log in', async ({ page }) => {
  // This page is in an isolated context
  await page.goto('/login');
  await page.fill('#email', 'user@example.com');
  await page.fill('#password', 'password');
  await page.click('button[type="submit"]');
  await expect(page).toHaveURL('/dashboard');
});

test('guest sees landing page', async ({ page }) => {
  // Completely isolated from the test above
  await page.goto('/');
  await expect(page.getByText('Welcome')).toBeVisible();
});

Multi-Browser, Actually

Cypress claimed multi-browser support but had significant limitations with Firefox and WebKit. Selenium supports everything but requires separate driver management.

Playwright ships with Chromium, Firefox, and WebKit built in. One npx playwright install downloads all three. Your tests run identically across browsers with zero configuration:

// playwright.config.ts
export default defineConfig({
  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'] } },
  ],
});

Five browser/device configurations. Same test code. Run in parallel by default.

Hybrid UI + API Testing

This is the feature that nobody talks about enough. Playwright can make API calls natively — no Axios, no Supertest, no separate API testing framework:

test('create and verify user', async ({ page, request }) => {
  // API call — no browser needed
  const response = await request.post('/api/users', {
    data: { name: 'Alice', email: 'alice@example.com' }
  });
  expect(response.ok()).toBeTruthy();
  const user = await response.json();

  // UI verification — same test
  await page.goto(`/users/${user.id}`);
  await expect(page.getByText('Alice')).toBeVisible();
});

You can set up test data via API, run UI interactions, and verify results through both API and UI — all in one test. This eliminates the entire "test data setup" problem that plagues Selenium and Cypress tests.

The Trace Viewer

When a test fails in CI, Playwright's trace viewer gives you a complete recording: DOM snapshots at every step, network requests and responses, console logs, screenshots, and a timeline. You open the trace file in your browser and replay the failed test step by step.

// Enable tracing on first retry
export default defineConfig({
  retries: 1,
  use: {
    trace: 'on-first-retry',
  },
});

When a test fails and retries, the trace is captured and attached to the test report. No more "it failed in CI and I have no idea why." You can see exactly what the page looked like, what network requests were pending, and what the DOM state was at the moment of failure.


Codegen: Record Your Tests

Playwright's code generator watches you interact with your app and writes the test code:

npx playwright codegen http://localhost:3000

A browser opens. You click, type, navigate. Playwright records every action and generates test code in real-time. It's not perfect — you'll want to refactor the output — but it's an excellent starting point for complex interaction flows.

In 2026, Playwright's Codegen can also generate toBeVisible() assertions automatically based on common UI patterns, and v1.56 introduced test agents that work with LLMs to plan, generate, and heal tests.


Setting Up Playwright From Scratch

Step 1: Initialize

npm init playwright@latest

This creates:

  • playwright.config.ts — configuration
  • tests/ — test directory
  • tests-examples/ — sample tests

Step 2: Write Your First Test

// tests/homepage.spec.ts
import { test, expect } from '@playwright/test';

test('homepage has correct title', async ({ page }) => {
  await page.goto('https://myapp.com');
  await expect(page).toHaveTitle(/My App/);
});

test('navigation works', async ({ page }) => {
  await page.goto('https://myapp.com');
  await page.getByRole('link', { name: 'About' }).click();
  await expect(page).toHaveURL(/about/);
  await expect(page.getByRole('heading', { name: 'About Us' })).toBeVisible();
});

Step 3: Run

# Run all tests
npx playwright test

# Run in headed mode (see the browser)
npx playwright test --headed

# Run a specific file
npx playwright test tests/homepage.spec.ts

# Run with UI mode (interactive debugging)
npx playwright test --ui

Step 4: CI Integration

# .github/workflows/e2e.yml
name: E2E Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/

The upload-artifact step ensures that when tests fail, the HTML report (with traces) is available for download.


Migrating from Cypress

If you have an existing Cypress test suite, here's the translation for common patterns:

CypressPlaywright
cy.visit('/page')await page.goto('/page')
cy.get('#id')page.locator('#id')
cy.get('[data-testid="btn"]')page.getByTestId('btn')
cy.contains('text')page.getByText('text')
cy.get('button').click()await page.getByRole('button').click()
cy.get('input').type('text')await page.fill('input', 'text')
cy.wait('@apiCall')await page.waitForResponse('**/api/**')
cy.intercept()await page.route()
.should('be.visible')await expect(locator).toBeVisible()
.should('have.text', 'x')await expect(locator).toHaveText('x')
before(() => {})test.beforeEach(async () => {})
cy.fixture('data.json')JSON.parse(readFileSync('data.json'))

The biggest mental shift: Playwright is async/await everywhere. Cypress uses a custom command chain that looks synchronous but isn't. If you're comfortable with async JavaScript, Playwright will feel more natural.

Migrating from Selenium

Selenium (Python)Playwright (Python)
driver.get(url)await page.goto(url)
driver.find_element(By.ID, 'x')page.locator('#x')
WebDriverWait(...).until(...)Auto-wait (built-in)
element.click()await locator.click()
element.send_keys('text')await locator.fill('text')

The migration from Selenium is more dramatic because you eliminate all explicit waits. Every WebDriverWait and expected_conditions call disappears. That alone can reduce test code by 30-40%.


When Playwright Is Not the Answer

Legacy browser testing. If you need to test IE11 or other browsers Playwright doesn't support, Selenium is your only option. Playwright supports Chromium, Firefox, and WebKit — nothing else.

Existing large Cypress codebase. If you have 2,000 Cypress tests that work, the migration cost may not justify the benefits. The speed improvement is real, but rewriting 2,000 tests is months of work. Consider migrating incrementally — new tests in Playwright, old tests stay in Cypress.

Mobile native testing. Playwright does device emulation (screen size, user agent, touch events) but doesn't test actual native apps. For that, you need Appium, Detox, or XCTest.

Visual regression at scale. Playwright has screenshot comparison built in, but dedicated tools like Percy or Chromatic offer more sophisticated visual diffing, baseline management, and cross-browser visual testing.


The 2026 AI Testing Story

Playwright v1.56 introduced test agents that work with LLMs through the Model Context Protocol (MCP). An MCP server gives AI agents access to the browser — they can navigate pages, fill forms, and verify results using structured accessibility snapshots instead of screenshots.

This means AI coding assistants can generate, run, debug, and iterate on Playwright tests the same way a human would. It's not replacing test engineers — it's accelerating test creation for the 80% of tests that are straightforward.

# Playwright MCP server for AI agents
npx @playwright/mcp

What I Actually Think

Playwright won the browser testing wars because it solved the right problem. Not speed (though it's faster). Not browser support (though it's better). The right problem was developer experience.

Writing a Selenium test felt like fighting the tool. Explicit waits everywhere. Driver management. Flaky element selection. Writing a Cypress test felt better but still had sharp edges — the .then() antipattern, the confusing sync-looking-but-async command chain, the slow test isolation.

Writing a Playwright test feels like writing normal code. await click. await fill. expect visible. The auto-wait means you write what you mean — "click this button" — and the framework handles the rest. No ceremony. No framework-specific patterns. Just async JavaScript.

Microsoft backing Playwright matters. Not because Microsoft is infallible, but because browser testing infrastructure requires continuous investment in browser engine compatibility. Playwright bundles specific versions of Chromium, Firefox, and WebKit and tests against them. That's expensive engineering work. Microsoft has the resources and motivation (VS Code, Azure DevOps, GitHub Actions) to sustain it.

If you're starting a new project, use Playwright. There's no reason to choose Cypress or Selenium for greenfield work in 2026. If you have an existing Cypress suite, plan a gradual migration — write new tests in Playwright and move old tests over time. If you're on Selenium, the migration is more urgent — you'll save significant maintenance hours on wait-related flakiness alone.

The tests that run in 14 minutes instead of 47 aren't just faster. They're more reliable, easier to debug, and simpler to write. That's the trifecta. And that's why Playwright won.


Sources

  1. Playwright — Official Website
  2. microsoft/playwright — GitHub
  3. Playwright — npm
  4. 2025 Playwright Adoption Statistics — DEV Community
  5. Playwright market share 2025 — TestDino
  6. Selenium vs Playwright vs Cypress Comparison 2026 — Vervali
  7. Performance Benchmarks: Playwright, Cypress, Selenium — TestDino
  8. Playwright vs Selenium: QA Automation 2026 — Panto
  9. Playwright vs Cypress in 2026 — Autonoma
  10. Playwright Codegen — Official Docs
  11. Playwright Automation Testing Guide 2026 — ThinKSys
  12. Playwright Features 2026 — ThinKSys
  13. Playwright MCP: AI-Powered Test Automation — TestLeaf
  14. Playwright vs Selenium — BrowserStack
  15. Cross-Browser Testing with Playwright — ThinKSys
  16. Playwright Releases — GitHub
  17. Playwright vs Cypress vs Selenium: Honest Comparison — Decipher