| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257 |
- /**
- * T21 真机走查:地面/管线视图切换(票 21 验收第 4、5 条)。
- *
- * 逻辑脚本(t21-view-modes.mjs)把取值测透了,这里只测真机上才成立的部分:
- * · 点「管线视图」:相机真的被压低 + pitch 真的加大,且**旋转中心一点没变**;
- * · 地形真的变半透明(globe.translucency 生效);
- * · 切换视图后**三键分工照旧**(左键仍只平移、滚轮仍只改距离);
- * · 回「地面视图」:倾角/距离按进管线前的基线还原;
- * · 视图条件在**切换一级模式时不重置**(与范围/行业同为全局条件)。
- *
- * 运行(需先起 `yarn dev`):node .scratch/cockpit-shell-panel-layout/verify/t21-view-modes-walkthrough.mjs
- */
- import { readFileSync } from 'node:fs'
- import { join } from 'node:path'
- import { tmpdir } from 'node:os'
- const BASE = process.env.COCKPIT_BASE || 'http://localhost:8000'
- const ENTRY_URL = process.env.COCKPIT_URL || `${BASE}/#/cockpit`
- const CHROME = process.env.CHROME_PATH || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
- const PUPPETEER = 'file:///C:/Users/ASUS/.dsh/profiles/desktop/node_modules/puppeteer-core/lib/puppeteer/puppeteer-core.js'
- const OUT = join(tmpdir(), 'cockpit-t21-pipeline.png')
- const config = JSON.parse(readFileSync(new URL('../../../public/cockpit/config.json', import.meta.url), 'utf8'))
- /** 地面视图的默认倾角(与 domain/viewModes.js 的 GROUND_PITCH 一致;走查独立写死,避免"用被测值断言被测值")。 */
- const GROUND_PITCH = -55
- const results = []
- function check(id, desc, pass, evidence) {
- results.push({ id, desc, pass })
- console.log(`${pass ? 'PASS' : 'FAIL'} ${id} ${desc}${evidence ? ` — ${evidence}` : ''}`)
- }
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
- /**
- * 等相机停稳(`settled === true`)再断言,而不是死等固定毫秒。
- * 依据:相机是阻尼收敛的,机器负载高时 2.5 秒可能还没到位 —— 实测在连续跑多个 Chrome 的
- * 回归批次里,固定 sleep 会把 A2/A5 判红(单独跑则 10/10),是**测试的时序问题**而非功能回归。
- *
- * 判据里必须容 null:引擎重建的空档里 `getCameraState()` 会返回 null,
- * 直接读 `.settled` 会在页面里抛错(进而被算成页面级报错)。
- */
- async function waitSettled(page, timeoutMs = 25000) {
- await page
- .waitForFunction(
- () => {
- const api = window.__cockpitMap
- if (!api) return false
- const state = api.getCameraState()
- return !!state && state.settled === true
- },
- { timeout: timeoutMs, polling: 250 },
- )
- .catch(() => {})
- await sleep(250)
- }
- /**
- * 把镜头放到目标点。**带重试** —— 与外壳里「归位 / 进入系统」同一套兜底:
- * 相机契约还没挂上时 `goHome()` 返回 false(不静默生效),单次调用会让后续断言全部错位。
- */
- async function goHome(page, home) {
- for (let i = 0; i < 40; i++) {
- const applied = await page.evaluate((h) => window.__cockpitMap.goHome(h), home).catch(() => false)
- if (applied) return true
- await sleep(250)
- }
- return false
- }
- const puppeteer = (await import(PUPPETEER)).default
- const browser = await puppeteer.launch({
- executablePath: CHROME,
- headless: true,
- userDataDir: join(tmpdir(), 'cockpit-t21vm-profile'),
- defaultViewport: { width: 1680, height: 950 },
- args: ['--no-sandbox', '--disable-dev-shm-usage', '--no-first-run', '--no-default-browser-check'],
- })
- console.log(`Chrome: ${await browser.version()}`)
- try {
- 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()}`)
- })
- await page.evaluateOnNewDocument((cfg) => {
- window.__COCKPIT_CONFIG__ = { ...cfg, map: { ...cfg.map, intro: { enabled: false } } }
- }, config)
- await page.goto(ENTRY_URL, { waitUntil: 'networkidle2', timeout: 60000 })
- await page.waitForSelector('.cockpit-shell', { timeout: 20000 })
- await page.waitForFunction(
- () => document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine === 'ready',
- { timeout: 60000 },
- )
- await page.waitForFunction(() => !!window.__cockpitMap, { timeout: 20000 })
- await waitSettled(page)
- const readState = () =>
- page.evaluate(() => {
- const s = window.__cockpitMap.getCameraState()
- const host = document.querySelector('.cockpit-shell__map .map-canvas')
- return {
- pivot: s.pivot,
- hpr: s.hpr,
- viewMode: window.__cockpitMap.getViewMode(),
- dataAttr: host?.dataset.mapViewMode || '',
- pressed: Array.from(document.querySelectorAll('.cockpit-shell__view-btn'))
- .filter((b) => b.getAttribute('aria-pressed') === 'true')
- .map((b) => b.dataset.viewMode),
- }
- })
- // 先把镜头放到沅陵(不然"管线视图"没有可看的对象)
- await goHome(page, config.map.home)
- await waitSettled(page)
- // ---------- A1 初始是地面视图,控件状态正确 ----------
- const ground = await readState()
- check(
- 'A1-ground-default',
- '初始为地面视图:状态与按钮 aria-pressed 同步,宿主带 data-map-view-mode',
- ground.viewMode === 'ground' &&
- ground.dataAttr === 'ground' &&
- ground.pressed.length === 1 &&
- ground.pressed[0] === 'ground',
- JSON.stringify({ viewMode: ground.viewMode, dataAttr: ground.dataAttr, pressed: ground.pressed }),
- )
- const beforeSwitch = { pivot: ground.pivot, hpr: ground.hpr }
- // ---------- A2 切到管线视图:压低相机 + 加大 pitch,中心不动 ----------
- await page.click('.cockpit-shell__view-btn[data-view-mode="pipeline"]')
- await waitSettled(page)
- const pipeline = await readState()
- const samePivot =
- pipeline.pivot &&
- Math.abs(pipeline.pivot.lon - beforeSwitch.pivot.lon) < 1e-9 &&
- Math.abs(pipeline.pivot.lat - beforeSwitch.pivot.lat) < 1e-9 &&
- Math.abs((pipeline.pivot.height || 0) - (beforeSwitch.pivot.height || 0)) < 1e-6
- check(
- 'A2-pipeline-camera',
- '管线视图真的压低相机(距离变小)且 pitch 加大(更接近平视)',
- pipeline.viewMode === 'pipeline' &&
- pipeline.hpr.range < beforeSwitch.hpr.range &&
- pipeline.hpr.pitch > beforeSwitch.hpr.pitch,
- JSON.stringify({ before: beforeSwitch.hpr, after: pipeline.hpr }),
- )
- check(
- 'A2-center-untouched',
- '切换视图**不重置旋转中心**(经纬高完全一致)—— 决策地图票 05 的硬约束',
- samePivot,
- JSON.stringify({ before: beforeSwitch.pivot, after: pipeline.pivot }),
- )
- check(
- 'A2-controls-synced',
- '按钮高亮/aria-pressed 与宿主 data-map-view-mode 都同步到 pipeline',
- pipeline.pressed.length === 1 && pipeline.pressed[0] === 'pipeline' && pipeline.dataAttr === 'pipeline',
- JSON.stringify({ pressed: pipeline.pressed, dataAttr: pipeline.dataAttr }),
- )
- // ---------- A3 管线视图的图层状态确实下发了 ----------
- const viewState = await page.evaluate(() => ({
- dataAttr: document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapViewMode || '',
- engine: window.__cockpitMap.getViewMode(),
- }))
- check(
- 'A3-view-applied',
- '管线视图已下发给地图服务(宿主 data-map-view-mode 与 getViewMode() 都为 pipeline)',
- viewState.dataAttr === 'pipeline' && viewState.engine === 'pipeline',
- JSON.stringify(viewState),
- )
- await page.screenshot({ path: OUT })
- // ---------- A4 三键分工不变:左键仍只平移、滚轮仍只改距离 ----------
- const canvas = await page.evaluate(() => {
- const c = document.querySelector('.cockpit-shell__map .map-canvas canvas')
- const r = c.getBoundingClientRect()
- return { x: r.x + r.width / 2, y: r.y + r.height / 2 }
- })
- const prePan = await readState()
- await page.mouse.move(canvas.x, canvas.y)
- await page.mouse.down({ button: 'left' })
- await page.mouse.move(canvas.x + 120, canvas.y + 80, { steps: 8 })
- await page.mouse.up({ button: 'left' })
- await sleep(1200)
- const postPan = await readState()
- check(
- 'A4-pan-still-pans',
- '管线视图里左键仍只平移:中心移动、距离与倾角基本不变',
- Math.abs(postPan.pivot.lon - prePan.pivot.lon) > 1e-6 &&
- Math.abs(postPan.hpr.range - prePan.hpr.range) < prePan.hpr.range * 0.02 &&
- Math.abs(postPan.hpr.pitch - prePan.hpr.pitch) < 1,
- JSON.stringify({ dLon: postPan.pivot.lon - prePan.pivot.lon, before: prePan.hpr, after: postPan.hpr }),
- )
- const preWheel = await readState()
- await page.mouse.wheel({ deltaY: 400 })
- await sleep(1200)
- const postWheel = await readState()
- check(
- 'A4-wheel-still-zooms',
- '管线视图里滚轮仍只改距离:距离变化、倾角与中心基本不变',
- Math.abs(postWheel.hpr.range - preWheel.hpr.range) > 1 &&
- Math.abs(postWheel.hpr.pitch - preWheel.hpr.pitch) < 1,
- JSON.stringify({ before: preWheel.hpr, after: postWheel.hpr }),
- )
- // ---------- A5 回地面:按进管线前的基线还原 ----------
- await page.click('.cockpit-shell__view-btn[data-view-mode="ground"]')
- await waitSettled(page)
- const backGround = await readState()
- check(
- 'A5-ground-restores',
- '回地面视图:倾角与距离还原到进管线前(不是留在管线视角)',
- backGround.viewMode === 'ground' &&
- Math.abs(backGround.hpr.pitch - GROUND_PITCH) < 3 &&
- Math.abs(backGround.hpr.range - beforeSwitch.hpr.range) < beforeSwitch.hpr.range * 0.05,
- JSON.stringify({ restored: backGround.hpr, expectedPitch: GROUND_PITCH, expectedRange: beforeSwitch.hpr.range }),
- )
- // ---------- A6 视图是全局条件:切一级模式不重置 ----------
- await page.click('.cockpit-shell__view-btn[data-view-mode="pipeline"]')
- await sleep(1500)
- const modeButtons = await page.$$('.cockpit-shell__mode')
- await modeButtons[1].click()
- await sleep(1200)
- const afterModeSwitch = await page.evaluate(() => {
- const pressed = Array.from(document.querySelectorAll('.cockpit-shell__view-btn'))
- .filter((b) => b.getAttribute('aria-pressed') === 'true')
- .map((b) => b.dataset.viewMode)
- return { mode: window.__cockpitMap.getViewMode(), pressed }
- })
- check(
- 'A6-global-across-modes',
- '视图条件是全局条件:切一级模式后仍是管线视图(与范围/行业同级,不随模式重置)',
- afterModeSwitch.mode === 'pipeline' && afterModeSwitch.pressed[0] === 'pipeline',
- JSON.stringify(afterModeSwitch),
- )
- check('A7-no-page-errors', '全程无页面级报错', errors.length === 0, JSON.stringify(errors.slice(0, 3)))
- } finally {
- await browser.close()
- }
- const failed = results.filter((r) => !r.pass)
- console.log('')
- console.log(`T21 视图走查:${results.length - failed.length}/${results.length} 项通过。截图:${OUT}`)
- if (failed.length) {
- console.log('未通过:' + failed.map((f) => f.id).join(', '))
- process.exit(1)
- }
- console.log('T21 视图走查全部通过。')
|