TestingOctober 6, 2026 · 8 min

End-to-end tests that don't flake: a Playwright setup for complex React apps

A flaky test suite is a special kind of poison: it goes red at random, the team learns to re-run until green, and then it misses the real regression because nobody trusts it anymore. Most E2E flakiness comes from a handful of fixable anti-patterns. Here's how I set up Playwright so end-to-end tests for complex React apps are actually reliable.

The root cause: racing the app

Flakiness is almost always a timing race — the test checks for something before the app has finished doing it. The classic culprit is a fixed sleep ('wait 2 seconds then click'): too short and it fails on a slow run, too long and your suite crawls. The cure is to wait for conditions, never for time.

// Flaky: races the app
await page.waitForTimeout(2000);
await page.click('.submit');

// Reliable: web-first assertions auto-wait for the condition
await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled();
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Saved')).toBeVisible();   // waits, retries

Locate by role and accessible name

Selecting by CSS class or DOM path couples the test to markup that changes constantly — and it's a big source of both flakiness and false breaks. Prefer role-based and text locators (getByRole, getByLabel, getByText). They're stable across restyles, they read like user intent, and they double as an accessibility check: if Playwright can't find the button by its role and name, neither can a screen reader.

Control the network

  • Don't let tests hit live third-party APIs — mock them so a slow or flaky upstream can't fail your suite.
  • Wait for the specific response your action triggers, not a blanket network-idle that's easy to get wrong.
  • Seed a known state before each test (via API or fixtures) so tests are independent and repeatable.
  • Make every test self-contained — no ordering dependencies, so one failure doesn't cascade.

The test of a good E2E suite is whether a red run means something. If the team's reflex is 'just re-run it,' the suite has already failed at its one job — trust.

Make failures debuggable in CI

When a test does fail in CI, you need to see why without reproducing locally. Playwright's trace viewer, screenshots and video on failure turn a cryptic red into a replay of exactly what happened. Run the suite in parallel to keep it fast, retry once in CI to distinguish a genuine failure from a rare infra blip, and quarantine (don't ignore) any test that retries too often.

Key takeaways

  • Flakiness is a timing race — use web-first, auto-waiting assertions, never fixed sleeps.
  • Locate by role and accessible name, not CSS/DOM paths, for stability and a11y coverage.
  • Mock third-party network calls and seed known state so tests are independent and repeatable.
  • Capture traces/screenshots on failure; a suite the team re-runs on red has lost its purpose.

FAQ

Why are my Playwright tests flaky?

Almost always a timing race — the test acts before the app is ready, usually because of fixed waitForTimeout sleeps or brittle CSS selectors. Switch to web-first assertions that auto-wait and retry (expect(...).toBeVisible()), locate elements by role and accessible name, and mock third-party network calls so upstream slowness can't fail your suite.

How do I stop my team from ignoring failing E2E tests?

Make red mean something. Eliminate flakiness at the source (auto-waiting assertions, role-based locators, mocked network, seeded state), capture traces and screenshots so failures are instantly debuggable, and quarantine any test that still flakes rather than letting it erode trust in the whole suite.

AA
Ali Asghar

Senior software engineer & technical lead — 6+ years shipping production multi-tenant SaaS, payments and AI integration in Next.js, Node & TypeScript.

Keep reading
AI-assisted UI validation: automating visual testing that survives layout change →What actually matters running the Next.js App Router in production →What actually makes a senior software engineer →