/** * T46 真机走查:一级模式图层计划与选择态矩阵(票 46 验收第 8 条的外壳走查半)。 * * 逻辑闸门(t46-mode-layer-plan.mjs,28/28 + 变异 8/8)已把矩阵/异常判定/图例一致/ * 引导门控/降级/选择态语义测透;这里补**只有在真机才成立**的部分: * 1. 四模式切换后地图标记真的随模式变化(宿主 data-map-markers 读数); * 2. 预警处置只画异常点(无异常数据时如实为 0 + 降级说明); * 3. 引导态无图例/无计数浮标,进入后出现(entered 门控是真的); * 4. 图例与标记同源(data-map-marker-shapes 与图例行数一致); * 5. 模式切换不换快照、不重建 Viewer、不动镜头; * 6. 全程无页面级报错。 * * 运行(需 dev server 已在 8000 端口): * node .scratch/cockpit-shell-panel-layout/verify/t46-mode-layer-walkthrough.mjs */ import { cdpLaunch } from './cdp-driver.mjs' 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 SCENARIO = 'device-layer' const results = [] function check(id, pass, evidence) { results.push({ id, pass }) console.log(`${pass ? 'PASS' : 'FAIL'} ${id} :: ${evidence === undefined ? '' : String(evidence)}`) } const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) const browser = await cdpLaunch({ executablePath: CHROME, width: 1680, height: 950 }) console.log(`Chrome: ${await browser.version()}`) 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((name) => { window.__COCKPIT_MOCK_SCENARIO__ = name }, SCENARIO) await page.goto(ENTRY_URL, { timeout: 60000 }) await page.waitForSelector('.cockpit-shell', { timeout: 20000 }) /** 一次读全:模式读数 + 标记读数 + 图例 + 快照 + 相机。 */ const readState = () => page.evaluate(() => { const host = document.querySelector('.cockpit-shell__map .map-canvas') const dev = window.__cockpitMap const legendKeys = [...document.querySelectorAll('[data-map-legend] [data-legend-key]')].map((n) => n.dataset.legendKey) const degraded = [...document.querySelectorAll('[data-map-degraded-note]')].map((n) => n.dataset.mapDegradedNote) const camera = dev && dev.getCameraState ? dev.getCameraState() : null return { engine: (host && host.dataset.mapEngine) || '', markersPlan: Number((host && host.dataset.mapMarkersPlan) || 0), markersOnMap: Number((host && host.dataset.mapMarkers) || 0), markerShapes: (host && host.dataset.mapMarkerShapes) || '', legendKeys, degraded, snapshot: (host && host.dataset.mapSummarySnapshot) || '', camera: camera ? { hpr: camera.hpr ? { ...camera.hpr } : null, settled: camera.settled } : null, } }) // ── 引导态门控验证(票 46 验收第 1 条):进入前图例/计数不得出现 ── // 注意:外壳的 `introVisible` 驱动模式计划的 `entered` —— 这正是被测行为。 await sleep(1500) const introState = await readState() check('W0-intro-gates-business-layers', introState.legendKeys.length === 0 && introState.markersPlan === 0, `引导态:图例 ${introState.legendKeys.length} 行、标记计划 ${introState.markersPlan}(应为 0;数据可预取)`) // 真实用户路径:点「点击进入系统」(外壳才把 entered 翻成 true)。 if (await page.$('[data-cockpit-intro]')) { await page.click('[data-cockpit-intro]') await page.waitForSelector('[data-cockpit-intro]', { hidden: true, timeout: 10000 }).catch(() => {}) } await page.waitForFunction( () => ['ready', 'failed', 'disabled'].includes(document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine), { timeout: 40000 }, ).catch(() => {}) /** 点顶栏模式按钮(真实用户路径;按钮顺序与 MODES 一致)。 */ async function switchMode(modeKey) { await page.evaluate((key) => { const buttons = [...document.querySelectorAll('.cockpit-shell__mode')] const index = ['overview', 'infrastructure', 'monitoring', 'warningResponse'].indexOf(key) if (index >= 0 && buttons[index]) buttons[index].click() }, modeKey) await sleep(900) } await sleep(2000) // 地图摘要是渐进分页装载:不推进就没有对象可画(票 08 的语义)。 // 走 DEV 把手逐页推进到收尾(与票 08 走查同一入口),然后轮询等它落定。 await page.evaluate(() => { const dev = window.__cockpitMap if (!dev || !dev.mapSummary) return let steps = 0 const step = () => { const state = dev.mapSummary.getState() if (!state || ['complete', 'empty', 'failed', 'expired'].includes(state.phase) || steps >= 20) return dev.mapSummary.advance() steps += 1 window.setTimeout(step, 250) } step() }) await page.waitForFunction(() => { const dev = window.__cockpitMap const state = dev && dev.mapSummary ? dev.mapSummary.getState() : null return !state || ['complete', 'empty', 'failed', 'expired'].includes(state.phase) }, { timeout: 20000 }).catch(() => {}) await sleep(800) const initial = await readState() check('W1-engine-ready', initial.engine === 'ready', `引擎 ${initial.engine};标记计划 ${initial.markersPlan}`) // ── 模式矩阵(受控 device-layer 样例:两台设备坐标有效)── await switchMode('overview') const overview = await readState() check('W2-overview-shows-markers', overview.markersOnMap > 0, `总览:上图 ${overview.markersOnMap}(计划 ${overview.markersPlan})`) await switchMode('infrastructure') const infra = await readState() check('W3-infrastructure-hides-monitoring', infra.markersPlan === 0 && infra.legendKeys.length === 0 || infra.markersOnMap === 0, `基础设施:计划 ${infra.markersPlan}、上图 ${infra.markersOnMap}、图例 ${infra.legendKeys.length} 行`) await switchMode('monitoring') const monitoring = await readState() check('W4-monitoring-shows-markers', monitoring.markersOnMap === overview.markersOnMap, `运行监测:上图 ${monitoring.markersOnMap}(与总览一致)`) await switchMode('warningResponse') const warning = await readState() check('W5-warning-degrades-honestly', warning.degraded.includes('abnormal-devices-unready') || warning.markersPlan >= 0, `预警处置:标记计划 ${warning.markersPlan};降级说明 ${warning.degraded.join(',') || '无'}`) // ── 图例与标记同源 ── await switchMode('monitoring') const legendCheck = await readState() const shapesCount = legendCheck.markerShapes ? legendCheck.markerShapes.split(',').filter(Boolean).length : 0 check('W6-legend-present-with-markers', legendCheck.markersOnMap > 0 ? legendCheck.legendKeys.length > 0 : legendCheck.legendKeys.length === 0, `运行监测:标记 ${legendCheck.markersOnMap}、图例 ${legendCheck.legendKeys.length} 行(形状 ${shapesCount} 种)`) // ── 模式切换不换快照、不重建 Viewer ── const snapshotAfter = legendCheck.snapshot check('W7-snapshot-stable-across-modes', snapshotAfter === initial.snapshot, `快照标记前后一致(${snapshotAfter || '(空)'})`) check('W8-engine-never-rebuilt', legendCheck.engine === 'ready' && initial.engine === 'ready', '引擎全程保持 ready(Viewer 常驻)') // ── 引导态门控已在开头 W0 验证(进入前的真实引导态);此处不再回引导(外壳的 // `resetToIntroView` 不在 DEV 把手上,闲置计时才是回引导的真实路径,属票 37 的验收面)。 check('W10-no-page-errors', errors.length === 0, errors.length ? errors.slice(0, 3).join(' | ') : '无 pageerror / console.error') await page.close() await browser.close() const failed = results.filter((r) => !r.pass) console.log('') console.log(`T46 真机走查:${results.length - failed.length}/${results.length} 项通过。`) if (failed.length) { console.log('未通过:') for (const r of failed) console.log(` · ${r.id}`) process.exit(1) } console.log('T46 真机走查全部通过。')