content-explorer/ui/components/journeys.test.tsx
tegwick f43edc1edc
Some checks failed
synthetic / suite (push) Has been cancelled
Add a synthetic quality suite and upstream parity record.
Assistant: grok
Assistant-Session: 01a1531b-6698-7732-8587-f28ea604bcdf
2026-10-19 10:38:49 +02:00

157 lines
6.4 KiB
TypeScript

// @vitest-environment happy-dom
import { createRoot, type Root } from "react-dom/client";
import { act, type ReactNode } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { afterEach, describe, expect, it } from "vitest";
import { EntityDetail } from "@/components/EntityDetail";
import { GraphExplorer } from "@/components/GraphExplorer";
import { RecordBrowser } from "@/components/RecordBrowser";
import { findEntity, findGraph, findKind, type EntityView } from "@/lib/catalog";
import type { GraphHistory } from "@/lib/graph";
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 fixedHistory(search: string): GraphHistory {
return {
read: () => search,
push: () => undefined,
subscribe: () => () => undefined,
};
}
function journeyEntity(dataspaceId: string): EntityView {
const kind = findKind(dataspaceId, "concept");
if (!kind) throw new Error(`missing concept kind in ${dataspaceId}`);
for (const row of kind.rows) {
const slug = row.route.split("/").filter(Boolean).at(-1) ?? "";
const entity = findEntity(dataspaceId, "concept", decodeURIComponent(slug));
if (entity && (entity.outgoing.length > 0 || entity.incoming.length > 0) && entity.sources.length > 0) {
return entity;
}
}
throw new Error(`no related record in ${dataspaceId}`);
}
function mount(node: ReactNode) {
const host = document.createElement("div");
document.body.appendChild(host);
root = createRoot(host);
return act(async () => {
root?.render(node);
});
}
describe("critical journeys", () => {
it("browses, searches, opens a record, follows a neighbour, shows a source, and opens the graph", async () => {
for (const id of SPACES) {
const kind = findKind(id, "concept");
const entity = journeyEntity(id);
expect(kind).not.toBeNull();
document.body.innerHTML = "";
await mount(<RecordBrowser rows={kind!.rows} columns={kind!.columns} history={memoryHistory()} />);
const names = () => [...document.querySelectorAll("tbody tr td:first-child")].map((cell) => cell.textContent ?? "");
expect(names().length).toBe(kind!.rows.length);
expect(names()).toContain(entity.title);
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, entity.title);
input.dispatchEvent(new Event("input", { bubbles: true }));
input.form?.requestSubmit();
});
expect(names()).toEqual([entity.title]);
const detailHtml = renderToStaticMarkup(<EntityDetail entity={entity} />);
const detail = new DOMParser().parseFromString(detailHtml, "text/html");
const source = entity.sources[0];
expect(source).toBeTruthy();
expect(detail.body.textContent).toContain(source.title);
expect(detail.body.textContent).toContain(source.license);
const neighbour = entity.outgoing[0] ?? entity.incoming[0];
expect(neighbour.other.href).toBeTruthy();
const neighbourLink = detail.querySelector(`a[href="${neighbour.other.href}"]`);
expect(neighbourLink?.textContent).toContain(neighbour.other.title);
const parts = neighbour.other.href!.split("/").filter(Boolean);
const followed = findEntity(parts[0], parts[1], decodeURIComponent(parts[2]));
expect(followed?.title).toBe(neighbour.other.title);
expect(renderToStaticMarkup(<EntityDetail entity={followed!} />)).toContain(followed!.title);
expect(detailHtml).toContain(entity.graphHref);
act(() => root?.unmount());
root = null;
document.body.innerHTML = "";
const graph = findGraph(id);
expect(graph).not.toBeNull();
const search = `?${new URLSearchParams({ focus: entity.id }).toString()}`;
await mount(<GraphExplorer model={graph!} initialSearch={search} history={fixedHistory(search)} />);
expect(document.querySelector("[data-focus]")?.getAttribute("data-focus")).toBe(entity.id);
expect(document.body.textContent).toContain(neighbour.wording);
expect(document.querySelector("[data-excluded-count]")).toBeTruthy();
act(() => root?.unmount());
root = null;
}
});
it("can focus search, a result, the record link, a relation, and the way back", async () => {
for (const id of SPACES) {
const kind = findKind(id, "concept");
const entity = journeyEntity(id);
document.body.innerHTML = "";
await mount(<RecordBrowser rows={kind!.rows} columns={kind!.columns} history={memoryHistory()} />);
const search = document.querySelector("input[aria-label='Search records']");
const result = document.querySelector("tbody a");
if (!(search instanceof HTMLElement) || !(result instanceof HTMLAnchorElement)) throw new Error("missing focus targets");
search.focus();
expect(document.activeElement).toBe(search);
expect(search.tabIndex).toBeGreaterThanOrEqual(0);
result.focus();
expect(document.activeElement).toBe(result);
expect(result.getAttribute("href")).toBeTruthy();
act(() => root?.unmount());
root = null;
document.body.innerHTML = "";
const host = document.createElement("div");
host.innerHTML = renderToStaticMarkup(<EntityDetail entity={entity} />);
document.body.appendChild(host);
const relation = host.querySelector("[data-relation] a");
const back = [...host.querySelectorAll("a")].find((link) => link.textContent?.startsWith("Back to"));
if (!(relation instanceof HTMLAnchorElement) || !(back instanceof HTMLAnchorElement)) {
throw new Error(`missing record links in ${id}`);
}
relation.focus();
expect(document.activeElement).toBe(relation);
back.focus();
expect(document.activeElement).toBe(back);
expect(back.getAttribute("href")).toBe(entity.kindHref);
}
});
});