yeg
2026-09-08 cfd4511378aefa04d4e06058cee628b6edd8e9c1
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
import assert from 'node:assert/strict'
import { readFile, mkdir } from 'node:fs/promises'
import { chromium, expect } from '@playwright/test'
import XLSX from 'xlsx'
import { consoleImportColumns, createConsoleImportWorkbook } from '../src/utils/consoleDataImport.ts'
 
// 拦截构建产物和 API 完成页面验收,不启动业务服务或连接共享数据库。
const origin = 'http://single-sheet-import.test'
const output = '../.scratch/simulator-and-data-import-v1/single-sheet-browser'
await mkdir(output, { recursive: true })
const browser = await chromium.launch({ channel: 'chrome', headless: true })
const calls = []
const liveCalls = []
let workbookRows = null
let result = { importedCount: 1, skippedCount: 0 }
const detail = { testId: 1, testName: '甲方数据导入验收', testStatus: 'COMPLETED', receptionStatus: 'ENDED',
  thresholds: [], logicStepMappings: [], batteries: [{ testBatteryId: 1, batteryNo: 'BAT-1', groupName: '演示电池组', fields: [], collectorChannels: [], binding: { channelId: 1 } }] }
 
function standardFile(type, invalid = false) {
  const fields = consoleImportColumns[type]
  const row = { recordNo: '17', batteryNo: 'BAT-1', recordedAt: '2023-05-08 11:42:11', cycleNo: 1, stepNo: 1, stepName: '恒流放电', sequenceId: 17, capacity: 8.5122, temperature: 25, positivePotential: 1.5, negativePotential: -0.1 }
  const rows = [Object.values(fields), Object.keys(fields).map(field => row[field] ?? '')]
  if (invalid) {
    while (rows.length < 19) rows.push([])
    rows.push(Object.keys(fields).map(field => field === 'cycleNo' ? 0 : row[field] ?? ''))
  }
  const workbook = XLSX.utils.book_new()
  XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet(rows), { REALTIME: '实时', STEP: '工步', CYCLE: '循环' }[type])
  return { name: '单页数据.xlsx', mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', buffer: XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' }) }
}
 
