Files
lupin-demo/tests/Luopan.test.ts
2026-01-21 13:22:26 +08:00

546 lines
17 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.

/**
* Luopan 组件单元测试
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mount } from '@vue/test-utils';
import Luopan from '../src/Luopan.vue';
import { EXAMPLES } from '../src/constants';
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}`);
});
});
describe('示例切换', () => {
it('应该渲染示例切换按钮', () => {
const wrapper = mount(Luopan);
const buttons = wrapper.findAll('.toolbar button:not(.zoom-controls button)');
expect(buttons.length).toBeGreaterThanOrEqual(EXAMPLES.length);
});
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');
}
});
});
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');
// 辅助线可能被隐藏或不渲染
});
});
describe('文字位置模式', () => {
it('应该有文字位置选择器', () => {
const wrapper = mount(Luopan);
const select = wrapper.find('select');
expect(select.exists()).toBe(true);
});
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');
});
});
describe('缩放功能', () => {
it('应该渲染缩放控件', () => {
const wrapper = mount(Luopan);
const zoomControls = wrapper.find('.zoom-controls');
expect(zoomControls.exists()).toBe(true);
});
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();
});
});
describe('鼠标滚轮缩放', () => {
it('应该监听滚轮事件', () => {
const wrapper = mount(Luopan);
const container = wrapper.find('.svg-container');
expect(container.exists()).toBe(true);
});
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();
});
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);
});
});
});