yeg
2026-09-09 b226aa0db7fbecb8fff92476537adc0ca3e16a4f
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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
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/console-excel-field-alignment/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, relativeTimeSeconds: '1:02:32.125', stepDuration: '1:02:32.125', capacity: 8.5122, temperature: 25, positivePotential: 1.5, negativePotential: -0.1, chargeMedianVoltage: 0, dischargeMedianVoltage: 1.1709 }
  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 = createConsoleImportWorkbook(type)
  workbook.Sheets[workbook.SheetNames[0]] = XLSX.utils.aoa_to_sheet(rows)
  if (type !== 'REALTIME') XLSX.utils.sheet_add_aoa(workbook.Sheets.record, [Object.keys(consoleImportColumns.REALTIME).map(field => row[field] ?? '')], { origin: 'A2' })
  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: result.importedCount ? 1 : null,
        actionIds: result.importedCount ? calls.at(-1).dataType === 'ALL' ? [1, 2, 3] : [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, sourceType: type === 'REALTIME' ? 'SNAPSHOT' : undefined, deviceTime: row.recordedAt, 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.setViewportSize({ width: 1440, height: 1000 })
    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, type === 'REALTIME' ? 1 : 2)
    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))
      }
    }
  }
  if (process.env.PARTY_EXCEL) {
    const imported = JSON.parse(calls.find(call => call.dataType === 'STEP').changesSummary)
    const source = XLSX.utils.sheet_to_json(XLSX.readFile(process.env.PARTY_EXCEL).Sheets.step)
    assert.deepEqual(imported.map(row => [row.chargeMedianVoltage, row.dischargeMedianVoltage]),
      source.map(row => [row['充电中值电压(V)'], row['放电中值电压(V)']]))
  }
  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、#2、#3')
  await expect(page.locator('.result-stage')).not.toContainText('导入批次')
  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')
    const expectedHeaders = ['记录编号', '电池', ...XLSX.utils.sheet_to_json(XLSX.readFile(process.env.PARTY_EXCEL || (await import('node:path')).resolve('../outputs/ticket03-excel/277#(5)(1)_补充三项测试值.xls')).Sheets[{ REALTIME: 'record', STEP: 'step', CYCLE: 'cycle' }[type]], { header: 1 })[0]]
    assert.deepEqual((await page.locator('.table-wrap th').allTextContents()).map(value => value.trim()).slice(1, -1), expectedHeaders)
    await expect(page.locator('.record-code').first()).toHaveText(type.toLowerCase() + ':1')
    await page.screenshot({ path: `${output}/list-${type.toLowerCase()}-390.png`, fullPage: true })
    await page.setViewportSize({ width: 1440, height: 1000 })
    await page.screenshot({ path: `${output}/list-${type.toLowerCase()}.png`, fullPage: true, animations: 'disabled' })
    const downloadEvent = page.waitForEvent('download')
    await page.getByRole('button', { name: '下载', exact: true }).click()
    const csvText = await readFile(await (await downloadEvent).path(), 'utf8')
    const csvRows = XLSX.utils.sheet_to_json(XLSX.read(csvText.replace(/^\uFEFF/, ''), { type: 'string', raw: true }).Sheets.Sheet1, { header: 1, raw: true })
    assert.deepEqual(csvRows[0], expectedHeaders)
    assert.equal(csvRows[1][0], type.toLowerCase() + ':1')
    if (type === 'REALTIME') {
      assert.equal(csvRows[1][6], process.env.PARTY_EXCEL ? '0:00:00.000' : '1:02:32.125')
      assert.match(csvRows[1][11], /^2023-05-08 11:42:11/)
    }
    if (type === 'STEP') {
      assert.equal(csvRows[1][5], process.env.PARTY_EXCEL ? '1:02:32.000' : '1:02:32.125')
      await expect(page.locator('.table-wrap thead')).toContainText('充电中值电压(V)')
      await expect(page.locator('.table-wrap thead')).toContainText('放电中值电压(V)')
      await expect(page.locator('.table-wrap tbody')).toContainText('0.0000')
      await expect(page.locator('.table-wrap tbody')).toContainText('1.1709')
      const csvEvent = page.waitForEvent('download')
      await page.getByRole('button', { name: '下载', exact: true }).click()
      const csv = await readFile(await (await csvEvent).path(), 'utf8')
      assert.match(csv, /充电中值电压\(V\)/)
      assert.match(csv, /放电中值电压\(V\)/)
      assert.match(csv, /"0\.0000","1\.1709"/)
      assert.doesNotMatch(csv, /峰值|谷值/)
      await page.locator('.row-actions').first().getByRole('button', { name: '编辑', exact: true }).click()
      const durationInput = page.locator('.single-form-grid label').filter({ hasText: '工步时间(h:min:s.ms)' }).locator('input')
      await expect(durationInput).toHaveValue(process.env.PARTY_EXCEL ? '1:02:32.000' : '1:02:32.125')
      await durationInput.fill('1:02:33.250')
      await page.locator('.remark-field textarea').fill('核对时长编辑与记录编号')
      const edited = page.waitForResponse(response => response.url().endsWith('/console-data/actions'))
      await page.getByRole('button', { name: '提交修改', exact: true }).click()
      await edited
      assert.equal(calls.at(-1).operationType, 'EDIT')
      assert.deepEqual(JSON.parse(calls.at(-1).identitySummary), ['step:1'])
      assert.deepEqual(JSON.parse(calls.at(-1).overrides[0].fieldValues), { stepDuration: '3753.25' })
    }
  }
  assert.deepEqual(errors, [])
  console.log('浏览器:独立及三 Sheet 模板、预览校验、全部修订编号、子页刷新与窄屏通过;导入未请求实时监控')
} finally {
  await browser.close()
}