Files
MailUI4Agents/client/electron/test/nav-merge.test.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

109 lines
6.0 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.

/**
* 导航重构的判据2026-09-14 用户三条要求)。
*
* 用户原话:
* ①「导航栏的内容有点多了,收件发件授权改为一个导航项,通过内部导航区分,
* 然后新建作为他们内部的一个悬浮的圆形加号,这样导航项就只剩通信,日历,联系人」
* ②「将管理和我的合并,管理员视角我的页面拉到最下面有一个管理」
* ③「复选框和地址猜测项的透明度问题还没改,他们才是真正需要拉低透明度的地方」
*
* 为什么写文件级判据:改完跑老套件 254 条**全绿**,因为它一条都没测导航结构 ——
* 我差点把"没红的测试"当成"改对了"。结构类改动必须自己带判据。
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const read = (...p) => readFileSync(join(HERE, '..', 'src', ...p), 'utf8');
const sidebar = read('components', 'Sidebar.tsx');
const narrow = read('components', 'NarrowNav.tsx');
const commTabs = read('components', 'CommTabs.tsx');
const app = read('App.tsx');
const account = read('components', 'AccountPage.tsx');
const uiStore = read('stores', 'uiStore.ts');
const css = read('index.css');
const mailView = read('components', 'MailView.tsx');
const addr = read('components', 'AddressInput.tsx');
test('① 导航只剩 通信/日历/联系人 三项(桌面与窄屏都要)', () => {
for (const [name, src] of [['Sidebar', sidebar], ['NarrowNav', narrow]]) {
// 三项都在
assert.match(src, /短:\s*'通信'|short:\s*'通信'/, `${name} 缺「通信」`);
assert.match(src, /'日历'/, `${name} 缺「日历」`);
assert.match(src, /'联系人'|'联系'/, `${name} 缺「联系人」`);
// 旧的独立项必须消失
for (const gone of ['收件', '发件', '授权请求', '用户管理']) {
assert.ok(!src.includes(`'${gone}'`), `${name} 里还留着旧的独立导航项「${gone}`);
}
}
});
test('① 「通信」的选中态覆盖三个子页签,点击回到上次那个', () => {
assert.match(sidebar, /modes:\s*\['inbox',\s*'sent',\s*'permissions'\]/);
assert.match(narrow, /modes:\s*\['inbox',\s*'sent',\s*'permissions'\]/);
assert.match(sidebar, /setViewMode\(target \|\| commTab/, '桌面点通信要回 commTab');
assert.match(narrow, /setViewMode\(isComm \? commTab : mode\)/, '窄屏点通信要回 commTab');
});
test('① 内部导航CommTabs存在、挂在通信页、且带徽标', () => {
assert.match(commTabs, /data-testid="comm-tabs"/);
assert.match(commTabs, /'收件箱'[\s\S]*'发件箱'[\s\S]*'授权'/, '三个内部页签');
assert.match(commTabs, /pendingPerms/, '授权徽标不能因为合并而消失');
assert.match(app, /<CommTabs \/>/, 'App 里要渲染内部导航');
assert.match(app, /const isComm = viewMode === 'inbox'/, '只挂在通信三个页签下');
});
test('① 新建是悬浮圆形加号,且不再是导航项', () => {
assert.match(commTabs, /data-testid="compose-fab"/);
assert.match(commTabs, /rounded-full/, '必须是圆的(用户点名"圆形加号"');
assert.match(commTabs, /absolute bottom-4 right-4/, '悬浮在列表栏右下角');
assert.match(app, /<ComposeFab \/>/, 'App 里要渲染它');
assert.ok(!/新建<\/span>/.test(sidebar), '桌面侧栏不该再有为"新建"的导航文字');
assert.ok(!/新建<\/span>/.test(narrow), '窄屏导航不该再有"新建"');
});
test('② 管理与我的合并:入口在「我的」最下面,且仅管理员可见', () => {
assert.match(account, /user\?\.role === 'admin' &&/, '必须是条件渲染而不是禁用');
const logout = account.indexOf('退出登录');
const adminEntry = account.indexOf("setViewMode('admin')");
assert.ok(logout > 0 && adminEntry > logout, '管理入口要在「登录状态」之后(=页面最下面)');
assert.ok(!/mode: 'admin'/.test(sidebar), '侧栏不该再有独立的 admin 导航项');
assert.ok(!/mode: 'admin'/.test(narrow), '窄屏不该再有独立的 admin 导航项');
});
test('② uiStore 记住通信子页签,且 reset 能清掉', () => {
assert.match(uiStore, /commTab: CommTab/);
assert.match(uiStore, /setCommTab/);
assert.match(uiStore, /commTab: mode as CommTab/, 'setViewMode 要顺手记住');
assert.match(uiStore, /reset:[\s\S]{0,120}commTab: 'inbox'/, '登出后要复位');
});
test('③ 复选框与地址建议用"控件档"透度(比正文面更透)', () => {
assert.match(mailView, /glass-control/, '授权多选胶囊');
assert.match(addr, /glass-control/, '地址建议菜单');
// 两处都不能再是 bg-white我的 .bg-white 接管会把它们拉回正文面那一档
const chip = mailView.match(/glass-control[^']*/);
assert.ok(chip, 'chip 类名');
assert.ok(!/glass-control[^']*bg-white/.test(mailView), '不能再同时写 bg-white');
assert.ok(!/glass-control[^"]*bg-white/.test(addr), '建议菜单不能再同时写 bg-white');
// 三档透度必须真的递减(正文 > 嵌套 > 控件)
const num = re => Number(css.match(re)[1]);
const big = num(/--bg-glass:\s*(0\.\d+)/);
const inner = num(/--bg-glass-inner:\s*(0\.\d+)/);
const ctrl = num(/--bg-glass-control:\s*(0\.\d+)/);
assert.ok(big > inner && inner > ctrl, `三档应递减:${big} > ${inner} > ${ctrl}`);
assert.ok(ctrl <= 0.6, `控件档要明显更透(当前 ${ctrl}`);
});
test('★ 判据自检:拿重构前的写法喂进来必须判红', () => {
const oldSidebar = `{ short: '收件', mode: 'inbox', Icon: InboxIcon },
{ short: '授权', title: '授权请求', mode: 'permissions', Icon: ShieldIcon }`;
assert.ok(oldSidebar.includes("'授权请求'"), '旧写法能被本判据的"必须消失"条款命中');
const oldChip = "'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'";
assert.ok(!/glass-control/.test(oldChip), '旧胶囊写法不含 glass-control ⇒ 会判红');
});