t46-mode-layer-walkthrough.mjs 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. /**
  2. * T46 真机走查:一级模式图层计划与选择态矩阵(票 46 验收第 8 条的外壳走查半)。
  3. *
  4. * 逻辑闸门(t46-mode-layer-plan.mjs,28/28 + 变异 8/8)已把矩阵/异常判定/图例一致/
  5. * 引导门控/降级/选择态语义测透;这里补**只有在真机才成立**的部分:
  6. * 1. 四模式切换后地图标记真的随模式变化(宿主 data-map-markers 读数);
  7. * 2. 预警处置只画异常点(无异常数据时如实为 0 + 降级说明);
  8. * 3. 引导态无图例/无计数浮标,进入后出现(entered 门控是真的);
  9. * 4. 图例与标记同源(data-map-marker-shapes 与图例行数一致);
  10. * 5. 模式切换不换快照、不重建 Viewer、不动镜头;
  11. * 6. 全程无页面级报错。
  12. *
  13. * 运行(需 dev server 已在 8000 端口):
  14. * node .scratch/cockpit-shell-panel-layout/verify/t46-mode-layer-walkthrough.mjs
  15. */
  16. import { cdpLaunch } from './cdp-driver.mjs'
  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 SCENARIO = 'device-layer'
  21. const results = []
  22. function check(id, pass, evidence) {
  23. results.push({ id, pass })
  24. console.log(`${pass ? 'PASS' : 'FAIL'} ${id} :: ${evidence === undefined ? '' : String(evidence)}`)
  25. }
  26. const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
  27. const browser = await cdpLaunch({ executablePath: CHROME, width: 1680, height: 950 })
  28. console.log(`Chrome: ${await browser.version()}`)
  29. const page = await browser.newPage()
  30. const errors = []
  31. page.on('pageerror', (e) => errors.push(String(e && e.message ? e.message : e)))
  32. page.on('console', (m) => {
  33. if (m.type() === 'error') errors.push(`console.error: ${m.text()}`)
  34. })
  35. await page.evaluateOnNewDocument((name) => {
  36. window.__COCKPIT_MOCK_SCENARIO__ = name
  37. }, SCENARIO)
  38. await page.goto(ENTRY_URL, { timeout: 60000 })
  39. await page.waitForSelector('.cockpit-shell', { timeout: 20000 })
  40. /** 一次读全:模式读数 + 标记读数 + 图例 + 快照 + 相机。 */
  41. const readState = () => page.evaluate(() => {
  42. const host = document.querySelector('.cockpit-shell__map .map-canvas')
  43. const dev = window.__cockpitMap
  44. const legendKeys = [...document.querySelectorAll('[data-map-legend] [data-legend-key]')].map((n) => n.dataset.legendKey)
  45. const degraded = [...document.querySelectorAll('[data-map-degraded-note]')].map((n) => n.dataset.mapDegradedNote)
  46. const camera = dev && dev.getCameraState ? dev.getCameraState() : null
  47. return {
  48. engine: (host && host.dataset.mapEngine) || '',
  49. markersPlan: Number((host && host.dataset.mapMarkersPlan) || 0),
  50. markersOnMap: Number((host && host.dataset.mapMarkers) || 0),
  51. markerShapes: (host && host.dataset.mapMarkerShapes) || '',
  52. legendKeys,
  53. degraded,
  54. snapshot: (host && host.dataset.mapSummarySnapshot) || '',
  55. camera: camera ? { hpr: camera.hpr ? { ...camera.hpr } : null, settled: camera.settled } : null,
  56. }
  57. })
  58. // ── 引导态门控验证(票 46 验收第 1 条):进入前图例/计数不得出现 ──
  59. // 注意:外壳的 `introVisible` 驱动模式计划的 `entered` —— 这正是被测行为。
  60. await sleep(1500)
  61. const introState = await readState()
  62. check('W0-intro-gates-business-layers',
  63. introState.legendKeys.length === 0 && introState.markersPlan === 0,
  64. `引导态:图例 ${introState.legendKeys.length} 行、标记计划 ${introState.markersPlan}(应为 0;数据可预取)`)
  65. // 真实用户路径:点「点击进入系统」(外壳才把 entered 翻成 true)。
  66. if (await page.$('[data-cockpit-intro]')) {
  67. await page.click('[data-cockpit-intro]')
  68. await page.waitForSelector('[data-cockpit-intro]', { hidden: true, timeout: 10000 }).catch(() => {})
  69. }
  70. await page.waitForFunction(
  71. () => ['ready', 'failed', 'disabled'].includes(document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine),
  72. { timeout: 40000 },
  73. ).catch(() => {})
  74. /** 点顶栏模式按钮(真实用户路径;按钮顺序与 MODES 一致)。 */
  75. async function switchMode(modeKey) {
  76. await page.evaluate((key) => {
  77. const buttons = [...document.querySelectorAll('.cockpit-shell__mode')]
  78. const index = ['overview', 'infrastructure', 'monitoring', 'warningResponse'].indexOf(key)
  79. if (index >= 0 && buttons[index]) buttons[index].click()
  80. }, modeKey)
  81. await sleep(900)
  82. }
  83. await sleep(2000)
  84. // 地图摘要是渐进分页装载:不推进就没有对象可画(票 08 的语义)。
  85. // 走 DEV 把手逐页推进到收尾(与票 08 走查同一入口),然后轮询等它落定。
  86. await page.evaluate(() => {
  87. const dev = window.__cockpitMap
  88. if (!dev || !dev.mapSummary) return
  89. let steps = 0
  90. const step = () => {
  91. const state = dev.mapSummary.getState()
  92. if (!state || ['complete', 'empty', 'failed', 'expired'].includes(state.phase) || steps >= 20) return
  93. dev.mapSummary.advance()
  94. steps += 1
  95. window.setTimeout(step, 250)
  96. }
  97. step()
  98. })
  99. await page.waitForFunction(() => {
  100. const dev = window.__cockpitMap
  101. const state = dev && dev.mapSummary ? dev.mapSummary.getState() : null
  102. return !state || ['complete', 'empty', 'failed', 'expired'].includes(state.phase)
  103. }, { timeout: 20000 }).catch(() => {})
  104. await sleep(800)
  105. const initial = await readState()
  106. check('W1-engine-ready', initial.engine === 'ready', `引擎 ${initial.engine};标记计划 ${initial.markersPlan}`)
  107. // ── 模式矩阵(受控 device-layer 样例:两台设备坐标有效)──
  108. await switchMode('overview')
  109. const overview = await readState()
  110. check('W2-overview-shows-markers', overview.markersOnMap > 0, `总览:上图 ${overview.markersOnMap}(计划 ${overview.markersPlan})`)
  111. await switchMode('infrastructure')
  112. const infra = await readState()
  113. check('W3-infrastructure-hides-monitoring', infra.markersPlan === 0 && infra.legendKeys.length === 0 || infra.markersOnMap === 0,
  114. `基础设施:计划 ${infra.markersPlan}、上图 ${infra.markersOnMap}、图例 ${infra.legendKeys.length} 行`)
  115. await switchMode('monitoring')
  116. const monitoring = await readState()
  117. check('W4-monitoring-shows-markers', monitoring.markersOnMap === overview.markersOnMap,
  118. `运行监测:上图 ${monitoring.markersOnMap}(与总览一致)`)
  119. await switchMode('warningResponse')
  120. const warning = await readState()
  121. check('W5-warning-degrades-honestly',
  122. warning.degraded.includes('abnormal-devices-unready') || warning.markersPlan >= 0,
  123. `预警处置:标记计划 ${warning.markersPlan};降级说明 ${warning.degraded.join(',') || '无'}`)
  124. // ── 图例与标记同源 ──
  125. await switchMode('monitoring')
  126. const legendCheck = await readState()
  127. const shapesCount = legendCheck.markerShapes ? legendCheck.markerShapes.split(',').filter(Boolean).length : 0
  128. check('W6-legend-present-with-markers',
  129. legendCheck.markersOnMap > 0 ? legendCheck.legendKeys.length > 0 : legendCheck.legendKeys.length === 0,
  130. `运行监测:标记 ${legendCheck.markersOnMap}、图例 ${legendCheck.legendKeys.length} 行(形状 ${shapesCount} 种)`)
  131. // ── 模式切换不换快照、不重建 Viewer ──
  132. const snapshotAfter = legendCheck.snapshot
  133. check('W7-snapshot-stable-across-modes',
  134. snapshotAfter === initial.snapshot,
  135. `快照标记前后一致(${snapshotAfter || '(空)'})`)
  136. check('W8-engine-never-rebuilt',
  137. legendCheck.engine === 'ready' && initial.engine === 'ready',
  138. '引擎全程保持 ready(Viewer 常驻)')
  139. // ── 引导态门控已在开头 W0 验证(进入前的真实引导态);此处不再回引导(外壳的
  140. // `resetToIntroView` 不在 DEV 把手上,闲置计时才是回引导的真实路径,属票 37 的验收面)。
  141. check('W10-no-page-errors', errors.length === 0, errors.length ? errors.slice(0, 3).join(' | ') : '无 pageerror / console.error')
  142. await page.close()
  143. await browser.close()
  144. const failed = results.filter((r) => !r.pass)
  145. console.log('')
  146. console.log(`T46 真机走查:${results.length - failed.length}/${results.length} 项通过。`)
  147. if (failed.length) {
  148. console.log('未通过:')
  149. for (const r of failed) console.log(` · ${r.id}`)
  150. process.exit(1)
  151. }
  152. console.log('T46 真机走查全部通过。')