From c5ea0a8bfe80d74690391dd1f9357226231a407e Mon Sep 17 00:00:00 2001 From: Factory Agent Date: Wed, 14 Oct 2026 02:48:01 +0700 Subject: [PATCH] Fix flaky GeneralTab and HooksTab tests --- .../settings/tabs/GeneralTab.test.tsx | 20 +++++++++++++ .../src/surfaces/settings/tabs/GeneralTab.tsx | 28 +++++++++++++++---- .../surfaces/settings/tabs/HooksTab.test.tsx | 13 ++++++--- 3 files changed, 52 insertions(+), 9 deletions(-) diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/GeneralTab.test.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/GeneralTab.test.tsx index 4656c91abf1..f35261dd2c1 100644 --- a/apps/desktop-tauri/src/surfaces/settings/tabs/GeneralTab.test.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/tabs/GeneralTab.test.tsx @@ -318,6 +318,26 @@ describe("GeneralTab language picker", () => { }); }); + it("cancels the sound preview timer on unmount", () => { + vi.useFakeTimers(); + try { + const { unmount } = render( + , + ); + fireEvent.click( + screen.getByRole("button", { + name: "NotificationSoundEventHighUsage: NotificationTestSound", + }), + ); + expect(vi.getTimerCount()).toBeGreaterThan(0); + + unmount(); + expect(vi.getTimerCount()).toBe(0); + } finally { + vi.useRealTimers(); + } + }); + it("assigns and clears a custom WAV for one notification", async () => { const set = vi.fn(); vi.mocked(open).mockResolvedValue("C:\\sounds\\high-usage.wav"); diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/GeneralTab.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/GeneralTab.tsx index 6778a7b42bd..76be95ac35e 100644 --- a/apps/desktop-tauri/src/surfaces/settings/tabs/GeneralTab.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/tabs/GeneralTab.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useLocale } from "../../../hooks/useLocale"; import { invoke } from "@tauri-apps/api/core"; import { open } from "@tauri-apps/plugin-dialog"; @@ -168,15 +168,33 @@ export default function GeneralTab({ .catch(() => {}); // version row degrades to absent, never blocks the tab }, []); + const soundPreviewTimeoutRef = useRef(null); + + useEffect( + () => () => { + if (soundPreviewTimeoutRef.current !== null) { + window.clearTimeout(soundPreviewTimeoutRef.current); + } + }, + [], + ); + const handleTestSound = useCallback((event: NotificationSoundEvent) => { setSoundError(null); setPlayingSound(event); - const timeoutId = window.setTimeout( - () => setPlayingSound(null), - NOTIFICATION_SOUND_PREVIEW_DURATION_MS, - ); + if (soundPreviewTimeoutRef.current !== null) { + window.clearTimeout(soundPreviewTimeoutRef.current); + } + const timeoutId = window.setTimeout(() => { + soundPreviewTimeoutRef.current = null; + setPlayingSound(null); + }, NOTIFICATION_SOUND_PREVIEW_DURATION_MS); + soundPreviewTimeoutRef.current = timeoutId; void playNotificationSound(event).catch((error: unknown) => { window.clearTimeout(timeoutId); + if (soundPreviewTimeoutRef.current === timeoutId) { + soundPreviewTimeoutRef.current = null; + } setPlayingSound(null); setSoundError(error instanceof Error ? error.message : String(error)); }); diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/HooksTab.test.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/HooksTab.test.tsx index ee0fdd667d6..fa22961dc6c 100644 --- a/apps/desktop-tauri/src/surfaces/settings/tabs/HooksTab.test.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/tabs/HooksTab.test.tsx @@ -256,11 +256,16 @@ describe("HooksTab", () => { ); await renderTab(snapshot({ rules: full })); expect(screen.getAllByTestId("hook-rule")).toHaveLength(32); - expect(screen.getByRole("button", { name: "+ HooksAddRule" })).toBeDisabled(); - for (const add of screen.getAllByRole("button", { name: "+ HooksAddArgument" })) { - expect(add).toBeDisabled(); + // Role queries over 32 × 32 argument rows are slow enough under a loaded + // CI runner to hit the default 5 s timeout; text queries find the same + // buttons, and the larger timeout covers the heavy render itself. + expect(screen.getByText("+ HooksAddRule").closest("button")).toBeDisabled(); + const addArguments = screen.getAllByText("+ HooksAddArgument"); + expect(addArguments).toHaveLength(32); + for (const add of addArguments) { + expect(add.closest("button")).toBeDisabled(); } - }); + }, 20_000); it("keeps an unreadable hooks.json read-only and shows why", async () => { await renderTab(snapshot({ error: "expected value at line 1 column 1" }));