fix(web): remove the wall-clock race in the import-JSON debounce guard test
`ServerImportJsonModal` > "guards against a live edit made before the debounce re-validates" opened its window on the real clock: it pasted invalid JSON and clicked Add Server, relying on less than VALIDATE_DEBOUNCE_MS (300ms) of wall time elapsing in between. When more did, the debounce landed first, `canAdd` went false, and the click hit a disabled button — a no-op that sets no submit error, so the `findByText(/Fix the validation errors/)` timed out. The first assertion (`onAddServer` not called) still passed, which is why the failure read as mysterious rather than as a disabled button. Run the test on fake timers end to end instead. The first validation is landed explicitly with `advanceTimersByTimeAsync(VALIDATE_DEBOUNCE_MS)` — imported from the hook, so the test cannot drift from the value it depends on — and after the second paste the timers are simply not advanced. The pending re-validation therefore cannot land at all, the window stays open by construction, and the final assertion becomes a synchronous `getByText`. Verified with a throwaway probe holding both shapes side by side with a deterministic 400ms stall injected between the paste and the click: the old shape fails exactly as reported, the new one passes. No timeout was widened and no production code changed. Closes #2250 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VQgwZ1kGzhkdkMJ81JVg42 Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
eb44e6d238
commit
dc2dd2bfcf
1 changed files with 36 additions and 21 deletions
|
|
@ -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(
|
||||
<ServerImportJsonModal
|
||||
opened
|
||||
existingIds={[]}
|
||||
onClose={vi.fn()}
|
||||
onAddServer={onAddServer}
|
||||
/>,
|
||||
);
|
||||
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(
|
||||
<ServerImportJsonModal
|
||||
opened
|
||||
existingIds={[]}
|
||||
onClose={vi.fn()}
|
||||
onAddServer={onAddServer}
|
||||
/>,
|
||||
);
|
||||
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 () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue