Files
MailUI4Agents/client/electron/test/manual/wide-regression.mjs
JianFeeeee 7ef3ea7306 fix(webui): 宽屏也同步折叠策略 —— 头部与回复框默认收起(正文 48% → 90%)
用户(2026-09-14):「我发现宽屏布局也有邮件内容显示区域过小的问题,
宽屏也同步窄屏的折叠策略吧」。

上一版是「窄屏默认收起 / 宽屏恒展开」,理由是「宽屏横向空间够、不该引入多余
点击」。那条理由只看了**横向**:实测 1280x800 下详情栏里头部 139px(17%)、
回复框 246px(31%),留给正文的只剩 385px(48%)—— 竖向一样不够用。

改法:两种宽度用同一套默认值(收起),点标题行 / 悬浮球展开。
- CollapsibleHeader:useState(false),toggle 不再以 narrow 为条件,展开箭头常显;
- ReplyBar:收起态恒为悬浮球(原来只有窄屏才收),「收起」按钮宽窄都有,
  autoFocus 不再以 narrow 为条件;
- MailView 两个根节点补 relative —— 否则宽屏下 `absolute` 会一路找到视口,
  悬浮球会挂到窗口右下角而不是这块详情栏里。

实测(真浏览器量盒子,1280x800,同一封正文撑满的长信):
  头部 139 → 48px,回复框 246 → 0(收起为球),正文 385 → 722px(占视口 48% → 90%);
  点标题展开 / 点球开输入框(带焦点)/「收起」收回,三条都通。
窄屏 390x844 同项通过,横向溢出 0。

判据:narrow-layout 里三条旧判据钉的是「窄屏专属」,已改成钉「宽窄同一套」,
并新增两条(折叠分支里不得再有 `narrow &&`;悬浮球必须锚在详情栏内)。
wide-regression 补 6 条宽屏实测 —— 此前宽屏的折叠行为**一条判据都没有**,
这正是它坏掉而没人发现的原因。

已知遗留(另行报告,本次未改):窄屏上回复框展开后,第一次点它上面的按钮
(抄送 / 回复全部 / 收起)会被吞掉 —— 焦点离开 textarea 会让 .form-editing
失效、底部导航重新占位 71px,按钮在 mousedown 与 mouseup 之间从指针下移走,
click 目标退化成公共祖先。HEAD 上已存在,与本次改动无关。
2026-09-14 15:38:49 +08:00

133 lines
5.6 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.

