// @vitest-environment happy-dom import { createRoot, type Root } from "react-dom/client"; import { act, type ComponentProps } from "react"; import { afterEach, describe, expect, it } from "vitest"; import { mapBrowserRow, type ColumnDef } from "@/lib/browser-row"; 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"], ), ]; let root: Root | null = null; afterEach(() => { act(() => root?.unmount()); root = null; document.body.innerHTML = ""; }); async function render(props: Partial> = {}) { const host = document.createElement("div"); document.body.appendChild(host); root = createRoot(host); await act(async () => { root?.render(); }); } function quietHistory() { return { read: () => "", push: () => undefined, subscribe: () => () => undefined }; } describe("record browser states", () => { it("shows loading and error without the table", async () => { await render({ status: "loading" }); expect(document.querySelector("[role='status']")?.textContent).toBe("Loading records."); expect(document.querySelector("table")).toBeNull(); await act(async () => root?.unmount()); await render({ status: "error", errorMessage: "The snapshot is unreadable." }); expect(document.querySelector("[role='alert']")?.textContent).toBe("The snapshot is unreadable."); expect(document.querySelector("table")).toBeNull(); }); it("distinguishes an empty catalog from a search that matches nothing", async () => { await render({ rows: [] }); expect(document.body.textContent).toContain("No records."); expect(document.body.textContent).not.toContain("Clear view"); await act(async () => root?.unmount()); await render(); 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, "missing"); input.dispatchEvent(new Event("input", { bubbles: true })); input.form?.requestSubmit(); }); expect(document.body.textContent).toContain("No records match this view."); const clear = [...document.querySelectorAll("button")].find((node) => node.textContent === "Clear view"); if (!(clear instanceof HTMLButtonElement)) throw new Error("missing clear"); await act(async () => { clear.click(); }); expect(document.body.textContent).toContain("Alpha"); }); it("focuses search on slash and clears it on escape", async () => { await render(); const input = document.querySelector("input[aria-label='Search records']"); if (!(input instanceof HTMLInputElement)) throw new Error("missing search"); await act(async () => { window.dispatchEvent(new KeyboardEvent("keydown", { key: "/", bubbles: true })); }); expect(document.activeElement).toBe(input); await act(async () => { const setValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set; setValue?.call(input, "draft"); input.dispatchEvent(new Event("input", { bubbles: true })); input.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true })); }); expect(input.value).toBe(""); }); });