| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151 |
- /**
- * 票 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)
|