/** * 「滑动硬截断」诊断(2026-09-14 用户:「webui 大片界面存在滑动硬截断」)。 * * 我当时的判据("内容溢出但没有可滚动祖先")量到 0 个候选 —— 说明这个量法 * 抓不到用户看到的现象。所以换成**用户视角**的四问: * 1. 这个视图里有没有可滚动容器? * 2. 能不能滚到底(滚到 scrollHeight)? * 3. 末尾内容有没有被切掉(最后子元素底边 vs 容器底边)? * 4. 长正文详情页(最容易硬截断的那块)单独量一遍。 * * 用法:AGENTMAIL_DIST= ADMIN_USER=.. ADMIN_PW=.. node test/manual/scroll-cut-verify.mjs * 前置:账号里要有**足够长的内容**,否则量不到溢出(我第一次就栽在这里: * 收件箱只有 1 封 ⇒ 没有溢出 ⇒ "0 候选"被误当成"没有问题")。 */ import { chromium } from '/usr/lib/node_modules/playwright/index.mjs'; // ★ 自带浏览器,**不连共享的 9222**。 // 共享实例被历次被中断的运行留下僵尸上下文后,connectOverCDP 会一直超时 // (我这个会话里已经遇到三次)。诊断脚本必须在"共享实例状态未知"时也能跑。 const APP = process.env.AGENTMAIL_APP || 'http://127.0.0.1:8180'; const VIEWS = [ ['通信', null], ['日历', '日历'], ['联系人', '联系人'], ['我的', '我的'] ]; const results = []; const record = (n, ok, note) => { results.push({ n, ok }); console.log(` ${ok ? '通过' : '待查'} ${n}${note ? ' — ' + note : ''}`); }; const probe = page => page.evaluate(() => { const scrollers = [...document.querySelectorAll('*')].filter(e => { const oy = getComputedStyle(e).overflowY; return (oy === 'auto' || oy === 'scroll') && e.scrollHeight > e.clientHeight + 4; }); scrollers.sort((a, b) => b.scrollHeight - b.clientHeight - (a.scrollHeight - a.clientHeight)); const s = scrollers[0]; if (!s) return { scroller: null, viewportOverflow: document.documentElement.scrollHeight - document.documentElement.clientHeight }; s.scrollTop = s.scrollHeight; const rect = s.getBoundingClientRect(); const last = s.lastElementChild?.getBoundingClientRect(); return { cls: s.className.toString().slice(0, 40), range: s.scrollHeight - s.clientHeight, reachedEnd: Math.abs(s.scrollTop + s.clientHeight - s.scrollHeight) < 3, lastChildCutBy: last ? Math.round(last.bottom - rect.bottom) : null }; }); for (const vp of [{ width: 390, height: 700 }, { width: 1400, height: 700 }]) { const browser = await chromium.launch({ args: ['--no-sandbox'] }); const ctx = await browser.newContext({ viewport: vp }); const page = await ctx.newPage(); await page.goto(`${APP}/`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(2200); if ((await page.locator('input[autocomplete=username]').count()) > 0) { await page.fill('input[autocomplete=username]', process.env.ADMIN_USER || 'gui-lab'); await page.fill('input[type=password]', process.env.ADMIN_PW || 'gui123456'); await page.click('button:has-text("登录")'); await page.waitForTimeout(3200); } console.log(` ── ${vp.width}×${vp.height} ──`); try { for (const [label, nav] of VIEWS) { if (nav) { await page .locator(`.nav-rail button:has-text("${nav}"), .narrow-nav button:has-text("${nav}")`) .first() .click() .catch(() => {}); await page.waitForTimeout(1400); } const r = await probe(page); if (!r.scroller) { // 没有可滚动容器:要么内容本来就不溢出(正常),要么整页被裁(硬截断 → 记待查) const overflow = r.viewportOverflow ?? 0; record(`${label}:无可滚动容器时内容本来就不溢出`, overflow <= 0, `页面溢出 ${overflow}px`); } else { record( `${label}:能滚到底且末尾不被切`, r.reachedEnd && (r.lastChildCutBy ?? 0) <= 1, `${r.cls} 范围=${r.range} 到底=${r.reachedEnd} 末尾被切=${r.lastChildCutBy}px` ); } } } finally { await ctx.close(); await browser.close(); } } const bad = results.filter(r => !r.ok); console.log(`\n 滑动硬截断诊断:${results.length - bad.length}/${results.length} 通过`); if (bad.length) console.log(' 待查:' + bad.map(b => b.n).join('; ')); process.exitCode = bad.length ? 1 : 0;