import { beforeEach, describe, expect, it, vi } from 'vitest' import { useOperationLog } from '@/composables/useOperationLog' describe('useOperationLog', () => { let opLog: ReturnType 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) }) }) })