test(ui): register global afterEach(cleanup) — no React tree outlives its test

The desk-caught defect: the full ui run reported "Errors 1" with a fully
green test count (ReferenceError: window is not defined at react-dom
performWorkOnRootViaSchedulerTask, thrown after environment teardown),
which flips the exit code that gates the A/B pin — and per vitest's own
warning can produce FALSE POSITIVE tests.

STRUCTURAL CAUSE (verified at source, not inferred): vitest.config.ts
leaves the "globals" option at its default FALSE, and React Testing Library
only registers its automatic afterEach(cleanup) when the framework afterEach
is a global. So auto-cleanup NEVER RAN: 117 of 153 test files left their
React trees mounted past the test. A mounted tree still has scheduled work;
when that task lands after the environment is disposed it dereferences a
window that no longer exists. The components are innocent —
TerminalPreviewPopover already cancels its rAF in effect cleanup, but effect
cleanup only runs on UNMOUNT, which never happened.

Fix: register cleanup globally in test/setup.ts (unmount, never suppress) —
the R5-family "work must not outlive its context" pattern in the React
harness. RED pin: harness-teardown-hygiene.test.tsx fails deterministically
without the registration (a rendered tree survives into the next test).

HONESTY NOTE: the original error did NOT reproduce in 6 full-suite runs at
this SHA before the fix (it is load/order-sensitive), so this is not an A/B
proof that the flake is gone — it removes the documented precondition of the
reported stack and pins that removal deterministically. 197/197 files, 1723
tests green after the change (no test depended on leaked DOM); tsc 0.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
v-openrig-buildandClaude Fable 5 committed 2026-08-07 18:24:24 +00:00
1 parent 29d28fb684
commit feeda3a808
2 files changed
+64

No files matched your search

@@ -0,0 +1,46 @@
// UI HARNESS TEARDOWN HYGIENE — the R5-family "work must not outlive its
// context" pattern, in the React harness.
//
// THE DEFECT (desk-caught, main c230909cf): the full ui run reported
// "Errors 1" with a fully GREEN test count — ReferenceError: window is not
// defined at react-dom performWorkOnRootViaSchedulerTask, thrown AFTER the
// test environment was torn down. Vitest's own warning: an unhandled
// post-teardown error can cause FALSE POSITIVE tests, and it flips the exit
// code that gates the A/B pin.
//
// THE STRUCTURAL CAUSE (verified at source): vitest.config.ts does not set
// `globals`, so it defaults FALSE — and React Testing Library only registers
// its automatic `afterEach(cleanup)` when the framework's afterEach is
// available as a global. With globals:false that auto-cleanup NEVER RUNS, so
// every test file that does not call cleanup itself (117 of 153 at the time
// of writing) leaves its React tree MOUNTED past the test. A mounted tree can
// still have scheduled work (React schedules through its own task queue); if
// that task lands after the environment is disposed, it dereferences a
// `window` that no longer exists. The components are innocent — e.g.
// TerminalPreviewPopover already cancels its rAF in the effect cleanup — but
// that cleanup only runs ON UNMOUNT, which never happens.
//
// THE FIX: register cleanup globally in test/setup.ts, so every test unmounts
// its tree (cancelling pending work) rather than suppressing the error.
//
// This file pins the PRECONDITION deterministically: with the fix, a test
// that renders leaves nothing behind for the next test. Without it, the leak
// is visible — which is the same leak that lets work outlive the environment.
import { describe, it, expect } from "vitest";
import { render } from "@testing-library/react";
describe("ui harness: every test unmounts its tree (global auto-cleanup registered)", () => {
it("renders a probe tree and does NOT unmount it explicitly", () => {
render(<div data-testid="teardown-hygiene-probe">probe</div>);
expect(document.querySelectorAll("[data-testid='teardown-hygiene-probe']").length).toBe(1);
});
it("the NEXT test starts with a clean DOM — the previous tree was unmounted by the harness", () => {
// RED without a global afterEach(cleanup): the probe from the previous
// test is still mounted here, and (crucially) so is every other suite's
// tree in a full run — the population whose scheduled work outlives the
// environment.
expect(document.querySelectorAll("[data-testid='teardown-hygiene-probe']").length).toBe(0);
expect(document.body.innerHTML).toBe("");
});
});
+18
View File
@@ -1,3 +1,21 @@
// HARNESS TEARDOWN HYGIENE (desk-caught post-teardown ReferenceError, 2026-08-07):
// vitest.config.ts leaves `globals` at its default FALSE, so React Testing
// Library's automatic afterEach(cleanup) never registers — every test file that
// does not clean up itself leaves its React tree MOUNTED. A mounted tree can
// still have scheduled work; when that task lands after the environment is
// disposed it dereferences a `window` that no longer exists ("ReferenceError:
// window is not defined" at performWorkOnRootViaSchedulerTask), which vitest
// warns can produce FALSE POSITIVE tests and which flips the run's exit code
// while every test reports green. Registering cleanup here unmounts each tree —
// cancelling the components' own effect cleanups (rAF, listeners, observers) —
// so no work outlives its context. The cure is unmounting, never suppression.
import { afterEach } from "vitest";
import { cleanup } from "@testing-library/react";
afterEach(() => {
cleanup();
});
// Polyfill ResizeObserver for jsdom (required by React Flow)
globalThis.ResizeObserver = class ResizeObserver {
observe() {}