all
This commit is contained in:
229
tests/unit/composables/useComponentTemplates.test.ts
Normal file
229
tests/unit/composables/useComponentTemplates.test.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
import type { Layer } from '@/components/editor/canvas/types'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useComponentTemplates } from '@/composables/useComponentTemplates'
|
||||
|
||||
const RE_TPL_PREFIX = /^tpl_/
|
||||
const RE_DEFAULT_NAME = /^模板 \d+$/
|
||||
|
||||
// 模拟 @vueuse/core 的 useLocalStorage
|
||||
// useComponentTemplates 通过 useLocalStorage 管理持久化,
|
||||
// 测试中用 ref 替代以隔离 localStorage 副作用
|
||||
vi.mock('@vueuse/core', async () => {
|
||||
const { ref } = await import('vue')
|
||||
return {
|
||||
useLocalStorage: (_key: string, defaultValue: any) => ref(defaultValue),
|
||||
}
|
||||
})
|
||||
|
||||
function createMockLayer(overrides: Partial<Layer> = {}): Layer {
|
||||
return {
|
||||
id: `layer_${Math.random().toString(36).slice(2, 6)}`,
|
||||
type: 'rect',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
height: 50,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('useComponentTemplates', () => {
|
||||
let tpl: ReturnType<typeof useComponentTemplates>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
tpl = useComponentTemplates()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
// ──── saveTemplate() ────
|
||||
|
||||
describe('saveTemplate() 保存模板', () => {
|
||||
it('应保存模板并返回模板对象', () => {
|
||||
const layers = [createMockLayer()]
|
||||
const result = tpl.saveTemplate('测试模板', layers)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.name).toBe('测试模板')
|
||||
expect(result!.layers).toHaveLength(1)
|
||||
expect(result!.id).toMatch(RE_TPL_PREFIX)
|
||||
expect(result!.createdAt).toBeTypeOf('number')
|
||||
})
|
||||
|
||||
it('空图层数组时应返回 null', () => {
|
||||
const result = tpl.saveTemplate('空模板', [])
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('名称为空字符串时应使用默认名称', () => {
|
||||
const layers = [createMockLayer()]
|
||||
const result = tpl.saveTemplate('', layers)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.name).toMatch(RE_DEFAULT_NAME)
|
||||
})
|
||||
|
||||
it('名称含前后空格时应自动 trim', () => {
|
||||
const layers = [createMockLayer()]
|
||||
const result = tpl.saveTemplate(' 模板名称 ', layers)
|
||||
|
||||
expect(result!.name).toBe('模板名称')
|
||||
})
|
||||
|
||||
it('应深拷贝图层数据,修改原图层不影响模板', () => {
|
||||
const layer = createMockLayer({ x: 10 })
|
||||
const layers = [layer]
|
||||
const result = tpl.saveTemplate('深拷贝测试', layers)
|
||||
|
||||
// 修改原图层
|
||||
layer.x = 999
|
||||
|
||||
expect(result!.layers[0].x).toBe(10)
|
||||
})
|
||||
|
||||
it('多次保存应生成不同的 id', () => {
|
||||
const layers = [createMockLayer()]
|
||||
|
||||
vi.setSystemTime(new Date('2026-01-01T00:00:00Z'))
|
||||
const t1 = tpl.saveTemplate('模板1', layers)
|
||||
|
||||
vi.setSystemTime(new Date('2026-01-01T00:00:01Z'))
|
||||
const t2 = tpl.saveTemplate('模板2', layers)
|
||||
|
||||
expect(t1!.id).not.toBe(t2!.id)
|
||||
})
|
||||
})
|
||||
|
||||
// ──── templates(sortedTemplates)────
|
||||
|
||||
describe('templates 排序', () => {
|
||||
it('空状态返回空数组', () => {
|
||||
expect(tpl.templates.value).toEqual([])
|
||||
})
|
||||
|
||||
it('应按 createdAt 降序排列(新模板在前)', () => {
|
||||
const layers = [createMockLayer()]
|
||||
|
||||
vi.setSystemTime(new Date('2026-01-01T00:00:00Z'))
|
||||
tpl.saveTemplate('旧模板', layers)
|
||||
|
||||
vi.setSystemTime(new Date('2026-01-02T00:00:00Z'))
|
||||
tpl.saveTemplate('新模板', layers)
|
||||
|
||||
expect(tpl.templates.value).toHaveLength(2)
|
||||
expect(tpl.templates.value[0].name).toBe('新模板')
|
||||
expect(tpl.templates.value[1].name).toBe('旧模板')
|
||||
})
|
||||
})
|
||||
|
||||
// ──── getTemplate() ────
|
||||
|
||||
describe('getTemplate() 按 id 获取模板', () => {
|
||||
it('存在时返回模板对象', () => {
|
||||
const layers = [createMockLayer()]
|
||||
const saved = tpl.saveTemplate('查找测试', layers)!
|
||||
|
||||
const found = tpl.getTemplate(saved.id)
|
||||
|
||||
expect(found).not.toBeNull()
|
||||
expect(found!.id).toBe(saved.id)
|
||||
expect(found!.name).toBe('查找测试')
|
||||
})
|
||||
|
||||
it('不存在时返回 null', () => {
|
||||
expect(tpl.getTemplate('nonexistent_id')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
// ──── removeTemplate() ────
|
||||
|
||||
describe('removeTemplate() 删除模板', () => {
|
||||
it('应按 id 删除指定模板', () => {
|
||||
const layers = [createMockLayer()]
|
||||
const t1 = tpl.saveTemplate('模板1', layers)!
|
||||
const t2 = tpl.saveTemplate('模板2', layers)!
|
||||
|
||||
tpl.removeTemplate(t1.id)
|
||||
|
||||
expect(tpl.templates.value).toHaveLength(1)
|
||||
expect(tpl.templates.value[0].id).toBe(t2.id)
|
||||
})
|
||||
|
||||
it('删除不存在的 id 不应报错', () => {
|
||||
const layers = [createMockLayer()]
|
||||
tpl.saveTemplate('模板', layers)
|
||||
|
||||
expect(() => tpl.removeTemplate('nonexistent')).not.toThrow()
|
||||
expect(tpl.templates.value).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
// ──── renameTemplate() ────
|
||||
|
||||
describe('renameTemplate() 重命名模板', () => {
|
||||
it('应更新指定模板的名称', () => {
|
||||
const layers = [createMockLayer()]
|
||||
const saved = tpl.saveTemplate('原始名称', layers)!
|
||||
|
||||
tpl.renameTemplate(saved.id, '新名称')
|
||||
|
||||
const updated = tpl.getTemplate(saved.id)
|
||||
expect(updated!.name).toBe('新名称')
|
||||
})
|
||||
|
||||
it('名称含空格时应自动 trim', () => {
|
||||
const layers = [createMockLayer()]
|
||||
const saved = tpl.saveTemplate('原始', layers)!
|
||||
|
||||
tpl.renameTemplate(saved.id, ' 修改后 ')
|
||||
|
||||
expect(tpl.getTemplate(saved.id)!.name).toBe('修改后')
|
||||
})
|
||||
|
||||
it('重命名不存在的 id 不应报错', () => {
|
||||
expect(() => tpl.renameTemplate('nonexistent', '新名称')).not.toThrow()
|
||||
})
|
||||
|
||||
it('重命名不应影响其他字段', () => {
|
||||
const layers = [createMockLayer({ x: 42 })]
|
||||
const saved = tpl.saveTemplate('原始', layers)!
|
||||
const originalCreatedAt = saved.createdAt
|
||||
|
||||
tpl.renameTemplate(saved.id, '新名称')
|
||||
|
||||
const updated = tpl.getTemplate(saved.id)!
|
||||
expect(updated.createdAt).toBe(originalCreatedAt)
|
||||
expect(updated.layers[0].x).toBe(42)
|
||||
})
|
||||
})
|
||||
|
||||
// ──── 边界情况 ────
|
||||
|
||||
describe('边界情况', () => {
|
||||
it('保存含多个图层的模板', () => {
|
||||
const layers = [
|
||||
createMockLayer({ x: 0, y: 0 }),
|
||||
createMockLayer({ x: 100, y: 100 }),
|
||||
createMockLayer({ x: 200, y: 200 }),
|
||||
]
|
||||
const result = tpl.saveTemplate('多图层模板', layers)
|
||||
|
||||
expect(result!.layers).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('图层含嵌套 config 对象时应深拷贝', () => {
|
||||
const config = { fill: 'red', nested: { value: 1 } }
|
||||
const layer = createMockLayer({ config })
|
||||
const result = tpl.saveTemplate('嵌套配置', [layer])
|
||||
|
||||
// 修改原始 config
|
||||
config.nested.value = 999
|
||||
|
||||
expect(result!.layers[0].config!.nested.value).toBe(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
182
tests/unit/composables/useOperationLog.test.ts
Normal file
182
tests/unit/composables/useOperationLog.test.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
304
tests/unit/composables/useRuntimeConsole.test.ts
Normal file
304
tests/unit/composables/useRuntimeConsole.test.ts
Normal file
@@ -0,0 +1,304 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user