fix(e2e): one helper for the reload-until-appears waits, and don't let a transient reload error abort them - #7201
Conversation
A `page.reload()` issued in the milliseconds after a navigation can be rejected by Chromium with `Protocol error (Page.reload): Not attached to an active page` while the frame swap settles. `expect.poll` retries a failing matcher but propagates a throw from its callback, so one such rejection ended a 60s wait after 2.6ms and reported itself as the failure, hiding which wait was actually in progress. Seen on main in run 33173217408. `reloadForRetry` swallows only the known transient messages, so inside a retry loop they cost one interval instead of the test, while a page that is genuinely gone still fails with its own cause.
|
This PR may be related to: #5118 (epic: Unflake integration tests) — this fix addresses one instance of test-infra flakiness that the epic is tracking. |
Replaces the hand-rolled `expect.poll` blocks and the nine untimed
`while (!visible) { reload }` loops with `reloadAndPoll` /
`reloadUntilVisible`.
The nine while loops had no timeout of their own, so they could only fail
by exhausting the enclosing test's budget, with no indication of which
wait was stuck. They now fail with a message naming what never appeared.
`waitForSequences` had a single caller and is inlined into it.
`expect.poll` prints a `message` twice before the matcher's own diff, and a value matcher already says `Expected: >= 3 / Received: 0`, so those messages were noise. A boolean matcher only says `Expected: true / Received: false`, which names nothing, so `reloadUntilVisible` builds its message from the locator instead of each call site repeating it. `waitForSequencesInSearch` polls the count and re-reads the rows rather than assigning to a captured variable as a side effect of polling.
|
Claude finished @corneliusroemer's task in 3m 23s —— View job Review
The diagnosis is right and unusually well evidenced — Three inline comments, one of which I think matters:
Two smaller things not worth their own threads:
On the three questions in the description: matching on error text over a blanket catch is the right call for exactly the reason you give — a refused connection should fail loudly and immediately, not retry silently for 90 seconds with its cause discarded. The I couldn't run the suite (no permission to |
There was a problem hiding this comment.
Pull request overview
This PR hardens Playwright integration tests against a Chromium-specific transient page.reload() failure that can abort expect.poll waits, and consolidates repeated “reload-until-condition” patterns into a shared helper to reduce duplication across the e2e suite.
Changes:
- Added
reloadAndPoll/reloadUntilVisiblehelpers that retrypage.reload()on known transient reload errors and centralize polling intervals/timeouts. - Refactored multiple specs/fixtures/page-object waits to use the new helpers instead of bespoke
expect.pollblocks and ad-hocwhileloops. - Updated
SearchPagewaiting utilities to use the shared helper for reload-based polling.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| integration-tests/tests/utils/reload-helpers.ts | Introduces shared reload+poll helpers and transient reload error handling. |
| integration-tests/tests/specs/features/submission-flow.spec.ts | Replaces custom reload polling with reloadUntilVisible. |
| integration-tests/tests/specs/features/sequence-version-banners.spec.ts | Uses reloadUntilVisible for reload-based waits. |
| integration-tests/tests/specs/features/search/override-hidden-fields.spec.ts | Switches long-running reload loops to reloadUntilVisible (now default-timeout bounded). |
| integration-tests/tests/specs/features/search/lineage-field.spec.ts | Refactors reload loop into reloadUntilVisible. |
| integration-tests/tests/specs/features/review-restricted.spec.ts | Refactors poll into reloadUntilVisible with explicit timeout. |
| integration-tests/tests/readonly.setup.ts | Uses reloadAndPoll for setup-time reload polling. |
| integration-tests/tests/pages/search.page.ts | Refactors page-object reload/poll waits to use reloadAndPoll / reloadUntilVisible. |
| integration-tests/tests/fixtures/sequence.fixture.ts | Uses reloadUntilVisible and removes now-unused expect import. |
Suppressed comments (1)
integration-tests/tests/specs/features/search/override-hidden-fields.spec.ts:83
- This wait is now capped by
reloadUntilVisible’s 90s default, which conflicts with the test’s own comment/timeout indicating this flow can legitimately exceed 90s in CI. Consider passing an explicit timeout here as well.
await reloadUntilVisible(page, page.getByRole('cell', { name: '2012-12-13' }));
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…nged - Tolerate the transient navigation errors around the read as well as the reload. After a swallowed reload rejection the frame swap is still in flight, and `Execution context was destroyed` / `Navigating frame was detached` come from reads, not reloads, so the guard was on the wrong line. - Return the array the poll asserted on again, via `reloadUntil`, rather than re-scraping afterwards. Two callers destructure the first element, so a re-read that came back empty would be a TypeError at the caller. - Read before the first reload. Every `while` loop this replaced checked before reloading, so the helper was doing a needless page load whenever the content was already there. - Keep the poll message where the matcher names a number but not what is being counted, and keep the setup's 60s timeout.
Fixes an integration test flake where a reload issued moments after a navigation gets rejected by the browser, which then aborts the whole wait it was part of.
Also refactors similar polling into a shared helper to reduce verbosity and improve maintainability. Lots of line number reductions in the tests!
Flake looks like this (run 33173217408, chromium, on main):
Details
What is actually going on
The message is misleading twice over: nothing was slow, and nothing was missing from the search results.
The reload was rejected 2.6 ms after it was sent. The line before it had just clicked the "Ebola Sudan" link, which navigates to
/ebola-sudanand gets redirected to/ebola-sudan/search; the reload arrived about 2 ms after that new page's response finished, while the browser was still swapping in the new frame. Chrome refuses a reload during that window, and the phrase it uses is "Not attached to an active page". The page was fine before and after — the trace still has video frames from it after the error, and the sibling test three lines down in the same file did the exact same thing successfully 3.5 seconds later.The reason a 2.6 ms hiccup ends a 60 second wait is the part worth knowing:
expect.pollretries while the matcher fails, but if the function being polled throws, the error escapes immediately. Thetimeoutandintervalsnever come into play, and the poll's own message is never printed. I measured this against the exact Playwright version we use — a function that throws on its first call ends the poll in 3 ms. So any polled function that does something, rather than just reading a value, needs to handle its own errors.That is also why the failure is so hard to read: the reported error is the reload, not the wait, so the log looks like a page that died rather than a wait that was interrupted. The giveaway is in the line above it in the job log — the test is recorded as taking
(1.0s), where a genuine timeout would say(60.0s).Every
page.reload()in the integration tests (15 of them) sits inside a loop that reloads until something appears, so all of them can be killed this way. One of them is inreadonly.setup.ts, where an abort fails every dependent test in the run.The waits were also written out longhand fifteen times, in two shapes: an
expect.pollblock, and a barewhile (!visible) { reload; wait 2s }. They are nowreloadAndPoll(page, read, { message })andreloadUntilVisible(page, locator, { message }).How rare is it
Once, in the 179 failed test jobs I could still get logs for (9 days, 359 finished runs). Logs expire after two to three weeks so that is as far back as it goes.
It is worth fixing anyway because the cost of the fix is one extra loop iteration, and because the failure it produces is actively misleading — it points at a page that has died rather than at whatever the test was waiting for.
Things you might want to decide differently
I match on the error text rather than catching everything. Catching everything is shorter, but a real failure like a refused connection would then retry silently until the wait's timeout with its cause thrown away, instead of failing straight away and saying why. Matching text means we may meet a fourth wording eventually and have to add it; happy to swap it for a blanket catch if you would rather have that.
The
console.warnis there so a burst of these still shows up in the job log rather than vanishing. It is Node-side, so it does not reach theconsole-warningsfixture, which only listens to messages logged inside the browser page —pages/CliPage.tsalready logs this way. Easy to drop if it looks noisy.The nine
whileloops previously had no timeout at all, so they could only fail by using up the enclosing test's whole budget. They now get the helper's default of 90 seconds, which is the more generous of the two values the existing polls of this same "wait for a released sequence to show up" kind already use. This is a behaviour change, not a free win, for two of them:sequence-version-bannershas a 300 second test budget andoverride-hidden-fieldshas 200, so a wait that legitimately took 150 seconds used to pass and would now fail at 90. I think 90 is right — nothing in this suite should need two and a half minutes for a sequence to appear in search, and if something does I would rather see it named than see a bare test timeout — but say the word and I will raise it. Forlineage-field(95 second budget) andsubmission-flow(120) the test timeout fires first regardless, so bounding them changes only the error message. The ninth is insearch.page.ts, whose callers set their own budgets.Left alone deliberately
Separately, and not fixed here:
waitForSequencesInSearchpolls for up to 60 seconds while the per-test timeout is also 60 seconds, so the test always runs out of time first and the poll's message ("Expected at least N sequences to appear in search results") can never actually be printed. That means a genuinely slow indexing run cannot report itself as slow indexing — it reports as a bare test timeout.waitForAccessionVersionInSearchhas the same problem. Worth a follow-up; it did not cause this failure.This does not overlap with #7198, which fixes a different flake in the same file and does not touch any reload.
🚀 Preview: Add
previewlabel to enable