Make Playwright tests more reliable
Replace timing assumptions with meaningful waits, isolate state, and use retries and traces to diagnose intermittent failures.
Intermittent Playwright failures often come from timing assumptions, fragile locators, shared state, external network dependencies, or resource contention. Start with locators and retrying assertions, wait for an application-specific condition rather than a fixed delay, and capture traces that explain the failure.
The examples here illustrate local patterns. Supply your application's fixtures, data, and navigation steps; they are not a benchmark of a particular test suite.
Why flaky tests deserve attention
When a test alternates between passing and failing without a relevant code change, people learn to ignore its failures. Genuine regressions then become harder to distinguish from noise. Improving reliability is part of making test results useful for release decisions.
Address the common causes
1. Understand automatic waiting. Locator actions such as click() and fill() wait for their required actionability conditions. That does not mean they know when an unrelated API request or background job has finished. Add explicit waits only for conditions the action itself cannot establish.
2. Prefer locators that describe the user's target.
// This also waits automatically, but couples the test to an implementation ID.
await page.click('#submit-btn');
// Prefer a meaningful locator when the accessible name is stable.
await page.getByRole('button', { name: 'Submit' }).click();
The benefit is clearer intent and less coupling to markup. A CSS selector used through a locator is still valid; selectors themselves are not the cause of every race.
3. Wait for a specific condition. Replace waitForTimeout(3000) with a retrying assertion or a relevant response wait. Register waitForResponse() before the click that triggers it, then assert the rendered result. This avoids missing a fast response.
4. Separate timeout budgets. Tune the test timeout and assertion timeout independently. Allow measured headroom for slower environments without making broken tests take indefinitely longer to fail.
5. Use a small retry budget. For example, configure retries: 2 in CI. Select a reporter that exposes retry outcomes and track the rate over time. A retry can reduce disruption while you investigate; recurring retries still require a fix.
6. Assert collections directly. Use expect(locator).toHaveCount(n) rather than reading an array and asserting its length once. The locator assertion retries until the required count appears. Check content as well if the right count does not mean the list is ready.
Capture the right trace
--trace on-first-retry captures the first retry, not the original failed attempt. Use retain-on-failure when you need to keep a trace from an initially failing run. Trace Viewer exposes DOM snapshots, network activity, and action timing so you can investigate races, animations, and slow responses.
Frequently asked questions
Can retries hide a real bug?
Yes. Keep the budget small and monitor which tests need it. A test that routinely passes only on retry is evidence of an unresolved problem, not a stable test.
Why does it fail only in CI?
Slower CPUs, competing jobs, and network differences can expose timing windows that are rare locally. Inspect traces, reduce worker concurrency if resources are saturated, and set realistic timeouts. Also check for shared databases, accounts, or files across workers.
How should I handle animation?
Disable irrelevant animation in the test environment with appropriate CSS, or wait for a meaningful completion condition. reduced-motion communicates a user preference; the application must implement styles or behavior that honor it. It does not automatically remove every animation.
References
Related reading
Contact us
Join the community
to join the community
Try Guance
Start online and pay only for what you use.
Get startedChoose a Guance plan