diff --git a/clients/web/src/components/groups/ServerImportJsonModal/ServerImportJsonModal.test.tsx b/clients/web/src/components/groups/ServerImportJsonModal/ServerImportJsonModal.test.tsx index 36b389b08..bc7396880 100644 --- a/clients/web/src/components/groups/ServerImportJsonModal/ServerImportJsonModal.test.tsx +++ b/clients/web/src/components/groups/ServerImportJsonModal/ServerImportJsonModal.test.tsx @@ -1,12 +1,14 @@ import { describe, it, expect, vi } from "vitest"; import userEvent from "@testing-library/user-event"; import { + act, renderWithMantine, screen, fireEvent, waitFor, } from "../../../test/renderWithMantine"; import { setAceText } from "../../../test/aceEditor"; +import { VALIDATE_DEBOUNCE_MS } from "../../../hooks/useServerJsonImport"; import { ServerImportJsonModal } from "./ServerImportJsonModal"; const npmJson = JSON.stringify({ @@ -221,28 +223,41 @@ describe("ServerImportJsonModal", () => { expect(screen.getByRole("button", { name: "Add Server" })).toBeDisabled(); }); + // The window this exercises is the one between an edit and the debounce that + // re-disables the button, so the whole test runs on fake timers: the pending + // re-validation then cannot land unless this test advances it, and the window + // stops depending on how long the machine takes to get from the paste to the + // click. On real timers a loaded box could spend more than + // VALIDATE_DEBOUNCE_MS there, re-disable the button, and turn the click into a + // no-op that sets no submit error at all (#2250). it("guards against a live edit made before the debounce re-validates", async () => { - const onAddServer = vi.fn(); - renderWithMantine( - , - ); - await pasteJson(npmJson); - await waitFor(() => - expect(screen.getByRole("button", { name: "Add Server" })).toBeEnabled(), - ); - // Replace with invalid content; the button hasn't re-disabled yet (the - // debounce is still pending), so clicking exercises the submit-time guard. - await pasteJson("{not json"); - fireEvent.click(screen.getByRole("button", { name: "Add Server" })); - expect(onAddServer).not.toHaveBeenCalled(); - expect( - await screen.findByText(/Fix the validation errors/), - ).toBeInTheDocument(); + vi.useFakeTimers(); + try { + const onAddServer = vi.fn(); + renderWithMantine( + , + ); + await pasteJson(npmJson); + // Let the first validation land, so the button is enabled to click. + await act(async () => { + await vi.advanceTimersByTimeAsync(VALIDATE_DEBOUNCE_MS); + }); + expect(screen.getByRole("button", { name: "Add Server" })).toBeEnabled(); + // Replace with invalid content and do *not* advance: the debounce stays + // pending, the button stays enabled, and clicking exercises the + // submit-time guard that re-parses the live text. + await pasteJson("{not json"); + fireEvent.click(screen.getByRole("button", { name: "Add Server" })); + expect(onAddServer).not.toHaveBeenCalled(); + expect(screen.getByText(/Fix the validation errors/)).toBeInTheDocument(); + } finally { + vi.useRealTimers(); + } }); it("loads server.json from a chosen file", async () => {