// @vitest-environment happy-dom import { createRoot, type Root } from "react-dom/client"; import { act } from "react"; import { afterEach, describe, expect, it } from "vitest"; import { mapBrowserRow, type ColumnDef } from "@/lib/browser-row"; import { browserViewHistory, type ViewHistory } from "@/lib/table-view"; import { RecordBrowser } from "./RecordBrowser"; declare global { var IS_REACT_ACT_ENVIRONMENT: boolean; } globalThis.IS_REACT_ACT_ENVIRONMENT = true; const columns: ColumnDef[] = [ { key: "name", label: "Name" }, { key: "tldr", label: "Summary" }, ]; const rows = [ mapBrowserRow( { id: "a", title: "Alpha", tldr: "First", tags: ["one"], kindLabel: "Concept", route: "/a/", kindHref: "/concept/" }, ["tags"], ), mapBrowserRow( { id: "z", title: "Zebra", tldr: "Last", tags: ["two"], kindLabel: "Concept", route: "/z/", kindHref: "/concept/" }, ["tags"], ), ]; let root: Root | null = null; afterEach(() => { act(() => root?.unmount()); root = null; document.body.innerHTML = ""; }); function names(): string[] { return [...document.querySelectorAll("tbody tr td:first-child")].map((cell) => cell.textContent ?? ""); } function buttonNamed(label: string): HTMLButtonElement { const node = [...document.querySelectorAll("button")].find((item) => item.textContent?.includes(label)); if (!(node instanceof HTMLButtonElement)) throw new Error(`missing ${label}`); return node; } /** Same-document session history: back and forward keep this page and report the new query. */ function sessionHistory(): ViewHistory & { back: () => void; forward: () => void; location: () => string } { const stack = [""]; let index = 0; const listeners = new Set<() => void>(); const notify = () => { for (const listener of listeners) listener(); }; return { read: () => (stack[index] ? `?${stack[index]}` : ""), push: (search) => { stack.splice(index + 1); stack.push(search); index = stack.length - 1; }, subscribe: (listener) => { listeners.add(listener); return () => listeners.delete(listener); }, back: () => { if (index === 0) return; index -= 1; notify(); }, forward: () => { if (index >= stack.length - 1) return; index += 1; notify(); }, location: () => stack[index], }; } describe("record browser history", () => { it("reads a pushed query and a popstate traversal", () => { window.history.replaceState(null, "", "/records/"); const history = browserViewHistory(); const seen: string[] = []; const stop = history.subscribe(() => seen.push(history.read())); history.push("q=zebra"); expect(window.location.search).toBe("?q=zebra"); expect(seen).toEqual([]); window.history.pushState(null, "", "/records/?tags=one"); window.dispatchEvent(new PopStateEvent("popstate")); expect(seen).toEqual(["?tags=one"]); stop(); }); it("restores search, facets, and sort on back and forward", async () => { const history = sessionHistory(); const host = document.createElement("div"); document.body.appendChild(host); root = createRoot(host); await act(async () => { root?.render(); }); expect(names()).toEqual(["Alpha", "Zebra"]); const input = document.querySelector("input[aria-label='Search records']"); if (!(input instanceof HTMLInputElement)) throw new Error("missing search"); await act(async () => { const setValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set; setValue?.call(input, "zebra"); input.dispatchEvent(new Event("input", { bubbles: true })); }); await act(async () => { input.form?.requestSubmit(); }); expect(history.location()).toContain("q=zebra"); expect(names()).toEqual(["Zebra"]); await act(async () => { buttonNamed("two").click(); }); expect(history.location()).toContain("tags=two"); expect(buttonNamed("two").getAttribute("aria-pressed")).toBe("true"); await act(async () => { buttonNamed("Name").click(); }); expect(history.location()).toContain("sort=-name"); expect(names()).toEqual(["Zebra"]); await act(async () => { history.back(); }); expect(history.location()).not.toContain("sort="); expect(history.location()).toContain("tags=two"); await act(async () => { history.back(); }); expect(history.location()).toBe("q=zebra"); expect(buttonNamed("two").getAttribute("aria-pressed")).toBe("false"); expect(names()).toEqual(["Zebra"]); await act(async () => { history.back(); }); expect(history.location()).toBe(""); expect(names()).toEqual(["Alpha", "Zebra"]); await act(async () => { history.forward(); }); expect(history.location()).toBe("q=zebra"); expect(names()).toEqual(["Zebra"]); await act(async () => { history.forward(); }); expect(history.location()).toContain("tags=two"); await act(async () => { history.forward(); }); expect(history.location()).toContain("sort=-name"); expect(names()).toEqual(["Zebra"]); }); });