| New file |
| | |
| | | import assert from 'node:assert/strict' |
| | | import { readFile, mkdir } from 'node:fs/promises' |
| | | import { chromium, expect } from '@playwright/test' |
| | | |
| | | // 使用真实构建产物和可推送实时流,不修改共享测试或设备数据。 |
| | | const origin = 'http://threshold-exceptions.test' |
| | | const output = '../.scratch/simulator-and-data-import-v1/threshold-exceptions-browser' |
| | | await mkdir(output, { recursive: true }) |
| | | const browser = await chromium.launch({ channel: 'chrome', headless: true }) |
| | | const now = Date.UTC(2026, 8, 5, 12, 0, 0) |
| | | let receptionStatus = 'RECEIVING' |
| | | let temperature = 38 |
| | | const threshold = { thresholdId: 1, metricCode: 'TEMPERATURE', minValue: 8, maxValue: 33, severity: 'WARNING' } |
| | | const warning = { exceptionId: 1, testId: 1, testName: '温度预警验收', exceptionType: 'THRESHOLD', |
| | | thresholdId: 1, sourceType: 'COLLECTOR', sourceIdentity: '9:S-01', metricCode: 'TEMPERATURE', |
| | | abnormalValue: 38, unit: '℃', ruleSnapshot: JSON.stringify(threshold), 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: [threshold], planSteps: [], logicStepMappings: [], |
| | | batteries: [{ testBatteryId: 1, batteryNo: 'B-01', groupName: '演示电池组', collectorId: 9, collectorName: '演示采集器', fields: [], |
| | | collectorChannels: [{ channelCode: 'S-01', measurementCode: 'PRIMARY_TEMPERATURE' }], |
| | | 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: [{ sourceType: 'COLLECTOR', sourceId: 9, batteryNo: 'B-01', deviceStatus: 'ONLINE', |
| | | latestSnapshot: receptionStatus === 'RECEIVING' ? { observedAt: now, measurements: [ |
| | | { channelCode: 'S-01', metricCode: 'TEMPERATURE', value: temperature, unit: '℃', deviceTime: now, serverReceivedAt: now }, |
| | | ] } : {} }], |
| | | exceptions: receptionStatus === 'RECEIVING' && temperature > 33 ? [{ |
| | | exceptionType: 'THRESHOLD', thresholdId: 1, sourceType: 'COLLECTOR', sourceIdentity: '9:S-01', |
| | | metricCode: 'TEMPERATURE', value: temperature, unit: '℃', minValue: 8, maxValue: 33, |
| | | 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') |
| | | const fetch = window.fetch.bind(window) |
| | | window.fetch = (input, init) => { |
| | | if (!String(input).endsWith('/test/1/live')) return fetch(input, init) |
| | | return Promise.resolve(new Response(new ReadableStream({ start(controller) { |
| | | window.emitThresholdTestEvent = (event, data) => controller.enqueue(new TextEncoder().encode( |
| | | `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)) |
| | | } }), { 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.includes('/system/test-warning-exceptions/')) { |
| | | if (path.endsWith('/close')) { |
| | | Object.assign(warning, { exceptionStatus: 'CLOSED', handledAtUtc: now, handledByName: '若依', handlingNote: route.request().postDataJSON().handlingNote }) |
| | | return route.fulfill({ json: { code: 200, data: warning } }) |
| | | } |
| | | return route.fulfill({ json: { code: 200, rows: [warning], total: 1 } }) |
| | | } |
| | | 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') |
| | | const emitSnapshot = async () => { |
| | | await page.waitForFunction(() => typeof window.emitThresholdTestEvent === 'function') |
| | | await page.evaluate(state => window.emitThresholdTestEvent('snapshot', state), live()) |
| | | } |
| | | await expect(alarm).toContainText('温度超限 · 9:S-01') |
| | | await expect(alarm).toContainText('当前值:38 ℃') |
| | | await expect(alarm).toContainText('预警阈值:8 ~ 33 ℃') |
| | | 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('当前值:38 ℃') |
| | | 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 }) |
| | | temperature = 25 |
| | | await emitSnapshot() |
| | | await expect(alarm).toContainText('暂无异常') |
| | | assert.equal(warning.exceptionStatus, 'OPEN') |
| | | await page.goto(`${origin}/exception`) |
| | | const table = page.locator('.exception-page table') |
| | | await expect(table.locator('tbody tr')).toHaveCount(1) |
| | | await expect(table).toContainText('待处理') |
| | | await expect(table).toContainText('38') |
| | | await table.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('暂无异常') |
| | | temperature = 38 |
| | | await emitSnapshot() |
| | | await expect(alarm).toContainText('温度超限') |
| | | assert.equal(warning.exceptionStatus, 'CLOSED') |
| | | for (const status of ['PAUSED', 'ENDED', 'RECEIVING']) { |
| | | receptionStatus = status |
| | | await emitSnapshot() |
| | | if (status === 'RECEIVING') await expect(alarm).toContainText('当前值:38 ℃') |
| | | else await expect(alarm).toContainText('暂无异常') |
| | | } |
| | | await page.reload() |
| | | await expect(alarm).toContainText('温度超限') |
| | | assert.deepEqual(errors, []) |
| | | console.log('温度预警浏览器验证通过:38℃超限、实时恢复、历史处理独立、暂停和结束隐藏、刷新及桌面和窄屏布局。') |
| | | } finally { |
| | | await browser.close() |
| | | } |