Files
MailUI4Agents/client/electron/test/nav-merge.test.mjs
JianFeeeee e6048e9914 fix(webui): 候选菜单改用不透明弹层表面 —— 半透明+背景模糊把背后的表单糊进了列表
用户(2026-09-15,看着抄送/转发两处的候选):「你在选择框的模糊逻辑上用力过猛,
导致只能显示一条信息」。

实测(生产构建 4c2bf26,浏览器里读计算样式,壁纸开):
  菜单 background: rgba(255,255,255,0.5) + backdrop-filter: blur(8px) saturate(1.1)
  而它浮在**表单自己**之上(主题输入框 y=294 正落在菜单 284-510 下面)
⇒ 背后的输入框/标签被糊一遍再从半透明列表底下透上来,只有顶行还读得清。

根因与 2026-09-14 那次「模糊叠模糊」同族:把**控件档**(0.5 透 + 8px 模糊)用在
**弹层**上。控件背后是面板自身,透一点好看;弹层背后是别的内容,透就是叠两份内容。

- 新增 .popup-surface(rgb(var(--c-white)),不取 backdrop-filter;壁纸开时单独压回不透明,
  否则会撞上 html[data-bg='on'] .bg-white 那条接管规则)
- .popup-surface 并入「滚动区不淡」豁免(原来只认 .glass-control,换类会漏掉 → 弹层又被洗白)
- AddressInput 菜单 glass-control → popup-surface;旧断言 nav-merge.test.mjs:109 钉的正是
  那个错的意图(『地址建议菜单』必须用控件档),已改钉新意图

判据:narrow-layout 新增 4 条(表面不透明/不取模糊/壁纸开时也压回/菜单穿的类)+
nav-merge 反向对照。两侧都验:变异①换回 glass-control → 红;变异②改回 0.5 → 红;
复原 → 71 通过 0 失败。生产实测:rgba(...,0.5)+blur → rgb(255,255,255)+无模糊。
2026-09-15 07:46:54 +08:00

178 lines
10 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 { code } from './lib/read.mjs';
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const read = (...p) => code(join(HERE, '..', 'src', ...p));
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');
// 「授权多选胶囊」现在住在共用组件里(MailView 只是用它)
const chip = read('components', 'Composer.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'\]/);
/*
* 判据要的是**行为**:点「通信」(没有明确 target 时)回到上次那个子页签。
*
* 这里原先钉的是一条字面表达式 `setViewMode(target || commTab`,而代码后来写成了
* `setViewMode(target ?? (isComm ? commTab : modes[0]))` —— 行为一样(点通信回 commTab,
* 点日历/联系人落到该项第一个模式),字面却不一样,于是判据红、代码没错。
* 钉字面表达式的判据会在每次等价改写时误报;钉"isComm 时落到 commTab"这个行为不会。
*/
assert.match(sidebar, /setViewMode\([^)]*isComm\s*\?\s*commTab/, '桌面点通信要回 commTab');
const desktopHandler = sidebar.match(/setViewMode\([^)]*isComm\s*\?\s*commTab[^)]*\)/)[0];
assert.match(desktopHandler, /target/, '桌面点非通信项时要有明确的 target(否则日历/联系人点不动)');
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('③ 复选框与地址建议用"控件档"透度(比正文面更透)', () => {
/*
* 判据要的是**那颗胶囊的样式**,不是"MailView.tsx 这个文件里出现过 glass-control"。
*
* 授权多选的选项胶囊后来抽成了共用组件 `ComposerChip`(MailView 与 Composer 共用),
* 于是"在 MailView.tsx 里 grep glass-control"必然红 —— 而实际样式是对的
* (ComposerChip 的 neutral 未选中态就是 glass-control)。文件是会搬的,
* 组件不会:所以这里先钉"MailView 用的是 ComposerChip",再钉"ComposerChip 用控件档"。
*/
assert.match(mailView, /import \{[^}]*ComposerChip[^}]*\} from '\.\/Composer'/, 'MailView 要用共用的胶囊组件');
assert.match(mailView, /<ComposerChip/, '授权选项必须是那颗胶囊');
assert.match(chip, /glass-control/, '授权多选胶囊(ComposerChip 未选中态)要用控件档');
/*
* ★ 2026-09-15 用户(看着抄送/转发两处的候选列表):「你在选择框的模糊逻辑上用力过猛,
* 导致只能显示一条信息」。
*
* 实测(壁纸开、生产构建 4c2bf26):建议菜单是 `rgba(255,255,255,0.5)` +
* `backdrop-filter: blur(8px)`,而它浮在**表单自己**(主题输入框、各段标签)之上。
* 控件档背后是自己的面板,透一点好看;**弹层背后是别的内容**,透就是把两份内容叠在一起。
* 所以这里从「菜单必须用控件档」改成「菜单必须用不透明弹层档」——
* 旧断言正是那个错的意图的书面记录。
*/
assert.match(addr, /popup-surface/, '地址建议菜单要用不透明的弹层档');
assert.ok(!/absolute[^`]*glass-control/.test(addr), '建议菜单不能再穿控件档(那档浮在表单上会糊成一片)');
assert.ok(!/glass-control[^']*bg-white/.test(chip), '胶囊不能再同时写 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 ⇒ 会判红');
// ④ 的两半都验:旧写法(顶部指示条 + 背景高亮)必须被拦下
const oldNavItem =
"{active && <span className=\"absolute top-0 left-1/4 right-1/4 h-0.5 bg-blue-400\" />}";
assert.ok(/bg-blue-400/.test(oldNavItem), '旧写法能被"不得有顶部指示条"这条命中');
const oldActiveRule = ".nav-item[data-active='true'] { background-color: rgb(var(--nav-active-bg)); }";
assert.ok(
!/\.narrow-nav \.nav-item\[data-active='true'\][^}]*background-color:\s*transparent/.test(oldActiveRule),
'旧写法(选中态带背景)不含"transparent"⇒ 会判红'
);
});
test('④ 底部导航选中态只换颜色(不靠背景块、不靠顶部指示条)', () => {
/*
* 用户(2026-09-14):「同时底部导航栏选中对应的文字和图标变色即可」。
*
* 这条判据的三半各自防一个曾经真出现过的写法:
* ① 顶部指示条 —— 底部条只有 51px 高,多一条形状就挤图标;
* ② `--nav-active-bg` 底色 —— 那是侧栏的线索,窄屏用不上;
* ③ 图标不跟 currentColor —— 那只改了文字色,图标还是灰的,
* 看上去像"选中了一半"。所以图标必须钉 stroke="currentColor"。
*/
assert.ok(!/bg-blue-400/.test(narrow), 'NarrowNav 里仍有选中态顶部指示条');
assert.match(
css,
/\.narrow-nav \.nav-item\[data-active='true'\][^{]*\{\s*background-color:\s*transparent/,
'底部导航的选中态没把背景高亮关掉'
);
assert.match(
css,
/\.nav-item\[data-active='true'\][^{]*\{[^}]*color:\s*rgb\(var\(--nav-active-fg\)\)/,
'选中态必须换文字颜色(否则连唯一的线索也没了)'
);
// 只变色的规则必须限定在底部导航内:侧栏没有文字标签,全靠背景块
assert.ok(
!/^\.nav-item\[data-active='true'\][^{]*\{[^}]*transparent/m.test(css),
'不得把"选中态无背景"推广到全部 .nav-item(桌面侧栏会失去选中线索)'
);
const icons = read('components', 'icons.tsx');
assert.match(icons, /stroke="currentColor"/, '图标不跟 currentColor ⇒ 改了文字色图标也不会变色');
});