/**
* 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(!/