Files
test/tests/unit/composables/useComponentTemplates.test.ts
2026-04-08 21:26:18 +08:00

230 lines
6.7 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.
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> = {}): 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<typeof useComponentTemplates>
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)
})
})
// ──── templatessortedTemplates────
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)
})
})
})