t01-panel-catalog.mjs 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. /**
  2. * T01 纯逻辑检查:模式→面板映射、展开收放、全局筛选选项。
  3. *
  4. * 仓库没有配置测试运行器(node_modules 里没有 vitest/jest),
  5. * 且项目约定不为此引入新框架,所以这里用零依赖的 Node 断言脚本。
  6. *
  7. * 运行:node .scratch/cockpit-shell-panel-layout/verify/t01-panel-catalog.mjs
  8. */
  9. import assert from 'node:assert/strict'
  10. import {
  11. MODES,
  12. PANEL_LABELS,
  13. columnsForMode,
  14. defaultExpandedPanelIds,
  15. isKnownMode,
  16. isKnownPanel,
  17. labelForPanel,
  18. panelsForMode,
  19. toggleExpanded,
  20. } from '../../../src/modules/cockpit/domain/panelCatalog.js'
  21. import { DEFAULT_INDUSTRY, DEFAULT_SCOPE, INDUSTRIES, SCOPES, isKnownIndustry, isKnownScope } from '../../../src/modules/cockpit/domain/filterOptions.js'
  22. import {
  23. getLifecycleStats,
  24. getMapInstance,
  25. mountMap,
  26. resetLifecycleStatsForTest,
  27. resizeMap,
  28. unmountMap,
  29. } from '../../../src/modules/cockpit/services/mapService.js'
  30. import { resolveCesiumBaseUrl } from '../../../src/modules/cockpit/services/cesiumAssets.js'
  31. const EXPECTED_MODES = ['overview', 'infrastructure', 'monitoring', 'warningResponse']
  32. // 模式键是稳定的 camelCase 四值,且顺序固定。
  33. assert.deepEqual(MODES, EXPECTED_MODES, '一级模式键或顺序不符')
  34. EXPECTED_MODES.forEach((mode) => assert.equal(isKnownMode(mode), true, `${mode} 应被识别`))
  35. assert.equal(isKnownMode('总体概览'), false, '中文不得作为模式键')
  36. // 每个模式恰好左右各两块,共四块。
  37. EXPECTED_MODES.forEach((mode) => {
  38. const { left, right } = columnsForMode(mode)
  39. assert.equal(left.length, 2, `${mode} 左栏应为 2 块`)
  40. assert.equal(right.length, 2, `${mode} 右栏应为 2 块`)
  41. assert.equal(panelsForMode(mode).length, 4, `${mode} 应为 4 块面板`)
  42. })
  43. // 未知模式报错,不静默渲染空壳。
  44. assert.throws(() => columnsForMode('unknownMode'), /未知一级模式/, '未知模式应报错')
  45. // panelId 扁平、全局唯一、共 16 个,且每个都有中文展示名。
  46. const allPanelIds = EXPECTED_MODES.flatMap((mode) => panelsForMode(mode))
  47. assert.equal(allPanelIds.length, 16, '面板总数应为 16')
  48. assert.equal(new Set(allPanelIds).size, 16, 'panelId 必须全局唯一')
  49. allPanelIds.forEach((panelId) => {
  50. assert.equal(panelId.includes('.'), false, `panelId 不应带模式前缀: ${panelId}`)
  51. assert.equal(isKnownPanel(panelId), true, `${panelId} 应有中文展示名`)
  52. assert.notEqual(labelForPanel(panelId), panelId, `${panelId} 的中文名未解析`)
  53. })
  54. assert.equal(labelForPanel('noSuchPanel'), 'noSuchPanel', '未知面板应回退为原键')
  55. // 进入模式时默认全部展开。
  56. EXPECTED_MODES.forEach((mode) => {
  57. assert.deepEqual(defaultExpandedPanelIds(mode), panelsForMode(mode), `${mode} 默认应全部展开`)
  58. })
  59. // 展开集合的收放是纯函数:可来回切换,且不改动入参。
  60. const initial = ['archive', 'operation']
  61. const collapsed = toggleExpanded(initial, 'archive')
  62. assert.deepEqual(collapsed, ['operation'], '已展开的面板应收起')
  63. assert.deepEqual(initial, ['archive', 'operation'], 'toggleExpanded 不得改写入参')
  64. const reExpanded = toggleExpanded(collapsed, 'archive')
  65. // 展开集合语义上是无序集合(面板渲染顺序由目录决定),所以只断言成员与数量,不断言顺序。
  66. assert.equal(reExpanded.includes('archive'), true, '收起后面板应能重新展开')
  67. assert.equal(reExpanded.includes('operation'), true, '重展不得丢失其他已展开项')
  68. assert.equal(reExpanded.length, collapsed.length + 1, '重展后集合应恰好多一项')
  69. // ---- 全局筛选选项(范围 / 行业)----
  70. assert.equal(SCOPES.length >= 2, true, '范围至少要有全域与其他区域')
  71. assert.equal(INDUSTRIES.map((i) => i.key).join(','), 'all,gas,water,drainage,manhole', '行业键与顺序应按 ADR 0002')
  72. assert.equal(DEFAULT_SCOPE, 'all', '默认范围为全域')
  73. assert.equal(DEFAULT_INDUSTRY, 'all', '默认行业为全部')
  74. assert.equal(SCOPES.some((s) => s.key === DEFAULT_SCOPE), true, '默认范围必须是合法选项')
  75. assert.equal(INDUSTRIES.some((i) => i.key === DEFAULT_INDUSTRY), true, '默认行业必须是合法选项')
  76. assert.equal(isKnownScope(DEFAULT_SCOPE), true, '默认范围应被识别')
  77. assert.equal(isKnownIndustry('gas'), true, '燃气应被识别')
  78. assert.equal(isKnownIndustry('电力'), false, '不在行业清单内的键应被拒绝')
  79. assert.equal(isKnownScope('火星'), false, '不在范围清单内的键应被拒绝')
  80. // ---- 地图服务生命周期(票 02):只创建一次、幂等挂载、只在离开时销毁 ----
  81. // 用普通对象冒充宿主要素,验证本模块不依赖 DOM。
  82. resetLifecycleStatsForTest()
  83. assert.equal(getLifecycleStats().created, 0, '初始不应有实例')
  84. assert.equal(getMapInstance(), null, '初始应为未挂载')
  85. const hostA = { id: 'host-a' }
  86. const first = mountMap(hostA)
  87. const again = mountMap({ id: 'host-b' })
  88. assert.equal(first.id, again.id, '重复挂载必须返回同一实例(幂等,不重建)')
  89. assert.equal(getLifecycleStats().created, 1, '全程只创建一次')
  90. assert.equal(again.host, hostA, '重复挂载不得替换宿主要素')
  91. assert.equal(getLifecycleStats().mounted, true, '挂载后应处于已挂载态')
  92. resizeMap()
  93. assert.equal(getLifecycleStats().resize, 1, '尺寸上报应被记录')
  94. assert.equal(getLifecycleStats().lastResizeAt.length > 0, true, '尺寸上报应带时间戳')
  95. unmountMap()
  96. assert.equal(getLifecycleStats().destroyed, 1, '销毁应计一次')
  97. assert.equal(getMapInstance(), null, '销毁后不应残留实例')
  98. resizeMap()
  99. assert.equal(getLifecycleStats().resize, 1, '未挂载时尺寸上报应被忽略')
  100. const third = mountMap(hostA)
  101. assert.notEqual(third.id, first.id, '销毁后重新挂载应是新实例(id 不复用)')
  102. assert.equal(getLifecycleStats().created, 2, '重新挂载后创建数为 2')
  103. // ---- Cesium 资源基址(票 27):跟随 publicPath './' 与 qiankun 注入路径,不硬编码 /cesium/ ----
  104. // hash 路由下文档 URL 是 index.html(#/cockpit 不参与解析)。
  105. assert.equal(
  106. resolveCesiumBaseUrl('./', 'http://localhost:8000/#/cockpit'),
  107. 'http://localhost:8000/cesium/',
  108. '相对 publicPath 应相对文档解析成绝对 URL(hash 路由不影响)',
  109. )
  110. assert.equal(
  111. resolveCesiumBaseUrl('./', 'http://host/app/index.html'),
  112. 'http://host/app/cesium/',
  113. '子目录部署下应落在文档同级目录',
  114. )
  115. assert.equal(
  116. resolveCesiumBaseUrl('https://cos.icegl.cn/qiankun/tvt/', 'http://host/'),
  117. 'https://cos.icegl.cn/qiankun/tvt/cesium/',
  118. 'qiankun 注入的绝对基址应被采用',
  119. )
  120. assert.equal(resolveCesiumBaseUrl('/base/', 'http://host/'), 'http://host/base/cesium/', '根相对基址应被采用')
  121. assert.equal(resolveCesiumBaseUrl().endsWith('/cesium/'), true, '默认基址也必须以 cesium/ 结尾')
  122. // ---- 非浏览器宿主:不加载引擎、不抛错,且如实标注(票 27)----
  123. resetLifecycleStatsForTest()
  124. const domlessHost = { id: 'not-a-dom-node' }
  125. mountMap(domlessHost)
  126. assert.equal(getLifecycleStats().engine, 'unavailable', '非 DOM 宿主不应启动引擎')
  127. assert.equal(getLifecycleStats().basemap, 'not-configured', '本票不配置底图,状态应如实为未配置')
  128. assert.equal(getLifecycleStats().created, 1, '非 DOM 宿主仍应正常记一次创建')
  129. unmountMap()
  130. resetLifecycleStatsForTest()
  131. assert.equal(getLifecycleStats().created, 0, '重置后计数应归零')
  132. assert.equal(getLifecycleStats().engine, 'idle', '重置后引擎状态应回到 idle')
  133. console.log(
  134. `T01 检查通过:${EXPECTED_MODES.length} 个模式 / ${allPanelIds.length} 个面板 / 展开收放与默认展开一致 / 筛选选项(${SCOPES.length} 范围 · ${INDUSTRIES.length} 行业)正确 / 地图服务生命周期幂等且只创建一次 / Cesium 资源基址相对 publicPath 解析正确。`,
  135. )