Some checks failed
synthetic / suite (push) Has been cancelled
Assistant: grok Assistant-Session: 01a1531b-6698-7732-8587-f28ea604bcdf
157 lines
6.4 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
});
|