| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224 |
- /**
- * T06 检查:驾驶舱自有静态资产、基础 token、占位/图标/状态标签,以及资源引用走查记录。
- *
- * 仓库没有测试运行器,项目约定不为此引入新框架(同 T01),故用零依赖 Node 断言脚本。
- *
- * 运行:node .scratch/cockpit-shell-panel-layout/verify/t06-assets.mjs
- */
- import assert from 'node:assert/strict'
- import { readFileSync, readdirSync, statSync } from 'node:fs'
- import { dirname, join, relative } from 'node:path'
- import { fileURLToPath } from 'node:url'
- import { LEVEL_TAGS, TONES, isKnownTone } from '../../../src/modules/cockpit/domain/statusTones.js'
- import { ICON_NAMES, iconFor, isKnownIcon } from '../../../src/modules/cockpit/domain/icons.js'
- import { MODES, PANEL_LABELS, panelsForMode } from '../../../src/modules/cockpit/domain/panelCatalog.js'
- const here = dirname(fileURLToPath(import.meta.url))
- const root = join(here, '..', '..', '..')
- const cockpitDir = join(root, 'src', 'modules', 'cockpit')
- const publicCockpit = join(root, 'public', 'cockpit')
- const problems = []
- function check(name, fn) {
- try {
- fn()
- console.log(` PASS ${name}`)
- } catch (error) {
- problems.push(`${name}: ${error.message}`)
- console.log(` FAIL ${name}\n ${error.message}`)
- }
- }
- function read(path) {
- return readFileSync(path, 'utf8')
- }
- /** 递归收集目录下所有文件(相对仓库根的 POSIX 路径)。 */
- function walk(dir) {
- return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
- const full = join(dir, entry.name)
- if (entry.isDirectory()) return walk(full)
- return [relative(root, full).split('\\').join('/')]
- })
- }
- const cockpitSources = walk(cockpitDir).concat(walk(join(root, 'src', 'pages', 'cockpit')))
- const sourceText = new Map(cockpitSources.map((p) => [p, read(join(root, p))]))
- console.log('\n[A] 资产目录自持')
- check('A1 public/cockpit 存在且含临时底图与说明', () => {
- const files = readdirSync(publicCockpit)
- assert.ok(files.includes('aerial.webp'), '缺少 aerial.webp')
- assert.ok(files.includes('README.md'), '缺少 README.md(资产用途与替换约定)')
- const size = statSync(join(publicCockpit, 'aerial.webp')).size
- assert.ok(size > 1024, `aerial.webp 只有 ${size} 字节,疑似空文件`)
- })
- check('A2 不依赖插件静态资源目录', () => {
- const patterns = [/public\/plugins/, /\/plugins\/[\w-]+/, /\bPLS\b/, /FES_APP_PLSNAME/]
- const hits = []
- for (const [file, text] of sourceText) {
- patterns.forEach((re) => {
- if (re.test(text)) hits.push(`${file} 命中 ${re}`)
- })
- }
- assert.deepEqual(hits, [], `发现插件目录依赖:\n${hits.join('\n')}`)
- })
- console.log('\n[B] 基础视觉 token')
- const tokensPath = 'src/modules/cockpit/styles/tokens.css'
- const tokens = sourceText.get(tokensPath)
- check('B1 tokens.css 存在并集中声明主题令牌', () => {
- assert.ok(tokens, `${tokensPath} 不存在`)
- assert.ok(/\.cockpit-shell\s*\{/.test(tokens), 'token 应声明在 .cockpit-shell 作用域下')
- assert.ok(!/:root\s*\{/.test(tokens), '不得挂 :root,否则通用变量名会漏进宿主与插件样式')
- })
- check('B2 颜色/字体/字号/间距/层级五类 token 齐全', () => {
- const required = [
- '--u',
- '--bg-0',
- '--panel-top',
- '--panel-line',
- '--cyan',
- '--lv1',
- '--lv2',
- '--lv3',
- '--lv4',
- '--neutral',
- '--text',
- '--muted',
- '--font',
- '--num',
- '--fs-caption',
- '--fs-body',
- '--fs-title',
- '--space-1',
- '--space-2',
- '--space-3',
- '--z-map',
- '--z-columns',
- '--shadow',
- ]
- const missing = required.filter((name) => !new RegExp(`${name}\\s*:`).test(tokens))
- assert.deepEqual(missing, [], `缺少 token: ${missing.join(', ')}`)
- })
- check('B3 语义色只在 token 文件里声明(不准各处再写一份)', () => {
- const offenders = []
- for (const [file, text] of sourceText) {
- if (file === tokensPath) continue
- ;['--lv1', '--lv2', '--lv3', '--lv4', '--neutral', '--cyan'].forEach((name) => {
- if (new RegExp(`${name}\\s*:`).test(text)) offenders.push(`${file} 重复声明 ${name}`)
- })
- }
- assert.deepEqual(offenders, [], offenders.join('\n'))
- })
- check('B4 外壳引入 token 文件', () => {
- const shell = sourceText.get('src/modules/cockpit/CockpitShell.vue')
- assert.match(shell, /import\s+'\.\/styles\/tokens\.css'/, '外壳未引入 tokens.css')
- })
- console.log('\n[C] 地图占位 / 面板占位 / 图标 / 状态标签')
- check('C1 面板占位组件存在且文本契约未变', () => {
- const file = 'src/modules/cockpit/components/PanelPlaceholder.vue'
- const text = sourceText.get(file)
- assert.ok(text, `${file} 不存在`)
- assert.match(text, /面板占位 · \{\{ panelId \}\}/, '占位文本契约变化')
- // 票 17:外壳按 panelId 分派 —— 四块走真实面板,未注册的 panelId 仍走占位。
- // 票 20 起 16 块面板已全部接上真实视图,所以占位组件现在是**未注册 panelId 的兜底**;
- // 这里断言的是「占位组件仍带着 panelId 绑定被真实使用」,
- // 而不是钉死它前面不许出现任何指令(`v-else` 不改变这条契约)。
- assert.match(
- sourceText.get('src/modules/cockpit/CockpitShell.vue'),
- /<PanelPlaceholder[^>]*:panel-id="panelId"/,
- '外壳未接入面板占位组件',
- )
- })
- check('C2 地图占位仍在(临时底图 + 占位文案)', () => {
- const text = sourceText.get('src/modules/cockpit/components/MapCanvas.vue')
- assert.match(text, /中央一张图占位/, '地图占位文案缺失')
- assert.match(text, /BASE_URL/, '底图应走 BASE_URL 拼接,兼容相对部署')
- assert.ok(!/['"`]\/cockpit\//.test(text), '不得硬编码 /cockpit/ 绝对路径')
- })
- check('C3 图标集完整且为纯标记(无脚本)', () => {
- const required = ['brand', 'layers', 'points', 'event', 'resource', 'blank']
- const missing = required.filter((name) => !ICON_NAMES.includes(name))
- assert.deepEqual(missing, [], `缺少图标: ${missing.join(', ')}`)
- ICON_NAMES.forEach((name) => {
- const icon = iconFor(name)
- assert.match(icon.viewBox, /^0 0 \d+ \d+$/, `${name} viewBox 非法`)
- assert.ok(icon.inner.trim().length > 0, `${name} 内容为空`)
- assert.ok(!/<script/i.test(icon.inner), `${name} 含脚本标记`)
- })
- assert.equal(isKnownIcon('nope'), false, '未知图标应返回 false')
- assert.equal(iconFor('nope'), null, '未知图标不应渲染')
- })
- check('C4 状态标签色键与事件四级一致', () => {
- assert.deepEqual(TONES, ['lv1', 'lv2', 'lv3', 'lv4', 'neutral'], '色键集合变化')
- assert.equal(LEVEL_TAGS.length, 4, '事件四级应为 4 项')
- LEVEL_TAGS.forEach((tag) => assert.equal(isKnownTone(tag.tone), true, `${tag.label} 色键非法`))
- assert.equal(isKnownTone('red'), false, '不许用裸颜色名当色键')
- assert.match(
- sourceText.get('src/modules/cockpit/components/StatusTag.vue'),
- /status-tag__label/,
- '状态标签必须带文字(ADR 0006 不只靠颜色)',
- )
- })
- check('C5 三个组件都被真实引用(非死代码)', () => {
- const used = (component, files) =>
- files.filter((f) => new RegExp(`<${component}[\\s/>]`).test(sourceText.get(f)))
- assert.deepEqual(
- used('CockpitIcon', ['src/modules/cockpit/CockpitShell.vue', 'src/modules/cockpit/components/MapCanvas.vue']).length,
- 2,
- 'CockpitIcon 未被外壳与地图同时使用',
- )
- assert.equal(
- used('StatusTag', ['src/modules/cockpit/components/MapCanvas.vue']).length,
- 1,
- 'StatusTag 未被地图图例使用',
- )
- assert.equal(
- used('PanelPlaceholder', ['src/modules/cockpit/CockpitShell.vue']).length,
- 1,
- 'PanelPlaceholder 未被外壳使用',
- )
- })
- console.log('\n[D] 视觉可替换、不影响结构')
- check('D1 模式与面板结构未随视觉变化', () => {
- assert.deepEqual(MODES, ['overview', 'infrastructure', 'monitoring', 'warningResponse'])
- assert.equal(Object.keys(PANEL_LABELS).length, 16, '面板应为 16 个')
- MODES.forEach((mode) => assert.equal(panelsForMode(mode).length, 4, `${mode} 应为 4 块`))
- })
- check('D2 结构定义里没有视觉信息', () => {
- const catalog = sourceText.get('src/modules/cockpit/domain/panelCatalog.js')
- assert.ok(!/#[0-9a-fA-F]{3,6}/.test(catalog), 'panelCatalog 不应出现颜色值')
- assert.ok(!/icon/i.test(catalog), 'panelCatalog 不应出现图标名')
- })
- console.log('\n[E] 资源引用走查记录')
- const references = []
- check('E1 public/cockpit 下每个资源都有引用点', () => {
- // 「资源」指要被代码引用的东西;说明与部署模板不是资源,不需要引用点:
- // README.md —— 讲约定
- // *.example.json —— 部署配置模板(票 04),由运维复制成 config.json,代码永远不引用它
- const notAnAsset = /^(README\.md|.*\.example\.json)$/
- const assets = readdirSync(publicCockpit).filter((f) => !notAnAsset.test(f))
- assert.ok(assets.length > 0, 'public/cockpit 下没有资源')
- assets.forEach((asset) => {
- const hits = cockpitSources.filter((file) => sourceText.get(file).includes(asset))
- references.push({ asset, hits })
- assert.ok(hits.length > 0, `${asset} 没有任何源码引用`)
- })
- })
- console.log('\n资源引用清单')
- references.forEach(({ asset, hits }) => {
- console.log(` public/cockpit/${asset}`)
- hits.forEach((hit) => console.log(` ← ${hit}`))
- })
- if (problems.length > 0) {
- console.error(`\nT06 未通过:${problems.length} 项失败\n`)
- process.exit(1)
- }
- console.log('\nT06 全部通过\n')
|