305 lines
9.5 KiB
TypeScript
305 lines
9.5 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||
import { useRuntimeConsole } from '@/composables/useRuntimeConsole'
|
||
|
||
describe('useRuntimeConsole', () => {
|
||
let console_: ReturnType<typeof useRuntimeConsole>
|
||
|
||
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)
|
||
})
|
||
})
|
||
})
|