| | |
| | | export interface CompareSettings { |
| | | metrics: CompareMetricKey[] |
| | | seriesColors: string[] |
| | | hiddenSeriesKeys?: string[] |
| | | batteryTemplateFields: string[] |
| | | specialFields: string[] |
| | | detailFieldValues: Record<string, Record<string, string>> |
| | |
| | | import type { CompareItem } from '@/api/compareReports' |
| | | import { compareSelectionKey, type CompareItem } from '@/api/compareReports' |
| | | import { measurementChartOption, type ReportMetricKey } from './reportMeasurementChart' |
| | | |
| | | /** 完整所选范围参与图例身份,避免同名电池的不同循环或工步混淆。 */ |
| | | export function compareSeriesName(item: CompareItem) { |
| | | const source = item.source |
| | | return `${source.name} · ${source.batteryNo} · 循环${source.selection.cycleNo} · ${source.process}` |
| | | return `${source.name} (#${source.selection.testId}) · ${source.batteryGroup} · ${source.batteryNo} · 循环${source.selection.cycleNo} · ${source.process}` |
| | | } |
| | | |
| | | /** 对比页面与打印共享冻结测量的绘图口径。 */ |
| | | export function compareChartOption(metric: { key: ReportMetricKey; unit: string }, items: CompareItem[], colors: string[]) { |
| | | return measurementChartOption(metric, items.map(item => ({ ...item, name: compareSeriesName(item) })), colors) |
| | | export function compareChartOption(metric: { key: ReportMetricKey; unit: string }, items: CompareItem[], colors: string[], hiddenSeriesKeys: string[] = []) { |
| | | return measurementChartOption(metric, items.map(item => ({ ...item, name: compareSeriesName(item), hidden: hiddenSeriesKeys.includes(compareSelectionKey(item.source.selection)) })), colors) |
| | | } |
| | |
| | | function chartSvg(metric: CompareMetricDefinition, record: SavedCompareReport) { |
| | | const chart = init(null, undefined, { renderer: 'svg', ssr: true, width: 900, height: 300 }) |
| | | try { |
| | | chart.setOption({ ...compareChartOption(metric, record.items, record.settings.seriesColors), tooltip: { show: false } }) |
| | | chart.setOption({ ...compareChartOption(metric, record.items, record.settings.seriesColors, record.settings.hiddenSeriesKeys), tooltip: { show: false } }) |
| | | return chart.renderToSVGString() |
| | | } finally { chart.dispose() } |
| | | } |
| | |
| | | const legend = record.items.map((item, index) => { |
| | | const color = record.settings.seriesColors[index % record.settings.seriesColors.length] |
| | | const safeColor = color && /^#[0-9a-f]{6}$/i.test(color) ? color : '#52606d' |
| | | return `<span><i style="background:${safeColor}"></i>${escapeHtml(compareSeriesName(item))}</span>` |
| | | const hidden = record.settings.hiddenSeriesKeys?.includes(compareSelectionKey(item.source.selection)) |
| | | return `<span${hidden ? ' style="text-decoration:line-through;color:#8795a3"' : ''}><i style="background:${safeColor}"></i>${escapeHtml(compareSeriesName(item))}</span>` |
| | | }).join('') |
| | | const charts = compareMetricDefinitions.filter(metric => record.settings.metrics.includes(metric.key)).map(metric => |
| | | `<article class="chart-card"><header><strong>${metric.title} (${escapeHtml(record.displayUnits[metric.key] || metric.unit)})</strong></header>${chartSvg(metric, record)}<div class="chart-inner-legend">${legend}</div></article>`).join('') |
| | |
| | | import { toTestDisplayValue } from './testDataUnits' |
| | | |
| | | export type ReportMetricKey = 'voltage' | 'current' | 'positiveVoltage' | 'negativeVoltage' | 'temperature' |
| | | export interface MeasurementSeries { name: string; zeroTime: number | null; records: PersistedTestRecord[] } |
| | | export interface MeasurementSeries { name: string; zeroTime: number | null; records: PersistedTestRecord[]; hidden?: boolean } |
| | | |
| | | /** 页面和打印共用测量、时间及单位口径;不连接缺失值,也不限制曲线条数。 */ |
| | | export function measurementChartOption(metric: { key: ReportMetricKey; unit: string }, items: MeasurementSeries[], colors: string[]) { |
| | |
| | | value === null ? [low, high] : [Math.min(low, value), Math.max(high, value)], [min, max]), [0, 0]) |
| | | const range = Math.max(dataMax - dataMin, 1) |
| | | const timeMax = series.reduce((max, points) => points.reduce((value, [time]) => Math.max(value, time), max), 0) |
| | | const timeUnit = timeMax >= 3600 ? 3600 : timeMax >= 180 ? 60 : 1 |
| | | const timeLabel = timeUnit === 3600 ? 'h' : timeUnit === 60 ? 'min' : 's' |
| | | const axisMax = Math.max(metric.unit === '℃' ? Math.ceil(dataMax / 10) * 10 : Math.ceil(dataMax * 10) / 10, 1) |
| | | const yAxis = (position: 'left' | 'right') => ({ |
| | | type: 'value', name: metric.unit, position, min: dataMin - range * 0.05, max: axisMax, |
| | |
| | | }) |
| | | return { |
| | | animation: false, color: colors, grid: { top: 40, right: 32, bottom: 42, left: 32, containLabel: true }, |
| | | // 使用 ECharts 原生图例承载曲线显隐;外部图例仍用于来源说明和改色。 |
| | | tooltip: { trigger: 'axis' }, legend: { show: true, type: 'scroll', top: 0, left: 32, right: 32, itemWidth: 12, itemHeight: 8, textStyle: { color: '#52606d', fontSize: 10 } }, |
| | | xAxis: { type: 'value', min: 0, max: timeMax > 0 ? timeMax : undefined, name: '相对时间 / s', nameLocation: 'middle', nameGap: 28, |
| | | axisLine: { lineStyle: { color: '#cbd8e4' } }, axisLabel: { color: '#8795a3', fontSize: 10 }, |
| | | tooltip: { trigger: 'axis' }, legend: { show: false }, |
| | | xAxis: { type: 'value', min: 0, max: timeMax > 0 ? timeMax : undefined, splitNumber: 4, name: `相对时间 / ${timeLabel}`, nameLocation: 'middle', nameGap: 28, |
| | | axisLine: { lineStyle: { color: '#cbd8e4' } }, axisLabel: { color: '#8795a3', fontSize: 10, hideOverlap: true, |
| | | formatter: (value: number) => Number((value / timeUnit).toFixed(3)).toString() }, |
| | | nameTextStyle: { color: '#26333f', fontSize: 11, fontWeight: 600 } }, |
| | | yAxis: [yAxis('left'), yAxis('right')], |
| | | series: items.map((item, index) => ({ name: item.name, type: 'line', smooth: false, |
| | | connectNulls: false, showSymbol: true, symbolSize: 5, lineStyle: { width: 2 }, data: series[index] || [] })), |
| | | // 全部冻结点先参与坐标计算;隐藏仅控制渲染,不重算零点、不缩短范围、不移动其他来源颜色。 |
| | | series: items.flatMap((item, index) => item.hidden ? [] : [{ name: item.name, type: 'line', smooth: false, |
| | | connectNulls: false, showSymbol: true, symbolSize: 5, itemStyle: { color: colors[index % colors.length] }, |
| | | lineStyle: { width: 2, color: colors[index % colors.length] }, data: series[index] || [] }]), |
| | | } |
| | | } |
| | |
| | | temperature: true, |
| | | }) |
| | | const seriesColors = ref(['#3978f6', '#7d42d8', '#f08a3e']) |
| | | const hiddenSeriesKeys = ref<string[]>([]) |
| | | |
| | | const visibleMetrics = computed(() => metricDefinitions.filter(metric => metricVisible[metric.key])) |
| | | const selectedTemplate = computed(() => persistedTemplates.value.find(item => `remote:${item.id}` === selectedTemplateKey.value)) |
| | |
| | | const key = detailKey(item) |
| | | // 未填写才读取当前预览或保存快照的业务值,显式清空不被后续预览覆盖。 |
| | | if (detailFieldValues[field][key] === undefined) { |
| | | const source = sourceItems.value.find(value => detailKey(toSelectedTest(value.source)) === key) |
| | | detailFieldValues[field][key] = compareBusinessFieldValue(fieldDescriptor(field), source) |
| | | const source = sourceItems.value.find(value => compareSelectionKey(value.source.selection) === key) |
| | | const sourceDetail = source?.source.consoleDetail |
| | | const sourceValue = sourceDetail?.fieldValues?.[field]?.[item.batteryNo] |
| | | detailFieldValues[field][key] = sourceValue !== undefined |
| | | ? sourceValue |
| | | : compareBusinessFieldValue(fieldDescriptor(field), source) |
| | | } |
| | | }) |
| | | } |
| | |
| | | selectedTests.value = record.items.map(item => toSelectedTest(item.source)) |
| | | metricDefinitions.forEach(metric => { metricVisible[metric.key] = record.settings.metrics.includes(metric.key) }) |
| | | seriesColors.value = [...record.settings.seriesColors] |
| | | hiddenSeriesKeys.value = [...(record.settings.hiddenSeriesKeys || [])] |
| | | batteryFields.value = [...record.settings.batteryTemplateFields] |
| | | specialFields.value = [...record.settings.specialFields] |
| | | fieldStructure.value = (record.settings.fieldStructure || []).map(field => ({ ...field })) |
| | |
| | | |
| | | function updateSeriesColor(index: number, color: string | null) { |
| | | if (color) seriesColors.value[index] = color |
| | | } |
| | | |
| | | function toggleSeries(item: SelectedTest) { |
| | | if (readonly.value) return |
| | | const key = detailKey(item) |
| | | hiddenSeriesKeys.value = hiddenSeriesKeys.value.includes(key) |
| | | ? hiddenSeriesKeys.value.filter(value => value !== key) : [...hiddenSeriesKeys.value, key] |
| | | } |
| | | |
| | | detailFields.value.forEach(ensureDetailFieldValues) |
| | |
| | | selectedTemplateKey.value = '' |
| | | selectedBatteryTemplateName.value = '自定义' |
| | | seriesColors.value = ['#3978f6', '#7d42d8', '#f08a3e'] |
| | | hiddenSeriesKeys.value = [] |
| | | metricDefinitions.forEach(metric => { metricVisible[metric.key] = true }) |
| | | Object.keys(detailFieldValues).forEach(field => delete detailFieldValues[field]) |
| | | loadError.value = '' |
| | |
| | | return |
| | | } |
| | | const index = selectedTests.value.indexOf(itemToRemove) |
| | | hiddenSeriesKeys.value = hiddenSeriesKeys.value.filter(key => key !== detailKey(itemToRemove)) |
| | | if (index >= 0) { selectedTests.value.splice(index, 1); seriesColors.value.splice(index, 1) } |
| | | await refreshPreview() |
| | | } |
| | |
| | | name: reportName.value, description: description.value, experimentName: experimentName.value, |
| | | settings: { |
| | | metrics: visibleMetrics.value.map(item => item.key), seriesColors: seriesColors.value.slice(0, selectedTests.value.length), |
| | | hiddenSeriesKeys: [...hiddenSeriesKeys.value], |
| | | batteryTemplateFields: [...batteryFields.value], specialFields: [...specialFields.value], |
| | | fieldStructure: detailFields.value.map(fieldDescriptor), |
| | | ...(sourceDetailOverrides.value === undefined ? {} : { sourceDetailOverrides: Object.fromEntries(Object.entries(sourceDetailOverrides.value) |
| | |
| | | |
| | | <section class="metrics-panel"> |
| | | <div v-if="!readonly" class="metric-switches"><label v-for="metric in metricDefinitions" :key="metric.key"><span>{{ metric.title }}</span><el-switch v-model="metricVisible[metric.key]" inline-prompt active-text="显" inactive-text="隐" /></label></div> |
| | | <div v-if="chartSeries.length" class="chart-legend-bar"><strong>对比图例</strong><div v-for="(item, index) in chartSeries" :key="compareSelectionKey(item.selection)" class="chart-legend-item"><el-color-picker v-if="!readonly" class="legend-color-picker" :model-value="seriesColors[index]" :show-alpha="false" popper-class="legend-color-popper" :aria-label="`修改${item.name}颜色`" @change="updateSeriesColor(index, $event)" /><i v-else class="series-swatch" :style="{ background: seriesColors[index] }" /><span>{{ item.name }} · {{ item.batteryNo }} · 循环{{ item.cycle }} · {{ item.process }}</span></div></div> |
| | | <div v-if="chartSeries.length" class="chart-legend-bar"><strong>对比图例</strong><div v-for="(item, index) in chartSeries" :key="detailKey(item)" class="chart-legend-item"><el-color-picker v-if="!readonly" class="legend-color-picker" :model-value="seriesColors[index]" :show-alpha="false" popper-class="legend-color-popper" :aria-label="`修改${item.name}颜色`" @change="updateSeriesColor(index, $event)" /><i v-else class="series-swatch" :style="{ background: seriesColors[index] }" /><button type="button" class="legend-name" :class="{ 'is-hidden': hiddenSeriesKeys.includes(detailKey(item)) }" :aria-pressed="!hiddenSeriesKeys.includes(detailKey(item))" :disabled="readonly" @click="toggleSeries(item)">{{ item.name }} (#{{ item.id }}) · {{ item.batteryGroup }} · {{ item.batteryNo }} · 循环{{ item.cycle }} · {{ item.process }}</button></div></div> |
| | | <!-- 报表切换和比对项移除时完整替换配置,避免图表库将颜色数组缩短误判为组件合并。 --> |
| | | <div class="charts-grid"><article v-for="metric in visibleMetrics" :key="metric.key" class="chart-card"><header><strong>{{ metric.title }}</strong></header><VChart class="metric-chart" :option="compareChartOption(metric, sourceItems, seriesColors)" :update-options="{ notMerge: true }" autoresize /></article></div> |
| | | <div v-if="!visibleMetrics.length" class="chart-empty">当前未启用曲线指标</div> |
| | | <div class="charts-grid"><article v-for="metric in visibleMetrics" :key="metric.key" class="chart-card"><header><strong>{{ metric.title }}</strong></header><VChart class="metric-chart" :option="compareChartOption(metric, sourceItems, seriesColors, hiddenSeriesKeys)" :update-options="{ notMerge: true }" autoresize /><div v-if="!sourceItems.some(item => !hiddenSeriesKeys.includes(compareSelectionKey(item.source.selection)) && item.records.some(row => (metric.key === 'voltage' || metric.key === 'current' ? row.deviceTime : row.collectorDeviceTime) != null && (metric.key === 'positiveVoltage' ? row.positivePotential : metric.key === 'negativeVoltage' ? row.negativePotential : row[metric.key]) != null))" class="chart-empty">{{ sourceItems.length && hiddenSeriesKeys.length === sourceItems.length ? '当前全部曲线已隐藏' : '所选范围暂无该指标数据' }}</div></article></div> |
| | | </section> |
| | | |
| | | <footer class="form-actions"> |
| | |
| | | .chart-legend-bar > strong { color: #26333f; font-size: 12px; font-weight: 600; } |
| | | .chart-legend-item { display: inline-flex; align-items: center; gap: 7px; min-width: 0; color: #52606d; font-size: 12px; } |
| | | .chart-legend-item span { max-width: 360px; overflow-wrap: anywhere; } |
| | | .legend-name { max-width: 100%; border: 0; padding: 4px 0; background: transparent; color: inherit; font: inherit; text-align: left; overflow-wrap: anywhere; cursor: pointer; } |
| | | .legend-name.is-hidden { color: #8795a3; text-decoration: line-through; } |
| | | .legend-name:disabled { cursor: default; } |
| | | .chart-empty { padding: 12px; color: #8795a3; font-size: 12px; text-align: center; } |
| | | .legend-color-picker { width: 18px; height: 18px; display: inline-flex; align-items: center; justify-content: center; } |
| | | .legend-color-picker :deep(.el-color-picker__trigger) { width: 14px !important; height: 14px !important; padding: 0 !important; border: 0 !important; border-radius: 2px; background: transparent !important; box-shadow: none !important; overflow: hidden; } |
| | | .legend-color-picker :deep(.el-color-picker__color) { width: 14px; height: 14px; border: 0; border-radius: 2px; box-shadow: 0 0 0 1px rgba(36, 54, 71, .18); } |
| New file |
| | |
| | | import { expect, test, type Page } from '@playwright/test' |
| | | import { Ticket04Stack } from './support/ticket04-stack' |
| | | |
| | | const stack = new Ticket04Stack() |
| | | test.use({ actionTimeout: 20_000 }) |
| | | test.beforeAll(async () => stack.start()) |
| | | test.afterAll(async () => stack.stop()) |
| | | |
| | | 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 response = await page.request.fetch(`${stack.backendUrl}/system/${path}`, { method, data, headers: { Authorization: `Bearer ${token}` } }) |
| | | const body = await response.json() |
| | | expect(body.code, `${path}: ${body.msg}`).toBe(code) |
| | | return body.data |
| | | } |
| | | |
| | | function seedSources() { |
| | | stack.executeIsolatedSql(` |
| | | INSERT INTO t_test_type(test_type_id,test_type_name) VALUES(9401,'图表验收'); |
| | | ${[9401, 9402].map(id => ` |
| | | INSERT INTO t_battery_group(group_id,group_name,model) VALUES(${id},'图表组${id}','型号'); |
| | | INSERT INTO t_test(test_id,test_name,creator_id,principal_id,test_type_id,test_purpose,test_status,reception_status) |
| | | VALUES(${id},'图表测试${id}',1,1,9401,'隔离验收','COMPLETED','ENDED'); |
| | | INSERT INTO t_test_battery(test_battery_id,test_id,group_id,battery_no,collector_id) VALUES(${id},${id},${id},'BAT-1',${id}); |
| | | INSERT INTO t_detection_cabinet(cabinet_id,adapter_code,cabinet_code,cabinet_name,vendor_device_type,vendor_device_id) |
| | | VALUES(${id},'chart-${id}','chart-${id}','验收柜',27,${id}); |
| | | INSERT INTO t_detection_channel(channel_id,adapter_code,cabinet_id,devtype,devid,subdevid,chlid) VALUES(${id},'chart-${id}',${id},27,${id},1,1); |
| | | INSERT INTO t_test_channel_binding(binding_id,test_id,test_battery_id,channel_id) VALUES(${id},${id},${id},${id}); |
| | | INSERT INTO t_test_realtime_record(test_id,test_battery_id,binding_id,adapter_code,source_identity,source_record_id,cabinet_devtype,cabinet_devid,cabinet_subdevid,cabinet_chlid,cabinet_sequence_id,cabinet_device_time_utc,collector_device_time_utc,server_received_at_utc,cycle_no,step_no,step_name,voltage_v,current_a,temperature,positive_potential,negative_potential,content_sha256) |
| | | VALUES ${[1, 2].flatMap(step => Array.from({ length: step === 1 ? 65 : 3 }, (_, point) => { |
| | | const seq = step * 100 + point |
| | | const offset = step === 1 ? point * 113 + (point % 3) : point * 7 |
| | | const base = Date.parse(`2026-09-01T0${id - 9400}:00:00.123Z`) + step * 10_000_000 |
| | | const date = (time: number) => `'${new Date(time).toISOString().replace('T', ' ').replace('Z', '')}'` |
| | | return `(${id},${id},${id},'chart-${id}','chart-${id}:27:${id}:1:1','${seq}',27,${id},1,1,${seq},${date(base + offset * 1000 + 10000)},${date(base + offset * 1000)},${date(base + offset * 1000 + 11000)},1,${step},'${step === 1 ? '充电' : '放电'}',${3 + point / 100},${point === 1 ? 'NULL' : point === 0 ? '0' : '-0.125'},${point === 1 ? 'NULL' : point === 0 ? '0' : '25'},${step === 2 ? 'NULL' : '0.1'},-0.2,UNHEX(SHA2('${seq}',256)))` |
| | | })).join(',')}; |
| | | `).join('\n')} |
| | | `) |
| | | } |
| | | |
| | | test('对比图例按来源冻结,完整测量时间与双轴在重开和指定版打印中重现', async ({ page }) => { |
| | | seedSources() |
| | | const pageErrors: string[] = [] |
| | | page.on('pageerror', error => pageErrors.push(error.message)) |
| | | 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$/) |
| | | await page.goto('/report-compare/new') |
| | | await page.getByPlaceholder('请输入报表名称').fill('图表交互冻结验收') |
| | | for (const [id, step] of [[9401, 1], [9401, 2], [9402, 1]]) { |
| | | await page.getByRole('button', { name: '添加比较', exact: true }).click() |
| | | const dialog = page.getByRole('dialog', { name: '添加对比' }) |
| | | for (const [label, value] of [['选择测试', `图表测试${id}(${id})`], ['选择循环', '1'], ['选择工步', `${step}-${step === 1 ? '充电' : '放电'}`]]) { |
| | | const control = dialog.locator('label').filter({ hasText: label }).locator('.el-select') |
| | | await control.locator('.el-select__wrapper').click() |
| | | const listId = await control.getByRole('combobox').getAttribute('aria-controls') |
| | | await page.locator(`#${listId}`).getByRole('option', { name: value, exact: true }).click() |
| | | } |
| | | await dialog.getByRole('button', { name: '添加', exact: true }).click() |
| | | await expect(page.getByRole('button', { name: '添加比较', exact: true })).toBeEnabled() |
| | | } |
| | | const legends = page.locator('.legend-name') |
| | | await expect(legends).toHaveCount(3) |
| | | await legends.nth(0).click() |
| | | await expect(legends.nth(0)).toHaveAttribute('aria-pressed', 'false') |
| | | await expect(legends.nth(1)).toHaveAttribute('aria-pressed', 'true') |
| | | await expect(legends.nth(2)).toHaveAttribute('aria-pressed', 'true') |
| | | await legends.nth(1).click() |
| | | await legends.nth(2).click() |
| | | await expect(page.getByText('当前全部曲线已隐藏', { exact: true })).toHaveCount(5) |
| | | await legends.nth(1).click() |
| | | await legends.nth(2).click() |
| | | await page.locator('.chart-legend-item').nth(2).locator('.el-color-picker').click() |
| | | const panel = page.locator('.el-color-picker__panel:visible') |
| | | await panel.locator('input').fill('#12AB34') |
| | | await panel.getByRole('button', { name: /确定|确认|OK/ }).click() |
| | | await expect(legends.nth(2)).toHaveAttribute('aria-pressed', 'true') |
| | | await page.locator('.metric-switches label').filter({ hasText: '温度' }).locator('.el-switch__core').click() |
| | | const created = page.waitForResponse(response => response.url().endsWith('/system/compare-report') && response.request().method() === 'POST') |
| | | await page.getByRole('button', { name: '保存报表', exact: true }).click() |
| | | const v1 = (await (await created).json()).data |
| | | expect(v1.settings.hiddenSeriesKeys).toEqual(['9401|9401|1|1|']) |
| | | expect(v1.settings.seriesColors[2].toLowerCase()).toBe('#12ab34') |
| | | expect(v1.settings.metrics).not.toContain('temperature') |
| | | expect(v1.items.map((item: any) => item.records.length)).toEqual([65, 3, 65]) |
| | | expect(v1.units.current).toBe('A') |
| | | expect(v1.displayUnits.current).toBe('mA') |
| | | for (const item of v1.items) { |
| | | expect(item.zeroTime).toBe(Math.min(...item.records.flatMap((row: any) => [row.deviceTime, row.collectorDeviceTime]))) |
| | | expect(item.records[0].deviceTime - item.records[0].collectorDeviceTime).toBe(10000) |
| | | expect(item.records.find((row: any) => row.recordNo === String(item.source.selection.stepNo * 100 + 2)).current).toBe(-0.125) |
| | | } |
| | | const plot = await page.evaluate(async record => { |
| | | const path = '/src/utils/compareReportChart.ts' |
| | | const { compareChartOption } = await import(path) |
| | | const result: any = {} |
| | | for (const [key, unit] of [['current', 'mA'], ['temperature', '℃'], ['positiveVoltage', 'V']]) { |
| | | const all = compareChartOption({ key, unit }, record.items, record.settings.seriesColors) |
| | | const hidden = compareChartOption({ key, unit }, record.items, record.settings.seriesColors, record.settings.hiddenSeriesKeys) |
| | | result[key] = { all, hidden, tickLabels: [0, 1800, 3600].map(value => all.xAxis.axisLabel.formatter(value)) } |
| | | } |
| | | const short = compareChartOption({ key: 'current', unit: 'mA' }, [record.items[1]], record.settings.seriesColors) |
| | | return { ...result, short } |
| | | }, v1) |
| | | expect(plot.current.all.series[0].data).toHaveLength(65) |
| | | expect(plot.current.all.series[0].data.slice(0, 3)).toEqual([[10, 0], [124, null], [238, -125]]) |
| | | expect(plot.temperature.all.series[0].data.slice(0, 3)).toEqual([[0, 0], [114, null], [228, 25]]) |
| | | expect(plot.current.all.series.every((series: any) => series.connectNulls === false)).toBe(true) |
| | | expect(plot.short.xAxis.name).toBe('相对时间 / s') |
| | | expect(plot.current.all.xAxis.name).toBe('相对时间 / h') |
| | | expect(plot.current.tickLabels).toEqual(['0', '0.5', '1']) |
| | | for (const key of ['current', 'temperature', 'positiveVoltage']) { |
| | | expect(plot[key].all.xAxis.max).toBe(plot[key].hidden.xAxis.max) |
| | | expect(plot[key].hidden.series).toHaveLength(2) |
| | | expect(plot[key].all.yAxis.map((axis: any) => [axis.min, axis.max, axis.name])).toEqual(plot[key].hidden.yAxis.map((axis: any) => [axis.min, axis.max, axis.name])) |
| | | const [left, right] = plot[key].all.yAxis |
| | | expect([left.min, left.max, left.name]).toEqual([right.min, right.max, right.name]) |
| | | } |
| | | expect(plot.positiveVoltage.all.series[1].data.every((point: any) => point[1] === null)).toBe(true) |
| | | await page.goto(`/report-compare/${v1.id}/edit?mode=edit`) |
| | | await expect(legends.nth(0)).toHaveAttribute('aria-pressed', 'false') |
| | | await page.reload() |
| | | await expect(legends.nth(0)).toHaveAttribute('aria-pressed', 'false') |
| | | await expect(page.locator('.metric-chart canvas')).toHaveCount(4) |
| | | for (const width of [1440, 390]) { |
| | | await page.setViewportSize({ width, height: 900 }) |
| | | await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true) |
| | | await expect.poll(async () => (await page.locator('.metric-chart canvas').first().boundingBox())!.width).toBeGreaterThan(200) |
| | | const count = await page.locator('.metric-chart canvas').first().evaluate((canvas: HTMLCanvasElement) => { |
| | | const data = canvas.getContext('2d')!.getImageData(0, 0, canvas.width, canvas.height).data |
| | | let green = 0 |
| | | for (let i = 0; i < data.length; i += 4) if (data[i + 1]! > data[i]! + 60 && data[i + 1]! > data[i + 2]! + 50 && data[i + 3]! > 100) green++ |
| | | return green |
| | | }) |
| | | expect(count).toBeGreaterThan(100) |
| | | await page.screenshot({ path: `tests/e2e/.artifacts/compare-interactions-${width}.png`, fullPage: true }) |
| | | } |
| | | await page.setViewportSize({ width: 1440, height: 900 }) |
| | | await legends.nth(0).click() |
| | | await legends.nth(1).click() |
| | | await page.getByPlaceholder('请输入修改说明').fill('切换显示范围') |
| | | const edited = page.waitForResponse(response => response.url().endsWith(`/compare-report/${v1.id}/versions`) && response.request().method() === 'POST') |
| | | await page.getByRole('button', { name: '保存新版本', exact: true }).click() |
| | | const v2 = (await (await edited).json()).data |
| | | expect(v2.settings.hiddenSeriesKeys).toEqual(['9401|9401|1|2|']) |
| | | expect(v2.items).toEqual(v1.items) |
| | | const baseEdit = { name: v2.name, description: v2.description, experimentName: v2.experimentName, baseVersionNo: 2, changeSummary: '无效身份', settings: v2.settings } |
| | | for (const invalid of [['9402|9401|1|1|'], ['9401|9401|1|2|', '9401|9401|1|2|']]) { |
| | | await api(page, `compare-report/${v1.id}/versions`, 'POST', { ...baseEdit, settings: { ...v2.settings, hiddenSeriesKeys: invalid } }, 500) |
| | | } |
| | | // 未携带新增设置的旧契约保持原快照结构,重开和打印默认显示全部曲线。 |
| | | const { hiddenSeriesKeys: _hiddenSeriesKeys, ...legacySettings } = v1.settings |
| | | const legacy = await api(page, 'compare-report', 'POST', { |
| | | name: '无显隐设置的保存版本', description: '', experimentName: '', |
| | | selections: v1.items.map((item: any) => item.source.selection), settings: legacySettings, |
| | | }) |
| | | expect(legacy.settings).not.toHaveProperty('hiddenSeriesKeys') |
| | | await api(page, 'test/9401', 'DELETE') |
| | | await api(page, 'test/9402', 'DELETE') |
| | | await page.context().addInitScript(() => { window.print = () => { document.body.dataset.printRequested = 'true' } }) |
| | | for (const version of [v1, v2, legacy]) { |
| | | await page.goto(`/report-compare/${version.id}/edit?mode=view&version=${version.versionNo}`) |
| | | if (version === legacy) await expect(page.locator('.legend-name[aria-pressed="true"]')).toHaveCount(3) |
| | | else await expect(legends.nth(version.versionNo === 1 ? 0 : 1)).toHaveAttribute('aria-pressed', 'false') |
| | | const popupPromise = page.waitForEvent('popup') |
| | | await page.getByRole('button', { name: '打印 / 另存 PDF', exact: true }).click() |
| | | const popup = await popupPromise |
| | | await expect(popup.locator('.chart-card')).toHaveCount(4) |
| | | const voltage = popup.locator('.chart-card').first() |
| | | await expect(voltage.locator('svg path[stroke="#12AB34"], svg path[stroke="#12ab34"]').first()).toBeAttached() |
| | | if (version !== legacy) { |
| | | const hiddenColor = version.settings.seriesColors[version.versionNo === 1 ? 0 : 1] |
| | | await expect(voltage.locator(`svg path[stroke="${hiddenColor}"]`)).toHaveCount(0) |
| | | } |
| | | await expect(popup.locator('body')).toHaveAttribute('data-print-requested', 'true') |
| | | const artifact = version === legacy ? 'legacy' : `v${version.versionNo}` |
| | | await popup.screenshot({ path: `tests/e2e/.artifacts/compare-interactions-print-${artifact}.png`, fullPage: true }) |
| | | await popup.pdf({ path: `tests/e2e/.artifacts/compare-interactions-print-${artifact}.pdf`, preferCSSPageSize: true, printBackground: true }) |
| | | expect(await api(page, `compare-report/${version.id}/versions/${version.versionNo}/print`)).toEqual(version) |
| | | await popup.close() |
| | | } |
| | | expect(pageErrors).toEqual([]) |
| | | }) |