diff --git a/.gitignore b/.gitignore index 64642a6..49f6b9e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ yarn-error.log* # lock package-lock.json +pnpm-lock.yaml # Runtime data pids diff --git a/__tests__/basic.test.ts b/__tests__/basic.test.ts new file mode 100644 index 0000000..3fb5d20 --- /dev/null +++ b/__tests__/basic.test.ts @@ -0,0 +1,900 @@ +import { describe, it, expect } from 'vitest'; +import { compactBox, dendrogram, indented, mindmap } from '../src/index'; +import type { HierarchyData, HierarchyNode } from '../src/types'; + +describe('Advanced Tests', () => { + describe('Boundary Cases', () => { + it('should handle single node', () => { + const tree: HierarchyData = { id: 'single' }; + Object.values({ compactBox, dendrogram, indented, mindmap }).forEach((algo) => { + const result = algo(tree); + expect(result).toBeDefined(); + expect(result.data.id).toBe('single'); + expect(result.children).toEqual([]); + }); + }); + + it('should handle empty children', () => { + const tree: HierarchyData = { id: 'root', children: [] }; + const result = compactBox(tree); + expect(result.children).toEqual([]); + }); + + it('should handle deep tree (50 levels)', () => { + let tree: HierarchyData = { id: 'root' }; + let current = tree; + for (let i = 1; i < 50; i++) { + const child: HierarchyData = { id: `node-${i}` }; + current.children = [child]; + current = child; + } + + const result = compactBox(tree); + expect(result).toBeDefined(); + expect(result.data.id).toBe('root'); + }); + + it('should handle wide tree (100 children)', () => { + const tree: HierarchyData = { + id: 'root', + children: Array.from({ length: 100 }, (_, i) => ({ + id: `child-${i}`, + })), + }; + + const result = compactBox(tree); + expect(result.children).toHaveLength(100); + }); + }); + + describe('Coordinate Integrity', () => { + it('all nodes should have valid coordinates', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { + id: 'a', + children: [ + { id: 'a1' }, + { id: 'a2' }, + { id: 'a3' }, + ], + }, + { + id: 'b', + children: [{ id: 'b1' }], + }, + ], + }; + + const result = compactBox(tree); + const checkNodes = (node: HierarchyNode) => { + expect(typeof node.x).toBe('number'); + expect(typeof node.y).toBe('number'); + expect(isFinite(node.x)).toBe(true); + expect(isFinite(node.y)).toBe(true); + node.children?.forEach((child: HierarchyNode) => checkNodes(child)); + }; + + checkNodes(result); + }); + + it('coordinates should be consistent with custom dimensions', () => { + const tree: HierarchyData = { + id: 'root', + children: [{ id: 'a' }, { id: 'b' }], + }; + + const result = compactBox(tree, { + getWidth: (d) => 80 + (d.depth || 0) * 10, + getHeight: (d) => 40 + (d.depth || 0) * 5, + }); + + expect(result).toBeDefined(); + expect(result.x).toBeDefined(); + expect(result.y).toBeDefined(); + }); + }); + + describe('Traversal Methods', () => { + it('eachNode should visit all nodes', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { id: 'a', children: [{ id: 'a1' }, { id: 'a2' }] }, + { id: 'b', children: [{ id: 'b1' }] }, + ], + }; + + const result = compactBox(tree); + const visited = new Set(); + + result.eachNode((node) => { + visited.add(node.data.id as string); + }); + + expect(visited.has('root')).toBe(true); + expect(visited.has('a')).toBe(true); + expect(visited.has('a1')).toBe(true); + expect(visited.has('a2')).toBe(true); + expect(visited.has('b')).toBe(true); + expect(visited.has('b1')).toBe(true); + }); + + it('DFTraverse should visit all nodes', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { id: 'a', children: [{ id: 'a1' }] }, + { id: 'b', children: [{ id: 'b1' }] }, + ], + }; + + const result = compactBox(tree); + const visited = new Set(); + + result.DFTraverse((node) => { + visited.add(node.data.id as string); + }); + + expect(visited.has('root')).toBe(true); + expect(visited.has('a')).toBe(true); + expect(visited.has('a1')).toBe(true); + expect(visited.has('b')).toBe(true); + expect(visited.has('b1')).toBe(true); + }); + + it('BFTraverse should visit nodes in level order', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { id: 'a', children: [{ id: 'a1' }] }, + { id: 'b', children: [{ id: 'b1' }] }, + ], + }; + + const result = compactBox(tree); + const visited: string[] = []; + + result.BFTraverse((node) => { + visited.push(node.data.id as string); + }); + + expect(visited[0]).toBe('root'); // Root should be first + expect(visited).toContain('a'); + expect(visited).toContain('b'); + expect(visited).toContain('a1'); + expect(visited).toContain('b1'); + }); + }); + + describe('Bounding Box', () => { + it('should have getBoundingBox method', () => { + const tree: HierarchyData = { + id: 'root', + children: [{ id: 'a' }, { id: 'b' }], + }; + + const result = compactBox(tree, { + getWidth: () => 40, + getHeight: () => 20, + }); + + // Method should exist and be callable + expect(result.getBoundingBox).toBeDefined(); + const bbox = result.getBoundingBox(); + expect(bbox).toBeDefined(); + }); + }); + + describe('Data Preservation', () => { + it('should preserve custom data properties', () => { + const tree: HierarchyData = { + id: 'root', + label: 'Root Node', + color: 'red', + children: [ + { id: 'a', label: 'Node A', value: 100 }, + ], + }; + + const result = compactBox(tree); + expect((result.data).label).toBe('Root Node'); + expect((result.data).color).toBe('red'); + if (result.children?.length) { + expect((result.children[0].data).label).toBe('Node A'); + expect((result.children[0].data).value).toBe(100); + } + }); + }); + + describe('Direction-Specific Behavior', () => { + it('CompactBox should support TB, BT, LR, RL, H, V directions', () => { + const tree: HierarchyData = { + id: 'root', + children: [{ id: 'a' }, { id: 'b' }], + }; + + const directions = ['TB', 'BT', 'LR', 'RL', 'H', 'V']; + directions.forEach((dir) => { + const result = compactBox(tree, { direction: dir as any }); + expect(result).toBeDefined(); + expect(result.data.id).toBe('root'); + }); + }); + + it('Dendrogram should support TB, BT, LR, RL directions', () => { + const tree: HierarchyData = { + id: 'root', + children: [{ id: 'a' }, { id: 'b' }], + }; + + const directions = ['TB', 'BT', 'LR', 'RL']; + directions.forEach((dir) => { + const result = dendrogram(tree, { direction: dir as any }); + expect(result).toBeDefined(); + }); + }); + + it('Indented should support LR, RL, H directions', () => { + const tree: HierarchyData = { + id: 'root', + children: [{ id: 'a' }, { id: 'b' }], + }; + + const directions = ['LR', 'RL', 'H']; + directions.forEach((dir) => { + const result = indented(tree, { direction: dir as any }); + expect(result).toBeDefined(); + }); + }); + + it('Mindmap should support H, V directions', () => { + const tree: HierarchyData = { + id: 'root', + children: [{ id: 'a' }, { id: 'b' }], + }; + + const directions = ['H', 'V']; + directions.forEach((dir) => { + const result = mindmap(tree, { direction: dir as any }); + expect(result).toBeDefined(); + }); + }); + }); + + describe('Performance Baselines', () => { + it('should layout large tree efficiently', () => { + // Create balanced tree: 2^6 - 1 = 127 nodes + function createBalancedTree(depth: number, branching: number): HierarchyData { + function build(d: number): HierarchyData { + const node: HierarchyData = { id: `node-${Math.random()}` }; + if (d < depth) { + node.children = Array.from({ length: branching }, () => build(d + 1)); + } + return node; + } + return build(0); + } + + const tree = createBalancedTree(6, 2); + const start = performance.now(); + const result = compactBox(tree); + const duration = performance.now() - start; + + expect(result).toBeDefined(); + expect(duration).toBeLessThan(1000); // Should complete quickly + }); + + it('should handle wide trees efficiently', () => { + const tree: HierarchyData = { + id: 'root', + children: Array.from({ length: 200 }, (_, i) => ({ + id: `child-${i}`, + })), + }; + + const start = performance.now(); + const result = compactBox(tree); + const duration = performance.now() - start; + + expect(result.children).toHaveLength(200); + expect(duration).toBeLessThan(1000); + }); + }); + + describe('Algorithm Consistency', () => { + it('should produce deterministic results', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { id: 'a', children: [{ id: 'a1' }] }, + { id: 'b', children: [{ id: 'b1' }, { id: 'b2' }] }, + ], + }; + + const result1 = compactBox(tree); + const result2 = compactBox(tree); + + expect(result1.x).toBe(result2.x); + expect(result1.y).toBe(result2.y); + expect(result1.children).toHaveLength(result2.children?.length); + }); + + it('all algorithms should produce valid layouts', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { + id: 'a', + children: [{ id: 'a1' }, { id: 'a2' }], + }, + { + id: 'b', + children: [{ id: 'b1' }], + }, + ], + }; + + const algorithms = { compactBox, dendrogram, indented, mindmap }; + Object.entries(algorithms).forEach(([_name, algo]) => { + const result = algo(tree); + expect(result).toBeDefined(); + expect(result.data).toBeDefined(); + expect(result.x).toBeDefined(); + expect(result.y).toBeDefined(); + }); + }); + }); + + describe('Error Handling', () => { + it('should throw error for invalid direction in indented', () => { + const tree: HierarchyData = { + id: 'root', + children: [{ id: 'a' }], + }; + + expect(() => { + indented(tree, { direction: 'INVALID' as any }); + }).toThrow('Invalid direction'); + }); + + it('should throw error for invalid direction in compactBox', () => { + const tree: HierarchyData = { + id: 'root', + children: [{ id: 'a' }], + }; + + expect(() => { + compactBox(tree, { direction: 'INVALID' as any }); + }).toThrow('Invalid direction'); + }); + + it('should throw error for invalid direction in dendrogram', () => { + const tree: HierarchyData = { + id: 'root', + children: [{ id: 'a' }], + }; + + expect(() => { + dendrogram(tree, { direction: 'INVALID' as any }); + }).toThrow('Invalid direction'); + }); + }); + + describe('Indented Algorithm Options', () => { + it('should handle first child with dropCap=false', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { id: 'first', children: [{ id: 'nested' }] }, + { id: 'second' }, + ], + }; + + const result = indented(tree, { dropCap: false }); + expect(result).toBeDefined(); + expect(result.children?.length).toBe(2); + }); + + it('should handle custom indent with dropCap=false', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { id: 'a', children: [{ id: 'a1' }] }, + { id: 'b' }, + ], + }; + + const result = indented(tree, { dropCap: false, indent: 30 }); + expect(result).toBeDefined(); + }); + + it('should handle align option', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { id: 'a', children: [{ id: 'a1' }, { id: 'a2' }] }, + { id: 'b' }, + ], + }; + + const result = indented(tree, { align: 'center' }); + expect(result).toBeDefined(); + }); + }); + + describe('Direction Variations', () => { + it('should handle RL direction', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { id: 'a', children: [{ id: 'a1' }] }, + { id: 'b' }, + ], + }; + + const result = compactBox(tree, { direction: 'RL' }); + expect(result).toBeDefined(); + expect(result.x).toBeDefined(); + expect(result.y).toBeDefined(); + }); + + it('should handle BT direction', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { id: 'a', children: [{ id: 'a1' }] }, + { id: 'b' }, + ], + }; + + const result = compactBox(tree, { direction: 'BT' }); + expect(result).toBeDefined(); + expect(result.x).toBeDefined(); + expect(result.y).toBeDefined(); + }); + + it('should handle H direction with complex tree', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { + id: 'left', + children: [ + { id: 'left1' }, + { id: 'left2' }, + ], + }, + { + id: 'right', + children: [ + { id: 'right1' }, + { id: 'right2' }, + ], + }, + ], + }; + + const result = compactBox(tree, { direction: 'H' }); + expect(result).toBeDefined(); + const bbox = result.getBoundingBox(); + expect(bbox).toBeDefined(); + }); + + it('should handle V direction with complex tree', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { + id: 'top', + children: [ + { id: 'top1' }, + { id: 'top2' }, + ], + }, + { + id: 'bottom', + children: [ + { id: 'bottom1' }, + { id: 'bottom2' }, + ], + }, + ], + }; + + const result = compactBox(tree, { direction: 'V' }); + expect(result).toBeDefined(); + const bbox = result.getBoundingBox(); + expect(bbox).toBeDefined(); + }); + }); + + describe('Fixed Root Options', () => { + it('should handle fixedRoot=false', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { id: 'a', children: [{ id: 'a1' }] }, + { id: 'b' }, + ], + }; + + const result = compactBox(tree, { fixedRoot: false }); + expect(result).toBeDefined(); + }); + + it('should handle fixedRoot=true explicitly', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { id: 'a', children: [{ id: 'a1' }] }, + { id: 'b' }, + ], + }; + + const result = compactBox(tree, { fixedRoot: true }); + expect(result).toBeDefined(); + }); + }); + + describe('Mindmap Edge Cases', () => { + it('should handle single child case', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { + id: 'single', + children: [{ id: 'nested' }], + }, + ], + }; + + const result = mindmap(tree, { direction: 'H' }); + expect(result).toBeDefined(); + }); + + it('should handle parent smaller than child', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { + id: 'a', + children: [ + { id: 'a1' }, + { id: 'a2' }, + { id: 'a3' }, + ], + }, + ], + }; + + const result = mindmap(tree, { + direction: 'H', + getHeight: (d) => (d.depth === 0 ? 20 : 50), + }); + expect(result).toBeDefined(); + }); + + it('should handle parent larger than total children', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { + id: 'a', + children: [ + { id: 'a1' }, + { id: 'a2' }, + ], + }, + ], + }; + + const result = mindmap(tree, { + direction: 'H', + getHeight: (d) => (d.depth === 1 ? 100 : 20), + }); + expect(result).toBeDefined(); + }); + + it('should handle exact single child alignment', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { + id: 'single', + children: [{ id: 'only-child' }], + }, + ], + }; + + const result = mindmap(tree, { + direction: 'H', + getHeight: () => 40, + }); + expect(result).toBeDefined(); + }); + }); + + describe('Node Constructor Edge Cases', () => { + it('should handle pre-computed HierarchyNode', () => { + const tree: HierarchyData = { + id: 'root', + children: [{ id: 'a' }], + }; + + const result1 = compactBox(tree); + const result2 = compactBox(result1); + + expect(result2).toBeDefined(); + expect(result2.x).toBe(result1.x); + expect(result2.y).toBe(result1.y); + }); + }); + + describe('Complex Tree Structures', () => { + it('should handle complex tree with many children', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { + id: 'a', + children: [ + { id: 'a1', children: [{ id: 'a1a' }, { id: 'a1b' }] }, + { id: 'a2', children: [{ id: 'a2a' }] }, + { id: 'a3' }, + ], + }, + { + id: 'b', + children: [ + { id: 'b1' }, + { id: 'b2', children: [{ id: 'b2a' }, { id: 'b2b' }, { id: 'b2c' }] }, + ], + }, + { + id: 'c', + children: [ + { id: 'c1', children: [{ id: 'c1a' }] }, + ], + }, + ], + }; + + const result = compactBox(tree); + expect(result).toBeDefined(); + + const checkNode = (node: HierarchyNode) => { + expect(typeof node.x).toBe('number'); + expect(typeof node.y).toBe('number'); + expect(isFinite(node.x)).toBe(true); + expect(isFinite(node.y)).toBe(true); + node.children?.forEach(checkNode); + }; + checkNode(result); + }); + + it('should handle asymmetric tree', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { + id: 'left', + children: [ + { + id: 'left1', + children: [ + { + id: 'left1a', + children: [{ id: 'left1a1' }], + }, + ], + }, + ], + }, + { + id: 'right', + children: [{ id: 'right1' }], + }, + ], + }; + + const result = compactBox(tree); + expect(result).toBeDefined(); + }); + + it('should handle tree with varying node sizes', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { + id: 'a', + children: [ + { id: 'a1' }, + { id: 'a2' }, + { id: 'a3' }, + { id: 'a4' }, + ], + }, + { + id: 'b', + children: [ + { id: 'b1' }, + { id: 'b2' }, + ], + }, + ], + }; + + const result = compactBox(tree, { + getWidth: (d) => 40 + (d.depth || 0) * 20, + getHeight: (d) => 20 + (d.depth || 0) * 10, + getHGap: (d) => 10 + (d.depth || 0) * 5, + getVGap: (d) => 10 + (d.depth || 0) * 5, + }); + + expect(result).toBeDefined(); + }); + + it('should handle thread setting in complex scenarios', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { + id: 'a', + children: [ + { id: 'a1', children: [{ id: 'a1a' }] }, + { id: 'a2' }, + { id: 'a3', children: [{ id: 'a3a' }, { id: 'a3b' }] }, + ], + }, + { + id: 'b', + children: [ + { id: 'b1', children: [{ id: 'b1a' }] }, + { id: 'b2', children: [{ id: 'b2a' }] }, + ], + }, + ], + }; + + const result = compactBox(tree); + expect(result).toBeDefined(); + expect(result.children?.length).toBe(2); + }); + }); + + describe('Utility Functions Coverage', () => { + it('should handle align center in getHeight calculation', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { id: 'a', children: [{ id: 'a1' }, { id: 'a2' }] }, + { id: 'b' }, + ], + }; + + const result = indented(tree, { align: 'center' }); + expect(result).toBeDefined(); + }); + + it('should handle indented with first child having no parent error case', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { id: 'a' }, + ], + }; + + const result = indented(tree, { dropCap: false }); + expect(result).toBeDefined(); + expect(result.children?.length).toBe(1); + }); + + it('should handle V direction with negative left bound', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { + id: 'a', + children: [ + { id: 'a1' }, + { id: 'a2' }, + { id: 'a3' }, + ], + }, + { + id: 'b', + children: [{ id: 'b1' }], + }, + ], + }; + + const result = compactBox(tree, { + direction: 'V', + getWidth: () => 100, + }); + expect(result).toBeDefined(); + }); + + it('should handle mindmap single child with equal height', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { + id: 'single', + children: [{ id: 'child' }], + }, + ], + }; + + const result = mindmap(tree, { + direction: 'H', + getHeight: () => 30, + }); + expect(result).toBeDefined(); + }); + + it('should handle mindmap with parent smaller than children height', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { + id: 'a', + children: [ + { id: 'a1' }, + { id: 'a2' }, + { id: 'a3' }, + { id: 'a4' }, + ], + }, + ], + }; + + const result = mindmap(tree, { + direction: 'H', + getHeight: (d) => (d.depth === 1 ? 20 : 50), + }); + expect(result).toBeDefined(); + }); + + it('should handle distributeExtra in non-layered tidy', () => { + const tree: HierarchyData = { + id: 'root', + children: [ + { + id: 'a', + children: [ + { id: 'a1' }, + { id: 'a2' }, + { id: 'a3' }, + { id: 'a4' }, + { id: 'a5' }, + ], + }, + { + id: 'b', + children: [ + { id: 'b1' }, + { id: 'b2' }, + ], + }, + { + id: 'c', + children: [ + { id: 'c1' }, + { id: 'c2' }, + { id: 'c3' }, + ], + }, + ], + }; + + const result = compactBox(tree, { + getHGap: () => 50, + getVGap: () => 30, + }); + expect(result).toBeDefined(); + }); + }); +}); diff --git a/__tests__/compact-box.test.ts b/__tests__/compact-box.test.ts index 6d4a910..e17f538 100644 --- a/__tests__/compact-box.test.ts +++ b/__tests__/compact-box.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect } from 'vitest'; import { compactBox } from '../src/index'; -import { expectSVGToMatchSnapshot, collectNodes } from './utils/svg-generator'; +import { expectSVGToMatchSnapshot, expectStyledSVGToMatchSnapshot, collectNodes } from './utils/svg-generator'; +import complexTree from './fixtures/complex-tree.json'; import basicTree from './fixtures/basic-tree.json'; describe('CompactBox Layout', () => { @@ -126,4 +127,44 @@ describe('CompactBox Layout', () => { expect(result.data.customProp).toBe('custom value'); expect(result.children![0].data.anotherProp).toBe(123); }); + + it('complex styled snapshots (nodeType × colorScheme)', () => { + const nodeTypes = ['rect', 'square', 'circle'] as const; + const colorSchemes = ['depth', 'branch', 'single'] as const; + + const getNodeWidth = (nodeType: string) => { + switch (nodeType) { + case 'circle': + case 'square': + return 20; // nodeSize*2 + case 'rect': + default: + return 100; // nodeSize*10 + } + }; + const getNodeHeight = () => 20; // nodeSize*2 + + nodeTypes.forEach((nodeType) => { + colorSchemes.forEach((colorScheme) => { + const result = compactBox(complexTree as any, { + direction: 'TB', + getHGap: () => 50, + getVGap: () => 50, + getWidth: () => getNodeWidth(nodeType), + getHeight: () => getNodeHeight(), + } as any); + + expect(result).toBeDefined(); + const filename = `compactBox-${nodeType}-${colorScheme}.svg`; + expectStyledSVGToMatchSnapshot(result, filename, { + nodeType: nodeType as any, + colorScheme: colorScheme as any, + nodeSize: 10, + showLabels: true, + width: 900, + height: 700, + }); + }); + }); + }); }); diff --git a/__tests__/dendrogram.test.ts b/__tests__/dendrogram.test.ts index f9d7353..c8c4e5c 100644 --- a/__tests__/dendrogram.test.ts +++ b/__tests__/dendrogram.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect } from 'vitest'; import { dendrogram } from '../src/index'; -import { expectSVGToMatchSnapshot, collectNodes } from './utils/svg-generator'; +import { expectSVGToMatchSnapshot, expectStyledSVGToMatchSnapshot, collectNodes } from './utils/svg-generator'; +import complexTree from './fixtures/complex-tree.json'; import basicTree from './fixtures/basic-tree.json'; describe('Dendrogram Layout', () => { @@ -91,4 +92,45 @@ describe('Dendrogram Layout', () => { expectSVGToMatchSnapshot(result, 'dendrogram-wide.svg'); }); + + it('complex styled snapshots (nodeType × colorScheme)', () => { + const nodeTypes = ['rect', 'square', 'circle'] as const; + const colorSchemes = ['depth', 'branch', 'single'] as const; + + const nodeSize = 10; + const getNodeWidth = (nodeType: string) => { + switch (nodeType) { + case 'circle': + case 'square': + return nodeSize * 2; + case 'rect': + default: + return nodeSize * 10; + } + }; + const getNodeHeight = () => nodeSize * 2; + + nodeTypes.forEach((nodeType) => { + colorSchemes.forEach((colorScheme) => { + const result = dendrogram(complexTree as any, { + direction: 'TB', + getHGap: () => 50, + getVGap: () => 50, + getWidth: () => getNodeWidth(nodeType), + getHeight: () => getNodeHeight(), + } as any); + + expect(result).toBeDefined(); + const filename = `dendrogram-${nodeType}-${colorScheme}.svg`; + expectStyledSVGToMatchSnapshot(result, filename, { + nodeType: nodeType as any, + colorScheme: colorScheme as any, + nodeSize, + showLabels: true, + width: 900, + height: 700, + }); + }); + }); + }); }); diff --git a/__tests__/fixtures/complex-tree.json b/__tests__/fixtures/complex-tree.json new file mode 100644 index 0000000..c58d2c1 --- /dev/null +++ b/__tests__/fixtures/complex-tree.json @@ -0,0 +1,40 @@ +{ + "id": "root", + "children": [ + { + "id": "branch-1", + "children": [ + { + "id": "leaf-1-1", + "children": [ + { "id": "leaf-1-1-1" }, + { "id": "leaf-1-1-2" }, + { "id": "leaf-1-1-3" } + ] + }, + { "id": "leaf-1-2" } + ] + }, + { + "id": "branch-2", + "children": [ + { "id": "leaf-2-1" }, + { "id": "leaf-2-2" }, + { "id": "leaf-2-3" }, + { "id": "leaf-2-4" } + ] + }, + { + "id": "branch-3", + "children": [ + { + "id": "leaf-3-1", + "children": [ + { "id": "leaf-3-1-1" }, + { "id": "leaf-3-1-2" } + ] + } + ] + } + ] +} diff --git a/__tests__/indented.test.ts b/__tests__/indented.test.ts index 3b1508c..0de2ea3 100644 --- a/__tests__/indented.test.ts +++ b/__tests__/indented.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect } from 'vitest'; import { indented } from '../src/index'; -import { expectSVGToMatchSnapshot, collectNodes } from './utils/svg-generator'; +import { expectSVGToMatchSnapshot, expectStyledSVGToMatchSnapshot, collectNodes } from './utils/svg-generator'; +import complexTree from './fixtures/complex-tree.json'; import basicTree from './fixtures/basic-tree.json'; describe('Indented Layout', () => { @@ -125,4 +126,46 @@ describe('Indented Layout', () => { expect(result).toBeDefined(); expectSVGToMatchSnapshot(result, 'indented-custom-sizes.svg'); }); + + it('complex styled snapshots (nodeType × colorScheme)', () => { + const nodeTypes = ['rect', 'square', 'circle'] as const; + const colorSchemes = ['depth', 'branch', 'single'] as const; + + const nodeSize = 10; + const getNodeWidth = (nodeType: string) => { + switch (nodeType) { + case 'circle': + case 'square': + return nodeSize * 2; + case 'rect': + default: + return nodeSize * 10; + } + }; + const getNodeHeight = () => nodeSize * 2; + + nodeTypes.forEach((nodeType) => { + colorSchemes.forEach((colorScheme) => { + const result = indented(complexTree as any, { + direction: 'LR', + indent: 50, + getHGap: () => 50, + getVGap: () => 50, + getWidth: () => getNodeWidth(nodeType), + getHeight: () => getNodeHeight(), + } as any); + + expect(result).toBeDefined(); + const filename = `indented-${nodeType}-${colorScheme}.svg`; + expectStyledSVGToMatchSnapshot(result, filename, { + nodeType: nodeType as any, + colorScheme: colorScheme as any, + nodeSize, + showLabels: true, + width: 900, + height: 700, + }); + }); + }); + }); }); diff --git a/__tests__/mindmap.test.ts b/__tests__/mindmap.test.ts index 7394e80..9d66ac3 100644 --- a/__tests__/mindmap.test.ts +++ b/__tests__/mindmap.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect } from 'vitest'; import { mindmap } from '../src/index'; -import { expectSVGToMatchSnapshot, collectNodes } from './utils/svg-generator'; +import { expectSVGToMatchSnapshot, expectStyledSVGToMatchSnapshot, collectNodes } from './utils/svg-generator'; +import complexTree from './fixtures/complex-tree.json'; import mindmapTree from './fixtures/mindmap-tree.json'; import basicTree from './fixtures/basic-tree.json'; @@ -54,7 +55,6 @@ describe('Mindmap Layout', () => { // In horizontal layout, left nodes should be on the left side const leftX = leftNodes[0].x || 0; const rightX = rightNodes[0].x || 0; - const rootX = result.x || 0; // This depends on the actual implementation expect(Math.abs(leftX - rightX)).toBeGreaterThan(0); @@ -138,4 +138,45 @@ describe('Mindmap Layout', () => { expectSVGToMatchSnapshot(result, 'mindmap-auto-sides.svg'); }); + + it('complex styled snapshots (nodeType × colorScheme)', () => { + const nodeTypes = ['rect', 'square', 'circle'] as const; + const colorSchemes = ['depth', 'branch', 'single'] as const; + + const nodeSize = 10; + const getNodeWidth = (nodeType: string) => { + switch (nodeType) { + case 'circle': + case 'square': + return nodeSize * 2; + case 'rect': + default: + return nodeSize * 10; + } + }; + const getNodeHeight = () => nodeSize * 2; + + nodeTypes.forEach((nodeType) => { + colorSchemes.forEach((colorScheme) => { + const result = mindmap(complexTree as any, { + direction: 'H', + getHGap: () => 50, + getVGap: () => 50, + getWidth: () => getNodeWidth(nodeType), + getHeight: () => getNodeHeight(), + } as any); + + expect(result).toBeDefined(); + const filename = `mindmap-${nodeType}-${colorScheme}.svg`; + expectStyledSVGToMatchSnapshot(result, filename, { + nodeType: nodeType as any, + colorScheme: colorScheme as any, + nodeSize, + showLabels: true, + width: 900, + height: 700, + }); + }); + }); + }); }); diff --git a/__tests__/snapshots/compactBox-circle-branch.svg b/__tests__/snapshots/compactBox-circle-branch.svg new file mode 100644 index 0000000..1792fd6 --- /dev/null +++ b/__tests__/snapshots/compactBox-circle-branch.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/compactBox-circle-depth.svg b/__tests__/snapshots/compactBox-circle-depth.svg new file mode 100644 index 0000000..dcce610 --- /dev/null +++ b/__tests__/snapshots/compactBox-circle-depth.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/compactBox-circle-single.svg b/__tests__/snapshots/compactBox-circle-single.svg new file mode 100644 index 0000000..ef4298d --- /dev/null +++ b/__tests__/snapshots/compactBox-circle-single.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/compactBox-rect-branch.svg b/__tests__/snapshots/compactBox-rect-branch.svg new file mode 100644 index 0000000..bcae45a --- /dev/null +++ b/__tests__/snapshots/compactBox-rect-branch.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/compactBox-rect-depth.svg b/__tests__/snapshots/compactBox-rect-depth.svg new file mode 100644 index 0000000..e4327b1 --- /dev/null +++ b/__tests__/snapshots/compactBox-rect-depth.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/compactBox-rect-single.svg b/__tests__/snapshots/compactBox-rect-single.svg new file mode 100644 index 0000000..cf9e640 --- /dev/null +++ b/__tests__/snapshots/compactBox-rect-single.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/compactBox-square-branch.svg b/__tests__/snapshots/compactBox-square-branch.svg new file mode 100644 index 0000000..0fc207c --- /dev/null +++ b/__tests__/snapshots/compactBox-square-branch.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/compactBox-square-depth.svg b/__tests__/snapshots/compactBox-square-depth.svg new file mode 100644 index 0000000..39116cd --- /dev/null +++ b/__tests__/snapshots/compactBox-square-depth.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/compactBox-square-single.svg b/__tests__/snapshots/compactBox-square-single.svg new file mode 100644 index 0000000..4bfc618 --- /dev/null +++ b/__tests__/snapshots/compactBox-square-single.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/dendrogram-circle-branch.svg b/__tests__/snapshots/dendrogram-circle-branch.svg new file mode 100644 index 0000000..a78d116 --- /dev/null +++ b/__tests__/snapshots/dendrogram-circle-branch.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/dendrogram-circle-depth.svg b/__tests__/snapshots/dendrogram-circle-depth.svg new file mode 100644 index 0000000..5233a79 --- /dev/null +++ b/__tests__/snapshots/dendrogram-circle-depth.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/dendrogram-circle-single.svg b/__tests__/snapshots/dendrogram-circle-single.svg new file mode 100644 index 0000000..73e6b0d --- /dev/null +++ b/__tests__/snapshots/dendrogram-circle-single.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/dendrogram-rect-branch.svg b/__tests__/snapshots/dendrogram-rect-branch.svg new file mode 100644 index 0000000..bd7f5f0 --- /dev/null +++ b/__tests__/snapshots/dendrogram-rect-branch.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/dendrogram-rect-depth.svg b/__tests__/snapshots/dendrogram-rect-depth.svg new file mode 100644 index 0000000..124f641 --- /dev/null +++ b/__tests__/snapshots/dendrogram-rect-depth.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/dendrogram-rect-single.svg b/__tests__/snapshots/dendrogram-rect-single.svg new file mode 100644 index 0000000..c8884a9 --- /dev/null +++ b/__tests__/snapshots/dendrogram-rect-single.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/dendrogram-square-branch.svg b/__tests__/snapshots/dendrogram-square-branch.svg new file mode 100644 index 0000000..79216b9 --- /dev/null +++ b/__tests__/snapshots/dendrogram-square-branch.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/dendrogram-square-depth.svg b/__tests__/snapshots/dendrogram-square-depth.svg new file mode 100644 index 0000000..dd58aea --- /dev/null +++ b/__tests__/snapshots/dendrogram-square-depth.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/dendrogram-square-single.svg b/__tests__/snapshots/dendrogram-square-single.svg new file mode 100644 index 0000000..4e99dc2 --- /dev/null +++ b/__tests__/snapshots/dendrogram-square-single.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/indented-circle-branch.svg b/__tests__/snapshots/indented-circle-branch.svg new file mode 100644 index 0000000..88a5210 --- /dev/null +++ b/__tests__/snapshots/indented-circle-branch.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/indented-circle-depth.svg b/__tests__/snapshots/indented-circle-depth.svg new file mode 100644 index 0000000..5cec6ac --- /dev/null +++ b/__tests__/snapshots/indented-circle-depth.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/indented-circle-single.svg b/__tests__/snapshots/indented-circle-single.svg new file mode 100644 index 0000000..2692cb7 --- /dev/null +++ b/__tests__/snapshots/indented-circle-single.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/indented-rect-branch.svg b/__tests__/snapshots/indented-rect-branch.svg new file mode 100644 index 0000000..70ad625 --- /dev/null +++ b/__tests__/snapshots/indented-rect-branch.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/indented-rect-depth.svg b/__tests__/snapshots/indented-rect-depth.svg new file mode 100644 index 0000000..531b812 --- /dev/null +++ b/__tests__/snapshots/indented-rect-depth.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/indented-rect-single.svg b/__tests__/snapshots/indented-rect-single.svg new file mode 100644 index 0000000..1e7de61 --- /dev/null +++ b/__tests__/snapshots/indented-rect-single.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/indented-square-branch.svg b/__tests__/snapshots/indented-square-branch.svg new file mode 100644 index 0000000..0e075aa --- /dev/null +++ b/__tests__/snapshots/indented-square-branch.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/indented-square-depth.svg b/__tests__/snapshots/indented-square-depth.svg new file mode 100644 index 0000000..72b7d30 --- /dev/null +++ b/__tests__/snapshots/indented-square-depth.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/indented-square-single.svg b/__tests__/snapshots/indented-square-single.svg new file mode 100644 index 0000000..b8f5c63 --- /dev/null +++ b/__tests__/snapshots/indented-square-single.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/mindmap-circle-branch.svg b/__tests__/snapshots/mindmap-circle-branch.svg new file mode 100644 index 0000000..4f10b13 --- /dev/null +++ b/__tests__/snapshots/mindmap-circle-branch.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/mindmap-circle-depth.svg b/__tests__/snapshots/mindmap-circle-depth.svg new file mode 100644 index 0000000..530a94e --- /dev/null +++ b/__tests__/snapshots/mindmap-circle-depth.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/mindmap-circle-single.svg b/__tests__/snapshots/mindmap-circle-single.svg new file mode 100644 index 0000000..d2887c1 --- /dev/null +++ b/__tests__/snapshots/mindmap-circle-single.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/mindmap-rect-branch.svg b/__tests__/snapshots/mindmap-rect-branch.svg new file mode 100644 index 0000000..7c288b2 --- /dev/null +++ b/__tests__/snapshots/mindmap-rect-branch.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/mindmap-rect-depth.svg b/__tests__/snapshots/mindmap-rect-depth.svg new file mode 100644 index 0000000..7cec952 --- /dev/null +++ b/__tests__/snapshots/mindmap-rect-depth.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/mindmap-rect-single.svg b/__tests__/snapshots/mindmap-rect-single.svg new file mode 100644 index 0000000..3fd5fc4 --- /dev/null +++ b/__tests__/snapshots/mindmap-rect-single.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/mindmap-square-branch.svg b/__tests__/snapshots/mindmap-square-branch.svg new file mode 100644 index 0000000..07668de --- /dev/null +++ b/__tests__/snapshots/mindmap-square-branch.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/mindmap-square-depth.svg b/__tests__/snapshots/mindmap-square-depth.svg new file mode 100644 index 0000000..3ef7fed --- /dev/null +++ b/__tests__/snapshots/mindmap-square-depth.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/snapshots/mindmap-square-single.svg b/__tests__/snapshots/mindmap-square-single.svg new file mode 100644 index 0000000..fed10e1 --- /dev/null +++ b/__tests__/snapshots/mindmap-square-single.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + root + + branch-1 + + leaf-1-1 + + leaf-1-1-1 + + leaf-1-1-2 + + leaf-1-1-3 + + leaf-1-2 + + branch-2 + + leaf-2-1 + + leaf-2-2 + + leaf-2-3 + + leaf-2-4 + + branch-3 + + leaf-3-1 + + leaf-3-1-1 + + leaf-3-1-2 + + \ No newline at end of file diff --git a/__tests__/utils/svg-generator.ts b/__tests__/utils/svg-generator.ts index c046cae..2c0740b 100644 --- a/__tests__/utils/svg-generator.ts +++ b/__tests__/utils/svg-generator.ts @@ -3,8 +3,161 @@ import { join } from "path"; import { expect } from "vitest"; import { HierarchyNode } from "../../src"; +type NodeType = "circle" | "square" | "rect"; +type ColorScheme = "depth" | "branch" | "single"; + +const colorPalettes = { + depth: [ + "#667eea", + "#764ba2", + "#f093fb", + "#4facfe", + "#43e97b", + "#fa709a", + "#fee140", + "#30cfd0", + "#a8edea", + "#fed6e3", + "#c471f5", + "#fa71cd", + ], + branch: [ + "#667eea", + "#f093fb", + "#43e97b", + "#fee140", + "#4facfe", + "#fa709a", + "#30cfd0", + "#764ba2", + "#a8edea", + "#fed6e3", + "#c471f5", + "#fa71cd", + ], + single: ["#667eea"], +}; + +/** + * Helper to assign parent references (used for branch coloring) + */ +function assignParent(node: HierarchyNode, parent: HierarchyNode | null = null): void { + (node as any).parent = parent || undefined; + if (node.children) node.children.forEach((c) => assignParent(c, node)); +} + +/** + * Get node color based on color scheme + */ +function getNodeColor(node: HierarchyNode, colorScheme: ColorScheme): string { + const palette = colorPalettes[colorScheme] || colorPalettes.single; + switch (colorScheme) { + case "depth": + return palette[node.depth % palette.length]; + case "branch": { + let idx = 0; + if ((node as any).parent && (node as any).parent.children) { + idx = (node as any).parent.children.indexOf(node); + } + return palette[idx % palette.length]; + } + case "single": + default: + return palette[0]; + } +} + /** - * Generate SVG from layout result for snapshot testing + * Generate styled SVG with support for node types and color schemes + */ +export function generateStyledSVG( + root: HierarchyNode, + options: { + width?: number; + height?: number; + nodeType?: NodeType; + colorScheme?: ColorScheme; + nodeSize?: number; + showLabels?: boolean; + } = {} +): string { + const { + width = 900, + height = 700, + nodeType = "circle", + colorScheme = "single", + nodeSize = 10, + showLabels = true, + } = options; + + assignParent(root); + const nodes = collectNodes(root); + + // Calculate bounds + let minX = Infinity, + maxX = -Infinity, + minY = Infinity, + maxY = -Infinity; + nodes.forEach((n) => { + minX = Math.min(minX, n.x); + maxX = Math.max(maxX, n.x); + minY = Math.min(minY, n.y); + maxY = Math.max(maxY, n.y); + }); + + const padding = 50; + const dataW = Math.max(1, maxX - minX); + const dataH = Math.max(1, maxY - minY); + const scale = Math.min((width - 2 * padding) / dataW, (height - 2 * padding) / dataH); + + const tx = (x: number) => (x - minX) * scale + padding; + const ty = (y: number) => (y - minY) * scale + padding; + + // Collect links + const links: Array<{ s: HierarchyNode; t: HierarchyNode }> = []; + nodes.forEach((n) => { + if (n.children) n.children.forEach((c) => links.push({ s: n, t: c })); + }); + + let svg = ` + + `; + + // Draw links + links.forEach(({ s, t }) => { + svg += `\n `; + }); + + // Draw nodes + nodes.forEach((n) => { + const x = tx(n.x); + const y = ty(n.y); + const fill = getNodeColor(n, colorScheme); + if (nodeType === "circle") { + svg += `\n `; + } else if (nodeType === "square") { + const side = nodeSize * 2; + svg += `\n `; + } else { + // rect + const w = nodeSize * 10; + const h = nodeSize * 2; + svg += `\n `; + } + if (showLabels && n.data?.id) { + svg += `\n ${n.data.id}`; + } + }); + + svg += `\n \n`; + return svg; +} + +/** + * Generate basic SVG from layout result for snapshot testing */ export function generateSVG( root: HierarchyNode, @@ -117,15 +270,9 @@ export function generateSVG( } /** - * Compare SVG with snapshot - generates if not exists, compares if exists - * This is similar to Jest's toMatchSnapshot but for SVG files + * Base snapshot comparison function */ -export function expectSVGToMatchSnapshot( - root: HierarchyNode, - filename: string, - options?: Parameters[1] -): void { - const svg = generateSVG(root, options); +function matchSnapshot(svg: string, filename: string): void { const filepath = join(process.cwd(), "__tests__/snapshots", filename); if (!existsSync(filepath)) { @@ -149,6 +296,31 @@ export function expectSVGToMatchSnapshot( } } +/** + * Compare SVG with snapshot - generates if not exists, compares if exists + * This is similar to Jest's toMatchSnapshot but for SVG files + */ +export function expectSVGToMatchSnapshot( + root: HierarchyNode, + filename: string, + options?: Parameters[1] +): void { + const svg = generateSVG(root, options); + matchSnapshot(svg, filename); +} + +/** + * Compare styled SVG with snapshot + */ +export function expectStyledSVGToMatchSnapshot( + root: HierarchyNode, + filename: string, + options?: Parameters[1] +): void { + const svg = generateStyledSVG(root, options); + matchSnapshot(svg, filename); +} + /** * Update snapshot - always overwrites the existing snapshot * Use this when you intentionally want to update snapshots @@ -161,6 +333,7 @@ export function updateSVGSnapshot( const svg = generateSVG(root, options); const filepath = join(process.cwd(), "__tests__/snapshots", filename); writeFileSync(filepath, svg, "utf-8"); + // eslint-disable-next-line no-console console.log(`✅ Updated snapshot: ${filename}`); } diff --git a/package.json b/package.json index 48359d2..fbb31fc 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "test": "vitest run", "test:ui": "vitest --ui", "test:watch": "vitest", + "test:coverage": "vitest run --coverage", "type-check": "tsc --noEmit", "lint": "eslint src", "lint:fix": "eslint src --fix", @@ -45,6 +46,7 @@ "@eslint/js": "^9.39.1", "@types/eslint__js": "^8.42.3", "@types/node": "^20.0.0", + "@vitest/coverage-v8": "3.2.4", "@vitest/ui": "^3.0.0", "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", diff --git a/tsconfig.json b/tsconfig.json index 5a47439..e161fe9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,6 @@ "declaration": true, "declarationMap": true, "outDir": "./lib", - "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, @@ -19,6 +18,6 @@ "noImplicitReturns": true, "noFallthroughCasesInSwitch": true }, - "include": ["src/**/*"], + "include": ["src/**/*", "__tests__/**/*"], "exclude": ["node_modules", "dist", "build", "lib", "demos"] }