import { describe, it, expect } from 'vitest'; import { pickStructuralOwner, buildFamilyForest, type Unit } from './familyForest'; import type { components } from '$lib/generated/api'; type PersonNodeDTO = components['schemas']['PersonNodeDTO']; type RelationshipDTO = components['schemas']['RelationshipDTO']; type Person = { id: string; birthYear?: number }; function person(id: string, opts: { birthYear?: number; generation?: number } = {}): PersonNodeDTO { const n: PersonNodeDTO = { id, displayName: id, familyMember: true }; if (opts.birthYear != null) n.birthYear = opts.birthYear; if (opts.generation != null) n.generation = opts.generation; return n; } function parent(p: string, c: string): RelationshipDTO { return { id: `${p}>${c}`, personId: p, relatedPersonId: c, personDisplayName: '', relatedPersonDisplayName: '', relationType: 'PARENT_OF' }; } function spouse(a: string, b: string, fromYear?: number): RelationshipDTO { return { id: `${a}~${b}`, personId: a, relatedPersonId: b, personDisplayName: '', relatedPersonDisplayName: '', relationType: 'SPOUSE_OF', ...(fromYear != null ? { fromYear } : {}) }; } // Find the unit (anywhere in the forest) whose primary id matches. function findUnit(roots: Unit[], primaryId: string): Unit | undefined { for (const r of roots) { if (r.id === primaryId) return r; const found = findUnit(r.children, primaryId); if (found) return found; } return undefined; } describe('pickStructuralOwner', () => { const p = (id: string, birthYear?: number): Person => ({ id, birthYear }); it('picks the earlier-born spouse as structural owner', () => { expect(pickStructuralOwner(p('a', 1900), p('b', 1920))).toBe('a'); expect(pickStructuralOwner(p('a', 1920), p('b', 1900))).toBe('b'); }); it('sorts a missing birthYear last (the dated spouse owns)', () => { expect(pickStructuralOwner(p('a'), p('b', 1900))).toBe('b'); expect(pickStructuralOwner(p('a', 1900), p('b'))).toBe('a'); }); it('breaks ties on stable id when birth years match or are both missing', () => { expect(pickStructuralOwner(p('zzz', 1900), p('aaa', 1900))).toBe('aaa'); expect(pickStructuralOwner(p('zzz'), p('aaa'))).toBe('aaa'); }); }); describe('buildFamilyForest — loose-spouse absorption', () => { it('absorbs a parentless spouse into the partner run; their child anchors to the couple', () => { // A (founder) ⚭ S (married-in, no parents). Their child C. S has no // ancestor subtree of its own, but C still hangs off the couple. const forest = buildFamilyForest( [ person('A', { birthYear: 1900 }), person('S', { birthYear: 1905 }), person('C', { birthYear: 1930 }) ], [spouse('A', 'S'), parent('A', 'C'), parent('S', 'C')] ); // One root unit (A), with S absorbed — S is not its own root. expect(forest.roots.map((r) => r.id)).toEqual(['A']); const a = findUnit(forest.roots, 'A')!; expect(a.members).toEqual(['A', 'S']); // The child anchors through the couple unit. expect(a.children.map((u) => u.id)).toEqual(['C']); // S is parentless, so no displaced cross-link. expect(forest.crossLinks).toEqual([]); }); it('keeps all marriages of a multi-spouse founder in one run (marriage-year order)', () => { // Albert-like: one founder, three parentless wives. All absorbed into the // founder run, ordered by marriage year NULLS LAST then displayName/id. const forest = buildFamilyForest( [ person('alb', { birthYear: 1829 }), person('w1925', { birthYear: 1900 }), person('wNull', { birthYear: 1901 }), person('w1910', { birthYear: 1902 }) ], [spouse('alb', 'w1925', 1925), spouse('alb', 'wNull'), spouse('alb', 'w1910', 1910)] ); expect(forest.roots.map((r) => r.id)).toEqual(['alb']); const alb = findUnit(forest.roots, 'alb')!; // Founder first, then spouses by marriage year (1910, 1925, null last). expect(alb.members).toEqual(['alb', 'w1910', 'w1925', 'wNull']); }); }); describe('buildFamilyForest — sibling/branch ordering', () => { it('orders children by birthYear ASC, NULLS LAST, then displayName, then id', () => { // Provided out of order; the comparator must reorder to 1910, 1920, then // the two undated by displayName/id (NULLS LAST). const undatedB: PersonNodeDTO = { id: 'u-b', displayName: 'Zoe', familyMember: true }; const undatedA: PersonNodeDTO = { id: 'u-a', displayName: 'Anna', familyMember: true }; const forest = buildFamilyForest( [ person('P', { birthYear: 1880 }), person('c1920', { birthYear: 1920 }), undatedB, person('c1910', { birthYear: 1910 }), undatedA ], [parent('P', 'c1920'), parent('P', 'u-b'), parent('P', 'c1910'), parent('P', 'u-a')] ); const p = findUnit(forest.roots, 'P')!; // 1910, 1920 (dated ASC), then undated by displayName (Anna < Zoe). expect(p.children.map((u) => u.id)).toEqual(['c1910', 'c1920', 'u-a', 'u-b']); }); it('orders roots by the same rule', () => { const forest = buildFamilyForest( [person('late', { birthYear: 1900 }), person('early', { birthYear: 1850 })], [] ); expect(forest.roots.map((r) => r.id)).toEqual(['early', 'late']); }); });