用户(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)+无模糊。
178 lines
10 KiB
JavaScript
178 lines
10 KiB
JavaScript
/**
|
||
* 导航重构的判据(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 ⇒ 改了文字色图标也不会变色');
|
||
});
|