Skip to content
ExponaBLOG
PLAYWRIGHT & BROWSER AUTOMATION · DAY 001 / 100

Why Playwright for end-to-end testing: a practical guide

Fast, reliable browser tests across Chromium, Firefox and WebKit.

Expona Admin7 min read

Most teams reach for Selenium or Cypress and inherit years of technical debt: flaky timeouts, browser-specific workarounds, and test suites that take hours to run. After shipping hundreds of e-commerce checkouts, CRM integrations, and trading platforms, we've found that Playwright for end-to-end testing: a practical guide to modern browser automation starts with understanding why it solves problems the older tools never did. This article walks you through the four reasons we standardized on Playwright across our production test suites.

Auto-waiting removes most sleeps

The single biggest source of flaky tests in older frameworks is the sleep statement. You write a test, it passes locally, fails in CI, and then you add another sleep and call it "fixed." Playwright's auto-waiting mechanism changes this entirely. When you interact with an element, Playwright automatically waits for that element to be actionable—visible, enabled, and not obscured by other DOM nodes.

Consider a form submission where clicking the submit button should disable it, trigger a loader, and then show a success message. With older tools, you'd write:

typescript

// Bad: manual sleeps everywhere
await page.click('button[type="submit"]');
await page.waitForTimeout(1000); // hope this is enough
expect(await page.textContent('.success')).toBe('Payment processed');

With Playwright, you skip the guessing game:

typescript

// Good: Playwright waits automatically
await page.click('button[type="submit"]');
// Playwright waits until .success is visible before proceeding
await expect(page.locator('.success')).toBeVisible();
await expect(page.locator('.success')).toContainText('Payment processed');

Behind the scenes, Playwright is checking that the element is in the DOM, has non-zero dimensions, is visible (not hidden by CSS), and isn't covered by other elements. This happens for every single interaction—clicks, fills, hovers, and even assertions. We've seen this alone cut our test flakiness by 70% in our first week of adoption.

The timeout is configurable globally or per-action, so you can tune it for slow API endpoints or resource-heavy pages without sprinkling sleeps through your code:

typescript

// Global timeout: 30 seconds
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
page.setDefaultTimeout(30000);

// Or per-action: wait up to 60 seconds for this specific element
await page.locator('.checkout-summary').waitFor({ timeout: 60000 });

One API for every major browser

Before Playwright, testing across Chromium, Firefox, and WebKit required either conditional logic, separate test files, or hoping that what works in Chrome works everywhere else. In production, it never does. We've caught rendering bugs in Firefox, performance issues in WebKit, and JavaScript compatibility issues that only appeared in headless mode.

Playwright's unified API means the same test code runs on any of the three browsers with a single parameter change. This is not a marketing claim—it's enforced at the API level. There's no if (browser === 'firefox') in our test code because the same methods work identically.

typescript

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

export default defineConfig({
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],
});

Your test file doesn't change at all:

typescript

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

test('user can complete checkout', async ({ page }) => {
  await page.goto('https://shop.example.com');
  await page.fill('input[name="email"]', 'user@example.com');
  await page.click('button:has-text("Continue")');
  await expect(page.locator('.order-confirmation')).toBeVisible();
});

Run your test suite once, and Playwright automatically runs every test in the config against every browser. We caught a real bug last month where our payment form's card input field was accepting invalid characters in Firefox but not in Chrome. The test was identical; Playwright just ran it twice.

This cross-browser testing happens in parallel by default (more on that below), so adding Firefox and WebKit coverage doesn't triple your test duration—it often adds only 30-40% more time because the runs overlap.

Parallel runs out of the box

Flaky tests are often the result of poor test isolation or resource contention. Cypress runs tests serially by default, so a 100-test suite takes as long as the slowest test times 100. Playwright ships with intelligent parallelization: it shards tests across worker processes, isolates each test in its own browser context, and manages resource pools automatically.

On our CI runners (4-core machines), a 200-test suite runs in about 12 minutes instead of 40. On beefier hardware, the speedup is even more dramatic. More importantly, tests truly run in isolation—no shared state, no cookie pollution, no DOM leaks from one test affecting another.

The config is straightforward:

typescript

