import { describe, it, expect } from 'vitest'; import { layoutForest, type TidyNode } from './tidyTree'; // tidyTree is domain-agnostic: it lays out abstract { id, width, children } // nodes, so these tests use hand-built trees with no PersonNodeDTO import. const W = 160; const GAP = 40; function leaf(id: string, width = W): TidyNode { return { id, width, children: [] }; } function node(id: string, children: TidyNode[], width = W): TidyNode { return { id, width, children }; } function center(x: Map, n: TidyNode): number { return x.get(n.id)! + n.width / 2; } // Walk a forest, recording each node's tree depth and width. function depths(roots: TidyNode[]): Map { const out = new Map(); const walk = (n: TidyNode, d: number) => { out.set(n.id, { depth: d, width: n.width }); for (const c of n.children) walk(c, d + 1); }; for (const r of roots) walk(r, 0); return out; } // Assert no two boxes at the same depth overlap (clearance >= gap). function expectNoOverlap(x: Map, roots: TidyNode[], gap: number) { const meta = depths(roots); const ids = [...x.keys()]; for (let i = 0; i < ids.length; i++) { for (let j = i + 1; j < ids.length; j++) { const a = meta.get(ids[i])!; const b = meta.get(ids[j])!; if (a.depth !== b.depth) continue; const xa = x.get(ids[i])!; const xb = x.get(ids[j])!; const lo = Math.min(xa, xb); const hi = Math.max(xa, xb); const loW = xa <= xb ? a.width : b.width; expect(hi - lo).toBeGreaterThanOrEqual(loW + gap); } } } describe('tidyTree — leaf base case', () => { it('a single leaf lays out at x = 0', () => { const a = leaf('a'); const x = layoutForest([a], GAP); expect(x.get('a')).toBe(0); }); }); describe('tidyTree — ancestor centring', () => { it('a parent is centred over the span of its two children', () => { const c1 = leaf('c1'); const c2 = leaf('c2'); const p = node('p', [c1, c2]); const x = layoutForest([p], GAP); const pc = center(x, p); const lo = center(x, c1); const hi = center(x, c2); // Parent sits exactly at the midpoint of its children's centres … expect(pc).toBe((lo + hi) / 2); // … which is within their span (the named-bug guard generalises this). expect(pc).toBeGreaterThanOrEqual(Math.min(lo, hi)); expect(pc).toBeLessThanOrEqual(Math.max(lo, hi)); // Children do not overlap. expect(Math.abs(x.get('c2')! - x.get('c1')!)).toBeGreaterThanOrEqual(W + GAP); }); }); describe('tidyTree — contour nesting', () => { it('a deep subtree and a shallow sibling nest without overlap', () => { // root // ├─ a (leaf) // └─ b ─ b1, b2 (deeper) // The contour push must keep b's whole subtree clear of leaf a, and a // clear of b's grandchildren, at every depth. const a = leaf('a'); const b = node('b', [leaf('b1'), leaf('b2')]); const root = node('root', [a, b]); const x = layoutForest([root], GAP); expectNoOverlap(x, [root], GAP); // Each parent still centred over its own children. expect(center(x, b)).toBe( (center(x, { id: 'b1', width: W, children: [] }) + center(x, { id: 'b2', width: W, children: [] })) / 2 ); }); });