yeg
2026-09-09 d67f933e50c7b5bf8c90e7d7db0000721ce796cc
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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
import assert from 'node:assert/strict'
import { readFile, mkdir } from 'node:fs/promises'
import { chromium, expect } from '@playwright/test'
 
// 使用实际模拟器页面与拦截响应验证交互,不启动业务服务。
const html = await readFile('../BatteryDetection/ruoyi-admin/src/main/resources/static/dev/bts-simulator.html', 'utf8')
const output = '../.scratch/simulator-and-data-import-v1/browser'
await mkdir(output, { recursive: true })
const browser = await chromium.launch({ channel: 'chrome', headless: true })
try {
  const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } })
  const errors = []
  page.on('pageerror', error => errors.push(error.message))
  const state = {
    running: true, softwareAccessible: true, listenAddress: '127.0.0.1:15020',
    devices: [{ deviceId: 1, ip: '127.0.0.2', online: true,
      profile: { devtype: 22, subDeviceId: 1, model: 'BTS Virtual Cabinet' },
      channels: [1, 2].map(channelId => ({ channelId, online: true, workStatus: 'WORKING', configuration: {
        currentA: 0, voltageV: 3.7, capacityAh: 1.2, energyWh: 4.44, cycleNo: 1, stepNo: 1,
        plannedCycleCount: 1, totalTimeSeconds: 0, relativeTimeSeconds: 0,
        steps: [{ name: '恒流充电', stepType: 'cc', durationSeconds: 60, voltageV: 3.7 }],
      } })) }],
    collectors: [{ collectorId: 1, deviceCode: 'dev-sim-collector-1', mac: '02:43:00:00:00:01',
      name: 'dev-sim 采集器 1', ip: '127.0.1.2', online: true, channelCount: 1,
      channels: [{ channelCode: 'S-01', workStatus: 'WORKING',
        metrics: [{ metricCode: 'temperature', value: 25.1, unit: '℃' }] }], latestMeasurements: [] }],
    recentRequests: [{ receivedAt: Date.now(), command: 'inquire', request: '<bts><cmd>inquire</cmd></bts>',
      response: '<inquire voltage="3.700" />' }],
  }
  const calls = []
  let failSoftware = false
  let failConfiguration = false
  await page.route('http://simulator.test/**', async route => {
    const request = route.request()
    const path = new URL(request.url()).pathname
    if (path === '/dev/bts-simulator.html') return route.fulfill({ contentType: 'text/html', body: html })
    const body = request.postDataJSON()
    calls.push({ path, method: request.method(), body })
    if (path.endsWith('/software')) {
      if (failSoftware) return route.fulfill({ status: 503, json: { message: '保存失败' } })
      state.softwareAccessible = body.accessible
    } else if (path.endsWith('/devices/1/channels/1/configuration')) {
      if (failConfiguration) return route.fulfill({ status: 503, json: { message: '配置保存失败' } })
      state.devices[0].channels[0].configuration = body
    }
    else if (path.endsWith('/devices/1/channels/1')) Object.assign(state.devices[0].channels[0], body)
    else if (path.endsWith('/collectors/1/channels/S-01/status')) state.collectors[0].channels[0].workStatus = body.workStatus
    else if (path.endsWith('/collectors/1/channels/S-01')) state.collectors[0].channels[0].metrics = body.metrics
    else if (path.endsWith('/devices/1')) Object.assign(state.devices[0], body)
    else if (path.endsWith('/collectors/1')) Object.assign(state.collectors[0], body)
    return route.fulfill({ json: state })
  })
  await page.goto('http://simulator.test/dev/bts-simulator.html')
  await page.locator('#devices').getByRole('button', { name: '参数与方案' }).first().click()
  const editor = page.getByRole('dialog')
  await editor.getByLabel('电压 (V)', { exact: true }).fill('4.125')
  await editor.getByLabel('电流 (A)').fill('-2.5')
  await editor.getByRole('button', { name: '添加工步' }).click()
  await editor.getByLabel('工步名称', { exact: true }).last().fill('放电测试')
  await editor.getByLabel('工步类型').last().selectOption('dc')
  await editor.getByLabel('时长 (s)', { exact: true }).last().fill('90')
  await editor.getByLabel('当前工步').fill('2')
  await editor.getByLabel('计划循环数').fill('3')
  await page.clock.install()
  await page.clock.fastForward(5000)
  await expect(editor.getByLabel('电压 (V)', { exact: true })).toHaveValue('4.125')
  await editor.screenshot({ path: output + '/channel-editor.png' })
  await editor.getByRole('button', { name: '保存通道配置' }).click()
  await expect(editor).not.toBeVisible()
  assert.equal(state.devices[0].channels[0].configuration.voltageV, 4.125)
  assert.equal(state.devices[0].channels[0].configuration.steps[1].name, '放电测试')
  assert.equal(state.devices[0].channels[1].configuration.voltageV, 3.7)
  await page.reload()
  await page.locator('#devices').getByRole('button', { name: '参数与方案' }).first().click()
  await expect(editor.getByLabel('当前工步')).toHaveValue('2')
  await expect(editor.getByLabel('工步名称', { exact: true }).last()).toHaveValue('放电测试')
  await editor.getByRole('button', { name: '上移工步', exact: true }).last().click()
  await expect(editor.getByLabel('工步名称', { exact: true }).first()).toHaveValue('放电测试')
  await editor.getByRole('button', { name: '下移工步', exact: true }).first().click()
  await editor.getByRole('button', { name: '添加工步' }).click()
  await editor.getByRole('button', { name: '删除工步', exact: true }).last().click()
  await expect(editor.getByLabel('工步名称', { exact: true })).toHaveCount(2)
  failConfiguration = true
  await editor.getByLabel('电压 (V)', { exact: true }).fill('4.2')
  await editor.getByRole('button', { name: '保存通道配置' }).click()
  await expect(editor.getByRole('alert')).toContainText('配置保存失败')
  await expect(editor.getByLabel('电压 (V)', { exact: true })).toHaveValue('4.2')
  failConfiguration = false
  await page.setViewportSize({ width: 390, height: 844 })
  assert.equal(await editor.evaluate(element => element.scrollWidth > element.clientWidth), false)
  await editor.screenshot({ path: output + '/channel-editor-390.png' })
  await editor.getByRole('button', { name: '取消' }).click()
  await page.setViewportSize({ width: 1440, height: 1000 })
  await page.locator('#collectors').getByRole('button', { name: '编辑测量值' }).click()
  await editor.getByLabel('数值', { exact: true }).first().fill('32.75')
  await editor.getByRole('button', { name: '添加测量项' }).click()
  await editor.getByLabel('测量项', { exact: true }).last().selectOption('negative_potential')
  await editor.getByLabel('数值', { exact: true }).last().fill('-0.15')
  await editor.screenshot({ path: output + '/collector-editor.png' })
  await editor.getByRole('button', { name: '保存通道配置' }).click()
  await expect(editor).not.toBeVisible()
  assert.equal(state.collectors[0].channels[0].metrics[0].value, 32.75)
  assert.equal(state.collectors[0].channels[0].metrics[1].value, -0.15)
  await page.reload()
  await expect(page.locator('#collectors')).toContainText('温度 32.75 ℃')
  await expect(page.locator('#collectors')).toContainText('负电位 -0.15 V')
  await expect(page.getByLabel('新威软件可访问')).toBeChecked()
  await page.getByLabel('新威软件可访问').uncheck()
  await expect(page.getByLabel('新威软件可访问')).not.toBeChecked()
  assert.equal(state.devices[0].online, true)
  await page.getByLabel('检测柜 1 通道 1 工作状态').selectOption('PROTECTED')
  await expect(page.getByLabel('检测柜 1 通道 1 工作状态')).toHaveValue('PROTECTED')
  await page.getByLabel('S-01 工作状态').selectOption('UNOBSERVABLE')
  await expect(page.getByLabel('S-01 工作状态')).toHaveValue('UNOBSERVABLE')
  await page.locator('#devices').getByLabel('整机在线').uncheck()
  await page.locator('#devices').getByRole('button', { name: '保存配置' }).click()
  await expect(page.locator('#devices').getByLabel('整机离线')).not.toBeChecked()
  await page.reload()
  await expect(page.getByLabel('检测柜 1 通道 1 工作状态')).toHaveValue('PROTECTED')
  await expect(page.getByLabel('新威软件可访问')).not.toBeChecked()
  await expect(page.locator('#requests')).toContainText('voltage="3.700"')
  await expect(page.getByText('600x', { exact: true })).toHaveCount(0)
  await expect(page.getByRole('button', { name: '下载历史' })).toHaveCount(0)
  await page.screenshot({ path: output + '/desktop.png', fullPage: true })
  for (const width of [390, 768]) {
    await page.setViewportSize({ width, height: 844 })
    assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false)
    await page.screenshot({ path: output + '/width-' + width + '.png', fullPage: true })
  }
  failSoftware = true
  await page.getByLabel('新威软件可访问').check()
  await expect(page.locator('#error')).toContainText('保存失败')
  assert.deepEqual(errors, [])
  assert.ok(calls.some(call => call.path.endsWith('/software') && call.method === 'PUT'))
  assert.ok(calls.every(call => !/playback|sampling|history/.test(call.path)))
  const consolePage = await browser.newPage({ viewport: { width: 1440, height: 1000 } })
  consolePage.on('pageerror', error => errors.push(error.message))
  await consolePage.addInitScript(() => localStorage.setItem('battery_detection_auth_token', 'browser-test'))
  const config = state.devices[0].channels[0].configuration
  const detail = { testId: 1, testName: '通道方案验收', testStatus: 'RUNNING', receptionStatus: 'RECEIVING',
    creatorName: '若依', principalName: '若依', testTypeName: '性能测试', testPurpose: '方案观测',
    thresholds: [], batteries: [], planSteps: [], logicStepMappings: [] }
  const live = { testId: 1, testStatus: 'RUNNING', receptionStatus: 'RECEIVING', serverTime: Date.now(), sources: [], exceptions: [],
    testPlan: { currentCycleNo: config.cycleNo, currentStepNo: config.stepNo, plannedCycleCount: config.plannedCycleCount,
      rows: config.steps.map((step, index) => ({ rowType: 'STEP', stepNo: index + 1, cyclePosition: index + 1,
        stepName: step.name, durationSeconds: step.durationSeconds, voltageV: step.voltageV,
        executionStatus: index === 1 ? '进行中' : '已完成' })) } }
  await consolePage.addInitScript(state => {
    const fetch = window.fetch.bind(window)
    window.fetch = (input, init) => String(input).endsWith('/test/1/live')
      ? Promise.resolve(new Response(new ReadableStream({ start(controller) {
        controller.enqueue(new TextEncoder().encode(`event: snapshot\ndata: ${JSON.stringify(state)}\n\n`))
      } }), { headers: { 'Content-Type': 'text/event-stream' } })) : fetch(input, init)
  }, live)
  await consolePage.route('http://simulator-console.test/**', 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/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 consolePage.goto('http://simulator-console.test/test-list/1/console')
  const plan = consolePage.locator('.step-panel')
  await expect(plan).toContainText('放电测试')
  await expect(plan).toContainText('恒流充电')
  await expect(plan).toContainText('进行中')
  await expect(plan).not.toContainText('暂无后端')
  await plan.screenshot({ path: output + '/console-plan.png' })
  assert.deepEqual(errors, [])
  console.log('模拟器页面:逐通道参数、工步增删排序、采集器数值、刷新保留、编辑防覆盖、保存失败及 1440/768/390 布局通过。')
} finally {
  await browser.close()
}