476 lines
16 KiB
TypeScript
476 lines
16 KiB
TypeScript
/**
|
||
* useLuopan 组合函数单元测试
|
||
*/
|
||
|
||
import { describe, it, expect } from 'vitest';
|
||
import { ref } from 'vue';
|
||
import { useLuopan } from '../src/composables/useLuopan';
|
||
import type { Example, TextRadialPosition } from '../src/types';
|
||
|
||
describe('useLuopan', () => {
|
||
const createMockExample = (): Example => ({
|
||
name: '测试示例',
|
||
angles: [0, 90, 180, 270, 360],
|
||
radii: [50, 100, 150],
|
||
});
|
||
|
||
describe('基本功能', () => {
|
||
it('应该返回所有必需的属性和方法', () => {
|
||
const example = ref(createMockExample());
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const result = useLuopan(example, textRadialPosition);
|
||
|
||
expect(result).toHaveProperty('anglesDeg');
|
||
expect(result).toHaveProperty('rings');
|
||
expect(result).toHaveProperty('outerMost');
|
||
expect(result).toHaveProperty('sectors');
|
||
expect(result).toHaveProperty('getLabelTransform');
|
||
expect(result).toHaveProperty('toXY');
|
||
});
|
||
|
||
it('anglesDeg 应该返回角度数组', () => {
|
||
const example = ref(createMockExample());
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { anglesDeg } = useLuopan(example, textRadialPosition);
|
||
|
||
expect(anglesDeg.value).toEqual([0, 90, 180, 270, 360]);
|
||
});
|
||
|
||
it('rings 应该返回半径数组', () => {
|
||
const example = ref(createMockExample());
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { rings } = useLuopan(example, textRadialPosition);
|
||
|
||
expect(rings.value).toEqual([50, 100, 150]);
|
||
});
|
||
|
||
it('outerMost 应该返回最外层半径', () => {
|
||
const example = ref(createMockExample());
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { outerMost } = useLuopan(example, textRadialPosition);
|
||
|
||
expect(outerMost.value).toBe(150);
|
||
});
|
||
});
|
||
|
||
describe('扇区生成', () => {
|
||
it('应该生成正确数量的扇区', () => {
|
||
const example = ref(createMockExample());
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { sectors } = useLuopan(example, textRadialPosition);
|
||
|
||
// 4个角度分割 × 3层 = 12个扇区
|
||
expect(sectors.value.length).toBe(12);
|
||
});
|
||
|
||
it('每个扇区应该有必需的属性', () => {
|
||
const example = ref(createMockExample());
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { sectors } = useLuopan(example, textRadialPosition);
|
||
|
||
sectors.value.forEach((sector) => {
|
||
expect(sector).toHaveProperty('key');
|
||
expect(sector).toHaveProperty('layerIndex');
|
||
expect(sector).toHaveProperty('pieIndex');
|
||
expect(sector).toHaveProperty('rInner');
|
||
expect(sector).toHaveProperty('rOuter');
|
||
expect(sector).toHaveProperty('aStart');
|
||
expect(sector).toHaveProperty('aEnd');
|
||
expect(sector).toHaveProperty('aMidDeg');
|
||
expect(sector).toHaveProperty('aMidRad');
|
||
expect(sector).toHaveProperty('cx');
|
||
expect(sector).toHaveProperty('cy');
|
||
expect(sector).toHaveProperty('fill');
|
||
expect(sector).toHaveProperty('textColor');
|
||
expect(sector).toHaveProperty('label');
|
||
expect(sector).toHaveProperty('path');
|
||
expect(sector).toHaveProperty('textPath');
|
||
expect(sector).toHaveProperty('textPathId');
|
||
expect(sector).toHaveProperty('needReverse');
|
||
expect(sector).toHaveProperty('isVertical');
|
||
expect(sector).toHaveProperty('fontSize');
|
||
});
|
||
});
|
||
|
||
it('扇区的 key 应该是唯一的', () => {
|
||
const example = ref(createMockExample());
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { sectors } = useLuopan(example, textRadialPosition);
|
||
|
||
const keys = sectors.value.map((s) => s.key);
|
||
const uniqueKeys = new Set(keys);
|
||
expect(uniqueKeys.size).toBe(keys.length);
|
||
});
|
||
|
||
it('扇区应该有正确的层索引和扇区索引', () => {
|
||
const example = ref(createMockExample());
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { sectors } = useLuopan(example, textRadialPosition);
|
||
|
||
// 检查第一层的扇区
|
||
const layer0Sectors = sectors.value.filter((s) => s.layerIndex === 0);
|
||
expect(layer0Sectors.length).toBe(4);
|
||
expect(layer0Sectors.map((s) => s.pieIndex)).toEqual([0, 1, 2, 3]);
|
||
|
||
// 检查第二层的扇区
|
||
const layer1Sectors = sectors.value.filter((s) => s.layerIndex === 1);
|
||
expect(layer1Sectors.length).toBe(4);
|
||
expect(layer1Sectors.map((s) => s.pieIndex)).toEqual([0, 1, 2, 3]);
|
||
});
|
||
|
||
it('第一层扇区应该从圆心开始(rInner = 0)', () => {
|
||
const example = ref(createMockExample());
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { sectors } = useLuopan(example, textRadialPosition);
|
||
|
||
const layer0Sectors = sectors.value.filter((s) => s.layerIndex === 0);
|
||
layer0Sectors.forEach((sector) => {
|
||
expect(sector.rInner).toBe(0);
|
||
expect(sector.rOuter).toBe(50);
|
||
});
|
||
});
|
||
|
||
it('扇区应该有正确的角度范围', () => {
|
||
const example = ref(createMockExample());
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { sectors } = useLuopan(example, textRadialPosition);
|
||
|
||
const sector0 = sectors.value.find((s) => s.layerIndex === 0 && s.pieIndex === 0);
|
||
expect(sector0?.aStart).toBe(0);
|
||
expect(sector0?.aEnd).toBe(90);
|
||
|
||
const sector1 = sectors.value.find((s) => s.layerIndex === 0 && s.pieIndex === 1);
|
||
expect(sector1?.aStart).toBe(90);
|
||
expect(sector1?.aEnd).toBe(180);
|
||
});
|
||
|
||
it('扇区应该有有效的颜色', () => {
|
||
const example = ref(createMockExample());
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { sectors } = useLuopan(example, textRadialPosition);
|
||
|
||
sectors.value.forEach((sector) => {
|
||
expect(sector.fill).toMatch(/^(hsl\(.*\)|#[0-9a-fA-F]{6}|#ffffff)$/);
|
||
expect(sector.textColor).toMatch(/^(#[0-9a-fA-F]{6}|#ffffff)$/);
|
||
});
|
||
});
|
||
|
||
it('扇区应该有有效的字体大小', () => {
|
||
const example = ref(createMockExample());
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { sectors } = useLuopan(example, textRadialPosition);
|
||
|
||
sectors.value.forEach((sector) => {
|
||
expect(sector.fontSize).toBeGreaterThan(0);
|
||
expect(sector.fontSize).toBeLessThanOrEqual(30);
|
||
});
|
||
});
|
||
|
||
it('扇区应该有非空的标签', () => {
|
||
const example = ref(createMockExample());
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { sectors } = useLuopan(example, textRadialPosition);
|
||
|
||
sectors.value.forEach((sector) => {
|
||
expect(sector.label).toBeTruthy();
|
||
expect(sector.label.length).toBeGreaterThan(0);
|
||
});
|
||
});
|
||
|
||
it('扇区应该有有效的 SVG 路径', () => {
|
||
const example = ref(createMockExample());
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { sectors } = useLuopan(example, textRadialPosition);
|
||
|
||
sectors.value.forEach((sector) => {
|
||
expect(sector.path).toContain('M ');
|
||
expect(sector.path).toContain('Z');
|
||
});
|
||
});
|
||
|
||
it('扇区应该有有效的文字路径', () => {
|
||
const example = ref(createMockExample());
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { sectors } = useLuopan(example, textRadialPosition);
|
||
|
||
sectors.value.forEach((sector) => {
|
||
expect(sector.textPath).toContain('M ');
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('文字位置模式', () => {
|
||
it('应该在 middle 模式下使用中点位置', () => {
|
||
const example = ref(createMockExample());
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { sectors } = useLuopan(example, textRadialPosition);
|
||
|
||
// 第二层的中点应该在 (50 + 100) / 2 = 75 附近
|
||
const layer1Sector = sectors.value.find((s) => s.layerIndex === 1);
|
||
expect(layer1Sector).toBeDefined();
|
||
});
|
||
|
||
it('应该在 centroid 模式下使用形心位置', () => {
|
||
const example = ref(createMockExample());
|
||
const textRadialPosition = ref<TextRadialPosition>('centroid');
|
||
|
||
const { sectors } = useLuopan(example, textRadialPosition);
|
||
|
||
// 形心位置应该与中点位置不同
|
||
expect(sectors.value.length).toBeGreaterThan(0);
|
||
});
|
||
|
||
it('最内层应该始终使用形心位置', () => {
|
||
const example = ref(createMockExample());
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { sectors } = useLuopan(example, textRadialPosition);
|
||
|
||
const innermostSectors = sectors.value.filter((s) => s.layerIndex === 0);
|
||
expect(innermostSectors.length).toBeGreaterThan(0);
|
||
// 最内层应该使用形心位置,即使设置为 middle
|
||
});
|
||
});
|
||
|
||
describe('文字方向', () => {
|
||
it('应该为左半圆的扇区设置 needReverse', () => {
|
||
const example = ref(createMockExample());
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { sectors } = useLuopan(example, textRadialPosition);
|
||
|
||
// 中间角度在 (90, 270) 范围内的扇区应该需要反向
|
||
const leftSectors = sectors.value.filter(
|
||
(s) => s.aMidDeg > 90 && s.aMidDeg < 270
|
||
);
|
||
leftSectors.forEach((sector) => {
|
||
expect(sector.needReverse).toBe(true);
|
||
});
|
||
});
|
||
|
||
it('应该为右半圆的扇区不设置 needReverse', () => {
|
||
const example = ref(createMockExample());
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { sectors } = useLuopan(example, textRadialPosition);
|
||
|
||
// 中间角度在 [0, 90] 或 [270, 360] 范围内的扇区不需要反向
|
||
const rightSectors = sectors.value.filter(
|
||
(s) => s.aMidDeg <= 90 || s.aMidDeg >= 270
|
||
);
|
||
rightSectors.forEach((sector) => {
|
||
expect(sector.needReverse).toBe(false);
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('竖排文字判断', () => {
|
||
it('应该为窄扇区设置 isVertical', () => {
|
||
const example = ref({
|
||
name: '窄扇区示例',
|
||
angles: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200, 210, 220, 230, 240, 250, 260, 270, 280, 290, 300, 310, 320, 330, 340, 350, 360],
|
||
radii: [50, 100, 150],
|
||
});
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { sectors } = useLuopan(example, textRadialPosition);
|
||
|
||
// 某些窄扇区应该被设置为竖排
|
||
const verticalSectors = sectors.value.filter((s) => s.isVertical);
|
||
expect(verticalSectors.length).toBeGreaterThan(0);
|
||
});
|
||
|
||
it('应该为宽扇区不设置 isVertical', () => {
|
||
const example = ref({
|
||
name: '宽扇区示例',
|
||
angles: [0, 180, 360],
|
||
radii: [50, 60],
|
||
});
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { sectors } = useLuopan(example, textRadialPosition);
|
||
|
||
// 宽扇区应该都不是竖排
|
||
sectors.value.forEach((sector) => {
|
||
expect(sector.isVertical).toBe(false);
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('内部填色', () => {
|
||
it('某些扇区应该有内部填色', () => {
|
||
const example = ref(createMockExample());
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { sectors } = useLuopan(example, textRadialPosition);
|
||
|
||
const filledSectors = sectors.value.filter(
|
||
(s) => s.innerFillPath && s.innerFillColor
|
||
);
|
||
expect(filledSectors.length).toBeGreaterThan(0);
|
||
});
|
||
|
||
it('有内部填色的扇区应该使用白色底色和黑色文字', () => {
|
||
const example = ref(createMockExample());
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { sectors } = useLuopan(example, textRadialPosition);
|
||
|
||
const filledSectors = sectors.value.filter(
|
||
(s) => s.innerFillPath && s.innerFillColor
|
||
);
|
||
filledSectors.forEach((sector) => {
|
||
expect(sector.fill).toBe('#ffffff');
|
||
expect(sector.textColor).toBe('#111827');
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('响应式更新', () => {
|
||
it('应该响应示例变化', () => {
|
||
const example = ref(createMockExample());
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { sectors } = useLuopan(example, textRadialPosition);
|
||
|
||
const initialCount = sectors.value.length;
|
||
|
||
// 更改示例
|
||
example.value = {
|
||
name: '新示例',
|
||
angles: [0, 120, 240, 360],
|
||
radii: [100, 200],
|
||
};
|
||
|
||
// 扇区数量应该变化
|
||
expect(sectors.value.length).not.toBe(initialCount);
|
||
expect(sectors.value.length).toBe(6); // 3个角度分割 × 2层
|
||
});
|
||
|
||
it('应该响应文字位置模式变化', () => {
|
||
const example = ref(createMockExample());
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { sectors } = useLuopan(example, textRadialPosition);
|
||
|
||
const middlePaths = sectors.value.map((s) => s.textPath);
|
||
|
||
// 更改文字位置模式
|
||
textRadialPosition.value = 'centroid';
|
||
|
||
const centroidPaths = sectors.value.map((s) => s.textPath);
|
||
|
||
// 非最内层的文字路径应该有所不同
|
||
const differentPaths = middlePaths.filter(
|
||
(path, index) => path !== centroidPaths[index]
|
||
);
|
||
expect(differentPaths.length).toBeGreaterThan(0);
|
||
});
|
||
});
|
||
|
||
describe('getLabelTransform', () => {
|
||
it('应该返回有效的 SVG transform 字符串', () => {
|
||
const example = ref(createMockExample());
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { sectors, getLabelTransform } = useLuopan(example, textRadialPosition);
|
||
|
||
const sector = sectors.value[0];
|
||
const transform = getLabelTransform(sector);
|
||
|
||
expect(transform).toContain('translate');
|
||
expect(transform).toContain('rotate');
|
||
});
|
||
});
|
||
|
||
describe('toXY', () => {
|
||
it('应该正确转换极坐标', () => {
|
||
const example = ref(createMockExample());
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { toXY } = useLuopan(example, textRadialPosition);
|
||
|
||
const point = toXY(0, 100);
|
||
expect(point.x).toBeCloseTo(0);
|
||
expect(point.y).toBeCloseTo(-100);
|
||
});
|
||
});
|
||
|
||
describe('边界情况', () => {
|
||
it('应该处理单层罗盘', () => {
|
||
const example = ref({
|
||
name: '单层',
|
||
angles: [0, 90, 180, 270, 360],
|
||
radii: [100],
|
||
});
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { sectors } = useLuopan(example, textRadialPosition);
|
||
|
||
expect(sectors.value.length).toBe(4);
|
||
sectors.value.forEach((sector) => {
|
||
expect(sector.layerIndex).toBe(0);
|
||
expect(sector.rInner).toBe(0);
|
||
expect(sector.rOuter).toBe(100);
|
||
});
|
||
});
|
||
|
||
it('应该处理两个扇区的罗盘', () => {
|
||
const example = ref({
|
||
name: '两扇区',
|
||
angles: [0, 180, 360],
|
||
radii: [100],
|
||
});
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { sectors } = useLuopan(example, textRadialPosition);
|
||
|
||
expect(sectors.value.length).toBe(2);
|
||
});
|
||
|
||
it('应该处理大量层数的罗盘', () => {
|
||
const example = ref({
|
||
name: '多层',
|
||
angles: [0, 90, 180, 270, 360],
|
||
radii: [20, 40, 60, 80, 100, 120, 140, 160, 180, 200],
|
||
});
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { sectors } = useLuopan(example, textRadialPosition);
|
||
|
||
expect(sectors.value.length).toBe(40); // 4个扇区 × 10层
|
||
const layerIndices = new Set(sectors.value.map((s) => s.layerIndex));
|
||
expect(layerIndices.size).toBe(10);
|
||
});
|
||
|
||
it('应该处理大量扇区的罗盘', () => {
|
||
const example = ref({
|
||
name: '多扇区',
|
||
angles: Array.from({ length: 37 }, (_, i) => i * 10), // 36个扇区
|
||
radii: [100, 200],
|
||
});
|
||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||
|
||
const { sectors } = useLuopan(example, textRadialPosition);
|
||
|
||
expect(sectors.value.length).toBe(72); // 36个扇区 × 2层
|
||
});
|
||
});
|
||
});
|