import { describe, expect, it } from 'vitest'; import { LabelView, buildLabelTree, collectAncestorIds, collectSubtreeIds, flattenLabelTree, labelDepth, subtreeHeight, } from './labels'; /** Minimal label row; timestamps are irrelevant to the tree helpers. */ function label(id: string, parentId: string | null, name = id): LabelView { return { id, pondId: 'pond', parentId, name, color: '#64748b', createdAt: '2026-01-01T00:00:00.000Z', updatedAt: '2026-01-01T00:00:00.000Z', }; } /** * Tree used across the cases: * a z * ├─ b └─ y * │ └─ d * └─ c */ const tree: LabelView[] = [ label('a', null, 'Alpha'), label('b', 'a', 'Bravo'), label('c', 'a', 'Charlie'), label('d', 'b', 'Delta'), label('z', null, 'Zulu'), label('y', 'z', 'Yankee'), ]; describe('buildLabelTree (issue #43)', () => { it('nests children under parents and sorts siblings and roots by name', () => { const roots = buildLabelTree(tree); expect(roots.map((r) => r.id)).toEqual(['a', 'z']); // Alpha before Zulu const a = roots.find((r) => r.id === 'a')!; expect(a.children.map((c) => c.id)).toEqual(['b', 'c']); // Bravo before Charlie const b = a.children.find((c) => c.id === 'b')!; expect(b.children.map((c) => c.id)).toEqual(['d']); }); it('treats an orphaned parent reference as a root instead of dropping the row', () => { const roots = buildLabelTree([label('x', 'missing')]); expect(roots.map((r) => r.id)).toEqual(['x']); }); }); describe('flattenLabelTree (issue #44)', () => { it('round-trips buildLabelTree back into a depth-first list', () => { const flat = flattenLabelTree(buildLabelTree(tree)); // Depth-first, siblings by name: a, b, d, c, z, y. expect(flat.map((l) => l.id)).toEqual(['a', 'b', 'd', 'c', 'z', 'y']); // Views carry no `children` field. expect(flat.every((l) => !('children' in l))).toBe(true); }); }); describe('collectSubtreeIds (issue #43)', () => { it('returns the label and all its descendants', () => { expect(collectSubtreeIds(tree, 'a')).toEqual(new Set(['a', 'b', 'c', 'd'])); expect(collectSubtreeIds(tree, 'b')).toEqual(new Set(['b', 'd'])); expect(collectSubtreeIds(tree, 'd')).toEqual(new Set(['d'])); }); it('detects that a new parent inside the subtree would form a cycle', () => { // Moving 'a' under 'd' is illegal: 'd' is in a's subtree. expect(collectSubtreeIds(tree, 'a').has('d')).toBe(true); // Moving 'c' under 'z' is fine: 'z' is not in c's subtree. expect(collectSubtreeIds(tree, 'c').has('z')).toBe(false); }); it('terminates on a malformed cycle in the input', () => { const cyclic = [label('p', 'q'), label('q', 'p')]; expect(collectSubtreeIds(cyclic, 'p')).toEqual(new Set(['p', 'q'])); }); }); describe('collectAncestorIds + labelDepth (issue #43)', () => { it('walks ancestors nearest-first', () => { expect(collectAncestorIds(tree, 'd')).toEqual(['b', 'a']); expect(collectAncestorIds(tree, 'a')).toEqual([]); }); it('reports depth as a 1-based level', () => { expect(labelDepth(tree, 'a')).toBe(1); expect(labelDepth(tree, 'b')).toBe(2); expect(labelDepth(tree, 'd')).toBe(3); }); it('terminates on a malformed cycle', () => { const cyclic = [label('p', 'q'), label('q', 'p')]; expect(collectAncestorIds(cyclic, 'p')).toEqual(['q']); }); }); describe('subtreeHeight (issue #43)', () => { it('measures the deepest branch in levels', () => { expect(subtreeHeight(tree, 'a')).toBe(3); // a → b → d expect(subtreeHeight(tree, 'b')).toBe(2); // b → d expect(subtreeHeight(tree, 'c')).toBe(1); // leaf }); });