import assert from 'node:assert/strict'
|
import { readFile, mkdir } from 'node:fs/promises'
|
import { chromium, expect } from '@playwright/test'
|
|
// 通过真实构建产物检查告警,API 拦截不会修改开发测试或设备。
|
const origin = 'http://offline-exceptions.test'
|
const output = '../.scratch/simulator-and-data-import-v1/offline-exceptions-browser'
|
await mkdir(output, { recursive: true })
|
const browser = await chromium.launch({ channel: 'chrome', headless: true })
|
const now = Date.UTC(2026, 8, 5, 11, 0, 0)
|
let receptionStatus = 'RECEIVING'
|
let deviceStatus = 'OFFLINE'
|
const warnings = ['CABINET_CHANNEL', 'COLLECTOR'].map((sourceType, index) => ({
|
exceptionId: index + 1, testId: 1, testName: '离线告警验收', exceptionType: 'DEVICE_OFFLINE',
|
sourceType, sourceIdentity: index ? '9' : 'dev-sim:22:1:1:1', metricCode: 'DEVICE_ONLINE',
|
abnormalValue: null, unit: '', ruleSnapshot: '{"expectedStatus":"ONLINE"}', exceptionStatus: 'OPEN',
|
firstOccurredAtUtc: now, lastOccurredAtUtc: now, occurrenceCount: 1,
|
}))
|
const detail = () => ({ testId: 1, testName: '离线告警验收', testStatus: receptionStatus === 'RECEIVING' ? 'RUNNING' : receptionStatus === 'ENDED' ? 'COMPLETED' : 'PAUSED',
|
receptionStatus, creatorName: '若依', principalName: '若依', testTypeName: '性能测试', testPurpose: '离线观测',
|
startedAtUtc: now, createTime: now, thresholds: [], planSteps: [], logicStepMappings: [],
|
batteries: [{ testBatteryId: 1, batteryNo: 'B-01', groupName: '演示电池组', collectorId: 9, collectorName: '演示采集器', fields: [], collectorChannels: [],
|
binding: { channelId: 1, cabinetId: 1, cabinetName: '演示检测柜', adapterCode: 'dev-sim', devtype: 22, devid: 1, subdevid: 1, chlid: 1 } }],
|
})
|
const live = () => ({ testId: 1, testStatus: detail().testStatus, receptionStatus, serverTime: now, testPlan: null,
|
sources: ['CABINET_CHANNEL', 'COLLECTOR'].map(sourceType => ({ sourceType, sourceId: sourceType === 'COLLECTOR' ? 9 : 1, batteryNo: 'B-01', deviceStatus, latestSnapshot: {} })),
|
exceptions: receptionStatus === 'RECEIVING' && deviceStatus === 'OFFLINE' ? warnings.map(w => ({
|
exceptionType: 'DEVICE_OFFLINE', sourceType: w.sourceType, sourceIdentity: w.sourceIdentity,
|
status: 'OPEN', message: '接收中数据来源离线,当前无法取得测量数据',
|
})) : [],
|
})
|
try {
|
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } })
|
const errors = []
|
page.on('pageerror', error => errors.push(error.message))
|
await page.addInitScript(() => localStorage.setItem('battery_detection_auth_token', 'browser-test'))
|
await page.addInitScript(() => {
|
const fetch = window.fetch.bind(window)
|
window.fetch = (input, init) => {
|
if (!String(input).endsWith('/test/1/live')) return fetch(input, init)
|
// 保持实时流打开,直接验证设备状态和异常事件无需刷新即可更新页面。
|
const body = new ReadableStream({ start(controller) {
|
window.emitOfflineTestEvent = (event, data) => controller.enqueue(
|
new TextEncoder().encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`))
|
} })
|
return Promise.resolve(new Response(body, { headers: { 'Content-Type': 'text/event-stream' } }))
|
}
|
})
|
await page.route(`${origin}/**`, async route => {
|
const path = new URL(route.request().url()).pathname
|
if (path.startsWith('/assets/')) return route.fulfill({ body: await readFile(`dist${path}`), contentType: path.endsWith('.css') ? 'text/css' : 'application/javascript' })
|
if (path.endsWith('/test/1/live')) return route.fulfill({ contentType: 'text/event-stream', body: `event: snapshot\ndata: ${JSON.stringify(live())}\n\n` })
|
if (path.includes('/system/test-warning-exceptions/')) {
|
if (path.endsWith('/close')) {
|
const id = Number(path.split('/').at(-2))
|
const record = warnings.find(w => w.exceptionId === id)
|
Object.assign(record, { exceptionStatus: 'CLOSED', handledAtUtc: now, handledByName: '若依', handlingNote: route.request().postDataJSON().handlingNote })
|
return route.fulfill({ json: { code: 200, data: record } })
|
}
|
return route.fulfill({ json: { code: 200, rows: warnings, total: warnings.length } })
|
}
|
if (path.includes('/system/test/1')) return route.fulfill({ json: { code: 200, data: path.endsWith('/live-state') ? live() : detail() } })
|
if (path.endsWith('/getInfo')) return route.fulfill({ json: { code: 200, user: { userName: 'admin' }, roles: ['admin'] } })
|
return route.fulfill({ contentType: 'text/html', body: await readFile('dist/index.html', 'utf8') })
|
})
|
await page.goto(`${origin}/test-list/1/console`)
|
const alarm = page.locator('.alarm-panel')
|
await expect(alarm).toContainText('检测柜通道离线')
|
await expect(alarm).toContainText('采集器离线')
|
await expect(alarm).not.toContainText('超限')
|
await expect(alarm).not.toContainText('处理')
|
await expect(alarm).not.toContainText('暂无异常')
|
await alarm.screenshot({ path: `${output}/console-alarm.png` })
|
await page.setViewportSize({ width: 390, height: 844 })
|
await expect(alarm).toContainText('采集器离线')
|
assert.equal(await alarm.evaluate(element => element.scrollWidth > element.clientWidth), false)
|
await alarm.screenshot({ path: `${output}/console-alarm-390.png` })
|
await page.setViewportSize({ width: 1440, height: 1000 })
|
for (const status of ['PAUSED', 'ENDED', 'RECEIVING']) {
|
receptionStatus = status
|
await page.reload()
|
if (status === 'RECEIVING') await expect(alarm).toContainText('采集器离线')
|
else await expect(alarm).toContainText('暂无异常')
|
}
|
deviceStatus = 'ONLINE'
|
await page.waitForFunction(() => typeof window.emitOfflineTestEvent === 'function')
|
await page.evaluate(state => {
|
window.emitOfflineTestEvent('device-status', state.sources)
|
window.emitOfflineTestEvent('exception', state.exceptions)
|
}, live())
|
await expect(alarm).toContainText('暂无异常')
|
await page.reload()
|
await expect(alarm).toContainText('暂无异常')
|
assert.ok(warnings.every(w => w.exceptionStatus === 'OPEN'))
|
if (process.argv[2] !== 'console') {
|
await page.goto(`${origin}/exception`)
|
const table = page.locator('.exception-page table')
|
await expect(table.locator('tbody tr')).toHaveCount(2)
|
await expect(table).toContainText('设备离线')
|
await expect(table).toContainText('待处理')
|
await expect(table).toContainText('2026-09-05')
|
await expect(table).not.toContainText('null')
|
await table.locator('tbody tr').first().getByRole('button', { name: '处理', exact: true }).click()
|
await page.getByRole('textbox').last().fill('已检查设备连接')
|
await page.getByRole('button', { name: '完成处理' }).click()
|
await expect(table).toContainText('已处理')
|
await page.reload()
|
await expect(table).toContainText('已处理')
|
await page.screenshot({ path: `${output}/exception-management.png`, fullPage: true, animations: 'disabled' })
|
await page.goto(`${origin}/test-list/1/console`)
|
await expect(alarm).toContainText('暂无异常')
|
deviceStatus = 'OFFLINE'
|
await page.waitForFunction(() => typeof window.emitOfflineTestEvent === 'function')
|
await page.evaluate(state => window.emitOfflineTestEvent('snapshot', state), live())
|
await expect(alarm.locator('.alarm-list > div')).toHaveCount(2)
|
await expect(alarm).toContainText('检测柜通道离线')
|
await expect(alarm).toContainText('采集器离线')
|
await expect(alarm).not.toContainText('处理')
|
assert.equal(warnings[0].exceptionStatus, 'CLOSED')
|
}
|
assert.deepEqual(errors, [])
|
console.log('离线告警浏览器验证通过:接收中告警、暂停和结束隐藏、恢复在线移除提醒;完整模式同时检查异常处理与实时提醒独立。')
|
} finally {
|
await browser.close()
|
}
|