You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
CORRECTION (posted after diagnosis). The "Likely cause" section as originally filed blamed the @testing-library/user-event 14.6.1 -> 14.6.7 bump. That is wrong and has been refuted twice. The corrected diagnosis is below. Do not re-run the dependency bisection — it has now been done twice with a negative result.
Problem
Quality Gates fails on the beta -> main promotion PR (#2075). 11 tests fail, all of them 60-second timeouts, confined to two files:
Shards 1-4 were healthy on the same run where 5 and 6 blew up, so the CI runner as a whole was fine.
Root cause (corrected)
Not a dependency regression. Not a production bug. Not a bad assertion. This is real-timer scheduling latency in the test harness.
Under real timers, every userEvent sub-event awaits wait(config) — a real setTimeout(..., 0) inside the jsdom vm context. A single user.click() dispatches the full pointer/focus/click sequence, each step yielding a macrotask, each running pointerEventsCheck: EachApiCall (an ancestor walk calling getComputedStyle per level). So:
cost = (number of macrotask yields) x (event-loop scheduling latency)
On a contended runner with 6 Jest workers the latency term dominates. That is precisely why the timeline above worsens monotonically (8 -> 26 -> 45 min) with no code change between runs — a dependency-caused regression would have stepped once and stayed flat.
The decisive evidence is a control group inside the failing file itself.SearchPicker.test.tsx lines 79 and 100 use the fake-timer idiom. Those two tests type into the same input, flip the same isOpen, and mount the same FloatingPortal for the first time — and they are fast and have never failed. The cost therefore is not the portal mount's CPU work; it is the real-timer scheduling around it. Fake timers collapse every yield into a synchronous advanceTimersByTime, which is why the control group is immune.
Binary diff of the two published user-event tarballs (normalizing transpiler churn): the only substantive changes in dist/esm are a key-repeat flag in keyboard/index.js and a property-descriptor form change in document/patchFocus.js. utils/misc/wait.js is byte-identical and delay: 0 is unchanged in setup/setup.js. No hot-path change capable of a 5x regression.
A separate earlier investigation on PR #2073 had already excluded i18next/react-i18next by controlled swap.
Why raising the timeout again is the wrong fix
PR #2070 responded to this by raising the global testTimeout to 60000. We are now past that ceiling too. Raising it a third time keeps a 45-minute shard in the critical path of every promotion.
Fix
Convert all 57 real-timer userEvent.setup() call sites in the two files (47 + 10) to the fake-timer idiom already proven by the in-file control group, via a local per-file helper:
Partial conversion will not work. The failing set is not determined by test content — line 307 fails while the near-identical line 323 passes; line 164 fails while line 141 with the same shape passes. There is no property separating them, so any "just the marginal ones" list is a snapshot of one run's luck. Converting 9 of 47 also leaves the file's wall clock dominated by the other 38, missing the shard-time criterion.
Scope note on testTimeout
testTimeout: 60000stays in this fix. There are ~649 real-timer userEvent.setup() call sites across the client suite; this change converts 57. The remaining ~592 still depend on the raised ceiling. Lowering it now would turn a scoped 2-file hotfix into a repo-wide firefight during a blocked promotion. Lowering belongs to the suite-wide follow-up.
Acceptance criteria
SearchPicker.test.tsx and HouseholdItemPicker.breadcrumb.test.tsx pass reliably in CI (run twice — the failure is probabilistic)
Shard 5 and shard 6 wall-clock times return toward the ~10-minute range
Per-test before/after timings captured, so the mechanism is evidenced rather than assumed
testTimeout value stays exactly 60000 (comment amended, number untouched)
No production code changes
No assertion is weakened — any test that goes green by asserting less is a regression, not a fix
Problem
Quality Gatesfails on thebeta->mainpromotion PR (#2075). 11 tests fail, all of them 60-second timeouts, confined to two files:client/src/components/SearchPicker/SearchPicker.test.tsx— 9 failuresclient/src/components/HouseholdItemPicker/HouseholdItemPicker.breadcrumb.test.tsx— 2 failuresAll 16 E2E shards pass. No assertion failures anywhere — purely wall-clock exhaustion.
Evidence
Shard wall-clock times across consecutive runs:
Shards 1-4 were healthy on the same run where 5 and 6 blew up, so the CI runner as a whole was fine.
Root cause (corrected)
Not a dependency regression. Not a production bug. Not a bad assertion. This is real-timer scheduling latency in the test harness.
Under real timers, every
userEventsub-event awaitswait(config)— a realsetTimeout(..., 0)inside the jsdomvmcontext. A singleuser.click()dispatches the full pointer/focus/click sequence, each step yielding a macrotask, each runningpointerEventsCheck: EachApiCall(an ancestor walk callinggetComputedStyleper level). So:On a contended runner with 6 Jest workers the latency term dominates. That is precisely why the timeline above worsens monotonically (8 -> 26 -> 45 min) with no code change between runs — a dependency-caused regression would have stepped once and stayed flat.
The decisive evidence is a control group inside the failing file itself.
SearchPicker.test.tsxlines 79 and 100 use the fake-timer idiom. Those two tests type into the same input, flip the sameisOpen, and mount the sameFloatingPortalfor the first time — and they are fast and have never failed. The cost therefore is not the portal mount's CPU work; it is the real-timer scheduling around it. Fake timers collapse every yield into a synchronousadvanceTimersByTime, which is why the control group is immune.Why the dependency bump was ruled out
@testing-library/react@16.3.2 @testing-library/user-event@14.6.1 jest@30.4.2 jest-environment-jsdom@30.4.1reproduced identical failures — same test names, same shape — on the old versions. This result is already recorded in thejest.config.tscomment committed by chore(deps-dev): bump the dev-dependencies group across 1 directory with 20 updates #2070.user-eventtarballs (normalizing transpiler churn): the only substantive changes indist/esmare a key-repeat flag inkeyboard/index.jsand a property-descriptor form change indocument/patchFocus.js.utils/misc/wait.jsis byte-identical anddelay: 0is unchanged insetup/setup.js. No hot-path change capable of a 5x regression.A separate earlier investigation on PR #2073 had already excluded
i18next/react-i18nextby controlled swap.Why raising the timeout again is the wrong fix
PR #2070 responded to this by raising the global
testTimeoutto 60000. We are now past that ceiling too. Raising it a third time keeps a 45-minute shard in the critical path of every promotion.Fix
Convert all 57 real-timer
userEvent.setup()call sites in the two files (47 + 10) to the fake-timer idiom already proven by the in-file control group, via a local per-file helper:Partial conversion will not work. The failing set is not determined by test content — line 307 fails while the near-identical line 323 passes; line 164 fails while line 141 with the same shape passes. There is no property separating them, so any "just the marginal ones" list is a snapshot of one run's luck. Converting 9 of 47 also leaves the file's wall clock dominated by the other 38, missing the shard-time criterion.
Scope note on
testTimeouttestTimeout: 60000stays in this fix. There are ~649 real-timeruserEvent.setup()call sites across the client suite; this change converts 57. The remaining ~592 still depend on the raised ceiling. Lowering it now would turn a scoped 2-file hotfix into a repo-wide firefight during a blocked promotion. Lowering belongs to the suite-wide follow-up.Acceptance criteria
SearchPicker.test.tsxandHouseholdItemPicker.breadcrumb.test.tsxpass reliably in CI (run twice — the failure is probabilistic)testTimeoutvalue stays exactly60000(comment amended, number untouched)Blocking
This blocks the v2.15.0 promotion (PR #2075).