diff --git a/src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx b/src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx index 88eca1ec0f92..b0a98ef815b4 100644 --- a/src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx @@ -237,6 +237,16 @@ export default function CompressionSettingsTab() { const savedRef = useRef(config); const queuedRef = useRef[]>([]); const saveQueueRef = useRef(Promise.resolve()); + const statusTimerRef = useRef | null>(null); + + // The "saved" badge clears itself after 2s; a timer that outlives the tab fires into an + // unmounted (or torn-down) React tree. + useEffect( + () => () => { + if (statusTimerRef.current) clearTimeout(statusTimerRef.current); + }, + [] + ); useEffect(() => { fetch("/api/settings/compression") @@ -301,7 +311,13 @@ export default function CompressionSettingsTab() { // A failure stays on screen until the next edit, so a queued success or an earlier // save's timeout cannot hide a field that just rolled back. setStatus((shown) => (ok ? (shown === "error" ? shown : "saved") : "error")); - if (ok) setTimeout(() => setStatus((shown) => (shown === "saved" ? "" : shown)), 2000); + if (ok) { + if (statusTimerRef.current) clearTimeout(statusTimerRef.current); + statusTimerRef.current = setTimeout( + () => setStatus((shown) => (shown === "saved" ? "" : shown)), + 2000 + ); + } }); }; diff --git a/tests/unit/ui/compression-settings-tab-partial-save.test.tsx b/tests/unit/ui/compression-settings-tab-partial-save.test.tsx index 724fdf56eb1b..6386b34c5b14 100644 --- a/tests/unit/ui/compression-settings-tab-partial-save.test.tsx +++ b/tests/unit/ui/compression-settings-tab-partial-save.test.tsx @@ -138,11 +138,37 @@ async function renderTab() { } afterEach(() => { + vi.restoreAllMocks(); vi.useRealTimers(); vi.unstubAllGlobals(); }); describe("CompressionSettingsTab saves only what changed", () => { + it("clears the saved-badge timer when the tab unmounts", async () => { + startServer(); + const nativeSetTimeout = globalThis.setTimeout; + const badgeTimers: unknown[] = []; + vi.spyOn(globalThis, "setTimeout").mockImplementation((( + fn: TimerHandler, + ms?: number, + ...args: unknown[] + ) => { + const id = nativeSetTimeout(fn as () => void, ms, ...(args as [])); + if (ms === 2000) badgeTimers.push(id); + return id; + }) as typeof setTimeout); + const clearSpy = vi.spyOn(globalThis, "clearTimeout"); + + const view = render(); + await settle(); + fireEvent.change(inputFor("compressionCacheTTL"), { target: { value: "10" } }); + await settle(); + expect(badgeTimers).toHaveLength(1); + + view.unmount(); + expect(clearSpy).toHaveBeenCalledWith(badgeTimers[0]); + }); + // Rendering the whole caveman page takes over 5 seconds on a cold run. it( "keeps Auto-Clarity off on the caveman page when the embedded tab saves",