yeg
2026-09-08 33576db53f58030a6f3b090e82d552d5e75166d1
按当前业务契约完成全量验收脚本复验

修正完成态导入时序、来源详情合列、采集器当前观测和测试当前字段断言。保留恢复、修订、归属、权限和删除完整验收,更新正式迁移及数据列表回归。七份脚本十项受影响浏览器验收均通过,前端构建通过。
7个文件已修改
210 ■■■■■ 已修改文件
tests/e2e/collector-sampling.spec.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tests/e2e/compare-save-and-reopen.spec.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tests/e2e/pause-resume-reception.spec.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tests/e2e/remove-drifted-result-query.spec.ts 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tests/e2e/runtime-without-simulator.spec.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tests/e2e/test-management-acceptance.spec.ts 152 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tests/e2e/test-required-device-bindings.spec.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tests/e2e/collector-sampling.spec.ts
@@ -86,19 +86,19 @@
  page.once('dialog', dialog => dialog.accept())
  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 page.getByRole('button', { name: '全部设备离线', exact: true }).click()
  await off
  await expect(page.locator('#device-power')).toHaveText('已关机')
  await expect(page.locator('#device-power')).toHaveText('全部离线')
  expect(await realtime(page)).toMatchObject({ online: false, measurements: [] })
  await stack.restartBackend()
  await page.goto(stack.backendUrl + '/dev/bts-simulator.html')
  await expect(page.locator('#device-power')).toHaveText('已关机')
  await expect(page.locator('#device-power')).toHaveText('全部离线')
  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 page.getByRole('button', { name: '全部设备上线', exact: true }).click()
  await on
  await expect(page.locator('#device-power')).toHaveText('已开机')
  await expect(page.locator('#device-power')).toHaveText('全部在线')
  const resumed = await realtime(page)
  expect(resumed.measurements).toHaveLength(13)
  expect(resumed.measurements[0]!.sequenceId).toBeGreaterThan(second.measurements[0]!.sequenceId)
tests/e2e/compare-save-and-reopen.spec.ts
@@ -90,7 +90,7 @@
  expect(saved.items[1].records).toHaveLength(3)
  expect(saved.items[0].records.find((row: any) => row.recordNo === '1').current).toBe(-8.1651)
  expect(saved.items[0].businessFields[0].fieldValue).toBe('12.5')
  expect(saved.settings).toEqual(payload.settings)
  expect(saved.settings).toEqual({ ...payload.settings, sourceDetailOverrides: {} })
  expect((await api(page, `/system/compare-report/${saved.id}`)).data).toEqual(saved)
  expect(stack.queryDatabase('SELECT COUNT(*) FROM t_compare_report_version')).toBe('1')
@@ -145,7 +145,7 @@
  expect(browserSaved.items).toHaveLength(3)
  expect(browserSaved.settings.metrics).not.toContain('positiveVoltage')
  expect(browserSaved.description).toBe('浏览器说明')
  expect(browserSaved.settings.detailFieldValues['质量']['8101|8101|1|1|']).toBe('88')
  expect(browserSaved.settings.detailFieldValues['质量']).toEqual({ '8101|8101': '88' })
  await api(page, '/system/test/8101/console-data/actions', 'POST', {
    dataType: 'REALTIME', operationType: 'EDIT', scopeSummary: '首行电流修订', reason: '验证报表独立',
tests/e2e/pause-resume-reception.spec.ts
@@ -31,9 +31,9 @@
  await page.goto('/test-list/42/console')
  await expect(page.getByRole('button', { name: '暂停接收', exact: true })).toBeVisible()
  await page.getByRole('button', { name: '暂停接收', exact: true }).click()
  await expect(page.getByText('暂停接收将停止系统向检测柜和采集器请求新数据,但设备仍继续运行,不会向设备发送暂停命令,是否继续?', { exact: true })).toBeVisible()
  await page.getByRole('button', { name: '暂停接收', exact: true }).last().click()
  await expect(page.locator('.reception-state')).toHaveText('暂停接收')
  await expect(page.getByText('暂停期间停止请求,当前快照不纳入结果', { exact: true }).first()).toBeVisible()
  await expect(page.getByText('已暂停接收,系统已停止请求新数据', { exact: true })).toBeVisible()
  await page.getByRole('button', { name: '继续接收', exact: true }).click()
  await page.getByRole('button', { name: '继续接收', exact: true }).last().click()
tests/e2e/remove-drifted-result-query.spec.ts
@@ -24,6 +24,16 @@
test('测试列表仍可进入控制台数据列表', async ({ page }) => {
  await page.addInitScript(() => localStorage.setItem('battery_detection_auth_token', 'ticket20-test-token'))
  await page.route('**/system/testType/list**', route => route.fulfill({
    status: 200, contentType: 'application/json', body: JSON.stringify({ code: 200, total: 0, rows: [] }),
  }))
  const loadedData = new Set<string>()
  for (const kind of ['records', 'steps', 'cycles']) {
    await page.route(`**/system/test/77/data/${kind}`, route => {
      loadedData.add(kind)
      return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 200, data: [] }) })
    })
  }
  await page.route('**/system/test/list**', route => route.fulfill({
    status: 200,
    contentType: 'application/json',
@@ -81,6 +91,16 @@
  await expect(page).toHaveURL(/\/test-list\/77\/console$/)
  await page.getByRole('button', { name: '数据列表', exact: true }).click()
  await expect(page.getByText('查询表格', { exact: true })).toBeVisible()
  const panel = page.locator('.console-data-panel')
  await expect(panel).toBeVisible()
  await expect(panel.locator('.section-heading small')).toHaveText('共 0 条')
  await expect.poll(() => [...loadedData].sort()).toEqual(['cycles', 'records', 'steps'])
  await expect(panel.locator('.load-error')).toHaveCount(0)
  for (const tab of ['实时', '工步', '循环']) {
    const button = panel.getByRole('button', { name: tab, exact: true })
    await button.click()
    await expect(button).toHaveAttribute('aria-current', 'page')
    await expect(panel.locator('tbody .empty-cell')).toBeVisible()
  }
  await expect(page.getByRole('button', { name: '结果查询', exact: true })).toHaveCount(0)
})
tests/e2e/runtime-without-simulator.spec.ts
@@ -9,7 +9,7 @@
  expect((await request.get(`${stack.backendUrl}/captchaImage`)).ok()).toBe(true)
  expect(stack.queryDatabase('SELECT COUNT(*) FROM t_device_adapter')).toBe('0')
  const migrations = stack.queryDatabase('SELECT GROUP_CONCAT(version ORDER BY installed_rank) FROM flyway_schema_history WHERE success=1')
  expect(migrations).toBe('1,2,3,4,5,6')
  expect(migrations).toBe('1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19')
  await stack.restartBackend('druid,bts')
  expect((await request.get(`${stack.backendUrl}/captchaImage`)).ok()).toBe(true)
  expect(stack.queryDatabase('SELECT GROUP_CONCAT(version ORDER BY installed_rank) FROM flyway_schema_history WHERE success=1')).toBe(migrations)
tests/e2e/test-management-acceptance.spec.ts
@@ -215,7 +215,8 @@
    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('采集器 · 温度超限')
    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()
@@ -231,13 +232,85 @@
    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).toBeGreaterThan(pausedCount)
    // 继续接收仍按正式分钟采样,不要求在页面轮询的二十秒内入库。
    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,
