// @vitest-environment jsdom // // ONE test, ONE file, on purpose. This is the close→reclaim→run ordering contract of the palette // (see closeTopOverlayThenRun), and it ASSERTS on a real useBackToClose entry driven through a real // programmatic history.back() — which is why it cannot share module state with anything. (Any test // rendering a real Modal can let an unstubbed back() go, too.) // // It cannot live in CommandPalette.dom.test.tsx. jsdom gives a test FILE a single session history, // and history.ts's overlay bookkeeping is module-global, so the sibling tests there leave an // in-flight programmatic back() behind — under which this one either hangs or, worse, goes green on // somebody else's popstate. That is exactly how it used to pass: it asserted nothing on its own, and // running it with `-t` failed (E10 review round 1). Its own file gives it clean module state. import { useState } from "react"; import { describe, it, expect, vi, beforeAll } from "vitest"; import { render, screen, fireEvent, waitFor, act } from "@testing-library/react"; import CommandPalette, { type CommandGroup } from "./CommandPalette"; vi.mock("react-i18next", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useTranslation: () => ({ t: (k: string) => k }) }; }); beforeAll(() => { // jsdom has no layout, so scrollIntoView is missing on the elements the active-row effect touches. Element.prototype.scrollIntoView ??= () => {}; }); const ran: string[] = []; function Harness() { const [isOpen, setIsOpen] = useState(true); const groups: CommandGroup[] = [ { id: "channels", label: "Channels", items: [{ id: "ch:1", label: "TheVR Podcast", run: () => ran.push("ch:1") }], }, ]; return ( { ran.push("closed"); setIsOpen(false); }} groups={groups} /> ); } describe("CommandPalette · close-then-run ordering", () => { it("closes, and runs the command only once the close has landed", async () => { render(); // Load-bearing: useBackToClose pushes its entry from a coalesced microtask, so a click in the // same synchronous tick as the render finds no entry to reclaim — no back(), no popstate. await act(async () => { await Promise.resolve(); }); fireEvent.click(screen.getByText("TheVR Podcast")); expect(ran).toEqual(["closed"]); // Well inside closeTopOverlayThenRun's 1500 ms give-up timer, so a pass means the RECLAIM ran the // command — not the late path for a back() that never reports. (The margin used to be 200 ms // under a 250 ms fallback, which made this load-sensitive by construction.) await waitFor(() => expect(ran).toEqual(["closed", "ch:1"]), { timeout: 1000 }); }); });