Building resilient Playwright suites that don't flake
Flaky end-to-end tests erode trust faster than no tests at all. Here's how I keep Playwright suites fast, stable and worth running.
Flaky tests are worse than no tests. A suite that fails randomly trains a team to ignore red builds — and once people stop trusting the pipeline, automation stops paying for itself. Over years of building test automation, a few principles keep suites stable.
Wait for state, never for time
The single biggest source of flakiness is sleep. Playwright's auto-waiting and
web-first assertions remove almost all of it — assert on the condition you care
about and let Playwright poll.
// Don't: arbitrary timing, race conditions waiting to happen
await page.waitForTimeout(2000);
expect(await page.locator(".total").textContent()).toBe("€42.00");
// Do: assert on state, Playwright retries until it's true
await expect(page.getByTestId("cart-total")).toHaveText("€42.00");Isolate every test
Shared state between tests is the second-biggest cause of intermittent failures. Each test should set up its own data and tear it down — no ordering assumptions.
- Seed data through the API, not the UI: it's faster and less brittle.
- Use a fresh browser context per test for clean cookies and storage.
- Never depend on a test that ran before you.
Make selectors intentional
Target elements by role or test id, not by brittle CSS paths that break on every restyle.
// Resilient: survives layout and styling changes
await page.getByRole("button", { name: "Continue" }).click();
await page.getByTestId("email").fill("user@example.com");Run them like production
A suite that only passes locally isn't done. Run it in CI, in containers, in parallel — the same way it'll run for real. That's where the remaining flakiness hides, and it's exactly what a good Docker setup makes reproducible.
Stable automation is a feature. Treat flakiness as a bug with a root cause, not as background noise.