| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200 |
- /**
- * 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 走查全部通过。')
|