import { expect, test, type Locator, type Page } from '@playwright/test'
|
import { Ticket04Stack } from './support/ticket04-stack'
|
|
const stack = new Ticket04Stack()
|
|
interface ApiResult<T> {
|
code: number
|
data: T
|
}
|
|
interface PersistedRow {
|
id: string
|
recordNo: string
|
batteryNo: string
|
stableIdentity?: string
|
sequenceId?: number | null
|
voltage?: number | string | null
|
endVoltage?: number | string | null
|
dischargeCapacity?: number | string | null
|
}
|
|
interface WarningRecord {
|
exceptionId: number
|
occurrenceCount: number
|
exceptionStatus: 'OPEN' | 'CLOSED'
|
ruleSnapshot: string
|
}
|
|
test.use({ actionTimeout: 25_000 })
|
test.beforeAll(async () => stack.start())
|
test.afterAll(async () => stack.stop())
|
|
async function login(page: Page) {
|
await page.goto('/login')
|
await page.locator('input[autocomplete="username"]').fill('admin')
|
await page.locator('input[autocomplete="current-password"]').fill('admin123')
|
await page.getByRole('button', { name: '登录', exact: true }).click()
|
await expect(page).toHaveURL(/\/dashboard$/)
|
}
|
|
async function apiJson<T>(page: Page, path: string, init: RequestInit = {}) {
|
return page.evaluate(async ({ requestPath, requestInit }) => {
|
const token = localStorage.getItem('battery_detection_auth_token')
|
const headers = new Headers(requestInit.headers)
|
if (requestInit.body) headers.set('Content-Type', 'application/json')
|
if (token) headers.set('Authorization', `Bearer ${token}`)
|
const response = await fetch(`/dev-api${requestPath}`, { ...requestInit, headers })
|
return response.json()
|
}, { requestPath: path, requestInit: init }) as Promise<T>
|
}
|
|
async function selectOption(select: Locator, optionName: string) {
|
const combobox = select.getByRole('combobox')
|
await select.locator('.el-select__wrapper').click()
|
const listboxId = await combobox.getAttribute('aria-controls')
|
const option = select.page().locator(`#${listboxId}`).getByRole('option').filter({ hasText: optionName }).first()
|
await expect(option).toBeVisible()
|
await option.click()
|
}
|
|
async function selectFirstOption(select: Locator) {
|
const combobox = select.getByRole('combobox')
|
await select.locator('.el-select__wrapper').click()
|
const listboxId = await combobox.getAttribute('aria-controls')
|
const option = select.page().locator(`#${listboxId}`).getByRole('option').first()
|
await expect(option).toBeVisible()
|
await option.click()
|
}
|
|
async function registerDevice(page: Page, path: string, code: string, label: string, name: string) {
|
await page.goto(path)
|
await page.getByRole('button', { name: '选择设备' }).click()
|
const candidate = page.locator('.device-table tbody tr').filter({ hasText: code })
|
await expect(candidate).toBeVisible()
|
await candidate.getByRole('button', { name: '选择' }).click()
|
await page.locator('label').filter({ hasText: label }).locator('input').fill(name)
|
if (label === '检测柜名称') {
|
await page.locator('label').filter({ hasText: '规格/容量' }).locator('input').fill('120')
|
}
|
await page.getByRole('button', { name: '保存', exact: true }).click()
|
}
|
|
async function configureSimulator(page: Page) {
|
await page.goto(`${stack.backendUrl}/dev/bts-simulator.html`)
|
page.once('dialog', dialog => dialog.accept())
|
const reset = page.waitForResponse(response => response.url().endsWith('/dev/bts-simulator/reset') && response.ok())
|
await page.getByRole('button', { name: '重置模拟器' }).click()
|
await reset
|
|
await expect(page.getByRole('heading', { name: '检测柜 1' })).toBeVisible()
|
// 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.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 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>) {
|
await registerDevice(page, '/device-cabinet/new', 'dev-sim-22-1', '检测柜名称', names.cabinet)
|
await expect(page).toHaveURL(/\/device-cabinet$/)
|
await registerDevice(page, '/device-collector/new', 'dev-sim-collector-1', '采集器名称', names.collector)
|
await expect(page).toHaveURL(/\/device-collector$/)
|
|
await page.goto('/test-type/new')
|
await page.getByPlaceholder('输入测试类型名称').fill(names.testType)
|
await page.getByRole('button', { name: '保存', exact: true }).click()
|
await expect(page.locator('tbody tr').filter({ hasText: names.testType })).toBeVisible()
|
|
await page.goto('/battery-group/new')
|
await page.getByPlaceholder('输入电池组名称').fill(names.group)
|
await page.getByPlaceholder('输入样品型号').fill('TICKET06')
|
await page.getByPlaceholder('输入F').fill('2')
|
await page.getByPlaceholder('输入目的').fill('测试管理模块整体验收')
|
await page.getByPlaceholder('输入电池号').fill(names.battery)
|
await page.getByRole('button', { name: '保存新建电池组', exact: true }).click()
|
await expect(page.locator('tbody tr').filter({ hasText: names.group })).toBeVisible()
|
expect(stack.queryDatabase("SELECT field_value FROM t_battery_group_relation_field relation JOIN t_battery_group_field definition ON definition.field_id=relation.battery_group_field_id WHERE definition.field_name='F' AND relation.del_flag=0")).toBe('2')
|
}
|
|
async function createTest(page: Page, names: Record<string, string>) {
|
await page.goto('/test-list/new')
|
await page.getByPlaceholder('输入测试名称').fill(names.test)
|
await selectOption(page.locator('.parameter-grid label').filter({ hasText: '测试负责人' }).locator('.el-select'), '若依')
|
await selectOption(page.locator('.parameter-grid label').filter({ hasText: '测试类型' }).locator('.el-select'), names.testType)
|
await page.getByPlaceholder('输入测试目的').fill('验证完整观察、修订、图表和预警异常流程')
|
|
await page.getByRole('button', { name: '添加电池' }).click()
|
await selectOption(page.locator('.battery-group-search .el-select'), names.group)
|
await page.getByLabel(`选择电池 ${names.battery}`).click()
|
await page.locator('.battery-dialog').getByRole('button', { name: '提交', exact: true }).click()
|
|
const row = page.locator('.battery-table tbody tr').first()
|
const selects = row.locator('.el-select')
|
await selectOption(selects.nth(0), names.cabinet)
|
await selectFirstOption(selects.nth(1))
|
await selectOption(selects.nth(2), names.collector)
|
await selectOption(selects.nth(3), 'S-01')
|
await selectOption(selects.nth(6), 'S-02')
|
await selectOption(selects.nth(8), 'S-03')
|
|
const thresholdRows = page.locator('.threshold-table tbody tr')
|
await thresholdRows.nth(0).locator('input[type="number"]').nth(0).fill('0')
|
await thresholdRows.nth(0).locator('input[type="number"]').nth(1).fill('20')
|
await thresholdRows.nth(1).locator('input[type="number"]').nth(0).fill('0')
|
await thresholdRows.nth(1).locator('input[type="number"]').nth(1).fill('10')
|
await page.getByRole('button', { name: '提交测试', exact: true }).click()
|
await expect(page).toHaveURL(/\/test-list$/)
|
|
const listRow = page.locator('tbody tr').filter({ hasText: names.test })
|
await expect(listRow).toContainText('待测试')
|
return Number(((await listRow.locator('.id-cell').textContent()) || '').replace('#', ''))
|
}
|
|
async function persistedRows(page: Page, testId: number, kind: 'records' | 'steps' | 'cycles') {
|
const result = await apiJson<ApiResult<PersistedRow[]>>(page, `/system/test/${testId}/data/${kind}`)
|
expect(result.code).toBe(200)
|
return result.data || []
|
}
|
|
async function editFirstRow(page: Page, tabName: '实时' | '工步' | '循环', fieldLabel: string, value: string, reason: string) {
|
await page.getByRole('button', { name: tabName, exact: true }).click()
|
const firstRow = page.locator('.console-data-panel tbody tr').first()
|
await expect(firstRow).toBeVisible()
|
await firstRow.getByRole('button', { name: '编辑', exact: true }).click()
|
const dialog = page.getByRole('dialog', { name: '编辑数据' })
|
await dialog.locator('label').filter({ hasText: fieldLabel }).locator('input').fill(value)
|
await dialog.getByPlaceholder('请输入数据修改原因,提交后将写入修订记录').fill(reason)
|
await dialog.getByRole('button', { name: '提交修改', exact: true }).click()
|
await expect(dialog).not.toBeVisible()
|
await expect(page.getByRole('alert').filter({ hasText: '已提交 1 条数据修改' }).last()).toBeVisible()
|
}
|
|
test('测试管理模块完整观察、修订、图表和预警异常流程可恢复', async ({ page }) => {
|
const suffix = Date.now()
|
const names = {
|
cabinet: `整体验收检测柜${suffix}`,
|
collector: `整体验收采集器${suffix}`,
|
testType: `整体验收类型${suffix}`,
|
group: `整体验收电池组${suffix}`,
|
battery: `BAT-${suffix}`,
|
test: `整体验收测试${suffix}`,
|
}
|
|
await test.step('使用开发模拟器和正式页面创建完整测试配置', async () => {
|
await configureSimulator(page)
|
await login(page)
|
await createPrerequisites(page, names)
|
})
|
const testId = await createTest(page, names)
|
|
await test.step('开始、暂停和继续只改变系统接收状态', async () => {
|
await page.goto(`/test-list/${testId}/console`)
|
await page.getByRole('button', { name: '开始接收', exact: true }).click()
|
await expect(page.getByText('已开始接收,未向设备发送控制命令', { exact: true })).toBeVisible()
|
await expect(page.locator('.running-state')).toHaveText('检测中')
|
await expect(page.locator('.reception-state')).toHaveText('接收中')
|
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('采集器 · 温度超限')
|
await expect(page.locator('.alarm-list')).toContainText('当前值:48.5 ℃ · 预警阈值:0 ~ 20 ℃')
|
|
await page.getByRole('button', { name: '暂停接收', exact: true }).click()
|
await page.getByRole('button', { name: '暂停接收', exact: true }).last().click()
|
await expect(page.locator('.running-state')).toHaveText('暂停')
|
await expect(page.locator('.reception-state')).toHaveText('暂停接收')
|
await expect(page.locator('.cell-card .temperature')).toHaveText('-')
|
const pausedRows = await persistedRows(page, testId, 'records')
|
const pausedCount = pausedRows.length
|
const pausedCabinetSequence = Math.max(0, ...pausedRows.map(row => Number(row.sequenceId) || 0))
|
await page.waitForTimeout(1_500)
|
expect((await persistedRows(page, testId, 'records')).length).toBe(pausedCount)
|
|
await page.getByRole('button', { name: '继续接收', exact: true }).click()
|
await page.getByRole('button', { name: '继续接收', exact: true }).last().click()
|
await expect(page.locator('.running-state')).toHaveText('检测中')
|
// 继续接收仍按正式分钟采样,不要求在页面轮询的二十秒内入库。
|
await expect.poll(async () => (await persistedRows(page, testId, 'records')).length, { timeout: 90_000 }).toBeGreaterThan(pausedCount)
|
await expect.poll(async () => Math.max(0, ...(await persistedRows(page, testId, 'records')).map(row => Number(row.sequenceId) || 0)))
|
.toBeGreaterThan(pausedCabinetSequence)
|
})
|
|
await test.step('重新登录、后端重启和 SSE 重连后恢复状态与事实', async () => {
|
await page.getByRole('button', { name: '工步图表', exact: true }).click()
|
await page.locator('.detail-meta input').fill('Ticket06 最新测试详情')
|
await page.getByRole('button', { name: '保存详情', exact: true }).click()
|
await expect(page.getByText('测试详情已保存', { exact: true })).toBeVisible()
|
await page.getByRole('button', { name: '退出当前账号' }).click()
|
await page.getByRole('button', { name: '退出', exact: true }).click()
|
await login(page)
|
await page.goto(`/test-list/${testId}/console`)
|
await expect(page.locator('.running-state')).toHaveText('检测中')
|
|
await stack.restartBackend()
|
await expect.poll(async () => {
|
try {
|
const result = await apiJson<ApiResult<{ testStatus: string }>>(page, `/system/test/${testId}/live-state`)
|
return result.data?.testStatus
|
} catch {
|
return ''
|
}
|
}, { timeout: 90_000 }).toBe('RUNNING')
|
await expect(page.locator('.running-state')).toHaveText('检测中')
|
await page.reload()
|
await page.getByRole('button', { name: '工步图表', exact: true }).click()
|
await expect(page.locator('.detail-meta input')).toHaveValue('Ticket06 最新测试详情')
|
expect((await persistedRows(page, testId, 'records')).length).toBeGreaterThan(0)
|
})
|
|
await test.step('停止立即进入完成态,异常事实仍可人工关闭追溯', async () => {
|
const warningsBeforeStop = await apiJson<{ code: number; rows: WarningRecord[] }>(page,
|
`/system/test-warning-exceptions/list?testId=${testId}&status=OPEN`)
|
expect(warningsBeforeStop.rows.length).toBeGreaterThan(0)
|
expect(warningsBeforeStop.rows[0].occurrenceCount).toBeGreaterThan(0)
|
expect(JSON.parse(warningsBeforeStop.rows[0].ruleSnapshot).maxValue).toBe(20)
|
|
await page.getByRole('button', { name: '实时监控', exact: true }).click()
|
await page.route(`**/dev-api/system/test/${testId}/reception/end`, async route => {
|
await new Promise(resolve => setTimeout(resolve, 700))
|
await route.fulfill({ status: 500, contentType: 'application/json', body: JSON.stringify({ code: 500, msg: '模拟停止失败' }) })
|
})
|
await page.getByRole('button', { name: '停止测试', exact: true }).click()
|
await page.getByRole('button', { name: '停止测试', exact: true }).last().click()
|
await expect(page.locator('.running-state')).toHaveText('检测中')
|
await expect(page.getByRole('alert').filter({ hasText: '模拟停止失败' })).toBeVisible()
|
await expect(page.locator('.project-meta')).toContainText('检测中')
|
await page.unroute(`**/dev-api/system/test/${testId}/reception/end`)
|
|
await page.getByRole('button', { name: '停止测试', exact: true }).click()
|
await page.getByRole('button', { name: '停止测试', exact: true }).last().click()
|
await expect(page.locator('.running-state')).toHaveText('检测完成')
|
await expect(page.locator('.reception-state')).toHaveText('已结束接收')
|
await expect(page.locator('.alarm-empty')).toContainText('暂无异常')
|
await expect(page.locator('.cell-card .temperature')).toHaveText('-')
|
|
const stoppedCount = (await persistedRows(page, testId, 'records')).length
|
await page.waitForTimeout(1_500)
|
expect((await persistedRows(page, testId, 'records')).length).toBe(stoppedCount)
|
|
await page.goto('/exception')
|
const exceptionRow = page.locator('tbody tr').filter({ hasText: names.test }).first()
|
await expect(exceptionRow).toContainText('待处理')
|
await exceptionRow.getByRole('button', { name: '处理', exact: true }).click()
|
await page.locator('.el-message-box__input textarea').fill('Ticket06 人工确认并关闭')
|
await page.getByRole('button', { name: '完成处理', exact: true }).click()
|
await expect(exceptionRow).toContainText('已处理')
|
|
const warningsAfterClose = await apiJson<{ code: number; rows: WarningRecord[] }>(page,
|
`/system/test-warning-exceptions/list?testId=${testId}`)
|
expect(warningsAfterClose.rows.some(item => item.exceptionStatus === 'CLOSED')).toBe(true)
|
})
|
|
await test.step('三类当前有效数据可修订并形成统一修订记录', async () => {
|
// 模拟器没有工步或循环历史;检测完成后才通过正式导入准备供修订的事实。
|
await page.goto(`/test-list/${testId}/console`)
|
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')
|
await page.getByRole('button', { name: '下载', exact: true }).click()
|
expect((await download).suggestedFilename()).toMatch(/-realtime\.csv$/)
|
await editFirstRow(page, '实时', '电压', '3.3333', 'Ticket06 实时数据修订')
|
await editFirstRow(page, '工步', '终止电压', '3.2222', '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()
|
const recordToRemove = (await persistedRows(page, testId, 'records')).find(row => firstRowText.includes(row.recordNo))
|
expect(recordToRemove).toBeDefined()
|
await page.locator('.console-data-panel tbody tr').first().getByRole('button', { name: '删除', exact: true }).click()
|
await expect(page.getByRole('dialog', { name: '物理删除数据' })).toBeVisible()
|
await page.locator('.el-message-box__input input').fill('Ticket06 删除当前有效数据')
|
await page.getByRole('button', { name: '确定', exact: true }).click()
|
await expect(page.getByRole('alert').filter({ hasText: '已从当前有效视图移除 1 条数据' })).toBeVisible()
|
await expect.poll(async () => (await persistedRows(page, testId, 'records')).some(row => row.id === recordToRemove?.id)).toBe(false)
|
|
await page.getByRole('button', { name: '修订记录', exact: true }).click()
|
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(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 循环数据修订')
|
await expect(revisionTable).toContainText('Ticket06 删除当前有效数据')
|
await page.goto(`/test-list/${testId}/console?tab=data-list`)
|
})
|
|
await test.step('工步详情、具体循环图表和项目图表从后端事实恢复', async () => {
|
await page.getByRole('button', { name: '工步图表', exact: true }).click()
|
await page.locator('.detail-meta input').fill('Ticket06 最新测试详情')
|
await page.getByRole('button', { name: '保存详情', exact: true }).click()
|
await expect(page.getByText('测试详情已保存', { exact: true })).toBeVisible()
|
await expect(page.locator('.data-select-panel')).not.toContainText('全部循环')
|
await expect(page.locator('.metric-chart canvas')).toHaveCount(5)
|
|
await page.reload()
|
await page.getByRole('button', { name: '工步图表', exact: true }).click()
|
await expect(page.locator('.detail-meta input')).toHaveValue('Ticket06 最新测试详情')
|
|
await page.getByRole('button', { name: '项目图表', exact: true }).click()
|
await expect(page.getByText('最终电压1', { exact: true })).toBeVisible()
|
await expect(page.getByText('最终电压2', { exact: true })).toBeVisible()
|
await expect(page.getByText('每循环放电末容量 / F', { exact: true })).toBeVisible()
|
await expect(page.locator('.project-chart canvas')).toHaveCount(3)
|
await expect(page.getByText(/电池组 F 不可用/)).toHaveCount(0)
|
})
|
|
await test.step('完成态重启后保留详情和三类修订事实', async () => {
|
await stack.restartBackend()
|
await page.goto(`/test-list/${testId}/console`)
|
await expect(page.locator('.running-state')).toHaveText('检测完成')
|
await expect(page.locator('.reception-state')).toHaveText('已结束接收')
|
await page.getByRole('button', { name: '工步图表', exact: true }).click()
|
await expect(page.locator('.detail-meta input')).toHaveValue('Ticket06 最新测试详情')
|
expect((await persistedRows(page, testId, 'records')).length).toBeGreaterThan(0)
|
expect((await persistedRows(page, testId, 'steps')).some(row => Number(row.endVoltage) === 3.2222)).toBe(true)
|
expect((await persistedRows(page, testId, 'cycles')).some(row => Number(row.dischargeCapacity) === 4)).toBe(true)
|
})
|
|
await test.step('三类导入直接落表,修改当前 F 后已有测试读取新值', async () => {
|
for (const [index, kind] of ['REALTIME', 'STEP', 'CYCLE'].entries()) {
|
const row = { recordNo: `IMPORT-${kind}`, batteryNo: names.battery, recordedAt: Date.now() + 3_600_000 + index,
|
cycleNo: 999, stepNo: 1, stepName: '人工核对', sequenceId: 1, voltage: 3.8, current: 1,
|
capacity: 2, energy: 4, endVoltage: 3.9, startVoltage: 3.1, stepDuration: 60,
|
chargeCapacity: 2, dischargeCapacity: 1.9, chargeEnergy: 4, dischargeEnergy: 3.8, efficiency: 95 }
|
const result = 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: 'Ticket13 导入验收' }),
|
})
|
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_record_id IN ('INITIAL-${kind}', '${row.recordNo}')`)).toBe(kind === 'REALTIME' ? '1' : '2')
|
// 正式导入使用该电池绑定的完整通道身份,IMPORT 只是修订动作类型。
|
expect(stack.queryDatabase(`SELECT COUNT(*) FROM ${table} r JOIN t_test_channel_binding b ON b.binding_id=r.binding_id
|
JOIN t_detection_channel c ON c.channel_id=b.channel_id WHERE r.test_id=${testId} AND r.source_record_id='${row.recordNo}'
|
AND r.source_identity=CONCAT(c.adapter_code,':',c.devtype,':',c.devid,':',c.subdevid,':',c.chlid)`)).toBe('1')
|
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_record_id='${row.recordNo}'`)).toBe('1')
|
}
|
const detail = await apiJson<ApiResult<{ batteries: Array<{ groupId: number }> }>>(page, `/system/test/${testId}`)
|
const groupId = detail.data.batteries[0]!.groupId
|
const group = await apiJson<ApiResult<Record<string, unknown> & { batteries: Array<{ batteryNo: string }> }>>(page, `/system/batteryGroup/${groupId}`)
|
const updated = await apiJson<ApiResult<unknown>>(page, '/system/batteryGroup', { method: 'PUT',
|
body: JSON.stringify({ ...group.data, f: '4', batteryNos: group.data.batteries.map(battery => battery.batteryNo) }) })
|
expect(updated.code).toBe(200)
|
expect((await apiJson<ApiResult<{ f: string }>>(page, `/system/batteryGroup/${groupId}`)).data.f).toBe('4')
|
const current = await apiJson<ApiResult<{ batteries: Array<{ fields: Array<{ fieldName: string; fieldValue: string }> }> }>>(page, `/system/test/${testId}`)
|
expect(current.data.batteries[0]!.fields.find(field => field.fieldName === 'F')?.fieldValue).toBe('4')
|
await page.goto(`/test-list/${testId}/console?tab=project-chart`)
|
await expect(page.getByText(/电池组 F 不可用/)).toHaveCount(0)
|
})
|
|
await test.step('设备协议日志只包含发现和实时查询', async () => {
|
await page.goto(`${stack.backendUrl}/dev/bts-simulator.html`)
|
await expect(page.locator('#requests .request-command').first()).toBeVisible()
|
const commands = await page.locator('#requests .request-command').allTextContents()
|
expect(commands.length).toBeGreaterThan(0)
|
expect(commands.every(command => ['discover', 'observe', 'cabinet-realtime', 'collector-realtime', 'getdevinfo', 'inquire'].includes(command))).toBe(true)
|
})
|
|
await test.step('同通道跨测试复用,所有状态整体删除无痕且不影响其他事实', async () => {
|
await page.goto(`/test-list/${testId}/console`)
|
const original = await apiJson<ApiResult<Record<string, unknown>>>(page, `/system/test/${testId}`)
|
const deviceCounts = stack.queryDatabase('SELECT (SELECT COUNT(*) FROM t_detection_cabinet), (SELECT COUNT(*) FROM t_collector), (SELECT COUNT(*) FROM t_battery_group)')
|
const tables = ['t_test_realtime_record', 't_test_step_record', 't_test_cycle_record', 't_test_data_revision_action',
|
't_test_warning_exception', 't_test_logic_step_mapping', 't_test_collector_channel', 't_test_channel_binding',
|
't_test_warning_threshold', 't_test_battery', 't_test']
|
for (const lifecycle of ['WAITING', 'RUNNING', 'PAUSED']) {
|
const created = await apiJson<ApiResult<{ testId: number }>>(page, '/system/test', {
|
method: 'POST', body: JSON.stringify({ ...original.data, testId: null, testName: `${names.test}-${lifecycle}` }),
|
})
|
expect(created.code).toBe(200)
|
const id = created.data.testId
|
if (lifecycle !== 'WAITING') {
|
expect((await apiJson<ApiResult<unknown>>(page, `/system/test/${id}/reception/start`, { method: 'POST' })).code).toBe(200)
|
await expect.poll(async () => (await persistedRows(page, id, 'records')).length).toBeGreaterThan(0)
|
}
|
if (lifecycle === 'PAUSED') expect((await apiJson<ApiResult<unknown>>(page, `/system/test/${id}/reception/pause`, { method: 'POST' })).code).toBe(200)
|
expect((await apiJson<ApiResult<unknown>>(page, `/system/test/${id}`, { method: 'DELETE' })).code).toBe(200)
|
for (const table of tables) expect(stack.queryDatabase(`SELECT COUNT(*) FROM ${table} WHERE test_id=${id}`)).toBe('0')
|
expect((await apiJson<ApiResult<unknown>>(page, `/system/test/${testId}`)).code).toBe(200)
|
}
|
expect((await apiJson<ApiResult<unknown>>(page, `/system/test/${testId}`, { method: 'DELETE' })).code).toBe(200)
|
for (const table of tables) expect(stack.queryDatabase(`SELECT COUNT(*) FROM ${table} WHERE test_id=${testId}`)).toBe('0')
|
expect(stack.queryDatabase('SELECT COUNT(*) FROM sys_oper_log WHERE business_type=3 AND oper_url LIKE \'%/system/test/%\'')).toBe('0')
|
expect(stack.queryDatabase('SELECT (SELECT COUNT(*) FROM t_detection_cabinet), (SELECT COUNT(*) FROM t_collector), (SELECT COUNT(*) FROM t_battery_group)')).toBe(deviceCounts)
|
})
|
|
await test.step('无设备模式和真实 BTS 装配均可启动', async () => {
|
await stack.restartBackend('druid')
|
expect((await page.request.get(`${stack.backendUrl}/captchaImage`)).ok()).toBe(true)
|
await stack.restartBackend('druid,bts')
|
expect((await page.request.get(`${stack.backendUrl}/captchaImage`)).ok()).toBe(true)
|
})
|
})
|