All challenges · Expert & Verification
Challenge 30 of 30 · Expert authoring · Verification
Fix the Flaky Test
A colleague’s test passes on their laptop and fails on the CI runner, where the page is slower and its timing changes on every run. Find why and make it pass however long the page takes.
Skills you will practise
- flaky tests
- auto-waiting actions
- passing in CI
The concept
Most “only fails in CI” bugs are not CI bugs. Your laptop is quietly giving the test something for free: a fast CPU, a warm cache, nothing else running. The shared runner does not, so every step the page takes is a little slower, and by a different amount each run.
In Playwright, actions such as click() and fill() wait on their own: they retry until the element is visible, enabled and stable. Web-first assertions such as expect(locator).toBeVisible() retry too. Methods that return a value straight away do not wait. isVisible(), isEnabled(), textContent() and count() answer once, at the moment they are called.
Wrap one of those snapshot checks in an if and you get the worst kind of flaky test. When the page is quick the check is true and the test passes. When the page is slow the check is false, the if is skipped without an error, and the test fails several lines later, far from the real cause.
A fixed wait such as waitForTimeout(3000) hides the problem rather than fixing it. It is too short on a bad day and wastes time on every good one. In this challenge the hidden validator also checks how soon you moved on after the page was ready.
The fragile version
await page.getByRole('button', { name: 'Place order' }).click();
if (await page.getByText('Payment accepted').isVisible()) {
await page.getByRole('link', { name: 'View receipt' }).click();
}Why it breaks: isVisible() checks once, straight after the click. On a slow runner payment has not finished yet, the if is skipped silently, and the test fails later when the receipt is missing.
A robust Playwright version
await page.getByRole('button', { name: 'Place order' }).click();
await expect(page.getByText('Payment accepted')).toBeVisible();
await page.getByRole('link', { name: 'View receipt' }).click();The assertion retries until payment is accepted, so the test waits exactly as long as the page needs and fails with a clear message if it never happens.
If a later action already depends on the page being ready, that action’s own auto-wait may be all you need.
This is a general example on a made-up page, not the challenge’s reference solution.
Common mistakes
- Raising timeouts or adding
waitForTimeout()until the build goes green. - Using
isVisible()orcount()as a wait. They are snapshots; use an action or anexpectinstead. - Turning on retries to hide the flake instead of finding it. Retries are a safety net, not a fix.
- Reading only the last error. A skipped step usually fails much later than the line that caused it.
FAQ
Why does my Playwright test pass locally but fail in CI?
Usually timing: the CI runner is slower, so a check that does not wait sees the page before it is ready. Fixed sleeps, missing awaits and snapshot checks such as isVisible() are the usual causes.
Does isVisible() wait in Playwright?
No. locator.isVisible() returns the current state immediately. To wait, use await expect(locator).toBeVisible() or an action such as click(), which waits for the element.
How do I make a flaky test fail locally?
Slow the page down: throttle the CPU or network, run the test many times with --repeat-each, or run it in the same container image your CI uses.
Should I just turn on retries?
Retries keep the pipeline moving while you investigate, but they hide the bug. Fix the check that does not wait, then keep retries low.
Try it
Practise this in Challenge 30. You write the code in the browser editor with no setup. It runs in a fresh Chromium, a hidden validator checks what actually happened, and you get pass or fail, the logs and a video of the run. Create a free account to open the practice page and its hints. The reference solution and the hidden validator are shown only after you pass. We never publish solutions here.