Playwright Flakiness in Real Projects: Why Tests Fail Randomly and How I Fixed It
January 31, 2026 3 min read 884 views
Introduction
Playwright is powerful, fast, and reliable — until tests start failing randomly.
In real-world projects, I experienced a frustrating pattern:
- Tests passed on local machines
- The same tests failed in CI
- Re-running the pipeline sometimes fixed the issue
- No application code was changed
This post explains why Playwright tests become flaky and how I fixed them using proper test design instead of increasing timeouts.
What Is Test Flakiness?
A test is considered flaky when it:
- Passes and fails intermittently
- Fails without code changes
- Cannot be reproduced consistently
Flaky tests are dangerous because they reduce trust in automation and slow down development.
Common Causes of Playwright Flakiness
1. UI Not Fully Ready
A common mistake is assuming that an element is ready as soon as it exists in the DOM.
await page.click('#submitButton');
The element may exist but still be disabled, animating, or waiting for data.
2. Using waitForTimeout
Hardcoded waits like this cause instability:
await page.waitForTimeout(2000);
They work locally but fail in slower CI environments.
3. Weak or Fragile Selectors
Selectors such as:
.page > div:nth-child(3) > button
Break easily when UI structure changes.
4. Network Requests Still Pending
Tests often interact with the UI before API calls finish, leading to partial rendering and failures.
5. Shared State Between Tests
Some tests relied on:
Previous logins
Cached data
Execution order
This caused random failures when tests ran in parallel.
How I Fixed Playwright Flakiness
1. Wait for State, Not Time
Instead of waiting blindly, I waited for meaningful conditions.
await expect(page.locator('#submitButton')).toBeEnabled();
Or:
await page.waitForLoadState('networkidle');
2. Assert Before Acting
Before clicking or typing, I verified visibility and readiness.
const saveButton = page.locator('#saveBtn');
await expect(saveButton).toBeVisible();
await expect(saveButton).toBeEnabled();
await saveButton.click();
3. Use Stable Selectors
I standardized test selectors using data-testid.
<button data-testid="submit-btn">Submit</button>
await page.getByTestId('submit-btn').click();
4. Explicitly Wait for API Responses
For critical flows, I waited for backend confirmation.
await Promise.all([
page.waitForResponse(r => r.url().includes('/api/save') && r.status() === 200),
page.click('#saveBtn'),
]);
5. Fully Isolate Tests
Each test:
Creates its own data
Logs in independently
Does not rely on execution order
This eliminated order-based failures.
6. Improve CI Debugging
To debug CI-only issues, I enabled Playwright diagnostics.
use: {
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
}
This made failures explainable instead of mysterious.
Results After Fixing Flakiness
CI pass rate improved from ~70% to over 99%
No random failures
Faster debugging
Increased confidence in automated tests
Key Takeaways
Flaky tests are usually a test design problem
Avoid hardcoded timeouts
Always wait for state, not time
Stable selectors are essential
Treat tests like production code
Conclusion
Playwright is extremely reliable when used correctly.
Once I redesigned tests to respect UI state, network timing, and isolation, the flakiness disappeared.
Tags:PlaywrightTestingE2ECI/CDQuality
Share: