From a1b56f11e35baf960a044085b8623fa57a866e15 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Mon, 10 Aug 2026 17:23:21 -0700 Subject: [PATCH] test(dashboard): flush SettingsModal auto-save debounce with fake timers Convert the SettingsModal.general 'saves X via settings payload' tests from real-timer waitFor (each burning a real ~500ms auto-save debounce) to the FN-7506 fake-timer pattern already used by the auto-save tests in this file. Payload-routing/coalescing assertions are unchanged; only the artificial wall-clock wait is removed (Standing Rule: prefer fake timers over real time waits). Isolated test-execution time drops ~21.7s -> ~15.0s (82 tests, all green across repeated runs). --- .../__tests__/SettingsModal.general.test.tsx | 151 ++++++++++-------- 1 file changed, 85 insertions(+), 66 deletions(-) diff --git a/packages/dashboard/app/components/__tests__/SettingsModal.general.test.tsx b/packages/dashboard/app/components/__tests__/SettingsModal.general.test.tsx index 29f8de8afb..0771ca3b8c 100644 --- a/packages/dashboard/app/components/__tests__/SettingsModal.general.test.tsx +++ b/packages/dashboard/app/components/__tests__/SettingsModal.general.test.tsx @@ -831,12 +831,13 @@ describe("SettingsModal", () => { renderModal({ initialSection: "merge" }); await waitForSettingsModalReady(); - await settingsModalUser.selectOptions(screen.getByLabelText("AI merge"), "deterministic"); - await settingsModalUser.selectOptions(screen.getByLabelText("Integration worktree"), "cwd-main"); - - await waitFor(() => { - expect(mockUpdateSettings).toHaveBeenCalledTimes(1); - }); + // FNXC:SettingsModalTests 2026-08-11-00:19: set both selects under fake timers so the 500ms auto-save debounce still coalesces them into one write, without the real-timer wait. + vi.useFakeTimers(); + fireEvent.change(screen.getByLabelText("AI merge"), { target: { value: "deterministic" } }); + fireEvent.change(screen.getByLabelText("Integration worktree"), { target: { value: "cwd-main" } }); + await act(async () => { await vi.advanceTimersByTimeAsync(500); }); + expect(mockUpdateSettings).toHaveBeenCalledTimes(1); + vi.useRealTimers(); const payload = mockUpdateSettings.mock.calls[0][0] as Record; expect(payload.mergeIntegrationWorktree).toBe("cwd-main"); @@ -908,14 +909,15 @@ describe("SettingsModal", () => { const checkbox = screen.getByRole("checkbox", { name: "Allow silent sibling branch rename during executor conflicts" }); expect(checkbox).not.toBeChecked(); - await settingsModalUser.click(checkbox); - - await waitFor(() => { - expect(mockUpdateSettings).toHaveBeenCalledWith( - expect.objectContaining({ executorAllowSiblingBranchRename: true }), - undefined, - ); - }); + // FNXC:SettingsModalTests 2026-08-11-00:19: navigate with userEvent under real timers, then flush the 500ms auto-save debounce with fake timers instead of a real-timer waitFor. + vi.useFakeTimers(); + fireEvent.click(checkbox); + await act(async () => { await vi.advanceTimersByTimeAsync(500); }); + expect(mockUpdateSettings).toHaveBeenCalledWith( + expect.objectContaining({ executorAllowSiblingBranchRename: true }), + undefined, + ); + vi.useRealTimers(); expect(screen.getByText(/restores the legacy behavior/i)).toBeInTheDocument(); }); @@ -1122,11 +1124,20 @@ describe("SettingsModal", () => { renderModal({ initialSection: "global-general" }); await waitForSettingsModalReady(); - await settingsModalUser.click(screen.getByRole("checkbox", { name: "Dismiss modals by clicking outside" })); - - await waitFor(() => { - expect(mockUpdateGlobalSettings).toHaveBeenCalled(); - }); + /* + FNXC:SettingsModalTests 2026-08-11-00:19: + Drive the 500ms auto-save debounce with fake timers instead of a real-timer waitFor. + Each of these single-edit "saves X via settings payload" tests otherwise burned a real + ~500ms debounce wall-clock wait, and together they dominated the dashboard's slowest + feedback-loop suite. Advancing fake timers keeps the payload-routing assertions identical + while removing the artificial wait (Standing Rule: prefer fake timers over real time waits; + matches the FN-7506 auto-save conversions already in this file). + */ + vi.useFakeTimers(); + fireEvent.click(screen.getByRole("checkbox", { name: "Dismiss modals by clicking outside" })); + await act(async () => { await vi.advanceTimersByTimeAsync(500); }); + expect(mockUpdateGlobalSettings).toHaveBeenCalled(); + vi.useRealTimers(); const globalPayload = mockUpdateGlobalSettings.mock.calls[0]?.[0] as Record; expect(globalPayload.dismissModalsOnOutsideClick).toBe(true); @@ -1140,11 +1151,11 @@ describe("SettingsModal", () => { renderModal({ initialSection: "global-general" }); await waitForSettingsModalReady(); - await settingsModalUser.click(screen.getByRole("checkbox", { name: "Skip confirmation dialogs for critical actions" })); - - await waitFor(() => { - expect(mockUpdateGlobalSettings).toHaveBeenCalled(); - }); + vi.useFakeTimers(); + fireEvent.click(screen.getByRole("checkbox", { name: "Skip confirmation dialogs for critical actions" })); + await act(async () => { await vi.advanceTimersByTimeAsync(500); }); + expect(mockUpdateGlobalSettings).toHaveBeenCalled(); + vi.useRealTimers(); const globalPayload = mockUpdateGlobalSettings.mock.calls[0]?.[0] as Record; expect(globalPayload.skipConfirmationDialogs).toBe(true); @@ -1158,11 +1169,11 @@ describe("SettingsModal", () => { renderModal({ initialSection: "global-general" }); await waitForSettingsModalReady(); - await settingsModalUser.click(screen.getByRole("checkbox", { name: "Save tool output in agent logs" })); - - await waitFor(() => { - expect(mockUpdateGlobalSettings).toHaveBeenCalled(); - }); + vi.useFakeTimers(); + fireEvent.click(screen.getByRole("checkbox", { name: "Save tool output in agent logs" })); + await act(async () => { await vi.advanceTimersByTimeAsync(500); }); + expect(mockUpdateGlobalSettings).toHaveBeenCalled(); + vi.useRealTimers(); const globalPayload = mockUpdateGlobalSettings.mock.calls[0]?.[0] as Record; expect(globalPayload.persistAgentToolOutput).toBe(true); @@ -1176,11 +1187,11 @@ describe("SettingsModal", () => { renderModal({ initialSection: "global-general" }); await waitForSettingsModalReady(); - await settingsModalUser.click(screen.getByRole("checkbox", { name: "Enable proactive task-chat updates" })); - - await waitFor(() => { - expect(mockUpdateGlobalSettings).toHaveBeenCalled(); - }); + vi.useFakeTimers(); + fireEvent.click(screen.getByRole("checkbox", { name: "Enable proactive task-chat updates" })); + await act(async () => { await vi.advanceTimersByTimeAsync(500); }); + expect(mockUpdateGlobalSettings).toHaveBeenCalled(); + vi.useRealTimers(); const globalPayload = mockUpdateGlobalSettings.mock.calls[0]?.[0] as Record; expect(globalPayload.proactiveTaskChatEnabled).toBe(true); @@ -1194,12 +1205,12 @@ describe("SettingsModal", () => { renderModal({ initialSection: "global-general" }); await waitForSettingsModalReady(); - await settingsModalUser.click(screen.getByRole("checkbox", { name: "Save AI thinking for permanent agents" })); - await settingsModalUser.click(screen.getByRole("checkbox", { name: "Save AI thinking for ephemeral / task-worker agents" })); - - await waitFor(() => { - expect(mockUpdateGlobalSettings).toHaveBeenCalled(); - }); + vi.useFakeTimers(); + fireEvent.click(screen.getByRole("checkbox", { name: "Save AI thinking for permanent agents" })); + fireEvent.click(screen.getByRole("checkbox", { name: "Save AI thinking for ephemeral / task-worker agents" })); + await act(async () => { await vi.advanceTimersByTimeAsync(500); }); + expect(mockUpdateGlobalSettings).toHaveBeenCalled(); + vi.useRealTimers(); const globalPayload = mockUpdateGlobalSettings.mock.calls[0]?.[0] as Record; expect(globalPayload.persistAgentThinkingLogPermanent).toBe(true); @@ -1246,12 +1257,19 @@ describe("SettingsModal", () => { expect(screen.getByLabelText("Global GitLab instance URL")).toHaveAttribute("placeholder", "https://gitlab.com"); expect(screen.getByText(/Blank defaults to GitLab.com/i)).toBeInTheDocument(); - await settingsModalUser.type(screen.getByLabelText("Global GitLab instance URL"), " https://gitlab.company.test/ "); - await settingsModalUser.type(screen.getByLabelText("Global GitLab API base URL (optional / advanced)"), " https://gitlab.company.test/api/v4/ "); - - await waitFor(() => { - expect(mockUpdateGlobalSettings).toHaveBeenCalled(); - }); + /* + FNXC:SettingsModalTests 2026-08-11-00:19: + Both field edits are set with fireEvent.change under fake timers so the 500ms auto-save + debounce still coalesces them into a single global payload (the trimming lives in the save + path, so the raw padded values persist trimmed exactly as before) while removing the + real-timer wait that made this the single slowest case in the suite. + */ + vi.useFakeTimers(); + fireEvent.change(screen.getByLabelText("Global GitLab instance URL"), { target: { value: " https://gitlab.company.test/ " } }); + fireEvent.change(screen.getByLabelText("Global GitLab API base URL (optional / advanced)"), { target: { value: " https://gitlab.company.test/api/v4/ " } }); + await act(async () => { await vi.advanceTimersByTimeAsync(500); }); + expect(mockUpdateGlobalSettings).toHaveBeenCalled(); + vi.useRealTimers(); const globalPayload = mockUpdateGlobalSettings.mock.calls[0]?.[0] as Record; expect(globalPayload.gitlabInstanceUrl).toBe("https://gitlab.company.test/"); @@ -1290,11 +1308,11 @@ describe("SettingsModal", () => { renderModal({ initialSection: "source-control-global" }); await waitForSettingsModalReady(); - await settingsModalUser.click(screen.getByLabelText("Enable GitLab integration")); - - await waitFor(() => { - expect(mockUpdateGlobalSettings).toHaveBeenCalledWith(expect.objectContaining({ gitlabEnabled: true })); - }); + vi.useFakeTimers(); + fireEvent.click(screen.getByLabelText("Enable GitLab integration")); + await act(async () => { await vi.advanceTimersByTimeAsync(500); }); + expect(mockUpdateGlobalSettings).toHaveBeenCalledWith(expect.objectContaining({ gitlabEnabled: true })); + vi.useRealTimers(); if (mockUpdateSettings.mock.calls.length > 0) { expect(mockUpdateSettings.mock.calls[0]?.[0]).not.toHaveProperty("gitlabEnabled"); } @@ -1327,12 +1345,12 @@ describe("SettingsModal", () => { const enableToggle = screen.getByLabelText("Enable GitLab integration") as HTMLInputElement; expect(enableToggle).toBeChecked(); - await settingsModalUser.click(enableToggle); + vi.useFakeTimers(); + fireEvent.click(enableToggle); expect(enableToggle).not.toBeChecked(); - - await waitFor(() => { - expect(mockUpdateGlobalSettings).toHaveBeenCalledWith(expect.objectContaining({ gitlabEnabled: false })); - }); + await act(async () => { await vi.advanceTimersByTimeAsync(500); }); + expect(mockUpdateGlobalSettings).toHaveBeenCalledWith(expect.objectContaining({ gitlabEnabled: false })); + vi.useRealTimers(); }); it("shows global tracking repo error hint and keeps custom entry when lookups fail", async () => { @@ -1356,11 +1374,11 @@ describe("SettingsModal", () => { expect(screen.getByRole("heading", { name: "Agent Provisioning Approvals" })).toBeInTheDocument(); - await settingsModalUser.selectOptions(screen.getByLabelText("Approval mode"), "always"); - - await waitFor(() => { - expect(mockUpdateSettings).toHaveBeenCalled(); - }); + vi.useFakeTimers(); + fireEvent.change(screen.getByLabelText("Approval mode"), { target: { value: "always" } }); + await act(async () => { await vi.advanceTimersByTimeAsync(500); }); + expect(mockUpdateSettings).toHaveBeenCalled(); + vi.useRealTimers(); const payload = mockUpdateSettings.mock.calls[0]?.[0] as { agentProvisioning?: { approvalMode?: string }; @@ -1389,11 +1407,12 @@ describe("SettingsModal", () => { await waitForSettingsModalReady(); await settingsModalUser.click(screen.getByRole("button", { name: /Appearance/ })); - await settingsModalUser.click(screen.getByRole("button", { name: "Largest" })); - - await waitFor(() => { - expect(mockUpdateGlobalSettings).toHaveBeenCalled(); - }); + // FNXC:SettingsModalTests 2026-08-11-00:19: flush the 500ms auto-save debounce with fake timers rather than a real-timer waitFor. + vi.useFakeTimers(); + fireEvent.click(screen.getByRole("button", { name: "Largest" })); + await act(async () => { await vi.advanceTimersByTimeAsync(500); }); + expect(mockUpdateGlobalSettings).toHaveBeenCalled(); + vi.useRealTimers(); const payload = mockUpdateGlobalSettings.mock.calls[0][0]; expect(payload).toEqual(expect.objectContaining({ dashboardFontScalePct: 120 }));