try {
  const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } })
  const errors = []
  page.on('pageerror', error => errors.push(error.message))
  await page.addInitScript(() => localStorage.setItem('battery_detection_auth_token', 'browser-test'))
  await page.route(`${origin}/**`, 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('/console-data/actions')) {
      calls.push(route.request().postDataJSON())
      if (calls.at(-1).dataType === 'ALL') workbookRows = JSON.parse(calls.at(-1).changesSummary)
      return route.fulfill({ json: { code: 200, actionId: 1, ...result } })
    }
    if (path.endsWith('/live-state')) { liveCalls.push(path); return route.fulfill({ json: { code: 200, data: { ...detail, sources: [], exceptions: [] } } }) }
    if (/\/data\/(records|steps|cycles)$/.test(path)) {
      const type = { records: 'REALTIME', steps: 'STEP', cycles: 'CYCLE' }[path.split('/').at(-1)]
      const data = workbookRows?.[type]?.slice(0, 1).map(row => ({ ...row, recordId: 1, stepRecordId: 1, cycleRecordId: 1, stableIdentity: `${type.toLowerCase()}:1` })) || []
      return route.fulfill({ json: { code: 200, data } })
    }
    if (path.endsWith('/system/test/1')) return route.fulfill({ json: { code: 200, data: detail } })
    if (path.endsWith('/getInfo')) return route.fulfill({ json: { code: 200, user: { userName: 'admin' }, roles: ['admin'] } })
    return route.fulfill({ body: await readFile('dist/index.html'), contentType: 'text/html' })
  })
  for (const type of ['REALTIME', 'STEP', 'CYCLE']) {
    await page.goto(`${origin}/test-list/1/console?tab=data-list&dataType=${type}`)
    const downloadEvent = page.waitForEvent('download')
    await page.getByRole('button', { name: '下载模板', exact: true }).click()
    const download = await downloadEvent
    const workbook = XLSX.read(await readFile(await download.path()))
    assert.equal(workbook.SheetNames.length, 1)
    assert.deepEqual(XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]], { header: 1 })[0], Object.values(consoleImportColumns[type]))
    await page.goto(`${origin}/test-list/1/console/data/import?dataType=${type}`)
    await expect(page.locator('.status-notice')).toHaveCount(0)
    await page.locator('input[type=file]').setInputFiles(process.env.PARTY_EXCEL || standardFile(type))
    await expect(page.getByRole('button', { name: '确认导入', exact: true })).toBeEnabled()
    if (type === 'REALTIME') await page.screenshot({ path: `${output}/preview-1440.png`, fullPage: true })
    await page.getByRole('button', { name: '确认导入', exact: true }).click()
    await page.locator('.el-message-box input').fill('甲方数据兼容验收')
    await page.locator('.el-message-box').getByRole('button', { name: '确定', exact: true }).click()
    await expect(page.locator('.result-stage')).toContainText('导入完成')
    assert.equal(calls.at(-1).dataType, type)
    const imported = JSON.parse(calls.at(-1).changesSummary)
    assert.equal(imported[0].batteryNo, 'BAT-1')
    if (process.env.PARTY_EXCEL) assert.equal(imported.length, { REALTIME: 7605, STEP: 129, CYCLE: 33 }[type])
    if (process.env.PARTY_EXCEL && type === 'REALTIME') {
      const source = XLSX.readFile(process.env.PARTY_EXCEL).Sheets.record
      const fields = ['temperature', 'positivePotential', 'negativePotential']
      for (let row = 0; row < imported.length; row++) {
        assert.deepEqual(fields.map(field => imported[row][field]), fields.map((_, column) =>
          source[XLSX.utils.encode_cell({ r: row + 1, c: column + 10 })]?.v ?? null))
      }
    }
  }
  await page.goto(`${origin}/test-list/1/console/data/import?dataType=REALTIME`)
  await page.locator('input[type=file]').setInputFiles(standardFile('REALTIME', true))
  await expect(page.getByRole('button', { name: '确认导入', exact: true })).toBeDisabled()
  await expect(page.locator('.el-alert')).toContainText('第20行,列「循环序号」')
  assert.equal(calls.length, 3)
  await page.setViewportSize({ width: 390, height: 844 })
  await expect(page.locator('.app-main')).toHaveCSS('margin-left', '0px')
  await page.screenshot({ path: `${output}/errors-390.png`, fullPage: true })
  assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false)
  result = { importedCount: 0, skippedCount: 1 }
  await page.goto(`${origin}/test-list/1/console/data/import?dataType=REALTIME`)
  await page.locator('input[type=file]').setInputFiles(standardFile('REALTIME'))
  await page.getByRole('button', { name: '确认导入', exact: true }).click()
  await page.locator('.el-message-box input').fill('重复导入核对')
  await page.locator('.el-message-box').getByRole('button', { name: '确定', exact: true }).click()
  await expect(page.locator('.result-stage')).toContainText('重复跳过1 条')
  await page.screenshot({ path: `${output}/result-390.png`, fullPage: true })
 
  function workbookFile(mode = 'valid') {
    const workbook = createConsoleImportWorkbook()
    for (const [index, type] of ['REALTIME', 'STEP', 'CYCLE'].entries()) {
      const single = XLSX.read(standardFile(type).buffer)
      workbook.Sheets[workbook.SheetNames[index]] = single.Sheets[single.SheetNames[0]]
    }
    if (mode === 'missing') { workbook.SheetNames.pop(); delete workbook.Sheets['循环'] }
    if (mode === 'invalid') XLSX.utils.sheet_add_aoa(workbook.Sheets['循环'], [['错误数值']], { origin: 'F20' })
    if (mode === 'identity') XLSX.utils.sheet_add_aoa(workbook.Sheets['循环'], [['OTHER']], { origin: 'B2' })
    return { name: '三 Sheet.xlsx', mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', buffer: XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' }) }
  }
  await page.setViewportSize({ width: 1440, height: 1000 })
  await page.goto(`${origin}/test-list/1/console?tab=data-list`)
  await page.getByRole('button', { name: '三 Sheet 一键导入', exact: true }).click()
  const templateEvent = page.waitForEvent('download')
  await page.getByRole('button', { name: '下载三 Sheet 模板', exact: true }).click()
  const template = XLSX.read(await readFile(await (await templateEvent).path()))
  assert.deepEqual(template.SheetNames, ['实时', '工步', '循环'])
  for (const [index, type] of ['REALTIME', 'STEP', 'CYCLE'].entries()) assert.deepEqual(XLSX.utils.sheet_to_json(template.Sheets[template.SheetNames[index]], { header: 1 })[0], Object.values(consoleImportColumns[type]))
  const beforeInvalid = calls.length
  for (const mode of ['missing', 'invalid', 'identity']) {
    await page.goto(`${origin}/test-list/1/console/data/import?mode=workbook`)
    await page.locator('input[type=file]').setInputFiles(workbookFile(mode))
    await expect(page.locator('.sheet-summary tbody tr')).toHaveCount(3)
    await expect(page.getByRole('button', { name: '确认导入', exact: true })).toBeDisabled()
    await expect(page.locator('.el-alert')).toContainText('循环')
    const errorDownload = page.waitForEvent('download')
    await page.getByRole('button', { name: '下载全部错误', exact: true }).click()
    const errorsWorkbook = XLSX.read(await readFile(await (await errorDownload).path()))
    assert.equal(XLSX.utils.sheet_to_json(errorsWorkbook.Sheets['失败数据'])[0]['工作表'], '循环')
  }
  assert.equal(calls.length, beforeInvalid)
  await page.goto(`${origin}/test-list/1/console/data/import?mode=workbook`)
  const beforeLive = liveCalls.length
  result = { importedCount: process.env.PARTY_EXCEL ? 7767 : 3, skippedCount: 0 }
  await page.locator('input[type=file]').setInputFiles(process.env.PARTY_EXCEL || workbookFile())
  await expect(page.getByRole('button', { name: '确认导入', exact: true })).toBeEnabled()
  for (const label of ['实时', '工步', '循环']) {
    await page.getByRole('tab', { name: label, exact: true }).click()
    await expect(page.locator('.preview-table-wrap tbody tr')).not.toHaveCount(0)
  }
  await page.screenshot({ path: `${output}/workbook-preview-1440.png`, fullPage: true })
  await page.setViewportSize({ width: 390, height: 844 })
  await expect(page.locator('.app-main')).toHaveCSS('margin-left', '0px')
  await page.screenshot({ path: `${output}/workbook-preview-390.png`, fullPage: true, animations: 'disabled' })
  assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false)
  await page.getByRole('button', { name: '确认导入', exact: true }).click()
  await page.locator('.el-message-box input').fill('三个 Sheet 共用原因')
  await page.locator('.el-message-box').getByRole('button', { name: '确定', exact: true }).click()
  await expect(page.locator('.result-stage')).toContainText('导入批次 #1')
  assert.equal(calls.length, beforeInvalid + 1)
  assert.equal(calls.at(-1).dataType, 'ALL')
  assert.equal(calls.at(-1).reason, '三个 Sheet 共用原因')
  assert.deepEqual(Object.keys(workbookRows), ['REALTIME', 'STEP', 'CYCLE'])
  assert.deepEqual(Object.values(workbookRows).map(rows => rows.length), process.env.PARTY_EXCEL ? [7605, 129, 33] : [1, 1, 1])
  assert.equal(liveCalls.length, beforeLive)
  await page.screenshot({ path: `${output}/workbook-result-390.png`, fullPage: true })
  for (const type of ['REALTIME', 'STEP', 'CYCLE']) {
    await page.goto(`${origin}/test-list/1/console?tab=data-list&dataType=${type}`)
    await expect(page.locator('.data-toolbar')).toBeVisible()
    await expect(page.locator('.table-wrap tbody')).toContainText('BAT-1')
    await page.reload()
    await expect(page.locator('.table-wrap tbody')).toContainText('BAT-1')
  }
  assert.deepEqual(errors, [])
  console.log('Ticket03/04 浏览器:独立模板、一键模板、三 Sheet 预览、错误下载和阻断、单次提交原因与批次、子页刷新及窄屏通过;导入未请求实时监控')
} finally {
  await browser.close()
}