Files
familienarchiv/frontend/src/lib/person/genealogy/layout/tidyTree.test.ts
2026-06-04 13:01:06 +02:00

100 lines
3.2 KiB
TypeScript

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<string, number>, 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<string, { depth: number; width: number }> {
const out = new Map<string, { depth: number; width: number }>();
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<string, number>, 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
);
});
});