Files
MailUI4Agents/client/electron/test/manual/narrow-glass-verify.mjs
JianFeeeee 76ce201c3a feat(webui): 导航合并 + 悬浮玻璃 + 页面动画 + 控件档透明度 + 圆角无条件生效
用户四封信(同一线索)提的六件事,都在这里:

## ① 导航合并(「导航栏的内容有点多了」)

收件箱/发件箱/授权 → 一项「通信」,进去由**内部页签**区分(CommTabs);
新建 → 「通信」页内**悬浮圆形加号**(ComposeFab);导航只剩 通信/日历/联系人;
管理 → 合进「我的」,管理员在页面**最下面**看得到(非管理员不渲染,不是禁用)。

`viewMode` 仍是原来那三个值(几十处调用点不用改),新增 `commTab` 记住上次用的子页签;
通信项的徽标 = 未读 + 待决策(授权信息不能因为合并而消失)。

## ② 窄屏底部导航:悬浮玻璃(「还是固定底部延伸,没有玻璃效果,也没有悬浮」)

原来是 `border-t bg-chrome-900` 通栏贴底。改成:左右/离底各留 10px、圆角、
深色半透明 + `blur(18px)`、投影,安全区并入下外边距。

## ③ 页面动画(「页面极度缺少动画,所有页面都是直接出现」)

在 `<html>` 上挂一个短命类触发外壳直接子面板的入场动画。**没有用 key 重挂载**:
面板是 flex 链上的一环,套 wrapper 会改掉 flex 传递(窄屏覆盖层最先坏)。
并且尊重 `prefers-reduced-motion`(关了就一点都不动)。

## ④ 控件档透明度(「复选框和地址猜测项…他们才是真正需要拉低透明度的地方」)

新增第三档 `--bg-glass-control`(0.5/0.55):授权多选胶囊、地址建议菜单。
三档递减:正文面 0.88 > 嵌套 0.82 > 控件 0.5。

## ⑤ 打底 vs 透明(上一封「该打底的地方透了、该透的空白处糊了」)

正文面回到 0.88(读得清优先)、空白/页面底透明**且不模糊**、面板模糊降到 10px。

## ⑥ 圆角与玻璃**无条件**生效(「大面积缺失圆角与玻璃效果…都是硬截断」)

根因:`.app-shell` 的留缝/圆角/投影原先全写在 `html[data-bg='on']` 里
⇒ **没开壁纸的账号**看到的是硬边不透明面板。现在几何下沉为无条件基线,
壁纸相关的加强仍叠加在上面。窄屏内容面板同样浮起来。

## 判据

- `test/nav-merge.test.mjs` 8 条(含自检:重构前的写法必须判红)——
  改完跑老套件 254 条**全绿却一条都没测导航结构**,结构改动必须自带判据。
- `test/manual/nav-restructure-verify.mjs`:真浏览器 9 条(导航项数、页签切换真的换内容、
  加号 56×56 且 `elementFromPoint` 命中自己、我的页底部管理入口在退出登录之后、
  控件档 α=0.5 对正文面 α=0.88 的反向对照)。
- `test/manual/narrow-glass-verify.mjs` 6 条:悬浮几何(三边留缝 10px、圆角 14px、
  缝隙处命中的是页面底 ⇒ 真的浮着)、玻璃(α=0.82 + blur18)、触摸高度最小 49px、
  动画真的在跑(静止时 `animationName=none`,切换后 =pane-in)且 reduced-motion 下不动。
- `test/background.test.mjs`:把三条**过时判据**(早前那版"越透越好")按现行契约重写 ——
  旧判据留着只会把我拽回错误方向。30 条全绿。

## 顺带

窄屏探针原先只调视口、**没模拟触屏**,于是 `(hover: hover)` 仍为真,
把「悬停才显形的次要动作」误报成透明 —— 差点去"修"一个本来就对的规则。
已改成 hasTouch + isMobile + DPR 的触屏上下文,且收尾只关自己的 context
(CDP 连的是共享 Chromium,`browser.close()` 会把别人的标签页一起关掉)。
2026-09-14 10:35:05 +08:00

121 lines
5.2 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.

