| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207 |
- /**
- * T33 真机走查:一张图浮层布局(维护者 2026-09-18 的三条要求)。
- *
- * ① 点位状态图例放到**一张图最上方**(原来在底部,位置效果不好);
- * ② 地图摘要**最小化形态**(去掉「地图摘要」文字标题,收成状态点 + 短文案 + 动作的小胶囊);
- * ③ 浮层之间**不能互相叠字** —— 顺带修掉票 32 引入的「生产形态下引擎状态与底部摘要行重叠」。
- *
- * 断言方式是**真实几何**(getBoundingClientRect)而不是"看截图":图例是否贴顶、三块浮层是否相交、
- * 摘要块宽度是否收窄、动作按钮与选择器是否还在(票 07/08/09/10 的走查依赖它们)。
- *
- * 运行(需先起 `yarn dev`):node .scratch/cockpit-shell-panel-layout/verify/t33-overlay-layout-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-t33-layout.png')
- 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))
- /** 两个矩形是否相交(留 0 容差:边缘相接不算叠)。 */
- function overlaps(a, b) {
- if (!a || !b) return false
- return a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h
- }
- const puppeteer = (await import(PUPPETEER)).default
- const browser = await puppeteer.launch({
- executablePath: CHROME,
- headless: true,
- userDataDir: join(tmpdir(), 'cockpit-t33-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 openWith(uiDebug, shot) {
- const page = await browser.newPage()
- const errors = []
- page.on('pageerror', (e) => errors.push(String(e && e.message ? e.message : e)))
- await page.evaluateOnNewDocument(
- (cfg, d) => {
- window.__COCKPIT_CONFIG__ = { ...cfg, map: { ...cfg.map, intro: { enabled: false }, ui: { debug: d } } }
- },
- config,
- uiDebug,
- )
- 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 sleep(2500)
- if (shot) await page.screenshot({ path: shot })
- const m = await page.evaluate(() => {
- const r = (sel) => {
- const el = document.querySelector(sel)
- if (!el) return null
- const b = el.getBoundingClientRect()
- return { x: Math.round(b.x), y: Math.round(b.y), w: Math.round(b.width), h: Math.round(b.height) }
- }
- const panel = document.querySelector('[data-map-blocked-panel]')
- const list = document.querySelector('[data-map-blocked-list]')
- const readText = (sel) => {
- const el = panel ? panel.querySelector(sel) : null
- return el ? el.textContent.trim() : ''
- }
- return {
- map: r('.cockpit-shell__map .map-canvas'),
- rightCol: r('.cockpit-shell__column.is-right'),
- legend: r('[data-map-legend]'),
- statusLine: r('.map-holo__status'),
- pill: r('[data-map-engine-status]'),
- headline: r('.map-holo__headline'),
- summary: r('.cockpit-shell__map-summary'),
- summaryDot: r('.cockpit-shell__map-summary-dot'),
- blockedPanel: r('[data-map-blocked-panel]'),
- blockedCollapsed: panel ? panel.classList.contains('is-compact') : null,
- blockedListPresent: !!list,
- // 票 07 的断言面:收起形态下这两个文本也必须仍在(DOM 常驻,只是视觉收起)
- backendText: readText('[data-map-coordinate-backend]'),
- loadedText: readText('[data-map-coordinate-loaded]'),
- hasSummaryLabelText: !!Array.from(document.querySelectorAll('.cockpit-shell__scope-right .cockpit-shell__scope-label'))
- .find((el) => el.textContent.trim() === '地图摘要'),
- summaryAction: !!document.querySelector('[data-map-summary-action]'),
- summaryLabel: !!document.querySelector('[data-map-summary-label]'),
- legendTags: document.querySelectorAll('[data-map-legend] .status-tag').length,
- uiMode: (document.querySelector('[data-map-engine-status]') || {}).dataset?.uiMode || '',
- }
- })
- return { page, m, errors }
- }
- try {
- for (const uiDebug of [false, true]) {
- const tag = uiDebug ? 'debug' : 'prod'
- const { page, m, errors } = await openWith(uiDebug, uiDebug ? null : OUT)
- // ① 图例贴顶
- const legendTopOffset = m.legend && m.map ? m.legend.y - m.map.y : null
- check(
- `A1-${tag}-legend-at-top`,
- '点位状态图例在**一张图最上方**(距画布顶 ≤ 24px),且图例本身仍在(颜色语义)',
- legendTopOffset !== null && legendTopOffset >= 0 && legendTopOffset <= 24 && m.legendTags > 0,
- JSON.stringify({ legendTopOffset, tags: m.legendTags, mode: m.uiMode }),
- )
- // ③ 浮层不叠字
- const legendVsStatus = overlaps(m.legend, m.statusLine)
- const pillVsStatus = overlaps(m.pill, m.statusLine)
- const pillVsLegend = overlaps(m.pill, m.legend)
- const pillVsHeadline = overlaps(m.pill, m.headline)
- check(
- `A2-${tag}-no-overlap`,
- '图例 / 引擎状态 / 底部摘要行三者互不重叠(票 32 引入的叠字已修)',
- !legendVsStatus && !pillVsStatus && !pillVsLegend && !pillVsHeadline,
- JSON.stringify({ legendVsStatus, pillVsStatus, pillVsLegend, pillVsHeadline, legend: m.legend, pill: m.pill, statusLine: m.statusLine }),
- )
- // ② 摘要最小化
- check(
- `A3-${tag}-summary-minimized`,
- '地图摘要收成小胶囊:去掉「地图摘要」文字标题、带状态点、宽度收窄,但动作按钮与状态文案选择器仍在',
- m.hasSummaryLabelText === false &&
- !!m.summaryDot &&
- m.summary &&
- m.summary.w <= 300 &&
- m.summaryAction &&
- m.summaryLabel,
- JSON.stringify({ labelText: m.hasSummaryLabelText, dot: !!m.summaryDot, w: m.summary ? m.summary.w : null, action: m.summaryAction, label: m.summaryLabel }),
- )
- // 票 35 续:两块摘要都贴一张图**右下角**(让开右栏),且竖排不叠
- const rightCol = m.rightCol
- const gapOf = (r) => (r && rightCol ? Math.round(rightCol.x - r.x - r.w) : null)
- const blockedGap = gapOf(m.blockedPanel)
- const pillGap = gapOf(m.pill)
- // 胶囊在**下半屏**、且整体在状态日志之上(不叠)
- const blockedLow = m.map && m.blockedPanel ? m.blockedPanel.y > m.map.y + m.map.h * 0.5 : false
- const stackedAbovePill = m.blockedPanel && m.pill ? m.blockedPanel.y + m.blockedPanel.h <= m.pill.y : false
- // 生产形态:状态日志也贴右下角、并在胶囊下方;调试形态:药丸是顶部诊断读数(t32 A7 另测),不参与右下角。
- const pillOk = uiDebug ? true : pillGap !== null && pillGap >= 4 && pillGap <= 60
- check(
- `A7-${tag}-bottom-right-stack`,
- '「无地图位置」胶囊贴**右下角**(让开右栏、在下半屏),生产形态下引擎状态日志同为右下角且在胶囊下方、互不叠',
- blockedGap !== null && blockedGap >= 4 && blockedGap <= 60 && blockedLow && pillOk && !overlaps(m.blockedPanel, m.pill) && (uiDebug || stackedAbovePill),
- JSON.stringify({ blockedGap, pillGap, blockedLow, stackedAbovePill, blocked: m.blockedPanel, pill: m.pill, rightColX: rightCol ? rightCol.x : null }),
- )
- check(`A4-${tag}-no-errors`, `${tag} 形态无页面级报错`, errors.length === 0, JSON.stringify(errors.slice(0, 2)))
- // 票 35:无地图位置浮块必须是「收起的一行小胶囊」,不能再占中央视野
- check(
- `A5-${tag}-blocked-panel-compact`,
- '「无地图位置」默认收起成一行小胶囊(高度 ≤ 32px、宽度 ≤ 300px),且两条口径文本仍在 DOM 里(票 07 断言面不变)',
- m.blockedCollapsed === true &&
- m.blockedPanel &&
- m.blockedPanel.h <= 32 &&
- m.blockedPanel.w <= 300 &&
- m.backendText.length > 0 &&
- m.loadedText.length > 0,
- JSON.stringify({ collapsed: m.blockedCollapsed, rect: m.blockedPanel, backend: m.backendText.slice(0, 24), loaded: m.loadedText.slice(0, 24) }),
- )
- // 展开后仍应比旧版窄(宽度上限 280,不再是 340)
- await page.click('[data-map-blocked-toggle]')
- await sleep(500)
- const expanded = await page.evaluate(() => {
- const el = document.querySelector('[data-map-blocked-panel]')
- const b = el.getBoundingClientRect()
- return { w: Math.round(b.width), h: Math.round(b.height), list: !!document.querySelector('[data-map-blocked-list]') }
- })
- check(
- `A6-${tag}-blocked-panel-expanded-narrow`,
- '展开后仍比旧版收窄(宽 ≤ 300px)且清单出现',
- expanded.w <= 300 && expanded.list === true,
- JSON.stringify(expanded),
- )
- await page.click('[data-map-blocked-toggle]')
- await page.close()
- }
- } finally {
- await browser.close()
- }
- const failed = results.filter((r) => !r.pass)
- console.log('')
- console.log(`T33 走查:${results.length - failed.length}/${results.length} 项通过。截图:${OUT}`)
- if (failed.length) {
- console.log('未通过:' + failed.map((f) => f.id).join(', '))
- process.exit(1)
- }
- console.log('T33 走查全部通过。')
|