t32-ui-mode-walkthrough.mjs 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. /**
  2. * T32 真机走查:界面形态(开发诊断 vs 生产干净)。
  3. *
  4. * 维护者 2026-09-18 要求:正式环境不要那么多明显文本压在中央 3D 场景上,状态信息收成
  5. * 底部半透明小字;**颜色语义(点位状态图例、状态灯)必须保留**。
  6. *
  7. * 断言的两种形态(同一个 dev server,靠注入配置切换,所以不依赖构建模式):
  8. * · `map.ui.debug: false` → 干净形态:标题/图层占位/实例提示/顶栏配置标签/当前模式读数都不在;
  9. * 图例与状态灯在;引擎状态在**底部且半透明、字号更小**;
  10. * · `map.ui.debug: true` → 调试形态:以上都在(且与票 04/09/21/29 走查依赖的选择器一致)。
  11. *
  12. * 运行(需先起 `yarn dev`):node .scratch/cockpit-shell-panel-layout/verify/t32-ui-mode-walkthrough.mjs
  13. */
  14. import { readFileSync } from 'node:fs'
  15. import { join } from 'node:path'
  16. import { tmpdir } from 'node:os'
  17. const BASE = process.env.COCKPIT_BASE || 'http://localhost:8000'
  18. const ENTRY_URL = process.env.COCKPIT_URL || `${BASE}/#/cockpit`
  19. const CHROME = process.env.CHROME_PATH || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
  20. const PUPPETEER = 'file:///C:/Users/ASUS/.dsh/profiles/desktop/node_modules/puppeteer-core/lib/puppeteer/puppeteer-core.js'
  21. const OUT_PROD = join(tmpdir(), 'cockpit-t32-production.png')
  22. const OUT_DEBUG = join(tmpdir(), 'cockpit-t32-debug.png')
  23. const config = JSON.parse(readFileSync(new URL('../../../public/cockpit/config.json', import.meta.url), 'utf8'))
  24. const results = []
  25. function check(id, desc, pass, evidence) {
  26. results.push({ id, desc, pass })
  27. console.log(`${pass ? 'PASS' : 'FAIL'} ${id} ${desc}${evidence ? ` — ${evidence}` : ''}`)
  28. }
  29. const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
  30. const puppeteer = (await import(PUPPETEER)).default
  31. const browser = await puppeteer.launch({
  32. executablePath: CHROME,
  33. headless: true,
  34. userDataDir: join(tmpdir(), 'cockpit-t32-profile'),
  35. defaultViewport: { width: 1680, height: 950 },
  36. args: ['--no-sandbox', '--disable-dev-shm-usage', '--no-first-run', '--no-default-browser-check'],
  37. })
  38. console.log(`Chrome: ${await browser.version()}`)
  39. /** 打开一页,注入 `map.ui.debug` 指定的形态,等引擎就绪后把「界面清单」读回来。 */
  40. async function openWith(uiDebug, screenshotPath) {
  41. const page = await browser.newPage()
  42. const errors = []
  43. page.on('pageerror', (e) => errors.push(String(e && e.message ? e.message : e)))
  44. await page.evaluateOnNewDocument(
  45. (cfg, debug) => {
  46. window.__COCKPIT_CONFIG__ = {
  47. ...cfg,
  48. map: { ...cfg.map, intro: { enabled: false }, ui: { debug } },
  49. }
  50. },
  51. config,
  52. uiDebug,
  53. )
  54. await page.goto(ENTRY_URL, { waitUntil: 'networkidle2', timeout: 60000 })
  55. await page.waitForSelector('.cockpit-shell', { timeout: 20000 })
  56. await page.waitForFunction(
  57. () => document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine === 'ready',
  58. { timeout: 60000 },
  59. )
  60. await sleep(2500)
  61. if (screenshotPath) await page.screenshot({ path: screenshotPath })
  62. const inventory = await page.evaluate(() => {
  63. const pill = document.querySelector('[data-map-engine-status]')
  64. const pillStyle = pill ? getComputedStyle(pill) : null
  65. const statusLine = document.querySelector('.map-holo__status')
  66. const hostRect = document.querySelector('.cockpit-shell__map .map-canvas')?.getBoundingClientRect()
  67. const pillRect = pill ? pill.getBoundingClientRect() : null
  68. return {
  69. uiMode: pill ? pill.dataset.uiMode : '',
  70. // 票 33 起图例常驻一张图最上方,所以 `.map-holo__top` 在干净形态下也在;
  71. // 干净形态要收起的是它里面的**调试抬头**(标题 + 图层占位)。
  72. holoTop: document.querySelectorAll('.map-holo__top').length,
  73. headline: document.querySelectorAll('.map-holo__headline').length,
  74. placeholderTitle: document.querySelectorAll('.map-canvas__title').length,
  75. devHint: document.querySelectorAll('.map-holo__hint').length,
  76. configTag: document.querySelectorAll('.cockpit-shell__config').length,
  77. modeReadout: document.querySelectorAll('.cockpit-shell__meta').length,
  78. // 用户需要看的东西:颜色语义与图层控件
  79. legend: document.querySelectorAll('[data-map-legend] .status-tag').length,
  80. statusLine: document.querySelectorAll('.map-holo__status').length,
  81. statusDot: document.querySelectorAll('.map-holo__status i').length,
  82. homeButton: document.querySelectorAll('.cockpit-shell__home').length,
  83. viewButtons: document.querySelectorAll('.cockpit-shell__view-btn').length,
  84. scopeButtons: document.querySelectorAll('[data-scope]').length,
  85. // 引擎状态那一行的位置与观感
  86. pillBottomOffset: pillRect && hostRect ? Math.round(hostRect.bottom - pillRect.bottom) : null,
  87. pillTopOffset: pillRect && hostRect ? Math.round(pillRect.top - hostRect.top) : null,
  88. pillFontSize: pillStyle ? parseFloat(pillStyle.fontSize) : null,
  89. pillOpacity: pillStyle ? Number(pillStyle.color.match(/[\d.]+\)$/) ? pillStyle.color.match(/[\d.]+\s*\)/)[0].replace(')', '') : 1) : null,
  90. pillHasBackground: pillStyle ? pillStyle.backgroundColor !== 'rgba(0, 0, 0, 0)' : null,
  91. quietClass: statusLine ? statusLine.classList.contains('is-quiet') : null,
  92. statusFontSize: statusLine ? parseFloat(getComputedStyle(statusLine).fontSize) : null,
  93. pillText: pill ? pill.textContent.trim() : '',
  94. }
  95. })
  96. return { page, inventory, errors }
  97. }
  98. try {
  99. // ---------------- 生产干净形态 ----------------
  100. const prod = await openWith(false, OUT_PROD)
  101. const p = prod.inventory
  102. check(
  103. 'A1-prod-chrome-stripped',
  104. '干净形态:中央图上的调试抬头(标题/图层占位)、实例提示与顶栏诊断读数全部收起',
  105. p.headline === 0 && p.placeholderTitle === 0 && p.devHint === 0 && p.configTag === 0 && p.modeReadout === 0,
  106. JSON.stringify({ headline: p.headline, title: p.placeholderTitle, hint: p.devHint, configTag: p.configTag, mode: p.modeReadout }),
  107. )
  108. check(
  109. 'A2-prod-keeps-color-semantics',
  110. '干净形态**保留颜色语义**:点位状态图例、状态灯、图层控件与归位按钮都还在',
  111. p.legend > 0 && p.statusDot > 0 && p.statusLine > 0 && p.homeButton > 0 && p.viewButtons >= 2 && p.scopeButtons > 0,
  112. JSON.stringify({ legend: p.legend, dots: p.statusDot, home: p.homeButton, viewBtns: p.viewButtons, scopes: p.scopeButtons }),
  113. )
  114. check(
  115. 'A3-prod-pill-is-quiet-bottom',
  116. '引擎状态收紧成**右下角状态日志**:贴底、无底色、字号更小、颜色带透明度',
  117. p.uiMode === 'production' &&
  118. p.pillBottomOffset !== null &&
  119. p.pillBottomOffset <= 16 &&
  120. p.pillHasBackground === false &&
  121. p.pillFontSize <= 10 &&
  122. p.pillOpacity !== null &&
  123. p.pillOpacity < 1,
  124. JSON.stringify({ uiMode: p.uiMode, bottom: p.pillBottomOffset, bg: p.pillHasBackground, font: p.pillFontSize, alpha: p.pillOpacity }),
  125. )
  126. check(
  127. 'A4-prod-status-line-quiet',
  128. '底部摘要行同步收敛(is-quiet:字号更小、字色更淡)',
  129. p.quietClass === true && p.statusFontSize <= 10,
  130. JSON.stringify({ quietClass: p.quietClass, font: p.statusFontSize }),
  131. )
  132. check('A5-prod-no-errors', '干净形态无页面级报错', prod.errors.length === 0, JSON.stringify(prod.errors.slice(0, 3)))
  133. await prod.page.close()
  134. // ---------------- 调试形态(回归:既有走查依赖的选择器必须还在) ----------------
  135. const debug = await openWith(true, OUT_DEBUG)
  136. const d = debug.inventory
  137. check(
  138. 'A6-debug-keeps-everything',
  139. '调试形态保持原样:调试抬头/实例提示/配置标签/模式读数都在(票 04/09/21/29 走查依赖的选择器不变)',
  140. d.headline === 1 &&
  141. d.placeholderTitle === 1 &&
  142. d.devHint === 1 &&
  143. d.configTag === 1 &&
  144. d.modeReadout === 1 &&
  145. d.uiMode === 'debug',
  146. JSON.stringify({ headline: d.headline, title: d.placeholderTitle, hint: d.devHint, configTag: d.configTag, mode: d.modeReadout, uiMode: d.uiMode }),
  147. )
  148. check(
  149. 'A7-debug-pill-keeps-reading',
  150. '调试形态仍是可读的药丸(有底色、字号 11),位置让开顶部的图例与标题',
  151. d.pillHasBackground === true && d.pillFontSize === 11 && d.pillTopOffset !== null && d.pillTopOffset >= 80,
  152. JSON.stringify({ top: d.pillTopOffset, bg: d.pillHasBackground, font: d.pillFontSize }),
  153. )
  154. check(
  155. 'A8-both-forms-read-state',
  156. '两种形态都能读到引擎状态(形态只改呈现,不改可观测性)',
  157. p.pillText.includes('地形') && d.pillText.includes('地形'),
  158. JSON.stringify({ prod: p.pillText.slice(0, 40), debug: d.pillText.slice(0, 40) }),
  159. )
  160. check('A9-debug-no-errors', '调试形态无页面级报错', debug.errors.length === 0, JSON.stringify(debug.errors.slice(0, 3)))
  161. await debug.page.close()
  162. } finally {
  163. await browser.close()
  164. }
  165. const failed = results.filter((r) => !r.pass)
  166. console.log('')
  167. console.log(`T32 走查:${results.length - failed.length}/${results.length} 项通过。截图:干净 ${OUT_PROD} / 调试 ${OUT_DEBUG}`)
  168. if (failed.length) {
  169. console.log('未通过:' + failed.map((f) => f.id).join(', '))
  170. process.exit(1)
  171. }
  172. console.log('T32 走查全部通过。')