t46-no-location-detail-walkthrough.mjs 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. /**
  2. * T46 回归走查:不上图设备的设备级详情仍可见(票 46 交付后人工走查抓到的真缺陷)。
  3. *
  4. * ---- 缺陷背景(本脚本就是当时的复现环)----
  5. * 用户人工走查「重叠设备聚合与展开」(票 45)时用的是默认样例(动不了坐标场景):
  6. * 1. 设备统一 `UNVERIFIED_COORDINATE/renderable=false` → `no-location` → **地图上没有设备图标**
  7. * —— 这是票 42 照抄的现网正式边界,**不是缺陷**(要看到图标需注入 `device-layer-overlap` 场景);
  8. * 2. 点监测点点位摘要里的**设备卡片** → 摘要面板**直接消失** —— 这是**真缺陷**:
  9. * `domain/modeLayerPlan.js` 的 `selectionVisible()` 对设备级选择要求
  10. * `row.placement === 'drawn'`,而 `no-location` 设备不满足 ⇒ 外壳
  11. * `<aside v-if="… selectionVisibleInMode">` 卸载 ⇒ 面板消失。
  12. * 设备级详情(档案/指标/预警等分区)与该设备能否上图**无关**:一台无地图位置的设备
  13. * 仍要看它的详情分区。修复:设备级选择只要在 `associatedDevices` 里找到该设备行,
  14. * 就按**模式可见性**判(预警处置只显示报警/预警),不再要求 `placement === 'drawn'`。
  15. *
  16. * ---- 本脚本断言 ----
  17. * 1. 默认样例(success)选中 MP-001:设备清单里有 EQ-001,且 placement=no-location;
  18. * 2. 真实鼠标点击 EQ-001 设备卡片(与用户相同的操作路径);
  19. * 3. 断言摘要浮层**仍在**(aside 存在、尺寸正常),且已进入设备级
  20. * (`detailLevel=equipment`、`deviceCode=EQ-001`、`data-selection-paused='no'`);
  21. * 4. 无页面级报错。
  22. */
  23. import { cdpLaunch } from './cdp-driver.mjs'
  24. const BASE = process.env.COCKPIT_BASE || 'http://localhost:8000'
  25. const ENTRY_URL = process.env.COCKPIT_URL || `${BASE}/#/cockpit`
  26. const CHROME = process.env.CHROME_PATH || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
  27. // 默认样例(success)= 现网正式边界:设备坐标统一 renderable=false。
  28. const SCENARIO = 'success'
  29. const results = []
  30. function check(id, pass, evidence) {
  31. results.push({ id, pass })
  32. console.log(`${pass ? 'PASS' : 'FAIL'} ${id} :: ${evidence === undefined ? '' : String(evidence)}`)
  33. }
  34. const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
  35. const browser = await cdpLaunch({ executablePath: CHROME, width: 1680, height: 950 })
  36. console.log(`Chrome: ${await browser.version()}`)
  37. const page = await browser.newPage()
  38. const errors = []
  39. page.on('pageerror', (e) => errors.push(String(e && e.message ? e.message : e)))
  40. page.on('console', (m) => {
  41. if (m.type() === 'error') errors.push(`console.error: ${m.text()}`)
  42. })
  43. await page.evaluateOnNewDocument((name) => {
  44. window.__COCKPIT_MOCK_SCENARIO__ = name
  45. }, SCENARIO)
  46. await page.goto(ENTRY_URL, { timeout: 60000 })
  47. await page.waitForSelector('.cockpit-shell', { timeout: 20000 })
  48. if (await page.$('[data-cockpit-intro]')) {
  49. await page.click('[data-cockpit-intro]')
  50. await page.waitForSelector('[data-cockpit-intro]', { hidden: true, timeout: 10000 }).catch(() => {})
  51. }
  52. await page.waitForFunction(
  53. () => ['ready', 'failed', 'disabled'].includes(document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine),
  54. { timeout: 40000 },
  55. ).catch(() => {})
  56. // 推进地图摘要到 complete。
  57. await page.evaluate(() => {
  58. const dev = window.__cockpitMap
  59. if (!dev || !dev.mapSummary) return
  60. let steps = 0
  61. const step = () => {
  62. const state = dev.mapSummary.getState()
  63. if (!state || ['complete', 'empty', 'failed', 'expired'].includes(state.phase) || steps >= 20) return
  64. dev.mapSummary.advance()
  65. steps += 1
  66. window.setTimeout(step, 220)
  67. }
  68. step()
  69. })
  70. await page.waitForFunction(() => {
  71. const dev = window.__cockpitMap
  72. const state = dev && dev.mapSummary ? dev.mapSummary.getState() : null
  73. return !state || ['complete', 'empty', 'failed', 'expired'].includes(state.phase)
  74. }, { timeout: 20000 }).catch(() => {})
  75. // 统一选择入口打开 MP-001 点位摘要(与地图点选/台账同一用户路径)。
  76. await page.evaluate(() => window.__cockpitMap && window.__cockpitMap.selectMonitoringPoint && window.__cockpitMap.selectMonitoringPoint('MP-001'))
  77. await page.waitForSelector('[data-device-card]', { timeout: 20000 }).catch(() => {})
  78. await sleep(2500)
  79. const snapshot = () => page.evaluate(() => {
  80. const aside = document.querySelector('.cockpit-shell__detail')
  81. const card = document.querySelector('[data-point-detail]')
  82. const rows = [...document.querySelectorAll('[data-device-layer-row]')].map((n) => ({
  83. code: n.dataset.deviceLayerCode,
  84. placement: n.dataset.deviceLayerRow,
  85. }))
  86. const host = document.querySelector('.cockpit-shell__map .map-canvas')
  87. return {
  88. asidePresent: !!aside,
  89. asideRect: aside ? (() => { const r = aside.getBoundingClientRect(); return { w: Math.round(r.width), h: Math.round(r.height), left: Math.round(r.left), top: Math.round(r.top) } })() : null,
  90. detailLevel: card ? card.dataset.detailLevel : '',
  91. pointCode: card ? card.dataset.pointCode : '',
  92. deviceCode: card ? card.dataset.deviceCode : '',
  93. selectionPaused: aside ? aside.dataset.selectionPaused : '',
  94. deviceRows: rows,
  95. mapDevices: host && host.dataset.mapDevices,
  96. }
  97. })
  98. const before = await snapshot()
  99. // 断言默认样例下设备确实不上图(现网正式边界;这是复现缺陷的前置条件,不是本轮测缺陷)。
  100. const noLocationRow = before.deviceRows.find((r) => r.code === 'EQ-001')
  101. check('R1-production-boundary-no-location',
  102. Boolean(noLocationRow) && noLocationRow.placement === 'no-location' && before.mapDevices === '0',
  103. `默认样例 EQ-001 placement=${noLocationRow && noLocationRow.placement}、地图设备数 ${before.mapDevices}(现网正式边界:不上图仅清单)`)
  104. check('R2-point-summary-open', before.asidePresent && before.detailLevel === 'point',
  105. `点位摘要打开:detailLevel=${before.detailLevel}、aside w=${before.asideRect && before.asideRect.w} h=${before.asideRect && before.asideRect.h}`)
  106. // 真实鼠标点击 EQ-001 设备卡片(与用户相同的操作路径)。
  107. const cardRect = await page.evaluate((code) => {
  108. const el = document.querySelector(`[data-device-card][data-device-code="${code}"]`)
  109. if (!el) return null
  110. const r = el.getBoundingClientRect()
  111. return { x: r.left + r.width / 2, y: r.top + r.height / 2, w: Math.round(r.width), h: Math.round(r.height) }
  112. }, 'EQ-001')
  113. check('R3-device-card-found', Boolean(cardRect), `设备卡片 EQ-001 存在(rect w=${cardRect && cardRect.w} h=${cardRect && cardRect.h})`)
  114. if (cardRect) {
  115. await page.mouse.move(cardRect.x, cardRect.y)
  116. await sleep(200)
  117. await page.mouse.click(cardRect.x, cardRect.y)
  118. }
  119. await sleep(2500)
  120. const after = await snapshot()
  121. check('R4-detail-panel-still-visible',
  122. after.asidePresent &&
  123. after.detailLevel === 'equipment' &&
  124. after.deviceCode === 'EQ-001' &&
  125. after.selectionPaused === 'no' &&
  126. after.asideRect && after.asideRect.w > 0 && after.asideRect.h > 0,
  127. `点设备卡片后面板仍在(aside=${after.asidePresent}、detailLevel=${after.detailLevel}、deviceCode=${after.deviceCode}、selectionPaused=${after.selectionPaused}、w=${after.asideRect && after.asideRect.w} h=${after.asideRect && after.asideRect.h})`)
  128. check('R5-no-page-errors', errors.length === 0, errors.length ? errors.slice(0, 3).join(' | ') : '无 pageerror / console.error')
  129. await page.close()
  130. await browser.close()
  131. const failed = results.filter((r) => !r.pass)
  132. console.log('')
  133. console.log(`T46 回归走查(不上图设备详情可见):${results.length - failed.length}/${results.length} 项通过。`)
  134. if (failed.length) {
  135. console.log('未通过:')
  136. for (const r of failed) console.log(` · ${r.id}`)
  137. process.exit(1)
  138. }
  139. console.log('T46 回归走查全部通过。')