Skip to content

Build1 publisher3 min readPublished

A suite failing one run in four needs each test to flake only 0.3 percent of the time

A dev.to post names five habits that rot a Playwright suite, starting with waitForTimeout used to patch a race condition. Working back from its one-bad-run-in-four figure shows how little per-test failure that takes.

The Engineer · Build desk

Illustration accompanying A suite failing one run in four needs each test to flake only 0.3 percent of the time

What happened

  • A dev.to post tracks a Playwright suite from twenty tests green in ninety seconds to a hundred tests failing about one run in four, never the same test twice, with the team re-running until green.
  • Its first cause is a waitForTimeout(2000) added after an intermittent failure, sometimes too short on a loaded CI runner at 3am and costing the full two seconds on every run where it was not needed.
  • Hardcoded test data passes in isolation and collides on 'email already registered' under two workers or a rerun without a database reset, which the post says looks like a registration bug.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • cost The sleeps cost the runs that pass, and nobody profiles a passing pipeline, so the time accumulates without anyone filing a bug against it.
  • decision Enforcement moves from code review into build config: with no-wait-for-timeout at error, the argument happens once in the lint setup instead of in every pull request under deadline.
  • constraint Worker count is capped by fixture design. Until test data is generated per test, adding a second worker converts a suite that passes into a suite that fails.
  • contradiction The post's own per-test login cost implies a bigger saving than the several minutes it claims for a 200-test suite, and the gap is worker count, so the number holds only if you know your own parallelism.

Work the one-in-four figure backwards. Model each test as failing independently with probability p; a 100-test run then goes red with probability 1 - (1-p)^100. Setting that to 0.25 gives p of about 0.29 percent, roughly one failure in 350 test executions [1]. Apply the same rate to the 20-test suite the post starts from and it goes red 5.6 percent of the time, about one run in 18 [2]. That version gets called clean. Independence is a generous assumption here: shared-state failures correlate, and they correlate harder under parallelism [11].

On a loaded CI runner at 3am, two seconds is sometimes not enough [5]. When it is enough, the suite still waits the full two seconds, and the post puts that at three minutes per run across a hundred tests [6]. 2000 ms times 100 tests is 200 seconds, so three minutes and twenty [3]. The figure transfers only if those sleeps are serial. Take the post's own baseline of 90 seconds for 20 tests: that is 4.5 seconds per test, so 450 seconds for 100 tests, and 200 seconds of sleep is a 44 percent increase [4]. Split across four workers, the same 200 seconds is 50 seconds of wall clock [7].

Playwright's assertions already retry until they pass or time out, so an explicit wait is almost never needed [7]. When a click fires an XHR the next step depends on, page.waitForResponse against that request URL waits for the thing itself [8]. The post puts enforcement in eslint-plugin-playwright, with no-wait-for-timeout set to error, so the pipeline rejects the pattern and no reviewer has to police it [9]. "Discipline fails under deadline pressure," the author wrote [10].

The second cause is shared state. A hardcoded email passes alone and fails the moment a second worker, or a rerun without a database reset, hits the same registration endpoint, and the post notes the failure looks like a registration bug [11]. The guard is a data factory with unit tests on the factory itself: asserting that 2000 generated emails are unique takes 30 milliseconds [12].

The login saving is worth re-deriving before you quote it. The post puts a UI login at three to five seconds per test [13] and claims several minutes off a 200-test suite [16]. Run serially, 200 tests at three seconds is 600 seconds and at five seconds is 1000, so 10 to nearly 17 minutes [5]. Divide 600 seconds across three or four workers and you get 200 to 150 seconds, which is where "several minutes" lands [6]. So the claim assumes a parallel run at the cheap end of the login estimate. In practice the setup project writes storageState to playwright/.auth/user.json once [14], each browser project consumes it through dependencies: ['setup'] [15], and the setup asserts the Logout link is visible so a silent auth failure does not arrive as 200 confusing test failures [17].

The fourth cause is the selector chain. '.btn-primary.submit-form > span:nth-child(2)' passes today and breaks the next time a developer wraps something in a div; getByRole('button', { name: 'Submit order' }) locates what the user perceives [18]. Four causes appear in the text: timeouts, hardcoded data, per-test UI login, and selector chains [19]. The list rests on the author's own experience, on projects he does not name [4].

What to watch

  • A per-test flake rate measured on a real suite would confirm or break the compounding model implied by the one-in-four figure.
  • A storageState saving reported alongside worker count and per-test login cost would make the several-minutes claim transferable to other suites.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories