183 lines
5.8 KiB
TypeScript
183 lines
5.8 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { useOperationLog } from '@/composables/useOperationLog'
|
|
|
|
describe('useOperationLog', () => {
|
|
let opLog: ReturnType<typeof useOperationLog>
|
|
|
|
beforeEach(() => {
|
|
opLog = useOperationLog()
|
|
opLog.clear()
|
|
opLog.filterType.value = ''
|
|
opLog.searchText.value = ''
|
|
})
|
|
|
|
// ──── log() ────
|
|
|
|
describe('log() 添加日志条目', () => {
|
|
it('应添加一条日志到 entries 列表头部', () => {
|
|
opLog.log('创建', '图层A', '新建矩形图层')
|
|
|
|
expect(opLog.entries.value).toHaveLength(1)
|
|
expect(opLog.entries.value[0]).toMatchObject({
|
|
action: '创建',
|
|
target: '图层A',
|
|
detail: '新建矩形图层',
|
|
})
|
|
expect(opLog.entries.value[0].id).toBeTruthy()
|
|
expect(opLog.entries.value[0].timestamp).toBeTruthy()
|
|
})
|
|
|
|
it('新日志应排在列表最前面(降序)', () => {
|
|
opLog.log('创建', '图层A', '第一条')
|
|
opLog.log('删除', '图层B', '第二条')
|
|
|
|
expect(opLog.entries.value[0].action).toBe('删除')
|
|
expect(opLog.entries.value[1].action).toBe('创建')
|
|
})
|
|
|
|
it('每条日志应有唯一 id', () => {
|
|
opLog.log('创建', '图层A', '详情A')
|
|
opLog.log('创建', '图层B', '详情B')
|
|
|
|
const ids = opLog.entries.value.map(e => e.id)
|
|
expect(new Set(ids).size).toBe(2)
|
|
})
|
|
|
|
it('超过 500 条时应截断旧日志', () => {
|
|
for (let i = 0; i < 510; i++) {
|
|
opLog.log('操作', `目标${i}`, `详情${i}`)
|
|
}
|
|
|
|
expect(opLog.entries.value).toHaveLength(500)
|
|
// 最新的应该在最前面
|
|
expect(opLog.entries.value[0].detail).toBe('详情509')
|
|
})
|
|
})
|
|
|
|
// ──── clear() ────
|
|
|
|
describe('clear() 清空日志', () => {
|
|
it('应清空所有条目', () => {
|
|
opLog.log('创建', '图层', '详情')
|
|
opLog.log('删除', '图层', '详情')
|
|
expect(opLog.entries.value.length).toBeGreaterThan(0)
|
|
|
|
opLog.clear()
|
|
|
|
expect(opLog.entries.value).toHaveLength(0)
|
|
})
|
|
|
|
it('清空后 filteredEntries 也应为空', () => {
|
|
opLog.log('创建', '图层', '详情')
|
|
opLog.clear()
|
|
|
|
expect(opLog.filteredEntries.value).toHaveLength(0)
|
|
})
|
|
})
|
|
|
|
// ──── filteredEntries ────
|
|
|
|
describe('filteredEntries 过滤逻辑', () => {
|
|
beforeEach(() => {
|
|
opLog.log('创建', '图层A', '新建矩形')
|
|
opLog.log('删除', '图层B', '删除圆形')
|
|
opLog.log('修改', '图层C', '修改颜色属性')
|
|
opLog.log('创建', '图层D', '新建文本')
|
|
})
|
|
|
|
it('无过滤条件时应返回全部条目', () => {
|
|
expect(opLog.filteredEntries.value).toHaveLength(4)
|
|
})
|
|
|
|
it('按 filterType 过滤 action', () => {
|
|
opLog.filterType.value = '创建'
|
|
|
|
expect(opLog.filteredEntries.value).toHaveLength(2)
|
|
expect(opLog.filteredEntries.value.every(e => e.action === '创建')).toBe(true)
|
|
})
|
|
|
|
it('按 searchText 模糊搜索 detail', () => {
|
|
opLog.searchText.value = '矩形'
|
|
|
|
expect(opLog.filteredEntries.value).toHaveLength(1)
|
|
expect(opLog.filteredEntries.value[0].detail).toBe('新建矩形')
|
|
})
|
|
|
|
it('按 searchText 模糊搜索 target', () => {
|
|
opLog.searchText.value = '图层B'
|
|
|
|
expect(opLog.filteredEntries.value).toHaveLength(1)
|
|
expect(opLog.filteredEntries.value[0].target).toBe('图层B')
|
|
})
|
|
|
|
it('searchText 不区分大小写', () => {
|
|
opLog.log('创建', 'LayerX', 'New Rectangle')
|
|
opLog.searchText.value = 'rectangle'
|
|
|
|
expect(opLog.filteredEntries.value).toHaveLength(1)
|
|
expect(opLog.filteredEntries.value[0].detail).toBe('New Rectangle')
|
|
})
|
|
|
|
it('同时使用 filterType 和 searchText', () => {
|
|
opLog.filterType.value = '创建'
|
|
opLog.searchText.value = '文本'
|
|
|
|
expect(opLog.filteredEntries.value).toHaveLength(1)
|
|
expect(opLog.filteredEntries.value[0].target).toBe('图层D')
|
|
})
|
|
|
|
it('过滤条件无匹配时返回空数组', () => {
|
|
opLog.filterType.value = '不存在的操作'
|
|
|
|
expect(opLog.filteredEntries.value).toHaveLength(0)
|
|
})
|
|
})
|
|
|
|
// ──── exportJSON() ────
|
|
|
|
describe('exportJSON() 导出日志', () => {
|
|
it('应创建下载链接并触发点击', () => {
|
|
opLog.log('创建', '图层', '详情')
|
|
|
|
const mockClick = vi.fn()
|
|
const mockAppendChild = vi.spyOn(document.body, 'appendChild').mockImplementation(node => node)
|
|
const mockRemoveChild = vi.spyOn(document.body, 'removeChild').mockImplementation(node => node)
|
|
const mockCreateElement = vi.spyOn(document, 'createElement').mockReturnValue({
|
|
get href() { return '' },
|
|
set href(_: string) {},
|
|
get download() { return '' },
|
|
set download(_: string) {},
|
|
click: mockClick,
|
|
} as unknown as HTMLAnchorElement)
|
|
const mockRevokeObjectURL = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {})
|
|
const mockCreateObjectURL = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:test')
|
|
|
|
opLog.exportJSON()
|
|
|
|
expect(mockCreateObjectURL).toHaveBeenCalled()
|
|
expect(mockClick).toHaveBeenCalled()
|
|
expect(mockRevokeObjectURL).toHaveBeenCalledWith('blob:test')
|
|
|
|
mockAppendChild.mockRestore()
|
|
mockRemoveChild.mockRestore()
|
|
mockCreateElement.mockRestore()
|
|
mockRevokeObjectURL.mockRestore()
|
|
mockCreateObjectURL.mockRestore()
|
|
})
|
|
})
|
|
|
|
// ──── 单例共享 ────
|
|
|
|
describe('单例模式', () => {
|
|
it('多次调用 useOperationLog() 应共享同一份状态', () => {
|
|
const log1 = useOperationLog()
|
|
const log2 = useOperationLog()
|
|
|
|
log1.log('测试', '目标', '详情')
|
|
|
|
expect(log2.entries.value).toHaveLength(log1.entries.value.length)
|
|
expect(log2.entries.value[0].id).toBe(log1.entries.value[0].id)
|
|
})
|
|
})
|
|
})
|