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()
|
}
|