/* eslint-env node */ import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' const currentDir = path.dirname(fileURLToPath(import.meta.url)) const rootDir = path.resolve(currentDir, '../../../..') const files = { api: path.resolve(rootDir, 'api/steady/steadyDataView/index.ts'), apiTypes: path.resolve(rootDir, 'api/steady/steadyDataView/interface/index.ts'), page: path.resolve(rootDir, 'views/steady/checksquare/index.vue'), workbench: path.resolve(rootDir, 'views/steady/checksquare/components/ChecksquareWorkbench.vue'), summaryTable: path.resolve(rootDir, 'views/steady/checksquare/components/ChecksquareSummaryTable.vue'), detailPanel: path.resolve(rootDir, 'views/steady/checksquare/components/ChecksquareDetailPanel.vue'), payload: path.resolve(rootDir, 'views/steady/checksquare/utils/checksquarePayload.ts'), table: path.resolve(rootDir, 'views/steady/checksquare/utils/checksquareTable.ts') } const read = file => (exists(file) ? fs.readFileSync(file, 'utf8') : '') const exists = file => fs.existsSync(file) const checks = [ ['checksquare query api exists', () => /querySteadyChecksquare/.test(read(files.api))], [ 'checksquare api posts to expected endpoint', () => /\/steady\/data-view\/checksquare\/query/.test(read(files.api)) ], [ 'checksquare request type uses single lineId', () => /interface SteadyChecksquareQueryParams[\s\S]*lineId: string/.test(read(files.apiTypes)) ], [ 'checksquare request type supports per-order harmonic query only', () => { const typeBlock = read(files.apiTypes).match(/interface SteadyChecksquareQueryParams\s*\{[\s\S]*?\n {4}\}/)?.[0] || '' return /harmonicOrders\?: number\[\]/.test(typeBlock) && !/qualityFlag|statTypes|phases|lineIds/.test(typeBlock) } ], ['workbench component exists', () => exists(files.workbench)], ['summary table component exists', () => exists(files.summaryTable)], ['detail panel component exists', () => exists(files.detailPanel)], ['payload utility exists', () => exists(files.payload)], ['table utility exists', () => exists(files.table)], ['page reuses steady ledger tree', () => /SteadyLedgerTree/.test(read(files.workbench))], ['page reuses shared time period search', () => /TimePeriodSearch/.test(read(files.workbench))], ['payload keeps shared time period unit state', () => /timeUnit:\s*TimePeriodUnit/.test(read(files.payload))], [ 'checksquare time search exposes day week month year custom units', () => /CHECKSQUARE_TIME_PERIOD_UNITS\s*:\s*TimePeriodUnit\[\]\s*=\s*\['day',\s*'week',\s*'month',\s*'year',\s*'custom'\]/.test( read(files.workbench) ) && /:visible-units="CHECKSQUARE_TIME_PERIOD_UNITS"/.test(read(files.workbench)) ], [ 'checksquare defaults to day range', () => /timeRange:\s*buildTimePeriodRange\('day',\s*baseDate\)/.test(read(files.payload)) && /timeUnit:\s*'day'/.test(read(files.payload)) ], ['page no longer tracks floating indicator panel state', () => !/indicatorPanelCollapsed|indicator-panel-collapsed/.test(read(files.page))], [ 'query form uses tree select for steady indicators', () => / /class="toolbar-field toolbar-field--time"[\s\S]*class="toolbar-field indicator-form-item"/.test(read(files.workbench)) ], [ 'query form supports selecting all steady indicators', () => /@click="handleSelectAllIndicators"/.test(read(files.workbench)) && /collectAllIndicatorKeys/.test(read(files.workbench)) ], [ 'checksquare no longer renders floating indicator panel', () => !/SteadyIndicatorFloatingPanel|indicatorPanelCollapsedProxy|is-indicator-expanded/.test(read(files.workbench)) ], ['summary table renders unsupported stats as dash', () => /formatStatMissingRate[\s\S]*'-'/.test(read(files.table))], [ 'summary table has localized AVG MAX MIN CP95 columns', () => /平均值缺失率[\s\S]*最大值缺失率[\s\S]*最小值缺失率[\s\S]*CP95缺失率/.test(read(files.summaryTable)) ], [ 'table utility localizes checksquare stat type names', () => /AVG:\s*'平均值'[\s\S]*MAX:\s*'最大值'[\s\S]*MIN:\s*'最小值'/.test(read(files.table)) ], ['detail panel renders missing segments', () => /segments/.test(read(files.detailPanel))] , [ 'summary table title changed to check result', () => /指标校验结果/.test(read(files.summaryTable)) && !/指标校验总览/.test(read(files.summaryTable)) ], [ 'summary table shows monitor fallback and keeps meta 15px from title', () => { const summaryTable = read(files.summaryTable) return ( /class="summary-meta"/.test(summaryTable) && /result\.lineName\s*\|\|\s*result\.lineId\s*\|\|\s*'未返回监测点'/.test(summaryTable) && /\.summary-meta\s*\{[\s\S]*margin-left:\s*15px/.test(summaryTable) ) } ], [ 'summary table uses tree rows for harmonic results', () => /row-key="itemKey"/.test(read(files.summaryTable)) && /tree-props/.test(read(files.summaryTable)) && /children/.test(read(files.summaryTable)) ], [ 'summary table keeps harmonic tree rows collapsed by default', () => !/default-expand-all/.test(read(files.summaryTable)) ], [ 'summary table removes harmonic order column', () => !/]*prop="harmonicOrder"/.test(read(files.summaryTable)) ], [ 'summary table uses balanced column widths for check result', () => { const summaryTable = read(files.summaryTable) const indicatorColumn = summaryTable.match(/]*prop="indicatorName"[^>]*>/)?.[0] || '' const hasDataColumn = summaryTable.match(/]*prop="hasData"[^>]*>/)?.[0] || '' const missingRateColumn = summaryTable.match(/]*prop="missingRate"[^>]*>/)?.[0] || '' const avgColumn = summaryTable.match(/]*label="平均值缺失率"[^>]*>/)?.[0] || '' const maxColumn = summaryTable.match(/]*label="最大值缺失率"[^>]*>/)?.[0] || '' const minColumn = summaryTable.match(/]*label="最小值缺失率"[^>]*>/)?.[0] || '' const cp95Column = summaryTable.match(/]*label="CP95缺失率"[^>]*>/)?.[0] || '' const maxMissingColumn = summaryTable.match(/]*prop="maxContinuousMissingMinutes"[^>]*>/)?.[0] || '' const operationColumn = summaryTable.match(/]*label="操作"[^>]*>/)?.[0] || '' const stretchColumns = [ hasDataColumn, missingRateColumn, avgColumn, maxColumn, minColumn, cp95Column, maxMissingColumn ] return ( /min-width="208"/.test(indicatorColumn) && /min-width="120"/.test(hasDataColumn) && /min-width="130"/.test(missingRateColumn) && /min-width="130"/.test(avgColumn) && /min-width="130"/.test(maxColumn) && /min-width="130"/.test(minColumn) && /min-width="140"/.test(cp95Column) && /min-width="150"/.test(maxMissingColumn) && /width="96"/.test(operationColumn) && stretchColumns.every(column => /min-width=/.test(column) && !/\swidth=/.test(column)) && stretchColumns.every(column => /align="center"/.test(column)) && /align="center"/.test(operationColumn) && !/align=/.test(indicatorColumn) ) } ], [ 'workbench query card follows steady data view toolbar sizing', () => { const workbench = read(files.workbench) return ( /\.query-card\s*\{[\s\S]*display:\s*grid[\s\S]*grid-template-columns:\s*minmax\(430px,\s*1\.35fr\)\s+minmax\(0,\s*1fr\)\s+auto[\s\S]*gap:\s*10px[\s\S]*align-items:\s*center[\s\S]*padding:\s*12px/.test( workbench ) && /\.checksquare-time\s*\{[\s\S]*flex:\s*1\s+1\s+0[\s\S]*min-width:\s*0/.test(workbench) && /\.checksquare-time\s*:deep\(\.time-period-search__unit\)\s*\{[\s\S]*width:\s*88px[\s\S]*flex:\s*0\s+0\s+88px/.test( workbench ) && /\.checksquare-time\s*:deep\(\.time-period-search__picker\)\s*\{[\s\S]*width:\s*136px[\s\S]*flex:\s*0\s+0\s+136px/.test( workbench ) && /\.query-actions\s*\{[\s\S]*display:\s*flex[\s\S]*justify-content:\s*flex-end[\s\S]*gap:\s*8px/.test(workbench) ) } ], [ 'summary table exposes detail action', () => /详情/.test(read(files.summaryTable)) && /emit\('detail'/.test(read(files.summaryTable)) ], [ 'workbench shows detail in dialog instead of inline panel', () => / /buildPendingChecksquareResult/.test(read(files.page)) && /refreshPendingResult/.test(read(files.page)) ], [ 'page queries indicators sequentially', () => /for \(const indicator of queryIndicators\)/.test(read(files.page)) && /mergeChecksquareIndicatorResult/.test(read(files.page)) ], [ 'page queries harmonic orders with controlled concurrency', () => /CHECKSQUARE_HARMONIC_QUERY_CONCURRENCY\s*=\s*6/.test(read(files.page)) && /runChecksquareHarmonicQuery/.test(read(files.page)) && /workers = Array\.from\(\{[\s\S]*length: Math\.min\(CHECKSQUARE_HARMONIC_QUERY_CONCURRENCY/.test(read(files.page)) && /await Promise\.all\(workers\)/.test(read(files.page)) && /const harmonicOrders = \[\.\.\.CHECKSQUARE_HARMONIC_ORDERS\]/.test(read(files.page)) && /if \(orderIndex >= harmonicOrders\.length\) return/.test(read(files.page)) ], [ 'table pre-creates harmonic rows from second to fiftieth order', () => /CHECKSQUARE_HARMONIC_ORDER_MIN\s*=\s*2/.test(read(files.table)) && /CHECKSQUARE_HARMONIC_ORDER_MAX\s*=\s*50/.test(read(files.table)) && /CHECKSQUARE_HARMONIC_ORDER_MAX - CHECKSQUARE_HARMONIC_ORDER_MIN \+ 1/.test(read(files.table)) && /children: isChecksquareHarmonicIndicator\(indicator\)\s*\?\s*buildPendingChecksquareHarmonicItems/.test( read(files.table) ) ], [ 'table only merges indicators whose harmonic order range intersects second to fiftieth order', () => { const table = read(files.table) return ( /CHECKSQUARE_HARMONIC_ORDER_MIN/.test(table) && /CHECKSQUARE_HARMONIC_ORDER_MAX/.test(table) && /hasChecksquareHarmonicOrderRange/.test(table) && /isChecksquareHarmonicIndicator[\s\S]*hasChecksquareHarmonicOrderRange\(indicator\)/.test(table) && /const shouldMergeHarmonicItems\s*=\s*isChecksquareHarmonicIndicator\(indicator\)/.test(table) && /const normalItems\s*=\s*shouldMergeHarmonicItems[\s\S]*resultItems/.test(table) && /const harmonicItems\s*=\s*shouldMergeHarmonicItems[\s\S]*\[\]/.test(table) ) } ], [ 'table summarizes harmonic parent after all orders finish', () => /buildHarmonicParentSummary/.test(read(files.table)) && /every\(item => isResolvedChecksquareItem\(item\)\)/.test(read(files.table)) && /missingPointCount \/ expectedPointCount/.test(read(files.table)) ], [ 'table marks harmonic parent valid when every order child has data', () => /hasData:\s*children\.every\(item => item\.hasData === true\),/.test(read(files.table)) && !/children\.every\(item => \(item\.missingPointCount \|\| 0\) === 0\)/.test(read(files.table)) ], [ 'table keeps harmonic row keys stable while merging returned order results', () => /normalizeChecksquareResultItemKey/.test(read(files.table)) && /normalizeChecksquareResultItemKey\([\s\S]*child\.itemKey/.test(read(files.table)) && /resolveChecksquareHarmonicOrder/.test(read(files.table)) && /resolveChecksquareHarmonicOrder\(item\) === child\.harmonicOrder/.test(read(files.table)) ], [ 'table formats harmonic parent progress before final summary is ready', () => /resolveChecksquareRowName[\s\S]*getHarmonicProgressText/.test(read(files.table)) && /已完成 \$\{resolvedCount\}\/\$\{totalCount\}/.test(read(files.table)) ], [ 'page keeps selected checksquare detail synced after async row replacement', () => /syncSelectedItemWithLatestResult/.test(read(files.page)) && /selectedItem\.value\.itemKey/.test(read(files.page)) && /mergeChecksquareIndicatorResult\(queryResult\.value/.test(read(files.page)) ] ] const failures = checks.filter(([, check]) => !check()).map(([name]) => name) if (failures.length) { console.error('checksquare feature contract failed:') for (const failure of failures) { console.error(`- ${failure}`) } process.exit(1) } console.log('checksquare feature contract passed')