Files
next-ai-draw-io/tests/unit/diagram-engine-embed-graph.test.ts

114 lines
4.0 KiB
TypeScript
Raw Normal View History

import { describe, expect, it } from "vitest"
import { restructureDiagram } from "@/lib/diagram-engine"
/**
* add_graph: arrow-driven layout as a CONTAINER, not just a page-level tool.
*
* D2/TALA's core claim is that containers are first-class at every layout stage
* hierarchical zones and arrow-ordered graphs mix in one diagram. Before this, the
* engine was split: draw_graph did whole-page flowcharts, containers did nesting, and
* "a poster column with a small flowchart in it" was inexpressible.
*/
const rectOf = (xml: string, id: string) => {
const m = xml.match(
new RegExp(
`id="${id}"[^>]*>\\s*<mxGeometry[^>]*width="([\\d.]+)" height="([\\d.]+)"`,
),
)
if (!m) throw new Error(`no geometry for ${id}`)
return { w: Number(m[1]), h: Number(m[2]) }
}
const FLOW = {
nodes: [
{ id: "a", label: "request" },
{ id: "b", label: "validate", shape: "decision" },
{ id: "c", label: "process" },
{ id: "d", label: "reject" },
],
edges: [
{ source: "a", target: "b" },
{ source: "b", target: "c", label: "ok" },
{ source: "b", target: "d", label: "no" },
],
}
describe("add_graph", () => {
it("embeds an arrow-ordered graph inside a flexbox column", () => {
const r = restructureDiagram("", [
{ op: "add_container", id: "page", label: "", dir: "row", gap: 20 },
{
op: "add_container",
id: "left",
parent: "page",
label: "",
dir: "col",
gap: 12,
},
{
op: "add_box",
id: "intro",
parent: "left",
label: "How requests flow:",
},
{ op: "add_graph", id: "flow", parent: "left", ...FLOW },
{ op: "add_box", id: "right", parent: "page", label: "Notes" },
])
expect(r.errors).toEqual([])
const xml = r.xml as string
// The decision's two branches share a layer — arrow-driven, not declaration order.
const c = rectOf(xml, "c")
const d = rectOf(xml, "d")
expect(c).toBeTruthy()
expect(d).toBeTruthy()
// And the graph sits inside the column: the outline nests flow under left.
expect(r.outline).toMatch(/left:[\s\S]*flow:/)
})
it("two graphs on one page do not collide on synthetic layer ids", () => {
const r = restructureDiagram("", [
{ op: "add_graph", id: "g1", ...FLOW },
{
op: "add_graph",
id: "g2",
nodes: [
{ id: "x", label: "start" },
{ id: "y", label: "end" },
],
edges: [{ source: "x", target: "y" }],
},
])
expect(r.errors).toEqual([])
expect(r.xml).toContain('id="g1__layer')
expect(r.xml).toContain('id="g2"')
})
it("dir=row transposes the flow", () => {
const make = (dir: "col" | "row") =>
restructureDiagram("", [{ op: "add_graph", id: "g", dir, ...FLOW }])
.xml as string
const down = rectOf(make("col"), "g")
const right = rectOf(make("row"), "g")
// Four layers tall vs four layers wide.
expect(down.h).toBeGreaterThan(down.w * 0.8)
expect(right.w).toBeGreaterThan(right.h)
})
feat(diagram-engine): corners, borderless fills, shadows and strikethrough Four more Tailwind classes, all four verified against draw.io's own source in public/drawio rather than against a prose reference — which is how three earlier exclusions turned out to be wrong: rounded-* mxShape.js:1172-1189 — absoluteArcSize=1 switches arcSize to absolute pixels and halves it, so the same class is the same corner on every box. Previously excluded as 'percentage only' shadow-sm..xl mxShape.js:505-535 — getShadowStyle reads five independent params, not one flag, so Tailwind's offset+blur rungs map one to one. Previously excluded as 'six sizes collapse to one' line-through mxConstants.js:2054 FONT_STRIKETHROUGH: 8, read at both mxText.js:723 and :1040. The bitmask has four bits, not three border-none the only one of the four that adds something previously inexpressible: a fill with no outline Also fixes an edge style growing 76 characters per re-layout, without bound. The router recomputes ports on every pass, and appending them to a style recovered from the canvas — which already carried the previous pass's eight port keys — grew the string forever. draw.io resolves duplicates last-wins so the arrow always looked right; a byte-identity check is what caught it. Two traps found while wiring the readback, both the same shape: a value the THEME emits being recorded as one the model asked for. strokeColor=none from a filled or ghost role, and rounded=0 from the fallback style. Either one would outlive a set_role, since that clears style but keeps text. Deliberately not included, with reasons in tw.ts: per-side borders and per-corner radius (both would take the shape slot, and what a node IS matters more than which of its edges show), per-side padding (draw.io's keys pad the label, not the room left for children), text-shadow (a bare flag with no offset or blur), opacity (Tailwind's is any integer, not a scale), tracking/uppercase/leading (absent from draw.io — zero grep hits, not merely coarse). 615 tests, 90 new. Browser-verified: four shadow rungs visibly differ, radius is real pixels, terminator + rounded-lg becomes a small-cornered rounded rect while an untouched terminator stays a stadium.
2026-08-11 09:09:45 +09:00
it("draws the rest of the graph and warns about an unknown edge endpoint", () => {
const r = restructureDiagram("", [
{
op: "add_graph",
id: "g",
nodes: [{ id: "a", label: "a" }],
edges: [{ source: "a", target: "ghost" }],
},
])
feat(diagram-engine): corners, borderless fills, shadows and strikethrough Four more Tailwind classes, all four verified against draw.io's own source in public/drawio rather than against a prose reference — which is how three earlier exclusions turned out to be wrong: rounded-* mxShape.js:1172-1189 — absoluteArcSize=1 switches arcSize to absolute pixels and halves it, so the same class is the same corner on every box. Previously excluded as 'percentage only' shadow-sm..xl mxShape.js:505-535 — getShadowStyle reads five independent params, not one flag, so Tailwind's offset+blur rungs map one to one. Previously excluded as 'six sizes collapse to one' line-through mxConstants.js:2054 FONT_STRIKETHROUGH: 8, read at both mxText.js:723 and :1040. The bitmask has four bits, not three border-none the only one of the four that adds something previously inexpressible: a fill with no outline Also fixes an edge style growing 76 characters per re-layout, without bound. The router recomputes ports on every pass, and appending them to a style recovered from the canvas — which already carried the previous pass's eight port keys — grew the string forever. draw.io resolves duplicates last-wins so the arrow always looked right; a byte-identity check is what caught it. Two traps found while wiring the readback, both the same shape: a value the THEME emits being recorded as one the model asked for. strokeColor=none from a filled or ghost role, and rounded=0 from the fallback style. Either one would outlive a set_role, since that clears style but keeps text. Deliberately not included, with reasons in tw.ts: per-side borders and per-corner radius (both would take the shape slot, and what a node IS matters more than which of its edges show), per-side padding (draw.io's keys pad the label, not the room left for children), text-shadow (a bare flag with no offset or blur), opacity (Tailwind's is any integer, not a scale), tracking/uppercase/leading (absent from draw.io — zero grep hits, not merely coarse). 615 tests, 90 new. Browser-verified: four shadow rungs visibly differ, radius is real pixels, terminator + rounded-lg becomes a small-cornered rounded rect while an untouched terminator stays a stadium.
2026-08-11 09:09:45 +09:00
// A warning, not an error: node "a" is perfectly drawable, and rejecting the whole
// call would cost a turn to arrive back at the same diagram.
expect(r.errors).toEqual([])
expect(r.xml).toBeTruthy()
expect(r.warnings.join(" ")).toContain("ghost")
})
})