CONTENT-WP-0002-T06 runs that path on the systems-thinking and software-architecture fixtures. Assistant: grok Assistant-Session: 01a1346c-fdff-7741-b1af-b7283661554b
127 lines
5 KiB
TypeScript
127 lines
5 KiB
TypeScript
// @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 () => {
|
|
expect(listNavigation().map((item) => item.id).sort()).toEqual([...SPACES].sort());
|
|
|
|
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(<RecordBrowser rows={kind!.rows} columns={kind!.columns} history={memoryHistory()} />);
|
|
});
|
|
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(<EntityDetail entity={detail!} />);
|
|
expect(html).toContain(`>${detail!.title}<`);
|
|
expect(html).toContain(detail!.tldr);
|
|
expect(html).toContain("Sources");
|
|
|
|
act(() => root?.unmount());
|
|
root = null;
|
|
}
|
|
});
|
|
});
|