import assert from "node:assert/strict"; import test from "node:test"; import type { DiagramPage } from "../../model/diagram-ir.js"; import { layoutPage } from "./layout-engine.js"; const PAGE: DiagramPage = { id: "system", title: "System", layout: { type: "layered", direction: "horizontal", gridSize: 10, nodeGap: 40, layerGap: 80 }, nodes: [ { id: "client", label: "Client", width: 120, height: 60 }, { id: "worker", label: "Worker", width: 120, height: 60 }, { id: "api", label: "API", width: 120, height: 60 }, { id: "db", label: "Database", width: 120, height: 60 }, ], edges: [ { id: "client-api", source: "client", target: "api" }, { id: "worker-api", source: "worker", target: "api" }, { id: "api-db", source: "api", target: "db" }, ], }; test("layered layout is deterministic, dependency-aware, and grid-aligned", () => { const first = layoutPage(PAGE); const second = layoutPage(PAGE); assert.deepEqual(first, second); const positions = new Map(first.nodes.map((node) => [node.id, node.geometry!])); assert.ok(positions.get("client")!.x < positions.get("api")!.x); assert.ok(positions.get("worker")!.x < positions.get("api")!.x); assert.ok(positions.get("api")!.x < positions.get("db")!.x); assert.notEqual(positions.get("client")!.y, positions.get("worker")!.y); for (const geometry of positions.values()) { assert.equal(geometry.x % 10, 0); assert.equal(geometry.y % 10, 0); } }); test("layered layout avoids explicit-geometry siblings", () => { const page: DiagramPage = { id: "explicit", title: "Explicit", layout: { type: "layered", direction: "horizontal", gridSize: 10, nodeGap: 40, layerGap: 80 }, nodes: [ { id: "a", label: "A", geometry: { x: 40, y: 80, width: 120, height: 60 } }, { id: "b", label: "B" }, ], edges: [], }; const laidOut = layoutPage(page); const [a, b] = laidOut.nodes.map((node) => node.geometry!); const overlaps = a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y; assert.equal(overlaps, false); }); test("manual layout preserves explicit geometry", () => { const page: DiagramPage = { id: "manual", title: "Manual", layout: { type: "manual" }, nodes: [{ id: "node", label: "Node", geometry: { x: 35, y: 45, width: 130, height: 70 } }], edges: [], }; assert.deepEqual(layoutPage(page), page); }); test("grid and linear layouts avoid explicit siblings and honor grid/gap settings", () => { for (const type of ["grid", "linear"] as const) { const page: DiagramPage = { id: `mixed-${type}`, title: "Mixed", layout: { type, direction: "horizontal", gridSize: 20, nodeGap: 60 }, nodes: [ { id: "generated-one", label: "Generated one", width: 130, height: 60 }, { id: "fixed", label: "Fixed", geometry: { x: 40, y: 80, width: 130, height: 60 } }, { id: "generated-two", label: "Generated two", width: 130, height: 60 }, ], edges: [], }; const laidOut = layoutPage(page); const geometries = laidOut.nodes.map((node) => node.geometry!); for (const geometry of geometries) { assert.equal(geometry.x % 20, 0); assert.equal(geometry.y % 20, 0); } for (let left = 0; left < geometries.length; left += 1) for (let right = left + 1; right < geometries.length; right += 1) { const a = geometries[left]; const b = geometries[right]; const overlaps = a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y; assert.equal(overlaps, false, `${type} layout overlapped mixed siblings`); } if (type === "linear") { const generated = laidOut.nodes.filter((node) => node.id.startsWith("generated")).map((node) => node.geometry!).sort((a, b) => a.x - b.x); assert.ok(generated[1].x - (generated[0].x + generated[0].width) >= 60); } } }); test("automatic layout uses parent-relative coordinates and expands containers", () => { const page: DiagramPage = { id: "containers", title: "Containers", layout: { type: "grid", gridSize: 10, nodeGap: 40 }, nodes: [ { id: "zone", label: "Zone", kind: "container", width: 120, height: 80 }, { id: "one", label: "One", parentId: "zone", width: 120, height: 60 }, { id: "two", label: "Two", parentId: "zone", width: 120, height: 60 }, ], edges: [], }; const laidOut = layoutPage(page); const byId = new Map(laidOut.nodes.map((node) => [node.id, node.geometry!])); const zone = byId.get("zone")!; for (const childId of ["one", "two"]) { const child = byId.get(childId)!; assert.ok(child.x >= 40 && child.y >= 40); assert.ok(child.x + child.width + 40 <= zone.width); assert.ok(child.y + child.height + 40 <= zone.height); } }); test("automatic layout positions generated sibling containers without overlap", () => { for (const type of ["grid", "linear"] as const) { const page: DiagramPage = { id: "siblings", title: "Siblings", layout: { type, gridSize: 10, nodeGap: 40 }, nodes: [ { id: "left", label: "Left", kind: "container" }, { id: "left-child", label: "Left child", parentId: "left", width: 240 }, { id: "right", label: "Right", kind: "container" }, { id: "right-child", label: "Right child", parentId: "right", width: 240 }, ], edges: [], }; const laidOut = layoutPage(page); const byId = new Map(laidOut.nodes.map((node) => [node.id, node.geometry!])); const left = byId.get("left")!; const right = byId.get("right")!; const overlaps = left.x < right.x + right.width && left.x + left.width > right.x && left.y < right.y + right.height && left.y + left.height > right.y; assert.equal(overlaps, false, `${type} layout overlapped sibling containers`); } });