yeg
2026-09-08 c334ea50112a9b4bbb314cd2eb68ee367f6173f0
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
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
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_battery_group_field(field_id,field_name,field_type) VALUES(9701,'质量','number'),(9702,'另一质量','number');
    INSERT INTO t_battery_group_relation_field(group_id,battery_group_field_id,field_value) VALUES(9701,9701,'12.5'),(9702,9701,'28'),(9701,9702,'12500'),(9702,9702,'28000');
    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 }) => {
  const errors: string[] = []
  page.on('pageerror', error => errors.push(error.message))
  await login(page)
  const detailA = { experimentName: '原实验', batteryFields: ['质量', '取消字段'], specialFields: ['备注'], fieldValues: { 质量: { '1号': '控制台未知单位', '2号': '' }, 取消字段: { '1号': '取消原值' }, 备注: { '1号': '来源备注', '2号': '' } } }
  await api(page, 'test/9701/console-detail', 'PUT', detailA)
  await api(page, 'test/9702/console-detail', 'PUT', { experimentName: '另一实验', batteryFields: ['质量'], specialFields: [], fieldValues: { 质量: { '1号': '跨组人工质量' } } })
  const templateFields = [
    { name: '质量', displayName: '质量', category: 'battery', unit: 'g', fieldDefinitionId: 9701 },
    { name: '另一质量', displayName: '质量', category: 'battery', unit: 'mg', fieldDefinitionId: 9702 },
    { name: '备注', displayName: '模板备注', category: 'special', unit: '', fieldDefinitionId: null },
  ]
  const templateId = await api(page, 'splice-report/templates', 'POST', { name: '同名不同身份纯结构', fields: templateFields })
  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()
  }
  const sourceQuality = page.getByRole('textbox', { name: '测试甲 1号 电池字段 质量', exact: true })
  await expect(sourceQuality).toHaveValue('控制台未知单位')
  await expect(page.getByRole('textbox', { name: '测试甲 2号 电池字段 质量', exact: true })).toHaveValue('')
  await sourceQuality.fill('报表来源覆盖')
  await page.getByRole('textbox', { name: '测试甲 1号 特殊字段 备注', exact: true }).fill('')
  await page.getByRole('textbox', { name: '测试甲 1号 实验名称', exact: true }).fill('')
  const applyTemplate = async () => {
    await page.getByRole('button', { name: '选择字段模板', exact: true }).click()
    await page.getByRole('dialog').getByText('同名不同身份纯结构', { exact: true }).click()
    await page.getByRole('dialog').getByRole('button', { name: '应用模板', exact: true }).click()
  }
  await applyTemplate()
  const rowG = page.locator('.report-field-table tr').filter({ has: page.locator('td').filter({ hasText: /^质量 \(g\)$/ }) })
  const rowMg = page.locator('.report-field-table tr').filter({ has: page.locator('td').filter({ hasText: /^质量 \(mg\)$/ }) })
  await expect(rowG.locator('input').first()).toHaveValue('12.5')
  await expect(rowMg.locator('input').first()).toHaveValue('12500')
  await expect(sourceQuality).toHaveValue('报表来源覆盖')
  await expect(page.getByRole('textbox', { name: '测试甲 1号 报表字段 模板备注', exact: true })).toHaveValue('')
  await rowG.locator('input').first().fill('13.7')
  await page.getByRole('textbox', { name: '测试甲 1号 报表字段 模板备注', exact: true }).fill('模板人工值')
  await applyTemplate()
  await expect(rowG.locator('input').first()).toHaveValue('13.7')
  await expect(rowMg.locator('input').first()).toHaveValue('12500')
  await expect(sourceQuality).toHaveValue('报表来源覆盖')
  // 模板保存只取报表结构,来源实验、控制台字段及两类人工值都不能夹带。
  await page.getByRole('button', { name: '选择字段模板', exact: true }).click()
  await page.getByRole('dialog').getByRole('button', { name: '保存为模板', exact: true }).click()
  const templateDialog = page.getByRole('dialog', { name: '保存拼接字段模板', exact: true })
  await templateDialog.getByRole('textbox').fill('边界另存结构')
  await templateDialog.getByRole('button', { name: '保存', exact: true }).click()
  await expect(templateDialog).toBeHidden()
  await page.getByRole('dialog', { name: '选择拼接字段模板', exact: true }).getByRole('button', { name: '取消', exact: true }).click()
  const template = (await api(page, 'splice-report/templates')).find((item: any) => item.name === '边界另存结构')
  expect(template.fields).toEqual(templateFields)
  expect(JSON.stringify(template)).not.toContain('报表来源覆盖')
  expect(JSON.stringify(template)).not.toContain('模板人工值')
  expect(JSON.stringify(template)).not.toContain('取消字段')
  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 id = list.rows.find((item: any) => item.name === '来源模板边界').id
  const first = await api(page, `splice-report/${id}`)
  expect(first.settings.fieldStructure).toHaveLength(3)
  expect(first.settings.sourceDetailOverrides['9701|9701']).toEqual({ experimentName: '', fieldValues: { 'battery|质量': '报表来源覆盖', 'special|备注': '' } })
  expect(first.settings.detailFieldValues.质量['9701|9701']).toBe('13.7')
  expect(first.segments[0].consoleDetail.fieldValues.质量).toBe('控制台未知单位')
  expect((await api(page, 'test/9701')).consoleDetail).toMatchObject(detailA)
  await page.goto(`/report-splice/${id}/edit`)
  await page.reload()
  await expect(sourceQuality).toHaveValue('报表来源覆盖')
  await expect(rowG.locator('input').first()).toHaveValue('13.7')
  await expect(rowMg.locator('input').first()).toHaveValue('12500')
  await page.getByRole('button', { name: '保存修改', exact: true }).click()
  await expect(page).toHaveURL(/report-splice$/)
  const second = await api(page, `splice-report/${id}`)
  expect(second.segments).toEqual(first.segments)
  // 更新新增/取消字段不能依赖模板列表;消失但人工改过的质量、备注及空实验继续保留。
  await api(page, 'test/9701/console-detail', 'PUT', { experimentName: '新实验', batteryFields: ['新增来源字段'], specialFields: [], fieldValues: { 新增来源字段: { '1号': '新增内容', '2号': '' } } })
  const refreshed = await api(page, `splice-report/${id}/refresh-source`, 'POST', { baseVersionNo: 2, changeSummary: '来源字段增减' })
  expect(refreshed.settings.fieldStructure).toEqual(first.settings.fieldStructure)
  expect(refreshed.settings.sourceDetailOverrides).toEqual(first.settings.sourceDetailOverrides)
  await page.goto(`/report-splice/${id}/edit`)
  await expect(page.getByRole('textbox', { name: '测试甲 1号 电池字段 新增来源字段', exact: true })).toHaveValue('新增内容')
  await expect(page.getByRole('textbox', { name: '测试甲 1号 电池字段 取消字段', exact: true })).toHaveCount(0)
  await expect(sourceQuality).toHaveValue('报表来源覆盖')
  await expect(page.getByRole('textbox', { name: '测试甲 1号 特殊字段 备注', exact: true })).toHaveValue('')
  await expect(page.getByRole('textbox', { name: '测试甲 1号 实验名称', exact: true })).toHaveValue('')
  await page.goto(`/report-splice/${id}/edit?mode=view`)
  const sourceSection = page.locator('.source-detail-section')
  await expect(sourceSection).toContainText('新增内容')
  await expect(sourceSection).toContainText('报表来源覆盖')
  await expect(sourceSection).not.toContainText('取消字段')
  await expect(sourceSection).not.toContainText('(g)')
  await expect(page.locator('.report-field-section')).toContainText('13.7')
  await expect(page.locator('.report-field-section')).toContainText('12500')
  await page.screenshot({ path: 'tests/e2e/.artifacts/splice-source-template-boundary.png', fullPage: true })
  await page.context().addInitScript(() => { window.print = () => { document.body.dataset.printRequested = 'true' } })
  const pending = page.waitForEvent('popup')
  await page.getByRole('button', { name: '打印 / 另存 PDF', exact: true }).click()
  const popup = await pending
  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.pdf({ path: 'tests/e2e/.artifacts/splice-source-template-boundary.pdf', preferCSSPageSize: true, printBackground: true })
  await popup.close()
  await page.goto(`/report-splice/${id}/edit?mode=view&version=1`)
  await expect(page.locator('.source-detail-section')).toContainText('取消原值')
  await expect(page.locator('.source-detail-section')).not.toContainText('新增内容')
  expect(await api(page, `splice-report/${id}/versions/1`)).toEqual(first)
  for (const sourceDetailOverrides of [{ '9701|9999': { experimentName: '错归属' } }, { '9701|9701': { fieldValues: { 'voltage': '不能冒充设备值' } } }])
    await api(page, `splice-report/${id}/versions`, 'POST', { name: first.name, baseVersionNo: 3, changeSummary: '拒绝无效详情', settings: { ...refreshed.settings, sourceDetailOverrides } }, 500)
  expect((await api(page, `splice-report/${id}`)).versionNo).toBe(3)
  // 隔离库复原旧07快照形状:普通读写不补来源行,明确更新才启用新详情,同时保留旧人工值。
  const legacy = await api(page, 'splice-report', 'POST', { name: '旧07快照', selections: [selection(9701), selection(9702)], settings: {
    fieldStructure: [{ name: '旧人工字段', displayName: '旧人工字段', category: 'special', unit: '' }],
    detailFieldValues: { 旧人工字段: { '9701|9701': '旧人工保留' } }, sourceExperimentNames: { '9701|9701': '旧实验保留', '9702|9703': '已移除来源残留实验' },
  } })
  stack.executeIsolatedSql(`UPDATE t_splice_report_version SET snapshot_json=JSON_REMOVE(snapshot_json,'$.settings.sourceDetailOverrides') WHERE report_id=${legacy.id}`)
  const old = await api(page, `splice-report/${legacy.id}`)
  expect(old.settings.sourceDetailOverrides).toBeUndefined()
  await page.goto(`/report-splice/${legacy.id}/edit`)
  await expect(page.locator('.source-detail-table')).toHaveCount(0)
  await expect(page.getByRole('textbox', { name: '测试甲 1号 报表字段 旧人工字段', exact: true })).toHaveValue('旧人工保留')
  await page.getByRole('button', { name: '保存修改', exact: true }).click()
  await expect(page).toHaveURL(/report-splice$/)
  const legacyEdited = await api(page, `splice-report/${legacy.id}`)
  expect(legacyEdited.settings.sourceDetailOverrides).toBeUndefined()
  expect(legacyEdited.segments).toEqual(old.segments)
  const legacyUpdated = await api(page, `splice-report/${legacy.id}/refresh-source`, 'POST', { baseVersionNo: 2, changeSummary: '旧版明确更新' })
  expect(legacyUpdated.settings.detailFieldValues).toEqual(old.settings.detailFieldValues)
  expect(legacyUpdated.settings.sourceDetailOverrides['9701|9701'].experimentName).toBe('旧实验保留')
  expect(legacyUpdated.settings.sourceDetailOverrides['9702|9703']).toBeUndefined()
  await page.goto(`/report-splice/${legacy.id}/edit?mode=view`)
  await expect(page.locator('.source-detail-section')).toContainText('旧实验保留')
  await expect(page.locator('.source-detail-section')).toContainText('新增内容')
  await expect(page.locator('.report-field-section')).toContainText('旧人工保留')
  expect(await api(page, `splice-report/${legacy.id}/versions/1`)).toEqual(old)
  expect((await api(page, `splice-report/${legacy.id}/versions/1`)).settings.sourceExperimentNames).toEqual({ '9701|9701': '旧实验保留', '9702|9703': '已移除来源残留实验' })
  expect(templateId).toBeGreaterThan(0)
  expect(errors).toEqual([])
})
 
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('.source-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.sourceDetailOverrides['9702|9703'].fieldValues['special|备注']).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)
})