/** * 收件箱分组 + 授权独立列表的实测验收。 * * 起因:生产库里一个会话独占 17 封权限邮件,把另外两个会话的信挤出视野 * —— 收件箱失去了它唯一的作用(让人知道有哪几件事在等我)。 * * 修法两层,这里各验一层: * 1. 权限请求整体移出收件箱,进「授权」导航项(一级会话 / 二级请求) * 2. 收件箱剩下的普通邮件按会话折叠 * * 单元测试(test/components/mailGroups.test.tsx)守住分组函数的算术, * 量不出「组头真的只有一行」「折叠时组内邮件确实不在 DOM 里」这些 * 只有真实渲染才能验的事。 * * 用法:ADMIN_PW=<密码> ADMIN_USER=jianf AGENTMAIL_URL=http://127.0.0.1:8180 \ * node client/electron/test/manual/inbox-group-verify.mjs */ import { openApp, WIDE } from './narrow-probe-helper.mjs'; const { browser, page, issues } = await openApp(WIDE); const failed = []; async function check(name, fn) { try { const r = await fn(); console.log(` ${r.ok ? '通过' : '失败'} ${name}${r.note ? ' — ' + r.note : ''}`); if (!r.ok) failed.push(name); } catch (e) { console.log(` 错误 ${name} — ${e.message.slice(0, 100)}`); failed.push(name); } } /** 中间栏里可点的条目数(组头 + 展开出来的行)。 */ const rowCount = () => page.locator('div.overflow-y-auto button').count(); async function goto(label) { await page.click(`aside button[title*="${label}"], button[title*="${label}"]`); await page.waitForTimeout(1200); } // ───────────────── 收件箱:权限已移出、其余按会话折叠 ───────────────── console.log('\n收件箱:'); await goto('收件箱'); await check('权限请求不再出现在收件箱', async () => { const txt = await page.locator('div.overflow-y-auto').innerText(); // 「待决策」「等待你决策」都是授权列表的措辞;收件箱里不该有 const leaked = /待决策|等待你决策/.test(txt); return { ok: !leaked, note: leaked ? '收件箱里出现了权限措辞' : '干净' }; }); await check('多封邮件的会话折叠成一行', async () => { const header = await page.locator('h2:has-text("收件箱")').locator('..').innerText(); const m = header.match(/(\d+)\s*组\s*·\s*(\d+)\s*封/); if (!m) { // 每个会话都只有一封时不显示「N 组 · M 封」,这是设计(单封平铺) return { ok: true, note: `无可折叠会话:${header.replace(/\n/g, ' ').trim()}` }; } return { ok: Number(m[1]) < Number(m[2]), note: `${m[1]} 组 / ${m[2]} 封` }; }); // ───────────────── 授权:一级会话 / 二级请求 ───────────────── console.log('\n授权列表:'); await goto('授权'); await check('侧栏有独立的「授权」入口', async () => { const n = await page.locator('button[title*="授权"]').count(); return { ok: n > 0, note: `${n} 个入口` }; }); await check('权限请求按会话分组(一级是会话)', async () => { const heads = await page.locator('div.overflow-y-auto > div > button').count(); const txt = await page.locator('div.overflow-y-auto').innerText(); if (heads === 0 && /没有授权请求/.test(txt)) { return { ok: true, note: '当前无授权请求(空态正常)' }; } // 组头带会话别名(.alias 或「(未命名会话)」) const hasAlias = /\.\S+|\(未命名会话\)/.test(txt); return { ok: heads > 0 && hasAlias, note: `${heads} 个会话组头` }; }); await check('待决策的会话默认展开(在等人的不能藏)', async () => { const txt = await page.locator('div.overflow-y-auto').innerText(); if (/没有授权请求/.test(txt)) return { ok: true, note: '无授权请求,跳过' }; const pendingBadge = await page.locator('span:has-text("待决策")').count(); if (pendingBadge === 0) { return { ok: /已全部处理/.test(txt), note: '全部已处理,组头显示「已全部处理」' }; } // 有待决策 → 组内应已展开,能看到「等待你决策」的行 const rows = await page.locator('span:has-text("等待你决策")').count(); return { ok: rows > 0, note: `${pendingBadge} 个待决策徽标,${rows} 行展开可见` }; }); await check('已决策的历史折进二级,不默认铺开', async () => { const toggle = page.locator('button:has-text("已决策")'); const n = await toggle.count(); if (n === 0) return { ok: true, note: '无已决策历史,跳过' }; const label = await toggle.first().innerText(); return { ok: label.includes('展开'), note: label.trim() }; }); await check('二级折叠可展开', async () => { const toggle = page.locator('button:has-text("已决策")'); if ((await toggle.count()) === 0) return { ok: true, note: '无二级折叠,跳过' }; const before = await rowCount(); await toggle.first().click(); await page.waitForTimeout(600); const after = await rowCount(); return { ok: after > before, note: `${before} → ${after} 个条目` }; }); await check('点一条授权请求能打开决策面板', async () => { // 授权行带「等待你决策」或决策结果;组头带「待决策 N」或「已全部处理」 const row = page .locator('div.overflow-y-auto button') .filter({ hasText: /等待你决策|同意|拒绝/ }) .filter({ hasNotText: /待决策 \d|已全部处理|展开已决策|收起已决策/ }); if ((await row.count()) === 0) return { ok: true, note: '无授权请求,跳过' }; await row.first().click(); await page.waitForTimeout(1500); // 右栏出现权限决策按钮或已决策的结果说明 const panel = (await page.locator('button:has-text("同意")').count()) > 0 || (await page.locator('text=/已(同意|拒绝|决策)/').count()) > 0; return { ok: panel, note: panel ? '决策面板已渲染' : '右栏没有内容' }; }); await check('组头可收起', async () => { const head = page.locator('div.overflow-y-auto > div > button').first(); if ((await head.count()) === 0) return { ok: true, note: '无组头,跳过' }; const before = await rowCount(); await head.click(); await page.waitForTimeout(600); const after = await rowCount(); // 原本折叠的组点一下会展开;原本展开的会收起。两种都算「可切换」 return { ok: after !== before, note: `${before} → ${after}` }; }); // ───────────────── 徽标语义 ───────────────── console.log('\n徽标:'); await check('收件箱未读数不把权限请求算进来', async () => { const badge = page.locator('button[title*="收件箱"] span').filter({ hasText: /^\d+$/ }); const inboxBadge = (await badge.count()) > 0 ? await badge.first().innerText() : '0'; const permBadge = page.locator('button[title*="授权"] span').filter({ hasText: /^\d+$/ }); const pBadge = (await permBadge.count()) > 0 ? await permBadge.first().innerText() : '0'; // 两个数字各自独立;一个待批的 bash 不该在两处都计数 return { ok: true, note: `收件箱未读 ${inboxBadge},授权待决策 ${pBadge}` }; }); await check('无 JS 运行时错误', async () => { // 404 资源(favicon 之类)不算 JS 错误 const real = issues.filter(i => !/404|Failed to load resource/.test(i)); return { ok: real.length === 0, note: real.length ? real.slice(0, 2).join(' | ') : '无' }; }); console.log( failed.length === 0 ? '\n收件箱分组 + 授权列表:全部通过' : `\n收件箱分组 + 授权列表:${failed.length} 项失败 — ${failed.join('、')}` ); await page.close(); await browser.close(); process.exit(failed.length === 0 ? 0 : 1);