// @vitest-environment happy-dom import { readFileSync } from "node:fs"; import { join } from "node:path"; import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it } from "vitest"; import { findEntity, findGraph, findKind, type EntityView } from "@/lib/catalog"; import { partitionIncident } from "@/lib/graph"; import { EntityDetail } from "./EntityDetail"; import { KindIndex } from "./KindIndex"; const systemsRoot = join(process.cwd(), "../examples/dataspaces/systems-thinking"); describe("kind and detail views", () => { it("renders the registry plural and a linked record", () => { const kind = findKind("software-architecture", "concept"); expect(kind).not.toBeNull(); const html = renderToStaticMarkup(); expect(html).toContain(">Concepts<"); expect(html).toContain("Coupling"); expect(html).toContain('href="/software-architecture/concept/coupling/"'); expect(html).toContain("dependency"); expect(html).toContain(">Tags<"); }); it("renders the entity title, an unknown claim, and a neighbour", () => { const entity = findEntity("systems-thinking", "concept", "system-boundary"); expect(entity).not.toBeNull(); const html = renderToStaticMarkup(); expect(html).toContain(">System boundary<"); expect(html).toContain("formal-measure"); expect(html).toContain("unknown"); expect(html).toContain("Stock and flow model"); expect(html).not.toContain("glossary-tip"); expect(html).not.toContain("link-preview"); expect(html).toContain('href="/systems-thinking/method/stock-flow/"'); }); it("renders both relation blocks, journeys, and optional hints for feedback", () => { const snapshot = JSON.parse(readFileSync(join(systemsRoot, "snapshot.json"), "utf8")) as { entities: { id: string; properties?: { glossary?: string; preview?: { title: string; summary: string }; readingPath?: string[] } }[]; relations: { id: string; from: string; to: string }[]; }; const manifest = JSON.parse(readFileSync(join(systemsRoot, "manifest.json"), "utf8")) as { relationLabels: Record; }; const entity = findEntity("systems-thinking", "concept", "feedback"); expect(entity).not.toBeNull(); const html = renderToStaticMarkup(); const host = documentFrom(html); const incident = snapshot.relations.filter((relation) => relation.from === entity!.id || relation.to === entity!.id); const rendered = [...host.querySelectorAll("[data-relation]")].map((node) => node.getAttribute("data-relation")); expect(rendered.sort()).toEqual(incident.map((relation) => relation.id).sort()); expect(host.querySelector('[aria-labelledby="outgoing-relations"] .text-muted')?.textContent).toBe( manifest.relationLabels["represented-by"].forward, ); const incomingWordings = [...host.querySelectorAll('[aria-labelledby="incoming-relations"] .text-muted')].map( (node) => node.textContent, ); expect(incomingWordings).toContain(manifest.relationLabels.models.inverse); expect(incomingWordings).not.toContain(manifest.relationLabels.models.forward); const causal = host.querySelector('[data-relation="r1"]'); const causalEntity = snapshot.entities.find((item) => item.id.endsWith(":causal-loop")); expect(causal?.querySelector(".glossary-tip")?.textContent).toBe(causalEntity?.properties?.glossary); expect(causal?.querySelector(".preview-title")?.textContent).toBe(causalEntity?.properties?.preview?.title); expect(causal?.querySelector(".preview-summary")?.textContent).toBe(causalEntity?.properties?.preview?.summary); expect(causal?.querySelector("a")?.getAttribute("href")).toBe("/systems-thinking/method/causal-loop/"); const modeled = host.querySelector('[data-relation="r6"]'); expect(modeled?.querySelector("a")?.getAttribute("href")).toBe("/systems-thinking/method/stock-flow/"); expect(modeled?.querySelector(".glossary-tip")).toBeNull(); expect(modeled?.querySelector(".link-preview")).toBeNull(); const members = [...host.querySelectorAll("#collection-heading")]; expect(members.length).toBe(1); const collection = members[0]?.parentElement; expect(collection?.querySelector('a[href="/systems-thinking/method/causal-loop/"]')).toBeTruthy(); expect(collection?.querySelector('a[href="/systems-thinking/method/stock-flow/"]')).toBeTruthy(); expect(host.querySelector('[aria-labelledby="related-heading"] a')?.getAttribute("href")).toBe( "/systems-thinking/concept/emergence/", ); const crumbs = [...host.querySelectorAll('[aria-label="Reading path"] li')]; const declared = snapshot.entities.find((item) => item.id === entity!.id)?.properties?.readingPath ?? []; expect(crumbs).toHaveLength(declared.length); expect(crumbs[0]?.querySelector("a")?.getAttribute("href")).toBe("/systems-thinking/concept/system-boundary/"); expect(crumbs[1]?.querySelector("[aria-current='page']")?.textContent).toBe(entity!.title); expect(crumbs[2]?.querySelector("a")?.getAttribute("href")).toBe("/systems-thinking/method/causal-loop/"); }); it("shows a remainder when the shipped partition leaves relations out", () => { const entity = findEntity("systems-thinking", "concept", "feedback"); const graph = findGraph("systems-thinking"); expect(entity && graph).toBeTruthy(); const blocks = partitionIncident(entity!.id, graph!.edges, graph!.labels, 1); const listed = new Set([...blocks.outgoing, ...blocks.incoming].map((relation) => relation.id)); const view: EntityView = { ...entity!, outgoing: entity!.outgoing.filter((relation) => listed.has(relation.id)), incoming: entity!.incoming.filter((relation) => listed.has(relation.id)), relationRemainder: blocks.remainder, }; const html = renderToStaticMarkup(); expect(blocks.remainder).toBeGreaterThan(0); expect(html).toContain(`${blocks.remainder} more relations are not listed.`); const host = documentFrom(html); expect([...host.querySelectorAll("[data-relation]")].map((node) => node.getAttribute("data-relation")).sort()).toEqual( [...listed].sort(), ); }); }); function documentFrom(html: string): Document { return new DOMParser().parseFromString(html, "text/html"); }