/**
* 宽屏回归:窄屏修复不能把桌面布局改坏。
*
* 特别是两个只该在窄屏生效的东西:
* - `.tap` 的伪元素命中区(桌面密排工具栏里会互相重叠)
* - `BackButton`(宽屏列表与详情并排,返回没有意义)
*
* 用法ADMIN_PW=<密码> node client/electron/test/manual/wide-regression.mjs
*/
import { openApp, WIDE } from './narrow-probe-helper.mjs';
const { browser, page, issues } = await openApp(WIDE);
const failed = [];
const chk = (n, ok, note = '') => {
console.log(` ${ok ? '通过' : '失败'} ${n}${note ? ' — ' + note : ''}`);
if (!ok) failed.push(n);
};
console.log('宽屏回归1280px');
const cols = await page.evaluate(() => {
const root = document.querySelector('#root > div');
return { n: root?.children.length, first: root?.children[0]?.className?.toString().slice(0, 30) };
});
chk('仍是三栏并排', cols.n === 3, `栏数=${cols.n} 首栏=${cols.first}`);
// 常驻侧栏是宽屏唯一的退出入口(账号页也有,两处都要在)
const side = await page.evaluate(() => {
const s = document.querySelector('#root > div > div');
const btns = s
? [...s.querySelectorAll('button')].map(b =>
(b.getAttribute('title') || b.textContent || '').trim().slice(0, 12)
)
: [];
return { w: s ? Math.round(s.getBoundingClientRect().width) : 0, btns };
});
chk('常驻侧栏仍有退出登录', side.btns.some(b => b.includes('退出')), `宽=${side.w}`);
// 宽屏不该出现返回按钮
// 邮件行是 <button class="w-full text-left ...">
const rows = page.locator('button.w-full.text-left');
if ((await rows.count()) > 0) await rows.first().click();
await page.waitForTimeout(1000);
const backN = await page.locator('button[aria-label="返回"], button[aria-label="会话"]').count();
chk('没有返回按钮', backN === 0, `${backN}`);
// .tap 只在 max-width:767px 生效
const tapWide = await page.evaluate(() => {
const b = [...document.querySelectorAll('.tap')].find(x => x.getBoundingClientRect().height > 0);
if (!b) return null;
const cs = getComputedStyle(b, '::after');
return { content: cs.content, w: cs.width, h: cs.height };
});
chk(
'.tap 伪元素在宽屏不生效',
!tapWide || tapWide.content === 'none' || tapWide.w === 'auto',
JSON.stringify(tapWide)
);
const of = await page.evaluate(() => ({
d: document.documentElement.clientWidth,
s: document.documentElement.scrollWidth
}));
chk('无横向溢出', of.s <= of.d, `doc=${of.d} scroll=${of.s}`);
/*
* 折叠策略在**宽屏**同样生效2026-09-14 用户:「我发现宽屏布局也有邮件内容
* 显示区域过小的问题,宽屏也同步窄屏的折叠策略吧」)。
*
* 为什么必须量真实的盒子:上一版宽屏是「恒展开」,而头部 139px(17%) + 回复框
* 246px(31%) 把正文挤到只剩 385px(48%)。那时**没有任何判据会红** ——
* narrow-layout 只判窄屏,而宽屏这边一条断言都没有。
*/
const fold = await page.evaluate(() => {
const px = n => Math.round(n);
const exp = document.querySelector('[aria-expanded]');
const header = exp ? exp.closest('.glass-card') : null;
let scroller = document.querySelector('.markdown');
while (scroller && !['auto', 'scroll'].includes(getComputedStyle(scroller).overflowY)) {
scroller = scroller.parentElement;
}
const fab = document.querySelector('[data-testid="reply-fab"]');
const pane = document.querySelector('#root > div > div:last-child');
const R = el => (el ? el.getBoundingClientRect() : null);
const hb = R(header), sb = R(scroller), fb = R(fab), pb = R(pane);
return {
vh: px(window.innerHeight),
expanded: exp ? exp.getAttribute('aria-expanded') : null,
headerH: hb ? px(hb.height) : 0,
scrollerH: sb ? px(sb.height) : 0,
fab: fb ? { x: px(fb.x), y: px(fb.y), w: px(fb.width), h: px(fb.height) } : null,
pane: pb ? { x: px(pb.x), y: px(pb.y), w: px(pb.width), h: px(pb.height) } : null,
hasTextarea: !!document.querySelector('textarea')
};
});
chk('宽屏头部也默认收起', fold.expanded === 'false', String(fold.expanded));
chk(
'宽屏正文占视口 80% 以上',
fold.scrollerH / fold.vh >= 0.8,
`${Math.round((fold.scrollerH / fold.vh) * 100)}%(收起态实测 90%`
);
chk('宽屏回复框也默认收起、只剩悬浮球', fold.fab !== null && !fold.hasTextarea);
chk(
'悬浮球在详情栏内(不是视口右下角)',
fold.fab && fold.pane &&
fold.fab.x >= fold.pane.x && fold.fab.y >= fold.pane.y &&
fold.fab.x + fold.fab.w <= fold.pane.x + fold.pane.w + 1 &&
fold.fab.y + fold.fab.h <= fold.pane.y + fold.pane.h + 1,
`球=${JSON.stringify(fold.fab)} 栏=${JSON.stringify(fold.pane)}`
);
// 点标题行要真的展开(“默认收起”不能变成“永远打不开”)
await page.locator('[aria-expanded]').click();
await page.waitForTimeout(500);
const afterExpand = await page.evaluate(() => ({
expanded: document.querySelector('[aria-expanded]')?.getAttribute('aria-expanded'),
hasFrom: document.body.innerText.includes('发件')
}));
chk('宽屏点标题行能展开并看到收发件人', afterExpand.expanded === 'true' && afterExpand.hasFrom);
// 点悬浮球要能打开输入框
await page.locator('[data-testid="reply-fab"]').click();
await page.waitForTimeout(500);
chk('宽屏点悬浮球能展开输入框(带焦点)', await page.evaluate(() => document.activeElement?.tagName === 'TEXTAREA'));
console.log('\nissues:', issues.length ? issues : '无');
console.log(failed.length === 0 ? '\n宽屏回归全部通过' : `\n宽屏回归:${failed.length} 项失败`);
await page.close();
await browser.close();
process.exit(failed.length === 0 ? 0 : 1);