| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167 |
- /**
- * 票 36 验收探针:调色 shader 修复后的真实页面交互覆盖(验收 1/2/3 的程序面)。
- *
- * 覆盖(对应票面验收条目):
- * ① 进入系统 → 引擎 ready,无片元 shader 编译错误、无 Rendering has stopped(验收 1);
- * ② freshness tick(等待 >30s 覆盖一个刷新周期)期间渲染持续(验收 3「freshness刷新」);
- * ③ 四模式切换(总体概览/基础设施/运行监测/预警处置)各回 overview,渲染不停止(验收 3「模式切换」);
- * ④ 真实对象定位:点击一个台账/清单定位入口或双击标记不炸帧(验收 3「真实对象定位」——以模式切换后引擎仍 ready + 无 pageerror 为准,不依赖具体命中);
- * ⑤ 90 秒静置窗口的帧间隔 p95,无周期尖峰(验收 3「没有周期抽搐」——软渲染环境只看相对形态,与票 33 同口径);
- * ⑥ 全程 console error 分类计数:shader/Rendering has stopped 必须为 0(验收 1)。
- *
- * 运行(dev server 需已在 :8000,无需浏览器会话——绕开 5 会话上限):
- * node .scratch/cockpit-real-data-wayfinding/verify/artifacts/t36/probe-acceptance.mjs
- */
- import { mkdir, writeFile } from 'node:fs/promises'
- import path from 'node:path'
- import { cdpLaunch } from '../../../../cockpit-shell-panel-layout/verify/cdp-driver.mjs'
- const CHROME = process.env.CHROME_PATH || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
- const TARGET_URL = process.env.T36_URL || 'http://127.0.0.1:8000/#/cockpit'
- const OUT_DIR = path.resolve('.scratch/cockpit-real-data-wayfinding/verify/artifacts/t36')
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
- const consoleErrors = []
- const pageErrors = []
- async function main() {
- await mkdir(OUT_DIR, { recursive: true })
- const browser = await cdpLaunch({ executablePath: CHROME, headless: true, width: 1920, height: 1080 })
- const page = await browser.newPage()
- page.on('console', (msg) => consoleErrors.push(msg.text()))
- page.on('pageerror', (err) => pageErrors.push(String(err)))
- // ① 进入系统
- await page.goto(TARGET_URL, { timeout: 60000 })
- await sleep(1500)
- try {
- await page.click('[data-cockpit-intro]', { timeout: 8000 })
- } catch { /* 入口按钮可能已自动消失 */ }
- // 引擎态就绪(map-canvas data-map-engine ∈ ready/failed/disabled)
- await page.waitForFunction(
- () => ['ready', 'failed', 'disabled'].includes(
- document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine || ''
- ),
- { timeout: 45000 },
- )
- const engineState = await page.evaluate(
- () => document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine || 'unknown',
- )
- const mapGrade = await page.evaluate(
- () => document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapGrade || 'unknown',
- )
- // ② freshness tick:静置 35s(>30s 默认刷新周期),同时采帧间隔
- const frameGaps = []
- await page.evaluate(() => {
- window.__t36frames = []
- const tick = (t) => { window.__t36frames.push(t); if (window.__t36frames.length < 4000) requestAnimationFrame(tick) }
- requestAnimationFrame(tick)
- })
- await sleep(35000)
- const gaps = await page.evaluate(() => {
- const f = window.__t36frames || []
- const g = []
- for (let i = 1; i < f.length; i++) g.push(f[i] - f[i - 1])
- return g
- })
- for (const g of gaps) frameGaps.push(g)
- const stats = (v) => {
- if (!v.length) return null
- const s = [...v].sort((a, b) => a - b)
- return { n: s.length, p50: Math.round(s[Math.floor(s.length * 0.5)]), p95: Math.round(s[Math.floor(s.length * 0.95)]), max: Math.round(s[s.length - 1]) }
- }
- const idleStats = stats(frameGaps)
- // ③ 四模式切换(回 overview 收尾)
- const modes = ['总体概览', '基础设施', '运行监测', '预警处置']
- const modeResults = []
- for (const label of modes) {
- const before = consoleErrors.length
- await page.evaluate((text) => {
- const btn = [...document.querySelectorAll('nav[aria-label="一级模式"] button, nav button')]
- .find((b) => b.textContent.trim() === text)
- if (btn) btn.click()
- return !!btn
- }, label)
- await sleep(4000)
- const state = await page.evaluate(
- () => document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine || 'unknown',
- )
- modeResults.push({ label, engineAfter: state, newConsoleErrors: consoleErrors.length - before })
- }
- // ④ 真实对象定位:运行监测模式里点第一个可定位卡片(有则点,无则如实记录跳过)
- await page.evaluate(() => {
- const btn = [...document.querySelectorAll('nav button')].find((b) => b.textContent.trim() === '运行监测')
- if (btn) btn.click()
- })
- await sleep(3500)
- let locateResult = 'no-locate-target'
- try {
- await page.evaluate(() => {
- const card = [...document.querySelectorAll('button, [role="button"], .panel-action')]
- .find((b) => /定位|locate/.test(b.textContent || '') || (b.dataset && /locate/.test(b.dataset.action || '')))
- if (card) card.click()
- })
- await sleep(2500)
- locateResult = 'locate-clicked'
- } catch (e) {
- locateResult = 'locate-error:' + String(e).slice(0, 80)
- }
- const engineAfterLocate = await page.evaluate(
- () => document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine || 'unknown',
- )
- // ⑤ 交互后第二段 30s 静置(覆盖一次 freshness tick 发生在切换之后)
- const frameGaps2 = []
- await page.evaluate(() => { window.__t36frames = [] })
- await sleep(30000)
- const gaps2 = await page.evaluate(() => {
- const f = window.__t36frames || []
- const g = []
- for (let i = 1; i < f.length; i++) g.push(f[i] - f[i - 1])
- return g
- })
- for (const g of gaps2) frameGaps2.push(g)
- const postStats = stats(frameGaps2)
- // ⑥ 分类断言
- const shaderErrors = consoleErrors.filter((t) =>
- /Fragment shader failed to compile|Illegal use of reserved word|varying/i.test(t))
- const renderStopped = consoleErrors.filter((t) => /Rendering has stopped/i.test(t))
- const engineOk = engineState === 'ready' && modeResults.every((m) => m.engineAfter === 'ready') && engineAfterLocate === 'ready'
- const checks = [
- { id: 'engine-ready', passed: engineOk, evidence: { engineState, modes: modeResults.map(m => m.engineAfter), afterLocate: engineAfterLocate } },
- { id: 'no-shader-compile-error', passed: shaderErrors.length === 0, evidence: { count: shaderErrors.length, sample: shaderErrors.slice(0, 2) } },
- { id: 'no-rendering-stopped', passed: renderStopped.length === 0, evidence: { count: renderStopped.length } },
- { id: 'grade-stage-enabled-and-served', passed: mapGrade === 'on', evidence: { mapGrade } },
- { id: 'no-pageerror', passed: pageErrors.length === 0, evidence: { count: pageErrors.length, sample: pageErrors.slice(0, 2) } },
- { id: 'idle-frame-budget', passed: !!idleStats && idleStats.max < 2000, evidence: idleStats },
- { id: 'post-interaction-frame-budget', passed: !!postStats && postStats.max < 2000, evidence: postStats },
- { id: 'modes-no-new-console-errors', passed: modeResults.every((m) => m.newConsoleErrors === 0), evidence: modeResults },
- { id: 'locate-exercised', passed: ['locate-clicked', 'no-locate-target'].includes(locateResult), evidence: { locateResult, engineAfterLocate } },
- ]
- const failed = checks.filter((c) => !c.passed)
- const shot = await page.screenshot({ format: 'png' })
- await writeFile(path.join(OUT_DIR, 'final-state.png'), Buffer.from(shot, 'base64'))
- const report = {
- startedAt: new Date().toISOString(),
- url: TARGET_URL,
- engineState, mapGrade, idleStats, postStats, modeResults, locateResult,
- consoleErrorTotal: consoleErrors.length,
- pageErrorTotal: pageErrors.length,
- checks,
- summary: { passed: checks.length - failed.length, failed: failed.length },
- scope: 'CDP headless probe on dev server (working tree == commit 850c6c6 for both changed files); no browser-session plugin use; no build.',
- }
- await writeFile(path.join(OUT_DIR, 'acceptance-report.json'), JSON.stringify(report, null, 2))
- console.log(JSON.stringify(report.summary))
- for (const c of failed) console.log('FAIL ' + c.id + ' ' + JSON.stringify(c.evidence).slice(0, 300))
- await browser.close()
- process.exitCode = failed.length ? 1 : 0
- }
- main().catch((e) => { console.error('PROBE-ERROR', e); process.exit(2) })
|