/** * 票 27 离线回归:面板视图**不得**把 `undefined`/`NaN`/`null` 字面量渲染到界面。 * * 缺陷原样(票 18 真实浏览器取证 2026-10-10,`DATA_SOURCE.REAL` 运行监测): * `已列出 0 个点位 · 本端点序列共 undefined 条(含未关联设备)` * * 根因(两条合起来才成立,缺一条都不会有这句话): * ① `fetchMonitoringDistribution()` 的 `isEmpty` 在真实空态成立 → `sectionFromResult()` * 走空分支,**不带 `data`**(`section.data === null`);外壳把 `section.data` 传给视图; * ② 台账视图的守卫写的是 `pagination.total === null`,而 `data` 为 null 时 * `pagination` 是 `{}`、`total` 是 **`undefined`** —— 守卫漏了这一态, * 模板串把 `undefined` 直出;且 `PanelBlocks.vue` 的 `pageNote` 渲染在 * `hasContent` 分支之前,空态也照画。 * * 本脚本不联网、不启浏览器,用注入的 `fetchImpl` 走**真实消费链** * (service → sectionState → 视图)复现同一形状,并做一次跨全部面板的普查。 * * 运行:node .scratch/cockpit-real-data-wayfinding/verify/t27-ledger-undefined.mjs */ import assert from 'node:assert/strict' import { fetchMonitoringDistribution } from '../../../src/modules/cockpit/services/monitoringApi.js' import { monitoringPointLedgerPanel, toMonitoringDistribution, } from '../../../src/modules/cockpit/domain/monitoringPanels.js' import { implementedPanelIds, panelView } from '../../../src/modules/cockpit/domain/panelViews.js' import { SECTION_STATE, section } from '../../../src/modules/cockpit/domain/sectionState.js' const config = { dataSource: 'real', api: { baseUrl: 'http://acceptance.invalid/api/cockpit/v1' } } const respond = (body) => ({ ok: true, status: 200, json: async () => ({ code: 200, msg: 'success', data: body }) }) let failed = 0 async function check(name, action) { try { await action() console.log(`PASS ${name}`) } catch (error) { failed += 1 console.error(`FAIL ${name}: ${String(error.message).split('\n')[0]}`) } } /** 递归收集视图对象里每个字符串(面板视图全是纯数据,字符串即最终渲染面)。 */ function strings(value, out = []) { if (typeof value === 'string') out.push(value) else if (Array.isArray(value)) for (const item of value) strings(item, out) else if (value && typeof value === 'object') for (const item of Object.values(value)) strings(item, out) return out } /** 界面不得出现这些字面量(缺失一律回占位符,见 `domain/fields.js` 抬头)。 */ const LEAK = /\bundefined\b|\bNaN\b|\bnull\b/ // ================================================================ 一、真实空态端到端 await check('real empty state keeps data null and renders a readable page note', async () => { const result = await fetchMonitoringDistribution({ config, fetchImpl: async () => respond({ pagination: { page: 1, pageSize: 20, total: 0, totalPages: 0, hasMore: false }, monitoringPoints: [], unassociatedEquipment: [], freshness: { status: 'CURRENT' }, }), }) // 前提:空态确实不带 data —— 这正是根因 ① 的现场,把它钉住。 assert.equal(result.state, SECTION_STATE.EMPTY) assert.equal(result.data, null, '空分区不带 data(根因①)') const view = panelView('monitoringPointLedger', result.data, result) assert.ok(!LEAK.test(view.pageNote), `pageNote 不得含字面量:${JSON.stringify(view.pageNote)}`) assert.equal(view.pageNote, '', '分母未知时整句不出(宁可不说,也不报假读数)') assert.equal(view.hasMore, false, 'hasMore 也必须是布尔,不能是 undefined') }) // ================================================================ 二、契约缺口形态(缺 pagination / total 为 null / 空对象) for (const [label, payload] of [ ['missing pagination', { monitoringPoints: [], unassociatedEquipment: [], freshness: { status: 'CURRENT' } }], ['explicit null total', { pagination: { page: 1, pageSize: 20, total: null, totalPages: null, hasMore: false }, monitoringPoints: [], unassociatedEquipment: [], freshness: { status: 'CURRENT' } }], ['empty object payload', {}], ]) { await check(`ledger page note stays clean when ${label}`, async () => { const data = toMonitoringDistribution(payload) const view = monitoringPointLedgerPanel(data, section(SECTION_STATE.SUCCESS, { data })) assert.ok(!LEAK.test(view.pageNote), `pageNote 含字面量:${JSON.stringify(view.pageNote)}`) assert.equal(view.pageNote, '') }) } await check('ledger page note is omitted even when data itself is null', async () => { const view = monitoringPointLedgerPanel(null, section(SECTION_STATE.EMPTY)) assert.equal(view.pageNote, '') assert.equal(view.hasMore, false) }) // ================================================================ 三、有数据时计数仍然正确(修复不能把好路径改坏) await check('ledger page note still reports the real denominator when present', async () => { const data = toMonitoringDistribution({ pagination: { page: 1, pageSize: 3, total: 8, totalPages: 3, hasMore: true }, monitoringPoints: [{ pointCode: 'MP-001', pointName: '供水干管监测点', pointStatus: 'NORMAL' }], unassociatedEquipment: [], freshness: { status: 'CURRENT' }, excluded: {}, }) const view = monitoringPointLedgerPanel(data, section(SECTION_STATE.SUCCESS, { data })) assert.equal(view.pageNote, '已列出 1 个点位 · 本端点序列共 8 条(含未关联设备) · 还有下一页') assert.equal(view.hasMore, true) }) await check('numeric-string denominator from the wire is still accepted', async () => { // BigDecimal 在线上是字符串(仓库既有事实),`numberish` 两种都认 —— 别把字符串误判成缺失。 const data = toMonitoringDistribution({ pagination: { page: 1, pageSize: 20, total: '13', totalPages: '1', hasMore: false }, monitoringPoints: [], unassociatedEquipment: [], freshness: { status: 'CURRENT' }, }) const view = monitoringPointLedgerPanel(data, section(SECTION_STATE.SUCCESS, { data })) assert.ok(view.pageNote.includes('共 13 条'), `字符串分母应照常显示:${view.pageNote}`) }) // ================================================================ 四、跨全部已注册面板的普查 await check('no registered panel view leaks a missing-value literal', async () => { const shapes = { null: null, emptyObject: {}, missingPagination: { points: [], equipment: [], pagination: {} }, nullTotal: { points: [], equipment: [], pagination: { page: 1, pageSize: 20, total: null, totalPages: null, hasMore: false } }, emptyRows: { monitoringPoints: [], unassociatedEquipment: [], abnormalEquipment: [], districtIndustry: [], trend: [], events: [], highlights: [] }, } const leaks = [] for (const panelId of implementedPanelIds()) { for (const [shapeName, data] of Object.entries(shapes)) { for (const state of [SECTION_STATE.EMPTY, SECTION_STATE.SUCCESS, SECTION_STATE.STALE, SECTION_STATE.FAILED]) { const view = panelView(panelId, data, section(state, { data })) if (!view) continue for (const text of strings(view)) { if (LEAK.test(text)) leaks.push(`${panelId}/${shapeName}/${state}: ${JSON.stringify(text)}`) } } } } assert.deepEqual(leaks, [], `有面板把缺失值直出:\n${leaks.join('\n')}`) }) console.log('') console.log(`T27 ledger undefined: ${failed ? `${failed} FAILED` : 'all passed'}`) if (failed) process.exit(1)