Files
MailUI4Agents/client/electron/test/manual/scroll-cut-verify.mjs
JianFeeeee f6feb7c0df test(webui): 「滑动硬截断」诊断脚本(自带浏览器,不依赖共享实例)
用户:「webui 大片界面存在滑动硬截断」。

## 现状:这一轮我**没能复现**

量法是"内容溢出但没有可滚动祖先"⇒ 候选 0 个;换成用户视角四问
(有没有可滚动容器 / 能不能滚到底 / 末尾有没有被切 / 页面自己溢不溢出)后:

    390×700  通信  日历  联系人  我的 
    1400×700 通信  日历  联系人  我的 
    (造了 12 个会话、正文都很长;验完已归档)

即按这四种量法都测不到硬截断。**长正文详情页**那一块我的探针超时没跑完 ⇒
**未判定**,不能算"没问题"。

## 这次的交付物

`client/electron/test/manual/scroll-cut-verify.mjs`:把上面这套量法固化下来,
并且**自带浏览器**(不连共享 9222)—— 共享实例被历次被中断的运行留下僵尸上下文后,
`connectOverCDP` 会一直超时(我这个会话里已经遇到三次),诊断脚本必须在
"共享实例状态未知"时也能跑。

## 我需要的(否则只能继续猜)

请指一下**具体页面 + 具体手势/位置**:哪一屏、滚轮还是触摸拖动、硬截断出现在
列表末尾还是详情正文?也可以直接看截图。我上一轮"窄屏显示不全"也是同样情况 ——
我量不出来、但你说有,那多半是我量的维度不对。
2026-09-14 13:43:11 +08:00

101 lines
4.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 「滑动硬截断」诊断2026-09-14 用户「webui 大片界面存在滑动硬截断」)。
*
* 我当时的判据("内容溢出但没有可滚动祖先")量到 0 个候选 —— 说明这个量法
* 抓不到用户看到的现象。所以换成**用户视角**的四问:
* 1. 这个视图里有没有可滚动容器?
* 2. 能不能滚到底(滚到 scrollHeight
* 3. 末尾内容有没有被切掉(最后子元素底边 vs 容器底边)?
* 4. 长正文详情页(最容易硬截断的那块)单独量一遍。
*
* 用法AGENTMAIL_DIST=<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;