all
This commit is contained in:
229
tests/unit/composables/useComponentTemplates.test.ts
Normal file
229
tests/unit/composables/useComponentTemplates.test.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
// ──── 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user