/** * T30 真机走查:进入引导(星球自转 + 点击进入)→ 飞到目标城市 → 快速归位(票 30)。 * * 断言的是维护者提出的痛点能不能被证伪:他"完全找不到目标位置,无法判断 DEM 与精细影像是否部署成功"。 * 所以这里不只测 UI,还要证明: * · 引导层真的在、文案真的在闪(animation 生效); * · 引导期间星球**真的在转**(采样两次相机经度有位移); * · 点一下之后镜头**真的落到沅陵县城坐标**(经纬度误差 < 1e-4°,且距离是县域级); * · 手动把镜头折腾走后,点「归位」能回到同一视角(中心 + heading/pitch/range)。 * * 运行(需先起 `yarn dev`):node .scratch/cockpit-shell-panel-layout/verify/t30-intro-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-t30-intro.png') const OUT_HOME = join(tmpdir(), 'cockpit-t30-home.png') const config = JSON.parse(readFileSync(new URL('../../../public/cockpit/config.json', import.meta.url), 'utf8')) const HOME = config.map.home 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)) const puppeteer = (await import(PUPPETEER)).default const browser = await puppeteer.launch({ executablePath: CHROME, headless: true, userDataDir: join(tmpdir(), 'cockpit-t30-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((injected) => { window.__COCKPIT_CONFIG__ = injected }, 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 sleep(1200) // ---------- A1 引导层存在、文案与闪烁 ---------- const intro = await page.evaluate(() => { const layer = document.querySelector('[data-cockpit-intro]') const title = layer?.querySelector('.cockpit-intro__title') const style = title ? getComputedStyle(title) : null return { present: !!layer, title: title?.textContent?.trim() || '', animation: style ? style.animationName : '', animationDuration: style ? style.animationDuration : '', zIndex: layer ? getComputedStyle(layer).zIndex : '', } }) check( 'A1-intro-visible', '进入时先看到引导层:中央「点击进入系统」文案在位,且闪烁动画真的生效', intro.present && intro.title.includes('点击进入系统') && intro.animation !== 'none' && Number(intro.zIndex) >= 10, JSON.stringify(intro), ) await page.screenshot({ path: OUT }) // ---------- A2 星球真的在自转(采样相机经度) ---------- const sampleLon = () => page.evaluate(() => { const state = window.__cockpitMap.getCameraState() return state?.pivot ? state.pivot.lon : null }) const spinA = await sampleLon() await sleep(2600) const spinB = await sampleLon() check( 'A2-globe-spinning', '引导期间星球确实在自转(2.6 秒内相机中心经度发生位移)', spinA !== null && spinB !== null && Math.abs(spinB - spinA) > 0.05, `lon ${spinA} → ${spinB} (Δ=${spinB !== null && spinA !== null ? (spinB - spinA).toFixed(3) : 'n/a'})`, ) // ---------- A3 点击进入:引导消失 + 镜头落到沅陵县城 ---------- await page.click('[data-cockpit-intro]') await sleep(2500) const afterEnter = await page.evaluate(() => { const state = window.__cockpitMap.getCameraState() return { introGone: !document.querySelector('[data-cockpit-intro]'), pivot: state?.pivot || null, hpr: state?.hpr || null, settled: state?.settled, atHomeAttr: document.querySelector('.cockpit-shell__home')?.dataset.homeAt || '', } }) const dLon = Math.abs((afterEnter.pivot?.lon ?? 999) - HOME.lon) const dLat = Math.abs((afterEnter.pivot?.lat ?? 999) - HOME.lat) check( 'A3-enter-flies-to-target', '点击后引导层消失,镜头落到目标城市(经纬度误差 < 1e-4°,距离为县域级)', afterEnter.introGone && dLon < 1e-4 && dLat < 1e-4 && afterEnter.hpr?.range > 1000, JSON.stringify({ dLon, dLat, pivot: afterEnter.pivot, hpr: afterEnter.hpr, settled: afterEnter.settled }), ) await sleep(1500) await page.screenshot({ path: OUT_HOME }) // ---------- A4 自转在进入后停止 ---------- const stopA = await sampleLon() await sleep(2000) const stopB = await sampleLon() check( 'A4-spin-stopped', '进入系统后自转停止(2 秒内相机中心不再漂移)', stopA !== null && stopB !== null && Math.abs(stopB - stopA) < 1e-6, `lon ${stopA} → ${stopB}`, ) // ---------- A5 折腾走之后,归位按钮能回到同一视角 ---------- const homeBefore = await page.evaluate(() => { const s = window.__cockpitMap.getCameraState() return { pivot: s.pivot, hpr: s.hpr } }) // 用真实输入把它推开:左键拖拽平移 + 滚轮缩放 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 } }) await page.mouse.move(canvas.x, canvas.y) await page.mouse.down({ button: 'left' }) await page.mouse.move(canvas.x + 180, canvas.y + 120, { steps: 8 }) await page.mouse.up({ button: 'left' }) await page.mouse.wheel({ deltaY: 600 }) await sleep(1500) const moved = await page.evaluate(() => { const s = window.__cockpitMap.getCameraState() return { pivot: s.pivot, hpr: s.hpr, atHomeAttr: document.querySelector('.cockpit-shell__home')?.dataset.homeAt } }) const movedAway = Math.abs(moved.pivot.lon - homeBefore.pivot.lon) > 1e-4 || Math.abs(moved.hpr.range - homeBefore.hpr.range) > 1 check( 'A5a-moved-away', '用手动操作把镜头推离目标(平移 + 缩放),且顶栏「归位」按钮显示未在家', movedAway && moved.atHomeAttr === 'no', JSON.stringify({ moved: { lon: moved.pivot.lon, range: moved.hpr.range }, atHome: moved.atHomeAttr }), ) await page.click('.cockpit-shell__home') await sleep(3000) const backHome = await page.evaluate(() => { const s = window.__cockpitMap.getCameraState() return { pivot: s.pivot, hpr: s.hpr, atHomeAttr: document.querySelector('.cockpit-shell__home')?.dataset.homeAt } }) const dLonBack = Math.abs(backHome.pivot.lon - HOME.lon) const dLatBack = Math.abs(backHome.pivot.lat - HOME.lat) const dRangeBack = Math.abs(backHome.hpr.range - HOME.range) / HOME.range check( 'A5b-home-button-returns', '点「归位」后回到目标视角(中心 < 1e-4°、距离误差 < 5%),按钮显示已在家', dLonBack < 1e-4 && dLatBack < 1e-4 && dRangeBack < 0.05 && backHome.atHomeAttr === 'yes', JSON.stringify({ dLon: dLonBack, dLat: dLatBack, rangeErr: dRangeBack, atHome: backHome.atHomeAttr }), ) check('A6-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(`T30 走查:${results.length - failed.length}/${results.length} 项通过。`) console.log(`截图:引导页 ${OUT} / 进入后 ${OUT_HOME}`) if (failed.length) { console.log('未通过:' + failed.map((f) => f.id).join(', ')) process.exit(1) } console.log('T30 走查全部通过。')