用户(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 上已存在,与本次改动无关。
133 lines
5.6 KiB
JavaScript
133 lines
5.6 KiB
JavaScript
/**
|
||
* 宽屏回归:窄屏修复不能把桌面布局改坏。
|
||
*
|
||
* 特别是两个只该在窄屏生效的东西:
|
||
* - `.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);
|