EduFlow
Testing JavaScript Apps
0%

Section 4 · End-to-End with Playwright

Debugging and Fixing Flaky Tests

20 min3 min readSpot the bug

Lesson at a glance

Debug failing Playwright tests with UI mode, the inspector and the trace viewer, then find and remove the usual causes of flakiness instead of retrying them away.

  1. 1Seeing what happened
  2. 2The usual suspects
  3. 3Proving the fix

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

At a previous job we had an end-to-end suite that was red about one run in five. Everyone knew which tests “always do that”, so everyone reran the pipeline. Then one Friday a real checkout bug shipped, because its failing test was one of the ones that “always do that”. A flaky test isn’t a minor annoyance; it trains the whole team to ignore the alarm.

This lesson has two halves: the tools for seeing why a test failed, and the short list of causes behind almost every flaky test.

Seeing what happened

UI mode is where I spend most of my local debugging time:

Terminal
npx playwright test --ui

It opens a window listing every test. Run one and you get a timeline of its actions; click any step to see a DOM snapshot of the page at that moment, plus console messages, network requests and the test source. It watches files and reruns on save. There’s a locator picker too: hover the snapshot and it suggests a locator.

The inspector steps through a test one action at a time in a headed browser:

Terminal
npx playwright test e2e/settle-up.spec.ts --debug

Add await page.pause() in a test to stop at a specific point.

Traces are for failures you didn’t see happen, usually in CI. With trace: 'on-first-retry' in the config, Playwright records a trace on the first retry of a failed test: every action, a DOM snapshot before and after it, console output, network calls and the source line. Download the report from CI and open the trace:

Terminal
npx playwright show-report            # HTML report, with traces attached
npx playwright show-trace trace.zip   # a single trace

Ninety percent of the time, the snapshot at the failing step tells you the answer immediately: a spinner covering the button, an error toast, a different page than expected.

Four steps: the test fails in CI, Playwright retries it and records a trace, you open the trace and inspect the snapshot, then you fix the cause and confirm with repeat-each. Fails in CI first attempt Retry + trace on-first-retry Read the trace show-trace Fix + prove --repeat-each
From a CI failure to a fix

The usual suspects

Almost every flaky end-to-end test I’ve deleted or fixed had one of these causes.

1. Racing the page. Fixed sleeps (page.waitForTimeout(2000)), values read once and asserted with a non-retrying matcher (expect(await locator.textContent()).toBe(...), expect(await locator.count()).toBe(3)), or if (await locator.isVisible()) branches that check at one arbitrary instant. Fix: web-first assertions (toHaveText, toHaveCount, toBeVisible) and actions that auto-wait.

2. Shared data. Tests that use the same group, the same user or the same seed records, and pass only in a particular order or with one worker. Fix: each test creates its own data, with unique names, through fixtures.

3. Uncontrolled time and randomness. “This week” logic that fails on Mondays, time zones that differ between your laptop and the CI runner, animations still running, random ids in assertions. Fix: page.clock to set the time (await page.clock.setFixedTime(new Date('2026-03-04T10:00:00'))), a fixed timezoneId in the config’s use block, and assertions that don’t depend on generated values.

4. Missing await. page.getByRole('button', { name: 'Add expense' }).click() without await returns immediately; the next line races the click. Fix: the @typescript-eslint/no-floating-promises lint rule catches these at write time.

Third-party scripts (analytics, chat widgets, payment iframes) are a fifth, external cause: block them with page.route in tests unless the test is about them.

Proving the fix

A flaky test that passed once proves nothing. Reproduce first, then prove the fix under the same pressure:

Terminal
npx playwright test e2e/settle-up.spec.ts --repeat-each=30 --workers=4

If it fails a few times in 30 before the fix and zero after, you’re done. In CI, --fail-on-flaky-tests turns the flaky label into a failure, which some teams enable once the suite is clean so new flakiness can’t creep back in quietly.

The exercise shows a test from a real-looking suite with several of these problems. Find them all. That closes the end-to-end section; section 5 steps back to ask how you judge a suite as a whole.

Key takeaways

  • UI mode (npx playwright test --ui) is the fastest local loop: watch mode, a timeline of every action and a DOM snapshot at each step.
  • With trace: 'on-first-retry', a test that fails in CI leaves a trace you can open with npx playwright show-trace to replay it step by step.
  • Most flakiness comes from four causes: fixed sleeps or non-retrying assertions, shared data between tests, uncontrolled time or randomness, and missing awaits.
  • Reproduce flakiness with --repeat-each, fix the cause, and treat tests marked flaky as bugs; retries are a smoke alarm, not a fix.

Further reading

Check your understanding

4 questions

Question 1 of 4

A test failed once in CI and passed on retry. Your config has trace: 'on-first-retry'. What’s the fastest way to see what the page looked like when it failed?

Question 2 of 4

Which change fixes a test that sometimes fails on expect(await page.getByRole('listitem').count()).toBe(3)?

Question 3 of 4

A test checks that an expense added today appears under “This week”. It fails every Monday at 00:30 in CI. What’s the most robust fix?

Question 4 of 4

Playwright reports a test as “flaky”. What does that label mean?

Practice

Spot the bug

Find what makes this test flaky

This Playwright test fails about once in ten CI runs, never on the author’s laptop. Select every line that makes it flaky or unreliable. (Assume the app itself works.)

Select the line(s) you think are wrong, then check.