t45-main-scene-samples-walkthrough.mjs 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. /**
  2. * T45 主场景示例走查:**不注入任何场景**(默认 `success`),验证维护者要求的两条示例直接可见:
  3. * · MP-001 城南测试监测点 —— K=2 正常重叠(聚合符号 K=2,可逐台展开);
  4. * · MP-002 太常测试监测点 —— K=11 降级(本屏 10 台 / 其余见清单)。
  5. *
  6. * 全程真实鼠标点地图标记(与用户路径一致);坐标读取用只读 `getPointMarkerTargets()`,
  7. * 并在投影点附近做小网格扫描点击(贴地形标记会随地形细化抖动几像素,真人点不中也会再点)。
  8. */
  9. import { cdpLaunch } from './cdp-driver.mjs'
  10. const BASE = process.env.COCKPIT_BASE || 'http://localhost:8000'
  11. const CHROME = process.env.CHROME_PATH || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
  12. // 主场景:**不带** scenario 查询参数。
  13. const ENTRY_URL = process.env.COCKPIT_URL || `${BASE}/#/cockpit`
  14. const results = []
  15. function check(id, pass, evidence) {
  16. results.push({ id, pass })
  17. console.log(`${pass ? 'PASS' : 'FAIL'} ${id} :: ${evidence === undefined ? '' : String(evidence)}`)
  18. }
  19. const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
  20. const browser = await cdpLaunch({ executablePath: CHROME, width: 1680, height: 950 })
  21. console.log(`Chrome: ${await browser.version()}`)
  22. const page = await browser.newPage()
  23. const errors = []
  24. page.on('pageerror', (e) => errors.push(String(e && e.message ? e.message : e)))
  25. page.on('console', (m) => { if (m.type() === 'error') errors.push(`console.error: ${m.text()}`) })
  26. await page.goto(ENTRY_URL, { timeout: 60000 })
  27. await page.waitForSelector('.cockpit-shell', { timeout: 20000 })
  28. if (await page.$('[data-cockpit-intro]')) {
  29. await page.click('[data-cockpit-intro]')
  30. await page.waitForSelector('[data-cockpit-intro]', { hidden: true, timeout: 10000 }).catch(() => {})
  31. }
  32. await page.waitForFunction(
  33. () => ['ready', 'failed', 'disabled'].includes(document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine),
  34. { timeout: 40000 },
  35. ).catch(() => {})
  36. await page.evaluate(() => {
  37. const dev = window.__cockpitMap
  38. if (!dev || !dev.mapSummary) return
  39. let steps = 0
  40. const step = () => {
  41. const state = dev.mapSummary.getState()
  42. if (!state || ['complete', 'empty', 'failed', 'expired'].includes(state.phase) || steps >= 20) return
  43. dev.mapSummary.advance(); steps += 1; window.setTimeout(step, 220)
  44. }
  45. step()
  46. })
  47. await page.waitForFunction(() => {
  48. const dev = window.__cockpitMap
  49. const s = dev && dev.mapSummary ? dev.mapSummary.getState() : null
  50. return !s || ['complete', 'empty', 'failed', 'expired'].includes(s.phase)
  51. }, { timeout: 20000 }).catch(() => {})
  52. await page.waitForFunction(() => {
  53. const dev = window.__cockpitMap
  54. const s = dev && dev.getCameraState ? dev.getCameraState() : null
  55. return !s || s.settled === true
  56. }, { timeout: 15000 }).catch(() => {})
  57. await sleep(1500)
  58. const locate = (code) => page.evaluate((pointCode) => {
  59. const dev = window.__cockpitMap
  60. const all = dev && dev.getPointMarkerTargets ? dev.getPointMarkerTargets() : []
  61. return all.find((item) => item.visible && item.code === pointCode) || null
  62. }, code)
  63. const readOverlap = () => page.evaluate(() => {
  64. const host = document.querySelector('.cockpit-shell__map .map-canvas')
  65. const dev = window.__cockpitMap
  66. const devices = dev && dev.getAssociatedDevices ? dev.getAssociatedDevices() : { entities: [], onMap: 0 }
  67. return {
  68. overlap: dev && dev.getOverlapState ? dev.getOverlapState() : null,
  69. detailLevel: (document.querySelector('[data-point-detail]') || { dataset: {} }).dataset.detailLevel || '',
  70. pointCode: (document.querySelector('[data-point-detail]') || { dataset: {} }).dataset.pointCode || '',
  71. rows: [...document.querySelectorAll('[data-device-layer-row]')].map((n) => n.dataset.deviceLayerCode),
  72. aggregateEntities: (devices.entities || []).filter((e) => e.overlapAggregate).length,
  73. mapOverlapState: host && host.dataset.mapOverlapState,
  74. mapOverlapCount: host && host.dataset.mapOverlapCount,
  75. }
  76. })
  77. const OFFSETS = [[0, 0], [0, -6], [0, 6], [-6, 0], [6, 0], [0, -12], [0, 12], [-10, -6], [10, 6]]
  78. /** 真实鼠标点某个点位标记,直到点位详情打开(或重试用尽)。 */
  79. async function openPoint(code) {
  80. for (let round = 0; round < 5; round += 1) {
  81. const marker = await locate(code)
  82. if (marker) {
  83. for (const [dx, dy] of OFFSETS) {
  84. await page.mouse.move(1200, 300)
  85. await sleep(70)
  86. await page.mouse.move(marker.x + dx, marker.y + dy)
  87. await sleep(140)
  88. await page.mouse.click(marker.x + dx, marker.y + dy)
  89. await sleep(800)
  90. const now = await readOverlap()
  91. if (now.detailLevel === 'point' && now.pointCode === code) return now
  92. }
  93. }
  94. await sleep(400)
  95. }
  96. return readOverlap()
  97. }
  98. // 遮罩让真实鼠标能命中画布(既有锚定浮层会盖住画布;仅测试态)。
  99. async function passthroughDetail() {
  100. await page.evaluate(() => {
  101. const detail = document.querySelector('.cockpit-shell__detail')
  102. if (detail) {
  103. detail.style.pointerEvents = 'none'
  104. detail.querySelectorAll('*').forEach((child) => { child.style.pointerEvents = 'none' })
  105. }
  106. })
  107. }
  108. /** 真实点击顶栏「归位」,等相机收敛回广视角(相机飞近会把另一个标记挤出画布)。 */
  109. async function returnHome() {
  110. await page.click('.cockpit-shell__home').catch(() => {})
  111. await page.waitForFunction(() => {
  112. const dev = window.__cockpitMap
  113. const s = dev && dev.getCameraState ? dev.getCameraState() : null
  114. return !s || s.settled === true
  115. }, { timeout: 20000 }).catch(() => {})
  116. await sleep(1200)
  117. }
  118. // ── 示例 1:MP-001 = K=2 正常重叠 ──
  119. const first = await openPoint('MP-001')
  120. await passthroughDetail()
  121. check('M1-main-scene-MP001-opens',
  122. first.detailLevel === 'point' && first.pointCode === 'MP-001',
  123. `真实点 MP-001 后 detailLevel=${first.detailLevel} pointCode=${first.pointCode}`)
  124. check('M2-main-scene-MP001-K2',
  125. first.overlap && first.overlap.count === 2 && first.overlap.state === 'collapsed' &&
  126. first.aggregateEntities === 1 && first.rows.length === 2 && first.mapOverlapCount === '2',
  127. `MP-001:K=${first.overlap && first.overlap.count} state=${first.overlap && first.overlap.state} 聚合实体=${first.aggregateEntities} 清单=${first.rows.length} 行 data-count=${first.mapOverlapCount}`)
  128. // ── 示例 2:MP-002 = K=11 降级 ──
  129. // 先归位(真实点顶栏按钮):点开 MP-001 后相机会飞近,MP-002 会被挤出画布。
  130. await returnHome()
  131. const markerDiag = await page.evaluate(() => {
  132. const dev = window.__cockpitMap
  133. const host = document.querySelector('.cockpit-shell__map .map-canvas')
  134. const rect = host ? host.getBoundingClientRect() : null
  135. const all = dev && dev.getPointMarkerTargets ? dev.getPointMarkerTargets() : []
  136. const cam = dev && dev.getCameraState ? dev.getCameraState() : null
  137. return {
  138. host: rect ? { left: Math.round(rect.left), top: Math.round(rect.top), right: Math.round(rect.right), bottom: Math.round(rect.bottom) } : null,
  139. camera: cam ? { settled: cam.settled, hpr: cam.hpr } : null,
  140. targets: all.map((t) => ({ code: t.code, x: Math.round(t.x), y: Math.round(t.y), visible: t.visible })),
  141. }
  142. })
  143. console.log(`归位后标记坐标(证据):${JSON.stringify(markerDiag)}`)
  144. const second = await openPoint('MP-002')
  145. await passthroughDetail()
  146. check('M3-main-scene-MP002-opens',
  147. second.detailLevel === 'point' && second.pointCode === 'MP-002',
  148. `真实点 MP-002 后 detailLevel=${second.detailLevel} pointCode=${second.pointCode}`)
  149. check('M4-main-scene-MP002-K11-degraded',
  150. second.overlap && second.overlap.count === 11 && second.overlap.state === 'degraded' &&
  151. second.aggregateEntities === 1 && second.rows.length === 11 && second.mapOverlapCount === '11' &&
  152. second.mapOverlapState === 'degraded',
  153. `MP-002:K=${second.overlap && second.overlap.count} state=${second.overlap && second.overlap.state} 聚合实体=${second.aggregateEntities} 清单=${second.rows.length} 行 data-state=${second.mapOverlapState} data-count=${second.mapOverlapCount}`)
  154. check('M5-no-page-errors', errors.length === 0, errors.length ? errors.slice(0, 3).join(' | ') : '无 pageerror / console.error')
  155. await page.close()
  156. await browser.close()
  157. const failed = results.filter((r) => !r.pass)
  158. console.log('')
  159. console.log(`T45 主场景示例走查:${results.length - failed.length}/${results.length} 项通过。`)
  160. if (failed.length) { for (const item of failed) console.log(` · ${item.id}`); process.exit(1) }
  161. console.log('T45 主场景示例走查全部通过。')