/** * T32 真机走查:界面形态(开发诊断 vs 生产干净)。 * * 维护者 2026-09-18 要求:正式环境不要那么多明显文本压在中央 3D 场景上,状态信息收成 * 底部半透明小字;**颜色语义(点位状态图例、状态灯)必须保留**。 * * 断言的两种形态(同一个 dev server,靠注入配置切换,所以不依赖构建模式): * · `map.ui.debug: false` → 干净形态:标题/图层占位/实例提示/顶栏配置标签/当前模式读数都不在; * 图例与状态灯在;引擎状态在**底部且半透明、字号更小**; * · `map.ui.debug: true` → 调试形态:以上都在(且与票 04/09/21/29 走查依赖的选择器一致)。 * * 运行(需先起 `yarn dev`):node .scratch/cockpit-shell-panel-layout/verify/t32-ui-mode-walkthrough.mjs */ import { readFileSync } from 'node:fs' import { join } from 'node:path' import { tmpdir } from 'node:os' const BASE = process.env.COCKPIT_BASE || 'http://localhost:8000' const ENTRY_URL = process.env.COCKPIT_URL || `${BASE}/#/cockpit` const CHROME = process.env.CHROME_PATH || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' const PUPPETEER = 'file:///C:/Users/ASUS/.dsh/profiles/desktop/node_modules/puppeteer-core/lib/puppeteer/puppeteer-core.js' const OUT_PROD = join(tmpdir(), 'cockpit-t32-production.png') const OUT_DEBUG = join(tmpdir(), 'cockpit-t32-debug.png') const config = JSON.parse(readFileSync(new URL('../../../public/cockpit/config.json', import.meta.url), 'utf8')) const results = [] function check(id, desc, pass, evidence) { results.push({ id, desc, pass }) console.log(`${pass ? 'PASS' : 'FAIL'} ${id} ${desc}${evidence ? ` — ${evidence}` : ''}`) } const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) const puppeteer = (await import(PUPPETEER)).default const browser = await puppeteer.launch({ executablePath: CHROME, headless: true, userDataDir: join(tmpdir(), 'cockpit-t32-profile'), defaultViewport: { width: 1680, height: 950 }, args: ['--no-sandbox', '--disable-dev-shm-usage', '--no-first-run', '--no-default-browser-check'], }) console.log(`Chrome: ${await browser.version()}`) /** 打开一页,注入 `map.ui.debug` 指定的形态,等引擎就绪后把「界面清单」读回来。 */ async function openWith(uiDebug, screenshotPath) { const page = await browser.newPage() const errors = [] page.on('pageerror', (e) => errors.push(String(e && e.message ? e.message : e))) await page.evaluateOnNewDocument( (cfg, debug) => { window.__COCKPIT_CONFIG__ = { ...cfg, map: { ...cfg.map, intro: { enabled: false }, ui: { debug } }, } }, config, uiDebug, ) await page.goto(ENTRY_URL, { waitUntil: 'networkidle2', timeout: 60000 }) await page.waitForSelector('.cockpit-shell', { timeout: 20000 }) await page.waitForFunction( () => document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine === 'ready', { timeout: 60000 }, ) await sleep(2500) if (screenshotPath) await page.screenshot({ path: screenshotPath }) const inventory = await page.evaluate(() => { const pill = document.querySelector('[data-map-engine-status]') const pillStyle = pill ? getComputedStyle(pill) : null const statusLine = document.querySelector('.map-holo__status') const hostRect = document.querySelector('.cockpit-shell__map .map-canvas')?.getBoundingClientRect() const pillRect = pill ? pill.getBoundingClientRect() : null return { uiMode: pill ? pill.dataset.uiMode : '', // 票 33 起图例常驻一张图最上方,所以 `.map-holo__top` 在干净形态下也在; // 干净形态要收起的是它里面的**调试抬头**(标题 + 图层占位)。 holoTop: document.querySelectorAll('.map-holo__top').length, headline: document.querySelectorAll('.map-holo__headline').length, placeholderTitle: document.querySelectorAll('.map-canvas__title').length, devHint: document.querySelectorAll('.map-holo__hint').length, configTag: document.querySelectorAll('.cockpit-shell__config').length, modeReadout: document.querySelectorAll('.cockpit-shell__meta').length, // 用户需要看的东西:颜色语义与图层控件 legend: document.querySelectorAll('[data-map-legend] .status-tag').length, statusLine: document.querySelectorAll('.map-holo__status').length, statusDot: document.querySelectorAll('.map-holo__status i').length, homeButton: document.querySelectorAll('.cockpit-shell__home').length, viewButtons: document.querySelectorAll('.cockpit-shell__view-btn').length, scopeButtons: document.querySelectorAll('[data-scope]').length, // 引擎状态那一行的位置与观感 pillBottomOffset: pillRect && hostRect ? Math.round(hostRect.bottom - pillRect.bottom) : null, pillTopOffset: pillRect && hostRect ? Math.round(pillRect.top - hostRect.top) : null, pillFontSize: pillStyle ? parseFloat(pillStyle.fontSize) : null, pillOpacity: pillStyle ? Number(pillStyle.color.match(/[\d.]+\)$/) ? pillStyle.color.match(/[\d.]+\s*\)/)[0].replace(')', '') : 1) : null, pillHasBackground: pillStyle ? pillStyle.backgroundColor !== 'rgba(0, 0, 0, 0)' : null, quietClass: statusLine ? statusLine.classList.contains('is-quiet') : null, statusFontSize: statusLine ? parseFloat(getComputedStyle(statusLine).fontSize) : null, pillText: pill ? pill.textContent.trim() : '', } }) return { page, inventory, errors } } try { // ---------------- 生产干净形态 ---------------- const prod = await openWith(false, OUT_PROD) const p = prod.inventory check( 'A1-prod-chrome-stripped', '干净形态:中央图上的调试抬头(标题/图层占位)、实例提示与顶栏诊断读数全部收起', p.headline === 0 && p.placeholderTitle === 0 && p.devHint === 0 && p.configTag === 0 && p.modeReadout === 0, JSON.stringify({ headline: p.headline, title: p.placeholderTitle, hint: p.devHint, configTag: p.configTag, mode: p.modeReadout }), ) check( 'A2-prod-keeps-color-semantics', '干净形态**保留颜色语义**:点位状态图例、状态灯、图层控件与归位按钮都还在', p.legend > 0 && p.statusDot > 0 && p.statusLine > 0 && p.homeButton > 0 && p.viewButtons >= 2 && p.scopeButtons > 0, JSON.stringify({ legend: p.legend, dots: p.statusDot, home: p.homeButton, viewBtns: p.viewButtons, scopes: p.scopeButtons }), ) check( 'A3-prod-pill-is-quiet-bottom', '引擎状态收紧成**右下角状态日志**:贴底、无底色、字号更小、颜色带透明度', p.uiMode === 'production' && p.pillBottomOffset !== null && p.pillBottomOffset <= 16 && p.pillHasBackground === false && p.pillFontSize <= 10 && p.pillOpacity !== null && p.pillOpacity < 1, JSON.stringify({ uiMode: p.uiMode, bottom: p.pillBottomOffset, bg: p.pillHasBackground, font: p.pillFontSize, alpha: p.pillOpacity }), ) check( 'A4-prod-status-line-quiet', '底部摘要行同步收敛(is-quiet:字号更小、字色更淡)', p.quietClass === true && p.statusFontSize <= 10, JSON.stringify({ quietClass: p.quietClass, font: p.statusFontSize }), ) check('A5-prod-no-errors', '干净形态无页面级报错', prod.errors.length === 0, JSON.stringify(prod.errors.slice(0, 3))) await prod.page.close() // ---------------- 调试形态(回归:既有走查依赖的选择器必须还在) ---------------- const debug = await openWith(true, OUT_DEBUG) const d = debug.inventory check( 'A6-debug-keeps-everything', '调试形态保持原样:调试抬头/实例提示/配置标签/模式读数都在(票 04/09/21/29 走查依赖的选择器不变)', d.headline === 1 && d.placeholderTitle === 1 && d.devHint === 1 && d.configTag === 1 && d.modeReadout === 1 && d.uiMode === 'debug', JSON.stringify({ headline: d.headline, title: d.placeholderTitle, hint: d.devHint, configTag: d.configTag, mode: d.modeReadout, uiMode: d.uiMode }), ) check( 'A7-debug-pill-keeps-reading', '调试形态仍是可读的药丸(有底色、字号 11),位置让开顶部的图例与标题', d.pillHasBackground === true && d.pillFontSize === 11 && d.pillTopOffset !== null && d.pillTopOffset >= 80, JSON.stringify({ top: d.pillTopOffset, bg: d.pillHasBackground, font: d.pillFontSize }), ) check( 'A8-both-forms-read-state', '两种形态都能读到引擎状态(形态只改呈现,不改可观测性)', p.pillText.includes('地形') && d.pillText.includes('地形'), JSON.stringify({ prod: p.pillText.slice(0, 40), debug: d.pillText.slice(0, 40) }), ) check('A9-debug-no-errors', '调试形态无页面级报错', debug.errors.length === 0, JSON.stringify(debug.errors.slice(0, 3))) await debug.page.close() } finally { await browser.close() } const failed = results.filter((r) => !r.pass) console.log('') console.log(`T32 走查:${results.length - failed.length}/${results.length} 项通过。截图:干净 ${OUT_PROD} / 调试 ${OUT_DEBUG}`) if (failed.length) { console.log('未通过:' + failed.map((f) => f.id).join(', ')) process.exit(1) } console.log('T32 走查全部通过。')