| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160 |
- /** T42 刷新生命周期门禁(票 42 重开;规格 14:86 核对链)。 */
- import assert from 'node:assert/strict'
- import { readFileSync } from 'node:fs'
- import { createPinia, setActivePinia } from 'pinia'
- import { registerMockTransport, resetMockTransportsForTest } from '../../../src/modules/cockpit/services/cockpitApi.js'
- import { registerCockpitMocks } from '../../../src/modules/cockpit/mocks/contractMocks.js'
- import { useMapStore } from '../../../src/modules/cockpit/stores/mapStore.js'
- import { useDetailStore } from '../../../src/modules/cockpit/stores/detailStore.js'
- import { useDeviceLayerStore } from '../../../src/modules/cockpit/stores/deviceLayerStore.js'
- const root = new URL('../../../src/modules/cockpit/', import.meta.url)
- const read = (rel) => readFileSync(new URL(rel, root), 'utf8')
- const checks = []
- let failed = 0
- async function check(name, action) {
- checks.length += 1
- try { await action(); console.log(`PASS ${name}`) }
- catch (error) { failed += 1; console.error(`FAIL ${name}: ${String(error.message).split('\n')[0]}`) }
- }
- setActivePinia(createPinia())
- const mockConfig = { dataSource: 'mock', api: { baseUrl: '', token: '' } }
- /** real 模式:让注入的 fetchImpl 真正接到请求(mock 模式走 mock 传输,注入无效)。 */
- const realConfig = { dataSource: 'real', api: { baseUrl: 'http://acceptance' } }
- const point = (code, extra = {}) => ({ pointCode: code, pointName: `点 ${code}`, pointStatus: 'NORMAL', deviceCodes: [],
- longitude: 110.4, latitude: 28.44,
- coordinateValidation: { state: 'INVALID', reason: 'OUTSIDE_PRIMARY_AREA', renderable: false, crs: 'WGS84' }, ...extra })
- const pageBody = (points, { complete = true, total = points.length } = {}) => ({
- snapshotAt: 'S', batchUpdatedAt: 'S', page: 1, pageSize: 1, total, complete, monitoringPoints: points, unassociatedEquipment: [] })
- const ok = (body) => Promise.resolve({ ok: true, status: 200, json: async () => body })
- const expired = () => Promise.resolve({ ok: false, status: 400, json: async () => ({ code: 500, msg: 'fail', data: { category: 'SNAPSHOT_EXPIRED' } }) })
- /** 造一个分页 fetch:pages[i] 为第 i+1 页载荷;delayPage 挂起并回 .resolve(page) 供测试放行。 */
- function pagingFetch(pages, { delayPage = -1 } = {}) {
- const pending = new Map()
- const fetch = (url) => {
- const page = Number(new URL(url, 'http://acceptance').searchParams.get('page') || 1)
- const body = pages[page - 1]
- if (!body) return expired()
- if (page === delayPage) return new Promise((resolve) => { pending.set(page, () => resolve(ok(body))) })
- return ok(body)
- }
- fetch.resolve = (page) => { const release = pending.get(page); if (release) { pending.delete(page); release() } }
- return fetch
- }
- // ---- A1 refreshing:发起即亮(根因 ① 的撤层触发信号,同步可见) ----
- {
- const store = useMapStore()
- store.reset()
- const fetch = pagingFetch([pageBody([point('P-1')])], { delayPage: 1 })
- store.setup({ config: realConfig, fetchImpl: fetch })
- const pending = store.refresh()
- check('A1-refreshing-true-at-initiation', () => assert.equal(store.refreshing, true))
- fetch.resolve(1)
- await pending
- check('A1-refreshing-false-after-settle', () => assert.equal(store.refreshing, false))
- }
- // ---- A2 完整链收口 + 全页存在性索引(护栏无关) ----
- {
- const store = useMapStore()
- store.reset()
- store.setup({ config: realConfig, fetchImpl: pagingFetch([
- pageBody([point('P-1')], { complete: false, total: 2 }),
- pageBody([point('P-2')]),
- ]) })
- await store.refresh()
- check('A2-phase-complete', () => assert.equal(store.summary.phase, 'complete'))
- check('A2-refreshing-false-after-settle', () => assert.equal(store.refreshing, false))
- check('A2-page1-point-indexed', () => assert.equal(store.hasSnapshotObject('monitoring-point', 'P-1'), true))
- check('A2-page2-point-indexed', () => assert.equal(store.hasSnapshotObject('monitoring-point', 'P-2'), true))
- check('A2-unknown-absent', () => assert.equal(store.hasSnapshotObject('monitoring-point', 'P-X'), false))
- }
- // ---- A3 快照失效:expired 相位(batchUpdatedAt 不变,外壳单独触发撤层) ----
- {
- const store = useMapStore()
- store.reset()
- store.setup({ config: realConfig, fetchImpl: pagingFetch([pageBody([point('P-1')], { complete: false, total: 2 })]) })
- await store.refresh()
- check('A3-expired-phase', () => assert.equal(store.summary.phase, 'expired'))
- check('A3-refreshing-false-after-expired', () => assert.equal(store.refreshing, false))
- check('A3-index-keeps-fetched-pages', () => assert.equal(store.hasSnapshotObject('monitoring-point', 'P-1'), true))
- }
- // ---- A4 换快照重置存在性索引 ----
- {
- const store = useMapStore()
- store.reset()
- store.setup({ config: realConfig, fetchImpl: pagingFetch([pageBody([point('P-1')], { complete: false, total: 2 }), pageBody([point('P-2')])]) })
- await store.refresh()
- check('A4-old-snapshot-indexed', () => assert.equal(store.hasSnapshotObject('monitoring-point', 'P-2'), true))
- store.setup({ config: realConfig, fetchImpl: pagingFetch([pageBody([point('P-1')])]) })
- await store.refresh()
- check('A4-index-reset-on-new-snapshot', () => assert.equal(store.hasSnapshotObject('monitoring-point', 'P-2'), false))
- }
- // ---- A5 reloadPointDetail:强制重取(不查缓存)且保留选择层级 ----
- {
- resetMockTransportsForTest()
- registerCockpitMocks({ scenario: 'success', handlers: { registerMockTransport, resetMockTransportsForTest } })
- const detail = useDetailStore()
- detail.reset()
- detail.configure({ config: mockConfig })
- let hits = 0
- registerMockTransport('/monitoring-points/MP-COUNT', () => { hits += 1; return {
- monitoringPointCode: 'MP-COUNT', monitoringPointName: '计数点', pointStatus: 'NORMAL', statusLabel: '正常',
- statusReasons: [], deviceStatistics: { totalDeviceCount: 0 }, devices: [],
- coordinateValidation: { state: 'VALID', reason: null, renderable: false, crs: 'WGS84' } } })
- await detail.openPoint('MP-COUNT')
- await detail.openPoint('MP-COUNT')
- check('A5-openPoint-uses-cache', () => assert.equal(hits, 1))
- await detail.selectDevice('TC-PS-WS-202609020012').catch(() => {})
- const levelBefore = detail.selection.level
- await detail.reloadPointDetail('MP-COUNT')
- check('A5-reload-forces-refetch', () => assert.equal(hits, 2))
- check('A5-reload-keeps-selection-level', () => assert.equal(detail.selection.level, levelBefore))
- check('A5-reload-refreshes-section', () => assert.equal(detail.sections.pointDetail.data.code, 'MP-COUNT'))
- }
- // ---- A6 suspendForSnapshot:撤下且不按旧摘要重算 ----
- {
- const device = useDeviceLayerStore()
- device.reset()
- device.rows = [{ code: 'EQ-1', pointCode: 'MP-1', placement: 'drawn', status: 'NORMAL' }]
- device.total = 5
- device.suspendForSnapshot()
- check('A6-suspend-withdraws-rows', () => assert.equal(device.rows.length, 0))
- check('A6-suspend-clears-total', () => assert.equal(device.total, 0))
- check('A6-suspend-paints-false', () => assert.equal(device.painted, false))
- const source = read('stores/deviceLayerStore.js')
- const body = source.match(/function suspendForSnapshot\(\)[^]*?\n \}/)[0]
- check('A6-suspend-does-not-replan', () => assert.ok(!body.includes('syncForSelection'), '挂起不得立即重算'))
- }
- // ---- B 外壳静态守卫:三个监听 + 核对函数 + 可观察 chip ----
- {
- const shell = read('CockpitShell.vue')
- check('B1-refreshing-watcher-suspends', () => assert.match(shell,
- /watch\(\(\) => mapStore\.refreshing, \(on\) => \{[^]*?deviceLayerStore\.suspendForSnapshot\(\)/))
- check('B2-expired-watcher-suspends', () => assert.match(shell,
- /watch\(\(\) => mapStore\.summary\.phase[\s\S]{0,120}MAP_SUMMARY_PHASE\.EXPIRED[\s\S]{0,160}suspendForSnapshot\(\)/))
- check('B3-settled-watcher-reconciles', () => assert.match(shell,
- /watch\(\(\) => mapStore\.settled[\s\S]{0,120}reconcileSnapshotSelection\(\)/))
- const reconcile = shell.match(/function reconcileSnapshotSelection\(\)[^]*?\n\}/)[0]
- check('B4-reconcile-found-reloads', () => assert.match(reconcile, /hasSnapshotObject\('monitoring-point', pointCode\)/) && assert.match(reconcile, /reloadPointDetail\(pointCode\)/))
- check('B5-reconcile-absent-closes', () => assert.match(reconcile, /snapshotAbsentNote\.value =/) && assert.match(reconcile, /closeDetail\(\)/))
- check('B6-reconcile-failure-keeps-pending', () => assert.match(reconcile, /MAP_SUMMARY_PHASE\.FAILED \|\| phase === MAP_SUMMARY_PHASE\.EXPIRED/))
- check('B7-pending-chip', () => assert.match(shell, /data-snapshot-pending/))
- check('B8-absent-chip', () => assert.match(shell, /data-snapshot-absent/))
- check('B9-store-exports', () => {
- assert.match(read('stores/mapStore.js'), /refreshing, filters, hasSnapshotObject/)
- assert.match(read('stores/detailStore.js'), /reloadPointDetail,/)
- assert.match(read('stores/deviceLayerStore.js'), /suspendForSnapshot,/)
- })
- }
- console.log(`T42 refresh lifecycle: ${checks.length - failed}/${checks.length}`)
- process.exitCode = failed ? 1 : 0
|