t41-fixed-detail-render.mjs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. /** Component-level SSR integration; not a browser/layout or GPU validation. */
  2. import assert from 'node:assert/strict'
  3. import { readFileSync } from 'node:fs'
  4. import { registerHooks } from 'node:module'
  5. import { parse, compileScript, compileTemplate } from '@vue/compiler-sfc'
  6. import { compile } from '@vue/compiler-ssr'
  7. import { createRequire } from 'node:module'
  8. import { createSSRApp, computed } from 'vue'
  9. import { renderToString } from '@vue/server-renderer'
  10. import { createPinia, setActivePinia } from 'pinia'
  11. const root = new URL('../../../src/modules/cockpit/', import.meta.url)
  12. const observation = Symbol.for('t41.location-observation')
  13. globalThis[observation] = []
  14. const hooks = registerHooks({
  15. resolve(specifier, context, nextResolve) {
  16. const resolved = nextResolve(specifier, context)
  17. if (resolved.url.endsWith('/cockpit/services/mapService.js')) return { ...resolved, url: `${resolved.url}?t41-observed` }
  18. return resolved
  19. },
  20. load(url, context, nextLoad) {
  21. const mutation = process.env.T41_MUTATION
  22. if (mutation === 'marker-gate' && url.endsWith('/domain/modeLayerPlan.js')) {
  23. const source = readFileSync(new URL(url), 'utf8')
  24. const previous = /return Boolean\(point && mode !== MODE_LAYER\.INFRASTRUCTURE &&\s*\(mode !== MODE_LAYER\.WARNING_RESPONSE \|\| isAbnormalPoint\(point, abnormalCodes\)\)\)/
  25. assert.ok(previous.test(source), 'mutation matches current gate')
  26. return { format: 'module', shortCircuit: true, source: source.replace(previous,
  27. `return markers.some((item) => item.code === current.pointCode)`) }
  28. }
  29. if (mutation === 'drop-empty' && url.endsWith('/services/detailApi.js')) {
  30. const source = readFileSync(new URL(url), 'utf8')
  31. assert.ok(source.includes('toData: toMonitoringPointDetail,'), 'mutation matches point adapter')
  32. return { format: 'module', shortCircuit: true, source: source.replace('toData: toMonitoringPointDetail,',
  33. `toData: toMonitoringPointDetail, isEmpty: isEmptyBy((payload) => payload?.pointStatus === 'EMPTY'),`) }
  34. }
  35. if (url.endsWith('?t41-observed')) {
  36. const actual = url.replace('?t41-observed', '?t41-actual')
  37. return { format: 'module', shortCircuit: true, source: `
  38. import * as actual from ${JSON.stringify(actual)};
  39. export * from ${JSON.stringify(actual)};
  40. export function locateMonitoringPoint(...args) {
  41. globalThis[Symbol.for('t41.location-observation')].push(args);
  42. return actual.locateMonitoringPoint(...args);
  43. }
  44. ` }
  45. }
  46. if (url.endsWith('.json')) return { format: 'module', shortCircuit: true, source: `export default ${readFileSync(new URL(url), 'utf8')}` }
  47. if (url.endsWith('.vue')) {
  48. const { descriptor } = parse(readFileSync(new URL(url), 'utf8'), { filename: new URL(url).pathname })
  49. const script = compileScript(descriptor, { id: 't41-render', genDefaultAs: '__sfc__' })
  50. const template = compileTemplate({ source: descriptor.template.content, filename: new URL(url).pathname,
  51. id: 't41-render', ssr: true, ssrCssVars: [], compilerOptions: { bindingMetadata: script.bindings } })
  52. assert.deepEqual(template.errors, [])
  53. return { format: 'module', shortCircuit: true,
  54. source: `${script.content}\n${template.code}\n__sfc__.ssrRender = ssrRender; export default __sfc__;` }
  55. }
  56. return nextLoad(url, context)
  57. },
  58. })
  59. const { useDetailStore } = await import(new URL('stores/detailStore.js', root))
  60. const { useMapStore } = await import(new URL('stores/mapStore.js', root))
  61. const { useMonitoringStore } = await import(new URL('stores/monitoringStore.js', root))
  62. const { registerMockTransport, resetMockTransportsForTest } = await import(new URL('services/cockpitApi.js', root))
  63. const { planModeLayers } = await import(new URL('domain/modeLayerPlan.js', root))
  64. const { resolveDetailAnchor, detailAnchorLocation } = await import(new URL('domain/detailAnchor.js', root))
  65. const { default: PointDetailCard } = await import(new URL('components/PointDetailCard.vue', root))
  66. const pinia = createPinia(); setActivePinia(pinia)
  67. const config = { dataSource: 'mock', api: { baseUrl: '', token: '' } }
  68. const detail = useDetailStore(); detail.configure({ config })
  69. const map = useMapStore(); map.setup({ config })
  70. const ledger = useMonitoringStore()
  71. resetMockTransportsForTest()
  72. const code = 'P-FIXED'
  73. const validation = { state: 'INVALID', reason: 'OUTSIDE_PRIMARY_AREA', renderable: false, crs: 'WGS84' }
  74. registerMockTransport('/map-snapshot', () => ({ page: 1, pageSize: 2, total: 1, complete: true,
  75. snapshotAt: '2026-10-12T10:00:00+08:00', monitoringPoints: [{ pointCode: code, pointName: '区外监测点',
  76. pointStatus: 'NORMAL', deviceCodes: ['EQ-FIXED'], longitude: 110.1, latitude: 28.4, coordinateValidation: validation }], unassociatedEquipment: [] }))
  77. registerMockTransport(`/monitoring-points/${code}`, () => ({ monitoringPointCode: code, monitoringPointName: '区外监测点',
  78. pointStatus: 'NORMAL', statusLabel: '正常', devices: [{ deviceCode: 'EQ-FIXED', deviceName: '区外设备', deviceStatus: 'NORMAL', coordinateValidation: validation }],
  79. deviceStatistics: { totalDeviceCount: 1, normalDeviceCount: 1 }, coordinateValidation: validation }))
  80. await map.advance()
  81. await ledger.openLedgerPoint(code)
  82. // Use the production floating detail template without invoking shell lifecycle/server actions.
  83. const shell = readFileSync(new URL('CockpitShell.vue', root), 'utf8')
  84. const begin = shell.indexOf('<div v-if="detailOpen && !introVisible && selectionVisibleInMode && anchorLayout.visible')
  85. const end = shell.indexOf('</aside>', begin) + '</aside>'.length
  86. assert.ok(begin >= 0 && end > begin, 'production float template found')
  87. const ssrRender = new Function('require', compile(shell.slice(begin, end), { mode: 'function' }).code)(createRequire(import.meta.url))
  88. let mode = 'overview'
  89. let failures = 0
  90. let checks = 0
  91. async function check(name, action) {
  92. checks++
  93. try { await action(); console.log(`PASS ${name}`) }
  94. catch (error) { failures++; console.error(`FAIL ${name}: ${error.message.split('\n')[0]}`) }
  95. }
  96. const verdict = () => detailAnchorLocation(resolveDetailAnchor({ selection: detail.selection, objects: map.summary.objects }))
  97. async function render() {
  98. const plan = planModeLayers({ mode, entered: true, objects: map.summary.objects, selection: detail.selection })
  99. const reason = verdict().reasonText
  100. const app = createSSRApp({ components: { PointDetailCard }, ssrRender, setup() {
  101. return { detailOpen: detail.selection.level !== 'none', introVisible: false, selectionVisibleInMode: plan.selection.visible,
  102. anchorLayout: { visible: false, side: '', reason: 'marker-missing' }, fixedLocationReason: reason,
  103. selectedDetailCode: detail.selection.pointCode, anchorLeadStyle: {}, detailStyle: computed(() => reason ? {} : { visibility: 'hidden' }) }
  104. } })
  105. app.use(pinia)
  106. return renderToString(app)
  107. }
  108. await check('ledger public entry opens loaded blocked point with its own identity', () => {
  109. assert.equal(detail.selection.level, 'point'); assert.equal(detail.selection.pointCode, code)
  110. assert.equal(detail.sections.pointDetail.data.name, '区外监测点')
  111. })
  112. await check('overview mounts fixed detail and prints its precise no-location reason', async () => {
  113. const html = await render()
  114. assert.match(html, /data-anchor-state="fixed"/)
  115. assert.match(html, /data-point-no-location/)
  116. assert.ok(html.includes('不在主场景区范围内'))
  117. assert.ok(!html.includes('data-anchor-lead'))
  118. assert.ok(!html.includes('visibility:hidden'))
  119. })
  120. await check('blocked selection issues no location request', () => assert.equal(globalThis[observation].length, 0))
  121. await check('monitoring mode retains the fixed detail without fake map marker', async () => {
  122. mode = 'monitoring'
  123. const html = await render()
  124. assert.match(html, /data-anchor-state="fixed"/)
  125. assert.equal(planModeLayers({ mode, objects: map.summary.objects }).markers.length, 0)
  126. })
  127. await check('infrastructure suspends fixed detail but keeps selected identity', async () => {
  128. mode = 'infrastructure'
  129. assert.ok(!(await render()).includes('data-point-detail'))
  130. assert.equal(detail.selection.pointCode, code)
  131. })
  132. await check('switching back to overview restores fixed detail', async () => {
  133. mode = 'overview'; assert.match(await render(), /data-anchor-state="fixed"/)
  134. })
  135. await check('unloaded object is not falsely labelled no-location', () => {
  136. assert.equal(detailAnchorLocation(resolveDetailAnchor({ selection: { level: 'point', pointCode: 'P-LATER' }, objects: map.summary.objects })).present, false)
  137. })
  138. await check('no-device blocked point retains its name and EMPTY business status', async () => {
  139. detail.closeDetail()
  140. registerMockTransport(`/monitoring-points/${code}`, () => ({ monitoringPointCode: code, monitoringPointName: '区外监测点',
  141. pointStatus: 'EMPTY', statusLabel: '无设备', devices: [], deviceStatistics: { totalDeviceCount: 0 }, coordinateValidation: validation }))
  142. await ledger.openLedgerPoint(code)
  143. assert.equal(detail.sections.pointDetail.data?.name, '区外监测点')
  144. assert.equal(detail.sections.pointDetail.data?.status, 'EMPTY')
  145. assert.equal(detail.sections.pointDetail.data.statistics.total, 0)
  146. assert.equal(detail.sections.pointDetail.data.coordinate.renderable, false)
  147. assert.equal(detail.sections.pointDetail.data.coordinate.reason, 'OUTSIDE_PRIMARY_AREA')
  148. const html = await render()
  149. assert.ok(html.includes('区外监测点'))
  150. assert.ok(html.includes('无设备'))
  151. assert.match(html, /data-point-code-value[^>]*>P-FIXED<\/dd>/)
  152. assert.match(html, /data-point-device-count[^>]*>0<\/b>/)
  153. assert.equal(globalThis[observation].length, 0)
  154. })
  155. await check('closing removes detail without issuing a point-location request', async () => {
  156. detail.closeDetail(); assert.ok(!(await render()).includes('data-point-detail'))
  157. assert.equal(globalThis[observation].length, 0)
  158. })
  159. resetMockTransportsForTest(); delete globalThis[observation]; hooks.deregister()
  160. console.log(`T41 fixed-detail SSR: ${checks-failures}/${checks}, not browser verification`)
  161. process.exitCode = failures ? 1 : 0