/** * T28 检查:外壳填满**所在容器**,而不是按 100vh 铺。 * * 背景(票 28):外壳不能只认视口高度。真实启动路径是 `yarn dev`(`.fes.js` → * `layout.navigation: null`,页面无页头,外壳贴初始包含块=铺满视口);但 `/cockpit` 一旦被 * 别的前端壳包住(例如将来 qiankun 主应用),容器的定位盒会比视口矮,`height: 100vh` * 就会把外壳顶出容器底部:底部被裁、滚动容器还多一条纵向滚动条。 * 维护者已明确:**真实应用环境不使用 predev 启动服务**,所以这里不依赖 predev 服务。 * * 做法:在当前真实页面里把 `#tvt-app` 临时改成「有定位 + top:54px + 高度 = 视口 - 54px + * 滚动容器」,复刻「宿主套壳」那条几何链路(预演站里实测的定位容器是 * `section.fes-layout.is-fixed`:absolute、top 54px、高 896),再对比旧实现与交付实现。 * * 运行(需先起 `yarn dev`,服务在 http://localhost:8000/): * node .scratch/cockpit-shell-panel-layout/verify/t28-fill-container.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' const results = [] function check(id, desc, pass, evidence) { results.push({ id, desc, pass }) console.log(`${pass ? 'PASS' : 'FAIL'} ${id} ${desc}${evidence ? ` — ${evidence}` : ''}`) } const puppeteer = (await import(PUPPETEER)).default const browser = await puppeteer.launch({ executablePath: CHROME, headless: true, userDataDir: join(tmpdir(), 'cockpit-t28-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()}`) try { const page = await browser.newPage() const pageErrors = [] page.on('pageerror', (e) => pageErrors.push(String(e && e.message ? e.message : e))) await page.goto(ENTRY_URL, { waitUntil: 'networkidle2', timeout: 60000 }) await page.waitForSelector('.cockpit-shell', { timeout: 20000 }) await new Promise((r) => setTimeout(r, 800)) const geom = () => page.evaluate(() => { const round = (n) => Math.round(n) const box = (el) => { if (!el) return null const b = el.getBoundingClientRect() return { y: round(b.y), h: round(b.height), bottom: round(b.bottom), w: round(b.width) } } const shell = document.querySelector('.cockpit-shell') const style = shell ? getComputedStyle(shell) : null // 外壳的定位容器:有定位祖先就是它,否则是初始包含块(=视口) const cb = shell && shell.offsetParent && shell.offsetParent !== document.body ? shell.offsetParent : null // 链上有没有滚动条(含 html) const scrollables = [] for (let el = shell; el; el = el.parentElement) { if (el.scrollHeight > el.clientHeight + 2) { scrollables.push(`${el.tagName.toLowerCase()}.${String(el.className || '').split(/\s+/)[0]}`) } } return { vh: window.innerHeight, scrollH: document.documentElement.scrollHeight, docScrollable: document.scrollingElement.scrollHeight > window.innerHeight + 2, scrollables, shell: box(shell), shellPosition: style ? style.position : '', containerBlock: cb ? { ...box(cb), cls: String(cb.className || cb.id) } : 'initial-containing-block', atmosphere: box(document.querySelector('.cockpit-shell__atmosphere')), main: box(document.querySelector('.cockpit-shell__main')), map: box(document.querySelector('.cockpit-shell__map')), } }) // 把宿主改成「顶部有 headerPx 页头」:定位祖先 + 高的滚动容器(= predev 里 is-fixed 的角色)。 const applyHostChrome = (headerPx) => page.evaluate((px) => { const root = document.querySelector('#tvt-app') root.style.position = 'relative' root.style.top = `${px}px` root.style.height = `calc(100vh - ${px}px)` root.style.overflow = 'auto' }, headerPx) // 旧实现(票 28 之前):外壳是普通流里的 100vh 块。 const applyOldShell = (on) => page.evaluate((enable) => { const shell = document.querySelector('.cockpit-shell') shell.style.position = enable ? 'static' : '' shell.style.inset = enable ? 'auto' : '' shell.style.height = enable ? '100vh' : '' }, on) // ---------- A 真实宿主(本仓库默认布局:无页头) ---------- const a = await geom() check( 'A-real-host', '真实宿主(无页头):外壳仍铺满视口,中央一张图铺满主体,页面无纵向滚动', !!a.shell && Math.abs(a.shell.h - a.vh) <= 2 && a.shell.y === 0 && a.shell.bottom === a.vh && a.scrollH <= a.vh + 2 && !a.docScrollable && a.scrollables.length === 0 && a.map.h >= Math.round(a.main.h * 0.95), JSON.stringify(a), ) // ---------- B1 带页头宿主 + 旧实现:复现 bug ---------- await applyHostChrome(54) await applyOldShell(true) await new Promise((r) => setTimeout(r, 300)) const b1 = await geom() check( 'B1-reproduce-old', '带 54px 页头的宿主 + 旧实现(height:100vh):外壳越过视口底 54px,宿主容器多出 54px 纵向滚动', !!b1.shell && b1.shell.bottom > b1.vh + 2 && b1.scrollables.length > 0, JSON.stringify({ shell: b1.shell, vh: b1.vh, scrollables: b1.scrollables }), ) // ---------- B2 同一宿主 + 交付实现:修复 ---------- await applyOldShell(false) await new Promise((r) => setTimeout(r, 400)) const b2 = await geom() check( 'B2-fixed', '带 54px 页头的宿主 + 交付实现(absolute + inset:0):外壳高 = 容器高、底边对齐视口底、无纵向滚动', !!b2.shell && typeof b2.containerBlock === 'object' && Math.abs(b2.shell.h - b2.containerBlock.h) <= 2 && Math.abs(b2.shell.bottom - b2.vh) <= 2 && b2.main.bottom <= b2.vh && b2.scrollables.length === 0 && b2.map.h >= Math.round(b2.main.h * 0.95), JSON.stringify({ shell: b2.shell, container: b2.containerBlock, vh: b2.vh, main: b2.main, scrollables: b2.scrollables, }), ) // ---------- B3 氛围层:只铺自己的范围,不盖到宿主页头那一带 ---------- check( 'B3-atmosphere-scoped', '氛围底不越界:随外壳自己的范围铺(y 与高都等于外壳),宿主页头那一带不归驾驶舱画', !!b2.atmosphere && !!b2.shell && b2.atmosphere.y === b2.shell.y && b2.atmosphere.h === b2.shell.h, JSON.stringify({ atmosphere: b2.atmosphere, shell: b2.shell }), ) // ---------- B4 容器再变矮:外壳跟着缩(证明不是挂载时量一次) ---------- await applyHostChrome(120) await new Promise((r) => setTimeout(r, 400)) const b4 = await geom() check( 'B4-follows-container', '容器再变矮 66px:外壳与地图跟着缩,仍无纵向滚动(CSS 跟随,不是一次性测量)', !!b4.shell && Math.abs(b4.shell.h - (b4.vh - 120)) <= 2 && Math.abs(b4.shell.bottom - b4.vh) <= 2 && b4.scrollables.length === 0, JSON.stringify({ shell: b4.shell, vh: b4.vh, scrollables: b4.scrollables }), ) check('A11-no-page-errors', '走查期间无页面级报错', pageErrors.length === 0, JSON.stringify(pageErrors.slice(0, 3))) // ---------- C 真·视口 resize:外壳跟着新视口走(不是挂载时量一次) ---------- await applyHostChrome(54) await page.setViewport({ width: 1280, height: 720 }) await new Promise((r) => setTimeout(r, 600)) const c = await geom() check( 'C-real-viewport-resize', '把视口从 1680x950 改成 1280x720(带 54px 页头):外壳跟着变矮、底边仍对齐视口底、无纵向滚动', !!c.shell && c.vh === 720 && Math.abs(c.shell.h - (720 - 54)) <= 2 && Math.abs(c.shell.bottom - 720) <= 2 && c.main.bottom <= 720 && c.scrollables.length === 0, JSON.stringify({ shell: c.shell, vh: c.vh, scrollables: c.scrollables }), ) } finally { await browser.close() } const failed = results.filter((r) => !r.pass) console.log( `\nT28:${results.length - failed.length}/${results.length} 项通过${failed.length ? `,未通过:${failed.map((r) => r.id).join(', ')}` : ''}`, ) process.exit(failed.length ? 1 : 0)