| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404 |
- /**
- * 票 60 离线闸门:统一分页批次二 —— 点位摘要两级清单(设备卡片、关联设备图层清单、未命中子清单)。
- *
- * 五组断言(不启浏览器、不联网;渲染层用 Vue 自定义渲染器在纯对象树上跑真实 patch,t64 成例):
- * S. SSR/静态守卫:PagerNav 的 showCount 默认 true(票 46/59 既有形态不变)、计数行是唯一受控块;
- * 点位卡三处接线 :show-count="false" 且零第二份翻页算术;设备详情侧未被牵连。
- * R. 真实渲染(行为级):每页 ≤10;窗口逐条对内容;三清单页码互相独立;
- * pageCount<=1 不画控件;到边禁用保占位;页码直达/跳页夹回;空态文案沿用既有。
- * K. 票 42 口径不变:store 仍持全部编码(含待确认/未命中),总数只认快照口径;
- * DOM 窗口化后总数/归类计数/图例纹丝不动(互斥归类之和仍等于全量行数)。
- * Z. 复位与保持(条文 5/6):换点位/换设备/返回第一级 → 页码回第 1 页;
- * 同点位重进与数据刷新(selection 换了对象但标量键同值)→ **不**复位;越界夹回最后一页。
- * C. SFC 真实编译(t41/59 教训:拼错的绑定只有加载层判得出)。
- *
- * 变异有牙见 t60-mutations.mjs。
- * 运行:node .scratch/cockpit-real-data-wayfinding/verify/t60-point-detail-lists.mjs
- */
- import assert from 'node:assert/strict'
- import { readFileSync } from 'node:fs'
- import { registerHooks } from 'node:module'
- import { parse, compileScript, compileTemplate } from '@vue/compiler-sfc'
- import { createRenderer, h, nextTick } from '@vue/runtime-core'
- import { createPinia, setActivePinia } from 'pinia'
- const SRC = '../../../src/modules/cockpit/'
- const read = (rel) => readFileSync(new URL(SRC + rel, import.meta.url), 'utf8')
- const stripComments = (text) => text.replace(/<!--[\s\S]*?-->/g, '').replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1')
- // runtime-dom 的 v-model 在 beforeUpdate 读 document.activeElement;自定义渲染器的宿主是纯对象树。
- if (typeof globalThis.document === 'undefined') globalThis.document = { activeElement: null }
- let passed = 0
- let failed = 0
- function check(name, condition, detail = '') {
- if (condition) { passed += 1; console.log(`PASS ${name}`) }
- else { failed += 1; console.error(`FAIL ${name}: ${detail}`) }
- }
- // .vue 编译钩子:SSR 默认版 + `?client` 版(子导入按 parentURL 传播,t64 实测成例)。
- registerHooks({
- resolve(specifier, context, nextResolve) {
- const resolved = nextResolve(specifier, context)
- const parent = context.parentURL || ''
- if (parent.endsWith('.vue?client') && resolved.url.endsWith('.vue')) {
- return { url: `${resolved.url}?client`, format: 'module', shortCircuit: true }
- }
- return resolved
- },
- load(url, context, nextLoad) {
- if (url.endsWith('.json')) return { format: 'module', shortCircuit: true, source: `export default ${readFileSync(new URL(url), 'utf8')}` }
- const isClient = url.endsWith('.vue?client')
- if (url.endsWith('.vue') || isClient) {
- const path = isClient ? url.slice(0, -'?client'.length) : url
- const source = readFileSync(new URL(path), 'utf8')
- const { descriptor } = parse(source, { filename: path })
- const script = compileScript(descriptor, { id: 't60', genDefaultAs: '__sfc__' })
- const template = compileTemplate({
- source: descriptor.template.content,
- filename: path,
- id: 't60',
- ssr: !isClient,
- compilerOptions: { bindingMetadata: script.bindings },
- })
- assert.deepEqual(template.errors, [], `${path} template`)
- const tail = isClient ? '__sfc__.render = render; export default __sfc__;' : '__sfc__.ssrRender = ssrRender; export default __sfc__;'
- return { format: 'module', shortCircuit: true, source: `${script.content}\n${template.code}\n${tail}` }
- }
- return nextLoad(url, context)
- },
- })
- const pinia = createPinia()
- setActivePinia(pinia)
- const config = { dataSource: 'mock', api: { baseUrl: '', token: '' } }
- const { useDetailStore } = await import(`${SRC}stores/detailStore.js`)
- const { useMapStore } = await import(`${SRC}stores/mapStore.js`)
- const { useDeviceLayerStore } = await import(`${SRC}stores/deviceLayerStore.js`)
- const { registerMockTransport, resetMockTransportsForTest } = await import(`${SRC}services/cockpitApi.js`)
- const { default: ClientCard } = await import(`${SRC}components/PointDetailCard.vue?client`)
- // ================================================================ 场景数据(真实链路灌入,不喂视图半成品)
- const VALIDATION = { state: 'VALID', reason: '', crs: 'WGS84', renderable: true }
- // P-MANY:快照 15 台 + 详情独有 12 台(筛选生效 → 未命中)→ 卡片 27、命中清单 15、未命中 12。
- const SNAPSHOT_15 = Array.from({ length: 15 }, (_, i) => `EQ-${String(i + 1).padStart(2, '0')}`)
- const DETAIL_27 = Array.from({ length: 27 }, (_, i) => `EQ-${String(i + 1).padStart(2, '0')}`)
- const deviceRaw = (code) => ({
- deviceCode: code,
- deviceName: `设备${code}`,
- deviceTypeName: '压力计',
- deviceStatus: 'NORMAL',
- statusLabel: '正常',
- longitude: 110.4,
- latitude: 28.44,
- coordinateValidation: VALIDATION,
- })
- const detailPayload = (pointCode, codes) => ({
- monitoringPointCode: pointCode,
- monitoringPointName: `${pointCode} 点位`,
- pointStatus: 'NORMAL',
- statusLabel: '正常',
- devices: codes.map(deviceRaw),
- deviceStatistics: { totalDeviceCount: codes.length, normalDeviceCount: codes.length },
- coordinateValidation: VALIDATION,
- })
- const snapshotPayload = (pointCode, codes, extraPoints = []) => ({
- page: 1, pageSize: 50, total: 1 + extraPoints.length,
- complete: true, snapshotAt: '2026-10-20T10:00:00+08:00',
- monitoringPoints: [{
- pointCode, pointName: `${pointCode} 点位`, pointStatus: 'NORMAL',
- deviceCodes: codes, longitude: 110.4, latitude: 28.44, coordinateValidation: VALIDATION,
- }, ...extraPoints],
- unassociatedEquipment: [],
- })
- const detail = useDetailStore()
- detail.configure({ config })
- const map = useMapStore()
- map.setup({ config })
- const layer = useDeviceLayerStore()
- resetMockTransportsForTest()
- registerMockTransport('/map-snapshot', () => snapshotPayload('P-MANY', SNAPSHOT_15, [
- {
- pointCode: 'P-FEW', pointName: 'P-FEW 点位', pointStatus: 'NORMAL',
- deviceCodes: ['EQ-01', 'EQ-02'], longitude: 110.5, latitude: 28.45, coordinateValidation: VALIDATION,
- },
- {
- pointCode: 'P-ZERO', pointName: 'P-ZERO 点位', pointStatus: 'EMPTY',
- deviceCodes: [], longitude: 110.6, latitude: 28.46, coordinateValidation: VALIDATION,
- },
- ]))
- registerMockTransport('/monitoring-points/P-MANY', () => detailPayload('P-MANY', DETAIL_27))
- registerMockTransport('/monitoring-points/P-FEW', () => detailPayload('P-FEW', ['EQ-01', 'EQ-02']))
- registerMockTransport('/monitoring-points/P-ZERO', () => detailPayload('P-ZERO', []))
- await map.advance()
- await detail.openPoint('P-MANY')
- // 票 14:设备级筛选生效(快照是命中子集)→ 详情独有落「未命中」,展开子清单才有得翻。
- layer.syncForSelection({ force: true, mode: 'overview', filterActive: true })
- // ================================================================ 假 DOM 渲染器(t64 成例)
- const nodeOps = {
- createElement: (tag) => ({ tag, props: {}, children: [], parent: null, text: '', style: {}, addEventListener() {}, removeEventListener() {} }),
- createText: (text) => ({ tag: '#text', text, children: [], parent: null, props: {}, style: {}, addEventListener() {}, removeEventListener() {} }),
- createComment: (text) => ({ tag: '#comment', text, children: [], parent: null, props: {}, style: {}, addEventListener() {}, removeEventListener() {} }),
- setText: (node, text) => { node.text = text },
- setElementText: (node, text) => { node.text = text; node.children = [] },
- insert: (child, parent, anchor) => {
- if (child.parent) child.parent.children = child.parent.children.filter((c) => c !== child)
- child.parent = parent
- if (!parent) return
- if (anchor) { const i = parent.children.indexOf(anchor); parent.children.splice(i < 0 ? parent.children.length : i, 0, child) }
- else parent.children.push(child)
- },
- remove: (child) => { if (child.parent) child.parent.children = child.parent.children.filter((c) => c !== child); child.parent = null },
- parentNode: (node) => node.parent,
- nextSibling: (node) => (node.parent ? node.parent.children[node.parent.children.indexOf(node) + 1] || null : null),
- querySelector: () => null,
- setScopeId: () => {},
- patchProp: (el, key, _prev, next) => { el.props[key] = next },
- }
- function findAll(node, predicate, acc = []) {
- if (!node) return acc
- if (predicate(node)) acc.push(node)
- for (const child of node.children || []) findAll(child, predicate, acc)
- return acc
- }
- function findNode(node, predicate) {
- if (!node) return null
- if (predicate(node)) return node
- for (const child of node.children || []) { const hit = findNode(child, predicate); if (hit) return hit }
- return null
- }
- function textOf(node) {
- if (!node) return ''
- const own = node.tag === '#text' ? node.text : typeof node.text === 'string' ? node.text : ''
- return own + (node.children || []).map(textOf).join('')
- }
- const byAttr = (name) => (node) => !!node.props && name in node.props
- const pagerWrap = (which) => findNode(container, (n) => String(n.props && n.props['data-point-pager']) === which)
- const app = createRenderer(nodeOps).createApp({ render: () => h(ClientCard, { locationReason: '' }) })
- app.config.warnHandler = () => {}
- app.use(pinia)
- const container = nodeOps.createElement('#root')
- app.mount(container)
- const cardCodes = () => findAll(container, byAttr('data-device-card')).map((n) => n.props['data-device-code'])
- const cardIndexAttr = () => findAll(container, byAttr('data-device-card')).map((n) => Number(n.props['data-device-card']))
- const layerCodes = () => findAll(container, (n) => 'data-device-layer-code' in (n.props || {}) && n.props['data-device-layer-row'] !== 'unmatched').map((n) => n.props['data-device-layer-code'])
- const unmatchedCodes = () => findAll(container, (n) => n.props && n.props['data-device-layer-row'] === 'unmatched').map((n) => n.props['data-device-layer-code'])
- const pagers = () => findAll(container, byAttr('data-pager'))
- const clickPage = async (which, n) => {
- const wrap = pagerWrap(which)
- assert.ok(wrap, `pager 包装层 ${which} 不在 DOM`)
- const btn = findNode(wrap, (node) => String(node.props && node.props['data-page-number']) === String(n))
- assert.ok(btn && typeof btn.props.onClick === 'function', `pager ${which} 的页码按钮 ${n} 不在`)
- btn.props.onClick(new Proxy({}, { get: () => undefined }))
- await nextTick()
- }
- const clickPrevNext = async (which, attr) => {
- const btn = findNode(pagerWrap(which), byAttr(attr))
- assert.ok(btn && typeof btn.props.onClick === 'function', `${which} 缺 ${attr}`)
- btn.props.onClick(new Proxy({}, { get: () => undefined }))
- await nextTick()
- }
- // ================================================================ R. 真实渲染(行为级)
- {
- // R1 设备卡片首屏只画 10 张(27 条 → 条文 1)。
- check('R1 设备卡片每页 ≤10(27 条首屏 10 张)', cardCodes().length === 10, `got=${cardCodes().length}`)
- // R2 关联清单首屏 10 行(命中 15 台)。
- check('R2 关联设备清单每页 ≤10(15 台首屏 10 行)', layerCodes().length === 10, `got=${layerCodes().length}`)
- // R3 三处 pager 都在(cards/layer 两页起步;unmatched 折叠未展开 ⇒ 其 pager 随折叠不存在)。
- check('R3 两清单的 pager 都接线', !!pagerWrap('cards') && !!pagerWrap('layer') && pagers().length === 2, `pagers=${pagers().length}`)
- // R4 计数行不画(维护者裁定 2):三处 pager 里都没有 data-page-state。
- const stateNodes = pagers().flatMap((p) => findAll(p, byAttr('data-page-state')))
- check('R4 本组件 pager 一律不画计数行(无第二个总数口径)', stateNodes.length === 0, `state=${stateNodes.length}`)
- // R5 到边禁用保占位:第 1 页「上一页」disabled 但节点在。
- const prev = findNode(pagerWrap('cards'), byAttr('data-page-prev'))
- check('R5 到边按钮 disabled 且保留占位', !!prev && prev.props.disabled === true, `prev=${prev && prev.props.disabled}`)
- // R6 点页码 2 → 窗口真的移动(逐条对编码,不是只数卡片)。
- await clickPage('cards', 2)
- check('R6 卡片点页码 2 → 画第 11–20 台(逐条对编码)',
- cardCodes().join(',') === DETAIL_27.slice(10, 20).join(','), cardCodes().join(','))
- // R7 独立页码:卡片翻页不牵连关联清单(它还在第 1 页)。
- check('R7 两清单页码互相独立', layerCodes().join(',') === SNAPSHOT_15.slice(0, 10).join(','), layerCodes().join(','))
- // R8 data-device-card 下标是**全局位置**(走查/探针按它取第 N 台,翻页不得让每页都从 0 数)。
- check('R8 卡片 data-device-card 续全局下标(第 2 页是 10..19)',
- cardIndexAttr().join(',') === '10,11,12,13,14,15,16,17,18,19', cardIndexAttr().join(','))
- // R9 关联清单翻页:第 2 页只剩余数 5 行(不补空行)。
- await clickPage('layer', 2)
- check('R9 关联清单末页只画余数', layerCodes().join(',') === SNAPSHOT_15.slice(10, 15).join(','), layerCodes().join(','))
- // R10 末页「下一页」disabled 且仍在。
- const nextOnLast = findNode(pagerWrap('layer'), byAttr('data-page-next'))
- check('R10 末页下一页 disabled 保占位', !!nextOnLast && nextOnLast.props.disabled === true, `next=${nextOnLast && nextOnLast.props.disabled}`)
- // R11 上一页走同一出口:layer 从 2 回 1。
- await clickPrevNext('layer', 'data-page-prev')
- check('R11 上一页回到第 1 页窗口', layerCodes().join(',') === SNAPSHOT_15.slice(0, 10).join(','), layerCodes().join(','))
- }
- // ================================================================ T. 未命中子清单(裁定 1:第三套独立页码)
- {
- const toggle = findNode(container, byAttr('data-device-more-toggle'))
- assert.ok(toggle && typeof toggle.props.onClick === 'function', '「另有 N 台」入口不在 DOM')
- check('T1 折叠钮文案仍是全量 12 台', textOf(toggle).includes('另有 12 台'), textOf(toggle))
- toggle.props.onClick(new Proxy({}, { get: () => undefined }))
- await nextTick()
- // T2 展开 → 第一页 ≤10 且独立第三套 pager 出现。
- check('T2 未命中子清单每页 ≤10(12 条 → 首屏 10 行)', unmatchedCodes().length === 10, `got=${unmatchedCodes().length}`)
- check('T3 第三套独立页码就位', !!pagerWrap('unmatched') && pagers().length === 3, `pagers=${pagers().length}`)
- // T4 翻第 2 页 → 余数 2 行;前两清单窗口不受牵连。
- const cardsBefore = cardCodes().join(',')
- const layerBefore = layerCodes().join(',')
- await clickPage('unmatched', 2)
- check('T4 未命中第 2 页只画余数', unmatchedCodes().join(',') === DETAIL_27.slice(25, 27).join(','), unmatchedCodes().join(','))
- check('T4b 第三套翻页不牵连前两处', cardCodes().join(',') === cardsBefore && layerCodes().join(',') === layerBefore, '')
- // T5 折叠再展开:另一套语义——差额计数(另有 N 台)始终读**全量**,不是当前页。
- const more = findNode(container, byAttr('data-device-more'))
- check('T5 另有 N 台读数仍是全量 12(与窗口无关)', more && String(more.props['data-device-more-count']) === '12', `count=${more && more.props['data-device-more-count']}`)
- }
- // ================================================================ K. 票 42 口径不变(窗口化只发生在渲染)
- // pinia 的 store 实例把 ref 解包成本体:这里读 layer.rows / layer.total 直接是数组与数字。
- {
- // K1 store 全量保留:27 行、总数 27(15 快照 + 12 未命中——命中子集口径下含未命中)。
- check('K1 store.rows 仍持全部编码(27 行,窗口化只在渲染层)', layer.rows.length === 27, `rows=${layer.rows.length}`)
- check('K1b 总数只认当前快照口径(15 命中 + 12 未命中 = 27)', layer.total === 27, `total=${layer.total}`)
- // K2 DOM 当前页 ≠ 全量:标题读 27,清单只画 5(layer 在第 2 页)。
- const totalNode = findNode(container, byAttr('data-device-layer-total'))
- const drawnNode = findNode(container, byAttr('data-device-layer-drawn'))
- check('K2 标题「总数 27 台(已上图 15)」不受窗口影响',
- totalNode && textOf(totalNode) === '27' && drawnNode && textOf(drawnNode) === '15',
- `total=${totalNode && textOf(totalNode)} drawn=${drawnNode && textOf(drawnNode)}`)
- // K3 互斥归类之和 = 全量行数(每台恰好落一类,与窗口无关)。
- const countSum = Object.values(layer.counts).reduce((acc, v) => acc + v, 0)
- check('K3 互斥归类之和仍等于全量 27', countSum === 27 && layer.counts.drawn === 15 && layer.counts.unmatched === 12, JSON.stringify(layer.counts))
- // K4 图例合计 = 全量(差额可追溯不受分页影响)。
- const legendSum = layer.legend.reduce((acc, row) => acc + row.count, 0)
- check('K4 设备图例合计 = 全量 27', legendSum === 27, `legend=${legendSum}`)
- }
- // ================================================================ Z. 复位与保持(条文 5/6)
- {
- // Z1 刷新数据(同点位重取,selection 标量键不变)→ 页码**保持**。
- await detail.reloadPointDetail('P-MANY')
- layer.syncForSelection({ mode: 'overview', filterActive: true })
- await nextTick()
- check('Z1 数据刷新不复位页码(卡片还在第 2 页)', cardCodes()[0] === DETAIL_27[10], `first=${cardCodes()[0]}`)
- // Z2 越界夹回:刷新后数据变少(改开 P-FEW 前先把卡片停末页再换数据)——用跳 999 夹回第 3 页。
- const jumpInput = findNode(pagerWrap('cards'), byAttr('data-page-jump-input'))
- assert.ok(jumpInput && typeof jumpInput.props['onUpdate:modelValue'] === 'function', '跳页输入框未接线')
- jumpInput.props['onUpdate:modelValue']('999')
- await nextTick()
- findNode(pagerWrap('cards'), byAttr('data-page-jump-btn')).props.onClick()
- await nextTick()
- check('Z2 跳页超界夹回最后一页(7 条余数,空表不出现)',
- cardCodes().join(',') === DETAIL_27.slice(20, 27).join(','), cardCodes().join(','))
- // Z3 重进同一点位(selection 换新对象、标量键同值)→ **不**误复位(票 43 的稳定键教训)。
- await detail.openPoint('P-MANY')
- await nextTick()
- check('Z3 同点位重进不误复位(标量键同值不触发 watch)', cardCodes()[0] === DETAIL_27[20], `first=${cardCodes()[0]}`)
- // Z4 换点位 → 三处页码全回第 1 页;P-FEW 只有 2 条 → pageCount<=1 → 控件**整块不画**。
- await detail.openPoint('P-FEW')
- layer.syncForSelection({ force: true, mode: 'overview', filterActive: true })
- await nextTick()
- check('Z4 换点位回第 1 页窗口', cardCodes().join(',') === 'EQ-01,EQ-02', cardCodes().join(','))
- check('Z4b 短列表(≤10)三处控件都不画(条文 4)', pagers().length === 0, `pagers=${pagers().length}`)
- // Z5 进设备级再返回第一级 → 页码复位(返回第一级也算一次层级条件变化)。
- await detail.openPoint('P-MANY')
- layer.syncForSelection({ force: true, mode: 'overview', filterActive: true })
- await clickPage('cards', 2)
- layer.selectDevice('EQ-03')
- await nextTick()
- layer.backToPointLevel()
- await nextTick()
- check('Z5 返回第一级页码回第 1 页', cardCodes()[0] === 'EQ-01' && layerCodes().length === 10, `first=${cardCodes()[0]}`)
- // Z6 空态文案沿用既有:P-ZERO(详情 0 台)→「该监测点当前没有关联设备」。
- await detail.openPoint('P-ZERO')
- layer.syncForSelection({ force: true, mode: 'overview', filterActive: true })
- await nextTick()
- const empty = findNode(container, byAttr('data-device-cards-empty'))
- check('Z6 空态文案原样保留(未被分页改写)', !!empty && textOf(empty).includes('该监测点当前没有关联设备'), textOf(empty))
- check('Z6b 空清单不画控件', pagers().length === 0, `pagers=${pagers().length}`)
- }
- // ================================================================ S. 静态守卫(单源与默认形态不变)
- {
- const card = stripComments(read('components/PointDetailCard.vue'))
- const pager = stripComments(read('components/PagerNav.vue'))
- const sections = stripComments(read('components/EquipmentDetailSections.vue'))
- // S1 PagerNav 的 showCount 默认 true:票 46/59 既有消费方(设备详情三清单)形态零改动。
- check('S1 showCount 默认 true(既有计数行不回归)',
- /showCount: \{ type: Boolean, default: true \}/.test(pager), '默认值被改了=批次一形态回归')
- check('S2 计数行是唯一受 showCount 门控的块(翻页能力不受它影响)',
- /v-if="showCount" class="pager-nav__state" data-page-state/.test(pager) && (pager.match(/v-if="showCount"/g) || []).length === 1, '')
- // S3 设备详情侧没被顺手关计数行(data-page-state 断言面保留,t59-S9 同源)。
- check('S3 EquipmentDetailSections 不传 show-count(既有计数行保留)',
- !/show-count/.test(sections) && (sections.match(/<PagerNav\b/g) || []).length === 2, '设备详情三清单的计数行不该在本票变')
- // S4 点位卡三处接线都关计数行、都走同一出口(唯一 @page→gotoPage;无第二套翻页算术)。
- check('S4 点位卡三处接共享 PagerNav 且都 :show-count="false"',
- (card.match(/<PagerNav\b/g) || []).length === 3 && (card.match(/:show-count="false"/g) || []).length === 3,
- `uses=${(card.match(/<PagerNav\b/g) || []).length}`)
- check('S5 三处共用唯一翻页出口 gotoPage(key, target)',
- (card.match(/@page="\(target\) => gotoPage\('[a-z]+', target\)"/g) || []).length === 3 && /function gotoPage\(key, target\)/.test(card), '')
- // S6 窗口取数只有一条 pagedRows 委托;组件零 Math/slice(条文 11)。
- check('S6 窗口计算单源一条委托',
- /return pagedRows\(rows, pages\.value\[key\]\)/.test(card) && !/Math\.(ceil|trunc|floor)/.test(card) && !/\.slice\(/.test(card), '组件里长出第二份算术')
- // S7 页码状态是稳定标量键三元表 + 复位 watch 收**标量字符串**(票 43 教训:getter 回数组会引用不等假复位)。
- check('S7 三清单各自独立页码 + 复位键是标量',
- /const pages = ref\(\{ cards: 1, layer: 1, unmatched: 1 \}\)/.test(card) &&
- /watch\(\s*\(\) => `\$\{selection\.value\.pointCode\}\|\$\{selection\.value\.deviceCode\}`/.test(card) &&
- !/watch\(\s*\(\) => \[selection\.value\.pointCode/.test(card) &&
- /pages\.value = \{ cards: 1, layer: 1, unmatched: 1 \}/.test(card), '复位触发器形状变了')
- // S8 数据源不被窗口反噬:store 的 rows/total/counts/legend 仍直接进标题与图例(t42/t44 断言面)。
- check('S8 标题总数仍读 store total、归类仍读 store counts',
- /data-device-layer-total>\{\{ deviceLayerTotal \}\}</.test(card) && /data-device-layer-drawn>\{\{ deviceCounts\.drawn \|\| 0 \}\}</.test(card), '总数口径被组件改写')
- // S9 空态文案原样在源码里(判绑定文本而不是新措辞)。
- check('S9 空态文案沿用既有', card.includes('该监测点当前没有关联设备'), '文案被改了')
- }
- // ================================================================ C. SFC 真实编译
- for (const [label, rel] of [['card', 'components/PointDetailCard.vue'], ['pagernav', 'components/PagerNav.vue'], ['sections', 'components/EquipmentDetailSections.vue']]) {
- let ok = false
- let why = ''
- try {
- const source = read(rel)
- const { descriptor, errors } = parse(source, { filename: rel })
- assert.deepEqual(errors, [], `${rel} parse`)
- const script = compileScript(descriptor, { id: 't60' })
- const template = compileTemplate({ source: descriptor.template.content, filename: rel, id: 't60', compilerOptions: { bindingMetadata: script.bindings } })
- assert.deepEqual(template.errors, [], `${rel} template`)
- ok = true
- } catch (error) {
- why = error.message.split('\n')[0]
- }
- check(`C1-${label}-compiles`, ok, why)
- }
- resetMockTransportsForTest()
- layer.reset()
- detail.reset()
- console.log('')
- console.log(`T60 点位摘要两级清单分页:${passed}/${passed + failed} 项通过。`)
- if (failed) { console.log('未通过项见上方 FAIL。'); process.exit(1) }
- console.log('T60 全部通过(浏览器走查按 AGENTS.md 未自动执行)。')
|