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()
|
}
|