370 lines
11 KiB
TypeScript
370 lines
11 KiB
TypeScript
/**
|
||
* 工具函数单元测试
|
||
* 使用 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);
|
||
});
|
||
});
|