import assert from 'node:assert/strict'
|
import { readFile, mkdir } from 'node:fs/promises'
|
import { chromium, expect } from '@playwright/test'
|
import XLSX from 'xlsx'
|
import { parseConsoleImportWorkbook } from '../src/utils/consoleDataImport.ts'
|
|
const workbookPath = process.env.PARTY_EXCEL
|
assert.ok(workbookPath, '请通过 PARTY_EXCEL 指定甲方验收文件')
|
const sheets = parseConsoleImportWorkbook(XLSX.readFile(workbookPath), ['BAT-1'])
|
assert.deepEqual(sheets.map(sheet => sheet.rows.length), [7605, 129, 33])
|
assert.deepEqual(sheets.flatMap(sheet => [...sheet.errors, ...sheet.rows.flatMap(row => row.errors)]), [])
|
// 仅模拟 API 返回与编辑持久化;输入数值使用甲方文件的真实解析结果。
|
const rows = Object.fromEntries(sheets.map(sheet => [sheet.type, sheet.rows.slice(0, 2).map((row, index) => ({ ...row.data, sourceType: 'SNAPSHOT', stableIdentity: `${sheet.type}:${index}`, deviceTime: row.data.recordedAt }))]))
|
const detail = { testId: 1, testName: '甲方单位对照验收', testStatus: 'COMPLETED', receptionStatus: 'ENDED', thresholds: [], logicStepMappings: [], batteries: [{ testBatteryId: 1, batteryNo: 'BAT-1', groupId: 1, groupName: '演示电池组', fields: [], collectorChannels: [], binding: { channelId: 1 } }] }
|
const origin = 'http://test-data-units.test'
|
const output = '../.scratch/data-analysis-v1/units-browser'
|
await mkdir(output, { recursive: true })
|
const calls = []
|
const browser = await chromium.launch({ channel: 'chrome', headless: true })
|
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')) {
|
const action = route.request().postDataJSON()
|
calls.push(action)
|
for (const override of action.overrides || []) {
|
const row = rows[action.dataType].find(row => row.stableIdentity === override.stableIdentity)
|
for (const [field, value] of Object.entries(JSON.parse(override.fieldValues))) row[field] = value === '-' ? null : Number(value)
|
}
|
return route.fulfill({ json: { code: 200, actionId: calls.length } })
|
}
|
if (/\/data\/(records|steps|cycles)$/.test(path)) return route.fulfill({ json: { code: 200, data: rows[{ records: 'REALTIME', steps: 'STEP', cycles: 'CYCLE' }[path.split('/').at(-1)]] } })
|
if (path.endsWith('/system/test/1')) return route.fulfill({ json: { code: 200, data: detail } })
|
if (path.endsWith('/live-state')) return route.fulfill({ json: { code: 200, data: { ...detail, sources: [], exceptions: [] } } })
|
if (path.endsWith('/system/batteryGroup/1')) return route.fulfill({ json: { code: 200, data: { groupId: 1, groupName: '演示电池组', f: '2', batteries: [] } } })
|
if (path.endsWith('/system/data-revision/1')) return route.fulfill({ json: { code: 200, data: { actionId: 1, testId: 1, testName: detail.testName, dataType: 'REALTIME', operationType: 'EDIT', scopeSummary: '修改电流', identitySummary: '["REALTIME:0"]', changesSummary: JSON.stringify([{ identity: 'REALTIME:0', before: { current: -8.1651, capacity: null }, after: { current: '-8', capacity: '0' } }]), reason: '单位验证', operatorName: 'admin', createdAtUtc: 1683521083000 } } })
|
if (path.endsWith('/getInfo')) return route.fulfill({ json: { code: 200, user: { userName: 'admin' }, roles: ['admin'] } })
|
if (path.includes('/system/')) return route.fulfill({ json: { code: 200, rows: [], data: [] } })
|
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/data/import?dataType=${type}`)
|
await page.locator('input[type=file]').setInputFiles(workbookPath)
|
const field = type === 'REALTIME' ? '电流(mA)' : type === 'STEP' ? '容量(mAh)' : '放电容量(mAh)'
|
const expected = type === 'REALTIME' ? -8165.1 : 8512.2
|
const previewHeaders = await page.locator('.preview-table-wrap th').allTextContents()
|
assert.equal(Number(await page.locator('.preview-table-wrap tbody tr').first().locator('td').nth(previewHeaders.indexOf(field)).textContent()), expected)
|
await page.goto(`${origin}/test-list/1/console?tab=data-list&dataType=${type}`)
|
await expect(page.locator('.table-wrap tbody')).toContainText('BAT-1')
|
await expect(page.locator('.table-wrap thead')).toContainText(field)
|
const downloadEvent = page.waitForEvent('download')
|
await page.getByRole('button', { name: '下载', exact: true }).click()
|
const csv = XLSX.read(await readFile(await (await downloadEvent).path()), { type: 'buffer' })
|
const exported = XLSX.utils.sheet_to_json(csv.Sheets[csv.SheetNames[0]])
|
assert.ok(exported.some(row => Number(row[field]) === expected))
|
await page.screenshot({ path: `${output}/${type}-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}/${type}-390.png`, fullPage: true })
|
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false, JSON.stringify(await page.evaluate(() => [...document.querySelectorAll('body *')].filter(el => el.getBoundingClientRect().width > innerWidth && getComputedStyle(el).position !== 'absolute').slice(0, 12).map(el => [el.className, el.getBoundingClientRect().width]))))
|
await page.setViewportSize({ width: 1440, height: 1000 })
|
await page.locator('.table-wrap tbody tr').first().getByRole('button', { name: '编辑', exact: true }).click()
|
const label = type === 'REALTIME' ? '电流(mA)' : type === 'STEP' ? '容量(mAh)' : '放电容量(mAh)'
|
await page.locator('.single-form-grid label').filter({ hasText: label }).locator('input').fill(type === 'REALTIME' ? '-8000' : '9000')
|
await page.locator('.remark-field textarea').fill('毫单位编辑验证')
|
await page.getByRole('button', { name: '提交修改', exact: true }).click()
|
await expect(page.locator('.el-dialog')).toBeHidden()
|
const key = type === 'REALTIME' ? 'current' : type === 'STEP' ? 'capacity' : 'dischargeCapacity'
|
assert.deepEqual(JSON.parse(calls.at(-1).overrides[0].fieldValues), { [key]: type === 'REALTIME' ? '-8' : '9' })
|
assert.equal(JSON.parse(calls.at(-1).changesSummary)[0].after[key], type === 'REALTIME' ? '-8' : '9')
|
await page.reload()
|
await expect(page.locator('.table-wrap tbody')).toContainText(type === 'REALTIME' ? '-8000.0000' : '9000.0000')
|
await page.getByRole('button', { name: '批量修改', exact: true }).click()
|
const headers = await page.locator('.table-wrap th').allTextContents()
|
const index = headers.indexOf(field)
|
for (const [i, row] of (await page.locator('.table-wrap tbody tr').all()).entries()) await row.locator('td').nth(index).locator('input').fill(i ? '-' : '0')
|
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.getByRole('button', { name: '批量修改', exact: true })).toBeVisible()
|
assert.deepEqual(calls.at(-1).overrides.map(override => JSON.parse(override.fieldValues)), [{ [key]: '0' }, { [key]: '-' }])
|
}
|
// 图表使用原始导入值,避免被上面的编辑场景改变对照基线。
|
for (const sheet of sheets) rows[sheet.type] = sheet.rows.slice(0, 8).map((row, i) => ({ ...row.data, sourceType: 'SNAPSHOT', stableIdentity: `${sheet.type}:${i}`, deviceTime: row.data.recordedAt }))
|
for (const tab of ['step-chart', 'project-chart']) {
|
await page.goto(`${origin}/test-list/1/console?tab=${tab}`)
|
await expect(page.locator('canvas').first()).toBeVisible()
|
for (const width of [1440, 390]) {
|
await page.setViewportSize({ width, height: width === 1440 ? 1000 : 844 })
|
await expect(page.locator('.app-main')).toHaveCSS('margin-left', width === 390 ? '0px' : '248px')
|
await expect.poll(() => page.locator('canvas').first().evaluate(canvas => canvas.getBoundingClientRect().width)).toBeGreaterThan(250)
|
await page.screenshot({ path: `${output}/${tab}-${width}.png`, fullPage: true, animations: 'disabled' })
|
const nonblank = await page.locator('canvas').evaluateAll(canvases => canvases.some(canvas => {
|
const pixels = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height).data
|
return pixels.some((value, index) => index % 4 === 3 && value !== 0)
|
}))
|
assert.ok(nonblank, `${tab} 必须实际绘图`)
|
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false)
|
}
|
}
|
await page.goto(`${origin}/data-revision/1`)
|
await expect(page.locator('.change-list')).toContainText('电流(mA)')
|
await expect(page.locator('.change-list')).toContainText('-8165.1000')
|
await expect(page.locator('.change-list')).toContainText('-8000.0000')
|
await expect(page.locator('.change-list')).toContainText('容量(mAh)')
|
await expect(page.locator('.change-list')).toContainText('0.0000')
|
assert.deepEqual(errors, [])
|
console.log('甲方三表预览、导出、单条和批量编辑、修订详情、刷新、桌面/窄屏图表及非空画布通过')
|
} finally {
|
await browser.close()
|
}
|