Files
lupin-demo/tests/useLuopan.test.ts
2026-01-21 21:48:08 +08:00

427 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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('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('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(/^#[0-9a-fA-F]{6}$/);
expect(sector.textColor).toMatch(/^#[0-9a-fA-F]{6}$/);
});
});
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('应该为窄扇区设置 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('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层
});
});
});