update at 2026-01-22 18:43:01
This commit is contained in:
@@ -2,544 +2,105 @@
|
||||
* Luopan 组件单元测试
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import Luopan from '../src/Luopan.vue';
|
||||
import { EXAMPLES } from '../src/constants';
|
||||
import type { LuopanConfig } from '../src/types';
|
||||
|
||||
const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
const baseConfig: LuopanConfig = {
|
||||
name: '测试配置',
|
||||
background: '#000000',
|
||||
theme: {
|
||||
colorPalettes: {
|
||||
A: '#111111',
|
||||
},
|
||||
},
|
||||
layers: [
|
||||
{
|
||||
divisions: 4,
|
||||
rInner: 0,
|
||||
rOuter: 80,
|
||||
sectors: [{ content: '甲' }, { content: '乙' }, { content: '丙' }, { content: '丁' }],
|
||||
},
|
||||
{
|
||||
type: 'degreeRing',
|
||||
degreeRing: {
|
||||
rInner: 90,
|
||||
rOuter: 100,
|
||||
showDegree: 1,
|
||||
mode: 'both',
|
||||
opacity: 0.3,
|
||||
tickLength: 6,
|
||||
tickLengthStep: 1,
|
||||
majorTick: 10,
|
||||
minorTick: 5,
|
||||
microTick: 1,
|
||||
tickColor: '#ffffff',
|
||||
ringColor: '#ffffff',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'centerIcon',
|
||||
centerIcon: {
|
||||
rIcon: 10,
|
||||
opacity: 1,
|
||||
name: 'centericon.svg',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('Luopan 组件', () => {
|
||||
describe('基本渲染', () => {
|
||||
it('应该成功渲染', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('应该渲染工具栏', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const toolbar = wrapper.find('.toolbar');
|
||||
expect(toolbar.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('应该渲染 SVG 容器', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const svg = wrapper.find('svg');
|
||||
expect(svg.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('应该使用默认尺寸', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const svg = wrapper.find('svg');
|
||||
expect(svg.attributes('width')).toBeTruthy();
|
||||
expect(svg.attributes('height')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('应该使用传入的 size prop', () => {
|
||||
const customSize = 600;
|
||||
const wrapper = mount(Luopan, {
|
||||
props: { size: customSize },
|
||||
});
|
||||
const svg = wrapper.find('svg');
|
||||
expect(svg.attributes('width')).toBe(String(customSize));
|
||||
expect(svg.attributes('height')).toBe(String(customSize));
|
||||
});
|
||||
|
||||
it('应该设置正确的 viewBox', () => {
|
||||
const size = 520;
|
||||
const wrapper = mount(Luopan, {
|
||||
props: { size },
|
||||
});
|
||||
const svg = wrapper.find('svg');
|
||||
const viewBox = svg.attributes('viewBox');
|
||||
expect(viewBox).toBe(`${-size / 2} ${-size / 2} ${size} ${size}`);
|
||||
});
|
||||
it('应该成功渲染', async () => {
|
||||
const wrapper = mount(Luopan, { props: { config: baseConfig } });
|
||||
await flushPromises();
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('示例切换', () => {
|
||||
it('应该渲染示例切换按钮', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const buttons = wrapper.findAll('.toolbar button:not(.zoom-controls button)');
|
||||
expect(buttons.length).toBeGreaterThanOrEqual(EXAMPLES.length);
|
||||
});
|
||||
it('应该渲染工具栏控件', async () => {
|
||||
const wrapper = mount(Luopan, { props: { config: baseConfig } });
|
||||
await flushPromises();
|
||||
|
||||
it('第一个示例按钮应该默认激活', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const firstButton = wrapper.findAll('.toolbar button')[0];
|
||||
expect(firstButton.classes()).toContain('active');
|
||||
});
|
||||
|
||||
it('点击示例按钮应该切换示例', async () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const buttons = wrapper.findAll('.toolbar button');
|
||||
|
||||
// 假设至少有2个示例
|
||||
if (buttons.length >= 2) {
|
||||
const secondButton = buttons[1];
|
||||
await secondButton.trigger('click');
|
||||
|
||||
expect(secondButton.classes()).toContain('active');
|
||||
expect(buttons[0].classes()).not.toContain('active');
|
||||
}
|
||||
});
|
||||
expect(wrapper.find('.toolbar').exists()).toBe(true);
|
||||
expect(wrapper.find('input[type="checkbox"]').exists()).toBe(true);
|
||||
expect(wrapper.find('select').exists()).toBe(true);
|
||||
expect(wrapper.find('.zoom-controls').exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('辅助线显示', () => {
|
||||
it('应该有辅助线切换开关', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const checkbox = wrapper.find('input[type="checkbox"]');
|
||||
expect(checkbox.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('辅助线应该默认显示', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const checkbox = wrapper.find('input[type="checkbox"]');
|
||||
expect((checkbox.element as HTMLInputElement).checked).toBe(true);
|
||||
});
|
||||
|
||||
it('切换辅助线开关应该显示/隐藏辅助线', async () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const checkbox = wrapper.find('input[type="checkbox"]');
|
||||
|
||||
// 默认显示辅助线
|
||||
let guidesGroup = wrapper.findAll('g').find(g => g.attributes('stroke') === '#111827');
|
||||
expect(guidesGroup).toBeDefined();
|
||||
|
||||
// 取消勾选
|
||||
await checkbox.setValue(false);
|
||||
guidesGroup = wrapper.findAll('g').find(g => g.attributes('stroke') === '#111827');
|
||||
// 辅助线可能被隐藏或不渲染
|
||||
});
|
||||
it('应该渲染 SVG 容器', async () => {
|
||||
const wrapper = mount(Luopan, { props: { config: baseConfig } });
|
||||
await flushPromises();
|
||||
expect(wrapper.find('svg').exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('文字位置模式', () => {
|
||||
it('应该有文字位置选择器', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const select = wrapper.find('select');
|
||||
expect(select.exists()).toBe(true);
|
||||
});
|
||||
it('应该渲染扇区路径', async () => {
|
||||
const wrapper = mount(Luopan, { props: { config: baseConfig } });
|
||||
await flushPromises();
|
||||
|
||||
it('选择器应该有两个选项', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const options = wrapper.findAll('option');
|
||||
expect(options.length).toBe(2);
|
||||
});
|
||||
|
||||
it('应该能切换文字位置模式', async () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const select = wrapper.find('select');
|
||||
|
||||
// 切换到形心模式
|
||||
await select.setValue('centroid');
|
||||
expect((select.element as HTMLSelectElement).value).toBe('centroid');
|
||||
|
||||
// 切换回中点模式
|
||||
await select.setValue('middle');
|
||||
expect((select.element as HTMLSelectElement).value).toBe('middle');
|
||||
});
|
||||
const sectorPaths = wrapper.findAll('path[fill]').filter((path) =>
|
||||
path.attributes('d')?.includes('M ')
|
||||
);
|
||||
expect(sectorPaths.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
describe('缩放功能', () => {
|
||||
it('应该渲染缩放控件', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const zoomControls = wrapper.find('.zoom-controls');
|
||||
expect(zoomControls.exists()).toBe(true);
|
||||
});
|
||||
it('应该渲染文字或 SVG 内容', async () => {
|
||||
const wrapper = mount(Luopan, { props: { config: baseConfig } });
|
||||
await flushPromises();
|
||||
|
||||
it('应该有放大、缩小和重置按钮', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const buttons = wrapper.findAll('.zoom-controls button');
|
||||
expect(buttons.length).toBe(3);
|
||||
});
|
||||
|
||||
it('应该显示当前缩放级别', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const zoomLevel = wrapper.find('.zoom-level');
|
||||
expect(zoomLevel.exists()).toBe(true);
|
||||
expect(zoomLevel.text()).toContain('%');
|
||||
});
|
||||
|
||||
it('点击放大按钮应该增加缩放', async () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const zoomInButton = wrapper.findAll('.zoom-controls button')[1];
|
||||
const svg = wrapper.find('svg');
|
||||
|
||||
const initialTransform = svg.attributes('style');
|
||||
|
||||
await zoomInButton.trigger('click');
|
||||
|
||||
const newTransform = svg.attributes('style');
|
||||
expect(newTransform).not.toBe(initialTransform);
|
||||
});
|
||||
|
||||
it('点击缩小按钮应该减少缩放', async () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const zoomOutButton = wrapper.findAll('.zoom-controls button')[0];
|
||||
const svg = wrapper.find('svg');
|
||||
|
||||
const initialTransform = svg.attributes('style');
|
||||
|
||||
await zoomOutButton.trigger('click');
|
||||
|
||||
const newTransform = svg.attributes('style');
|
||||
expect(newTransform).not.toBe(initialTransform);
|
||||
});
|
||||
|
||||
it('点击重置按钮应该重置缩放和平移', async () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const buttons = wrapper.findAll('.zoom-controls button');
|
||||
const zoomInButton = buttons[1];
|
||||
const resetButton = buttons[2];
|
||||
const svg = wrapper.find('svg');
|
||||
|
||||
// 先放大
|
||||
await zoomInButton.trigger('click');
|
||||
|
||||
// 然后重置
|
||||
await resetButton.trigger('click');
|
||||
|
||||
const transform = svg.attributes('style');
|
||||
expect(transform).toContain('scale(1)');
|
||||
});
|
||||
|
||||
it('缩放到达上限时应该禁用放大按钮', async () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const zoomInButton = wrapper.findAll('.zoom-controls button')[1];
|
||||
|
||||
// 多次点击放大直到达到上限
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await zoomInButton.trigger('click');
|
||||
}
|
||||
|
||||
expect(zoomInButton.attributes('disabled')).toBeDefined();
|
||||
});
|
||||
|
||||
it('缩放到达下限时应该禁用缩小按钮', async () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const zoomOutButton = wrapper.findAll('.zoom-controls button')[0];
|
||||
|
||||
// 多次点击缩小直到达到下限
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await zoomOutButton.trigger('click');
|
||||
}
|
||||
|
||||
expect(zoomOutButton.attributes('disabled')).toBeDefined();
|
||||
});
|
||||
const textPaths = wrapper.findAll('textPath');
|
||||
expect(textPaths.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
describe('鼠标滚轮缩放', () => {
|
||||
it('应该监听滚轮事件', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const container = wrapper.find('.svg-container');
|
||||
expect(container.exists()).toBe(true);
|
||||
});
|
||||
it('应该渲染刻度环和中心图标', async () => {
|
||||
const wrapper = mount(Luopan, { props: { config: baseConfig } });
|
||||
await flushPromises();
|
||||
|
||||
it('向下滚动应该缩小', async () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const container = wrapper.find('.svg-container');
|
||||
const svg = wrapper.find('svg');
|
||||
|
||||
await container.trigger('wheel', { deltaY: 100 });
|
||||
|
||||
// 验证缩放变化
|
||||
const transform = svg.attributes('style');
|
||||
expect(transform).toBeTruthy();
|
||||
});
|
||||
const lines = wrapper.findAll('line');
|
||||
expect(lines.length).toBeGreaterThan(0);
|
||||
|
||||
it('向上滚动应该放大', async () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const container = wrapper.find('.svg-container');
|
||||
const svg = wrapper.find('svg');
|
||||
|
||||
await container.trigger('wheel', { deltaY: -100 });
|
||||
|
||||
// 验证缩放变化
|
||||
const transform = svg.attributes('style');
|
||||
expect(transform).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('鼠标拖拽平移', () => {
|
||||
it('鼠标按下时光标应该变为抓取状态', async () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const container = wrapper.find('.svg-container');
|
||||
const svg = wrapper.find('svg');
|
||||
|
||||
// 初始状态
|
||||
expect(svg.attributes('style')).toContain('cursor: grab');
|
||||
|
||||
// 鼠标按下
|
||||
await container.trigger('mousedown', { clientX: 100, clientY: 100 });
|
||||
|
||||
// 应该变为抓取中状态
|
||||
expect(svg.attributes('style')).toContain('cursor: grabbing');
|
||||
});
|
||||
|
||||
it('鼠标释放时光标应该恢复', async () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const container = wrapper.find('.svg-container');
|
||||
const svg = wrapper.find('svg');
|
||||
|
||||
await container.trigger('mousedown', { clientX: 100, clientY: 100 });
|
||||
await container.trigger('mouseup');
|
||||
|
||||
expect(svg.attributes('style')).toContain('cursor: grab');
|
||||
});
|
||||
|
||||
it('拖拽应该改变平移位置', async () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const container = wrapper.find('.svg-container');
|
||||
const svg = wrapper.find('svg');
|
||||
|
||||
const initialTransform = svg.attributes('style');
|
||||
|
||||
// 模拟拖拽
|
||||
await container.trigger('mousedown', { clientX: 100, clientY: 100 });
|
||||
await container.trigger('mousemove', { clientX: 150, clientY: 150 });
|
||||
await container.trigger('mouseup');
|
||||
|
||||
const newTransform = svg.attributes('style');
|
||||
expect(newTransform).not.toBe(initialTransform);
|
||||
});
|
||||
|
||||
it('鼠标离开时应该停止拖拽', async () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const container = wrapper.find('.svg-container');
|
||||
const svg = wrapper.find('svg');
|
||||
|
||||
await container.trigger('mousedown', { clientX: 100, clientY: 100 });
|
||||
await container.trigger('mouseleave');
|
||||
|
||||
expect(svg.attributes('style')).toContain('cursor: grab');
|
||||
});
|
||||
});
|
||||
|
||||
describe('扇区渲染', () => {
|
||||
it('应该渲染多个扇区路径', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const sectorPaths = wrapper.findAll('path[fill]').filter(path =>
|
||||
!path.attributes('d')?.includes('none')
|
||||
);
|
||||
expect(sectorPaths.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('扇区应该有填充颜色', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const sectorPaths = wrapper.findAll('path[fill]');
|
||||
sectorPaths.forEach((path) => {
|
||||
const fill = path.attributes('fill');
|
||||
expect(fill).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('扇区应该有边框', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const sectorPaths = wrapper.findAll('path[stroke]');
|
||||
expect(sectorPaths.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('某些扇区应该有内部填色', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
// 查找内部填色路径(fill-opacity="0.6")
|
||||
const innerFillPaths = wrapper.findAll('path[fill-opacity]');
|
||||
expect(innerFillPaths.length).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('文字渲染', () => {
|
||||
it('应该渲染文字标签', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const textElements = wrapper.findAll('text');
|
||||
expect(textElements.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('文字应该使用 textPath', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const textPaths = wrapper.findAll('textPath');
|
||||
expect(textPaths.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('应该在 defs 中定义文字路径', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const defs = wrapper.find('defs');
|
||||
expect(defs.exists()).toBe(true);
|
||||
|
||||
const pathsInDefs = defs.findAll('path');
|
||||
expect(pathsInDefs.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('每个 textPath 应该引用对应的路径 id', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const textPaths = wrapper.findAll('textPath');
|
||||
|
||||
textPaths.forEach((textPath) => {
|
||||
const href = textPath.attributes('href');
|
||||
expect(href).toBeTruthy();
|
||||
expect(href?.startsWith('#')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('文字应该有字体大小', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const textElements = wrapper.findAll('text');
|
||||
|
||||
textElements.forEach((text) => {
|
||||
const fontSize = text.attributes('font-size');
|
||||
expect(fontSize).toBeTruthy();
|
||||
expect(parseFloat(fontSize!)).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('文字应该有填充颜色', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const textElements = wrapper.findAll('text');
|
||||
|
||||
textElements.forEach((text) => {
|
||||
const fill = text.attributes('fill');
|
||||
expect(fill).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('文字内容应该非空', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const textPaths = wrapper.findAll('textPath');
|
||||
|
||||
textPaths.forEach((textPath) => {
|
||||
expect(textPath.text().length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('辅助线渲染', () => {
|
||||
it('显示辅助线时应该渲染圆环', async () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const checkbox = wrapper.find('input[type="checkbox"]');
|
||||
await checkbox.setValue(true);
|
||||
|
||||
const circles = wrapper.findAll('circle[fill="none"]');
|
||||
expect(circles.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('显示辅助线时应该渲染径向线', async () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const checkbox = wrapper.find('input[type="checkbox"]');
|
||||
await checkbox.setValue(true);
|
||||
|
||||
const lines = wrapper.findAll('line');
|
||||
// 应该有一些径向线
|
||||
expect(lines.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('显示辅助线时应该渲染形心点', async () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const checkbox = wrapper.find('input[type="checkbox"]');
|
||||
await checkbox.setValue(true);
|
||||
|
||||
const centroidCircles = wrapper.findAll('circle[fill="#ef4444"]');
|
||||
expect(centroidCircles.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('说明文本', () => {
|
||||
it('应该显示说明区域', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const note = wrapper.find('.note');
|
||||
expect(note.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('说明应该包含角度约定', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const note = wrapper.find('.note');
|
||||
expect(note.text()).toContain('角度');
|
||||
});
|
||||
|
||||
it('说明应该包含文字方向', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const note = wrapper.find('.note');
|
||||
expect(note.text()).toContain('文字');
|
||||
});
|
||||
});
|
||||
|
||||
describe('背景渲染', () => {
|
||||
it('应该渲染白色背景矩形', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const bgRect = wrapper.find('rect[fill="white"]');
|
||||
expect(bgRect.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('背景应该覆盖整个 SVG 区域', () => {
|
||||
const size = 520;
|
||||
const wrapper = mount(Luopan, {
|
||||
props: { size },
|
||||
});
|
||||
const bgRect = wrapper.find('rect[fill="white"]');
|
||||
|
||||
expect(bgRect.attributes('width')).toBe(String(size));
|
||||
expect(bgRect.attributes('height')).toBe(String(size));
|
||||
});
|
||||
});
|
||||
|
||||
describe('组件样式', () => {
|
||||
it('主容器应该使用 grid 布局', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const wrap = wrapper.find('.luopan-wrap');
|
||||
expect(wrap.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('工具栏按钮应该有样式', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const buttons = wrapper.findAll('button');
|
||||
expect(buttons.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('SVG 容器应该存在', () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const container = wrapper.find('.svg-container');
|
||||
expect(container.exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('边界情况', () => {
|
||||
it('应该处理极小的尺寸', () => {
|
||||
const wrapper = mount(Luopan, {
|
||||
props: { size: 100 },
|
||||
});
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('应该处理极大的尺寸', () => {
|
||||
const wrapper = mount(Luopan, {
|
||||
props: { size: 2000 },
|
||||
});
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('应该在没有示例时不崩溃', () => {
|
||||
// 这个测试需要 mock EXAMPLES
|
||||
const wrapper = mount(Luopan);
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('性能', () => {
|
||||
it('应该在合理时间内渲染', () => {
|
||||
const startTime = performance.now();
|
||||
const wrapper = mount(Luopan);
|
||||
const endTime = performance.now();
|
||||
|
||||
expect(endTime - startTime).toBeLessThan(1000); // 应在1秒内完成
|
||||
});
|
||||
|
||||
it('切换示例应该不会造成内存泄漏', async () => {
|
||||
const wrapper = mount(Luopan);
|
||||
const buttons = wrapper.findAll('.toolbar button');
|
||||
|
||||
if (buttons.length >= 2) {
|
||||
// 多次切换示例
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await buttons[i % buttons.length].trigger('click');
|
||||
}
|
||||
}
|
||||
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
const images = wrapper.findAll('image');
|
||||
expect(images.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
25
tests/centerIcon.test.ts
Normal file
25
tests/centerIcon.test.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { loadCenterIcon } from '../src/centerIcon';
|
||||
|
||||
describe('centerIcon', () => {
|
||||
it('应生成带默认旋转的中心图标数据', async () => {
|
||||
const data = await loadCenterIcon({
|
||||
rIcon: 50,
|
||||
opacity: 0.8,
|
||||
name: 'centericon.svg',
|
||||
});
|
||||
|
||||
expect(data.rotation).toBe(0);
|
||||
expect(data.svgPath).toBe('src/assets/icons/centericon.svg');
|
||||
});
|
||||
|
||||
it('应支持自定义图标路径', async () => {
|
||||
const data = await loadCenterIcon(
|
||||
{ rIcon: 20, opacity: 1, name: 'icon.svg', rotation: 30 },
|
||||
'/icons'
|
||||
);
|
||||
|
||||
expect(data.svgPath).toBe('/icons/icon.svg');
|
||||
expect(data.rotation).toBe(30);
|
||||
});
|
||||
});
|
||||
53
tests/colorResolver.test.ts
Normal file
53
tests/colorResolver.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ColorResolver, applyPatternColoring } from '../src/colorResolver';
|
||||
import type { ThemeConfig, SectorLayerConfig, SectorConfig } from '../src/types';
|
||||
|
||||
describe('colorResolver', () => {
|
||||
const theme: ThemeConfig = {
|
||||
colorPalettes: {
|
||||
木: '#43A047',
|
||||
火: '#E53935',
|
||||
},
|
||||
};
|
||||
|
||||
it('resolveColor 应解析命名色并支持十六进制', () => {
|
||||
const resolver = new ColorResolver(theme, '#000000');
|
||||
expect(resolver.resolveColor('木')).toBe('#43A047');
|
||||
expect(resolver.resolveColor('#ffffff')).toBe('#ffffff');
|
||||
expect(resolver.resolveColor('不存在')).toBe('#000000');
|
||||
});
|
||||
|
||||
it('applyPatternColoring 应按 num + interval 生成映射', () => {
|
||||
const map = applyPatternColoring(6, '#111111', 2, 1);
|
||||
expect(map.has(0)).toBe(true);
|
||||
expect(map.has(1)).toBe(true);
|
||||
expect(map.has(2)).toBe(false);
|
||||
expect(map.has(3)).toBe(true);
|
||||
expect(map.has(4)).toBe(true);
|
||||
expect(map.has(5)).toBe(false);
|
||||
});
|
||||
|
||||
it('resolveLayerColors 仅在提供 colorRef 与 num 时生效', () => {
|
||||
const resolver = new ColorResolver(theme, '#000000');
|
||||
const layer: SectorLayerConfig = {
|
||||
divisions: 4,
|
||||
rInner: 0,
|
||||
rOuter: 10,
|
||||
colorRef: '火',
|
||||
num: 2,
|
||||
interval: 1,
|
||||
};
|
||||
const map = resolver.resolveLayerColors(layer);
|
||||
expect(map.size).toBe(3);
|
||||
});
|
||||
|
||||
it('resolveSectorColor 应遵循 sector > layer > background 优先级', () => {
|
||||
const resolver = new ColorResolver(theme, '#000000');
|
||||
const layerMap = new Map<number, string>([[1, '#E53935']]);
|
||||
const sector: SectorConfig = { content: 'x', colorRef: '木' };
|
||||
|
||||
expect(resolver.resolveSectorColor(layerMap, sector, 1)).toBe('#43A047');
|
||||
expect(resolver.resolveSectorColor(layerMap, undefined, 1)).toBe('#E53935');
|
||||
expect(resolver.resolveSectorColor(layerMap, undefined, 2)).toBe('#000000');
|
||||
});
|
||||
});
|
||||
34
tests/configParser.test.ts
Normal file
34
tests/configParser.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { parseConfig, stripJsonComments } from '../src/configParser';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
describe('configParser', () => {
|
||||
it('应移除 -- 行注释并保持 JSON 可解析', () => {
|
||||
const raw = '{\n -- 注释\n "name": "demo" -- 行尾注释\n}';
|
||||
const stripped = stripJsonComments(raw);
|
||||
expect(stripped).not.toContain('-- 注释');
|
||||
expect(() => JSON.parse(stripped)).not.toThrow();
|
||||
});
|
||||
|
||||
it('应能解析 demo.json', () => {
|
||||
const demoPath = resolve(__dirname, '..', 'public', 'demo.json');
|
||||
const text = readFileSync(demoPath, 'utf8');
|
||||
const config = parseConfig(text);
|
||||
|
||||
expect(config.name).toBe('demo');
|
||||
expect(config.layers.length).toBeGreaterThan(0);
|
||||
expect(config.theme.colorPalettes['木']).toBe('#43A047');
|
||||
expect(config.layers.some((layer) => layer.type === 'centerIcon')).toBe(true);
|
||||
expect(config.layers.some((layer) => layer.type === 'degreeRing')).toBe(true);
|
||||
});
|
||||
|
||||
it('缺少必填字段时应抛错', () => {
|
||||
const raw = '{ "background": "#000", "theme": { "colorPalettes": {} }, "layers": [] }';
|
||||
expect(() => parseConfig(raw)).toThrow('name 为必填字符串');
|
||||
});
|
||||
});
|
||||
60
tests/degreeRing.test.ts
Normal file
60
tests/degreeRing.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { buildDegreeRing } from '../src/degreeRing';
|
||||
import type { DegreeRingConfig } from '../src/types';
|
||||
|
||||
describe('degreeRing', () => {
|
||||
const baseConfig: DegreeRingConfig = {
|
||||
rInner: 90,
|
||||
rOuter: 100,
|
||||
showDegree: 1,
|
||||
mode: 'both',
|
||||
opacity: 0.3,
|
||||
tickLength: 6,
|
||||
tickLengthStep: 1,
|
||||
majorTick: 10,
|
||||
minorTick: 5,
|
||||
microTick: 1,
|
||||
tickColor: '#ffffff',
|
||||
ringColor: '#ffffff',
|
||||
};
|
||||
|
||||
it('应生成主/次/微刻度并支持 both 模式', () => {
|
||||
const data = buildDegreeRing(baseConfig);
|
||||
const angle0 = data.ticks.filter((tick) => tick.angle === 0);
|
||||
|
||||
expect(angle0).toHaveLength(2);
|
||||
expect(angle0[0].length).toBe(6);
|
||||
expect(angle0[1].length).toBe(6);
|
||||
});
|
||||
|
||||
it('应按 majorTick 生成度数标签', () => {
|
||||
const data = buildDegreeRing(baseConfig);
|
||||
expect(data.labels?.length).toBe(36);
|
||||
expect(data.labels?.[0].text).toBe('0');
|
||||
expect(data.labels?.[1].text).toBe('10');
|
||||
});
|
||||
|
||||
it('inner 模式应从 rInner 向外绘制', () => {
|
||||
const data = buildDegreeRing({ ...baseConfig, mode: 'inner' });
|
||||
const tick = data.ticks[0];
|
||||
expect(tick.startR).toBe(baseConfig.rInner);
|
||||
expect(tick.endR).toBe(baseConfig.rInner + tick.length);
|
||||
});
|
||||
|
||||
it('tickLengthStep 应影响次/微刻度长度', () => {
|
||||
const data = buildDegreeRing({ ...baseConfig, tickLength: 6, tickLengthStep: 2 });
|
||||
const major = data.ticks.find((tick) => tick.angle === 0);
|
||||
const minor = data.ticks.find((tick) => tick.angle === 5 && tick.type === 'minor');
|
||||
const micro = data.ticks.find((tick) => tick.angle === 1 && tick.type === 'micro');
|
||||
|
||||
expect(major?.length).toBe(6);
|
||||
expect(minor?.length).toBe(4);
|
||||
expect(micro?.length).toBe(2);
|
||||
});
|
||||
|
||||
it('度数标签应生成 textPath', () => {
|
||||
const data = buildDegreeRing(baseConfig);
|
||||
const label = data.labels?.[0];
|
||||
expect(label?.textPath).toContain('M ');
|
||||
});
|
||||
});
|
||||
26
tests/multiTextParser.test.ts
Normal file
26
tests/multiTextParser.test.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { splitMultiTextUnits } from '../src/multiTextParser';
|
||||
|
||||
describe('multiTextParser', () => {
|
||||
it('应按布局比例分配角度', () => {
|
||||
const units = splitMultiTextUnits('甲乙|子|丙丁', 0, 120);
|
||||
expect(units).toHaveLength(3);
|
||||
expect(units[0].aStart).toBe(0);
|
||||
expect(units[0].aEnd).toBe(30);
|
||||
expect(units[1].aStart).toBe(30);
|
||||
expect(units[1].aEnd).toBe(90);
|
||||
expect(units[2].aStart).toBe(90);
|
||||
expect(units[2].aEnd).toBe(120);
|
||||
});
|
||||
|
||||
it('应识别 SVG 文件并拼接路径', () => {
|
||||
const units = splitMultiTextUnits('a.svg|b', 0, 60, 'src/assets/icons');
|
||||
expect(units[0].isSvg).toBe(true);
|
||||
expect(units[0].svgPath).toBe('src/assets/icons/a.svg');
|
||||
expect(units[1].isSvg).toBe(false);
|
||||
});
|
||||
|
||||
it('空内容应返回空数组', () => {
|
||||
expect(splitMultiTextUnits('', 0, 60)).toEqual([]);
|
||||
});
|
||||
});
|
||||
96
tests/sectorBuilder.test.ts
Normal file
96
tests/sectorBuilder.test.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ColorResolver } from '../src/colorResolver';
|
||||
import { SectorBuilder } from '../src/sectorBuilder';
|
||||
import type { SectorLayerConfig, ThemeConfig } from '../src/types';
|
||||
|
||||
describe('sectorBuilder', () => {
|
||||
const theme: ThemeConfig = {
|
||||
colorPalettes: {
|
||||
A: '#111111',
|
||||
B: '#222222',
|
||||
},
|
||||
};
|
||||
|
||||
it('应生成正确数量的扇区', () => {
|
||||
const resolver = new ColorResolver(theme, '#000000');
|
||||
const builder = new SectorBuilder(resolver);
|
||||
const layer: SectorLayerConfig = {
|
||||
divisions: 4,
|
||||
rInner: 0,
|
||||
rOuter: 10,
|
||||
};
|
||||
|
||||
const sectors = builder.buildLayer(layer, 0);
|
||||
expect(sectors).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('应遵循 sector > layer > background 的颜色优先级', () => {
|
||||
const resolver = new ColorResolver(theme, '#000000');
|
||||
const builder = new SectorBuilder(resolver);
|
||||
const layer: SectorLayerConfig = {
|
||||
divisions: 4,
|
||||
rInner: 0,
|
||||
rOuter: 10,
|
||||
colorRef: 'A',
|
||||
num: 2,
|
||||
interval: 1,
|
||||
sectors: [{ content: 'x', colorRef: 'B' }],
|
||||
};
|
||||
|
||||
const sectors = builder.buildLayer(layer, 0);
|
||||
expect(sectors[0].fill).toBe('#222222');
|
||||
expect(sectors[1].fill).toBe('#111111');
|
||||
expect(sectors[2].fill).toBe('#000000');
|
||||
});
|
||||
|
||||
it('groupSplit=false 时应隐藏组内分割线', () => {
|
||||
const resolver = new ColorResolver(theme, '#000000');
|
||||
const builder = new SectorBuilder(resolver);
|
||||
const layer: SectorLayerConfig = {
|
||||
divisions: 4,
|
||||
rInner: 0,
|
||||
rOuter: 10,
|
||||
num: 2,
|
||||
interval: 1,
|
||||
groupSplit: false,
|
||||
};
|
||||
|
||||
const sectors = builder.buildLayer(layer, 0);
|
||||
expect(sectors[0].groupSplitVisible).toBe(false);
|
||||
expect(sectors[1].groupSplitVisible).toBe(true);
|
||||
expect(sectors[2].groupSplitVisible).toBe(true);
|
||||
});
|
||||
|
||||
it('应生成多文本单元并按角度分配', () => {
|
||||
const resolver = new ColorResolver(theme, '#000000');
|
||||
const builder = new SectorBuilder(resolver);
|
||||
const layer: SectorLayerConfig = {
|
||||
divisions: 4,
|
||||
rInner: 0,
|
||||
rOuter: 10,
|
||||
sectors: [{ content: '甲乙|子|丙丁' }],
|
||||
};
|
||||
|
||||
const sector = builder.buildLayer(layer, 0)[0];
|
||||
expect(sector.textUnits).toHaveLength(3);
|
||||
expect(sector.textUnits?.[0].aStart).toBeCloseTo(0);
|
||||
expect(sector.textUnits?.[0].aEnd).toBeCloseTo(22.5);
|
||||
expect(sector.textUnits?.[1].aStart).toBeCloseTo(22.5);
|
||||
expect(sector.textUnits?.[1].aEnd).toBeCloseTo(67.5);
|
||||
});
|
||||
|
||||
it('应识别单文本 SVG 内容并生成路径', () => {
|
||||
const resolver = new ColorResolver(theme, '#000000');
|
||||
const builder = new SectorBuilder(resolver);
|
||||
const layer: SectorLayerConfig = {
|
||||
divisions: 1,
|
||||
rInner: 0,
|
||||
rOuter: 10,
|
||||
sectors: [{ content: 'icon.svg' }],
|
||||
};
|
||||
|
||||
const sector = builder.buildLayer(layer, 0)[0];
|
||||
expect(sector.isSvgContent).toBe(true);
|
||||
expect(sector.svgPath).toBe('src/assets/icons/icon.svg');
|
||||
});
|
||||
});
|
||||
@@ -5,422 +5,88 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ref } from 'vue';
|
||||
import { useLuopan } from '../src/composables/useLuopan';
|
||||
import type { Example, TextRadialPosition } from '../src/types';
|
||||
import type { LuopanConfig, TextRadialPosition } from '../src/types';
|
||||
|
||||
const createMockConfig = (): LuopanConfig => ({
|
||||
name: '测试配置',
|
||||
background: '#000000',
|
||||
theme: {
|
||||
colorPalettes: {
|
||||
A: '#111111',
|
||||
},
|
||||
},
|
||||
layers: [
|
||||
{
|
||||
divisions: 4,
|
||||
rInner: 0,
|
||||
rOuter: 80,
|
||||
sectors: [{ content: '甲' }, { content: '乙' }, { content: '丙' }, { content: '丁' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
describe('useLuopan', () => {
|
||||
const createMockExample = (): Example => ({
|
||||
name: '测试示例',
|
||||
angles: [0, 90, 180, 270, 360],
|
||||
radii: [50, 100, 150],
|
||||
it('应该返回所有必需的属性和方法', async () => {
|
||||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||||
const result = useLuopan(createMockConfig(), textRadialPosition);
|
||||
await result.reload();
|
||||
|
||||
expect(result).toHaveProperty('anglesDeg');
|
||||
expect(result).toHaveProperty('rings');
|
||||
expect(result).toHaveProperty('outerMost');
|
||||
expect(result).toHaveProperty('sectors');
|
||||
expect(result).toHaveProperty('degreeRing');
|
||||
expect(result).toHaveProperty('centerIcon');
|
||||
expect(result).toHaveProperty('toXY');
|
||||
});
|
||||
|
||||
describe('基本功能', () => {
|
||||
it('应该返回所有必需的属性和方法', () => {
|
||||
const example = ref(createMockExample());
|
||||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||||
|
||||
const result = useLuopan(example, textRadialPosition);
|
||||
it('anglesDeg 应该按 divisions 生成角度数组', async () => {
|
||||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||||
const { anglesDeg, reload } = useLuopan(createMockConfig(), textRadialPosition);
|
||||
await reload();
|
||||
|
||||
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);
|
||||
});
|
||||
expect(anglesDeg.value).toEqual([0, 90, 180, 270, 360]);
|
||||
});
|
||||
|
||||
describe('扇区生成', () => {
|
||||
it('应该生成正确数量的扇区', () => {
|
||||
const example = ref(createMockExample());
|
||||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||||
|
||||
const { sectors } = useLuopan(example, textRadialPosition);
|
||||
it('rings 应该返回 rOuter 列表', async () => {
|
||||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||||
const { rings, reload } = useLuopan(createMockConfig(), textRadialPosition);
|
||||
await reload();
|
||||
|
||||
// 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 ');
|
||||
});
|
||||
});
|
||||
expect(rings.value).toEqual([80]);
|
||||
});
|
||||
|
||||
describe('文字位置模式', () => {
|
||||
it('应该在 middle 模式下使用中点位置', () => {
|
||||
const example = ref(createMockExample());
|
||||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||||
|
||||
const { sectors } = useLuopan(example, textRadialPosition);
|
||||
it('outerMost 应该返回最大半径', async () => {
|
||||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||||
const { outerMost, reload } = useLuopan(createMockConfig(), textRadialPosition);
|
||||
await reload();
|
||||
|
||||
// 第二层的中点应该在 (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
|
||||
});
|
||||
expect(outerMost.value).toBe(80);
|
||||
});
|
||||
|
||||
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);
|
||||
it('应该生成正确数量的扇区', async () => {
|
||||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||||
const { sectors, reload } = useLuopan(createMockConfig(), textRadialPosition);
|
||||
await reload();
|
||||
|
||||
// 某些窄扇区应该被设置为竖排
|
||||
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);
|
||||
});
|
||||
});
|
||||
expect(sectors.value.length).toBe(4);
|
||||
});
|
||||
|
||||
describe('内部填色', () => {
|
||||
it('某些扇区应该有内部填色', () => {
|
||||
const example = ref(createMockExample());
|
||||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||||
|
||||
const { sectors } = useLuopan(example, textRadialPosition);
|
||||
it('扇区应包含必要字段', async () => {
|
||||
const textRadialPosition = ref<TextRadialPosition>('middle');
|
||||
const { sectors, reload } = useLuopan(createMockConfig(), textRadialPosition);
|
||||
await reload();
|
||||
|
||||
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层
|
||||
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('path');
|
||||
expect(sector).toHaveProperty('textPath');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user