import { expect, test, type Page } from '@playwright/test'
|
import { Ticket04Stack } from './support/ticket04-stack'
|
|
const stack = new Ticket04Stack()
|
const selection = (testBatteryId: number) => ({ testId: testBatteryId < 9703 ? 9701 : 9702, testBatteryId })
|
const fields = [{ name: '备注', category: 'special', displayName: '备注', unit: '' }]
|
test.use({ actionTimeout: 20_000 })
|
test.beforeAll(async () => {
|
await stack.start()
|
stack.executeIsolatedSql(`
|
INSERT INTO t_battery_group(group_id,group_name,model) VALUES(9701,'甲组','型号'),(9702,'乙组','型号');
|
INSERT INTO t_test_type(test_type_id,test_type_name) VALUES(9701,'容量');
|
INSERT INTO t_test(test_id,test_name,creator_id,principal_id,test_type_id,test_purpose,test_status,reception_status,started_at_utc)
|
VALUES(9701,'测试甲',1,1,9701,'隔离验收','COMPLETED','ENDED','2026-09-01 00:00:00'),(9702,'测试乙',1,1,9701,'隔离验收','COMPLETED','ENDED','2026-09-02 00:00:00');
|
INSERT INTO t_test_battery(test_battery_id,test_id,group_id,battery_no,collector_id)
|
VALUES(9701,9701,9701,'1号',9701),(9702,9701,9701,'2号',9702),(9703,9702,9702,'1号',9703),(9704,9702,9702,'空段',9704),(9705,9702,9702,'长段',9705);
|
`)
|
for (const [id, cycles] of [[9701, [1,3]], [9702,[2,4]], [9703,[1]], [9705, Array.from({length:45},(_,index) => index+1)]] as const) {
|
for (const no of cycles) stack.executeIsolatedSql(`
|
INSERT INTO t_test_cycle_record(test_id,test_battery_id,binding_id,adapter_code,source_identity,source_record_id,dedupe_key,adapter_timezone_id,device_time_utc,device_time_raw,server_received_at_utc,cycle_no,charge_energy_wh,discharge_energy_wh,content_sha256)
|
VALUES(${selection(id).testId},${id},${id},'splice','splice:${id}','${no}','${no}','Asia/Hong_Kong','2026-09-01 00:00:00','2026-09-01 08:00:00','2026-09-01 00:00:01',${no},${no === 3 ? 0 : 1},1.2,UNHEX(SHA2('${id}:${no}',256)));
|
INSERT INTO t_test_step_record(test_id,test_battery_id,binding_id,adapter_code,source_identity,source_record_id,dedupe_key,adapter_timezone_id,device_time_utc,device_time_raw,server_received_at_utc,cycle_no,step_no,voltage_v,content_sha256)
|
VALUES(${selection(id).testId},${id},${id},'splice','splice:${id}','${no}','${no}','Asia/Hong_Kong','2026-09-01 00:00:00','2026-09-01 08:00:00','2026-09-01 00:00:01',${no},1,${no === 3 ? 'NULL' : no === 1 ? 0 : -0.2},UNHEX(SHA2('${id}:${no}',256)));
|
`)
|
}
|
})
|
test.afterAll(async () => stack.stop())
|
async function login(page: Page) {
|
await page.goto('/login')
|
await page.locator('input[autocomplete="username"]').fill('admin')
|
await page.locator('input[autocomplete="current-password"]').fill('admin123')
|
await page.getByRole('button',{name:'登录',exact:true}).click()
|
await expect(page).toHaveURL(/dashboard$/)
|
}
|
async function api(page: Page, path: string, method = 'GET', data?: unknown, code = 200) {
|
const token = await page.evaluate(() => localStorage.getItem('battery_detection_auth_token'))
|
const result = await (await page.request.fetch(`${stack.backendUrl}/system/${path}`,{method,data,headers:{Authorization:`Bearer ${token}`}})).json()
|
expect(result.code, `${path}: ${result.msg}`).toBe(code)
|
return result.data
|
}
|
test('逐电池身份、接续、来源详情、界面重排及保存打印闭环',async ({page}) => {
|
await login(page)
|
await api(page,'test/9701/console-detail','PUT',{experimentName:'实验甲',batteryFields:['人工字段无共享身份'],specialFields:['备注'],fieldValues:{备注:{'1号':'甲一','2号':''},人工字段无共享身份:{'1号':'原样单位未知','2号':'另一值'}}})
|
await api(page,'test/9702/console-detail','PUT',{experimentName:'实验乙',batteryFields:[],specialFields:['备注'],fieldValues:{备注:{'1号':'乙一','空段':'空来源','长段':'长来源'}}})
|
const selections = [9701,9702,9703].map(selection)
|
const preview = await api(page,'splice-report/preview','POST',selections)
|
expect(preview.segments.map((s:any) => s.originalCycleNos.map((no:number) => no+s.offset))).toEqual([[1,3],[4,6],[7]])
|
expect(preview.segments.map((s:any) => s.consoleDetail.fieldValues.备注)).toEqual(['甲一','','乙一'])
|
expect(preview.segments[0].statistics['1']).toMatchObject({finalVoltage1:0,energyEfficiency:120})
|
expect(preview.segments[0].statistics['3'].finalVoltage1 ?? null).toBeNull()
|
expect(preview.segments[0].statistics['3'].energyEfficiency ?? null).toBeNull()
|
for (const charge of ['NULL','0','-1']) {
|
stack.executeIsolatedSql(`UPDATE t_test_cycle_record SET charge_energy_wh=${charge} WHERE test_battery_id=9703`)
|
expect((await api(page,'splice-report/preview','POST',selections)).segments[2].statistics['1'].energyEfficiency ?? null).toBeNull()
|
}
|
stack.executeIsolatedSql('UPDATE t_test_cycle_record SET charge_energy_wh=1 WHERE test_battery_id=9703')
|
stack.executeIsolatedSql("UPDATE t_test SET test_status='WAITING' WHERE test_id=9702")
|
await api(page,'splice-report/preview','POST',selections,500)
|
stack.executeIsolatedSql("UPDATE t_test SET test_status='COMPLETED' WHERE test_id=9702")
|
await api(page,'splice-report/preview','POST',[selection(9701),selection(9701)],500)
|
await api(page,'splice-report/preview','POST',[selection(9701),{testId:9701,testBatteryId:9703}],500)
|
const sameTest = await api(page,'splice-report','POST',{name:'同测试两段',selections:selections.slice(0,2),settings:{fieldStructure:fields}})
|
expect(sameTest.segments).toHaveLength(2)
|
const reordered = await api(page,'splice-report/preview','POST',[selection(9703),selection(9704),selection(9701)])
|
expect(reordered.segments.map((s:any) => s.originalCycleNos.map((no:number) => no+s.offset))).toEqual([[1],[],[2,4]])
|
await page.goto('/report-splice/new')
|
await page.getByPlaceholder('请输入拼接报表名称').fill('逐电池拼接界面')
|
for (const label of ['测试甲(甲组,1号)','测试甲(甲组,2号)','测试乙(乙组,1号)']) {
|
await page.locator('.panel-actions').getByRole('button',{name:'添加拼接',exact:true}).click()
|
await page.getByRole('dialog').locator('.el-select').last().click()
|
await page.getByRole('option',{name:label,exact:true}).click()
|
await page.getByRole('dialog').getByRole('button',{name:'添加拼接',exact:true}).click()
|
}
|
await expect(page.locator('.splice-chart')).toHaveCount(2)
|
await expect(page.locator('.splice-snapshot h1')).toHaveCount(0)
|
const detailRow = page.locator('.detail-table tr').filter({has:page.locator('td').filter({hasText:/^备注$/})})
|
await expect(detailRow.locator('input').nth(0)).toHaveValue('甲一')
|
await expect(detailRow.locator('input').nth(1)).toHaveValue('')
|
await detailRow.locator('input').nth(2).fill('乙一报表')
|
await page.getByLabel('测试乙 1号 循环1 最终电压1').fill('8')
|
await page.getByRole('button',{name:'上移测试',exact:true}).nth(2).click()
|
await expect(detailRow.locator('input').nth(1)).toHaveValue('乙一报表')
|
await expect(page.getByLabel('测试乙 1号 循环1 最终电压1')).toHaveValue('8')
|
await page.locator('.test-table .delete-link').last().click()
|
await expect(detailRow.locator('input')).toHaveCount(2)
|
await expect(detailRow.locator('input').nth(1)).toHaveValue('乙一报表')
|
await page.locator('.panel-actions').getByRole('button',{name:'添加拼接',exact:true}).click()
|
await page.getByRole('dialog').locator('.el-select').last().click()
|
await page.getByRole('option',{name:'测试甲(甲组,2号)',exact:true}).click()
|
await page.getByRole('dialog').getByRole('button',{name:'添加拼接',exact:true}).click()
|
await expect(page.getByLabel('测试乙 1号 循环1 最终电压1')).toHaveValue('8')
|
await page.getByRole('button',{name:'接续曲线',exact:true}).click()
|
await page.getByRole('button',{name:'保存报表',exact:true}).click()
|
await expect(page).toHaveURL(/report-splice$/)
|
const token = await page.evaluate(() => localStorage.getItem('battery_detection_auth_token'))
|
const list = await (await page.request.get(`${stack.backendUrl}/system/splice-report/list`,{headers:{Authorization:`Bearer ${token}`}})).json()
|
const saved = await api(page,`splice-report/${list.rows.find((r:any) => r.name === '逐电池拼接界面').id}`)
|
expect(saved.selections).toEqual([selection(9701),selection(9703),selection(9702)])
|
expect(saved.settings.hiddenSeries).toBe(true)
|
expect(saved.settings.detailFieldValues.备注['9702|9703']).toBe('乙一报表')
|
expect(saved.settings.statisticValues['9702|9703|1|finalVoltage1']).toBe('8')
|
expect(saved.segments[1].statistics['1'].finalVoltage1).toBe(0)
|
expect((await api(page,'test/9702')).consoleDetail.fieldValues.备注['1号']).toBe('乙一')
|
await page.goto(`/report-splice/${saved.id}/edit?mode=view`)
|
await page.reload()
|
await expect(page.locator('.splice-snapshot')).toContainText('乙一报表')
|
await page.context().addInitScript(() => {window.print=()=>{document.body.dataset.printRequested='true'}})
|
const popupPromise = page.waitForEvent('popup')
|
await page.getByRole('button',{name:'打印 / 另存 PDF',exact:true}).click()
|
const popup = await popupPromise
|
await expect(popup.locator('body')).toHaveAttribute('data-print-requested','true')
|
expect(await popup.locator('.splice-snapshot').innerText()).toEqual(await page.locator('.splice-snapshot').innerText())
|
await popup.close()
|
const long = await api(page,'splice-report','POST',{name:'完整长线',selections:[9701,9702,9703,9704,9705].map(selection),settings:{fieldStructure:fields,seriesColors:['#123456']}})
|
await page.goto(`/report-splice/${long.id}/edit?mode=view`)
|
await expect(page.locator('.splice-chart')).toHaveCount(2)
|
await expect(page.locator('.splice-chart svg path[stroke="#123456"]').first()).toBeAttached()
|
await expect(page.locator('.splice-statistics')).toHaveCount(5)
|
for (const width of [1440,390]) {
|
await page.setViewportSize({width,height:900})
|
await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
|
await page.screenshot({path:`tests/e2e/.artifacts/splice-segments-${width}.png`,fullPage:true})
|
}
|
const longPopup = page.waitForEvent('popup')
|
await page.getByRole('button',{name:'打印 / 另存 PDF',exact:true}).click()
|
const printed = await longPopup
|
await expect(printed.locator('.splice-statistics')).toHaveCount(5)
|
await printed.pdf({path:'tests/e2e/.artifacts/splice-segments-long.pdf',preferCSSPageSize:true,printBackground:true})
|
await printed.close()
|
await api(page,'test/9701','DELETE')
|
await api(page,'test/9702','DELETE')
|
expect(await api(page,`splice-report/${long.id}/versions/1/print`)).toEqual(long)
|
await api(page,`splice-report/${long.id}/refresh-source`,'POST',{baseVersionNo:1,changeSummary:'来源已删'},500)
|
})
|