first commit

This commit is contained in:
douboer
2026-01-21 13:22:26 +08:00
commit 24452838a1
28 changed files with 7901 additions and 0 deletions

369
tests/utils.test.ts Normal file
View File

@@ -0,0 +1,369 @@
/**
* 工具函数单元测试
* 使用 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);
});
});