Files
MailUI4Agents/client/electron/test/manual/wide-regression.mjs
JianFeeeee d78f19fe9a fix(webui): 日历页面补上圆角(圆角要给到「有底色的那一层」)
用户(2026-09-14):「日历页面还没有添加圆角」。

根因不是「忘了写 border-radius」—— 外壳那条 `.app-shell > *` 是生效的,
它给日历**根节点**加了 14px 圆角。问题是根节点写着
`flex-1 min-w-0 flex min-h-0`、**自己完全没有底色**:
真正白底的是里面那两块面板(网格栏、右栏),它们的直角从透明外壳里戳出来。
所以 `getComputedStyle(根).borderRadius` 一直是 14px,看上去却全是方角。

两处不能照搬收件箱做法的地方:

① 网格栏与右栏**不是两张卡**(中间只有 1px 分隔线,没有外壳间距)
   ⇒ 只能给**外沿**:左边那块给左两角、右边那块给右两角。
   内侧也给会露出底色缺口。
② 右栏外面还套了一层容器(管 `lg:w-[400px]` 与 `border-l`,自己不上色),
   真正上色的是它里面的 DayAgendaPane / CalendarEventEditor
   ⇒ 圆角要**再往下给一层**,只加在容器上会被内层直角原地盖掉。
   (左栏本身就是白底面板,不能再往下给:它的第一个子元素是工具条。)

实测(1280×800,真浏览器读渲染像素):
  改前 左栏 radius=0px、右栏 radius=0px,四个外角都是白像素(方角)
  改后 左栏 `14px 0px 0px 14px`、右栏内层 `0px 14px 14px 0px`,
       四个外角像素都等于页面底色(被切掉);上边缘内缩 8px,
       与收件箱详情栏(对照)完全一致;内侧分隔线两侧保持直角。

判据:narrow-layout 新增 4 条 —— 钉的是「规则与 JSX 接没接上」「给的是哪几条边」
「右栏有没有再往下给一层」「左栏有没有多给一层」,不是那条 14px。
wide-regression 新增一条**渲染层**判据:从 `elementFromPoint` 沿祖先链找第一个
自带底色的元素,四个外角都必须正是那个圆角面板(光看 computed style 区分不了,
因为被盖掉的形态 computed 也照样是 14px)。

顺手修掉 wide-regression 里一条**假失败**:`#root > div` 现在指向壁纸幕布
(`app-backdrop` 后来挂成了 `#root` 的第一个子节点),于是「仍是三栏并排」
一直报「栏数=0」,而三栏好好的。假失败比没有判据更贵 —— 看的人会去查一个
本来没坏的东西。
2026-09-14 16:05:15 +08:00

187 lines
8.1 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(() => {
/*
* 选 `.app-shell`,不是 `#root > div`。
*
* 壁纸层(`app-backdrop`)后来挂成了 `#root` 的第一个子节点,`#root > div`
* 从此指向**幕布本身**(它没有子节点)—— 这条判据于是一直报「栏数=0」的假失败。
* 假失败比没有判据更贵:看的人会去查一个本来好好的三栏布局。
*/
const root = document.querySelector('.app-shell');
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'));
/*
* 日历的圆角(用户 2026-09-14「日历页面还没有添加圆角」
*
* 为什么在这里验、而不是只验 CSS那一族的形态是**「computed style 里
* border-radius 就是 14px看上去却是方角」** —— 圆角加在一个**没有底色**的
* 外层上,白底的内层面板用直角原地把它盖掉了。
* 光看 `getComputedStyle(panel).borderRadius` 也区分不了:真正决定观感的是
* **角上那 2px 到底是谁在画**。所以从 elementFromPoint 起往上走,找第一个
* 自带底色的元素 —— 它必须就是那个圆角面板本身。
*/
await page.locator('button:has-text("日历")').first().click();
await page.waitForSelector('.cal-panes', { timeout: 20000 });
await page.waitForTimeout(600);
const cal = await page.evaluate(() => {
const w = document.querySelector('.cal-panes');
if (!w) return { found: false };
const first = w.firstElementChild;
const last = w.lastElementChild;
const lastInner = last.firstElementChild;
const painterAt = (x, y) => {
let e = document.elementFromPoint(x, y);
while (e) {
const bg = getComputedStyle(e).backgroundColor;
if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') return e;
e = e.parentElement;
}
return null;
};
const rf = first.getBoundingClientRect();
const rl = last.getBoundingClientRect();
return {
found: true,
firstRadius: getComputedStyle(first).borderRadius,
lastRadius: lastInner ? getComputedStyle(lastInner).borderRadius : null,
tl: painterAt(rf.x + 2, rf.y + 2) === first,
bl: painterAt(rf.x + 2, rf.bottom - 3) === first,
tr: painterAt(rl.right - 3, rl.y + 2) === lastInner,
br: painterAt(rl.right - 3, rl.bottom - 3) === lastInner
};
});
chk('日历布局挂上了面板栈标记', cal.found);
chk(
'日历四个外角由**自带底色的那层面板**画(不是透明外壳)',
cal.found && cal.tl && cal.bl && cal.tr && cal.br,
`左栏 radius=${cal.firstRadius} 右栏 radius=${cal.lastRadius} 角=${JSON.stringify({ tl: cal.tl, bl: cal.bl, tr: cal.tr, br: cal.br })}`
);
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);