import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' const read = (relative) => readFileSync(new URL(`../../../${relative}`, import.meta.url), 'utf8') const engine = read('src/modules/cockpit/services/cesiumEngine.js') const mapCanvas = read('src/modules/cockpit/components/MapCanvas.vue') const shell = read('src/modules/cockpit/CockpitShell.vue') const infoPanel = read('src/modules/cockpit/components/MapInfoPanel.vue') const blocked = read('src/modules/cockpit/components/MapCoordinateBlocked.vue') const mapService = read('src/modules/cockpit/services/mapService.js') // ---- ① 署名容器:必须「自己给一个不进 DOM 的容器」,而不是开关式传参 ---- assert.match(engine, /creditContainer\s*=\s*document\.createElement\('div'\)/, '必须自建署名容器') assert.match(engine, /new Viewer\(host,\s*\{[^}]*creditContainer/, 'creditContainer 必须传给 Viewer') const createViewer = engine.slice(engine.indexOf('export function createViewer')) const createViewerBody = createViewer.slice(0, createViewer.indexOf('\n}')) // 只查**真实语句**(注释里出现 appendChild 这个词不算数)。 assert.doesNotMatch(createViewerBody, /appendChild\(\s*creditContainer\s*\)/, '署名容器不得进 DOM,否则 Cesium 的默认署名照样上屏') assert.doesNotMatch(createViewerBody, /host\.appendChild/, '不得把署名容器挂到宿主上') // 传 false / 空串都关不掉(Cesium 两条路径都会建 CreditDisplay),不得回退到这种写法。 assert.doesNotMatch(engine, /creditContainer:\s*(false|''|"")/, '不得用开关式传参(Cesium 不支持)') // ---- ② 合法性前提:不得出现任何 ion 依赖,否则隐藏署名即违约 ---- const ionSources = ['src/modules/cockpit/services/cesiumEngine.js', 'src/modules/cockpit/services/mapService.js'] .map(read) .join('\n') for (const forbidden of [/Cesium\.Ion\b/, /defaultAccessToken/, /createWorldTerrain/, /createWorldImagery/, /assets\.ion\.cesium\.com/, /ArcGisMapServiceImageryProvider/]) { assert.doesNotMatch(ionSources, forbidden, `检测到 ion 依赖(${forbidden}),隐藏署名将违反 Cesium 条款`) } // ---- ③ 数据来源署名必须由我们自己的面板承担 ---- assert.match(mapService, /export function getDataSourceCredits\(\)/) assert.match(mapService, /export function getBasemapLabel\(\)/) assert.match(mapService, /dataSourceCredits\s*=\s*\[/, '署名必须从底图/叠加影像/3D Tiles 同一批计划里取') assert.match(mapService, /basemapPlan\.credit/, '底图 credit 也要收') assert.match(mapService, /overlayPlans\.map\(\(overlay\)\s*=>\s*overlay\.credit\)/, '叠加影像 credit 要收') assert.match(infoPanel, /data-map-info-credits/, '面板必须渲染数据来源署名') assert.match(infoPanel, /
  • modeLayerPlan\.value\.markers\.length\)/) // ---- ④c 内容块本身**不定位、不自建开关**(外壳那一个展开/收起已够)---- assert.doesNotMatch(infoPanel, /position:\s*absolute/, '内容块不得自己定位(会和外壳面板抢容器)') assert.doesNotMatch(infoPanel, /right:\s*calc\(var\(--column-w/, '不再自行让开右栏') assert.doesNotMatch(infoPanel, /const open = ref\(false\)/, '不再自带展开开关,复用外壳的') // ---- ④d 不得截断 ---- // 只查**真实声明**(注释里提到这些词不算数)。 assert.doesNotMatch(mapService, /'text-overflow:ellipsis'/, '引擎读数不得再用省略号截断') assert.doesNotMatch(mapService, /'white-space:nowrap'/, '引擎读数不得单行截断') assert.match(mapService, /'overflow-wrap:anywhere'/, '引擎读数必须换行显示') assert.match(infoPanel, /text-overflow:\s*clip\s*!important/, '面板内兜底覆盖引擎节点的内联省略号') assert.match(infoPanel, /white-space:\s*normal\s*!important/, '面板内兜底覆盖引擎节点的内联 nowrap') // 坐标准入清单:名称与编码都不许省略号截断(维护者反馈编码被从右侧切掉)。 assert.doesNotMatch(blocked, /text-overflow:\s*ellipsis/, '清单不得截断编码') assert.match(blocked, /\.map-blocked__code \{[\s\S]*?overflow-wrap:\s*anywhere/, '编码必须换行显示') // ---- ④e 「容器异常占用」与编码被裁已根治 ---- assert.match(blocked, /\.map-blocked\.map-blocked--embedded[\s\S]*?max-height:\s*none/, '嵌入态必须解除浮块的 max-height') assert.match(blocked, /\.map-blocked\.map-blocked--embedded[\s\S]*?align-self:\s*start/, '嵌入态不得被父容器拉高') assert.match(blocked, /overflow:\s*visible/, '嵌入态不得再被自身滚动裁掉') // 编码被裁的根因:并排时编码列是 `auto` 轨道,会撑到 max-content 顶出容器。 assert.match(blocked, /\.map-blocked\.map-blocked--embedded \.map-blocked__list li \{[\s\S]*?grid-template-columns:\s*minmax\(0, 1fr\)/, '嵌入态清单必须单列,长编码不再被裁') assert.match(blocked, /\.map-blocked\.map-blocked--embedded \.map-blocked__code \{[\s\S]*?word-break:\s*break-all/, '长编码必须能断行') // 图层清单不得静默消失(曾因模板仍引用已删除的 layers prop 而整块不渲染)。 assert.match(infoPanel, /v-for="layer in LAYERS"/, '图层清单必须渲染') assert.doesNotMatch(infoPanel, /v-for="layer in layers"/, '不得引用已移除的 layers prop') assert.match(shell, /import MapInfoPanel from/, '外壳必须引入地图信息内容块') // ---- ④f 子项宽度必须撑满容器(维护者反馈:展开后清单宽度与容器严重不符)---- // 根因:`.map-info` 是 grid,`justify-items: start` 会作用到**所有子项**, // 让它们按 fit-content 收缩 —— 展开的清单因此比容器窄一大截。 // 只匹配**真实声明**(注释里提到这个词不算数)。 assert.doesNotMatch(infoPanel, /^\s*justify-items:\s*start/m, '内容块不得用 justify-items 收窄子项') assert.match(infoPanel, /\.map-info > \* \{[\s\S]*?justify-self:\s*stretch/, '子项必须显式撑满容器宽度') assert.match(blocked, /\.map-blocked\.map-blocked--embedded[\s\S]*?max-width:\s*100%/, '嵌入块必须允许撑满容器') // ---- ⑤ 走查断言面一个都不能丢(面板搬家 ≠ 走查失灵)---- assert.match(infoPanel, /data-map-marker-count/, '标记计数断言面') assert.match(infoPanel, /data-map-degraded-note/, '降级说明断言面') assert.match(infoPanel, /]*:embedded="true"/, '坐标准入浮块并入面板且进入嵌入模式') assert.match(blocked, /data-map-blocked-panel/, '坐标准入面板断言面') assert.match(blocked, /map-blocked--embedded/, '嵌入模式样式类') assert.match(blocked, /position:\s*static/, '嵌入后不再是绝对定位浮块') // ---- ⑥ 引擎读数靠「搬节点」而不是重算,避免出现第二份真相 ---- assert.match(mapService, /className = 'map-engine-status'/) assert.match(infoPanel, /querySelector\('\.map-engine-status'\)/, '面板提供插槽并搬入引擎节点') assert.doesNotMatch(mapService, /createViewer[\s\S]{0,400}creditContainer:\s*new CreditDisplay/, '不得另建一份署名') console.log('T25: credit container off-screen, no ion dependency, credits owned by panel, only legend stays on map, walkthrough hooks kept.')