Skip to content

fix(site): flush deferred Radix focus-scope timer before jsdom teardown#26983

Draft
EhabY wants to merge 2 commits into
mainfrom
fix/site-radix-focus-scope-teardown-flake
Draft

fix(site): flush deferred Radix focus-scope timer before jsdom teardown#26983
EhabY wants to merge 2 commits into
mainfrom
fix/site-radix-focus-scope-teardown-flake

Conversation

@EhabY

@EhabY EhabY commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

UserDropdownContent.test.tsx intermittently fails CI with an unhandled error:

TypeError: Failed to execute 'dispatchEvent' on 'EventTarget': parameter 1 is not of type 'Event'.
 ❯ Timeout._onTimeout @radix-ui/react-focus-scope/dist/index.mjs:92:21

Radix's FocusScope defers its unmount dispatch with setTimeout(0). The shared afterEach cleanup() schedules that timer, and for the last test in a file it can still be pending when vitest tears down the per-file jsdom environment. It then fires with Node's built-in CustomEvent instead of jsdom's, and the dispatch throws. Any test file whose last test unmounts a focus-trapped Radix component (Dialog, Popover, DropdownMenu) can hit this.

Await one timer turn in the shared afterAll, before environment teardown. Same-delay timers run in FIFO order, so this deterministically drains the pending timer rather than shrinking the race window. Costs ~1ms per test file; a vi.isFakeTimers() guard keeps the flush from hanging if a file leaves fake timers installed.

Verified with A/B stress runs alternating main's and this branch's msw.ts, 20 jsdom teardowns per invocation:

Conditions main this fix
idle machine, 60 invocations 3/60 failed 0/60
pinned to 2 cores, 12 invocations 12/12 failed 0/12

All main failures match the CI error signature exactly; the full unit suite passes.

Closes coder/internal#1613


This PR was generated by Coder Agents on behalf of @EhabY.

Radix's FocusScope defers its unmount event dispatch and focus restore
with setTimeout(0). The cleanup() in the shared afterEach schedules that
timer, and for the last test in a file it could still be pending when
vitest tears down the jsdom environment. The callback then constructs a
CustomEvent from Node's built-in constructor instead of jsdom's, and
jsdom rejects the dispatch with "parameter 1 is not of type 'Event'" as
an unhandled error, failing the run.

Await one timer turn in afterAll, while the jsdom environment is still
alive, to deterministically drain the 0ms timers scheduled by cleanup().
Timers with the same delay run in FIFO order, so this is not a race.

Fixes coder/internal#1613
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

flake: UserDropdownContent.test.tsx

1 participant