用户:「webui 大片界面存在滑动硬截断」。
## 现状:这一轮我**没能复现**
量法是"内容溢出但没有可滚动祖先"⇒ 候选 0 个;换成用户视角四问
(有没有可滚动容器 / 能不能滚到底 / 末尾有没有被切 / 页面自己溢不溢出)后:
390×700 通信 ✅ 日历 ✅ 联系人 ✅ 我的 ✅
1400×700 通信 ✅ 日历 ✅ 联系人 ✅ 我的 ✅
(造了 12 个会话、正文都很长;验完已归档)
即按这四种量法都测不到硬截断。**长正文详情页**那一块我的探针超时没跑完 ⇒
**未判定**,不能算"没问题"。
## 这次的交付物
`client/electron/test/manual/scroll-cut-verify.mjs`:把上面这套量法固化下来,
并且**自带浏览器**(不连共享 9222)—— 共享实例被历次被中断的运行留下僵尸上下文后,
`connectOverCDP` 会一直超时(我这个会话里已经遇到三次),诊断脚本必须在
"共享实例状态未知"时也能跑。
## 我需要的(否则只能继续猜)
请指一下**具体页面 + 具体手势/位置**:哪一屏、滚轮还是触摸拖动、硬截断出现在
列表末尾还是详情正文?也可以直接看截图。我上一轮"窄屏显示不全"也是同样情况 ——
我量不出来、但你说有,那多半是我量的维度不对。
101 lines
4.4 KiB
JavaScript
101 lines
4.4 KiB
JavaScript
/**
|
||
* 「滑动硬截断」诊断(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;
|