yeg
2026-09-08 4b6fbf192b8e3c97ba279c96a085b98d734b8afd
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
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)
})