546 lines
17 KiB
TypeScript
546 lines
17 KiB
TypeScript
/**
|
||
* 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);
|
||
});
|
||
});
|
||
});
|