diff --git a/src/react/components/ui/adapter/drawer.conformance.test.tsx b/src/react/components/ui/adapter/drawer.conformance.test.tsx index e0c6e64d7c..24cb0d269b 100644 --- a/src/react/components/ui/adapter/drawer.conformance.test.tsx +++ b/src/react/components/ui/adapter/drawer.conformance.test.tsx @@ -47,9 +47,25 @@ function installDom(dom: JSDOM): () => void { g.window = w; g.getComputedStyle = (w.getComputedStyle as (e: Element) => CSSStyleDeclaration).bind(w); g.ResizeObserver = ResizeObserverStub; - g.requestAnimationFrame = (cb: (t: number) => void) => setTimeout(() => cb(0), 0); - g.cancelAnimationFrame = (id: number) => clearTimeout(id); + // The stub is a real timer, so a frame still queued when the test ends is a + // pending timer and trips the leak sanitizer. React can schedule a frame right + // up to unmount, so track outstanding frames and drain them on teardown. + const pendingFrames = new Set>(); + g.requestAnimationFrame = (cb: (t: number) => void) => { + const id = setTimeout(() => { + pendingFrames.delete(id); + cb(0); + }, 0); + pendingFrames.add(id); + return id as unknown as number; + }; + g.cancelAnimationFrame = (id: number) => { + pendingFrames.delete(id as unknown as ReturnType); + clearTimeout(id); + }; return () => { + for (const frame of pendingFrames) clearTimeout(frame); + pendingFrames.clear(); for (const k of keys) g[k] = prev[k]; dom.window.close(); }; diff --git a/src/react/components/ui/alert-dialog.test.tsx b/src/react/components/ui/alert-dialog.test.tsx index 2f7284064a..6ea7234f96 100644 --- a/src/react/components/ui/alert-dialog.test.tsx +++ b/src/react/components/ui/alert-dialog.test.tsx @@ -86,10 +86,26 @@ function installDom(dom: JSDOM): () => void { onchange: null, dispatchEvent: () => false, }); - g.requestAnimationFrame = (cb: (t: number) => void) => setTimeout(() => cb(0), 0); - g.cancelAnimationFrame = (id: number) => clearTimeout(id); + // The stub is a real timer, so a frame still queued when the test ends is a + // pending timer and trips the leak sanitizer. React can schedule a frame right + // up to unmount, so track outstanding frames and drain them on teardown. + const pendingFrames = new Set>(); + g.requestAnimationFrame = (cb: (t: number) => void) => { + const id = setTimeout(() => { + pendingFrames.delete(id); + cb(0); + }, 0); + pendingFrames.add(id); + return id as unknown as number; + }; + g.cancelAnimationFrame = (id: number) => { + pendingFrames.delete(id as unknown as ReturnType); + clearTimeout(id); + }; return () => { + for (const frame of pendingFrames) clearTimeout(frame); + pendingFrames.clear(); for (const k of keys) g[k] = prev[k]; dom.window.close(); }; diff --git a/src/react/components/ui/breadcrumb.test.tsx b/src/react/components/ui/breadcrumb.test.tsx index f3c4c73b4a..142dff4fd0 100644 --- a/src/react/components/ui/breadcrumb.test.tsx +++ b/src/react/components/ui/breadcrumb.test.tsx @@ -72,10 +72,26 @@ function installDom(dom: JSDOM): () => void { onchange: null, dispatchEvent: () => false, }); - g.requestAnimationFrame = (cb: (t: number) => void) => setTimeout(() => cb(0), 0); - g.cancelAnimationFrame = (id: number) => clearTimeout(id); + // The stub is a real timer, so a frame still queued when the test ends is a + // pending timer and trips the leak sanitizer. React can schedule a frame right + // up to unmount, so track outstanding frames and drain them on teardown. + const pendingFrames = new Set>(); + g.requestAnimationFrame = (cb: (t: number) => void) => { + const id = setTimeout(() => { + pendingFrames.delete(id); + cb(0); + }, 0); + pendingFrames.add(id); + return id as unknown as number; + }; + g.cancelAnimationFrame = (id: number) => { + pendingFrames.delete(id as unknown as ReturnType); + clearTimeout(id); + }; return () => { + for (const frame of pendingFrames) clearTimeout(frame); + pendingFrames.clear(); for (const k of keys) g[k] = prev[k]; dom.window.close(); }; diff --git a/src/react/components/ui/calendar.test.tsx b/src/react/components/ui/calendar.test.tsx index 8cd070aa4f..b102cdc358 100644 --- a/src/react/components/ui/calendar.test.tsx +++ b/src/react/components/ui/calendar.test.tsx @@ -69,10 +69,26 @@ function installDom(dom: JSDOM): () => void { onchange: null, dispatchEvent: () => false, }); - g.requestAnimationFrame = (cb: (t: number) => void) => setTimeout(() => cb(0), 0); - g.cancelAnimationFrame = (id: number) => clearTimeout(id); + // The stub is a real timer, so a frame still queued when the test ends is a + // pending timer and trips the leak sanitizer. React can schedule a frame right + // up to unmount, so track outstanding frames and drain them on teardown. + const pendingFrames = new Set>(); + g.requestAnimationFrame = (cb: (t: number) => void) => { + const id = setTimeout(() => { + pendingFrames.delete(id); + cb(0); + }, 0); + pendingFrames.add(id); + return id as unknown as number; + }; + g.cancelAnimationFrame = (id: number) => { + pendingFrames.delete(id as unknown as ReturnType); + clearTimeout(id); + }; return () => { + for (const frame of pendingFrames) clearTimeout(frame); + pendingFrames.clear(); for (const k of keys) g[k] = prev[k]; dom.window.close(); }; diff --git a/src/react/components/ui/conformance.test.tsx b/src/react/components/ui/conformance.test.tsx index 7162ba3821..d0d4c1b3a1 100644 --- a/src/react/components/ui/conformance.test.tsx +++ b/src/react/components/ui/conformance.test.tsx @@ -110,10 +110,26 @@ function installDom(dom: JSDOM): () => void { onchange: null, dispatchEvent: () => false, }); - g.requestAnimationFrame = (cb: (t: number) => void) => setTimeout(() => cb(0), 0); - g.cancelAnimationFrame = (id: number) => clearTimeout(id); + // The stub is a real timer, so a frame still queued when the test ends is a + // pending timer and trips the leak sanitizer. React can schedule a frame right + // up to unmount, so track outstanding frames and drain them on teardown. + const pendingFrames = new Set>(); + g.requestAnimationFrame = (cb: (t: number) => void) => { + const id = setTimeout(() => { + pendingFrames.delete(id); + cb(0); + }, 0); + pendingFrames.add(id); + return id as unknown as number; + }; + g.cancelAnimationFrame = (id: number) => { + pendingFrames.delete(id as unknown as ReturnType); + clearTimeout(id); + }; return () => { + for (const frame of pendingFrames) clearTimeout(frame); + pendingFrames.clear(); for (const k of keys) g[k] = prev[k]; dom.window.close(); }; diff --git a/src/react/components/ui/date-picker.test.tsx b/src/react/components/ui/date-picker.test.tsx index d5fee5fce3..9f5220a8c3 100644 --- a/src/react/components/ui/date-picker.test.tsx +++ b/src/react/components/ui/date-picker.test.tsx @@ -69,10 +69,26 @@ function installDom(dom: JSDOM): () => void { onchange: null, dispatchEvent: () => false, }); - g.requestAnimationFrame = (cb: (t: number) => void) => setTimeout(() => cb(0), 0); - g.cancelAnimationFrame = (id: number) => clearTimeout(id); + // The stub is a real timer, so a frame still queued when the test ends is a + // pending timer and trips the leak sanitizer. React can schedule a frame right + // up to unmount, so track outstanding frames and drain them on teardown. + const pendingFrames = new Set>(); + g.requestAnimationFrame = (cb: (t: number) => void) => { + const id = setTimeout(() => { + pendingFrames.delete(id); + cb(0); + }, 0); + pendingFrames.add(id); + return id as unknown as number; + }; + g.cancelAnimationFrame = (id: number) => { + pendingFrames.delete(id as unknown as ReturnType); + clearTimeout(id); + }; return () => { + for (const frame of pendingFrames) clearTimeout(frame); + pendingFrames.clear(); for (const k of keys) g[k] = prev[k]; dom.window.close(); }; diff --git a/src/react/components/ui/field.test.tsx b/src/react/components/ui/field.test.tsx index 9b0bc7e412..cf7cb19222 100644 --- a/src/react/components/ui/field.test.tsx +++ b/src/react/components/ui/field.test.tsx @@ -63,10 +63,26 @@ function installDom(dom: JSDOM): () => void { onchange: null, dispatchEvent: () => false, }); - g.requestAnimationFrame = (cb: (t: number) => void) => setTimeout(() => cb(0), 0); - g.cancelAnimationFrame = (id: number) => clearTimeout(id); + // The stub is a real timer, so a frame still queued when the test ends is a + // pending timer and trips the leak sanitizer. React can schedule a frame right + // up to unmount, so track outstanding frames and drain them on teardown. + const pendingFrames = new Set>(); + g.requestAnimationFrame = (cb: (t: number) => void) => { + const id = setTimeout(() => { + pendingFrames.delete(id); + cb(0); + }, 0); + pendingFrames.add(id); + return id as unknown as number; + }; + g.cancelAnimationFrame = (id: number) => { + pendingFrames.delete(id as unknown as ReturnType); + clearTimeout(id); + }; return () => { + for (const frame of pendingFrames) clearTimeout(frame); + pendingFrames.clear(); for (const k of keys) g[k] = prev[k]; dom.window.close(); }; diff --git a/src/react/components/ui/input-otp.test.tsx b/src/react/components/ui/input-otp.test.tsx index 00e8c1a4c4..1dc0078fed 100644 --- a/src/react/components/ui/input-otp.test.tsx +++ b/src/react/components/ui/input-otp.test.tsx @@ -67,10 +67,26 @@ function installDom(dom: JSDOM): () => void { onchange: null, dispatchEvent: () => false, }); - g.requestAnimationFrame = (cb: (t: number) => void) => setTimeout(() => cb(0), 0); - g.cancelAnimationFrame = (id: number) => clearTimeout(id); + // The stub is a real timer, so a frame still queued when the test ends is a + // pending timer and trips the leak sanitizer. React can schedule a frame right + // up to unmount, so track outstanding frames and drain them on teardown. + const pendingFrames = new Set>(); + g.requestAnimationFrame = (cb: (t: number) => void) => { + const id = setTimeout(() => { + pendingFrames.delete(id); + cb(0); + }, 0); + pendingFrames.add(id); + return id as unknown as number; + }; + g.cancelAnimationFrame = (id: number) => { + pendingFrames.delete(id as unknown as ReturnType); + clearTimeout(id); + }; return () => { + for (const frame of pendingFrames) clearTimeout(frame); + pendingFrames.clear(); for (const k of keys) g[k] = prev[k]; dom.window.close(); }; diff --git a/src/react/components/ui/meter.test.tsx b/src/react/components/ui/meter.test.tsx index a9828a011e..511d98fb7c 100644 --- a/src/react/components/ui/meter.test.tsx +++ b/src/react/components/ui/meter.test.tsx @@ -63,10 +63,26 @@ function installDom(dom: JSDOM): () => void { onchange: null, dispatchEvent: () => false, }); - g.requestAnimationFrame = (cb: (t: number) => void) => setTimeout(() => cb(0), 0); - g.cancelAnimationFrame = (id: number) => clearTimeout(id); + // The stub is a real timer, so a frame still queued when the test ends is a + // pending timer and trips the leak sanitizer. React can schedule a frame right + // up to unmount, so track outstanding frames and drain them on teardown. + const pendingFrames = new Set>(); + g.requestAnimationFrame = (cb: (t: number) => void) => { + const id = setTimeout(() => { + pendingFrames.delete(id); + cb(0); + }, 0); + pendingFrames.add(id); + return id as unknown as number; + }; + g.cancelAnimationFrame = (id: number) => { + pendingFrames.delete(id as unknown as ReturnType); + clearTimeout(id); + }; return () => { + for (const frame of pendingFrames) clearTimeout(frame); + pendingFrames.clear(); for (const k of keys) g[k] = prev[k]; dom.window.close(); }; diff --git a/src/react/components/ui/pagination.test.tsx b/src/react/components/ui/pagination.test.tsx index 4cacfb496f..acf6180e1f 100644 --- a/src/react/components/ui/pagination.test.tsx +++ b/src/react/components/ui/pagination.test.tsx @@ -70,10 +70,26 @@ function installDom(dom: JSDOM): () => void { onchange: null, dispatchEvent: () => false, }); - g.requestAnimationFrame = (cb: (t: number) => void) => setTimeout(() => cb(0), 0); - g.cancelAnimationFrame = (id: number) => clearTimeout(id); + // The stub is a real timer, so a frame still queued when the test ends is a + // pending timer and trips the leak sanitizer. React can schedule a frame right + // up to unmount, so track outstanding frames and drain them on teardown. + const pendingFrames = new Set>(); + g.requestAnimationFrame = (cb: (t: number) => void) => { + const id = setTimeout(() => { + pendingFrames.delete(id); + cb(0); + }, 0); + pendingFrames.add(id); + return id as unknown as number; + }; + g.cancelAnimationFrame = (id: number) => { + pendingFrames.delete(id as unknown as ReturnType); + clearTimeout(id); + }; return () => { + for (const frame of pendingFrames) clearTimeout(frame); + pendingFrames.clear(); for (const k of keys) g[k] = prev[k]; dom.window.close(); }; diff --git a/src/react/components/ui/scroll-area.test.tsx b/src/react/components/ui/scroll-area.test.tsx index e84c8ecf13..c91d363279 100644 --- a/src/react/components/ui/scroll-area.test.tsx +++ b/src/react/components/ui/scroll-area.test.tsx @@ -64,10 +64,26 @@ function installDom(dom: JSDOM): () => void { onchange: null, dispatchEvent: () => false, }); - g.requestAnimationFrame = (cb: (t: number) => void) => setTimeout(() => cb(0), 0); - g.cancelAnimationFrame = (id: number) => clearTimeout(id); + // The stub is a real timer, so a frame still queued when the test ends is a + // pending timer and trips the leak sanitizer. React can schedule a frame right + // up to unmount, so track outstanding frames and drain them on teardown. + const pendingFrames = new Set>(); + g.requestAnimationFrame = (cb: (t: number) => void) => { + const id = setTimeout(() => { + pendingFrames.delete(id); + cb(0); + }, 0); + pendingFrames.add(id); + return id as unknown as number; + }; + g.cancelAnimationFrame = (id: number) => { + pendingFrames.delete(id as unknown as ReturnType); + clearTimeout(id); + }; return () => { + for (const frame of pendingFrames) clearTimeout(frame); + pendingFrames.clear(); for (const k of keys) g[k] = prev[k]; dom.window.close(); };