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); + }); +});