| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264 |
- /**
- * T45 真实用户路径走查:进入系统 → **真实鼠标点地图标记** → 展开设备点 → 逐台拾取。
- *
- * 为什么另起一个脚本:既有 `t45-overlap-walkthrough.mjs` 用 DEV 把手 `selectMonitoringPoint()`
- * 打开点位(那是"统一入口",但不是**用户的手**)。维护者人工走查反馈「进入系统→点开监测点→
- * 展开设备点」不成立,所以这一趟必须全程用真实鼠标命中**地图上的标记**:
- * 1. 真实点击监测点标记(坐标由只读 `getPointMarkerTargets()` 投影给出);
- * 2. 断言点位详情真的打开(不是靠 DEV 入口开的);
- * 3. 真实 hover / click 聚合符号 → 粘性展开;
- * 4. 真实 click 展开后的设备点 → 选中并进设备级。
- *
- * 场景激活走 **URL 通道** `?scenario=xxx`(人工测试同一条入口;Console 全局变量刷新即丢)。
- *
- * **相机收敛纪律**(本脚本第一版踩到的坑):相机飞行/收敛期间投影出来的屏幕坐标会漂移
- * (同一标记两次读可以差 80px),拿过期坐标去点必然打不中。所以每次点击前都必须
- * 先等 `getCameraState().settled`,再**当场重读**坐标,然后立刻点。
- */
- import { cdpLaunch } from './cdp-driver.mjs'
- const BASE = process.env.COCKPIT_BASE || 'http://localhost:8000'
- const CHROME = process.env.CHROME_PATH || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
- const SCENARIO = process.env.REPRO_SCENARIO || 'device-layer-overlap'
- const ENTRY_URL = process.env.COCKPIT_URL || `${BASE}/?scenario=${SCENARIO}#/cockpit`
- /**
- * 按场景给出期望值(两个受控样例的 K 与降级形态不同):
- * · `device-layer-overlap` —— 2 台同坐标,K=2,全部可逐台展开(`expanded`);
- * · `device-layer-overlap-degraded` —— 11 台同坐标,K=11 > maxFan(10) ⇒ 降级:
- * 本屏只逐台画 `maxFan` 台、其余在清单里可见(`degraded`)。
- */
- const EXPECT = SCENARIO === 'device-layer-overlap-degraded'
- ? { k: 11, fanVisible: 10, firstDevice: 'EQ-DG-001', state: 'degraded' }
- : { k: 2, fanVisible: 2, firstDevice: 'EQ-001', state: 'expanded' }
- const results = []
- function check(id, pass, evidence) {
- results.push({ id, pass })
- console.log(`${pass ? 'PASS' : 'FAIL'} ${id} :: ${evidence === undefined ? '' : String(evidence)}`)
- }
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
- const browser = await cdpLaunch({ executablePath: CHROME, width: 1680, height: 950 })
- console.log(`Chrome: ${await browser.version()}`)
- const page = await browser.newPage()
- const errors = []
- page.on('pageerror', (e) => errors.push(String(e && e.message ? e.message : e)))
- page.on('console', (m) => { if (m.type() === 'error') errors.push(`console.error: ${m.text()}`) })
- /**
- * 真实鼠标点击一个**会漂移的投影目标**,直到满足断言或重试用尽。
- *
- * 贴地形渲染的标记会随地形细化继续漂移(实测稳定判定后仍会漂),
- * 所以每次重试都**当场重读坐标**再点 —— 这也是真人的行为(没点中就再点一下)。
- * @param {() => Promise<{x:number,y:number}|null>} locate 读取当前坐标
- * @param {() => Promise<boolean>} done 是否已达成目标
- */
- async function clickUntil(locate, done, tries = 6) {
- // 贴地形渲染的标记位置会随地形瓦片细化在几像素内抖动(实测 y 在 575↔584 之间跳),
- // 单点点击会偶发落空。这里在投影点附近做小网格扫描点击(真人点不中也会微调再点),
- // 只要**任何一个**点命中即算通过;全部落空说明产品真有问题。
- const offsets = [[0, 0], [0, -6], [0, 6], [-6, 0], [6, 0], [0, -12], [0, 12], [-10, -6], [10, 6], [-10, 6], [10, -6]]
- for (let index = 0; index < tries; index += 1) {
- const target = await locate()
- if (target && Number.isFinite(target.x) && Number.isFinite(target.y)) {
- for (const [dx, dy] of offsets) {
- await page.mouse.move(1200, 300)
- await sleep(80)
- await page.mouse.move(target.x + dx, target.y + dy)
- await sleep(150)
- await page.mouse.click(target.x + dx, target.y + dy)
- await sleep(900)
- if (await done()) return true
- }
- }
- await sleep(400)
- }
- return false
- }
- async function waitSettled(timeout = 15000) {
- await page.waitForFunction(() => {
- const dev = window.__cockpitMap
- const state = dev && dev.getCameraState ? dev.getCameraState() : null
- return !state || state.settled === true
- }, { timeout }).catch(() => {})
- await sleep(400)
- }
- /**
- * 等某个点位标记的**投影坐标稳定**后再返回它。
- *
- * 点位标记是 `CLAMP_TO_GROUND` 贴地形渲染的:地形瓦片细化会让 `globe.getHeight()` 变化,
- * 投影出来的屏幕坐标随之漂移(本脚本实测两次读之间 y 差 9px —— 18px 的符号正好点空)。
- * 所以不能"读一次就点",要连续两次读数一致才算稳。
- */
- async function stableMarker(code = 'MP-001', tries = 40) {
- let prev = null
- for (let index = 0; index < tries; index += 1) {
- const now = await page.evaluate((pointCode) => {
- const dev = window.__cockpitMap
- const all = dev && dev.getPointMarkerTargets ? dev.getPointMarkerTargets() : []
- return all.find((item) => item.visible && item.code === pointCode) || null
- }, code)
- if (now && prev && Math.abs(now.x - prev.x) < 2 && Math.abs(now.y - prev.y) < 2) return now
- prev = now
- await sleep(400)
- }
- const diag = await page.evaluate(() => {
- const dev = window.__cockpitMap
- const host = document.querySelector('.cockpit-shell__map .map-canvas')
- const all = dev && dev.getPointMarkerTargets ? dev.getPointMarkerTargets() : []
- return {
- targets: all.map((item) => `${item.code}@${Math.round(item.x)},${Math.round(item.y)}:${item.visible}`),
- engine: host ? host.dataset.mapEngine : '',
- markers: host ? host.dataset.mapMarkers : '',
- markersPlan: host ? host.dataset.mapMarkersPlan : '',
- summaryPhase: (() => {
- const s = dev && dev.mapSummary && dev.mapSummary.getState ? dev.mapSummary.getState() : null
- return s ? s.phase : ''
- })(),
- }
- })
- console.log(`MARKER DIAG (未等到 ${code}): ${JSON.stringify(diag)}`)
- return prev
- }
- await page.goto(ENTRY_URL, { timeout: 60000 })
- await page.waitForSelector('.cockpit-shell', { timeout: 20000 })
- if (await page.$('[data-cockpit-intro]')) {
- await page.click('[data-cockpit-intro]')
- await page.waitForSelector('[data-cockpit-intro]', { hidden: true, timeout: 10000 }).catch(() => {})
- }
- await page.waitForFunction(
- () => ['ready', 'failed', 'disabled'].includes(document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine),
- { timeout: 40000 },
- ).catch(() => {})
- // 推进地图摘要到 complete(真实用户点「加载下一页」;走查为省时用同一条装载器入口)。
- await page.evaluate(() => {
- const dev = window.__cockpitMap
- if (!dev || !dev.mapSummary) return
- let steps = 0
- const step = () => {
- const state = dev.mapSummary.getState()
- if (!state || ['complete', 'empty', 'failed', 'expired'].includes(state.phase) || steps >= 20) return
- dev.mapSummary.advance(); steps += 1; window.setTimeout(step, 220)
- }
- step()
- })
- await page.waitForFunction(() => {
- const dev = window.__cockpitMap
- const state = dev && dev.mapSummary ? dev.mapSummary.getState() : null
- return !state || ['complete', 'empty', 'failed', 'expired'].includes(state.phase)
- }, { timeout: 20000 }).catch(() => {})
- await waitSettled()
- // ── 第 1 步:真实鼠标点地图上的监测点标记 ──
- const marker = await stableMarker('MP-001')
- check('U1-monitoring-marker-projectable',
- Boolean(marker && Number.isFinite(marker.x) && Number.isFinite(marker.y)),
- marker ? `标记 ${marker.code} 窗口坐标 ${marker.x.toFixed(1)},${marker.y.toFixed(1)}` : '读不到可点的监测点标记')
- const locateMarker = () => page.evaluate(() => {
- const dev = window.__cockpitMap
- const all = dev && dev.getPointMarkerTargets ? dev.getPointMarkerTargets() : []
- return all.find((item) => item.visible && item.code === 'MP-001') || null
- })
- const readDetail = () => page.evaluate(() => {
- const card = document.querySelector('[data-point-detail]')
- const aside = document.querySelector('.cockpit-shell__detail')
- return {
- detailLevel: card ? card.dataset.detailLevel : '',
- pointCode: card ? card.dataset.pointCode : '',
- asidePresent: !!aside,
- rows: [...document.querySelectorAll('[data-device-layer-row]')].map((n) => n.dataset.deviceLayerCode),
- }
- })
- let opened = await readDetail()
- if (marker && !(opened.detailLevel === 'point' && opened.pointCode === 'MP-001')) {
- await clickUntil(locateMarker, async () => {
- const now = await readDetail()
- return now.detailLevel === 'point' && now.pointCode === 'MP-001'
- })
- opened = await readDetail()
- }
- check('U2-real-marker-click-opens-point-detail',
- opened.detailLevel === 'point' && opened.pointCode === 'MP-001' && opened.asidePresent,
- `真实点标记后 detailLevel=${opened.detailLevel} pointCode=${opened.pointCode} aside=${opened.asidePresent} 清单=${opened.rows.join('/')}`)
- // 遮罩让真实鼠标能命中画布(既有锚定浮层会盖住画布;仅测试态,不改生产行为)。
- await page.evaluate(() => {
- const detail = document.querySelector('.cockpit-shell__detail')
- if (detail) {
- detail.style.pointerEvents = 'none'
- detail.querySelectorAll('*').forEach((child) => { child.style.pointerEvents = 'none' })
- }
- })
- await waitSettled()
- const read = () => page.evaluate(() => {
- const host = document.querySelector('.cockpit-shell__map .map-canvas')
- const dev = window.__cockpitMap
- const devices = dev && dev.getAssociatedDevices ? dev.getAssociatedDevices() : { entities: [], onMap: 0 }
- return {
- overlap: dev && dev.getOverlapState ? dev.getOverlapState() : null,
- targets: dev && dev.getOverlapTargets ? dev.getOverlapTargets() : [],
- entities: devices.entities || [],
- onMap: devices.onMap,
- detailLevel: (document.querySelector('[data-point-detail]') || { dataset: {} }).dataset.detailLevel || '',
- devicePicked: host && host.dataset.mapDevicePicked,
- panelSelection: host && host.dataset.mapPanelSelection,
- rows: [...document.querySelectorAll('[data-device-layer-row]')].length,
- }
- })
- const before = await read()
- check('U3-overlap-aggregate-present',
- before.overlap && before.overlap.count === EXPECT.k &&
- before.entities.some((e) => e.overlapAggregate) &&
- before.rows === EXPECT.k,
- `K=${before.overlap && before.overlap.count}(期望 ${EXPECT.k})聚合实体=${before.entities.filter((e) => e.overlapAggregate).length} 清单=${before.rows} 行`)
- // ── 第 2 步:真实鼠标 hover + click 聚合符号 → 展开 ──
- const aggregateTarget = before.targets.find((t) => t.aggregate && t.visible)
- if (aggregateTarget) {
- await page.mouse.move(aggregateTarget.x, aggregateTarget.y)
- await sleep(350)
- const preview = await read()
- check('U4-real-hover-preview',
- preview.overlap && preview.overlap.preview === true && preview.overlap.expanded === false,
- `hover 后 preview=${preview.overlap && preview.overlap.preview} expanded=${preview.overlap && preview.overlap.expanded}`)
- await page.mouse.click(aggregateTarget.x, aggregateTarget.y)
- await sleep(600)
- }
- const expanded = await read()
- const visibleDevices = expanded.entities.filter((e) => e.deviceCode && e.visible)
- check('U5-real-click-expands-fan',
- expanded.overlap && expanded.overlap.state === EXPECT.state && visibleDevices.length === EXPECT.fanVisible,
- `state=${expanded.overlap && expanded.overlap.state}(期望 ${EXPECT.state})本屏设备=${visibleDevices.length}(期望 ${EXPECT.fanVisible}):${visibleDevices.map((e) => e.deviceCode).join('/')}`)
- // ── 第 3 步:真实鼠标点展开后的设备点 → 选中并进设备级 ──
- const deviceTarget = expanded.targets.find((t) => t.deviceCode === EXPECT.firstDevice && t.visible)
- if (deviceTarget) {
- await page.mouse.move(1200, 500)
- await sleep(150)
- await page.mouse.click(deviceTarget.x, deviceTarget.y)
- await sleep(1200)
- }
- const picked = await read()
- check('U6-real-click-fan-device-selects',
- Boolean(deviceTarget) && picked.devicePicked === EXPECT.firstDevice && picked.panelSelection === 'equipment',
- `设备目标=${Boolean(deviceTarget)}(${EXPECT.firstDevice}) picked=${picked.devicePicked} panel=${picked.panelSelection} detail=${picked.detailLevel}`)
- check('U7-no-page-errors', errors.length === 0, errors.length ? errors.slice(0, 3).join(' | ') : '无 pageerror / console.error')
- await page.close()
- await browser.close()
- const failed = results.filter((r) => !r.pass)
- console.log('')
- console.log(`T45 真实用户路径走查:${results.length - failed.length}/${results.length} 项通过。`)
- if (failed.length) { for (const item of failed) console.log(` · ${item.id}`); process.exit(1) }
- console.log('T45 真实用户路径走查全部通过。')
|