t36-primary-area-walkthrough.mjs 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. /**
  2. * T36 真机走查:主场景区配置在浏览器里**真的**透到浮标与宿主读数上(票 36 验收第 4 条)。
  3. *
  4. * 逻辑脚本(t36-primary-area.mjs)已把判定层测透,这里只测浏览器里才成立的部分:
  5. * · 合法配置 → 浮标出现「主场景区:N×M km」、`data-map-primary-area=ready`;
  6. * · 没配 → `missing`,浮标写明「未配置 · 限位与雾气关闭」;
  7. * · 顺序写反(甲方原样的 [W,N,E,S])→ `invalid`,浮标带顺序原因与「不自动翻转」,**且没有矩形**;
  8. * · 数值个数不对 → `invalid` 且原因是形状问题(与顺序问题分得开);
  9. * · 引擎被配置关闭(`map.cesium.enabled=false`)时,主场景区读数**照样如实**、
  10. * 浮标上也照样读得到 —— 它是配置事实,不该因为引擎没起来就消失
  11. * (逻辑闸门用最小 DOM 宿主已抓到过这条:修复前这句在浮标上是失声的)。
  12. *
  13. * 按 AGENTS.md:**本脚本不在改代码后自动运行**,由维护者明确要求时才跑(需先起 `yarn dev`)。
  14. * 运行:node .scratch/cockpit-shell-panel-layout/verify/t36-primary-area-walkthrough.mjs
  15. */
  16. import { join } from 'node:path'
  17. import { tmpdir } from 'node:os'
  18. const BASE = process.env.COCKPIT_BASE || 'http://localhost:8000'
  19. const ENTRY_URL = process.env.COCKPIT_URL || `${BASE}/#/cockpit`
  20. const CHROME = process.env.CHROME_PATH || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
  21. const PUPPETEER = 'file:///C:/Users/ASUS/.dsh/profiles/desktop/node_modules/puppeteer-core/lib/puppeteer/puppeteer-core.js'
  22. /** 本部署的显式值([西, 南, 东, 北])。走查独立写死,不 import 被测模块(否则是自己跟自己比)。 */
  23. const OK_BOUNDS = [110.328749941417, 28.3917230659234, 110.465779226663, 28.4913038992907]
  24. /** 甲方原样的顺序([W, N, E, S])—— 运行时必须判非法,不得被悄悄翻转。 */
  25. const SWAPPED_BOUNDS = [110.328749941417, 28.4913038992907, 110.465779226663, 28.3917230659234]
  26. const results = []
  27. function check(id, desc, pass, evidence) {
  28. results.push({ id, desc, pass })
  29. console.log(`${pass ? 'PASS' : 'FAIL'} ${id} ${desc}${evidence ? ` — ${evidence}` : ''}`)
  30. }
  31. const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
  32. const puppeteer = (await import(PUPPETEER)).default
  33. const browser = await puppeteer.launch({
  34. executablePath: CHROME,
  35. headless: true,
  36. userDataDir: join(tmpdir(), 'cockpit-t36-profile'),
  37. defaultViewport: { width: 1680, height: 950 },
  38. args: ['--no-sandbox', '--disable-dev-shm-usage', '--no-first-run', '--no-default-browser-check'],
  39. })
  40. console.log(`Chrome: ${await browser.version()}`)
  41. /** 开一页注入配置,等引擎落到终态,读回主场景区的浮标与宿主读数。 */
  42. async function openPage(injectConfig, waitFor = ['ready', 'failed', 'disabled']) {
  43. const page = await browser.newPage()
  44. await page.evaluateOnNewDocument(injectConfig)
  45. await page.goto(ENTRY_URL, { waitUntil: 'networkidle2', timeout: 60000 })
  46. await page.waitForSelector('.cockpit-shell', { timeout: 20000 })
  47. await page
  48. .waitForFunction(
  49. (states) => states.includes(document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine),
  50. { timeout: 60000 },
  51. waitFor,
  52. )
  53. .catch(() => {})
  54. await sleep(1200)
  55. const state = await page.evaluate(() => {
  56. const host = document.querySelector('.cockpit-shell__map .map-canvas')
  57. return {
  58. engine: (host && host.dataset.mapEngine) || '',
  59. area: (host && host.dataset.mapPrimaryArea) || '',
  60. reason: (host && host.dataset.mapPrimaryAreaReason) || '',
  61. pill: host?.querySelector('[data-map-engine-status]')?.textContent?.trim() || '',
  62. plan: window.__cockpitMap ? window.__cockpitMap.getPrimaryAreaPlan() : null,
  63. }
  64. })
  65. return { page, state }
  66. }
  67. try {
  68. // ---------- A1 合法:生效读得到 ----------
  69. {
  70. const { page, state } = await openPage(() => {
  71. window.__COCKPIT_CONFIG__ = {
  72. map: { intro: { enabled: false }, primaryArea: { bounds: [110.328749941417, 28.3917230659234, 110.465779226663, 28.4913038992907] } },
  73. }
  74. })
  75. check(
  76. 'A1-ready-visible',
  77. '合法配置:宿主 data-map-primary-area=ready、浮标出现「主场景区:N×M km」、出口给出矩形',
  78. state.area === 'ready' &&
  79. /主场景区:\d+\.\d×\d+\.\d km/.test(state.pill) &&
  80. !!state.plan &&
  81. state.plan.enabled === true &&
  82. Array.isArray(state.plan.bounds) &&
  83. state.plan.bounds.length === 4,
  84. JSON.stringify({ area: state.area, reason: state.reason, pill: state.pill.slice(0, 80), plan: state.plan }),
  85. )
  86. await page.close()
  87. }
  88. // ---------- A2 未配置:关闭 + 如实提示 ----------
  89. {
  90. const { page, state } = await openPage(() => {
  91. window.__COCKPIT_CONFIG__ = { map: { intro: { enabled: false } } }
  92. })
  93. check(
  94. 'A2-missing-visible',
  95. '没配 primaryArea:data-map-primary-area=missing,浮标写明「未配置 · 限位与雾气关闭」,出口不给矩形',
  96. state.area === 'missing' &&
  97. state.reason.includes('未配置') &&
  98. state.pill.includes('限位与雾气关闭') &&
  99. !!state.plan &&
  100. state.plan.enabled === false &&
  101. state.plan.bounds === null,
  102. JSON.stringify({ area: state.area, reason: state.reason, pill: state.pill.slice(0, 80) }),
  103. )
  104. await page.close()
  105. }
  106. // ---------- A3 顺序写反:非法 + 不自动翻转(本票最容易做错的一条) ----------
  107. {
  108. const { page, state } = await openPage(() => {
  109. window.__COCKPIT_CONFIG__ = {
  110. map: {
  111. intro: { enabled: false },
  112. primaryArea: { bounds: [110.328749941417, 28.4913038992907, 110.465779226663, 28.3917230659234] },
  113. },
  114. }
  115. })
  116. check(
  117. 'A3-swapped-rejected',
  118. '甲方原样的 [W,N,E,S] 被判非法:data-map-primary-area=invalid、浮标点明顺序与「不自动翻转」、出口**不给矩形**',
  119. state.area === 'invalid' &&
  120. state.reason.includes('不自动翻转') &&
  121. state.pill.includes('配置无效') &&
  122. state.pill.includes('限位与雾气关闭') &&
  123. !!state.plan &&
  124. state.plan.enabled === false &&
  125. state.plan.bounds === null,
  126. JSON.stringify({ area: state.area, reason: state.reason, pill: state.pill.slice(0, 90), bounds: state.plan && state.plan.bounds }),
  127. )
  128. await page.close()
  129. }
  130. // ---------- A4 个数不对:非法,且原因与顺序问题分得开 ----------
  131. {
  132. const { page, state } = await openPage(() => {
  133. window.__COCKPIT_CONFIG__ = { map: { intro: { enabled: false }, primaryArea: { bounds: [110, 28, 111] } } }
  134. })
  135. check(
  136. 'A4-shape-rejected',
  137. '三个数字被判非法:原因说「四个有限数字」,与顺序问题的措辞不同',
  138. state.area === 'invalid' &&
  139. state.reason.includes('四个有限数字') &&
  140. state.reason.includes('顺序') === false &&
  141. state.pill.includes('限位与雾气关闭'),
  142. JSON.stringify({ area: state.area, reason: state.reason, pill: state.pill.slice(0, 90) }),
  143. )
  144. await page.close()
  145. }
  146. // ---------- A5 关引擎:主场景区读数照样如实 ----------
  147. {
  148. const { page, state } = await openPage(
  149. () => {
  150. window.__COCKPIT_CONFIG__ = {
  151. map: {
  152. cesium: { enabled: false },
  153. intro: { enabled: false },
  154. primaryArea: { bounds: [110.328749941417, 28.3917230659234, 110.465779226663, 28.4913038992907] },
  155. },
  156. }
  157. },
  158. ['disabled'],
  159. )
  160. check(
  161. 'A5-visible-even-with-engine-off',
  162. 'map.cesium.enabled=false:引擎不启动,但主场景区仍如实标 ready,**且浮标上照样读得到**(原生实现里这条是失声的)',
  163. state.engine === 'disabled' &&
  164. state.area === 'ready' &&
  165. state.pill.includes('主场景区:') &&
  166. !!state.plan &&
  167. state.plan.enabled === true,
  168. JSON.stringify({ engine: state.engine, area: state.area, reason: state.reason, pill: state.pill.slice(0, 90) }),
  169. )
  170. await page.close()
  171. }
  172. } finally {
  173. await browser.close()
  174. }
  175. const failed = results.filter((r) => !r.pass)
  176. console.log('')
  177. console.log(`T36 走查:${results.length - failed.length}/${results.length} 项通过。`)
  178. if (failed.length) {
  179. console.log('未通过:' + failed.map((f) => f.id).join(', '))
  180. process.exit(1)
  181. }
  182. console.log('T36 走查全部通过。')