/** * T36 真机走查:主场景区配置在浏览器里**真的**透到浮标与宿主读数上(票 36 验收第 4 条)。 * * 逻辑脚本(t36-primary-area.mjs)已把判定层测透,这里只测浏览器里才成立的部分: * · 合法配置 → 浮标出现「主场景区:N×M km」、`data-map-primary-area=ready`; * · 没配 → `missing`,浮标写明「未配置 · 限位与雾气关闭」; * · 顺序写反(甲方原样的 [W,N,E,S])→ `invalid`,浮标带顺序原因与「不自动翻转」,**且没有矩形**; * · 数值个数不对 → `invalid` 且原因是形状问题(与顺序问题分得开); * · 引擎被配置关闭(`map.cesium.enabled=false`)时,主场景区读数**照样如实**、 * 浮标上也照样读得到 —— 它是配置事实,不该因为引擎没起来就消失 * (逻辑闸门用最小 DOM 宿主已抓到过这条:修复前这句在浮标上是失声的)。 * * 按 AGENTS.md:**本脚本不在改代码后自动运行**,由维护者明确要求时才跑(需先起 `yarn dev`)。 * 运行:node .scratch/cockpit-shell-panel-layout/verify/t36-primary-area-walkthrough.mjs */ 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' /** 本部署的显式值([西, 南, 东, 北])。走查独立写死,不 import 被测模块(否则是自己跟自己比)。 */ const OK_BOUNDS = [110.328749941417, 28.3917230659234, 110.465779226663, 28.4913038992907] /** 甲方原样的顺序([W, N, E, S])—— 运行时必须判非法,不得被悄悄翻转。 */ const SWAPPED_BOUNDS = [110.328749941417, 28.4913038992907, 110.465779226663, 28.3917230659234] 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-t36-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 openPage(injectConfig, waitFor = ['ready', 'failed', 'disabled']) { const page = await browser.newPage() await page.evaluateOnNewDocument(injectConfig) await page.goto(ENTRY_URL, { waitUntil: 'networkidle2', timeout: 60000 }) await page.waitForSelector('.cockpit-shell', { timeout: 20000 }) await page .waitForFunction( (states) => states.includes(document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine), { timeout: 60000 }, waitFor, ) .catch(() => {}) await sleep(1200) const state = await page.evaluate(() => { const host = document.querySelector('.cockpit-shell__map .map-canvas') return { engine: (host && host.dataset.mapEngine) || '', area: (host && host.dataset.mapPrimaryArea) || '', reason: (host && host.dataset.mapPrimaryAreaReason) || '', pill: host?.querySelector('[data-map-engine-status]')?.textContent?.trim() || '', plan: window.__cockpitMap ? window.__cockpitMap.getPrimaryAreaPlan() : null, } }) return { page, state } } try { // ---------- A1 合法:生效读得到 ---------- { const { page, state } = await openPage(() => { window.__COCKPIT_CONFIG__ = { map: { intro: { enabled: false }, primaryArea: { bounds: [110.328749941417, 28.3917230659234, 110.465779226663, 28.4913038992907] } }, } }) check( 'A1-ready-visible', '合法配置:宿主 data-map-primary-area=ready、浮标出现「主场景区:N×M km」、出口给出矩形', state.area === 'ready' && /主场景区:\d+\.\d×\d+\.\d km/.test(state.pill) && !!state.plan && state.plan.enabled === true && Array.isArray(state.plan.bounds) && state.plan.bounds.length === 4, JSON.stringify({ area: state.area, reason: state.reason, pill: state.pill.slice(0, 80), plan: state.plan }), ) await page.close() } // ---------- A2 未配置:关闭 + 如实提示 ---------- { const { page, state } = await openPage(() => { window.__COCKPIT_CONFIG__ = { map: { intro: { enabled: false } } } }) check( 'A2-missing-visible', '没配 primaryArea:data-map-primary-area=missing,浮标写明「未配置 · 限位与雾气关闭」,出口不给矩形', state.area === 'missing' && state.reason.includes('未配置') && state.pill.includes('限位与雾气关闭') && !!state.plan && state.plan.enabled === false && state.plan.bounds === null, JSON.stringify({ area: state.area, reason: state.reason, pill: state.pill.slice(0, 80) }), ) await page.close() } // ---------- A3 顺序写反:非法 + 不自动翻转(本票最容易做错的一条) ---------- { const { page, state } = await openPage(() => { window.__COCKPIT_CONFIG__ = { map: { intro: { enabled: false }, primaryArea: { bounds: [110.328749941417, 28.4913038992907, 110.465779226663, 28.3917230659234] }, }, } }) check( 'A3-swapped-rejected', '甲方原样的 [W,N,E,S] 被判非法:data-map-primary-area=invalid、浮标点明顺序与「不自动翻转」、出口**不给矩形**', state.area === 'invalid' && state.reason.includes('不自动翻转') && state.pill.includes('配置无效') && state.pill.includes('限位与雾气关闭') && !!state.plan && state.plan.enabled === false && state.plan.bounds === null, JSON.stringify({ area: state.area, reason: state.reason, pill: state.pill.slice(0, 90), bounds: state.plan && state.plan.bounds }), ) await page.close() } // ---------- A4 个数不对:非法,且原因与顺序问题分得开 ---------- { const { page, state } = await openPage(() => { window.__COCKPIT_CONFIG__ = { map: { intro: { enabled: false }, primaryArea: { bounds: [110, 28, 111] } } } }) check( 'A4-shape-rejected', '三个数字被判非法:原因说「四个有限数字」,与顺序问题的措辞不同', state.area === 'invalid' && state.reason.includes('四个有限数字') && state.reason.includes('顺序') === false && state.pill.includes('限位与雾气关闭'), JSON.stringify({ area: state.area, reason: state.reason, pill: state.pill.slice(0, 90) }), ) await page.close() } // ---------- A5 关引擎:主场景区读数照样如实 ---------- { const { page, state } = await openPage( () => { window.__COCKPIT_CONFIG__ = { map: { cesium: { enabled: false }, intro: { enabled: false }, primaryArea: { bounds: [110.328749941417, 28.3917230659234, 110.465779226663, 28.4913038992907] }, }, } }, ['disabled'], ) check( 'A5-visible-even-with-engine-off', 'map.cesium.enabled=false:引擎不启动,但主场景区仍如实标 ready,**且浮标上照样读得到**(原生实现里这条是失声的)', state.engine === 'disabled' && state.area === 'ready' && state.pill.includes('主场景区:') && !!state.plan && state.plan.enabled === true, JSON.stringify({ engine: state.engine, area: state.area, reason: state.reason, pill: state.pill.slice(0, 90) }), ) await page.close() } } finally { await browser.close() } const failed = results.filter((r) => !r.pass) console.log('') console.log(`T36 走查:${results.length - failed.length}/${results.length} 项通过。`) if (failed.length) { console.log('未通过:' + failed.map((f) => f.id).join(', ')) process.exit(1) } console.log('T36 走查全部通过。')