yeg
2026-09-28 6a4ca47df73d99e952d926f6103bcccea1ecea89
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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 persistedReads = new Set()
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 (/\/system\/test\/1\/data\/(records|steps|cycles)$/.test(path)) {
      persistedReads.add(path.split('/').at(-1))
      return route.fulfill({ json: { code: 200, data: [] } })
    }
    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`)
  await expect(page.getByRole('button', { name: '实时监控', exact: true })).toBeVisible()
  await expect(page.locator('.monitor-title')).toHaveText('实时监控约 5 秒观测')
  await expect(page.getByRole('button', { name: '实时数据', exact: true })).toHaveCount(0)
  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('待处理')
  assert.equal(persistedReads.size, 0, '实时监控不查询三子页持久化记录')
  await page.getByRole('button', { name: '数据列表', exact: true }).click()
  const dataPanel = page.locator('.console-data-panel')
  await expect(dataPanel).toContainText('实时采样记录 · 已持久化 · 当前有效值')
  for (const label of ['实时', '工步', '循环']) {
    await expect(dataPanel.getByRole('button', { name: label, exact: true })).toBeVisible()
    await dataPanel.getByRole('button', { name: label, exact: true }).click()
    await expect(alarm).toBeHidden()
  }
  assert.deepEqual([...persistedReads].sort(), ['cycles', 'records', 'steps'])
  await page.reload()
  await expect(dataPanel).toBeVisible()
  await expect(alarm).toBeHidden()
  await page.getByRole('button', { name: '实时监控', exact: true }).click()
  await expect(alarm).toContainText('当前值:38 ℃')
  await page.screenshot({ path: `${output}/live-monitor-1440.png`, fullPage: true, animations: 'disabled' })
  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 expect(page.locator('.monitor-title')).toBeVisible()
  assert.equal(await page.locator('.realtime-header').evaluate(element => element.scrollWidth > element.clientWidth), false)
  await page.screenshot({ path: `${output}/live-monitor-390.png`, fullPage: true, animations: 'disabled' })
  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()
}