230 lines
6.7 KiB
TypeScript
230 lines
6.7 KiB
TypeScript
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)
|
||
})
|
||
})
|
||
})
|