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

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)
})
})
})