| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303 |
- /**
- * T38 真机走查:区内视距雾的可观测行为与生命周期(票 38)。
- *
- * 逻辑闸门(t38-fog.mjs)把曲线、门控、降级、色相判据测透了,这里只测**浏览器里才成立**的部分:
- * · stage 真的挂在场景上(且**只有一个** —— 不引入第二渲染器/第二个后处理级);
- * · 进区 → 雾启用并推进到强度 1;返回 → 淡出到 0 并停在 global;
- * · **连续进出只认最新目标**(进 → 立刻返回 → 立刻再进,最终必须是 locked 强度 1,
- * 而不是被旧的那轮淡出关掉);
- * · 降级可演示:无深度支持 / 观察到透明错位(减弱)/ 用户暂关,且**相机限位不受影响**;
- * · 读数齐全(支持/可执行/强度/参数/原因),且 `coversEdges` 恒为 no(不承诺已遮缝);
- * · 性能采样能取到帧间隔(**明说这是渲染事件间隔、不是 GPU 时间,也不能单独判达标**)。
- *
- * **不做的事(诚实边界)**:最终色相的目视确认、以及"矩形边缘确实被遮住"的观感判定
- * 都是**维护者的**验收(决策 08 要求真实画面复核);本脚本只提供可观测读数与截图,不代判。
- * 性能采样(A9)只给出参考读数,**不**宣称 ≤15% 达标(B 与 A 的对照还需无原型基线)。
- *
- * **前置条件:标签页必须可见。** 隐藏标签页里浏览器冻结 rAF、Cesium 也不渲染,
- * 于是强度推进与性能采样都取不到数据(A9 会因 `count=0` 判红)。
- * 那是环境限制而非功能缺陷 —— 本票真机验证时就撞上过(该标签页 `document.hidden=true`)。
- *
- * 运行(需先起 `yarn dev`,并把浏览器标签页切到前台):
- * node .scratch/cockpit-shell-panel-layout/verify/t38-fog-walkthrough.mjs
- * 按 AGENTS.md:本脚本不在改代码后自动运行,由维护者明确要求时才跑。
- */
- 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 config = JSON.parse(readFileSync(new URL('../../../public/cockpit/config.json', import.meta.url), 'utf8'))
- 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-t38-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()}`)
- /** 等雾的强度达到目标(或阶段落定),避免死等毫秒。 */
- async function waitFog(page, predicate, timeoutMs = 15000) {
- await page
- .waitForFunction(
- (src) => {
- const api = window.__cockpitMap
- if (!api) return false
- const fog = api.getFogState()
- // 只支持简单谓词:阶段相等或强度达标(避免把函数传进页面)。
- if (src.kind === 'phase') return fog.phase === src.value
- if (src.kind === 'strengthAtLeast') return fog.strength >= src.value
- if (src.kind === 'strengthZero') return fog.strength <= 0.0001
- return false
- },
- { timeout: timeoutMs, polling: 120 },
- predicate,
- )
- .catch(() => {})
- await sleep(150)
- }
- const readFog = (page) =>
- page.evaluate(() => {
- const host = document.querySelector('.cockpit-shell__map .map-canvas')
- const api = window.__cockpitMap
- const fog = api ? api.getFogState() : null
- return {
- hostFog: host ? host.dataset.mapFog : '',
- hostPhase: host ? host.dataset.mapFogPhase : '',
- hostStrength: host ? host.dataset.mapFogStrength : '',
- hostDegrade: host ? host.dataset.mapFogDegrade : '',
- hostCoversEdges: host ? host.dataset.mapFogCoversEdges : '',
- hostStages: host ? host.dataset.mapFogStages : '',
- hostRangeMax: host ? host.dataset.mapRangeMax : '',
- areaState: api ? api.getAreaState() : '',
- fog,
- // chip 用 `[data-fog-chip]` 而不是 `[data-map-fog]`:后者同时挂在地图宿主上
- // (整页文本),querySelector 会先命中宿主 —— 那样这条断言读的是错误元素。
- chip: (document.querySelector('[data-fog-chip]') || {}).textContent?.trim() || '',
- }
- })
- 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(1500)
- // ---------- A1 参数就是规格那一套 ----------
- const initial = await readFog(page)
- check(
- 'A1-spec-params',
- '参数按规格固化:世界射线距离 18,000/42,000 m、基准雾色 #bdcdd6(缺省不配也是这一套)',
- initial.fog.params.startDistance === 18000 &&
- initial.fog.params.endDistance === 42000 &&
- initial.fog.params.color.toLowerCase() === '#bdcdd6',
- JSON.stringify({ s: initial.fog.params.startDistance, e: initial.fog.params.endDistance, c: initial.fog.params.color }),
- )
- // ---------- A2 区外:关闭、单 stage ----------
- check(
- 'A2-global-fog-off',
- '区外(global):雾关闭(强度 0、阶段 global),但处理级**已挂且只有一个**(复用而非每次重建)',
- initial.areaState === 'global' && initial.fog.strength === 0 && initial.fog.phase === 'global' && initial.hostStages === '1',
- JSON.stringify({ area: initial.areaState, strength: initial.fog.strength, phase: initial.fog.phase, stages: initial.hostStages }),
- )
- // ---------- A3 读数齐全 + 不承诺已遮缝 ----------
- check(
- 'A3-readout-complete-no-false-promise',
- '读数含支持/可执行/是否跑过/强度/参数/原因;且 coversEdges 恒为 no(**不提供未经验证的"已遮缝"承诺**)',
- initial.fog.supported === true &&
- typeof initial.fog.executable === 'boolean' &&
- typeof initial.fog.ranOnce === 'boolean' &&
- typeof initial.fog.strength === 'number' &&
- typeof initial.fog.startDistance === 'number' &&
- initial.fog.coversEdges === false &&
- initial.hostCoversEdges === 'no',
- JSON.stringify({
- supported: initial.fog.supported,
- executable: initial.fog.executable,
- ranOnce: initial.fog.ranOnce,
- coversEdges: initial.fog.coversEdges,
- host: initial.hostCoversEdges,
- }),
- )
- // ---------- A3b 待命不得报成失败(本票真机验证时踩到的读数 bug) ----------
- // 区外待命时若报 failed,用户会在最不该报警的时刻看到"视距雾不可用"。
- check(
- 'A3b-standby-is-not-failure',
- '区外待命(强度 0)时读数必须是 ready 而不是 failed(待命 ≠ 故障)',
- initial.areaState === 'global' && initial.fog.state === 'ready' && initial.fog.executable === true,
- JSON.stringify({ area: initial.areaState, state: initial.fog.state, executable: initial.fog.executable }),
- )
- // ---------- A4 进区 → 雾启用并达到满强度 ----------
- await page.evaluate(() => window.__cockpitMap.enterArea())
- await waitFog(page, { kind: 'strengthAtLeast', value: 1 })
- await page.evaluate(() => window.__cockpitMap.goHome())
- await waitFog(page, { kind: 'phase', value: 'locked' })
- const locked = await readFog(page)
- check(
- 'A4-locked-fog-on',
- '进区(locked):雾阶段落 locked、强度到 1、宿主读数同步、chip 显示强度与距离带',
- locked.areaState === 'locked' &&
- locked.fog.phase === 'locked' &&
- locked.fog.strength === 1 &&
- Number(locked.hostStrength) === 1 &&
- locked.chip.includes('视距雾'),
- JSON.stringify({ area: locked.areaState, phase: locked.fog.phase, strength: locked.fog.strength, chip: locked.chip }),
- )
- // ---------- A5 连续进出只认最新目标 ----------
- // 进 → 立刻返回 → 立刻再进;旧的淡出**不得**把新的进入关掉。
- await page.evaluate(() => {
- window.__cockpitMap.returnGlobal()
- window.__cockpitMap.enterArea()
- window.__cockpitMap.returnGlobal()
- window.__cockpitMap.enterArea()
- })
- await waitFog(page, { kind: 'phase', value: 'locked' })
- const rapid = await readFog(page)
- check(
- 'A5-rapid-toggle-latest-wins',
- '连续进/返 4 次后只认最新目标:阶段锁在 locked、强度仍为 1(过期淡出没有关掉新的进入)',
- rapid.fog.phase === 'locked' && rapid.fog.strength === 1 && rapid.areaState === 'locked',
- JSON.stringify({ phase: rapid.fog.phase, strength: rapid.fog.strength, area: rapid.areaState }),
- )
- // ---------- A5b 读数不抖(隐藏/不渲染时不得在 ready 与 failed 之间跳) ----------
- // `stage.ready` 随渲染活动变化:隐藏标签页不渲染时会落回 false。
- // 采样一段时间,state 必须始终是 ready —— 若拿 ready 当判据,这里会看到 failed 混进来。
- {
- const samples = []
- for (let i = 0; i < 5; i++) {
- const s = await readFog(page)
- samples.push(s.fog.state)
- await sleep(400)
- }
- const unique = [...new Set(samples)]
- check(
- 'A5b-readout-stable',
- `读数在连续采样中保持稳定(不得在 ready/failed 之间跳)`,
- unique.length === 1 && unique[0] === 'ready',
- JSON.stringify({ samples }),
- )
- }
- // ---------- A6 返回全球 → 淡出并停在 global ----------
- await page.evaluate(() => window.__cockpitMap.returnGlobal())
- await waitFog(page, { kind: 'phase', value: 'global' })
- const backGlobal = await readFog(page)
- check(
- 'A6-return-fades-out',
- '返回全球:雾淡出到 0 并停在 global 阶段(不是突然消失,也不是留着不走)',
- backGlobal.fog.phase === 'global' && backGlobal.fog.strength === 0 && backGlobal.areaState === 'global',
- JSON.stringify({ phase: backGlobal.fog.phase, strength: backGlobal.fog.strength }),
- )
- // ---------- A7 降级:减弱(观察透明错位)与暂关,且不动相机限位 ----------
- await page.evaluate(() => window.__cockpitMap.enterArea())
- await waitFog(page, { kind: 'strengthAtLeast', value: 1 })
- const beforeDegrade = await readFog(page)
- await page.evaluate(() => window.__cockpitMap.setViewMode('pipeline'))
- await sleep(600)
- const normalPipeline = await readFog(page)
- await page.evaluate(() => window.__cockpitMap.setFogTranslucentArtifacts(true))
- await sleep(400)
- const reduced = await readFog(page)
- await page.evaluate(() => window.__cockpitMap.setFogUserDisabled(true))
- await sleep(400)
- const off = await readFog(page)
- check(
- 'A7-degrade-reduced-then-off',
- '降级可演示:**正常管线视图不预关闭**;观察到透明错位 → 减弱(强度被压低);用户暂关 → 关闭并带原因',
- normalPipeline.fog.degrade === 'none' &&
- normalPipeline.fog.strength === 1 &&
- reduced.fog.degrade === 'reduced' &&
- Number(reduced.hostStrength) < 1 &&
- off.fog.degrade === 'off' &&
- Number(off.hostStrength) === 0 &&
- off.fog.reason.length > 0,
- JSON.stringify({
- normalPipeline: { degrade: normalPipeline.fog.degrade, strength: normalPipeline.fog.strength },
- reduced: { degrade: reduced.fog.degrade, strength: reduced.fog.strength },
- off: { degrade: off.fog.degrade, strength: off.fog.strength, reason: off.fog.reason },
- }),
- )
- check(
- 'A7b-degrade-keeps-camera-limits',
- '降级**不动相机限位**(票 37 的承诺:可读性降级不解除限位)——上限仍是 home.range',
- beforeDegrade.hostRangeMax === reduced.hostRangeMax && reduced.hostRangeMax === '12000',
- JSON.stringify({ before: beforeDegrade.hostRangeMax, after: reduced.hostRangeMax }),
- )
- // ---------- A8 恢复:暂关与减弱都能还原 ----------
- await page.evaluate(() => {
- window.__cockpitMap.setFogUserDisabled(false)
- window.__cockpitMap.setFogTranslucentArtifacts(false)
- })
- await sleep(400)
- const restored = await readFog(page)
- check(
- 'A8-degrade-recoverable',
- '恢复后回到正常档且强度回到 1(降级是**可逆**的,不是一次性关掉)',
- restored.fog.degrade === 'none' && restored.fog.strength === 1,
- JSON.stringify({ degrade: restored.fog.degrade, strength: restored.fog.strength }),
- )
- // ---------- A9 性能采样可跑(并如实标注口径) ----------
- await page.evaluate(() => window.__cockpitMap.startFogPerfSample('with-fog'))
- await sleep(3000)
- const perf = await page.evaluate(() => window.__cockpitMap.stopFogPerfSample())
- check(
- 'A9-perf-sample-collects',
- '性能采样能取到帧间隔并算出 P95;读数**明确标注**这是渲染事件间隔、不是 GPU 时间、不能单独判达标',
- !!perf && perf.count > 0 && Number.isFinite(perf.p95FrameIntervalMs) && /不是 GPU 时间/.test(perf.note),
- JSON.stringify(perf),
- )
- check('A10-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(`T38 走查:${results.length - failed.length}/${results.length} 项通过。`)
- if (failed.length) {
- console.log('未通过:' + failed.map((f) => f.id).join(', '))
- process.exit(1)
- }
- console.log('T38 走查全部通过。')
- console.log('注意:最终色相与"矩形边缘是否真被遮住"的观感判定属维护者验收,本脚本不代判。')
|