CONTENT-WP-0004 keeps outgoing and incoming labels apart, limits the graph to two hops, and stores focus and detail in the URL. Assistant: grok Assistant-Session: 01a1531b-6698-7732-8587-f28ea604bcdf
111 lines
6.3 KiB
TypeScript
111 lines
6.3 KiB
TypeScript
// @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(<KindIndex kind={kind!} />);
|
|
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(<EntityDetail entity={entity!} />);
|
|
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<string, { forward: string; inverse: string }>;
|
|
};
|
|
const entity = findEntity("systems-thinking", "concept", "feedback");
|
|
expect(entity).not.toBeNull();
|
|
const html = renderToStaticMarkup(<EntityDetail entity={entity!} />);
|
|
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(<EntityDetail entity={view} />);
|
|
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");
|
|
}
|