From 12f300c8cd4a252d1b83fb9a27f8980843da220b Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 4 Jun 2026 13:35:54 +0200 Subject: [PATCH] test(stammbaum): every unit centre sits within its child-units span (#724) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixture-wide loop over the canonical forest and a synthetic tree: each unit's run centre is within [min, max] of its child-unit centres — the ancestor centring invariant, asserted on real data. Co-Authored-By: Claude Opus 4.8 --- .../genealogy/layout/buildLayout.test.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/frontend/src/lib/person/genealogy/layout/buildLayout.test.ts b/frontend/src/lib/person/genealogy/layout/buildLayout.test.ts index 7923519d..21d5b958 100644 --- a/frontend/src/lib/person/genealogy/layout/buildLayout.test.ts +++ b/frontend/src/lib/person/genealogy/layout/buildLayout.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from 'vitest'; import { buildLayout, NODE_W, NODE_H, COL_GAP, ROW_GAP } from './buildLayout'; +import { buildFamilyForest, type Unit } from './familyForest'; import canonicalFixture from '../__fixtures__/stammbaum.json'; import type { components } from '$lib/generated/api'; @@ -461,3 +462,43 @@ describe('buildLayout — named-bug guard: deep bloodline (#724)', () => { expect(centerX(layout, d)).toBe(mid); }); }); + +// Centre-x of a unit's run, derived from its primary's left edge + run width. +function unitCenter(layout: ReturnType, u: Unit): number { + const left = layout.positions.get(u.id)!.x; + const width = u.members.length * NODE_W + (u.members.length - 1) * COL_GAP; + return left + width / 2; +} + +describe('buildLayout — ancestor centring invariant (#724)', () => { + const fixtureNodes = canonicalFixture.nodes as unknown as PersonNodeDTO[]; + const fixtureEdges = canonicalFixture.edges as unknown as RelationshipDTO[]; + + it('every unit centre sits within its child units span (canonical + synthetic)', () => { + const cases: [string, PersonNodeDTO[], RelationshipDTO[]][] = [ + ['canonical', fixtureNodes, fixtureEdges], + [ + 'synthetic', + [node('R', 'R', 0), node('c1', 'c1', 1), node('c2', 'c2', 1), node('c3', 'c3', 1)], + [parentEdge('R', 'c1'), parentEdge('R', 'c2'), parentEdge('R', 'c3')] + ] + ]; + + for (const [label, nodes, edges] of cases) { + const layout = buildLayout(nodes, edges); + const forest = buildFamilyForest(nodes, edges); + const walk = (u: Unit) => { + if (u.children.length > 0) { + const childCenters = u.children.map((c) => unitCenter(layout, c)); + const lo = Math.min(...childCenters); + const hi = Math.max(...childCenters); + const c = unitCenter(layout, u); + expect(c, `${label}: unit ${u.id} centred in child span`).toBeGreaterThanOrEqual(lo); + expect(c, `${label}: unit ${u.id} centred in child span`).toBeLessThanOrEqual(hi); + } + u.children.forEach(walk); + }; + forest.roots.forEach(walk); + } + }); +});