From 7627589844d80c5fe4f5c75f10a65cde91107da4 Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 4 Jun 2026 13:33:59 +0200 Subject: [PATCH] =?UTF-8?q?test(stammbaum):=20named-bug=20guard=20?= =?UTF-8?q?=E2=80=94=20deep-bloodline=20apex=20is=20centred,=20not=20stran?= =?UTF-8?q?ded=20left=20(#724)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A 5-generation single bloodline fanning out wide at the bottom: the apex great-great-grandparent (and every ancestor in the chain) sits at the centre of the descendant span, the exact symptom the old per-generation packer produced in reverse (apex pinned to the left edge). Co-Authored-By: Claude Opus 4.8 --- .../genealogy/layout/buildLayout.test.ts | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/frontend/src/lib/person/genealogy/layout/buildLayout.test.ts b/frontend/src/lib/person/genealogy/layout/buildLayout.test.ts index 75efeb49..7923519d 100644 --- a/frontend/src/lib/person/genealogy/layout/buildLayout.test.ts +++ b/frontend/src/lib/person/genealogy/layout/buildLayout.test.ts @@ -405,3 +405,59 @@ describe('buildLayout — cross-level marriage fallback (#724)', () => { expect(layout.positions.get(B)).toBeDefined(); }); }); + +function centerX(layout: ReturnType, id: string): number { + return layout.positions.get(id)!.x + NODE_W / 2; +} + +describe('buildLayout — named-bug guard: deep bloodline (#724)', () => { + // A 5-generation single bloodline whose deepest generation fans out wide. + // The OLD per-generation packer (now removed) stranded the apex ancestor at + // the LEFT edge of its descendants — the Albert/Martin symptom. The bottom-up + // tidy-tree centres every ancestor over the span of its descendants. + const gg = '00000000-0000-0000-0000-0000000000f0'; // G0 great-great-grandparent + const g = '00000000-0000-0000-0000-0000000000f1'; // G1 + const p = '00000000-0000-0000-0000-0000000000f2'; // G2 + const d = '00000000-0000-0000-0000-0000000000f3'; // G3 + const leaves = ['a', 'b', 'c', 'd', 'e'].map( + (s, i) => `00000000-0000-0000-0000-0000000000${(0xf4 + i).toString(16)}` + ); + + function buildBloodline() { + return buildLayout( + [ + node(gg, 'gg', 0), + node(g, 'g', 1), + node(p, 'p', 2), + node(d, 'd', 3), + ...leaves.map((id, i) => node(id, `leaf-${i}`, 4)) + ], + [ + parentEdge(gg, g, 'e1'), + parentEdge(g, p, 'e2'), + parentEdge(p, d, 'e3'), + ...leaves.map((id, i) => parentEdge(d, id, `el${i}`)) + ] + ); + } + + it('great_great_grandparent_is_not_stranded_left_of_descendants', () => { + const layout = buildBloodline(); + const leafCenters = leaves.map((id) => centerX(layout, id)); + const minLeaf = Math.min(...leafCenters); + const maxLeaf = Math.max(...leafCenters); + const ggX = centerX(layout, gg); + + // The apex ancestor sits strictly inside its descendant span — not pinned + // to the left edge as the old packer left it. + expect(ggX).toBeGreaterThan(minLeaf); + expect(ggX).toBeLessThan(maxLeaf); + // In fact it sits exactly at the centre of the descendant fan-out, and so + // does every ancestor in the chain (single-child chains inherit the centre). + const mid = (minLeaf + maxLeaf) / 2; + expect(ggX).toBe(mid); + expect(centerX(layout, g)).toBe(mid); + expect(centerX(layout, p)).toBe(mid); + expect(centerX(layout, d)).toBe(mid); + }); +});