Back to Blog
Playwright: The End of Flaky E2E Tests?
6 min readJun 11, 20266 views

Playwright: The End of Flaky E2E Tests?

I've spent years battling flaky end-to-end tests, and it's a war I'm tired of fighting. Playwright has emerged as a serious contender to finally bring stability and speed to this crucial part of our testing pyramid. Let's dig into why it's different and how it's changing the game.

FrontendToolingWeb DevelopmentTesting
Share

by Sunil Band

Testing: The Unsung Hero (or Silent Killer)

We've all been there: you build a fantastic feature, write some unit and integration tests, and then dread the moment you have to tackle end-to-end (E2E) tests. These tests are critical, simulating real user interactions to ensure the entire system works as expected. But they're also notoriously slow, brittle, and prone to flakiness. One minute they pass, the next they fail for no apparent reason, sucking up developer time and eroding confidence in the test suite.

For years, Selenium was the default, then Cypress came along and improved the developer experience significantly. But even with Cypress, I've still found myself wrestling with random failures, especially in CI environments. Enter Playwright. It's not just another browser automation library; it's a fundamentally different approach to E2E testing that directly addresses many of the pain points that have plagued us for so long.

Why Playwright is Different

Playwright, developed by Microsoft, isn't just a wrapper around browser APIs. It's built from the ground up to be cross-browser, cross-platform, and fast. It supports Chromium, Firefox, and WebKit (Safari's engine) with a single API, which is a huge win for consistency. But the real magic lies in its architecture and design choices.

Auto-Waiting and Actionability

One of the biggest causes of flaky tests is timing. Elements aren't visible, clickable, or enabled when your test script tries to interact with them. Traditional tools often require explicit waits, cy.wait() in Cypress, or arbitrary setTimeout calls, which are fragile and slow. Playwright's auto-waiting mechanism is a game-changer. When you tell Playwright to click an element, it automatically waits for that element to be:

  1. Attached to the DOM.
  2. Visible.
  3. Stable (not animating or moving).
  4. Enabled.
  5. Receiving events at the target point.

This intelligent waiting drastically reduces flakiness without you having to write a single waitFor call. It's not just waiting for an element to exist; it's waiting for it to be actionable from a user's perspective. This aligns perfectly with how a human would interact with the page, making tests more robust.

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

test('should allow a user to log in successfully', async ({ page }) => {
  await page.goto('http://localhost:3000/login'); // Navigate to the login page

  // Playwright automatically waits for these elements to be actionable
  await page.fill('input[name="email"]', 'test@example.com');
  await page.fill('input[name="password"]', 'mySecretPassword');
  await page.click('button[type="submit"]');

  // Assert that we are redirected to the dashboard and see a welcome message
  await expect(page).toHaveURL(/.*dashboard/);
  await expect(page.locator('.welcome-message')).toContainText('Welcome, test@example.com!');

  // Even complex interactions like dropdowns benefit from auto-waiting
  await page.locator('.profile-dropdown').click();
  await page.locator('text=Log out').click();
  await expect(page).toHaveURL(/.*login/);
});

In this example, I don't need to add any explicit waits between filling fields and clicking buttons. Playwright handles it. This makes the test code cleaner, faster, and much less prone to timing issues.

Contexts and Parallelization

Another major feature is browser contexts. Each context is an isolated incognito-like environment, complete with its own cookies, local storage, and sessions. This means you can run multiple tests simultaneously in isolation, even within the same browser instance, without them interfering with each other. This is a huge performance win, especially in CI.

Playwright's test runner also supports parallel execution of tests across multiple workers. By default, it runs tests in parallel using as many workers as your machine has CPU cores. This can dramatically speed up your test suite, turning a 30-minute run into a 5-minute one.

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

export default defineConfig({
  testDir: './tests', // Directory where your test files are located
  fullyParallel: true, // Run tests in parallel
  forbidOnly: !!process.env.CI, // Forbid test.only in CI
  retries: process.env.CI ? 2 : 0, // Retry tests twice on CI
  workers: process.env.CI ? 4 : undefined, // Use 4 workers on CI, otherwise default to CPU cores
  reporter: 'html', // Generate an HTML report

  use: {
    baseURL: 'http://localhost:3000', // Base URL for tests
    trace: 'on-first-retry', // Capture trace for failed tests
    screenshot: 'only-on-failure' // Capture screenshot on failure
  },

  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],

  webServer: {
    command: 'npm run start', // Command to start your dev server
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI, // Reuse server locally, but not on CI
  },
});

This playwright.config.ts shows how easy it is to configure parallelization and browser projects. You get cross-browser testing and parallel execution almost for free, right out of the box. The retries and trace options are also incredibly valuable for debugging those pesky, hard-to-reproduce failures.

Powerful Debugging Tools

Debugging E2E tests has always been a nightmare. Playwright provides an excellent codegen tool to generate tests by recording user interactions, which is great for getting started. But its Playwright Inspector and trace viewer are where it truly shines.

The Inspector allows you to step through your test, see what Playwright is doing at each step, and even pick locators. The trace viewer, however, is on another level. For every failed test (or any test if you configure it), Playwright can capture a complete trace: video of the test, network requests, DOM snapshots, logs, and action timings. This means you can replay exactly what happened in a failed test, frame by frame, without rerunning it. It's like having a perfect debugger from the future.

Trade-offs and Considerations

No tool is a silver bullet, and Playwright has its own set of trade-offs, though they are minor in my experience.

First, while it has excellent TypeScript support and a rich API, the learning curve can be slightly steeper than Cypress for absolute beginners due to its more explicit async/await nature. However, for experienced developers, this is often a preference rather than a true hurdle.

Second, because Playwright interacts with the browser at a lower level, you're not running your tests within the browser's context like Cypress does. This means you can't directly access your application's window object or manipulate its state from your test code in the same way. You'd typically achieve this by mocking network requests or using page.evaluate() to run code in the browser context. This is a deliberate design choice that contributes to Playwright's stability and speed, but it's something to be aware of if you're coming from a Cypress background.

Finally, while Playwright boasts excellent browser coverage, it doesn't currently support Internet Explorer (not that many of us are still actively testing there) or older versions of Safari. For most modern web applications, this isn't an issue, but it's worth noting if you have specific legacy browser requirements.

Wrapping up

If you're still battling flaky E2E tests, spending hours debugging CI failures, or just want a faster, more reliable way to ensure your application works across browsers, you owe it to yourself to try Playwright. Its intelligent auto-waiting, powerful parallelization, and unparalleled debugging tools genuinely change the game for E2E testing. It drastically reduces the mental overhead and frustration traditionally associated with this layer of testing.

My advice? Take an hour, head over to the official Playwright documentation, and follow their "Getting started" guide. Install it in one of your existing projects and convert a single, notoriously flaky E2E test. See for yourself how much more stable and performant it becomes. You might just find your new favorite testing tool.

More from the blog
Available for projectsReady to make something fun 🎈

Ready to build the next system?Wanna build something awesome together?

Currently accepting high-impact opportunities in frontend engineering and scalable web applications.Got a cool idea rattling around? Let's grab a virtual coffee and turn it into something people love. ☕