修复化成报表空字段回显并收敛未知工步提示
直接读取缺失字段以建立响应式依赖,避免首次填写后仍显示空白。未知工步改为数量摘要及分组弹窗,补充真实页面输入、保存、公式和十图交互回归。
| | |
| | | export interface FormationSelection { testId: number; testBatteryId: number } |
| | | 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' | 'calculated'; displayName: string; unit: string; fieldDefinitionId?: number | null } |
| | | export interface FormationChargeStep { stepNo: number; name: string; stepType: string; endVoltage: number | null; capacity?: number | null; direction?: 'CHARGE' | 'DISCHARGE' | 'UNKNOWN' } |
| | | export interface FormationChargeStep { stepNo: number; name: string; stepType: string; endVoltage: number | null; capacity?: number | null; direction?: 'CHARGE' | 'DISCHARGE' | 'REST' | 'UNKNOWN' } |
| | | export interface FormationItem { |
| | | source: FormationCandidate |
| | | cycleNos: number[] |
| | |
| | | /** 报表人工值优先,即使是空字符串也不能回退电池组;基础字段始终按来源列隔离。 */ |
| | | export function formationInputValue(item: FormationItem, field: FormationField, values: Record<string, string>, cycle = 0) { |
| | | const key = formationCellKey(item, cycle, field.key) |
| | | if (Object.hasOwn(values, key)) return values[key]! |
| | | // 直接读取缺失键也要建立响应式依赖,否则首次填写后输入框仍被旧空值覆盖。 |
| | | const override = values[key] |
| | | if (override !== undefined) return override |
| | | if (field.kind !== 'battery') return '' |
| | | return item.businessFields.find(value => field.fieldDefinitionId != null |
| | | ? value.fieldDefinitionId === field.fieldDefinitionId : value.fieldName === field.key)?.fieldValue ?? '' |
| | |
| | | const cycleFields = computed(()=>settings.value.fields.filter(field=>field.scope==='cycle' && !['method','method2'].includes(field.key))) |
| | | const activeCycle = computed({get:()=>settings.value.activeCycle??matrix.value.cycleNos[0]??null,set:(value:number|null)=>{settings.value.activeCycle=value;if(!readonly.value)dirty.value=true}}) |
| | | const visibleMetrics = computed(()=>formationCurveMetrics.filter(metric=>settings.value.metrics?.includes(metric.key))) |
| | | const unknownSteps = computed(()=>matrix.value.items.flatMap(item=>matrix.value.cycleNos.flatMap(cycle=>(item.chargeSteps?.[cycle]??[]).filter(step=>!step.direction || step.direction==='UNKNOWN').map(step=>`${item.source.name}-${item.source.batteryNo} 循环${cycle} 工步${step.stepNo}`))).join(';')) |
| | | const unknownStepsVisible = ref(false) |
| | | // 未知工步按来源、循环汇总,完整明细按需打开,避免长测试挤走报表主体。 |
| | | const unknownStepRows = computed(()=>matrix.value.items.flatMap(item=>item.cycleNos.flatMap(cycle=>{ |
| | | const steps=(item.chargeSteps?.[cycle]??[]).filter(step=>!step.direction || step.direction==='UNKNOWN') |
| | | return steps.length ? [{test:item.source.name,battery:item.source.batteryNo,cycle,count:steps.length,steps:steps.map(step=>`${step.stepNo}(${step.name || step.stepType || '类型缺失'})`).join('、')}] : [] |
| | | }))) |
| | | const unknownStepCount = computed(()=>unknownStepRows.value.reduce((count,row)=>count+row.count,0)) |
| | | let loadSequence = 0, previewSequence = 0 |
| | | const message = (cause: unknown) => cause instanceof Error ? cause.message : '请求失败,请重试' |
| | | |
| | |
| | | </tbody></table></div> |
| | | <div v-if="!readonly" class="report-data-actions"><button class="primary-button" type="button" @click="addReportField"><el-icon><Plus /></el-icon>添加项</button><button class="secondary-button" type="button" @click="openFieldTemplate('battery')">电池组字段模板</button><button class="secondary-button" type="button" @click="openFieldTemplate('special')">选择特殊字段模板</button><button class="secondary-button" type="button" @click="openFieldTemplate('special')">另存特殊字段模板</button></div> |
| | | </section> |
| | | <el-alert v-if="unknownSteps" :title="`以下工步方向未识别,未纳入充放电统计:${unknownSteps}`" type="warning" :closable="false" /> |
| | | <el-alert v-if="unknownStepCount" class="unknown-step-warning" type="warning" :closable="false" show-icon><template #title><span>有 {{ unknownStepCount }} 个工步方向未识别,未纳入充放电统计。</span><el-button link type="warning" @click="unknownStepsVisible=true">查看工步明细</el-button></template></el-alert> |
| | | <section class="cycle-panel"><header class="panel-heading"><div><strong>化成循环明细</strong><span>拐点两项暂为演示占位</span></div></header><el-empty v-if="!matrix.cycleNos.length" description="所选测试暂无已保存循环数据" /><div class="cycle-list"><article v-for="cycle in matrix.cycleNos" :key="cycle" class="cycle-block"><div class="cycle-block-heading"><strong>循环{{ cycle }}</strong></div><div class="cycle-table-wrap"><table class="cycle-matrix" :style="{minWidth:`${Math.max(920,matrix.items.length*170+190)}px`}"><thead><tr><th>指标字段</th><th v-for="item in matrix.items" :key="formationSeriesKey(item)">{{ item.source.name }}<br>{{ item.source.batteryNo }}</th></tr></thead><tbody> |
| | | <tr v-for="field in methodFields" :key="field.key" class="method-row"><td>{{ field.displayName }}</td><td v-for="item in matrix.items" :key="formationSeriesKey(item)"><span v-if="!item.cycleNos.includes(cycle)">无该循环</span><el-input v-else :model-value="inputValue(item,cycle,field)" :disabled="readonly" size="small" maxlength="500" placeholder="请输入" @update:model-value="updateValue(item,cycle,field,$event)" /></td></tr> |
| | | <tr v-for="index in chargeCount(cycle)" :key="`voltage-${index}`"><td>终止电压</td><td v-for="item in matrix.items" :key="formationSeriesKey(item)" :title="voltageTitle(item,cycle,index-1)"><span class="raw-value">{{ !item.cycleNos.includes(cycle)?'无该循环':formationNumberText(formationChargeSteps(item,cycle)[index-1]?.endVoltage) }}</span></td></tr> |
| | |
| | | <footer class="form-actions"><span v-if="saved">V{{ saved.versionNo }}</span><button v-if="saved&&can('query')" class="secondary-button" type="button" :disabled="busy" @click="openHistory">历史版本</button><button v-if="saved&&!readonly&&can('edit')" class="secondary-button" type="button" :disabled="busy||!!error" @click="refreshSource">按最新来源生成</button><button v-if="can('export')" class="secondary-button" type="button" :disabled="busy||!!error" @click="exportPdf"><el-icon><Download /></el-icon>导出PDF</button><button class="secondary-button" type="button" @click="router.push('/report-formation')">{{ readonly?'返回列表':'取消' }}</button><button v-if="!readonly&&can(editingId?'edit':'add')" class="primary-button" type="button" :disabled="busy||!!error||!matrix.items.length" @click="saveReport"><el-icon><Check /></el-icon>{{ saving?'保存中':editingId?'保存修改':'保存报表' }}</button></footer> |
| | | <el-dialog v-model="templateDialogVisible" class="formation-template-dialog" :title="templateMode==='battery'?'选择电池组字段模板':'选择特殊字段模板'" width="720px" align-center><div v-loading="templateLoading" class="battery-template-editor"><label class="battery-template-select"><span>选择模板</span><el-select v-model="templateId" clearable placeholder="自定义" @change="selectTemplate"><el-option v-for="template in templates" :key="template.id" :label="template.name" :value="template.id" /></el-select></label><el-checkbox-group v-model="templateFieldKeys" class="battery-template-fields"><el-checkbox v-for="field in availableFields" :key="field.key" :value="field.key" :disabled="['Z','H','F','A','B'].includes(field.key)">{{ field.displayName }}</el-checkbox></el-checkbox-group></div><template #footer><div class="battery-template-actions"><button class="primary-button" type="button" :disabled="templateLoading" @click="applyFieldTemplate">确认</button><button class="primary-button" type="button" :disabled="templateLoading" @click="saveFieldTemplate()">保存</button><button class="secondary-button" type="button" :disabled="templateLoading" @click="saveFieldTemplate(true)">另存模板</button></div></template></el-dialog> |
| | | <el-dialog v-model="historyVisible" title="历史版本" width="min(800px,calc(100vw - 28px))"><el-table :data="history"><el-table-column prop="versionNo" label="版本" width="80"/><el-table-column prop="operatorName" label="操作人"/><el-table-column label="时间"><template #default="{row}">{{ new Date(row.createdAt).toLocaleString() }}</template></el-table-column><el-table-column prop="changeSummary" label="修改说明"/><el-table-column label="操作" width="100"><template #default="{row}"><el-button link type="primary" @click="viewVersion(row.versionNo)">查看版本</el-button></template></el-table-column></el-table></el-dialog> |
| | | <el-dialog v-model="unknownStepsVisible" title="未识别工步明细" width="min(900px,calc(100vw - 28px))"><el-table :data="unknownStepRows" max-height="480"><el-table-column prop="test" label="测试" min-width="140"/><el-table-column prop="battery" label="电池号" min-width="100"/><el-table-column prop="cycle" label="循环" width="70"/><el-table-column prop="steps" label="工步号(名称或类型)" min-width="220"/></el-table></el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .unknown-step-warning{margin-bottom:16px}.unknown-step-warning .el-button{margin-left:12px} |
| | | .formation-form-page{width:100%;padding-bottom:78px}.page-tools{height:34px;display:flex;align-items:center;justify-content:space-between}.page-tools>span{color:#7f8d99;font-size:10px;font-weight:600}.back-button{display:inline-flex;align-items:center;gap:6px;border:0;color:#52606d;background:transparent;font-size:13px;cursor:pointer}.primary-button,.secondary-button{height:34px;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0 13px;border:1px solid transparent;border-radius:3px;font-size:13px;cursor:pointer;white-space:nowrap}.primary-button{color:#fff;background:var(--blue);border-color:var(--blue)}.secondary-button{color:#52606d;background:#fff;border-color:var(--line-strong)}.report-name-panel,.setup-panel,.cycle-panel,.metrics-panel,.note-panel{background:#fff;border:1px solid #d8e1e9;margin-bottom:16px}.report-name-panel{padding:18px 22px}.report-name-panel label,.note-panel label{display:grid;grid-template-columns:72px minmax(260px,520px);align-items:center;gap:12px}.report-name-panel label>span,.note-panel label>span,.setup-grid label>span{color:#52606d;font-size:12px;font-weight:500}.report-name-panel :deep(.el-input__wrapper),.setup-grid :deep(.el-input__wrapper),.setup-grid :deep(.el-select__wrapper),.note-panel :deep(.el-textarea__inner),.cycle-table :deep(.el-input__wrapper){border-radius:3px;box-shadow:0 0 0 1px #d8e1e9 inset;background:#f8fafc}.panel-heading{min-height:48px;display:flex;align-items:center;justify-content:space-between;padding:0 24px;border-bottom:1px solid var(--line)}.panel-heading>div{display:flex;align-items:center;gap:9px}.panel-heading>strong,.panel-heading>div>strong{color:#26333f;font-size:13px}.panel-heading span{color:#8795a3;font-size:11px;margin-left:10px}.setup-grid{display:grid;grid-template-columns:repeat(2,minmax(240px,1fr));gap:18px;padding:20px 24px 12px}.setup-grid label{display:grid;grid-template-columns:72px minmax(0,1fr);align-items:center;gap:10px}.sample-toolbar{display:flex;align-items:center;justify-content:space-between;padding:4px 24px 10px;color:#52606d;font-size:12px}.sample-list{display:flex;flex-wrap:wrap;gap:10px;padding:0 24px 20px}.sample-chip{display:flex;align-items:center;gap:6px}.sample-chip :deep(.el-select){width:150px}.delete-link{border:0;color:#c45454;background:transparent;font-size:12px;cursor:pointer}.cycle-tabs{display:flex;gap:0;padding:16px 24px 0}.cycle-tabs button{min-width:84px;height:30px;color:#1677dd;background:#fff;border:1px solid #78b2f9;font-size:12px;cursor:pointer}.cycle-tabs button:first-child{border-radius:3px 0 0 3px}.cycle-tabs button:last-child{border-radius:0 3px 3px 0}.cycle-tabs button+button{border-left:0}.cycle-tabs button.active{color:#fff;background:#2188f5}.cycle-table-wrap{overflow-x:auto;padding:14px 24px 22px}.cycle-table{width:100%;min-width:900px;border-collapse:collapse;color:#52606d;font-size:12px}.cycle-table th,.cycle-table td{height:42px;padding:0 10px;border:1px solid #cbd8e4;text-align:center;white-space:nowrap}.cycle-table th{color:#fff;background:#3c6497;font-weight:600}.cycle-table th:nth-child(1){width:90px}.cycle-table th:nth-child(2){width:240px}.cycle-table th:nth-child(3){width:150px}.cycle-table th:nth-child(4),.cycle-table th:nth-child(5){width:130px}.charts-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;padding:18px 24px 24px}.chart-legend-bar{display:flex;align-items:center;gap:20px;padding:12px 24px;border-bottom:1px solid var(--line);color:#52606d;font-size:12px}.legend-item{display:inline-flex;align-items:center;gap:7px}.legend-color{width:14px;height:14px;border-radius:2px}.chart-card{border:1px solid #d8e1e9;background:#fff}.chart-card header{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;color:#26333f;font-size:12px}.chart-card header span{color:#8795a3;font-size:10px}.metric-chart{width:100%;height:230px}.note-panel{padding:18px 24px}.note-panel :deep(.el-textarea){min-width:260px}.form-actions{position:sticky;bottom:0;z-index:4;display:flex;justify-content:flex-end;gap:10px;padding:14px 0;background:rgba(244,248,251,.96);border-top:1px solid #d8e1e9}@media(max-width:900px){.setup-grid,.charts-grid{grid-template-columns:1fr}.report-name-panel label,.note-panel label{grid-template-columns:1fr}.report-name-panel label>span,.note-panel label>span{margin-bottom:6px}.form-actions{position:static}} |
| | | .cycle-matrix{width:100%;border-collapse:collapse;color:#52606d;font-size:12px}.cycle-matrix th,.cycle-matrix td{height:40px;padding:0 10px;border:1px solid #cbd8e4;text-align:center;white-space:nowrap}.cycle-matrix th{color:#fff;background:#3c6497;font-weight:600}.cycle-matrix th:first-child{width:190px}.cycle-matrix th:last-child{width:90px}.cycle-matrix td:first-child{color:#31404d;background:#f2f7fb;font-weight:500;text-align:left}.cycle-matrix td:last-child{color:#8795a3;background:#fafcfe}.cycle-matrix :deep(.el-input__wrapper){border-radius:3px;box-shadow:0 0 0 1px #d8e1e9 inset;background:#f8fafc}.method-row td:first-child{border-left:3px solid #2188f5}.raw-value{color:#405261;font-family:'Cascadia Mono',Consolas,monospace}.auto-value{color:#2188f5;font-weight:500}.cycle-summary{display:flex;align-items:center;gap:28px;margin:0 24px 20px;padding:12px 14px;border:1px solid #d8e1e9;background:#f7fafc}.cycle-summary>div{display:flex;flex-direction:column;gap:3px}.cycle-summary span{color:#8795a3;font-size:10px}.cycle-summary strong{color:#33414e;font-size:12px}.cycle-summary>button{margin-left:auto}@media(max-width:900px){.cycle-summary{align-items:flex-start;flex-wrap:wrap}.cycle-summary>button{margin-left:0}} |
| | | .report-data-panel{background:#fff;border:1px solid #d8e1e9;margin-bottom:16px}.report-data-meta{display:flex;align-items:center;gap:12px}.report-data-meta label{display:flex;align-items:center;gap:6px;color:#52606d;font-size:11px;font-weight:500}.report-data-meta :deep(.el-select){width:190px}.report-data-meta :deep(.el-input){width:150px}.report-data-meta :deep(.el-input__wrapper),.report-data-meta :deep(.el-select__wrapper){min-height:30px;border-radius:3px;box-shadow:0 0 0 1px #d8e1e9 inset;background:#f8fafc}.report-data-table-wrap{overflow-x:auto;padding:18px 24px 0}.report-data-table{width:100%;border-collapse:collapse;color:#52606d;font-size:12px}.report-data-table th,.report-data-table td{height:38px;padding:0 10px;border:1px solid #cbd8e4;text-align:center;white-space:nowrap}.report-data-table th{color:#fff;background:#3c6497;font-weight:600}.report-data-table th:first-child,.report-data-table td:first-child{width:150px}.report-data-table td:first-child{color:#31404d;background:#f7fafc;font-weight:500}.report-data-table td:last-child,.report-data-table th:last-child{width:90px;color:#8795a3;background:#fafcfe}.report-data-table :deep(.el-select){width:100px}.report-data-table :deep(.el-select__wrapper),.report-data-table :deep(.el-input__wrapper){min-height:28px;border-radius:3px;box-shadow:0 0 0 1px #d8e1e9 inset;background:#fff}.group-index{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;margin-right:8px;border:1px solid #cbd8e4;border-radius:50%;color:#647380;background:#fff;font-size:10px}.report-data-actions{display:flex;align-items:center;gap:10px;padding:18px 24px 20px}.report-data-actions .primary-button{height:30px;padding:0 12px;font-size:12px}@media(max-width:900px){.report-data-meta{align-items:flex-start;flex-direction:column;gap:6px}.report-data-actions{flex-wrap:wrap}} |
| New file |
| | |
| | | import assert from 'node:assert/strict' |
| | | import { before, after, test } from 'node:test' |
| | | import { build } from 'vite' |
| | | import vue from '@vitejs/plugin-vue' |
| | | import { chromium } from '@playwright/test' |
| | | import { fileURLToPath } from 'node:url' |
| | | import { mkdir } from 'node:fs/promises' |
| | | import { join } from 'node:path' |
| | | import load from './load-typescript.cjs' |
| | | |
| | | const root = fileURLToPath(new URL('../', import.meta.url)).replaceAll('\\', '/') |
| | | const { defaultFormationFields } = load(new URL('../src/utils/formationReportFields.ts', import.meta.url)) |
| | | let browser, bundle, css |
| | | // 挂载真实页面及 Element Plus,仅替换 API 边界;不启动服务、不触碰用户报表。 |
| | | before(async () => { |
| | | const apiNames = ['getFormationSources','previewFormationReport','getFormationReport','createFormationReport','editFormationReport','refreshFormationSource','listFormationVersions','listFormationTemplates','createFormationTemplate','updateFormationTemplate'] |
| | | const virtual = { |
| | | 'formation-test-entry': ` |
| | | import {createApp} from 'vue' |
| | | import {createRouter,createMemoryHistory} from 'vue-router' |
| | | import ElementPlus from 'element-plus' |
| | | import 'element-plus/dist/index.css' |
| | | import * as icons from '@element-plus/icons-vue' |
| | | import zhCn from 'element-plus/es/locale/lang/zh-cn' |
| | | import '${root}/src/style.css' |
| | | import Form from '${root}/src/views/FormationReportForm.vue' |
| | | const router=createRouter({history:createMemoryHistory(),routes:[{path:'/report-formation/:id/edit',component:Form}]}) |
| | | router.push('/report-formation/1/edit').then(()=>{ |
| | | const app=createApp(Form) |
| | | app.use(router).use(ElementPlus,{locale:zhCn}) |
| | | for(const [name,component] of Object.entries(icons))app.component(name,component) |
| | | app.mount('#app') |
| | | })`, |
| | | '@/api/auth': `export async function getUserInfo(){return {permissions:['*:*:*']}}`, |
| | | '@/api/formationReports': apiNames.map(name=>`export async function ${name}(...args){return window.reportApi('${name}',args)}`).join('\n'), |
| | | '@/utils/formationSavedReportPrint': 'export async function openFormationSavedReportPrint(){}', |
| | | } |
| | | const output = await build({configFile:false,root,define:{'process.env.NODE_ENV':JSON.stringify('production')},logLevel:'error',plugins:[{name:'formation-test-api',enforce:'pre',resolveId(id){if(id.replaceAll('\\','/').endsWith('/formation-test-entry'))id='formation-test-entry';if(id in virtual)return '\0'+id},load(id){return virtual[id.slice(1)]}},vue()],resolve:{alias:[{find:/^@\/(?!api\/auth$|api\/formationReports$|utils\/formationSavedReportPrint$)/,replacement:root+'/src/'}]},build:{write:false,minify:false,lib:{entry:'formation-test-entry',formats:['iife'],name:'FormationTest'}}}) |
| | | const files = (Array.isArray(output)?output[0]:output).output |
| | | bundle = files.find(file=>file.type==='chunk').code |
| | | css = files.filter(file=>file.fileName.endsWith('.css')).map(file=>file.source).join('\n') |
| | | browser = await chromium.launch({channel:'chrome',headless:true}) |
| | | }) |
| | | after(async()=>browser?.close()) |
| | | |
| | | function fixture(cycles=1) { |
| | | const cycleNos=Array.from({length:cycles},(_,i)=>i+1) |
| | | return {id:1,name:'化成编辑回归',versionNo:1,description:'',calculatedValues:{},settings:{fields:defaultFormationFields,values:{},metrics:[],chargeStepSelections:{},activeCycle:1},cycleNos, |
| | | items:Array.from({length:4},(_,i)=>({source:{selection:{testId:i<2?1:2,testBatteryId:i+1},name:i<2?'化成甲':'化成乙',batteryGroup:'电池组',batteryNo:`电池${i%2+1}`},cycleNos,metrics:{},businessFields:[{fieldName:'F',fieldValue:'2'}],chargeSteps:Object.fromEntries(cycleNos.map(cycle=>[cycle,Array.from({length:4},(_,n)=>({stepNo:(cycle-1)*4+n+1,name:'未识别',stepType:'unknown',direction:'UNKNOWN',endVoltage:null}))])),curves:{}}))} |
| | | } |
| | | async function mount(record) { |
| | | const page=await browser.newPage({viewport:{width:1600,height:1000}}), errors=[], calls=[] |
| | | page.setDefaultTimeout(5000) |
| | | page.on('pageerror',error=>errors.push(error.message)) |
| | | await page.exposeFunction('reportApi',async(name,args)=>{ |
| | | calls.push({name,args}) |
| | | if(name==='editFormationReport'){record={...record,...args[1],versionNo:record.versionNo+1};return record} |
| | | if(name==='getFormationReport')return record |
| | | if(name==='listFormationVersions')return [] |
| | | throw new Error('意外请求:'+name) |
| | | }) |
| | | await page.setContent('<div id="app"></div>') |
| | | await page.addStyleTag({content:css}) |
| | | await page.addScriptTag({content:bundle}) |
| | | await page.locator('.report-data-table input').first().waitFor({timeout:5000}).catch(async cause=>{throw new Error(`${cause.message}; 页面: ${await page.locator('body').innerText()}; 异常: ${errors.join(';')}; 请求: ${calls.map(call=>call.name).join(',')}`)}) |
| | | return {page,errors,calls} |
| | | } |
| | | const detailInput=(page,key,column)=>page.locator('.report-data-table tbody tr').filter({has:page.locator('td:first-child',{hasText:new RegExp(`^${key}$`)})}).locator('input').nth(column) |
| | | async function screenshot(page,name){ |
| | | const directory=process.env.FORMATION_REPRO_ARTIFACT_DIR |
| | | if(directory){await mkdir(directory,{recursive:true});await page.screenshot({path:join(directory,name+'.png'),animations:'disabled'})} |
| | | } |
| | | |
| | | test('空字段逐格输入后立即回显,连续编辑不丢字,清空不回退,保存保留各来源值',async()=>{ |
| | | const {page,errors,calls}=await mount(fixture()) |
| | | try { |
| | | for(const key of ['Z','H','A','B'])for(let col=0;col<4;col++){ |
| | | const input=detailInput(page,key,col) |
| | | await input.fill('12.5') |
| | | await page.waitForTimeout(30) |
| | | assert.equal(await input.inputValue(),'12.5',`${key} 第${col+1}列输入后丢失`) |
| | | } |
| | | const input=detailInput(page,'Z',0) |
| | | await input.fill('');await input.pressSequentially('3.25');await input.blur() |
| | | assert.equal(await input.inputValue(),'3.25') |
| | | const f=detailInput(page,'F',0);await f.fill('');await f.blur() |
| | | assert.equal(await f.inputValue(),'') |
| | | await page.getByRole('button',{name:'保存修改',exact:true}).click() |
| | | await page.locator('.el-message-box input').fill('逐格输入回归') |
| | | await page.locator('.el-message-box').getByRole('button',{name:/确定|OK/}).click() |
| | | await page.waitForFunction(()=>document.querySelector('.is-readonly')) |
| | | const payload=calls.find(call=>call.name==='editFormationReport').args[1] |
| | | assert.equal(payload.settings.values['1|1|0|Z'],'3.25') |
| | | assert.equal(payload.settings.values['1|1|0|F'],'') |
| | | assert.equal(payload.settings.values['2|4|0|B'],'12.5') |
| | | assert.equal(await detailInput(page,'A',3).inputValue(),'12.5') |
| | | assert.deepEqual(errors,[]) |
| | | }finally{await page.close()} |
| | | }) |
| | | test('33循环四电池的未知工步提示默认保持紧凑,详情仍可查看',async()=>{ |
| | | const {page,errors}=await mount(fixture(33)) |
| | | try { |
| | | const warning=page.locator('.el-alert--warning') |
| | | const box=await warning.boundingBox() |
| | | assert.ok(box.height<=120,`工步提示占高 ${box.height}px,遮蔽后续明细`) |
| | | assert.ok((await warning.innerText()).includes('528')) |
| | | await warning.scrollIntoViewIfNeeded() |
| | | await screenshot(page,'compact-warning') |
| | | await page.getByText('查看工步明细',{exact:true}).click() |
| | | await page.getByRole('dialog').waitFor() |
| | | assert.ok((await page.getByRole('dialog').innerText()).includes('132')) |
| | | await screenshot(page,'unknown-step-details') |
| | | assert.deepEqual(errors,[]) |
| | | }finally{await page.close()} |
| | | }) |
| | | |
| | | test('循环人工项逐列隔离并联动公式,静置不误报,十图切换与保存值一致',async()=>{ |
| | | const record=fixture(2) |
| | | record.settings.metrics=['voltage','current','positiveVoltage','negativeVoltage','temperature'] |
| | | for(const item of record.items){ |
| | | item.businessFields=Object.entries({Z:'3',H:'2',F:'5',A:'4',B:'6'}).map(([fieldName,fieldValue])=>({fieldName,fieldValue})) |
| | | for(const cycle of record.cycleNos){ |
| | | item.chargeSteps[cycle]=[{stepNo:1,name:'恒流充电',direction:'CHARGE',endVoltage:4.2},{stepNo:2,name:'搁置',direction:'REST'},{stepNo:3,name:'恒流放电',direction:'DISCHARGE',capacity:0.25}] |
| | | item.curves[cycle]={zeroTime:1000,records:[{sourceType:'SNAPSHOT',stepNo:1,deviceTime:1000,voltage:4.2,current:1,temperature:25,positivePotential:2,negativePotential:-1},{sourceType:'SNAPSHOT',stepNo:2,deviceTime:2000,voltage:99,current:99},{sourceType:'SNAPSHOT',stepNo:3,deviceTime:3000,voltage:1,current:-1,temperature:26,positivePotential:1,negativePotential:-1}]} |
| | | } |
| | | } |
| | | const {page,errors,calls}=await mount(record) |
| | | const row=(cycle,label)=>page.locator('.cycle-block').nth(cycle-1).locator('tbody tr').filter({has:page.locator('td:first-child',{hasText:new RegExp(`^${label}$`)})}) |
| | | try{ |
| | | assert.equal(await page.locator('.el-alert--warning').count(),0) |
| | | assert.equal(await page.locator('.chart-card canvas').count(),10) |
| | | for(let cycle=1;cycle<=2;cycle++)for(let col=0;col<4;col++){ |
| | | const input=row(cycle,'数字').locator('input').nth(col) |
| | | await input.fill(String(cycle+col));await input.blur() |
| | | assert.equal(await input.inputValue(),String(cycle+col)) |
| | | const method=row(cycle,'测试方法').locator('input').nth(col) |
| | | await method.fill(`循环${cycle}列${col}`);await method.blur() |
| | | assert.equal(await method.inputValue(),`循环${cycle}列${col}`) |
| | | } |
| | | assert.equal(await row(1,'每克N充电量').locator('td').nth(1).innerText(),'120') |
| | | assert.equal(await row(1,'每克F充电量').locator('td').nth(1).innerText(),'144') |
| | | assert.equal(await row(1,'1.0VF完整容量').locator('td').nth(1).innerText(),'50') |
| | | await detailInput(page,'F',0).fill('10');await detailInput(page,'F',0).blur() |
| | | assert.equal(await row(1,'每克N充电量').locator('td').nth(1).innerText(),'240') |
| | | assert.equal(await row(1,'1.0VF完整容量').locator('td').nth(1).innerText(),'25') |
| | | await page.locator('.cycle-chart-tabs').getByRole('button',{name:'循环2',exact:true}).click() |
| | | assert.equal(await page.locator('.cycle-chart-tabs .active').innerText(),'循环2') |
| | | await screenshot(page,'cycle-charts') |
| | | await page.getByRole('button',{name:'保存修改',exact:true}).click() |
| | | await page.locator('.el-message-box input').fill('人工项与曲线回归') |
| | | await page.locator('.el-message-box').getByRole('button',{name:/确定|OK/}).click() |
| | | await page.waitForFunction(()=>document.querySelector('.is-readonly')) |
| | | const values=calls.find(call=>call.name==='editFormationReport').args[1].settings.values |
| | | assert.equal(values['1|1|1|manual'],'1') |
| | | assert.equal(values['2|4|2|manual'],'5') |
| | | assert.equal(values['2|4|2|method'],'循环2列3') |
| | | assert.deepEqual(errors,[]) |
| | | }finally{await page.close()} |
| | | }) |