// @vitest-environment happy-dom import { createRoot, type Root } from "react-dom/client"; import { act } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { afterEach, describe, expect, it } from "vitest"; import { EntityDetail } from "@/components/EntityDetail"; import { RecordBrowser } from "@/components/RecordBrowser"; import { findDataspace, findEntity, findKind, listNavigation } from "@/lib/catalog"; import { applyTableView } from "@/lib/table-view"; import type { ViewHistory } from "@/lib/table-view"; declare global { var IS_REACT_ACT_ENVIRONMENT: boolean; } globalThis.IS_REACT_ACT_ENVIRONMENT = true; const SPACES = ["systems-thinking", "software-architecture"] as const; let root: Root | null = null; afterEach(() => { act(() => root?.unmount()); root = null; document.body.innerHTML = ""; }); function memoryHistory(): ViewHistory { let current = ""; return { read: () => (current ? `?${current}` : ""), push: (search) => { current = search; }, subscribe: () => () => undefined, }; } 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 === label); if (!(node instanceof HTMLButtonElement)) throw new Error(`missing ${label}`); return node; } describe("both dataspaces", () => { it("browses, searches, filters, sorts, and opens a detail for each field", async () => { const ids = listNavigation().map((item) => item.id); for (const id of SPACES) expect(ids).toContain(id); for (const id of SPACES) { const space = findDataspace(id); const kind = findKind(id, "concept"); expect(space?.title.length).toBeGreaterThan(0); expect(kind?.rows.length).toBeGreaterThan(1); expect(kind?.rows.every((row) => row.route.startsWith(`/${id}/concept/`))).toBe(true); const sample = kind!.rows[0]; const tag = sample.facets.tags[0]; const searched = applyTableView(kind!.rows, { q: sample.name, facets: {}, sort: { key: "name", dir: 1 } }); expect(searched.map((row) => row.name)).toEqual([sample.name]); const filtered = applyTableView(kind!.rows, { q: "", facets: { tags: [tag] }, sort: { key: "name", dir: 1 } }); expect(filtered.length).toBeGreaterThan(0); expect(filtered.length).toBeLessThan(kind!.rows.length); expect(filtered.every((row) => row.facets.tags.includes(tag))).toBe(true); const descending = applyTableView(kind!.rows, { q: "", facets: {}, sort: { key: "name", dir: -1 } }); expect(descending.map((row) => row.name)).toEqual( [...kind!.rows.map((row) => row.name)].sort((a, b) => b.localeCompare(a)), ); const host = document.createElement("div"); document.body.innerHTML = ""; document.body.appendChild(host); root = createRoot(host); await act(async () => { root?.render(); }); expect(names().length).toBe(kind!.rows.length); 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, sample.name); input.dispatchEvent(new Event("input", { bubbles: true })); input.form?.requestSubmit(); }); expect(names()).toEqual([sample.name]); await act(async () => { const setValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set; setValue?.call(input, ""); input.dispatchEvent(new Event("input", { bubbles: true })); input.form?.requestSubmit(); }); expect(names().length).toBe(kind!.rows.length); await act(async () => { buttonNamed(tag).click(); }); expect(names().length).toBe(filtered.length); expect(document.body.textContent).toContain(tag); await act(async () => { buttonNamed(tag).click(); }); expect(names().length).toBe(kind!.rows.length); const sortName = [...document.querySelectorAll("button")].find((node) => node.textContent?.includes("Name")); if (!(sortName instanceof HTMLButtonElement)) throw new Error("missing sort"); await act(async () => { sortName.click(); }); expect(names()).toEqual(descending.map((row) => row.name)); const slug = sample.route.split("/").filter(Boolean).at(-1) ?? ""; const detail = findEntity(id, "concept", decodeURIComponent(slug)); expect(detail?.title).toBe(sample.name); const html = renderToStaticMarkup(); expect(html).toContain(`>${detail!.title}<`); expect(html).toContain(detail!.tldr); expect(html).toContain("Sources"); act(() => root?.unmount()); root = null; } }); });