import { beforeEach, describe, expect, it, vi } from 'vitest' import { useRuntimeConsole } from '@/composables/useRuntimeConsole' describe('useRuntimeConsole', () => { let console_: ReturnType beforeEach(() => { console_ = useRuntimeConsole() console_.clear() console_.levelFilter.value = '' console_.categoryFilter.value = '' console_.searchText.value = '' }) // ──── log() ──── describe('log() 添加运行日志', () => { it('应添加一条日志到列表头部', () => { console_.log('INFO', '系统', '启动完成') expect(console_.entries.value).toHaveLength(1) expect(console_.entries.value[0]).toMatchObject({ level: 'INFO', category: '系统', message: '启动完成', }) }) it('应支持 quality 和 source 可选参数', () => { console_.log('WARN', '信号', '信号质量下降', 'BAD', 'PLC-01') const entry = console_.entries.value[0] expect(entry.quality).toBe('BAD') expect(entry.source).toBe('PLC-01') }) it('quality 和 source 未提供时应为 undefined', () => { console_.log('INFO', '系统', '正常运行') const entry = console_.entries.value[0] expect(entry.quality).toBeUndefined() expect(entry.source).toBeUndefined() }) it('新日志应排在最前面', () => { console_.log('INFO', '系统', '第一条') console_.log('ERROR', '告警', '第二条') expect(console_.entries.value[0].message).toBe('第二条') expect(console_.entries.value[1].message).toBe('第一条') }) it('超过 1000 条时应截断旧日志', () => { for (let i = 0; i < 1010; i++) { console_.log('INFO', '系统', `消息${i}`) } expect(console_.entries.value).toHaveLength(1000) expect(console_.entries.value[0].message).toBe('消息1009') }) it('每条日志应有唯一 id 和时间戳', () => { console_.log('INFO', '系统', '消息A') console_.log('INFO', '系统', '消息B') const [a, b] = console_.entries.value expect(a.id).not.toBe(b.id) expect(a.timestamp).toBeTruthy() expect(b.timestamp).toBeTruthy() }) }) // ──── 便捷方法 info/warn/error ──── describe('info() 便捷方法', () => { it('应以 INFO 级别记录', () => { console_.info('系统', '信息消息') expect(console_.entries.value[0].level).toBe('INFO') expect(console_.entries.value[0].message).toBe('信息消息') }) it('应支持 quality 参数', () => { console_.info('信号', '信号正常', 'GOOD') expect(console_.entries.value[0].quality).toBe('GOOD') }) }) describe('warn() 便捷方法', () => { it('应以 WARN 级别记录', () => { console_.warn('系统', '警告消息') expect(console_.entries.value[0].level).toBe('WARN') }) it('应支持 quality 参数', () => { console_.warn('信号', '信号不确定', 'UNCERTAIN') expect(console_.entries.value[0].quality).toBe('UNCERTAIN') }) }) describe('error() 便捷方法', () => { it('应以 ERROR 级别记录', () => { console_.error('系统', '错误消息') expect(console_.entries.value[0].level).toBe('ERROR') }) it('应支持 source 参数', () => { console_.error('通信', '连接超时', 'PLC-02') expect(console_.entries.value[0].source).toBe('PLC-02') }) it('quality 应为 undefined(error 不传 quality)', () => { console_.error('系统', '错误消息', 'source') expect(console_.entries.value[0].quality).toBeUndefined() }) }) // ──── clear() ──── describe('clear() 清空日志', () => { it('应清空所有条目', () => { console_.info('系统', '消息1') console_.warn('告警', '消息2') console_.clear() expect(console_.entries.value).toHaveLength(0) }) }) // ──── filteredEntries ──── describe('filteredEntries 过滤逻辑', () => { beforeEach(() => { console_.info('系统', '系统启动') console_.warn('信号', '信号波动') console_.error('通信', '连接中断', 'PLC-01') console_.info('信号', '信号恢复正常') }) it('无过滤条件时应返回全部', () => { expect(console_.filteredEntries.value).toHaveLength(4) }) it('按 levelFilter 过滤级别', () => { console_.levelFilter.value = 'INFO' expect(console_.filteredEntries.value).toHaveLength(2) expect(console_.filteredEntries.value.every(e => e.level === 'INFO')).toBe(true) }) it('按 categoryFilter 过滤分类', () => { console_.categoryFilter.value = '信号' expect(console_.filteredEntries.value).toHaveLength(2) expect(console_.filteredEntries.value.every(e => e.category === '信号')).toBe(true) }) it('按 searchText 模糊搜索 message', () => { console_.searchText.value = '连接' expect(console_.filteredEntries.value).toHaveLength(1) expect(console_.filteredEntries.value[0].message).toBe('连接中断') }) it('按 searchText 模糊搜索 source', () => { console_.searchText.value = 'PLC' expect(console_.filteredEntries.value).toHaveLength(1) expect(console_.filteredEntries.value[0].source).toBe('PLC-01') }) it('按 searchText 模糊搜索 category', () => { console_.searchText.value = '通信' expect(console_.filteredEntries.value).toHaveLength(1) expect(console_.filteredEntries.value[0].category).toBe('通信') }) it('searchText 不区分大小写', () => { console_.log('INFO', 'System', 'Connection OK', undefined, 'Device-A') console_.searchText.value = 'connection' expect(console_.filteredEntries.value).toHaveLength(1) expect(console_.filteredEntries.value[0].message).toBe('Connection OK') }) it('多个过滤条件组合使用', () => { console_.levelFilter.value = 'INFO' console_.categoryFilter.value = '信号' expect(console_.filteredEntries.value).toHaveLength(1) expect(console_.filteredEntries.value[0].message).toBe('信号恢复正常') }) it('三个过滤条件同时使用', () => { console_.levelFilter.value = 'INFO' console_.categoryFilter.value = '信号' console_.searchText.value = '恢复' expect(console_.filteredEntries.value).toHaveLength(1) expect(console_.filteredEntries.value[0].message).toBe('信号恢复正常') }) it('过滤条件无匹配时返回空数组', () => { console_.levelFilter.value = 'ERROR' console_.categoryFilter.value = '系统' expect(console_.filteredEntries.value).toHaveLength(0) }) }) // ──── categories ──── describe('categories 计算属性', () => { it('空日志时返回空数组', () => { expect(console_.categories.value).toEqual([]) }) it('应返回去重排序后的分类列表', () => { console_.info('系统', '消息1') console_.info('信号', '消息2') console_.warn('系统', '消息3') console_.error('通信', '消息4') expect(console_.categories.value).toEqual(['信号', '系统', '通信']) }) }) // ──── levelCounts ──── describe('levelCounts 计算属性', () => { it('空日志时所有计数为零', () => { expect(console_.levelCounts.value).toEqual({ INFO: 0, WARN: 0, ERROR: 0 }) }) it('应正确统计各级别数量', () => { console_.info('系统', '消息1') console_.info('系统', '消息2') console_.warn('告警', '消息3') console_.error('通信', '消息4') console_.error('通信', '消息5') console_.error('通信', '消息6') expect(console_.levelCounts.value).toEqual({ INFO: 2, WARN: 1, ERROR: 3, }) }) }) // ──── exportJSON() ──── describe('exportJSON() 导出日志', () => { it('应创建 Blob 下载并清理', () => { console_.info('系统', '测试消息') 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') console_.exportJSON() expect(mockCreateObjectURL).toHaveBeenCalled() expect(mockClick).toHaveBeenCalled() expect(mockRevokeObjectURL).toHaveBeenCalledWith('blob:test') mockAppendChild.mockRestore() mockRemoveChild.mockRestore() mockCreateElement.mockRestore() mockRevokeObjectURL.mockRestore() mockCreateObjectURL.mockRestore() }) }) // ──── 单例共享 ──── describe('单例模式', () => { it('多次调用 useRuntimeConsole() 应共享同一份状态', () => { const c1 = useRuntimeConsole() const c2 = useRuntimeConsole() c1.info('系统', '测试') expect(c2.entries.value).toHaveLength(c1.entries.value.length) expect(c2.entries.value[0].id).toBe(c1.entries.value[0].id) }) }) })