/** * 导航重构的判据(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, //, '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, //, '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, / 嵌套 > 控件) 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 ⇒ 会判红'); });