On this page
Why Playwright tests still flake
Playwright's auto-waiting is a significant improvement over raw WebDriver, but it does not eliminate flakiness. Tests still fail due to timing assumptions, brittle selectors, shared state, and environment differences. The difference is that when Playwright tests flake, the root cause is usually a clear pattern rather than a generic race condition.
Understanding these patterns is the first step to eliminating them. For context on how flakiness affects overall release confidence, see how to measure release confidence like an engineering leader.
Timing anti-patterns and auto-waiting
The most common cause of flaky Playwright tests is relying on implicit timing rather than explicit conditions. Playwright auto-waits for element visibility and actionability, but it does not auto-wait for network responses or data mutations.
| Anti-pattern | Why it flakes | Fix |
|---|---|---|
page.waitForTimeout(n) | Fixed delay fails under varying network speed | Use waitForResponse or waitForSelector |
| Asserting immediately after navigation | Page may not be fully rendered | Add waitForLoadState('networkidle') |
| No wait after form submission | API response may not complete before assertion | Use waitForResponse matching the API call |
| Assuming animation is instant | CSS transitions can delay element availability | Wait for final state, not intermediate transitions |
page.waitForTimeout is the most dangerous API in Playwright for flakiness. It creates a false sense of stability. Tests pass locally and fail in CI because local and CI network speeds differ. Remove all fixed-timeout waits and replace them with condition-based waits.
Selector strategy for long-term stability
Selector fragility is the second most common cause of flaky Playwright tests. The fix is a clear selector hierarchy:
- Prefer
getByRolefor accessibility-based selection. These selectors are stable across design iterations and align with accessibility requirements. - Use
getByTextfor visible labels. Great for buttons, links, and headings where the text is unlikely to change. - Add
data-testidattributes for critical interactions. This is the most stable option for elements that need to be targeted across many tests. - Avoid CSS class selectors. CSS classes change frequently during refactoring and are the number one source of selector breakage.
For a comprehensive approach to building automation suites that stay stable over time, see QA automation architecture services.
State isolation and test fixtures
Tests that share state — whether through browser context, storage, or backend data — will fail intermittently. Playwright provides excellent tools for isolation:
- Use
test.beforeEachto set up fresh state. Never reuse state from a previous test even if the test order seems predictable. - Create isolated test fixtures. Each test should create its own user, session, and data set rather than depending on shared setup.
- Use
context.clearCookies()andcontext.clearPermissions()between tests if tests must share a browser context. - Prefer
test.describe.configure({ mode: 'parallel' })by default. Serial execution hides state dependencies that will surface in parallel mode.
This isolation approach mirrors the patterns used in the Flipkart case study, where zero state leakage was required to maintain 99 percent test stability at scale.
CI-specific failure patterns
Tests that pass locally but fail in CI are almost always environment-related. Common CI-specific issues include:
- Headless rendering differences: Font loading, image rendering, and layout can differ between headless and headed modes. Use
--headedin CI for debugging. - CPU throttling: CI runners have limited CPU. Animations and transitions that complete locally may not finish in CI. Disable CSS animations in test configurations.
- Screen resolution: Default CI resolutions differ from developer machines. Set a consistent viewport size with
page.setViewportSize(). - Network latency: CI environments may have different caching and latency characteristics. Use Playwright's
page.route()to mock network responses for critical API calls.
The Oracle release confidence case study documents how standardizing CI environment configuration reduced environment-related flakiness by 60 percent in the first month.
The Playwright flakiness triage workflow
Use this triage workflow every time you encounter a test that behaves unpredictably. It ensures you don't waste time debugging real defects as flaky or vice versa.
Before-and-after code patterns
Replace waitForTimeout with network-aware waits
// Before: brittle fixed timeout
await page.waitForTimeout(5000);
await expect(page.locator('.results')).toBeVisible();
// After: wait for the API response
const responsePromise = page.waitForResponse('**/api/search');
await page.getByRole('button', { name: 'Search' }).click();
await responsePromise;
await expect(page.getByTestId('results')).toBeVisible();
Use test fixtures for isolated state
// Before: shared setup creates hidden dependencies
let user;
test.beforeAll(async () => {
user = await createUserInDB();
});
// After: isolated fixture per test
test('can update profile', async ({ page }) => {
const user = await createUserInDB({ unique: true });
await page.goto(`/profile/${user.id}`);
// No risk of state leakage from other tests
});
Mock network for deterministic tests
// Before: depends on real API availability
await page.goto('/dashboard');
await expect(page.getByText('Welcome')).toBeVisible();
// After: mock the API for deterministic behavior
await page.route('**/api/user', async route => {
await route.fulfill({ json: { name: 'Test User' } });
});
await page.goto('/dashboard');
await expect(page.getByText('Welcome, Test User')).toBeVisible();
Prevention and health checks
Once your Playwright tests are stable, prevent regression with these practices:
- Run with
--repeat-each 5on critical tests to surface intermittent failures before they reach production CI. - Capture traces on failure using
--trace onso every flaky failure has a replayable trace. - Set a flaky rate budget. If the Playwright suite exceeds 3% flaky rate, pause new test development until stability is restored.
- Review selectors in every PR. Require that new tests use
getByRoleorgetByTestIdrather than CSS selectors.
For the broader context on suite health, read how to fix flaky test suites and CI quality gates that improve release confidence. You can also review the Release Confidence Sprint if you need a structured engagement to fix this fast.
Get the Playwright flakiness checklist
A one-page PDF checklist covering every common flakiness pattern in Playwright with before-and-after code examples. No email required.
Get the checklist via email