┌─ TS ───────────────────────────────────────────────────────────────────────┐
│ import { describe, expect, it } from "vitest"; │
│ import { renderChain, type FlowGraph } from "./flow"; │
│ import { checkGrid, len } from "./ascii"; │
│ │
│ const graph = (edges: [string, string, string?][], labels: [string, string][] = [] │
│ ): FlowGraph => ({ │
│ labels: new Map(labels), │
│ edges: edges.map(([from, to, label]) => ({ from, to, label: label ?? "" })), │
│ }); │
│ │
│ describe("renderChain", () => { │
│ it("lays a fitting chain out horizontally with labels over the wires", () => { │
│ expect(renderChain(graph([["a", "b", "go"]]), 40)).toBe( │
│ [ │
│ " go", │
│ "┌───┐ ┌───┐", │
│ "│ a │───>│ b │", │
│ "└───┘ └───┘", │
│ ].join("\n"), │
│ ); │
│ }); │
│ │
│ it("omits the label row when no edge has a label", () => { │
│ expect(renderChain(graph([["a", "b"]]), 40)!.split("\n").length).toBe(3); │
│ }); │
│ │
│ it("falls back to a straight vertical spine when too wide", () => { │
│ expect(renderChain(graph([["alpha", "b", "go"]]), 10)).toBe( │
│ [ │
│ "┌───────┐", │
│ "│ alpha │", │
│ "└───┬───┘", │
│ " │ go", │
│ " ↓", │
│ " ┌─┴─┐", │
│ " │ b │", │
│ " └───┘", │
│ ].join("\n"), │
│ ); │
│ }); │
│ │
│ it("renders vertically when horizontal is not preferred", () => { │
│ const art = renderChain(graph([["a", "b"]]), 80, false)!; │
│ expect(art.split("\n").length).toBeGreaterThan(4); │
│ expect(art).toContain("↓"); │
│ }); │
│ │
│ it("uses display labels and stays on-grid within width", () => { │
│ const art = renderChain( │
│ graph([["md", "rh", "transforms"], ["rh", "fr", "emits"]], [["md", "markdown │
│ "], ["rh", "rehype"], ["fr", "frames"]]), │
│ 86, │
│ )!; │
│ expect(art).toContain("markdown"); │
│ expect(() => checkGrid(art, 86, "t")).not.toThrow(); │
│ for (const line of art.split("\n")) expect(len(line)).toBeLessThanOrEqual(86); │
│ }); │
│ │
│ it("realigns edges authored out of chain order", () => { │
│ const art = renderChain(graph([["b", "c", "second"], ["a", "b", "first"]]), 80 │
│ )!; │
│ const labelRow = art.split("\n")[0]; │
│ expect(labelRow.indexOf("first")).toBeLessThan(labelRow.indexOf("second")); │
│ }); │
│ │
│ it("returns null for branching graphs and cycles", () => { │
│ expect(renderChain(graph([["a", "b"], ["a", "c"]]), 80)).toBeNull(); │
│ expect(renderChain(graph([["a", "b"], ["b", "a"]]), 80)).toBeNull(); │
│ }); │
│ }); │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ TS ─────────────────────────────────┐
│ import { describe, expect, it } from "vitest │
│ "; │
│ import { renderChain, type FlowGraph } from │
│ "./flow"; │
│ import { checkGrid, len } from "./ascii"; │
│ │
│ const graph = (edges: [string, string, strin │
│ g?][], labels: [string, string][] = []): Flo │
│ wGraph => ({ │
│ labels: new Map(labels), │
│ edges: edges.map(([from, to, label]) => ({ │
│ from, to, label: label ?? "" })), │
│ }); │
│ │
│ describe("renderChain", () => { │
│ it("lays a fitting chain out horizontally │
│ with labels over the wires", () => { │
│ expect(renderChain(graph([["a", "b", "go │
│ "]]), 40)).toBe( │
│ [ │
│ " go", │
│ "┌───┐ ┌───┐", │
│ "│ a │───>│ b │", │
│ "└───┘ └───┘", │
│ ].join("\n"), │
│ ); │
│ }); │
│ │
│ it("omits the label row when no edge has a │
│ label", () => { │
│ expect(renderChain(graph([["a", "b"]]), │
│ 40)!.split("\n").length).toBe(3); │
│ }); │
│ │
│ it("falls back to a straight vertical spin │
│ e when too wide", () => { │
│ expect(renderChain(graph([["alpha", "b", │
│ "go"]]), 10)).toBe( │
│ [ │
│ "┌───────┐", │
│ "│ alpha │", │
│ "└───┬───┘", │
│ " │ go", │
│ " ↓", │
│ " ┌─┴─┐", │
│ " │ b │", │
│ " └───┘", │
│ ].join("\n"), │
│ ); │
│ }); │
│ │
│ it("renders vertically when horizontal is │
│ not preferred", () => { │
│ const art = renderChain(graph([["a", "b" │
│ ]]), 80, false)!; │
│ expect(art.split("\n").length).toBeGreat │
│ erThan(4); │
│ expect(art).toContain("↓"); │
│ }); │
│ │
│ it("uses display labels and stays on-grid │
│ within width", () => { │
│ const art = renderChain( │
│ graph([["md", "rh", "transforms"], ["r │
│ h", "fr", "emits"]], [["md", "markdown"], [" │
│ rh", "rehype"], ["fr", "frames"]]), │
│ 86, │
│ )!; │
│ expect(art).toContain("markdown"); │
│ expect(() => checkGrid(art, 86, "t")).no │
│ t.toThrow(); │
│ for (const line of art.split("\n")) expe │
│ ct(len(line)).toBeLessThanOrEqual(86); │
│ }); │
│ │
│ it("realigns edges authored out of chain o │
│ rder", () => { │
│ const art = renderChain(graph([["b", "c" │
│ , "second"], ["a", "b", "first"]]), 80)!; │
│ const labelRow = art.split("\n")[0]; │
│ expect(labelRow.indexOf("first")).toBeLe │
│ ssThan(labelRow.indexOf("second")); │
│ }); │
│ │
│ it("returns null for branching graphs and │
│ cycles", () => { │
│ expect(renderChain(graph([["a", "b"], [" │
│ a", "c"]]), 80)).toBeNull(); │
│ expect(renderChain(graph([["a", "b"], [" │
│ b", "a"]]), 80)).toBeNull(); │
│ }); │
│ }); │
└──────────────────────────────────────────────┘