import { describe, expect, it } from 'vitest' import { useSampleGroups } from '../../composables/useSampleGroups' import type { SampleGroup } from '../../types' /** * `groupTree` is the full N-layer display model behind the sample selector: * interior nodes are path prefixes, leaves carry the concrete `SampleGroup` * (whose `name` stays the full path, since every mutation keys off it). */ function labelsAtDepth(nodes: ReturnType['groupTree']['value'], depth: number): string[] { let level = nodes for (let i = 0; i < depth; i++) level = level.flatMap(n => n.children) return level.map(n => n.label) } describe('useSampleGroups groupTree', () => { const threeLayer: SampleGroup[] = [ { name: 'Liver / High / 5min', color: '#3B82F6', samples: ['L1', 'L2'] }, { name: 'Liver / High / 10min', color: '#3B82F6', samples: ['L3'] }, { name: 'Liver / Low / 5min', color: '#3B82F6', samples: ['L4'] }, { name: 'Kidney / High / 5min', color: '#F43F5E', samples: ['K1'] }, ] it('should build one level per name segment', () => { const { groupTree } = useSampleGroups({ groups: threeLayer }) expect(labelsAtDepth(groupTree.value, 0)).toEqual(['Liver', 'Kidney']) expect(labelsAtDepth(groupTree.value, 1)).toEqual(['High', 'Low', 'High']) expect(labelsAtDepth(groupTree.value, 2)).toEqual(['5min', '10min', '5min', '5min']) }) it('should label each node with its own segment rather than the full path', () => { const { groupTree } = useSampleGroups({ groups: threeLayer }) expect(groupTree.value[0].children[0].label).toBe('High') }) it('should keep the full path on the leaf group so mutations still resolve it', () => { const { groupTree } = useSampleGroups({ groups: threeLayer }) expect(groupTree.value[0].children[0].children[0].group?.name).toBe('Liver / High / 5min') }) it('should aggregate every descendant sample onto an interior node', () => { const { groupTree } = useSampleGroups({ groups: threeLayer }) expect(groupTree.value[0].allSamples).toEqual(['L1', 'L2', 'L3', 'L4']) expect(groupTree.value[0].children[0].allSamples).toEqual(['L1', 'L2', 'L3']) }) it('should mark only childless nodes as leaves', () => { const { groupTree } = useSampleGroups({ groups: threeLayer }) expect(groupTree.value[0].children).not.toHaveLength(0) expect(groupTree.value[0].group).toBeUndefined() expect(groupTree.value[0].children[0].children[0].children).toHaveLength(0) }) it('should give every node a unique key so expansion state cannot collide', () => { const { groupTree } = useSampleGroups({ groups: threeLayer }) const keys: string[] = [] const walk = (nodes: typeof groupTree.value) => { for (const n of nodes) { keys.push(n.key) walk(n.children) } } walk(groupTree.value) expect(new Set(keys).size).toBe(keys.length) }) it('should keep two-segment names at two levels', () => { const { groupTree } = useSampleGroups({ groups: [{ name: 'Control/A', color: '#3B82F6', samples: ['S1'] }], }) expect(labelsAtDepth(groupTree.value, 0)).toEqual(['Control']) expect(labelsAtDepth(groupTree.value, 1)).toEqual(['A']) expect(labelsAtDepth(groupTree.value, 2)).toEqual([]) }) it('should keep an unsegmented name as a single root leaf', () => { const { groupTree } = useSampleGroups({ groups: [{ name: 'Control', color: '#3B82F6', samples: ['S1'] }], }) expect(groupTree.value).toHaveLength(1) expect(groupTree.value[0].children).toHaveLength(0) expect(groupTree.value[0].group?.name).toBe('Control') }) it('should nest mixed-depth siblings under the same parent', () => { const { groupTree } = useSampleGroups({ groups: [ { name: 'Liver / High / 5min', color: '#3B82F6', samples: ['S1'] }, { name: 'Liver / Low', color: '#3B82F6', samples: ['S2'] }, ], }) expect(labelsAtDepth(groupTree.value, 1)).toEqual(['High', 'Low']) // "Low" is a leaf at depth 1; "High" is interior with a leaf beneath it. expect(groupTree.value[0].children[1].group?.name).toBe('Liver / Low') expect(groupTree.value[0].children[0].children[0].group?.name).toBe('Liver / High / 5min') }) it('should expose a group whose path duplicates an interior node as its own leaf', () => { // "Liver" is both a standalone group and the prefix of another: the // standalone group must stay reachable as a leaf, or its samples vanish. const { groupTree } = useSampleGroups({ groups: [ { name: 'Liver', color: '#3B82F6', samples: ['S1'] }, { name: 'Liver / High', color: '#3B82F6', samples: ['S2'] }, ], }) const allLeafGroups: string[] = [] const walk = (nodes: typeof groupTree.value) => { for (const n of nodes) { if (n.group) allLeafGroups.push(n.group.name) walk(n.children) } } walk(groupTree.value) expect(allLeafGroups.sort()).toEqual(['Liver', 'Liver / High']) }) it('should tolerate whitespace around separators when segmenting', () => { const { groupTree } = useSampleGroups({ groups: [{ name: 'Liver / High', color: '#3B82F6', samples: ['S1'] }], }) expect(groupTree.value[0].label).toBe('Liver') expect(groupTree.value[0].children[0].label).toBe('High') }) }) describe('useSampleGroups hierarchy depth', () => { it('should report the deepest layer count', () => { const { maxDepth } = useSampleGroups({ groups: [ { name: 'Liver / High / 5min', color: '#3B82F6', samples: ['S1'] }, { name: 'Kidney / Low', color: '#F43F5E', samples: ['S2'] }, ], }) expect(maxDepth.value).toBe(3) }) it('should report depth 1 for flat groups', () => { const { maxDepth } = useSampleGroups({ groups: [{ name: 'Control', color: '#3B82F6', samples: ['S1'] }], }) expect(maxDepth.value).toBe(1) }) })