Round 4 reviewed round 3's commit. Nothing in production behaviour was wrong; what was wrong was the round's own test and its comments, which is the failure class this loop keeps finding. - The new NavigationProvider case named after the `&& !channelViewRef.current` clause never reached it. With storage cleared and a bare URL the provider lands on "home" (E24's own default), not "feed", so asking for "feed" was an ordinary page change and the guard's first conjunct short-circuited. Deleting the clause left all 828 tests green — a dead nav control (the rail's own module stops closing an open channel page) with no red anywhere. The test now asserts the precondition and calls `setPage` with the page that IS current. - Both new Home cases rejected a promise and then asserted after one fixed macrotask. React-query notifies through `notifyManager`'s own `setTimeout(0)`, registered after any timer the test schedules, so the assertion raced it; two reviewers measured intermittent reds. They poll for a signal that the rejection LANDED instead — the reroll button releasing, which is also the honest one for the empty-answer case, where the empty state is already on screen while the press is in flight and asserting it early would pass either way. - `buryUnder`'s comment still opened with "one rule for both entry points" ten lines above the paragraph saying the rule is complete only for one of them, listed the non-reporting palette rows without `openChannel` — which has its own `replaceState` no-push branch and is reached from PlayerModal too — and pointed at a ROADMAP.md this repository does not contain. - `history.dom.test.ts`'s header claimed its layers close by genuine Backs only, which two of its own tests contradict; and `CommandPalette.ordering.dom.test.tsx` still claimed to be "the only assertion in the suite" doing what the file corrected in round 3 also does. Round 3's message said that pair was handled and only one side had been. The provider test was re-mutation-checked (dropping the clause now goes red), and both hardened Home tests still go red against their own reverts. Seven consecutive full runs green. Gate: ruff, ruff format, 3x tsc, eslint (0 errors), prettier, knip, vitest 828, pytest 1005.
70 lines
2.8 KiB
TypeScript
70 lines
2.8 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// ONE test, ONE file, on purpose. This is the close→reclaim→run ordering contract of the palette
|
|
// (see closeThenNavigate), 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. (It is not
|
|
// the only place in the suite where an unstubbed back() fires; history.burial.dom.test.ts is the
|
|
// other file that asserts on one, and any test rendering a real Modal can let one go.)
|
|
//
|
|
// 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<typeof import("react-i18next")>();
|
|
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 (
|
|
<CommandPalette
|
|
open={isOpen}
|
|
onClose={() => {
|
|
ran.push("closed");
|
|
setIsOpen(false);
|
|
}}
|
|
groups={groups}
|
|
/>
|
|
);
|
|
}
|
|
|
|
describe("CommandPalette · closeThenNavigate ordering", () => {
|
|
it("closes, and runs the command only once the close has landed", async () => {
|
|
render(<Harness />);
|
|
// 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"]);
|
|
|
|
// Inside closeThenNavigate's 250ms no-popstate fallback, so a pass means the RECLAIM ran the
|
|
// command — not the safety net that exists for overlays which never got an entry at all.
|
|
await waitFor(() => expect(ran).toEqual(["closed", "ch:1"]), { timeout: 200 });
|
|
});
|
|
});
|