// @vitest-environment jsdom // // `setPage`'s no-op contract — the PRODUCER side of the history-burial fix, which nothing rendered // before this. `setPage` early-returns when the requested page is already current with no channel // page over it, and reports that by returning `false`; `lib/history.buryUnder` reads the boolean to // decide whether the overlay entries beneath a navigation are still reclaimable. Get it wrong and // the round-2 bug is back: Shift+W over the player on Home buries an entry nothing pushed past, and // the user's next browser Back does nothing. // // The type does NOT fence this. `NavigationActions` pins the SHAPE (`=> boolean`), so deleting one // `return` fails tsc — but round 3 measured that swapping the two values (early return `true`, tail // `false`) passes typecheck and all 822 tests. Hence assertions on the value AND on the effects the // early return skips, which is what the boolean actually stands for. import { describe, it, expect, afterEach, beforeEach, vi } from "vitest"; import { renderHook, act, cleanup } from "@testing-library/react"; import type { ReactNode } from "react"; // Partial: the provider reads `t` AND `i18n.language` (both feed the document-title effect), and the // import graph pulls in `src/i18n`, which initialises against the real `initReactI18next` — so a // wholesale mock throws before any test runs. vi.mock("react-i18next", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useTranslation: () => ({ t: (k: string) => k, i18n: { language: "en" } }), }; }); import { NavigationProvider, useNavigation, useNavigationActions } from "./NavigationProvider"; const wrapper = ({ children }: { children: ReactNode }) => ( {children} ); const render = () => renderHook(() => ({ ...useNavigation(), ...useNavigationActions() }), { wrapper }); beforeEach(() => { localStorage.clear(); sessionStorage.setItem("siftlode.activeAccount", "1"); // account-scoped page memory window.history.replaceState({ sfPage: "feed" }, "", "/"); }); // Not automatic: vitest runs without `globals`, so RTL never registers its own afterEach — leaving a // provider (and its popstate listener) mounted for the next case. Same reason Home.dom.test.tsx does. afterEach(cleanup); describe("NavigationProvider · setPage reports whether it navigated", () => { it("returns true and pushes an entry for a real page change", () => { const { result } = render(); const push = vi.spyOn(window.history, "pushState"); try { let reported: boolean | undefined; act(() => { reported = result.current.setPage("plex"); }); expect(reported).toBe(true); expect(push).toHaveBeenCalledTimes(1); expect(result.current.page).toBe("plex"); } finally { push.mockRestore(); } }); it("returns false and pushes nothing when it is already that page", () => { const { result } = render(); act(() => { result.current.setPage("plex"); }); const push = vi.spyOn(window.history, "pushState"); try { let reported: boolean | undefined; act(() => { reported = result.current.setPage("plex"); }); // Both halves matter: `false` is what `buryUnder` reads, and "pushed nothing" is what makes // that answer true. A value-only assertion would pass against an inverted pair. expect(reported).toBe(false); expect(push).not.toHaveBeenCalled(); } finally { push.mockRestore(); } }); it("returns true even for the current page when a channel page is over it", () => { // The nav rail's own module while a channel page is open over it is a real navigation: it closes // the channel page. The `&& !channelViewRef.current` half of the guard is what makes it one. const { result } = render(); // The page under test must be the one that is ALREADY current, or the guard's first conjunct is // false and this never reaches the clause it is named for — the mistake round 4 caught here: a // bare URL with cleared storage lands on "home" (navigationInit's E24 default), not "feed", so // asking for "feed" was just an ordinary page change and deleting the clause left it green. const current = result.current.page; expect(current).toBe("home"); act(() => { result.current.openChannel("UC1", "A channel"); }); expect(result.current.channelView).not.toBeNull(); const push = vi.spyOn(window.history, "pushState"); try { let reported: boolean | undefined; act(() => { reported = result.current.setPage(current); }); expect(reported).toBe(true); expect(push).toHaveBeenCalledTimes(1); expect(result.current.channelView).toBeNull(); } finally { push.mockRestore(); } }); });