/** * 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 \}\}/, '占位文本契约变化') assert.match( sourceText.get('src/modules/cockpit/CockpitShell.vue'), //, '外壳未接入面板占位组件', ) }) 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(!/