import type { Layer } from '@/components/editor/canvas/types' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { useComponentTemplates } from '@/composables/useComponentTemplates' const RE_TPL_PREFIX = /^tpl_/ const RE_DEFAULT_NAME = /^模板 \d+$/ // 模拟 @vueuse/core 的 useLocalStorage // useComponentTemplates 通过 useLocalStorage 管理持久化, // 测试中用 ref 替代以隔离 localStorage 副作用 vi.mock('@vueuse/core', async () => { const { ref } = await import('vue') return { useLocalStorage: (_key: string, defaultValue: any) => ref(defaultValue), } }) function createMockLayer(overrides: Partial = {}): Layer { return { id: `layer_${Math.random().toString(36).slice(2, 6)}`, type: 'rect', x: 0, y: 0, width: 100, height: 50, ...overrides, } } describe('useComponentTemplates', () => { let tpl: ReturnType beforeEach(() => { vi.useFakeTimers() tpl = useComponentTemplates() }) afterEach(() => { vi.useRealTimers() }) // ──── saveTemplate() ──── describe('saveTemplate() 保存模板', () => { it('应保存模板并返回模板对象', () => { const layers = [createMockLayer()] const result = tpl.saveTemplate('测试模板', layers) expect(result).not.toBeNull() expect(result!.name).toBe('测试模板') expect(result!.layers).toHaveLength(1) expect(result!.id).toMatch(RE_TPL_PREFIX) expect(result!.createdAt).toBeTypeOf('number') }) it('空图层数组时应返回 null', () => { const result = tpl.saveTemplate('空模板', []) expect(result).toBeNull() }) it('名称为空字符串时应使用默认名称', () => { const layers = [createMockLayer()] const result = tpl.saveTemplate('', layers) expect(result).not.toBeNull() expect(result!.name).toMatch(RE_DEFAULT_NAME) }) it('名称含前后空格时应自动 trim', () => { const layers = [createMockLayer()] const result = tpl.saveTemplate(' 模板名称 ', layers) expect(result!.name).toBe('模板名称') }) it('应深拷贝图层数据,修改原图层不影响模板', () => { const layer = createMockLayer({ x: 10 }) const layers = [layer] const result = tpl.saveTemplate('深拷贝测试', layers) // 修改原图层 layer.x = 999 expect(result!.layers[0].x).toBe(10) }) it('多次保存应生成不同的 id', () => { const layers = [createMockLayer()] vi.setSystemTime(new Date('2026-01-01T00:00:00Z')) const t1 = tpl.saveTemplate('模板1', layers) vi.setSystemTime(new Date('2026-01-01T00:00:01Z')) const t2 = tpl.saveTemplate('模板2', layers) expect(t1!.id).not.toBe(t2!.id) }) }) // ──── templates(sortedTemplates)──── describe('templates 排序', () => { it('空状态返回空数组', () => { expect(tpl.templates.value).toEqual([]) }) it('应按 createdAt 降序排列(新模板在前)', () => { const layers = [createMockLayer()] vi.setSystemTime(new Date('2026-01-01T00:00:00Z')) tpl.saveTemplate('旧模板', layers) vi.setSystemTime(new Date('2026-01-02T00:00:00Z')) tpl.saveTemplate('新模板', layers) expect(tpl.templates.value).toHaveLength(2) expect(tpl.templates.value[0].name).toBe('新模板') expect(tpl.templates.value[1].name).toBe('旧模板') }) }) // ──── getTemplate() ──── describe('getTemplate() 按 id 获取模板', () => { it('存在时返回模板对象', () => { const layers = [createMockLayer()] const saved = tpl.saveTemplate('查找测试', layers)! const found = tpl.getTemplate(saved.id) expect(found).not.toBeNull() expect(found!.id).toBe(saved.id) expect(found!.name).toBe('查找测试') }) it('不存在时返回 null', () => { expect(tpl.getTemplate('nonexistent_id')).toBeNull() }) }) // ──── removeTemplate() ──── describe('removeTemplate() 删除模板', () => { it('应按 id 删除指定模板', () => { const layers = [createMockLayer()] const t1 = tpl.saveTemplate('模板1', layers)! const t2 = tpl.saveTemplate('模板2', layers)! tpl.removeTemplate(t1.id) expect(tpl.templates.value).toHaveLength(1) expect(tpl.templates.value[0].id).toBe(t2.id) }) it('删除不存在的 id 不应报错', () => { const layers = [createMockLayer()] tpl.saveTemplate('模板', layers) expect(() => tpl.removeTemplate('nonexistent')).not.toThrow() expect(tpl.templates.value).toHaveLength(1) }) }) // ──── renameTemplate() ──── describe('renameTemplate() 重命名模板', () => { it('应更新指定模板的名称', () => { const layers = [createMockLayer()] const saved = tpl.saveTemplate('原始名称', layers)! tpl.renameTemplate(saved.id, '新名称') const updated = tpl.getTemplate(saved.id) expect(updated!.name).toBe('新名称') }) it('名称含空格时应自动 trim', () => { const layers = [createMockLayer()] const saved = tpl.saveTemplate('原始', layers)! tpl.renameTemplate(saved.id, ' 修改后 ') expect(tpl.getTemplate(saved.id)!.name).toBe('修改后') }) it('重命名不存在的 id 不应报错', () => { expect(() => tpl.renameTemplate('nonexistent', '新名称')).not.toThrow() }) it('重命名不应影响其他字段', () => { const layers = [createMockLayer({ x: 42 })] const saved = tpl.saveTemplate('原始', layers)! const originalCreatedAt = saved.createdAt tpl.renameTemplate(saved.id, '新名称') const updated = tpl.getTemplate(saved.id)! expect(updated.createdAt).toBe(originalCreatedAt) expect(updated.layers[0].x).toBe(42) }) }) // ──── 边界情况 ──── describe('边界情况', () => { it('保存含多个图层的模板', () => { const layers = [ createMockLayer({ x: 0, y: 0 }), createMockLayer({ x: 100, y: 100 }), createMockLayer({ x: 200, y: 200 }), ] const result = tpl.saveTemplate('多图层模板', layers) expect(result!.layers).toHaveLength(3) }) it('图层含嵌套 config 对象时应深拷贝', () => { const config = { fill: 'red', nested: { value: 1 } } const layer = createMockLayer({ config }) const result = tpl.saveTemplate('嵌套配置', [layer]) // 修改原始 config config.nested.value = 999 expect(result!.layers[0].config!.nested.value).toBe(1) }) }) })