import { expect, test } from "@playwright/test"; import { arrangeSpecialMind, specialNodeRect, specialSubtreeBounds, type SpecialLayoutNode, } from "@/lib/flow-board/mind-special-layout"; const node = ( x: number, y: number, width = 100, height = 40, children: SpecialLayoutNode[] = [] ): SpecialLayoutNode => ({ x, y, width, height, hGap: 0, vGap: 0, children, origin: {}, }); const tree = () => node(0, 0, 120, 60, [ node(200, -100, 100, 40, [ node(400, -150, 180, 40), node(400, -50, 110, 40), ]), node(200, 0, 130, 40), node(200, 100, 200, 40), node(200, 200, 100, 40), ]); test("fishbone places whole subtrees above/below a stable spine without overlap", () => { const root = tree(); arrangeSpecialMind(root, "fishbone"); expect(specialNodeRect(root)).toEqual({ x: 0, y: 0, width: 120, height: 60 }); const a = specialSubtreeBounds(root.children[0]), b = specialSubtreeBounds(root.children[1]), c = specialSubtreeBounds(root.children[2]); expect(a.y + a.height).toBeLessThan(30); expect(b.y).toBeGreaterThan(30); expect(c.x).toBeGreaterThan(a.x + a.width); for (const branch of root.children) expect(specialNodeRect(branch).x).toBeGreaterThan(120); }); test("timeline milestones alternate on distinct increasing positions", () => { const root = tree(); arrangeSpecialMind(root, "timeline"); const rects = root.children.map(specialNodeRect); for (let i = 1; i < rects.length; i++) expect(rects[i].x).toBeGreaterThan(rects[i - 1].x); expect(rects[0].y).toBeLessThan(rects[1].y); expect(rects[2].y).toBeLessThan(rects[3].y); }); test("long titles and descendants reserve column space and brace preserves native tree layout", () => { const root = tree(); root.children[0].children[0].width = 900; arrangeSpecialMind(root, "timeline"); const first = specialSubtreeBounds(root.children[0]); expect(specialNodeRect(root.children[2]).x).toBeGreaterThan( first.x + first.width ); const brace = tree(), before = JSON.stringify(brace); arrangeSpecialMind(brace, "brace"); expect(JSON.stringify(brace)).toBe(before); }); test("root summaries remain outside the packed branch columns", () => { const root = tree(); const summary = node(300, 10, 150, 40); summary.origin = { start: 0, end: 1 }; root.children.push(summary); arrangeSpecialMind(root, "fishbone"); const right = Math.max( ...root.children.slice(0, 4).map((child) => { const rect = specialSubtreeBounds(child); return rect.x + rect.width; }) ); expect(specialNodeRect(summary).x).toBeGreaterThan(right); }); test("vertical timeline alternates whole subtrees, keeps row order and mirrors descendants", () => { const root = tree(); root.children[1].children.push(node(400, 0, 700, 120)); arrangeSpecialMind(root, "verticalTimeline"); const bounds = root.children.map(specialSubtreeBounds); expect(bounds[0].x).toBeGreaterThan(60); expect(bounds[1].x + bounds[1].width).toBeLessThan(60); expect(specialNodeRect(root.children[1].children[0]).x).toBeLessThan( specialNodeRect(root.children[1]).x ); for (let i = 1; i < bounds.length; i++) expect(bounds[i].y).toBeGreaterThan(bounds[i - 1].y + bounds[i - 1].height); expect(specialNodeRect(root)).toEqual({ x: 0, y: 0, width: 120, height: 60 }); }); test("tree table creates aligned columns and lets parents span their descendant rows", () => { const root = tree(); arrangeSpecialMind(root, "treeTable"); const [first, second] = root.children; const [firstChild, secondChild] = first.children; expect(root.tableCell).toMatchObject({ depth: 0, x: 0, y: 0 }); expect(root.tableCell!.width).toBeGreaterThan(root.tableCell!.height); expect(first.tableCell).toMatchObject({ depth: 1, branch: 0 }); expect(first.tableCell!.height).toBe( firstChild.tableCell!.height + secondChild.tableCell!.height ); expect(firstChild.tableCell!.x).toBe(secondChild.tableCell!.x); expect(firstChild.tableCell!.width).toBe(secondChild.tableCell!.width); expect(second.tableCell!.y).toBeGreaterThanOrEqual( secondChild.tableCell!.y + secondChild.tableCell!.height ); expect(specialNodeRect(first).x).toBeGreaterThanOrEqual(first.tableCell!.x); expect(specialNodeRect(first).x + specialNodeRect(first).width).toBeLessThanOrEqual( first.tableCell!.x + first.tableCell!.width ); }); test("side-title tree table spans the root beside every leaf row", () => { const root = tree(); arrangeSpecialMind(root, "treeTableSide"); const [first, second] = root.children; const leaves = root.children.flatMap((branch) => branch.children.length ? branch.children : [branch] ); expect(root.tableCell).toMatchObject({ depth: 0, x: 0, y: 0 }); expect(root.tableCell!.height).toBe(78 * leaves.length); expect(first.tableCell!.x).toBe(root.tableCell!.x + root.tableCell!.width); expect(first.tableCell!.y).toBe(root.tableCell!.y); expect(second.tableCell!.y).toBeGreaterThan(first.tableCell!.y); expect(first.tableCell!.height).toBe(156); }); test("spreadsheet uses XMind columns and stacks entries that share a labelled cell", () => { const mondayA = node(400, 0, 100, 40); mondayA.origin.xmind = { labels: ["Monday"] }; const mondayB = node(400, 50, 100, 40); mondayB.origin.xmind = { labels: ["Monday"] }; const friday = node(400, 100, 100, 40); friday.origin.xmind = { labels: ["Friday"] }; const happy = node(200, 0, 120, 40, [mondayA, mondayB, friday]); const calmTuesday = node(400, 0, 100, 40); calmTuesday.origin.xmind = { labels: ["Tuesday"] }; const calm = node(200, 100, 120, 40, [calmTuesday]); const root = node(0, 0, 150, 60, [happy, calm]); root.origin.spreadsheetColumns = ["Monday", "Tuesday", "Friday"]; arrangeSpecialMind(root, "spreadsheet"); expect(root.tableGrid?.columns).toEqual(["Monday", "Tuesday", "Friday"]); expect(root.tableCell!.width).toBe(root.tableGrid!.width); expect(happy.tableCell!.x).toBe(root.tableGrid!.x); expect(mondayA.tableCell!.x).toBe(mondayB.tableCell!.x); expect(mondayB.tableCell!.y).toBeGreaterThan(mondayA.tableCell!.y); expect(friday.tableCell!.x).toBeGreaterThan(mondayA.tableCell!.x); expect(calmTuesday.tableCell!.x).toBeGreaterThan(mondayA.tableCell!.x); expect(calm.tableCell!.y).toBeGreaterThan( happy.tableCell!.y + happy.tableCell!.height - 1 ); });