/** * 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); }); }); });