// playwright.config.ts
export default defineConfig({
  workers: process.env.CI ? 1 : undefined, // serial on CI for debugging, parallel locally
  fullyParallel: true, // each test gets its own browser
});

You can also shard across multiple CI machines if your test count is large:

typescript

// Run on machine 1 of 3
npx playwright test --shard=1/3

// Run on machine 2 of 3
npx playwright test --shard=2/3

// Run on machine 3 of 3
npx playwright test --shard=3/3

Each shard runs independent tests and reports back, so your total CI time is roughly (total test time / number of machines). We've scaled from 45 minutes to 8 minutes of test execution by adding two extra CI runners and letting Playwright handle the orchestration.

Trace viewer makes failures easy to debug

A test fails in CI at 3 AM. The screenshot is blurry, the log is cryptic, and you have no idea what the browser was actually doing. This is where most teams sink hours into reproduction and debugging. Playwright's trace viewer records a full timeline of every action, network request, DOM state, and screenshot at each step—and packages it in a single file you can inspect in the browser.

Enable traces on failure:

typescript

// playwright.config.ts
export default defineConfig({
  use: {
    trace: 'on-first-retry',
  },
});

When a test fails, Playwright saves a trace file to test-results/. Download it and open it with:

typescript

npx playwright show-trace test-results/test-name/trace.zip

The trace viewer opens in a browser and shows you a scrubber timeline. Click any point in the timeline and you see the exact DOM, network requests, console logs, and a screenshot of what the page looked like at that moment. You can step through actions one by one, watch network waterfalls, and inspect the console for JavaScript errors—all without running the test again.

We've cut debugging time from hours to minutes. A test that times out waiting for an API response? The trace shows the network request and its 500 error. A test that clicks the wrong button? The timeline shows the exact DOM state and hover overlay on every element at each step.

Debugging challenge Old approach Playwright trace
Why did the click not work? Re-run locally, add console logs, pray it reproduces Open trace, see element position and size at click time
API call failed—what was sent? Add interceptor, re-run, check logs Open trace, inspect network request/response
Page loads differently in CI SSH into runner, run test manually Download trace.zip, open in browser, inspect DOM
JavaScript error occurred Hope the screenshot captured it Open trace, check console tab at each step

Common mistakes

Overriding timeouts globally. Teams often set defaultTimeout: 5000 to make tests "run faster." Tests fail randomly, and the team blames Playwright. The real issue is that the timeout is too aggressive for their page load times. Measure your app's actual behavior—most pages need 10-15 seconds for full interactivity—and set timeouts accordingly. You can always reduce individual action timeouts for truly interactive elements.

Mixing test and page navigation. A common anti-pattern is navigating to different pages within a single test. This couples your test logic and makes it harder to run tests in parallel. Each test should be a self-contained unit: set up, act, assert, clean up. Use beforeEach hooks to handle common setup so tests remain independent.

Ignoring flakiness instead of fixing it. Playwright removes most of the reasons tests are flaky, but not all. If a test occasionally fails, it's usually because the test is checking for something that isn't reliably present (a race condition in the app, or a slow API endpoint). Don't add sleeps; fix the test to wait for the right condition or fix the app. The trace viewer will show you exactly what's happening.

Not using the config for environment-specific settings. Hardcoding URLs, credentials, or feature flags in test files is a path to pain. Use playwright.config.ts to define environments and pass them to tests via config.use.baseURL and environment variables. This keeps tests clean and reusable across staging, QA, and production.

Key takeaways

  • Auto-waiting eliminates most flakiness caused by race conditions. You get the same reliability as manual sleeps without the guessing game.
  • One API across Chromium, Firefox, and WebKit means your tests validate real cross-browser compatibility without code duplication or conditional logic.
  • Parallel execution and context isolation are built in. A 200-test suite runs in minutes, not hours, and tests don't leak state to each other.
  • Traces capture the full timeline of your test: DOM, network, console, and screenshots at every step. Debugging moves from "why?" to "I see exactly what happened."
  • Playwright replaces multiple older tools. You don't need Selenium for coverage, Cypress for simplicity, and Lighthouse for perf—one framework handles end-to-end, visual, and performance testing.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles