/** * 工具函数单元测试 * 使用 Vitest 进行测试 */ import { describe, it, expect } from 'vitest'; import { polarToXY, normalizeDeg, annularSectorCentroid, annularSectorPath, annularSectorInsetPath, calculateLabelRotation, generateSectorColor, generateTextPath, generateVerticalTextPath, getTextColorForBackground, calculateSectorFontSize, } from '../src/utils'; describe('polarToXY', () => { it('应该正确转换 0° 角度(北方)', () => { const result = polarToXY(0, 100); expect(result.x).toBeCloseTo(0); expect(result.y).toBeCloseTo(-100); }); it('应该正确转换 90° 角度(东方)', () => { const result = polarToXY(90, 100); expect(result.x).toBeCloseTo(100); expect(result.y).toBeCloseTo(0); }); it('应该正确转换 180° 角度(南方)', () => { const result = polarToXY(180, 100); expect(result.x).toBeCloseTo(0); expect(result.y).toBeCloseTo(100); }); it('应该正确转换 270° 角度(西方)', () => { const result = polarToXY(270, 100); expect(result.x).toBeCloseTo(-100); expect(result.y).toBeCloseTo(0); }); }); describe('normalizeDeg', () => { it('应该保持 0-360 范围内的角度不变', () => { expect(normalizeDeg(45)).toBe(45); expect(normalizeDeg(180)).toBe(180); expect(normalizeDeg(359)).toBe(359); }); it('应该将负角度转换为正角度', () => { expect(normalizeDeg(-45)).toBe(315); expect(normalizeDeg(-180)).toBe(180); expect(normalizeDeg(-90)).toBe(270); }); it('应该将大于 360 的角度归一化', () => { expect(normalizeDeg(405)).toBe(45); expect(normalizeDeg(720)).toBe(0); expect(normalizeDeg(370)).toBe(10); }); }); describe('annularSectorCentroid', () => { it('应该为简单扇形计算正确的形心', () => { const result = annularSectorCentroid({ rInner: 0, rOuter: 100, aStartDeg: 0, aEndDeg: 90, }); expect(result.aMidDeg).toBe(45); expect(result.deltaDeg).toBe(90); expect(result.rho).toBeGreaterThan(0); }); it('应该为圆环扇形计算正确的形心', () => { const result = annularSectorCentroid({ rInner: 50, rOuter: 100, aStartDeg: 0, aEndDeg: 90, }); expect(result.aMidDeg).toBe(45); expect(result.deltaDeg).toBe(90); expect(result.rho).toBeGreaterThan(50); expect(result.rho).toBeLessThan(100); }); it('应该处理跨越 0° 的扇形', () => { const result = annularSectorCentroid({ rInner: 0, rOuter: 100, aStartDeg: 315, aEndDeg: 45, }); expect(result.aMidDeg).toBe(0); expect(result.deltaDeg).toBe(90); }); it('应该返回零形心当内外半径相等时', () => { const result = annularSectorCentroid({ rInner: 100, rOuter: 100, aStartDeg: 0, aEndDeg: 90, }); expect(result.cx).toBe(0); expect(result.cy).toBe(0); expect(result.rho).toBe(0); }); }); describe('annularSectorPath', () => { it('应该生成有效的 SVG 路径', () => { const path = annularSectorPath(50, 100, 0, 90); expect(path).toContain('M '); expect(path).toContain('A '); expect(path).toContain('L '); expect(path).toContain('Z'); }); it('应该为纯扇形(内半径为 0)生成简化路径', () => { const path = annularSectorPath(0, 100, 0, 90); expect(path).toContain('M '); expect(path).toContain('A '); expect(path).toContain('L 0 0'); expect(path).toContain('Z'); }); it('应该在大角度时设置 large-arc-flag', () => { const path = annularSectorPath(50, 100, 0, 270); // 大角度应包含 large-arc-flag = 1 expect(path).toBeTruthy(); }); }); describe('calculateLabelRotation', () => { it('应该在上半圆不进行翻转', () => { expect(calculateLabelRotation(0)).toBe(0); expect(calculateLabelRotation(45)).toBe(45); expect(calculateLabelRotation(90)).toBe(90); expect(calculateLabelRotation(180)).toBe(180); }); it('应该在下半圆翻转180°避免倒字', () => { expect(calculateLabelRotation(181)).toBe(361); // 181 + 180 expect(calculateLabelRotation(270)).toBe(450); // 270 + 180 expect(calculateLabelRotation(359)).toBe(539); // 359 + 180 }); it('应该在边界值正确处理', () => { expect(calculateLabelRotation(180)).toBe(180); // 等于180不翻转 expect(calculateLabelRotation(360)).toBe(360); // 等于360不翻转 }); }); describe('generateSectorColor', () => { it('应该生成有效的 HSL 颜色', () => { const color = generateSectorColor(0, 0); expect(color).toMatch(/^hsl\(\d+(\.\d+)? \d+% \d+%\)$/); }); it('应该根据层索引改变亮度', () => { const color1 = generateSectorColor(0, 0); const color2 = generateSectorColor(1, 0); const color3 = generateSectorColor(2, 0); // 提取亮度值 const light1 = parseInt(color1.match(/(\d+)%\)$/)?.[1] || '0'); const light2 = parseInt(color2.match(/(\d+)%\)$/)?.[1] || '0'); const light3 = parseInt(color3.match(/(\d+)%\)$/)?.[1] || '0'); expect(light1).toBeGreaterThan(light2); expect(light2).toBeGreaterThan(light3); }); it('应该根据扇区索引改变色相', () => { const color1 = generateSectorColor(0, 0); const color2 = generateSectorColor(0, 6); // 提取色相值 const hue1 = parseInt(color1.match(/^hsl\((\d+(\.\d+)?)/)?.[1] || '0'); const hue2 = parseInt(color2.match(/^hsl\((\d+(\.\d+)?)/)?.[1] || '0'); expect(hue1).not.toBe(hue2); }); it('应该为单层生成颜色', () => { const color = generateSectorColor(0, 0, 1, 24); expect(color).toMatch(/^hsl\(\d+(\.\d+)? \d+% \d+%\)$/); }); it('应该处理大量层数', () => { const color1 = generateSectorColor(0, 0, 31, 24); const color2 = generateSectorColor(30, 0, 31, 24); const light1 = parseInt(color1.match(/(\d+)%\)$/)?.[1] || '0'); const light2 = parseInt(color2.match(/(\d+)%\)$/)?.[1] || '0'); expect(light1).toBeGreaterThan(light2); }); }); describe('annularSectorInsetPath', () => { it('应该生成内缩路径', () => { const path = annularSectorInsetPath(50, 100, 0, 90, 2); expect(path).toContain('M '); expect(path).toContain('A '); expect(path).toContain('Z'); }); it('应该在内缩过大时返回空路径', () => { const path = annularSectorInsetPath(50, 60, 0, 90, 20); expect(path).toBe(''); }); it('应该处理小角度扇区', () => { const path = annularSectorInsetPath(50, 100, 0, 5, 2); expect(path).toBeTruthy(); }); it('应该处理从圆心开始的扇区', () => { const path = annularSectorInsetPath(0, 100, 0, 90, 2); expect(path).toContain('M '); expect(path).toContain('A '); }); }); describe('generateTextPath', () => { it('应该生成正向文字路径', () => { const path = generateTextPath(50, 100, 0, 90, false, 'middle', 12); expect(path).toContain('M '); expect(path).toContain('A '); expect(path).toContain('0 1'); // 正向扫描 }); it('应该生成反向文字路径', () => { const path = generateTextPath(50, 100, 0, 90, true, 'middle', 12); expect(path).toContain('M '); expect(path).toContain('A '); expect(path).toContain('0 0'); // 反向扫描 }); it('应该在 centroid 模式下使用形心半径', () => { const pathCentroid = generateTextPath(50, 100, 0, 90, false, 'centroid', 12); const pathMiddle = generateTextPath(50, 100, 0, 90, false, 'middle', 12); expect(pathCentroid).not.toBe(pathMiddle); }); it('应该处理跨越360°的扇区', () => { const path = generateTextPath(50, 100, 315, 45, false, 'middle', 12); expect(path).toContain('M '); expect(path).toContain('A '); }); it('应该处理大角度扇区', () => { const path = generateTextPath(50, 100, 0, 270, false, 'middle', 12); expect(path).toContain('M '); expect(path).toContain('A '); }); }); describe('generateVerticalTextPath', () => { it('应该生成竖排文字路径', () => { const path = generateVerticalTextPath(50, 100, 0, 30, 15, 'middle', 12); expect(path).toContain('M '); expect(path).toContain('L '); }); it('应该在 centroid 模式下使用形心', () => { const pathCentroid = generateVerticalTextPath(50, 100, 0, 30, 15, 'centroid', 12); const pathMiddle = generateVerticalTextPath(50, 100, 0, 30, 15, 'middle', 12); expect(pathCentroid).not.toBe(pathMiddle); }); it('应该处理不同的角度', () => { const path1 = generateVerticalTextPath(50, 100, 0, 30, 15, 'middle', 12); const path2 = generateVerticalTextPath(50, 100, 0, 30, 180, 'middle', 12); expect(path1).not.toBe(path2); }); it('应该处理窄扇区', () => { const path = generateVerticalTextPath(80, 100, 0, 10, 5, 'middle', 12); expect(path).toContain('M '); expect(path).toContain('L '); }); }); describe('getTextColorForBackground', () => { it('应该为深色背景返回白色', () => { const color = getTextColorForBackground('hsl(180 70% 25%)'); expect(color).toBe('#ffffff'); }); it('应该为浅色背景返回深色', () => { const color = getTextColorForBackground('hsl(180 70% 85%)'); expect(color).toBe('#111827'); }); it('应该处理边界值(50%)', () => { const color = getTextColorForBackground('hsl(180 70% 50%)'); expect(color).toBe('#111827'); }); it('应该处理边界值(49%)', () => { const color = getTextColorForBackground('hsl(180 70% 49%)'); expect(color).toBe('#ffffff'); }); it('应该处理无效输入', () => { const color = getTextColorForBackground('invalid'); expect(color).toBe('#111827'); }); }); describe('calculateSectorFontSize', () => { it('应该为标准扇区计算字体大小', () => { const fontSize = calculateSectorFontSize(50, 100, 0, 90, 4, 3, 24); expect(fontSize).toBeGreaterThan(3); expect(fontSize).toBeLessThanOrEqual(24); }); it('应该为窄扇区返回较小的字体', () => { const narrowSize = calculateSectorFontSize(90, 100, 0, 10, 4, 3, 24); const wideSize = calculateSectorFontSize(50, 100, 0, 90, 4, 3, 24); expect(narrowSize).toBeLessThan(wideSize); }); it('应该根据文字长度调整', () => { const shortText = calculateSectorFontSize(50, 100, 0, 90, 2, 3, 24); const longText = calculateSectorFontSize(50, 100, 0, 90, 8, 3, 24); expect(shortText).toBeGreaterThan(longText); }); it('应该为小角度扇区应用额外限制', () => { const size1 = calculateSectorFontSize(50, 100, 0, 10, 2, 3, 24); const size2 = calculateSectorFontSize(50, 100, 0, 90, 2, 3, 24); expect(size1).toBeLessThan(size2); }); it('应该尊重最小字体大小限制', () => { const fontSize = calculateSectorFontSize(95, 100, 0, 5, 10, 6, 24); expect(fontSize).toBeGreaterThanOrEqual(6); }); it('应该尊重最大字体大小限制', () => { const fontSize = calculateSectorFontSize(0, 200, 0, 180, 1, 3, 20); expect(fontSize).toBeLessThanOrEqual(20); }); it('应该处理零文字长度', () => { const fontSize = calculateSectorFontSize(50, 100, 0, 90, 0, 3, 24); expect(fontSize).toBe(3); }); it('应该处理从圆心开始的扇区', () => { const fontSize = calculateSectorFontSize(0, 100, 0, 90, 4, 3, 24); expect(fontSize).toBeGreaterThan(3); expect(fontSize).toBeLessThanOrEqual(24); }); });