| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200 |
- /**
- * 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)
|