From 4cac6bb94d7318be719bf5edda4e22f19bc7b003 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期二, 08 九月 2026 18:22:45 +0800
Subject: [PATCH] 按当前观测合同维护模拟器验收
---
tests/e2e/collector-sampling.spec.ts | 192 +++++++++++++++----------------------
tests/e2e/test-management-acceptance.spec.ts | 54 ++++++----
2 files changed, 113 insertions(+), 133 deletions(-)
diff --git a/tests/e2e/collector-sampling.spec.ts b/tests/e2e/collector-sampling.spec.ts
index 7e10158..d2b95ad 100644
--- a/tests/e2e/collector-sampling.spec.ts
+++ b/tests/e2e/collector-sampling.spec.ts
@@ -2,20 +2,17 @@
import { Ticket04Stack } from './support/ticket04-stack'
const stack = new Ticket04Stack()
+const readonlyCommands = ['discover', 'observe', 'cabinet-realtime', 'collector-realtime', 'getdevinfo', 'inquire']
+interface Measurement { channelCode: string; metricCode: string; value: number; unit: string; sequenceId: number; deviceTimeRaw: string }
+interface CollectorSnapshot { online: boolean; measurements: Measurement[] }
+const configuredMetrics = [
+ { metricCode: 'temperature', value: 28.6, unit: '℃' },
+ { metricCode: 'positive_potential', value: 3.812, unit: 'V' },
+ { metricCode: 'negative_potential', value: -0.116, unit: 'V' },
+ { metricCode: 'dynamic_pressure', value: 101.3, unit: 'kPa' },
+]
-interface StoredCollectorState {
- deviceCode: string
- nextSequence: number
- lastSampledAt: string
- historyRecordCount?: number
- latestMeasurements?: Record<string, unknown>[]
- history?: unknown[]
-}
-
-interface StoredSimulatorState {
- collectors: StoredCollectorState[]
-}
-
+test.use({ actionTimeout: 25_000 })
test.beforeAll(async () => stack.start())
test.afterAll(async () => stack.stop())
@@ -26,91 +23,86 @@
await page.getByRole('button', { name: '登录', exact: true }).click()
await expect(page).toHaveURL(/\/dashboard$/)
}
+async function realtime(page: Page): Promise<CollectorSnapshot> {
+ const response = await page.request.get(stack.backendUrl + '/dev/bts-simulator/collectors/1/realtime')
+ expect(response.ok()).toBe(true)
+ return response.json()
+}
+async function collectorState(page: Page) {
+ const response = await page.request.get(stack.backendUrl + '/dev/bts-simulator/state')
+ expect(response.ok()).toBe(true)
+ const state = await response.json()
+ expect(state.recentRequests.every((item: { command: string }) => readonlyCommands.includes(item.command))).toBe(true)
+ return state.collectors.find((item: { deviceCode: string }) => item.deviceCode === 'dev-sim-collector-1')
+}
-test('采集器登记后可观察独立实时与分页历史采样', async ({ page }) => {
- await page.goto(`${stack.backendUrl}/dev/bts-simulator.html`)
+test('采集器登记、请求时当前观测、测量配置与重启持久化', async ({ page }) => {
+ await page.goto(stack.backendUrl + '/dev/bts-simulator.html')
page.once('dialog', dialog => dialog.accept())
- const simulatorReset = page.waitForResponse(response => response.request().method() === 'POST'
+ const reset = page.waitForResponse(response => response.request().method() === 'POST'
&& response.url().endsWith('/dev/bts-simulator/reset') && response.ok())
await page.getByRole('button', { name: '重置模拟器' }).click()
- await simulatorReset
-
- let collector = page.locator('article.device').filter({ has: page.getByRole('heading', { name: 'dev-sim 采集器 1' }) })
+ await reset
+ const collector = page.locator('article.device').filter({ has: page.getByRole('heading', { name: 'dev-sim 采集器 1' }) })
await expect(collector.getByText('S-01', { exact: true })).toBeVisible()
await expect(collector.getByText('S-04', { exact: true })).toBeVisible()
- await collector.locator('label').filter({ hasText: '采样间隔(毫秒)' }).locator('input').fill('200')
- const samplingSaved = page.waitForResponse(response => response.request().method() === 'PUT'
- && response.url().endsWith('/dev/bts-simulator/collectors/1/sampling') && response.ok())
- await collector.getByRole('button', { name: '保存配置' }).click()
- await samplingSaved
-
- collector = page.locator('article.device').filter({ has: page.getByRole('heading', { name: 'dev-sim 采集器 1' }) })
- await collector.getByLabel('S-01 测量项').fill([
- 'temperature=28.6 ℃',
- 'positive_potential=3.812 V',
- 'negative_potential=-0.116 V',
- 'dynamic_pressure=101.3 kPa',
- ].join('\n'))
- const measurementsSaved = page.waitForResponse(response => response.request().method() === 'PUT'
+ const channel = collector.locator('tbody tr').filter({ has: page.getByText('S-01', { exact: true }) })
+ await channel.getByRole('button', { name: '编辑测量值', exact: true }).click()
+ const editor = page.getByRole('dialog', { name: '采集器 1 · S-01 测量值', exact: true })
+ for (let index = 0; index < 3; index++) await editor.getByLabel('数值', { exact: true }).nth(index).fill(String(configuredMetrics[index]!.value))
+ await editor.getByRole('button', { name: '添加测量项', exact: true }).click()
+ const custom = editor.locator('tbody tr').last()
+ await custom.getByLabel('自定义名称', { exact: true }).fill('dynamic_pressure')
+ await custom.getByLabel('数值', { exact: true }).fill('101.3')
+ await custom.getByLabel('单位', { exact: true }).fill('kPa')
+ const saved = page.waitForResponse(response => response.request().method() === 'PUT'
&& response.url().endsWith('/dev/bts-simulator/collectors/1/channels/S-01') && response.ok())
- await collector.getByRole('button', { name: '保存测量值' }).first().click()
- await measurementsSaved
+ await editor.getByRole('button', { name: '保存通道配置', exact: true }).click()
+ await saved
+ await expect(editor).not.toBeVisible()
+ await expect(channel).toContainText('dynamic_pressure 101.3 kPa')
+ const configured = await collectorState(page)
+ expect(configured.channels[0].metrics).toEqual(configuredMetrics)
+ expect(configured.latestMeasurements).toEqual([])
- collector = page.locator('article.device').filter({ has: page.getByRole('heading', { name: 'dev-sim 采集器 1' }) })
- await expect(collector.getByLabel('S-01 测量项')).toHaveValue(/dynamic_pressure=101\.3 kPa/)
- await expect(collector.getByText(/原始历史 [1-9]\d* 条/)).toBeVisible()
- await collector.getByRole('button', { name: '查看实时' }).click()
- await expect(collector.locator('.collector-output')).toContainText('dev-sim-collector-1')
+ const observed = page.waitForResponse(response => response.url().endsWith('/collectors/1/realtime') && response.ok())
+ await collector.getByRole('button', { name: '查看实时', exact: true }).click()
+ const first: CollectorSnapshot = await (await observed).json()
+ expect(first.online).toBe(true)
+ expect(first.measurements).toHaveLength(13)
+ expect(first.measurements.filter(item => item.channelCode === 'S-01').map(({ metricCode, value, unit }) => ({ metricCode, value, unit }))).toEqual(configuredMetrics)
await expect(collector.locator('.collector-output')).toContainText('S-01 dynamic_pressure=101.3 kPa')
await expect(collector.locator('.collector-output')).toContainText('设备 2026-')
await expect(collector.locator('.collector-output')).toContainText('/ 接收 2026-')
- await collector.getByRole('button', { name: '下载历史' }).click()
- await expect(collector.locator('.collector-output')).toContainText(/历史 \d+ 条 \/ 本页 \d+ 条/)
-
- const mainState = stack.redisCommand('GET', 'dev:bts-simulator:state:v1')
- expect(mainState).not.toMatch(/\\?"history\\?"\s*:/)
- const historyBeforePowerOff = Number(stack.redisCommand(
- 'LLEN', 'dev:bts-simulator:collector-history:v1:dev-sim-collector-1',
- ))
- expect(historyBeforePowerOff).toBeGreaterThan(0)
+ const second = await realtime(page)
+ expect(second.measurements[0]!.sequenceId).toBeGreaterThan(first.measurements[0]!.sequenceId)
+ expect(second.measurements.map(item => item.value)).toEqual(first.measurements.map(item => item.value))
+ const beforeIdle = await collectorState(page)
+ // ADR-0048:状态读取和请求空档不生成观测,也不积累历史。
+ await page.waitForTimeout(1_100)
+ expect((await collectorState(page)).latestMeasurements).toEqual(beforeIdle.latestMeasurements)
+ expect(stack.redisCommand('EXISTS', 'dev:bts-simulator:collector-history:v1:dev-sim-collector-1')).toBe('0')
+ expect(stack.redisCommand('GET', 'dev:bts-simulator:state:v1')).not.toMatch(/\\?"history\\?"\s*:/)
page.once('dialog', dialog => dialog.accept())
- const poweredOff = page.waitForResponse(response => response.request().method() === 'POST'
- && response.url().endsWith('/dev/bts-simulator/power') && response.ok())
+ const off = page.waitForResponse(response => response.request().method() === 'POST' && response.url().endsWith('/dev/bts-simulator/power') && response.ok())
await page.getByRole('button', { name: '一键模拟关机' }).click()
- await poweredOff
+ await off
await expect(page.locator('#device-power')).toHaveText('已关机')
- const historyAfterPowerOff = Number(stack.redisCommand(
- 'LLEN', 'dev:bts-simulator:collector-history:v1:dev-sim-collector-1',
- ))
- await page.waitForTimeout(700)
- expect(Number(stack.redisCommand(
- 'LLEN', 'dev:bts-simulator:collector-history:v1:dev-sim-collector-1',
- ))).toBe(historyAfterPowerOff)
-
+ expect(await realtime(page)).toMatchObject({ online: false, measurements: [] })
await stack.restartBackend()
- await page.goto(`${stack.backendUrl}/dev/bts-simulator.html`)
+ await page.goto(stack.backendUrl + '/dev/bts-simulator.html')
await expect(page.locator('#device-power')).toHaveText('已关机')
- await page.waitForTimeout(700)
- expect(Number(stack.redisCommand(
- 'LLEN', 'dev:bts-simulator:collector-history:v1:dev-sim-collector-1',
- ))).toBe(historyAfterPowerOff)
-
- const poweredOn = page.waitForResponse(response => response.request().method() === 'POST'
- && response.url().endsWith('/dev/bts-simulator/power') && response.ok())
+ expect(await realtime(page)).toMatchObject({ online: false, measurements: [] })
+ expect((await collectorState(page)).channels[0].metrics).toEqual(configuredMetrics)
+ const on = page.waitForResponse(response => response.request().method() === 'POST' && response.url().endsWith('/dev/bts-simulator/power') && response.ok())
await page.getByRole('button', { name: '一键模拟开机' }).click()
- await poweredOn
+ await on
await expect(page.locator('#device-power')).toHaveText('已开机')
- await expect(page.locator('#error')).not.toHaveClass(/visible/)
- await expect.poll(() => Number(stack.redisCommand(
- 'LLEN', 'dev:bts-simulator:collector-history:v1:dev-sim-collector-1',
- ))).toBeGreaterThan(historyAfterPowerOff)
- await expect.poll(() => {
- const stored = JSON.parse(stack.redisCommand('GET', 'dev:device-discovery:candidates:v1'))
- const candidates = typeof stored === 'string' ? JSON.parse(stored) : stored
- return candidates.collectors.find((item: { deviceCode: string }) =>
- item.deviceCode === 'dev-sim-collector-1')?.online
- }, { timeout: 10_000 }).toBe(true)
+ const resumed = await realtime(page)
+ expect(resumed.measurements).toHaveLength(13)
+ expect(resumed.measurements[0]!.sequenceId).toBeGreaterThan(second.measurements[0]!.sequenceId)
+ expect(resumed.measurements.map(item => item.value)).toEqual(first.measurements.map(item => item.value))
await login(page)
await page.goto('/device-collector/new')
@@ -118,42 +110,16 @@
const candidate = page.locator('.device-table tbody tr').filter({ hasText: 'dev-sim-collector-1' })
await expect(candidate).toBeVisible()
await candidate.getByRole('button', { name: '选择' }).click()
- await page.locator('label').filter({ hasText: '采集器名称' }).locator('input').fill('Ticket06 验收采集器')
+ await page.locator('label').filter({ hasText: '采集器名称' }).locator('input').fill('实时观测验收采集器')
await page.getByRole('button', { name: '保存', exact: true }).click()
await expect(page.locator('tbody tr').filter({ hasText: 'dev-sim-collector-1' })).toBeVisible()
-
await stack.restartBackend()
- await page.goto(`${stack.backendUrl}/dev/bts-simulator.html`)
- collector = page.locator('article.device').filter({ has: page.getByRole('heading', { name: 'dev-sim 采集器 1' }) })
- await expect(collector.locator('label').filter({ hasText: '采样间隔(毫秒)' }).locator('input')).toHaveValue('200')
- await expect(collector.getByLabel('S-01 测量项')).toHaveValue(/dynamic_pressure=101\.3 kPa/)
- await collector.getByRole('button', { name: '下载历史' }).click()
- await expect(collector.locator('.collector-output')).toContainText('dev-sim-collector-1')
- expect(Number(stack.redisCommand(
- 'LLEN', 'dev:bts-simulator:collector-history:v1:dev-sim-collector-1',
- ))).toBeGreaterThan(0)
-
- const legacyState = stack.simulatorState<StoredSimulatorState>()
- const legacyCollector = legacyState.collectors[0]
- const legacyRecord = legacyCollector.latestMeasurements?.[0]
- expect(legacyRecord).toBeDefined()
- delete legacyCollector.historyRecordCount
- delete legacyCollector.latestMeasurements
- legacyCollector.history = [legacyRecord]
- await stack.replaceSimulatorStateAndRestart(legacyState)
-
- const migratedState = stack.redisCommand('GET', 'dev:bts-simulator:state:v1')
- expect(migratedState).not.toMatch(/\\?"history\\?"\s*:/)
- expect(Number(stack.redisCommand(
- 'LLEN', 'dev:bts-simulator:collector-history:v1:dev-sim-collector-1',
- ))).toBeGreaterThan(1)
-
- await page.goto(`${stack.backendUrl}/dev/bts-simulator.html`)
- collector = page.locator('article.device').filter({ has: page.getByRole('heading', { name: 'dev-sim 采集器 1' }) })
- await collector.getByRole('button', { name: '下载历史' }).click()
- await expect(collector.locator('.collector-output')).toContainText('dev-sim-collector-1')
- expect(stack.backendLog()).not.toContain('JSONLargeObjectException')
-
+ await page.goto(stack.backendUrl + '/dev/bts-simulator.html')
+ expect((await collectorState(page)).channels[0].metrics).toEqual(configuredMetrics)
+ expect((await realtime(page)).measurements.map(item => item.value)).toEqual(first.measurements.map(item => item.value))
+ await collector.getByRole('button', { name: '查看实时', exact: true }).click()
+ await expect(collector.locator('.collector-output')).toContainText('S-01 dynamic_pressure=101.3 kPa')
const commands = await page.locator('#requests .request-command').allTextContents()
- expect(commands.every(command => command === 'getdevinfo' || command === 'inquire')).toBe(true)
+ expect(commands.length).toBeGreaterThan(0)
+ expect(commands.every(command => readonlyCommands.includes(command))).toBe(true)
})
diff --git a/tests/e2e/test-management-acceptance.spec.ts b/tests/e2e/test-management-acceptance.spec.ts
index 1bb4a7f..393169d 100644
--- a/tests/e2e/test-management-acceptance.spec.ts
+++ b/tests/e2e/test-management-acceptance.spec.ts
@@ -88,23 +88,30 @@
await reset
await expect(page.getByRole('heading', { name: '检测柜 1' })).toBeVisible()
- const paused = await page.request.post(`${stack.backendUrl}/dev/bts-simulator/devices/1/channels/1/playback/pause`)
- expect(paused.ok()).toBe(true)
+ // ADR-0048 的当前参数由操作员配置,模拟器不会自动播放或推进循环。
+ const configured = await page.request.put(`${stack.backendUrl}/dev/bts-simulator/devices/1/channels/1/configuration`, {
+ data: { voltageV: 3.7, currentA: 1.2, capacityAh: 2.5, energyWh: 9.25,
+ cycleNo: 1, stepNo: 1, plannedCycleCount: 3, totalTimeSeconds: 120, relativeTimeSeconds: 60,
+ steps: [{ name: '恒流充电', stepType: 'cc', durationSeconds: 600, voltageV: 4.2 }] },
+ })
+ expect(configured.ok()).toBe(true)
const working = await page.request.put(`${stack.backendUrl}/dev/bts-simulator/devices/1/channels/1`, {
data: { online: true, workStatus: 'WORKING' },
})
expect(working.ok()).toBe(true)
const collector = page.locator('article.device').filter({ has: page.getByRole('heading', { name: 'dev-sim 采集器 1' }) })
- await collector.getByLabel('S-01 测量项').fill([
- 'temperature=48.5 ℃',
- 'positive_potential=3.812 V',
- 'negative_potential=-0.116 V',
- ].join('\n'))
+ await collector.locator('tbody tr').filter({ has: page.getByText('S-01', { exact: true }) })
+ .getByRole('button', { name: '编辑测量值', exact: true }).click()
+ const editor = page.getByRole('dialog', { name: '采集器 1 · S-01 测量值', exact: true })
+ for (const [index, value] of ['48.5', '3.812', '-0.116'].entries())
+ await editor.getByLabel('数值', { exact: true }).nth(index).fill(value)
const saved = page.waitForResponse(response => response.request().method() === 'PUT'
&& response.url().endsWith('/dev/bts-simulator/collectors/1/channels/S-01') && response.ok())
- await collector.getByRole('button', { name: '保存测量值' }).first().click()
+ await editor.getByRole('button', { name: '保存通道配置', exact: true }).click()
await saved
+ await expect(editor).not.toBeVisible()
+ await expect(collector.locator('tbody tr').filter({ has: page.getByText('S-01', { exact: true }) })).toContainText('温度 48.5 ℃')
}
async function createPrerequisites(page: Page, names: Record<string, string>) {
@@ -206,11 +213,6 @@
await expect(page.getByText('已开始接收,未向设备发送控制命令', { exact: true })).toBeVisible()
await expect(page.locator('.running-state')).toHaveText('检测中')
await expect(page.locator('.reception-state')).toHaveText('接收中')
- const playback = await page.request.put(`${stack.backendUrl}/dev/bts-simulator/devices/1/channels/1/playback`, {
- data: { speedMultiplier: 60, playing: true },
- })
- expect(playback.ok()).toBe(true)
-
await expect.poll(async () => (await persistedRows(page, testId, 'records')).length, { timeout: 90_000 }).toBeGreaterThan(1)
await expect(page.locator('.cell-card .temperature')).not.toHaveText('-')
await expect(page.locator('.alarm-list')).toContainText('测试预警异常')
@@ -235,9 +237,19 @@
})
await test.step('三类当前有效数据可修订并形成统一修订记录', async () => {
- await page.request.post(`${stack.backendUrl}/dev/bts-simulator/devices/1/channels/1/playback/next-cycle`)
- await expect.poll(async () => (await persistedRows(page, testId, 'steps')).length, { timeout: 90_000 }).toBeGreaterThan(0)
- await expect.poll(async () => (await persistedRows(page, testId, 'cycles')).length, { timeout: 90_000 }).toBeGreaterThan(0)
+ // 模拟器没有工步或循环历史;通过已有正式导入动作准备供修订的事实。
+ for (const kind of ['STEP', 'CYCLE']) {
+ const row = { recordNo: `INITIAL-${kind}`, batteryNo: names.battery, recordedAt: Date.now(),
+ cycleNo: 1, stepNo: 1, stepName: '恒流充电', sequenceId: 1,
+ endVoltage: 4.2, startVoltage: 3.1, stepDuration: 60,
+ chargeCapacity: 2, dischargeCapacity: 1.9, chargeEnergy: 4, dischargeEnergy: 3.8, efficiency: 95 }
+ const imported = await apiJson<ApiResult<unknown>>(page, `/system/test/${testId}/console-data/actions`, {
+ method: 'POST', body: JSON.stringify({ dataType: kind, operationType: 'IMPORT',
+ scopeSummary: '准备修订来源', identitySummary: '[]', changesSummary: JSON.stringify([row]), reason: `准备${kind}修订事实` }),
+ })
+ expect(imported.code).toBe(200)
+ expect((await persistedRows(page, testId, kind === 'STEP' ? 'steps' : 'cycles')).some(item => item.recordNo === row.recordNo)).toBe(true)
+ }
await page.getByRole('button', { name: '数据列表', exact: true }).click()
const download = page.waitForEvent('download')
@@ -245,7 +257,7 @@
expect((await download).suggestedFilename()).toMatch(/-realtime\.csv$/)
await editFirstRow(page, '实时', '电压', '3.3333', 'Ticket06 实时数据修订')
await editFirstRow(page, '工步', '终止电压', '3.2222', 'Ticket06 工步数据修订')
- await editFirstRow(page, '循环', '放电容量', '4.0000', 'Ticket06 循环数据修订')
+ await editFirstRow(page, '循环', '放电容量', '4000.0000', 'Ticket06 循环数据修订')
await page.getByRole('button', { name: '实时', exact: true }).click()
const firstRowText = await page.locator('.console-data-panel tbody tr').first().innerText()
@@ -262,7 +274,9 @@
await expect(page).toHaveURL(/\/data-revision/)
await page.goto(`/data-revision?testId=${testId}`)
const revisionTable = page.locator('.result-panel tbody')
- await expect(revisionTable.locator('tr')).toHaveCount(4)
+ await expect(revisionTable.locator('tr')).toHaveCount(6)
+ await expect(revisionTable).toContainText('准备STEP修订事实')
+ await expect(revisionTable).toContainText('准备CYCLE修订事实')
await expect(revisionTable).toContainText('Ticket06 实时数据修订')
await expect(revisionTable).toContainText('Ticket06 工步数据修订')
await expect(revisionTable).toContainText('Ticket06 循环数据修订')
@@ -370,7 +384,7 @@
})
expect(result.code).toBe(200)
const table = `t_test_${kind.toLowerCase()}_record`
- expect(stack.queryDatabase(`SELECT COUNT(*) FROM ${table} WHERE test_id=${testId} AND source_identity='IMPORT'`)).toBe('1')
+ expect(stack.queryDatabase(`SELECT COUNT(*) FROM ${table} WHERE test_id=${testId} AND source_identity='IMPORT'`)).toBe(kind === 'REALTIME' ? '1' : '2')
const endpoint = kind === 'REALTIME' ? 'records' : kind === 'STEP' ? 'steps' : 'cycles'
expect((await persistedRows(page, testId, endpoint)).some(item => item.recordNo === row.recordNo)).toBe(true)
if (kind === 'REALTIME') expect(stack.queryDatabase(`SELECT cabinet_sequence_id FROM ${table} WHERE test_id=${testId} AND source_identity='IMPORT'`)).toBe('1')
@@ -392,7 +406,7 @@
await page.goto(`${stack.backendUrl}/dev/bts-simulator.html`)
const commands = await page.locator('#requests .request-command').allTextContents()
expect(commands.length).toBeGreaterThan(0)
- expect(commands.every(command => command === 'getdevinfo' || command === 'inquire')).toBe(true)
+ expect(commands.every(command => ['discover', 'observe', 'cabinet-realtime', 'collector-realtime', 'getdevinfo', 'inquire'].includes(command))).toBe(true)
})
await test.step('同通道跨测试复用,所有状态整体删除无痕且不影响其他事实', async () => {
--
Gitblit v1.7.1