t25-credit-info-panel.mjs 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. import assert from 'node:assert/strict'
  2. import { readFileSync } from 'node:fs'
  3. const read = (relative) => readFileSync(new URL(`../../../${relative}`, import.meta.url), 'utf8')
  4. const engine = read('src/modules/cockpit/services/cesiumEngine.js')
  5. const mapCanvas = read('src/modules/cockpit/components/MapCanvas.vue')
  6. const shell = read('src/modules/cockpit/CockpitShell.vue')
  7. const infoPanel = read('src/modules/cockpit/components/MapInfoPanel.vue')
  8. const blocked = read('src/modules/cockpit/components/MapCoordinateBlocked.vue')
  9. const mapService = read('src/modules/cockpit/services/mapService.js')
  10. // ---- ① 署名容器:必须「自己给一个不进 DOM 的容器」,而不是开关式传参 ----
  11. assert.match(engine, /creditContainer\s*=\s*document\.createElement\('div'\)/, '必须自建署名容器')
  12. assert.match(engine, /new Viewer\(host,\s*\{[^}]*creditContainer/, 'creditContainer 必须传给 Viewer')
  13. const createViewer = engine.slice(engine.indexOf('export function createViewer'))
  14. const createViewerBody = createViewer.slice(0, createViewer.indexOf('\n}'))
  15. // 只查**真实语句**(注释里出现 appendChild 这个词不算数)。
  16. assert.doesNotMatch(createViewerBody, /appendChild\(\s*creditContainer\s*\)/, '署名容器不得进 DOM,否则 Cesium 的默认署名照样上屏')
  17. assert.doesNotMatch(createViewerBody, /host\.appendChild/, '不得把署名容器挂到宿主上')
  18. // 传 false / 空串都关不掉(Cesium 两条路径都会建 CreditDisplay),不得回退到这种写法。
  19. assert.doesNotMatch(engine, /creditContainer:\s*(false|''|"")/, '不得用开关式传参(Cesium 不支持)')
  20. // ---- ② 合法性前提:不得出现任何 ion 依赖,否则隐藏署名即违约 ----
  21. const ionSources = ['src/modules/cockpit/services/cesiumEngine.js', 'src/modules/cockpit/services/mapService.js']
  22. .map(read)
  23. .join('\n')
  24. for (const forbidden of [/Cesium\.Ion\b/, /defaultAccessToken/, /createWorldTerrain/, /createWorldImagery/, /assets\.ion\.cesium\.com/, /ArcGisMapServiceImageryProvider/]) {
  25. assert.doesNotMatch(ionSources, forbidden, `检测到 ion 依赖(${forbidden}),隐藏署名将违反 Cesium 条款`)
  26. }
  27. // ---- ③ 数据来源署名必须由我们自己的面板承担 ----
  28. assert.match(mapService, /export function getDataSourceCredits\(\)/)
  29. assert.match(mapService, /export function getBasemapLabel\(\)/)
  30. assert.match(mapService, /dataSourceCredits\s*=\s*\[/, '署名必须从底图/叠加影像/3D Tiles 同一批计划里取')
  31. assert.match(mapService, /basemapPlan\.credit/, '底图 credit 也要收')
  32. assert.match(mapService, /overlayPlans\.map\(\(overlay\)\s*=>\s*overlay\.credit\)/, '叠加影像 credit 要收')
  33. assert.match(infoPanel, /data-map-info-credits/, '面板必须渲染数据来源署名')
  34. assert.match(infoPanel, /<li v-for="item in credits"/)
  35. // ---- ④ 地图上只保留顶部状态图例;其余并入**外壳既有状态面板**(不另起容器)----
  36. assert.match(mapCanvas, /data-map-legend/, '顶部状态筛选图例必须保留(决策 02 的全局入口)')
  37. for (const gone of ['map-holo__headline', 'map-holo__layers', 'map-holo__bottom', 'map-holo__status', 'map-holo__markers', 'map-holo__degraded']) {
  38. assert.doesNotMatch(mapCanvas, new RegExp(gone), `${gone} 已并入面板,不应再散挂在地图上`)
  39. }
  40. assert.doesNotMatch(mapCanvas, /CockpitIcon|MapCoordinateBlocked|MapInfoPanel/, '这些依赖已随内容块移交外壳')
  41. // 维护者反馈「容器异常占用」:地图组件不得再自带一个浮层容器。
  42. assert.doesNotMatch(mapCanvas, /<MapInfoPanel/, '地图组件不再挂载地图信息面板')
  43. // ---- ④b 并入外壳那一个既有容器(与「数据分区」「状态推送」同框)----
  44. assert.match(shell, /<MapInfoPanel/, '地图信息必须挂在外壳里')
  45. assert.match(shell, /class="cockpit-shell__status-details"[\s\S]*?<MapInfoPanel/, '必须落在外壳既有的状态详情容器内(不是另起容器)')
  46. assert.match(shell, /data-section-board[\s\S]*?<MapInfoPanel/, '紧接共享数据分区之后,与之同框')
  47. assert.match(shell, /getDataSourceCredits|getBasemapLabel/, '署名与底图措辞由外壳读取')
  48. // 计数取自同一份模式图层计划,不另开一份真相。
  49. assert.match(shell, /const mapMarkerCount = computed\(\(\) => modeLayerPlan\.value\.markers\.length\)/)
  50. // ---- ④c 内容块本身**不定位、不自建开关**(外壳那一个展开/收起已够)----
  51. assert.doesNotMatch(infoPanel, /position:\s*absolute/, '内容块不得自己定位(会和外壳面板抢容器)')
  52. assert.doesNotMatch(infoPanel, /right:\s*calc\(var\(--column-w/, '不再自行让开右栏')
  53. assert.doesNotMatch(infoPanel, /const open = ref\(false\)/, '不再自带展开开关,复用外壳的')
  54. // ---- ④d 不得截断 ----
  55. // 只查**真实声明**(注释里提到这些词不算数)。
  56. assert.doesNotMatch(mapService, /'text-overflow:ellipsis'/, '引擎读数不得再用省略号截断')
  57. assert.doesNotMatch(mapService, /'white-space:nowrap'/, '引擎读数不得单行截断')
  58. assert.match(mapService, /'overflow-wrap:anywhere'/, '引擎读数必须换行显示')
  59. assert.match(infoPanel, /text-overflow:\s*clip\s*!important/, '面板内兜底覆盖引擎节点的内联省略号')
  60. assert.match(infoPanel, /white-space:\s*normal\s*!important/, '面板内兜底覆盖引擎节点的内联 nowrap')
  61. // 坐标准入清单:名称与编码都不许省略号截断(维护者反馈编码被从右侧切掉)。
  62. assert.doesNotMatch(blocked, /text-overflow:\s*ellipsis/, '清单不得截断编码')
  63. assert.match(blocked, /\.map-blocked__code \{[\s\S]*?overflow-wrap:\s*anywhere/, '编码必须换行显示')
  64. // ---- ④e 「容器异常占用」与编码被裁已根治 ----
  65. assert.match(blocked, /\.map-blocked\.map-blocked--embedded[\s\S]*?max-height:\s*none/, '嵌入态必须解除浮块的 max-height')
  66. assert.match(blocked, /\.map-blocked\.map-blocked--embedded[\s\S]*?align-self:\s*start/, '嵌入态不得被父容器拉高')
  67. assert.match(blocked, /overflow:\s*visible/, '嵌入态不得再被自身滚动裁掉')
  68. // 编码被裁的根因:并排时编码列是 `auto` 轨道,会撑到 max-content 顶出容器。
  69. assert.match(blocked, /\.map-blocked\.map-blocked--embedded \.map-blocked__list li \{[\s\S]*?grid-template-columns:\s*minmax\(0, 1fr\)/, '嵌入态清单必须单列,长编码不再被裁')
  70. assert.match(blocked, /\.map-blocked\.map-blocked--embedded \.map-blocked__code \{[\s\S]*?word-break:\s*break-all/, '长编码必须能断行')
  71. // 图层清单不得静默消失(曾因模板仍引用已删除的 layers prop 而整块不渲染)。
  72. assert.match(infoPanel, /v-for="layer in LAYERS"/, '图层清单必须渲染')
  73. assert.doesNotMatch(infoPanel, /v-for="layer in layers"/, '不得引用已移除的 layers prop')
  74. assert.match(shell, /import MapInfoPanel from/, '外壳必须引入地图信息内容块')
  75. // ---- ④f 子项宽度必须撑满容器(维护者反馈:展开后清单宽度与容器严重不符)----
  76. // 根因:`.map-info` 是 grid,`justify-items: start` 会作用到**所有子项**,
  77. // 让它们按 fit-content 收缩 —— 展开的清单因此比容器窄一大截。
  78. // 只匹配**真实声明**(注释里提到这个词不算数)。
  79. assert.doesNotMatch(infoPanel, /^\s*justify-items:\s*start/m, '内容块不得用 justify-items 收窄子项')
  80. assert.match(infoPanel, /\.map-info > \* \{[\s\S]*?justify-self:\s*stretch/, '子项必须显式撑满容器宽度')
  81. assert.match(blocked, /\.map-blocked\.map-blocked--embedded[\s\S]*?max-width:\s*100%/, '嵌入块必须允许撑满容器')
  82. // ---- ⑤ 走查断言面一个都不能丢(面板搬家 ≠ 走查失灵)----
  83. assert.match(infoPanel, /data-map-marker-count/, '标记计数断言面')
  84. assert.match(infoPanel, /data-map-degraded-note/, '降级说明断言面')
  85. assert.match(infoPanel, /<MapCoordinateBlocked[^>]*:embedded="true"/, '坐标准入浮块并入面板且进入嵌入模式')
  86. assert.match(blocked, /data-map-blocked-panel/, '坐标准入面板断言面')
  87. assert.match(blocked, /map-blocked--embedded/, '嵌入模式样式类')
  88. assert.match(blocked, /position:\s*static/, '嵌入后不再是绝对定位浮块')
  89. // ---- ⑥ 引擎读数靠「搬节点」而不是重算,避免出现第二份真相 ----
  90. assert.match(mapService, /className = 'map-engine-status'/)
  91. assert.match(infoPanel, /querySelector\('\.map-engine-status'\)/, '面板提供插槽并搬入引擎节点')
  92. assert.doesNotMatch(mapService, /createViewer[\s\S]{0,400}creditContainer:\s*new CreditDisplay/, '不得另建一份署名')
  93. console.log('T25: credit container off-screen, no ion dependency, credits owned by panel, only legend stays on map, walkthrough hooks kept.')