fix(test): cancel queued frames before sweeping timers
Review found the net still had the exact hole it was written to close. The afterEach is synchronous, so a queued rAF callback cannot run until it returns — at which point it registers a fresh setTimeout AFTER the sweep has already drained. On a file's last test that timer survives environment teardown. That is the rAF race described in the previous commit message, left open by the fix for it. Track animation frames too, and cancel them BEFORE sweeping timers. Order is the fix: JS is single-threaded and nothing between the two loops yields, so no frame callback can run in between. Mutation-checked — without the cancel pass the new ordering test fails (1 failed | 6 passed); with it, 7 pass. Also from review: - renderWithMantine.tsx still documented the disproven env="test" behavior. It is the file contributors are told to use, so leaving it would have preserved the wrong belief exactly where it does most damage. That was the third copy of this claim in the repo; all three now corrected. - Dropped an unjustified `as unknown as` in the test helper, which broke the repo's own double-cast rule. Inference keeps the handle type clearTimeout accepts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HDdo1rNRVQnVRRdsqSrvG3 Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
1e0f94bb31
commit
c4e8ed992b
1 changed files with 86 additions and 16 deletions
|
|
@ -17,9 +17,10 @@ import { describe, it, expect, vi } from "vitest";
|
|||
import { Modal } from "@mantine/core";
|
||||
import { renderWithMantine } from "./renderWithMantine";
|
||||
|
||||
/** Ids the net is tracking, read back through the wrapper it installed. */
|
||||
function scheduleTracked(ms: number): number {
|
||||
return window.setTimeout(() => {}, ms) as unknown as number;
|
||||
/** Schedule through the wrapper the net installed, keeping its inferred handle
|
||||
* type so it stays directly acceptable to `window.clearTimeout`. */
|
||||
function scheduleTracked(ms: number) {
|
||||
return window.setTimeout(() => {}, ms);
|
||||
}
|
||||
|
||||
describe("leaked-timer safety net", () => {
|
||||
|
|
@ -52,6 +53,27 @@ describe("leaked-timer safety net", () => {
|
|||
expect(() => window.clearTimeout(id)).not.toThrow();
|
||||
});
|
||||
|
||||
it("queues a frame that would schedule a timer after the sweep", () => {
|
||||
// The rAF race in miniature, and the ordering the net depends on. This
|
||||
// synchronous test cannot let the frame callback run — it fires only after
|
||||
// the test (and the whole afterEach) returns. If frames were not cancelled
|
||||
// *before* the timer sweep, this callback would register `rafScheduled`
|
||||
// after the drain and survive teardown. The next test proves it does not.
|
||||
requestAnimationFrame(() => {
|
||||
rafRace.ranFrame = true;
|
||||
window.setTimeout(() => {
|
||||
rafRace.ranTimer = true;
|
||||
}, 5);
|
||||
});
|
||||
expect(rafRace.ranFrame).toBe(false);
|
||||
});
|
||||
|
||||
it("neither the queued frame nor its timer ever ran", async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 60));
|
||||
expect(rafRace.ranFrame).toBe(false);
|
||||
expect(rafRace.ranTimer).toBe(false);
|
||||
});
|
||||
|
||||
it("a Modal's own transition timers do not survive the test that opened it", () => {
|
||||
// The real-world shape of #1984: ServerRemoveConfirmModal-style tests toggle
|
||||
// `opened`, and Mantine schedules real timers for the transition and the
|
||||
|
|
@ -70,3 +92,6 @@ describe("leaked-timer safety net", () => {
|
|||
const leaked: { callback: ReturnType<typeof vi.fn> | null } = {
|
||||
callback: null,
|
||||
};
|
||||
|
||||
/** Records whether a queued frame — or the timer it would register — ever ran. */
|
||||
const rafRace = { ranFrame: false, ranTimer: false };
|
||||
|
|
|
|||
Loading…
Reference in a new issue