同步化成来源详情表单修复基线
# Conflicts:
# src/utils/formationSavedReportPrint.ts
| | |
| | | import { request, type ApiResponse } from './request' |
| | | import type { PersistedTestRecord } from './tests' |
| | | import type { PersistedTestRecord, TestConsoleDetail } from './tests' |
| | | import type { ReportMetricKey } from '@/utils/reportMeasurementChart' |
| | | |
| | | export interface FormationSelection { testId: number; testBatteryId: number } |
| | | export interface FormationCandidate { selection: FormationSelection; name: string; type: string; groupId: number; batteryGroup: string; batteryNo: string } |
| | | export interface FormationCandidate { selection: FormationSelection; name: string; type: string; groupId: number; batteryGroup: string; batteryNo: string; consoleDetail?: TestConsoleDetail | null } |
| | | export interface FormationField { key: string; scope: 'detail' | 'cycle'; kind: 'battery' | 'metric' | 'manual'; displayName: string; unit: string; fieldDefinitionId?: number | null } |
| | | export interface FormationChargeStep { stepNo: number; name: string; stepType: string; endVoltage: number | null; direction?: 'CHARGE' | 'DISCHARGE' | 'UNKNOWN' } |
| | | export interface FormationItem { |
| | |
| | | } |
| | | export interface FormationMatrix { items: FormationItem[]; cycleNos: number[] } |
| | | export interface FormationStepSelection { charge: number[]; discharge: number[] } |
| | | export interface FormationSettings { fields: FormationField[]; values: Record<string, string>; chargeStepSelections: Record<string, number>; metrics?: ReportMetricKey[]; seriesColors?: Record<string, string>; manualChargeSteps?: Record<string, number[]>; curveStepSelections?: Record<string, FormationStepSelection>; hiddenSeriesKeys?: string[]; activeCycle?: number | null } |
| | | export interface FormationSettings { fields: FormationField[]; values: Record<string, string>; chargeStepSelections: Record<string, number>; metrics?: ReportMetricKey[]; seriesColors?: Record<string, string>; manualChargeSteps?: Record<string, number[]>; curveStepSelections?: Record<string, FormationStepSelection>; hiddenSeriesKeys?: string[]; activeCycle?: number | null; sourceDetailOverrides?: Record<string, { experimentName?: string; fieldValues?: Record<string, string> }> } |
| | | export interface FormationSummary { id: number; name: string; creator: string; createdAt: number; testName?: string; batteryGroup?: string } |
| | | export interface FormationVersion { versionNo: number; operatorName: string; createdAt: number; changeSummary: string } |
| | | export interface SavedFormationReport extends FormationSummary, FormationMatrix { |
| | |
| | | export function spliceSourceValue(segment: SpliceSegment, settings: SpliceSettings, field: SpliceField) { |
| | | const manual = settings.detailFieldValues?.[field.name]?.[spliceSourceKey(segment.source)] |
| | | if (manual !== undefined) return manual |
| | | // 控制台详情只有名称,没有共享字段身份;模板字段一旦带 ID,必须按 ID 取组值,不能被同名详情冒认。 |
| | | if (field.fieldDefinitionId) { |
| | | return segment.businessFields?.find(value => value.fieldDefinitionId === field.fieldDefinitionId)?.fieldValue ?? '' |
| | | } |
| | | const detail = segment.consoleDetail?.fieldValues[field.name] |
| | | if (detail !== undefined) return detail |
| | | if (segment.consoleDetail?.batteryFields.includes(field.name) || segment.consoleDetail?.specialFields.includes(field.name)) return '' |
| | | // 用户明确选择的共享字段仍按其身份提供组值,不冒充缺失的控制台人工字段。 |
| | | return field.fieldDefinitionId ? segment.businessFields?.find(value => value.fieldDefinitionId === field.fieldDefinitionId)?.fieldValue ?? '' : '' |
| | | return '' |
| | | } |
| | | export function spliceExperiment(segment: SpliceSegment, settings: SpliceSettings) { |
| | | return settings.sourceExperimentNames?.[spliceSourceKey(segment.source)] ?? segment.consoleDetail?.experimentName ?? '' |
| | |
| | | ['efficiency', '充放电效率', '%'], ['energyEfficiency', '能量效率', '%'], |
| | | ].map(([key, displayName, unit]) => ({ key: key!, scope: 'cycle' as const, kind: 'metric' as const, displayName: displayName!, unit: unit! })), |
| | | ...[ |
| | | ['method2', '测试方法2'], ['manual', '数字'], ['chargePerGram', '每克瓦充电量'], ['chargePerGramTotal', '每克瓦完整充电量'], |
| | | ['capacity1v', '1.0V容量'], ['capacity1vf', '1.0VF完整容量'], ['dischargeInflection', '拐点容量'], ['dischargeInflectionTotal', '拐点完整容量'], |
| | | ['method2', '测试方法2'], ['manual', '数字'], ['chargePerGram', '每克N充电量'], ['chargePerGramTotal', '每克F充电量'], |
| | | ['capacity1v', '1.0V容量'], ['capacity1vf', '1.0VF克容量'], ['dischargeInflection', '拐点容量'], ['dischargeInflectionTotal', '拐点F克容量'], |
| | | ].map(([key, displayName]) => ({ key: key!, scope: 'cycle' as const, kind: 'manual' as const, displayName: displayName!, unit: '' })), |
| | | ] |
| | | |
| | |
| | | import { formationChartOption, formationCurveMetrics, formationSeriesColor, formationSeriesName } from './formationCycleChart' |
| | | import { formationChargeSteps, formationStepGroups, formationStepSeries, formationStepChartOption } from './formationStepChart' |
| | | import { openSavedReportPrint } from './savedReportPrint' |
| | | import { formationSourceFields, formationDetailValue } from './formationSourceDetails' |
| | | |
| | | use([LineChart, GridComponent, LegendComponent, SVGRenderer]) |
| | | function escapeHtml(value: unknown) { |
| | |
| | | function printHtml(record: SavedFormationReport) { |
| | | const colors = record.settings.seriesColors ?? {} |
| | | const items = [...new Set(record.items.map(item => item.source.selection.testId))].flatMap(id => record.items.filter(item => item.source.selection.testId === id)) |
| | | const cell = (value: unknown) => `<td>${escapeHtml(value == null || value === '' ? '缺失' : value)}</td>` |
| | | const cell = (value: unknown, preserveEmpty = false) => `<td>${escapeHtml(value == null || (!preserveEmpty && value === '') ? '缺失' : value)}</td>` |
| | | const sourceFields = formationSourceFields(items.map(item => item.source), record.settings.sourceDetailOverrides) |
| | | const tables = (cycle: number) => { |
| | | const result: string[] = [] |
| | | // 每组最多四列,按原测试分组顺序打印全部矩阵;长表重复列头并允许跨页。 |
| | |
| | | return `工步${step.stepNo} · ${step.name || '未命名工步'} · ${step.endVoltage ?? '缺失'}${step.endVoltage == null ? '' : ' V'}` |
| | | }))]) |
| | | } |
| | | if (!cycle) { |
| | | rows.push(['实验名称', ...group.map(item => formationDetailValue(item.source, record.settings.sourceDetailOverrides))]) |
| | | for (const field of sourceFields) |
| | | rows.push([`${field.name}(${field.category === 'battery' ? '电池字段' : '特殊字段'})`, ...group.map(item => formationDetailValue(item.source, record.settings.sourceDetailOverrides, field))]) |
| | | } |
| | | for (const field of record.settings.fields.filter(field => field.scope === (cycle ? 'cycle' : 'detail'))) { |
| | | rows.push([formationFieldLabel(field), ...group.map(item => value(item, () => |
| | | record.settings.values[formationValueKey(item, cycle, field)] ?? formationSourceValue(item, cycle, field)))]) |
| | | } |
| | | result.push(`<table class="matrix-table"><thead><tr><th>${cycle ? `循环${cycle} / 测试` : '测试'}</th>${group.map(item => `<th>${escapeHtml(item.source.name)}</th>`).join('')}</tr><tr><th>电池组 / 电池号</th>${group.map(item => `<th>${escapeHtml(item.source.batteryGroup)} / ${escapeHtml(item.source.batteryNo)}</th>`).join('')}</tr></thead><tbody>${rows.map(row => `<tr>${row.map(cell).join('')}</tr>`).join('')}</tbody></table>`) |
| | | // 来源详情的明确空字符串保持空白,既有指标缺失仍使用缺失标记。 |
| | | result.push(`<table class="matrix-table"><thead><tr><th>${cycle ? `循环${cycle} / 测试` : '测试'}</th>${group.map(item => `<th>${escapeHtml(item.source.name)}</th>`).join('')}</tr><tr><th>电池组 / 电池号</th>${group.map(item => `<th>${escapeHtml(item.source.batteryGroup)} / ${escapeHtml(item.source.batteryNo)}</th>`).join('')}</tr></thead><tbody>${rows.map((row, index) => `<tr>${row.map(value => cell(value, !cycle && index <= sourceFields.length)).join('')}</tr>`).join('')}</tbody></table>`) |
| | | } |
| | | return result.join('') |
| | | } |
| New file |
| | |
| | | import type { FormationCandidate, FormationSettings } from '@/api/formationReports' |
| | | |
| | | export function formationSourceKey(source: FormationCandidate) { return `${source.selection.testId}|${source.selection.testBatteryId}` } |
| | | |
| | | /** 控制台两类字段按名称保留,人工覆盖不进入业务字段和指标计算。 */ |
| | | export function formationSourceFields(sources: FormationCandidate[], overrides: FormationSettings['sourceDetailOverrides']) { |
| | | const fields = sources.flatMap(source => (['battery', 'special'] as const).flatMap(category => |
| | | (source.consoleDetail?.[category === 'battery' ? 'batteryFields' : 'specialFields'] || []).map(name => ({ key: `${category}|${name}`, name, category })))) |
| | | for (const source of sources) for (const key of Object.keys(overrides?.[formationSourceKey(source)]?.fieldValues || {})) { |
| | | const category = key.startsWith('battery|') ? 'battery' : 'special' |
| | | fields.push({ key, name: key.slice(category.length + 1), category }) |
| | | } |
| | | return [...new Map(fields.map(field => [field.key, field])).values()] |
| | | } |
| | | |
| | | /** 明确人工空字符串优先于冻结来源;缺失字段不从其他列补值。 */ |
| | | export function formationDetailValue(source: FormationCandidate, overrides: FormationSettings['sourceDetailOverrides'], field?: ReturnType<typeof formationSourceFields>[number]) { |
| | | const manual = overrides?.[formationSourceKey(source)] |
| | | if (!field) return manual?.experimentName ?? source.consoleDetail?.experimentName ?? '' |
| | | if (manual?.fieldValues?.[field.key] !== undefined) return manual.fieldValues[field.key]! |
| | | const names = source.consoleDetail?.[field.category === 'battery' ? 'batteryFields' : 'specialFields'] || [] |
| | | return names.includes(field.name) ? source.consoleDetail?.fieldValues[field.name]?.[source.batteryNo] ?? '' : '' |
| | | } |
| | |
| | | import type { ReportMetricKey } from '@/utils/reportMeasurementChart' |
| | | import { openFormationSavedReportPrint } from '@/utils/formationSavedReportPrint' |
| | | import FormationFieldControls from '@/components/FormationFieldControls.vue' |
| | | import { formationSourceKey, formationSourceFields, formationDetailValue } from '@/utils/formationSourceDetails' |
| | | import FormationStepCurves from '@/components/FormationStepCurves.vue' |
| | | import { formationChargeSteps, formationCycleKey } from '@/utils/formationStepChart' |
| | | import type { FormationSettings, FormationStepSelection } from '@/api/formationReports' |
| | |
| | | const cycleNos = ref<number[]>([]) |
| | | const fields = ref<FormationField[]>(structuredClone(defaultFormationFields)) |
| | | const values = ref<Record<string, string>>({}) |
| | | const sourceDetailOverrides = ref<NonNullable<FormationSettings['sourceDetailOverrides']>>({}) |
| | | const sourceFields = computed(() => formationSourceFields(reportItems.value.map(item => item.source), sourceDetailOverrides.value)) |
| | | function setDetail(source: FormationCandidate, value: string, field?: typeof sourceFields.value[number]) { |
| | | const key = formationSourceKey(source) |
| | | const current = sourceDetailOverrides.value[key] ?? {} |
| | | sourceDetailOverrides.value[key] = field ? { ...current, fieldValues: { ...current.fieldValues, [field.key]: value } } : { ...current, experimentName: value } |
| | | } |
| | | const chargeStepSelections = ref<Record<string, number>>({}) |
| | | const metrics = ref<ReportMetricKey[]>(formationCurveMetrics.map(metric => metric.key)) |
| | | const seriesColors = ref<Record<string, string>>({}) |
| | |
| | | const curveStepSelections = ref<Record<string, FormationStepSelection>>({}) |
| | | const hiddenSeriesKeys = ref<string[]>([]) |
| | | const actualSteps = computed(() => !saved.value || saved.value.curveMode === 'actual-steps') |
| | | const stepSettings = computed<FormationSettings>(() => ({ fields: fields.value, values: values.value, chargeStepSelections: chargeStepSelections.value, metrics: metrics.value, seriesColors: seriesColors.value, manualChargeSteps: manualChargeSteps.value, curveStepSelections: curveStepSelections.value, hiddenSeriesKeys: hiddenSeriesKeys.value, activeCycle: activeCycle.value })) |
| | | const stepSettings = computed<FormationSettings>(() => ({ fields: fields.value, values: values.value, sourceDetailOverrides: sourceDetailOverrides.value, chargeStepSelections: chargeStepSelections.value, metrics: metrics.value, seriesColors: seriesColors.value, manualChargeSteps: manualChargeSteps.value, curveStepSelections: curveStepSelections.value, hiddenSeriesKeys: hiddenSeriesKeys.value, activeCycle: activeCycle.value })) |
| | | function changeStepSettings(settings: Partial<FormationSettings>) { |
| | | if (settings.curveStepSelections) curveStepSelections.value = settings.curveStepSelections |
| | | if (settings.hiddenSeriesKeys) hiddenSeriesKeys.value = settings.hiddenSeriesKeys |
| | |
| | | } |
| | | function chargeKey(item: FormationItem, cycle: number) { return `${item.source.selection.testId}|${item.source.selection.testBatteryId}|${cycle}` } |
| | | function updateSeriesColor(item: FormationItem, color: string | null) { if (color) seriesColors.value[formationSeriesKey(item)] = color } |
| | | function formContent() { return JSON.stringify({ name: reportName.value, description: description.value, settings: actualSteps.value ? stepSettings.value : { fields: fields.value, values: values.value, chargeStepSelections: chargeStepSelections.value, metrics: metrics.value, seriesColors: seriesColors.value } }) } |
| | | function formContent() { return JSON.stringify({ name: reportName.value, description: description.value, settings: actualSteps.value ? stepSettings.value : { fields: fields.value, values: values.value, sourceDetailOverrides: sourceDetailOverrides.value, chargeStepSelections: chargeStepSelections.value, metrics: metrics.value, seriesColors: seriesColors.value } }) } |
| | | function restore(record: SavedFormationReport) { |
| | | saved.value = record; reportName.value = record.name; description.value = record.description || '' |
| | | items.value = record.items; cycleNos.value = record.cycleNos |
| | | fields.value = record.settings.fields; values.value = record.settings.values; chargeStepSelections.value = record.settings.chargeStepSelections || {} |
| | | sourceDetailOverrides.value = record.settings.sourceDetailOverrides ?? {} |
| | | metrics.value = record.settings.metrics ?? formationCurveMetrics.map(metric => metric.key) |
| | | seriesColors.value = record.settings.seriesColors ?? {} |
| | | manualChargeSteps.value = record.settings.manualChargeSteps ?? {}; curveStepSelections.value = record.settings.curveStepSelections ?? {} |
| | |
| | | (field.scope === 'detail' ? [0] : item.cycleNos).map(cycle => formationValueKey(item, cycle, field))))) |
| | | values.value = Object.fromEntries(Object.entries(values.value).filter(([key]) => allowed.has(key))) |
| | | const sourceKeys = new Set(items.value.flatMap(item => [formationSeriesKey(item), ...Object.entries(item.stepCurves ?? {}).flatMap(([cycle, steps]) => Object.keys(steps).map(step => `${formationSeriesKey(item)}|${cycle}|${step}`))])) |
| | | sourceDetailOverrides.value = Object.fromEntries(Object.entries(sourceDetailOverrides.value).filter(([key]) => sourceKeys.has(key))) |
| | | hiddenSeriesKeys.value = hiddenSeriesKeys.value.filter(key => sourceKeys.has(key)) |
| | | const cycleKeys = new Set(items.value.flatMap(item => item.cycleNos.map(cycle => formationCycleKey(item, cycle)))) |
| | | manualChargeSteps.value = Object.fromEntries(Object.entries(manualChargeSteps.value).filter(([key]) => cycleKeys.has(key))) |
| | |
| | | saving.value = true |
| | | try { |
| | | pruneValues() |
| | | const content = { name: reportName.value.trim(), description: description.value, settings: actualSteps.value ? stepSettings.value : { fields: fields.value, values: values.value, chargeStepSelections: chargeStepSelections.value, metrics: metrics.value, seriesColors: seriesColors.value } } |
| | | const content = { name: reportName.value.trim(), description: description.value, settings: actualSteps.value ? stepSettings.value : { fields: fields.value, values: values.value, sourceDetailOverrides: sourceDetailOverrides.value, chargeStepSelections: chargeStepSelections.value, metrics: metrics.value, seriesColors: seriesColors.value } } |
| | | const result = editingId.value && saved.value |
| | | ? await editFormationReport(editingId.value, { ...content, baseVersionNo: saved.value.versionNo, changeSummary: changeSummary.value.trim() }) |
| | | : await createFormationReport({ ...content, selections: items.value.map(item => item.source.selection) }) |
| | |
| | | loading.value = true; loadError.value = '' |
| | | previewSequence++; previewLoading.value = false; previewError.value = '' |
| | | saved.value = null; items.value = []; cycleNos.value = []; values.value = {}; chargeStepSelections.value = {}; selectedKeys.value = [] |
| | | sourceDetailOverrides.value = {} |
| | | try { |
| | | const permissions = (await getUserInfo()).permissions || [] |
| | | const has = (action: string) => permissions.includes('*:*:*') || permissions.includes(`system:formation-report:${action}`) |
| | |
| | | <div class="report-data-table-wrap"><table class="report-data-table" :style="tableStyle"> |
| | | <thead><tr><th>测试</th><th v-for="column in reportColumns" :key="column.id" :colspan="column.items.length">{{ column.name }}</th></tr> |
| | | <tr><th>电池组 / 电池号</th><th v-for="item in reportItems" :key="selectionKey(item.source)">{{ item.source.batteryGroup }} / {{ item.source.batteryNo }}</th></tr></thead> |
| | | <tbody><tr v-for="field in detailFields" :key="field.key"><td>{{ formationFieldLabel(field) }}</td><td v-for="item in reportItems" :key="selectionKey(item.source)"> |
| | | <tbody> |
| | | <tr><td>实验名称</td><td v-for="item in reportItems" :key="selectionKey(item.source)"><el-input :model-value="formationDetailValue(item.source, sourceDetailOverrides)" :disabled="readonly" :aria-label="`${item.source.name} ${item.source.batteryNo} 实验名称`" @update:model-value="setDetail(item.source, $event)" /></td></tr> |
| | | <tr v-for="field in sourceFields" :key="`console:${field.key}`"><td>{{ field.name }}({{ field.category === 'battery' ? '电池字段' : '特殊字段' }})</td><td v-for="item in reportItems" :key="selectionKey(item.source)"><el-input :model-value="formationDetailValue(item.source, sourceDetailOverrides, field)" :disabled="readonly" :aria-label="`${item.source.name} ${item.source.batteryNo} 详情 ${field.name}`" @update:model-value="setDetail(item.source, $event, field)" /></td></tr> |
| | | <tr v-for="field in detailFields" :key="field.key"><td>{{ formationFieldLabel(field) }}</td><td v-for="item in reportItems" :key="selectionKey(item.source)"> |
| | | <el-input :model-value="inputValue(item, 0, field)" @update:model-value="setValue(item, 0, field, $event)" :disabled="readonly" :aria-label="`${item.source.name} ${item.source.batteryNo} ${field.displayName}`" :placeholder="field.kind === 'manual' ? '人工填写' : '指标缺失'" size="small" /> |
| | | </td></tr></tbody> |
| | | </table></div> |
| | |
| | | const popupWait = page.waitForEvent('popup') |
| | | await page.getByRole('button', { name: '打印 / 另存 PDF', exact: true }).click() |
| | | const popup = await popupWait |
| | | await expect(popup.locator('.detail-table')).toContainText('乙温升摘要 (℃)') |
| | | await expect(popup.locator('.detail-table')).toContainText('样品质量 (g)') |
| | | await expect(popup.locator('.detail-table')).toContainText('20') |
| | | const printedFields = popup.locator('.detail-table').filter({ hasText: '乙温升摘要 (℃)' }) |
| | | await expect(printedFields).toContainText('乙温升摘要 (℃)') |
| | | await expect(printedFields).toContainText('样品质量 (g)') |
| | | await expect(printedFields).toContainText('20') |
| | | await expect(popup.locator('.chart-card')).toContainText('mA') |
| | | await popup.close() |
| | | await page.goto(`/report-compare/${fresh.id}/edit?mode=edit`) |
| 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 () => { |
| | | await stack.start() |
| | | stack.executeIsolatedSql(` |
| | | INSERT INTO t_battery_group(group_id,group_name,model) VALUES(9501,'化成详情甲组','甲'),(9502,'化成详情乙组','乙'); |
| | | INSERT INTO t_battery_group_relation_field(group_id,battery_group_field_id,field_value) SELECT 9501,field_id,'12.5' FROM t_battery_group_field WHERE field_name='F'; |
| | | INSERT INTO t_test_type(test_type_id,test_type_name) VALUES(9501,'化成'),(9502,'其他'),(9503,'化成扩展'); |
| | | INSERT INTO t_test(test_id,test_name,creator_id,principal_id,test_type_id,test_purpose,test_status,reception_status) |
| | | VALUES(9501,'化成详情甲',1,1,9501,'验收','COMPLETED','ENDED'),(9502,'化成详情乙',1,1,9501,'验收','COMPLETED','ENDED'), |
| | | (9503,'名称含化成',1,1,9502,'验收','COMPLETED','ENDED'),(9504,'未完成化成',1,1,9501,'验收','WAITING','NOT_STARTED'), |
| | | (9505,'近似化成类型',1,1,9503,'验收','COMPLETED','ENDED'),(9506,'无详情化成',1,1,9501,'验收','COMPLETED','ENDED'); |
| | | INSERT INTO t_test_battery(test_battery_id,test_id,group_id,battery_no,collector_id) |
| | | VALUES(9501,9501,9501,'同名电池',9501),(9502,9501,9501,'二号',9502),(9503,9502,9502,'同名电池',9503), |
| | | (9504,9503,9501,'其他',9504),(9505,9504,9501,'未完成',9505),(9506,9505,9501,'近似',9506),(9507,9506,9502,'无详情',9507); |
| | | INSERT INTO t_test_cycle_record(test_id,test_battery_id,binding_id,adapter_code,source_identity,source_record_id,dedupe_key,cycle_no,device_time_utc,server_received_at_utc,adapter_timezone_id,device_time_raw,charge_capacity_ah,discharge_capacity_ah,charge_energy_wh,discharge_energy_wh,content_sha256) |
| | | VALUES ${[[9501,9501,1],[9501,9501,3],[9501,9502,2],[9502,9503,3]].map(([id,battery,cycle]) => `(${id},${battery},${battery},'formation-detail','source:${battery}','${cycle}','${cycle}',${cycle},'2026-09-01 00:00:00.123','2026-09-01 00:00:01.123','Asia/Hong_Kong','2026-09-01 08:00:00.123',${cycle === 3 ? 'NULL' : '1.2'},0,1,2,UNHEX(SHA2('${battery}:${cycle}',256)))`).join(',')}; |
| | | `) |
| | | }) |
| | | 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 body = await (await page.request.fetch(`${stack.backendUrl}/system/${path}`, { method, data, headers: { Authorization: `Bearer ${token}` } })).json() |
| | | expect(body.code, `${path}: ${body.msg}`).toBe(code) |
| | | return body.data |
| | | } |
| | | const selections = [{ testId: 9501, testBatteryId: 9501 }, { testId: 9501, testBatteryId: 9502 }, { testId: 9502, testBatteryId: 9503 }] |
| | | const detailA = { experimentName: '实验甲', batteryFields: ['F', '人工字段'], specialFields: ['说明'], fieldValues: { F: { 同名电池: '人工F不能参与公式', 二号: '' }, 人工字段: { 同名电池: '甲值', 二号: '' }, 说明: { 同名电池: '甲说明', 二号: '二号说明' } } } |
| | | const detailB = { experimentName: '实验乙', batteryFields: ['人工字段'], specialFields: ['说明'], fieldValues: { 人工字段: { 同名电池: '乙值' }, 说明: { 同名电池: '' } } } |
| | | |
| | | test('化成按来源冻结详情和空字符串,独立编辑、字段模板、更新和删源打印', async ({ page }) => { |
| | | const errors: string[] = [] |
| | | page.on('pageerror', error => errors.push(error.message)) |
| | | await login(page) |
| | | await api(page, 'test/9501/console-detail', 'PUT', detailA) |
| | | await api(page, 'test/9502/console-detail', 'PUT', detailB) |
| | | const sources = await api(page, 'formation-report/sources') |
| | | for (const id of [9503,9504,9505]) expect(sources.map((source: any) => source.selection.testId)).not.toContain(id) |
| | | for (const selection of [{ testId: 9503, testBatteryId: 9504 }, { testId: 9504, testBatteryId: 9505 }, { testId: 9505, testBatteryId: 9506 }, { testId: 9501, testBatteryId: 9503 }]) |
| | | await api(page, 'formation-report/preview', 'POST', [selection], 500) |
| | | const preview = await api(page, 'formation-report/preview', 'POST', [...selections, { testId: 9506, testBatteryId: 9507 }]) |
| | | expect(preview.cycleNos).toEqual([1,2,3]) |
| | | expect(preview.items.map((item: any) => item.cycleNos)).toEqual([[1,3],[2],[3],[]]) |
| | | expect(preview.items[0].source.consoleDetail.fieldValues.人工字段).toEqual({ 同名电池: '甲值' }) |
| | | expect(preview.items[1].source.consoleDetail.fieldValues.人工字段).toEqual({ 二号: '' }) |
| | | expect(preview.items[3].source.consoleDetail).toBeNull() |
| | | await page.goto('/report-formation/new') |
| | | await page.locator('.source-panel .el-select').click() |
| | | for (const name of ['化成详情甲 / 化成详情甲组 / 同名电池', '化成详情甲 / 化成详情甲组 / 二号', '化成详情乙 / 化成详情乙组 / 同名电池']) |
| | | await page.getByRole('option', { name, exact: true }).click() |
| | | await page.getByRole('combobox', { name: '化成来源', exact: true }).press('Escape') |
| | | await expect(page.locator('.cycle-card')).toHaveCount(3) |
| | | await expect(page.getByRole('textbox', { name: '化成详情甲 同名电池 实验名称', exact: true })).toHaveValue('实验甲') |
| | | await expect(page.getByRole('textbox', { name: '化成详情乙 同名电池 实验名称', exact: true })).toHaveValue('实验乙') |
| | | await expect(page.getByRole('textbox', { name: '化成详情甲 二号 详情 人工字段', exact: true })).toHaveValue('') |
| | | await expect(page.getByRole('textbox', { name: '化成详情甲 同名电池 F', exact: true })).toHaveValue('12.5') |
| | | await expect(page.getByRole('textbox', { name: '化成详情甲 同名电池 详情 F', exact: true })).toHaveValue('人工F不能参与公式') |
| | | for (const label of ['每克N充电量','每克F充电量','1.0V容量','1.0VF克容量','拐点容量','拐点F克容量']) |
| | | await expect(page.getByRole('textbox', { name: `化成详情甲 同名电池 循环1 ${label}`, exact: true })).toHaveValue('') |
| | | await expect(page.locator('[data-cycle="2"]')).toContainText('无该循环') |
| | | await expect(page.getByRole('textbox', { name: '化成详情甲 同名电池 循环3 充电容量', exact: true })).toHaveAttribute('placeholder', '指标缺失') |
| | | await page.getByRole('textbox', { name: '化成详情甲 同名电池 详情 人工字段', exact: true }).fill('报表覆盖') |
| | | await page.getByRole('textbox', { name: '化成详情乙 同名电池 实验名称', exact: true }).fill('') |
| | | await page.getByPlaceholder('请输入报表名称').fill('化成详情独立报表') |
| | | // 保存前来源再次变化:后端冻结最新值,同时保留报表内明确调整。 |
| | | await api(page, 'test/9501/console-detail', 'PUT', { ...detailA, experimentName: '保存时实验甲' }) |
| | | await page.getByRole('button', { name: '保存报表', exact: true }).click() |
| | | await expect(page).toHaveURL(/\/report-formation\/\d+\/edit\?mode=view$/) |
| | | const id = Number(page.url().match(/formation\/(\d+)/)![1]) |
| | | const first = await api(page, `formation-report/${id}`) |
| | | expect(first.items.find((item: any) => item.source.selection.testBatteryId === 9501).source.consoleDetail.experimentName).toBe('保存时实验甲') |
| | | expect(first.settings.sourceDetailOverrides['9501|9501'].fieldValues['battery|人工字段']).toBe('报表覆盖') |
| | | expect(first.settings.sourceDetailOverrides['9502|9503'].experimentName).toBe('') |
| | | expect((await api(page, 'test/9501')).consoleDetail.fieldValues.人工字段.同名电池).toBe('甲值') |
| | | for (const sourceDetailOverrides of [ |
| | | { '9506|9507': { experimentName: '伪造来源' } }, |
| | | { '9501|9501|1|1': { experimentName: '曲线范围不是详情身份' } }, |
| | | { '9501|9501': { fieldValues: { voltage: '不能冒充设备字段' } } }, |
| | | ]) { |
| | | await api(page, `formation-report/${id}/versions`, 'POST', { |
| | | name: first.name, description: '', baseVersionNo: 1, changeSummary: '拒绝伪造详情', settings: { ...first.settings, sourceDetailOverrides }, |
| | | }, 500) |
| | | } |
| | | expect((await api(page, `formation-report/${id}`)).versionNo).toBe(1) |
| | | const fields = first.settings.fields |
| | | const template = await api(page, 'formation-report/templates', 'POST', { name: '纯结构模板', fields }) |
| | | expect(template).toBeGreaterThan(0) |
| | | expect(JSON.stringify(await api(page, 'formation-report/templates'))).not.toContain('报表覆盖') |
| | | await page.reload() |
| | | await expect(page.getByRole('textbox', { name: '化成详情甲 同名电池 详情 人工字段', exact: true })).toHaveValue('报表覆盖') |
| | | await page.getByRole('button', { name: '编辑', exact: true }).click() |
| | | 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 expect(page.getByRole('textbox', { name: '化成详情甲 同名电池 详情 人工字段', exact: true })).toHaveValue('报表覆盖') |
| | | await page.getByRole('textbox', { name: '化成详情甲 同名电池 实验名称', exact: true }).fill('报表实验甲') |
| | | await page.getByPlaceholder('请输入修改说明').fill('详情独立编辑') |
| | | const versionResponse = page.waitForResponse(response => response.url().endsWith(`/formation-report/${id}/versions`) && response.request().method() === 'POST') |
| | | await page.getByRole('button', { name: '保存新版本', exact: true }).click() |
| | | const versionBody = await (await versionResponse).json() |
| | | expect(versionBody.code, versionBody.msg).toBe(200) |
| | | expect(versionBody.data.versionNo).toBe(2) |
| | | await expect(page).toHaveURL(/mode=view$/) |
| | | await expect(page.getByRole('textbox', { name: '化成详情甲 同名电池 实验名称', exact: true })).toHaveValue('报表实验甲') |
| | | const second = await api(page, `formation-report/${id}`) |
| | | expect(second.versionNo).toBe(2) |
| | | expect(second.items).toEqual(first.items) |
| | | await api(page, `formation-report/${id}/versions`, 'POST', { name: first.name, description: '', baseVersionNo: 1, changeSummary: '陈旧', settings: first.settings }, 500) |
| | | await api(page, 'test/9501/console-detail', 'PUT', { ...detailA, experimentName: '最新来源实验' }) |
| | | const refreshed = await api(page, `formation-report/${id}/refresh-source`, 'POST', { baseVersionNo: 2, changeSummary: '明确更新' }) |
| | | expect(refreshed.items.find((item: any) => item.source.selection.testBatteryId === 9501).source.consoleDetail.experimentName).toBe('最新来源实验') |
| | | expect(refreshed.settings.sourceDetailOverrides).toEqual(second.settings.sourceDetailOverrides) |
| | | expect(await api(page, `formation-report/${id}/versions/1`)).toEqual(first) |
| | | 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/formation-details-${width}.png`, fullPage: true }) |
| | | } |
| | | for (const source of [9501,9502]) await api(page, `test/${source}`, 'DELETE') |
| | | await api(page, `formation-report/${id}/refresh-source`, 'POST', { baseVersionNo: 3, changeSummary: '删源后' }, 500) |
| | | await page.context().addInitScript(() => { window.print = () => { document.body.dataset.printRequested = 'true' } }) |
| | | for (const version of [1,3]) { |
| | | await page.goto(`/report-formation/${id}/edit?mode=view&version=${version}`) |
| | | await expect(page.getByRole('textbox', { name: '化成详情甲 同名电池 详情 人工字段', exact: true })).toHaveValue('报表覆盖') |
| | | 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') |
| | | await expect(popup.locator('.matrix-table').first()).toContainText('报表覆盖') |
| | | await expect(popup.locator('.matrix-table').first()).toContainText(version === 1 ? '保存时实验甲' : '报表实验甲') |
| | | const headers = await popup.locator('.matrix-table').first().locator('thead tr').last().locator('th').allTextContents() |
| | | const emptyColumn = headers.findIndex(header => header.includes('二号')) |
| | | expect(emptyColumn).toBeGreaterThan(0) |
| | | await expect(popup.locator('.matrix-table').first().locator('tr').filter({ hasText: '人工字段(电池字段)' }).locator('td').nth(emptyColumn)).toHaveText('') |
| | | await popup.pdf({ path: `tests/e2e/.artifacts/formation-details-v${version}.pdf`, preferCSSPageSize: true, printBackground: true }) |
| | | await popup.screenshot({ path: `tests/e2e/.artifacts/formation-details-print-v${version}.png`, fullPage: true }) |
| | | await popup.close() |
| | | } |
| | | expect(errors).toEqual([]) |
| | | }) |
| | | |
| | | test('旧版缺详情和旧字段名称原样保留,仅新版本纠正名称', async ({ page }) => { |
| | | await login(page) |
| | | const settings = { fields: [{ key: 'chargePerGram', scope: 'cycle', kind: 'manual', displayName: '每克瓦充电量', unit: '' }], values: {}, chargeStepSelections: {} } |
| | | const created = await api(page, 'formation-report', 'POST', { name: '旧化成详情版本', description: '', selections: [{ testId: 9506, testBatteryId: 9507 }], settings }) |
| | | expect(created.settings.fields[0].displayName).toBe('每克N充电量') |
| | | // 隔离库构造旧版事实,用于验证读取不静默改写历史,也不借来源补造详情。 |
| | | stack.executeIsolatedSql(`UPDATE t_formation_report_version SET snapshot_json=JSON_SET(JSON_REMOVE(snapshot_json,'$.items[0].source.consoleDetail','$.settings.sourceDetailOverrides'),'$.settings.fields[0].displayName','每克瓦充电量') WHERE report_id=${created.id}`) |
| | | const old = await api(page, `formation-report/${created.id}`) |
| | | await api(page, 'test/9506/console-detail', 'PUT', { experimentName: '后来才填写的实验', batteryFields: [], specialFields: [], fieldValues: {} }) |
| | | await page.goto(`/report-formation/${created.id}/edit?mode=view`) |
| | | await expect(page.getByRole('textbox', { name: '无详情化成 无详情 实验名称', exact: true })).toHaveValue('') |
| | | expect(old.items[0].source.consoleDetail).toBeUndefined() |
| | | expect(old.settings.fields[0].displayName).toBe('每克瓦充电量') |
| | | const edited = await api(page, `formation-report/${created.id}/versions`, 'POST', { name: '新版本名称', description: '', baseVersionNo: 1, changeSummary: '只调整名称', settings: old.settings }) |
| | | expect(edited.items).toEqual(old.items) |
| | | expect(edited.settings.fields[0].displayName).toBe('每克N充电量') |
| | | const refreshed = await api(page, `formation-report/${created.id}/refresh-source`, 'POST', { baseVersionNo: 2, changeSummary: '主动带入详情' }) |
| | | expect(refreshed.items[0].source.consoleDetail.experimentName).toBe('后来才填写的实验') |
| | | expect(await api(page, `formation-report/${created.id}/versions/1/print`)).toEqual(old) |
| | | }) |
| | |
| | | import { expect, test, type Page } from '@playwright/test' |
| | | import { Ticket04Stack } from './support/ticket04-stack' |
| | | import { spliceSourceValue } from '@/api/spliceReports' |
| | | |
| | | const stack = new Ticket04Stack() |
| | | const selection = (testBatteryId: number) => ({ testId: testBatteryId < 9703 ? 9701 : 9702, testBatteryId }) |
| | |
| | | } |
| | | }) |
| | | test.afterAll(async () => stack.stop()) |
| | | |
| | | test('同名控制台字段与共享字段按身份隔离', () => { |
| | | const segment: any = { |
| | | source: { selection: { testId: 1, testBatteryId: 2 }, batteryNo: '1号' }, |
| | | businessFields: [{ fieldDefinitionId: 10, fieldName: '质量', fieldValue: '共享值' }], |
| | | consoleDetail: { batteryFields: ['质量'], specialFields: [], fieldValues: { 质量: '控制台值' } }, |
| | | } |
| | | expect(spliceSourceValue(segment, {}, { name: '质量', category: 'battery', displayName: '质量', unit: 'kg', fieldDefinitionId: 10 })).toBe('共享值') |
| | | expect(spliceSourceValue(segment, {}, { name: '质量', category: 'battery', displayName: '质量', unit: '', fieldDefinitionId: null })).toBe('控制台值') |
| | | }) |
| | | async function login(page: Page) { |
| | | await page.goto('/login') |
| | | await page.locator('input[autocomplete="username"]').fill('admin') |
| | |
| | | if (!this.docker) return |
| | | this.dockerRun(['rm', '--force', this.mysqlContainer], true) |
| | | this.dockerRun(['rm', '--force', this.redisContainer], true) |
| | | // WSL 的端口转发可能晚于容器退出释放;确认本次端口已释放后才交给下一份验收。 |
| | | await waitUntil(async () => { |
| | | try { |
| | | await Promise.all([mysqlPort, redisPort, apiPort, simulatorPort].map(assertPortAvailable)) |
| | | return true |
| | | } catch { |
| | | return false |
| | | } |
| | | }, '本次隔离环境的端口未在清理后释放', 30_000) |
| | | } |
| | | |
| | | private dockerRun(args: string[], allowFailure = false) { |
| | |
| | | return new Function(...Object.keys(bindings), 'toTestDisplayValue', script)(...Object.values(bindings), toTestDisplayValue) |
| | | } |
| | | |
| | | async function printModule(name) { |
| | | async function measurementModule() { |
| | | const name = 'reportMeasurementChart' |
| | | const source = await readFile(new URL(`../src/utils/${name}.ts`, import.meta.url), 'utf8') |
| | | const script = ts.transpileModule(`${source}\nexport { createChartSvg }`, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS } }).outputText |
| | | const script = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS } }).outputText |
| | | const exports = {} |
| | | new Function('exports', 'require', script)(exports, id => { |
| | | assert.equal(id, './testDataUnits') |
| | |
| | | }) |
| | | return exports |
| | | } |
| | | const compare = await printModule('compareReportPrint') |
| | | const formation = await printModule('formationReportPrint') |
| | | const { measurementChartOption } = await measurementModule() |
| | | |
| | | test('工步图电流序列和纵轴同步为 mA,保留负数与零', async () => { |
| | | const records = [-8.1651, 0, -8.1674].map((current, index) => ({ batteryNo: 'BAT-1', current, deviceTime: index * 60000, sourceType: 'SNAPSHOT' })) |
| | | const { metricValue, chartOption } = await componentFunctions('components/ConsoleChartPanel.vue', ['metricValue', 'recordTime', 'chartOption'], { |
| | | props: { cells: [{ no: 'BAT-1' }] }, selectedRecords: () => records, selectedCells: { value: [{ no: 'BAT-1' }] }, metricMatches: () => true, seriesColors: { value: ['#3978f6'] }, |
| | | }) |
| | | const option = chartOption(compare.compareMetricDefinitions.find(metric => metric.key === 'current')) |
| | | const option = chartOption({ key: 'current', unit: 'mA' }) |
| | | assert.deepEqual(option.series[0].data, [-8165.1, 0, -8167.4]) |
| | | assert.equal(option.yAxis[0].name, 'mA') |
| | | assert.ok(option.yAxis[0].min < -8167.4) |
| | |
| | | assert.equal(funcs.dischargeCapacityPerFactor('BAT-1', 1), null) |
| | | }) |
| | | |
| | | test('三类演示报表曲线、偏移量、纵轴和打印统一换算', async () => { |
| | | const common = { seriesColors: { value: ['#3978f6'] } } |
| | | const compareChart = await componentFunctions('views/ReportCompareForm.vue', ['chartOption'], { ...common, chartSeries: { value: [{ name: '测试', batteryNo: 'BAT-1' }] } }) |
| | | const current = compare.compareMetricDefinitions.find(metric => metric.key === 'current') |
| | | const option = compareChart.chartOption(current) |
| | | assert.equal(option.yAxis[0].name, 'mA') |
| | | assert.equal(option.series[0].data[0], 1100) |
| | | assert.equal(option.yAxis[0].max, 1380) |
| | | const svg = compare.createChartSvg(current, { detailTests: [{ name: '测试', batteryNo: 'BAT-1' }], seriesColors: [] }) |
| | | assert.match(svg, />mA</) |
| | | assert.match(svg, />1380</) |
| | | const formationChart = await componentFunctions('views/FormationReportForm.vue', ['chartOption'], { ...common, reportSamples: { value: ['BAT-1'] }, activeCycle: { value: 2 } }) |
| | | const formationCurrent = formation.formationMetricDefinitions.find(metric => metric.key === 'current') |
| | | assert.equal(formationChart.chartOption(formationCurrent).series[0].data[0], 1212) |
| | | const formationSvg = formation.createChartSvg(formationCurrent, { cycle: 2 }, { samples: ['BAT-1'], seriesColors: [] }) |
| | | assert.match(formationSvg, />mA</) |
| | | assert.match(formationSvg, />1212</) |
| | | const spliceChart = await componentFunctions('views/SpliceReportForm.vue', ['chartOption', 'formatMinutes'], { ...common, samples: { value: ['BAT-1'] }, selectedTests: { value: [{}, {}] }, timeline: { value: { segments: [], labels: [] } } }) |
| | | const spliceOption = spliceChart.chartOption(formationCurrent) |
| | | assert.equal(spliceOption.series[0].data[0], 1200) |
| | | assert.equal(spliceOption.series[0].data[7], 1215) |
| | | assert.equal(spliceOption.yAxis[0].name, 'mA') |
| | | }) |
| | | test('保存报表共用测量图按真实时间和单位绘制,显隐不改变完整数据范围', () => { |
| | | const items = [ |
| | | { name: '测试甲 · 1号', zeroTime: 1000, records: [ |
| | | { deviceTime: 1000, collectorDeviceTime: 9000, current: -0.125, voltage: 3.2, temperature: 0 }, |
| | | { deviceTime: 18000, collectorDeviceTime: 22000, current: null, voltage: null, temperature: null }, |
| | | { deviceTime: 181000, collectorDeviceTime: 182000, current: 0, voltage: 0, temperature: -5 }, |
| | | ] }, |
| | | { name: '测试乙 · 1号', zeroTime: 3000, records: [{ deviceTime: 7203000, current: 0.25 }] }, |
| | | ] |
| | | const before = structuredClone(items) |
| | | const metric = { key: 'current', unit: 'mA' } |
| | | const option = measurementChartOption(metric, items, ['#123456', '#abcdef']) |
| | | assert.deepEqual(option.series[0].data, [[0, -125], [17, null], [180, 0]]) |
| | | assert.equal(option.series[0].connectNulls, false) |
| | | assert.equal(option.xAxis.max, 7200) |
| | | assert.equal(option.xAxis.axisLabel.formatter(1800), '0.5') |
| | | assert.deepEqual(option.yAxis.map(axis => [axis.name, axis.min, axis.max]), [['mA', -143.75, 250], ['mA', -143.75, 250]]) |
| | | const hidden = measurementChartOption(metric, [{ ...items[0], hidden: true }, items[1]], ['#123456', '#abcdef']) |
| | | assert.equal(hidden.series.length, 1) |
| | | assert.equal(hidden.series[0].lineStyle.color, '#abcdef') |
| | | assert.equal(hidden.xAxis.max, option.xAxis.max) |
| | | assert.equal(hidden.yAxis[0].min, option.yAxis[0].min) |
| | | assert.deepEqual(items, before) |
| | | const voltage = measurementChartOption({ key: 'voltage', unit: 'V' }, [items[0]], ['#123456']) |
| | | assert.deepEqual(voltage.series[0].data, [[0, 3.2], [17, null], [180, 0]]) |
| | | const temperature = measurementChartOption({ key: 'temperature', unit: '℃' }, [items[0]], ['#123456']) |
| | | assert.deepEqual(temperature.series[0].data, [[8, 0], [21, null], [181, -5]]) |
| | | }) |
| | |
| | | import { readFile } from 'node:fs/promises' |
| | | |
| | | const view = await readFile(new URL('../src/views/TestConsole.vue', import.meta.url), 'utf8') |
| | | const liveService = await readFile(new URL('../../BatteryDetection/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestLiveServiceImpl.java', import.meta.url), 'utf8') |
| | | const liveService = await readFile(new URL('../../BatteryDetection/battery-business/src/main/java/com/battery/business/service/impl/TTestLiveServiceImpl.java', import.meta.url), 'utf8') |
| | | |
| | | assert.match(view, /metric\(\['VOLTAGE', 'voltage'\], cabinetSource, 'voltageV'\)/) |
| | | assert.match(view, /metric\(\['CURRENT', 'current'\], cabinetSource, 'currentA'\)/) |
| | |
| | | assert.match(view, /liveConnection/) |
| | | assert.match(view, /不纳入结果/) |
| | | assert.doesNotMatch(view, /localStorage\.(setItem|getItem)\([^)]*test/i) |
| | | assert.match(view, /item\.sourceType !== 'COLLECTOR' \|\| !item\.status \|\| item\.status === 'OPEN'/) |
| | | assert.match(view, /filter\(item => !item\.status \|\| item\.status === 'OPEN'\)/) |
| | | assert.doesNotMatch(view, /完整性检查/) |
| | | assert.doesNotMatch(view, /integrity-panel/) |
| | | assert.match(api, /COMPLETION_PENDING: '检测完成'/) |
| | |
| | | |
| | | const view = await readFile(new URL('../src/views/TestConsole.vue', import.meta.url), 'utf8') |
| | | const dataList = await readFile(new URL('../src/components/ConsoleDataListPanel.vue', import.meta.url), 'utf8') |
| | | const liveService = await readFile(new URL('../../BatteryDetection/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestLiveServiceImpl.java', import.meta.url), 'utf8') |
| | | const liveService = await readFile(new URL('../../BatteryDetection/battery-business/src/main/java/com/battery/business/service/impl/TTestLiveServiceImpl.java', import.meta.url), 'utf8') |
| | | |
| | | assert.match(view, /<strong>项目参数<\/strong><span>STEP PARAMETERS<\/span>/) |
| | | assert.match(view, /testPlan\?\.rows/) |
| | |
| | | assert.doesNotMatch(dataList, /<th>逻辑工步<\/th>/) |
| | | assert.doesNotMatch(dataList, /<th>实际工步<\/th>/) |
| | | assert.match(dataList, /activeTab\.value === 'step' \? stepRows\.value/) |
| | | assert.doesNotMatch(view, /:logic-step-mappings=/) |
| | | assert.doesNotMatch(view, /:plan-steps=/) |
| | | assert.doesNotMatch(view.match(/<ConsoleDataListPanel\b[^]*?\/>/)[0], /:logic-step-mappings=/) |
| | | assert.doesNotMatch(view.match(/<ConsoleDataListPanel\b[^]*?\/>/)[0], /:plan-steps=/) |
| | | assert.doesNotMatch(dataList, /mappedStepRows/) |
| | | assert.doesNotMatch(dataList, /observedStepPositions/) |
| | | assert.doesNotMatch(dataList, /sumValue\('capacity'\)/) |
| | | assert.doesNotMatch(dataList, /logical-step:/) |
| | | assert.match(dataList, /const filteredRows = computed\(\(\) =>\n sourceRows\.value\.filter/) |
| | | assert.match(dataList, /const filteredRows = computed\(\(\) =>\r?\n sourceRows\.value\.filter/) |
| | | assert.match(dataList, /:total="filteredRows\.length"/) |
| | | assert.doesNotMatch(dataList, /receptionIntervals/) |
| | | assert.match(dataList, /physicalIds/) |
| | | assert.match(dataList, /props\.receptionStatus === 'NOT_STARTED'/) |
| | | assert.match(liveService, /simulator\.btsTest\(battery\.getBinding\(\)\.getDevid\(\), battery\.getBinding\(\)\.getChlid\(\), 1\)/) |
| | | assert.match(await readFile(new URL('../../BatteryDetection/battery-device-simulator/src/main/java/com/battery/device/simulator/BtsDevelopmentSimulator.java', import.meta.url), 'utf8'), /Integer cyclePosition/) |
| | | // 接收状态只触发同步,当前有效历史不因接收暂停或未开始而在前端清空。 |
| | | assert.match(dataList, /if \(next !== previous\) void loadRecords\(\)/) |
| | | assert.match(liveService, /adapter\.testPlan\(new ChannelIdentity\(binding\.getAdapterCode\(\), binding\.getDevtype\(\)/) |
| | | assert.match(liveService, /source\.currentCycleNo\(\), source\.currentStepNo\(\)/) |
| | | assert.doesNotMatch(liveService, /simulator\.btsTest/) |
| | | assert.match(await readFile(new URL('../../BatteryDetection/battery-device-simulator/src/main/java/com/battery/device/simulator/BtsDevelopmentSimulator.java', import.meta.url), 'utf8'), /new PlanRow\("STEP", index \+ 1, index \+ 1,/) |
| | | |
| | | console.log('Ticket23 实时工步方案与数据列表工步映射回归契约通过') |