/**
* 窄屏悬浮玻璃导航 + 视图切换动画的真实验收。
*
* 用户两条原话:
* 「窄屏布局的导航栏还是固定底部延伸,没有玻璃效果,也没有悬浮」
* 「页面极度缺少动画,所有页面都是直接出现」
*
* 判据只量**可观测的东西**:几何(留缝/圆角)、玻璃(半透明 + 模糊)、动画(真的在跑)。
* 每项都配反向对照,否则"没有动画"和"有动画但没触发"分不开。
*
* 用法AGENTMAIL_DIST=<dist> ADMIN_USER=.. ADMIN_PW=.. node test/manual/narrow-glass-verify.mjs
*/
import { openApp } from './narrow-probe-helper.mjs';
const PHONE = { width: 390, height: 844 };
const results = [];
const record = (name, ok, note) => {
results.push({ name, ok });
console.log(` ${ok ? '通过' : '失败'} ${name}${note ? ' — ' + note : ''}`);
};
const { page, ctx } = await openApp(PHONE);
try {
await page.waitForSelector('.narrow-nav', { timeout: 25000 });
await page.waitForTimeout(600);
// ── ① 悬浮:留缝 + 圆角 ──
const geo = await page.evaluate(() => {
const nav = document.querySelector('.narrow-nav');
const r = nav.getBoundingClientRect();
const cs = getComputedStyle(nav);
const pr = getComputedStyle(document.querySelector('.narrow-shell') || nav.parentElement);
return {
left: r.left,
right: r.right,
bottom: r.bottom,
vw: window.innerWidth,
vh: window.innerHeight,
radius: cs.borderRadius,
marginBottom: cs.marginBottom,
parentPadBottom: pr.paddingBottom,
pointerAtGap: document.elementFromPoint(r.left - 4, r.top + 4)?.className?.toString().slice(0, 40) || ''
};
});
record(
'① 导航不再贴底通栏:左右留缝、离底留缝、有圆角',
geo.left >= 6 && geo.vw - geo.right >= 6 && geo.vh - geo.bottom >= 6 && parseFloat(geo.radius) >= 8,
`left=${Math.round(geo.left)} right缝=${Math.round(geo.vw - geo.right)} bottom缝=${Math.round(geo.vh - geo.bottom)} radius=${geo.radius}`
);
record(
'① 左边缝露出来的是页面底(说明它真的浮在上面)',
!/narrow-nav/.test(geo.pointerAtGap),
`缝隙处命中=${geo.pointerAtGap || '(页面底)'}`
);
// ── ② 玻璃:半透明 + 模糊 ──
const glass = await page.evaluate(() => {
const cs = getComputedStyle(document.querySelector('.narrow-nav'));
const bg = cs.backgroundColor.match(/rgba?\(([^)]+)\)/);
const a = bg ? (bg[1].split(',').length === 4 ? parseFloat(bg[1].split(',')[3]) : 1) : null;
const bf = cs.backdropFilter || cs.webkitBackdropFilter || 'none';
return { alpha: a, blur: bf };
});
record(
'② 导航是玻璃:半透明 + 背景模糊',
glass.alpha !== null && glass.alpha > 0.4 && glass.alpha < 0.98 && /blur\((\d+)/.test(glass.blur),
`α=${glass.alpha} backdrop=${glass.blur}`
);
// ── ③ 触摸目标仍然达标(悬浮不能把可点面积改小)──
const taps = await page.$$eval('.narrow-nav button', els =>
els.map(e => {
const r = e.getBoundingClientRect();
return { w: Math.round(r.width), h: Math.round(r.height), label: e.textContent.trim().slice(0, 4) };
})
);
const small = taps.filter(t => t.h < 44);
record('③ 每项触摸高度 ≥44px', taps.length >= 4 && small.length === 0,
`${taps.length} 项,最小 ${Math.min(...taps.map(t => t.h))}px`);
// ── ④ 动画:切换页签时面板真的在跑 pane-in ──
const animBefore = await page.evaluate(
() => getComputedStyle(document.querySelector('.narrow-shell > *')).animationName
);
await page.click('.narrow-nav button:has-text("日历")');
await page.waitForTimeout(60);
const during = await page.evaluate(() => {
const root = document.documentElement;
const panes = [...document.querySelectorAll('.narrow-shell > *')];
return {
hasClass: root.classList.contains('view-switch'),
names: panes.map(p => getComputedStyle(p).animationName).filter(n => n !== 'none')
};
});
record(
'④ 切视图时面板在跑入场动画(反向对照:静止时不跑)',
animBefore === 'none' && during.names.length > 0 && during.names.every(n => n === 'pane-in'),
`静止 animationName=${animBefore};切换后 class=${during.hasClass} 动画=[${during.names.join(',')}]`
);
// ── ⑤ 动画必须尊重 reduced-motion ──
await page.emulateMedia({ reducedMotion: 'reduce' });
await page.click('.narrow-nav button:has-text("联系人")');
await page.waitForTimeout(60);
const reduced = await page.evaluate(() =>
[...document.querySelectorAll('.narrow-shell > *')]
.map(p => getComputedStyle(p).animationName)
.filter(n => n !== 'none')
);
record('⑤ 系统关了动画时一点都不动', reduced.length === 0, `动画=[${reduced.join(',')}]`);
await page.emulateMedia({ reducedMotion: null });
} finally {
await page.close();
await ctx.close();
}
const failed = results.filter(r => !r.ok);
console.log(`\n 窄屏悬浮玻璃 + 动画:${results.length - failed.length}/${results.length} 通过`);
if (failed.length) console.log(' 失败:' + failed.map(f => f.name).join('; '));
process.exitCode = failed.length ? 1 : 0;