From 28997fc391589d184090ca327fe9b4578dca06dd Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 4 Jun 2026 13:01:06 +0200 Subject: [PATCH] test(stammbaum): tidyTree nests deep and shallow siblings without overlap (#724) Co-Authored-By: Claude Opus 4.8 --- .../person/genealogy/layout/tidyTree.test.ts | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/frontend/src/lib/person/genealogy/layout/tidyTree.test.ts b/frontend/src/lib/person/genealogy/layout/tidyTree.test.ts index b68d63a4..75f045b8 100644 --- a/frontend/src/lib/person/genealogy/layout/tidyTree.test.ts +++ b/frontend/src/lib/person/genealogy/layout/tidyTree.test.ts @@ -18,6 +18,36 @@ 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'); @@ -45,3 +75,25 @@ describe('tidyTree — ancestor centring', () => { 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 + ); + }); +});