t45-overlap-walkthrough.mjs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. /** T45 真机走查:重叠设备聚合与展开(票45 / 规格17)。 */
  2. import { cdpLaunch } from './cdp-driver.mjs'
  3. const BASE = process.env.COCKPIT_BASE || 'http://localhost:8000'
  4. const ENTRY_URL = process.env.COCKPIT_URL || `${BASE}/#/cockpit`
  5. const CHROME = process.env.CHROME_PATH || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
  6. const SCENARIO = 'device-layer-overlap'
  7. const results = []
  8. const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
  9. function check(id, pass, evidence) {
  10. results.push({ id, pass })
  11. console.log(`${pass ? 'PASS' : 'FAIL'} ${id} :: ${evidence || ''}`)
  12. }
  13. const browser = await cdpLaunch({ executablePath: CHROME, width: 1680, height: 950 })
  14. console.log(`Chrome: ${await browser.version()}`)
  15. const page = await browser.newPage()
  16. const errors = []
  17. page.on('pageerror', (e) => errors.push(String(e && e.message ? e.message : e)))
  18. page.on('console', (m) => { if (m.type() === 'error') errors.push(`console.error: ${m.text()}`) })
  19. await page.evaluateOnNewDocument((name) => { window.__COCKPIT_MOCK_SCENARIO__ = name }, SCENARIO)
  20. await page.goto(ENTRY_URL, { timeout: 60000 })
  21. await page.waitForSelector('.cockpit-shell', { timeout: 20000 })
  22. if (await page.$('[data-cockpit-intro]')) {
  23. await page.click('[data-cockpit-intro]')
  24. await page.waitForSelector('[data-cockpit-intro]', { hidden: true, timeout: 10000 }).catch(() => {})
  25. }
  26. await page.waitForFunction(
  27. () => ['ready', 'failed', 'disabled'].includes(document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine),
  28. { timeout: 40000 },
  29. ).catch(() => {})
  30. await sleep(1200)
  31. // 推进地图摘要到 complete。
  32. await page.evaluate(() => {
  33. const dev = window.__cockpitMap
  34. if (!dev || !dev.mapSummary) return
  35. let steps = 0
  36. const step = () => {
  37. const state = dev.mapSummary.getState()
  38. if (!state || ['complete', 'empty', 'failed', 'expired'].includes(state.phase) || steps >= 20) return
  39. dev.mapSummary.advance(); steps += 1; window.setTimeout(step, 220)
  40. }
  41. step()
  42. })
  43. await page.waitForFunction(() => {
  44. const dev = window.__cockpitMap
  45. const state = dev && dev.mapSummary ? dev.mapSummary.getState() : null
  46. return !state || ['complete', 'empty', 'failed', 'expired'].includes(state.phase)
  47. }, { timeout: 20000 }).catch(() => {})
  48. await page.evaluate(() => window.__cockpitMap && window.__cockpitMap.selectMonitoringPoint && window.__cockpitMap.selectMonitoringPoint('MP-001'))
  49. await page.waitForSelector('[data-device-layer-list] [data-device-layer-row]', { timeout: 20000 }).catch(() => {})
  50. await sleep(2500)
  51. const read = () => page.evaluate(() => {
  52. const host = document.querySelector('.cockpit-shell__map .map-canvas')
  53. const dev = window.__cockpitMap
  54. const devices = dev && dev.getAssociatedDevices ? dev.getAssociatedDevices() : { entities: [], overlap: null, onMap: 0 }
  55. return {
  56. overlap: dev && dev.getOverlapState ? dev.getOverlapState() : null,
  57. targets: dev && dev.getOverlapTargets ? dev.getOverlapTargets() : [],
  58. entities: devices.entities || [],
  59. onMap: devices.onMap,
  60. mapPicked: host && host.dataset.mapPicked,
  61. devicePicked: host && host.dataset.mapDevicePicked,
  62. panelSelection: host && host.dataset.mapPanelSelection,
  63. deviceHovered: host && host.dataset.mapDeviceHovered,
  64. correspondence: dev && dev.getDeviceCorrespondence ? dev.getDeviceCorrespondence() : null,
  65. detailLevel: (document.querySelector('[data-point-detail]') || { dataset: {} }).dataset.detailLevel || '',
  66. rows: [...document.querySelectorAll('[data-device-layer-row]')].length,
  67. }
  68. })
  69. const collapsed = await read()
  70. const aggregate = collapsed.entities.filter((e) => e.overlapAggregate)
  71. const hiddenDevices = collapsed.entities.filter((e) => e.deviceCode && e.visible === false)
  72. check('W1-collapsed-has-aggregate',
  73. collapsed.overlap && collapsed.overlap.state === 'collapsed' && collapsed.overlap.count === 2 && aggregate.length === 1,
  74. `state=${collapsed.overlap && collapsed.overlap.state} K=${collapsed.overlap && collapsed.overlap.count} 聚合实体=${aggregate.length}`)
  75. check('W2-aggregate-has-no-device-identity',
  76. aggregate.length === 1 && !aggregate[0].deviceCode && aggregate[0].overlapCount === 2,
  77. `聚合实体 deviceCode=${aggregate[0] && aggregate[0].deviceCode || '(空)'} count=${aggregate[0] && aggregate[0].overlapCount}`)
  78. check('W3-collapsed-real-devices-hidden',
  79. hiddenDevices.length === 2 && collapsed.onMap === 0,
  80. `隐藏逐台设备=${hiddenDevices.length} onMap=${collapsed.onMap}`)
  81. check('W4-list-keeps-all-devices', collapsed.rows === 2, `清单行数=${collapsed.rows}`)
  82. const aggregateTarget = collapsed.targets.find((target) => target.aggregate && target.visible)
  83. check('W5-aggregate-projects-on-screen',
  84. Boolean(aggregateTarget && Number.isFinite(aggregateTarget.x) && Number.isFinite(aggregateTarget.y)),
  85. aggregateTarget ? `窗口坐标 ${aggregateTarget.x.toFixed(1)},${aggregateTarget.y.toFixed(1)}` : '聚合实体未投影到视窗')
  86. if (aggregateTarget) {
  87. await page.evaluate(() => {
  88. const detail = document.querySelector('.cockpit-shell__detail')
  89. if (detail) {
  90. detail.style.pointerEvents = 'none'
  91. detail.querySelectorAll('*').forEach((child) => { child.style.pointerEvents = 'none' })
  92. }
  93. })
  94. // 真实鼠标 hover:瞬态预览,离开即回折叠(不写选中态)。
  95. await page.mouse.move(aggregateTarget.x, aggregateTarget.y)
  96. await sleep(350)
  97. const preview = await read()
  98. check('W6-real-hover-preview',
  99. preview.overlap && preview.overlap.preview === true && preview.overlap.expanded === false,
  100. `真实pointermove后 preview=${preview.overlap && preview.overlap.preview} expanded=${preview.overlap && preview.overlap.expanded}`)
  101. await page.mouse.move(1200, 500)
  102. await sleep(100)
  103. await page.mouse.move(1500, 750)
  104. await sleep(350)
  105. const left = await read()
  106. check('W7-real-mouseleave-collapses',
  107. left.overlap && left.overlap.state === 'collapsed' && left.overlap.preview === false,
  108. `离开后 state=${left.overlap && left.overlap.state} preview=${left.overlap && left.overlap.preview}`)
  109. // 真实鼠标 click 聚合符号:粘性展开。
  110. const targetAgain = left.targets.find((target) => target.aggregate && target.visible) || aggregateTarget
  111. await page.mouse.move(1200, 500)
  112. await sleep(100)
  113. await page.mouse.click(targetAgain.x, targetAgain.y)
  114. await sleep(500)
  115. const expanded = await read()
  116. const visibleDevices = expanded.entities.filter((e) => e.deviceCode && e.visible)
  117. const expandedAggregate = expanded.entities.filter((e) => e.overlapAggregate && e.visible)
  118. check('W8-real-click-sticky-expands',
  119. expanded.overlap && expanded.overlap.state === 'expanded' && expanded.overlap.expanded === true,
  120. `真实click后 state=${expanded.overlap && expanded.overlap.state} expanded=${expanded.overlap && expanded.overlap.expanded}`)
  121. check('W9-expanded-shows-device-identities',
  122. visibleDevices.length === 2 && visibleDevices.every((e) => e.deviceCode) && expandedAggregate.length === 0,
  123. `可见设备=${visibleDevices.map((e) => e.deviceCode).join('/')} 聚合可见=${expandedAggregate.length}`)
  124. check('W10-expanded-screen-offsets-preserve-space',
  125. visibleDevices.every((e) => e.pixelOffset && (e.pixelOffset.x !== 0 || e.pixelOffset.y !== 0)),
  126. `pixelOffset=${visibleDevices.map((e) => `${e.deviceCode}:${e.pixelOffset && e.pixelOffset.x},${e.pixelOffset && e.pixelOffset.y}`).join(' ')}`)
  127. // 真实鼠标 click 展开子设备:普通拾取返回真实设备身份,设备优先拾取生效。
  128. const deviceTarget = expanded.targets.find((target) => target.deviceCode === 'EQ-001' && target.visible)
  129. if (deviceTarget) {
  130. await page.mouse.move(1200, 500)
  131. await sleep(150)
  132. await page.mouse.click(deviceTarget.x, deviceTarget.y)
  133. }
  134. await sleep(600)
  135. const picked = await read()
  136. check('W11-real-pick-expanded-device',
  137. Boolean(deviceTarget) && picked.devicePicked === 'EQ-001' && picked.panelSelection === 'equipment',
  138. `设备目标=${Boolean(deviceTarget)} picked=${picked.devicePicked} panel=${picked.panelSelection}`)
  139. // 面板返回点位结束展开(走 store 层级回退入口,与票 43 同一用户路径)。
  140. await page.evaluate(() => window.__cockpitMap && window.__cockpitMap.backToPointLevel && window.__cockpitMap.backToPointLevel())
  141. await sleep(450)
  142. const backed = await read()
  143. check('W12-back-to-point-collapses',
  144. backed.overlap && backed.overlap.state === 'collapsed' && !backed.overlap.expanded,
  145. `返回点位后 state=${backed.overlap && backed.overlap.state}`)
  146. // 重新真实点击聚合后点空白,展开也应收起。
  147. const aggregateAfterBack = backed.targets.find((target) => target.aggregate && target.visible)
  148. if (aggregateAfterBack) {
  149. await page.mouse.click(aggregateAfterBack.x, aggregateAfterBack.y)
  150. await sleep(300)
  151. // 纯只读探针:确认空白点落在 canvas 上(不被面板/浮层覆盖),不触发任何拾取。
  152. const blankProbe = await page.evaluate(() => {
  153. const el = document.elementFromPoint(700, 800)
  154. const host = document.querySelector('.cockpit-shell__map .map-canvas')
  155. window.__t45BlankPointer = { down: 0, up: 0 }
  156. if (host) {
  157. host.addEventListener('pointerdown', () => { window.__t45BlankPointer.down += 1 })
  158. host.addEventListener('pointerup', () => { window.__t45BlankPointer.up += 1 })
  159. }
  160. return { element: el ? `${el.tagName}.${String(el.className).replace(/ /g, '.')}` : '' }
  161. })
  162. await page.mouse.move(700, 800)
  163. await sleep(120)
  164. await page.mouse.click(700, 800)
  165. await sleep(400)
  166. const blankReachedCanvas = await page.evaluate(() => window.__t45BlankPointer.down === 1 && window.__t45BlankPointer.up === 1)
  167. const blank = await read()
  168. check('W13-blank-click-collapses',
  169. Boolean(aggregateAfterBack) && blankReachedCanvas === true && blank.overlap && blank.overlap.state === 'collapsed' && !blank.overlap.expanded,
  170. `空白click后 state=${blank.overlap && blank.overlap.state}(canvas命中=${blankReachedCanvas})`)
  171. }
  172. // 预警处置模式:分项切换按票46语义会清空选择;重新选择MP-001后再验证状态过滤。
  173. await page.evaluate(() => {
  174. const button = [...document.querySelectorAll('.cockpit-shell__mode')][3]
  175. if (button) button.click()
  176. })
  177. await sleep(900)
  178. await page.evaluate(() => window.__cockpitMap && window.__cockpitMap.selectMonitoringPoint && window.__cockpitMap.selectMonitoringPoint('MP-001'))
  179. await page.waitForSelector('[data-device-layer-list] [data-device-layer-row]', { timeout: 15000 }).catch(() => {})
  180. await sleep(900)
  181. const warning = await read()
  182. const warningDevices = warning.entities.filter((e) => e.deviceCode && e.visible).map((e) => e.deviceCode)
  183. const warningAggregates = warning.entities.filter((e) => e.overlapAggregate && e.visible)
  184. check('W15-warning-mode-filters-device-status',
  185. !warningDevices.includes('EQ-002') && (warningDevices.includes('EQ-001') || warningAggregates.length === 1),
  186. `预警模式可见设备=${warningDevices.join('/') || '无'} 聚合=${warningAggregates.length} K=${warning.overlap && warning.overlap.count}`)
  187. }
  188. const finalFields = await read()
  189. const legacyFields = [finalFields.mapPicked, finalFields.devicePicked, finalFields.panelSelection, finalFields.deviceHovered]
  190. check('W14-existing-selection-fields-preserved',
  191. legacyFields.every((value) => value === undefined || typeof value === 'string'),
  192. `mapPicked=${finalFields.mapPicked || '(未初始化)'} devicePicked=${finalFields.devicePicked || '(未初始化)'} panel=${finalFields.panelSelection || '(未初始化)'} hovered=${finalFields.deviceHovered || '(未初始化)'}`)
  193. check('W16-no-page-errors', errors.length === 0, errors.length ? errors.slice(0, 3).join(' | ') : '无 pageerror / console.error')
  194. await page.close(); await browser.close()
  195. const failed = results.filter((r) => !r.pass)
  196. console.log('')
  197. console.log(`T45 真机走查:${results.length - failed.length}/${results.length} 项通过。`)
  198. if (failed.length) { for (const item of failed) console.log(` · ${item.id}`); process.exit(1) }
  199. console.log('T45 真机走查全部通过。')