@@ -304,72 +377,16 @@
    await expect(page.getByText(/电池组 F 不可用/)).toHaveCount(0)
  })
  await test.step('重新登录、后端重启和 SSE 重连后恢复状态与事实', async () => {
    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 test.step('完成态重启后保留详情和三类修订事实', async () => {
    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.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('停止立即进入完成态,异常事实仍可人工关闭追溯', 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('三类导入直接落表,修改当前 F 后已有测试读取新值', async () => {
@@ -384,10 +401,14 @@
      })
      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(kind === 'REALTIME' ? '1' : '2')
      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_identity='IMPORT'`)).toBe('1')
      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
@@ -404,6 +425,7 @@
  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)
tests/e2e/test-required-device-bindings.spec.ts
@@ -54,7 +54,7 @@
  }, { requestPath: path, requestInit: init }) as Promise<T>
}
test('测试创建保存必填双设备绑定、可选映射和字段快照', async ({ page }) => {
test('测试创建保存必填双设备绑定、可选映射并读取电池组当前字段', async ({ page }) => {
  const suffix = Date.now()
  const cabinetName = `Ticket07检测柜${suffix}`
  const collectorName = `Ticket07采集器${suffix}`
@@ -178,12 +178,12 @@
    await expect(page.locator('.battery-table tbody tr').first().locator('.el-select').last()).toContainText('S-05')
  })
  await test.step('后端拒绝重复映射并保留测试共享字段快照', async () => {
  await test.step('后端拒绝重复映射并按测试电池读取共享字段当前值', async () => {
    const detail = await apiJson<{ code: number; data: Record<string, any> }>(page, `/system/test/${testId}`)
    expect(detail.code).toBe(200)
    expect(detail.data.fieldSnapshots).toEqual(expect.arrayContaining([
      expect.objectContaining({ fieldName, fieldValue: '72' }),
    ]))
    expect(detail.data.batteries.find((battery: Record<string, any>) => battery.batteryNo === batteryNo)).toMatchObject({
      batteryNo, fields: expect.arrayContaining([expect.objectContaining({ fieldName, fieldValue: '72' })]),
    })
    const duplicatePayload = {
      ...detail.data,
@@ -207,10 +207,12 @@
    await page.locator('tbody tr').filter({ hasText: groupName }).getByRole('button', { name: '编辑' }).click()
    await page.getByPlaceholder(`输入${fieldName}`).fill('99')
    await page.getByRole('button', { name: '保存编辑电池组', exact: true }).click()
    await expect(page).toHaveURL(/\/battery-group$/)
    const afterGroupChange = await apiJson<{ code: number; data: Record<string, any> }>(page, `/system/test/${testId}`)
    expect(afterGroupChange.data.fieldSnapshots).toEqual(expect.arrayContaining([
      expect.objectContaining({ fieldName, fieldValue: '72' }),
    ]))
    expect(afterGroupChange.code).toBe(200)
    expect(afterGroupChange.data.batteries.find((battery: Record<string, any>) => battery.batteryNo === batteryNo)).toMatchObject({
      batteryNo, fields: expect.arrayContaining([expect.objectContaining({ fieldName, fieldValue: '99' })]),
    })
  })
  await test.step('未开始接收的测试可确认删除并在刷新后保持消失', async () => {