/** * 导航重构的真实渲染验收(2026-09-14 用户三条要求)。 * * 文件级判据(test/nav-merge.test.mjs)只能证明"代码里写了",证明不了"点得到、 * 看得见、量出来对"。这里在真浏览器里量: * * ① 导航项真的只剩 3 项(宽屏侧栏)/ 3+我的(窄屏底部) * ② 内部页签能切换、切换后列表真的换内容 * ③ 悬浮加号命中区 >= 44px,且 elementFromPoint 命中的就是它(不是被别的层压住) * ④ 管理员在「我的」页最下面真的看得到管理入口 * ⑤ 控件档透度**真的**比正文面低(反向对照:两类面的 alpha 必须分得开) * * 用法:node client/electron/test/manual/nav-restructure-verify.mjs */ import { openApp, tapTargets } from './narrow-probe-helper.mjs'; const WIDE = { width: 1280, height: 900 }; const PHONE = { width: 390, height: 844 }; const results = []; function record(name, ok, note) { results.push({ name, ok }); console.log(` ${ok ? '通过' : '失败'} ${name}${note ? ' — ' + note : ''}`); } // ── 宽屏 ───────────────────────────────────────────────────────────── { const { page, ctx } = await openApp(WIDE); try { await page.waitForSelector('.w-\\[60px\\] , .narrow-nav', { timeout: 25000 }); // 侧栏根是 div.w-[60px](不是 nav 元素)——之前用 'nav' 选择器什么也没等到。 // 标签取"按钮文字去掉徽标数字":带徽标的项最后一个 span 是徽标, // 按 span:last-child 取会把「通信」「联系人」整条丢掉(判据自己的 bug,实测踩到)。 const navLabels = await page.$$eval('.w-\\[60px\\] button', els => els.map(e => e.textContent.replace(/[0-9]+\+?/g, '').trim()).filter(Boolean) ); const commCount = navLabels.filter(t => t === '通信').length; const stale = navLabels.filter(t => ['收件', '发件', '授权', '新建', '用户'].includes(t)); record( '① 宽屏侧栏只剩 通信/日历/联系(+我的)', commCount === 1 && stale.length === 0, `标签=[${navLabels.join(',')}] 残留=[${stale.join(',')}]` ); // ② 内部页签:切换后列表标题/内容变化 if (await page.locator('[data-testid="comm-tabs"]').count()) { const tabs = await page.$$eval('[data-testid="comm-tabs"] button', els => els.map(e => e.textContent.trim()) ); record( '① 通信内部有三个页签(收件箱/发件箱/授权)', tabs.length === 3 && tabs[0].includes('收件箱') && tabs[2].includes('授权'), `页签=[${tabs.join(' | ')}]` ); const listText = async () => (await page.locator('[data-testid="comm-tabs"]').locator('xpath=..').innerText()).slice(0, 400); const before = await listText(); await page.click('[data-testid="comm-tabs"] button:has-text("发件箱")'); await page.waitForTimeout(700); const after = await listText(); record('② 切换页签后内容真的变了', before !== after, `${before.length} 字符 → ${after.length} 字符`); await page.click('[data-testid="comm-tabs"] button:has-text("收件箱")'); await page.waitForTimeout(500); } else { record('① 通信内部有三个页签', false, '没找到 comm-tabs'); } // ③ 悬浮加号 const fab = page.locator('[data-testid="compose-fab"]'); if (await fab.count()) { const box = await fab.boundingBox(); const hit = await page.evaluate(() => { const b = document.querySelector('[data-testid="compose-fab"]'); if (!b) return null; const r = b.getBoundingClientRect(); const el = document.elementFromPoint(r.x + r.width / 2, r.y + r.height / 2); return { inside: b.contains(el) || b === el, r: { w: r.width, h: r.height } }; }); const round = box && Math.abs(box.width - box.height) < 2; record( '③ 悬浮加号是圆的、命中区 ≥44px、且没被压住', !!box && box.width >= 44 && box.height >= 44 && round && hit?.inside === true, `尺寸=${Math.round(box?.width)}×${Math.round(box?.height)} 命中自己=${hit?.inside} 圆=${round}` ); } else { record('③ 悬浮加号存在', false, '没找到 compose-fab'); } // ⑤ 透度分档(把壁纸状态直接打开,量的是 CSS 本身,不动用户保存的设置) await page.evaluate(() => { document.documentElement.dataset.bg = 'on'; }); await page.waitForTimeout(300); const alpha = sel => page.evaluate(s => { const el = document.querySelector(s); if (!el) return null; const bg = getComputedStyle(el).backgroundColor; const m = bg.match(/rgba?\(([^)]+)\)/); if (!m) return null; const parts = m[1].split(',').map(x => parseFloat(x)); return parts.length === 4 ? parts[3] : 1; }, sel); const card = await alpha('.bg-white'); // 地址建议菜单:先在写信页的收件人框里打字触发 await page.click('[data-testid="compose-fab"]'); await page.waitForTimeout(600); const toInput = page.locator('input[placeholder*="收件人"], input[autocomplete="off"]').first(); if (await toInput.count()) { await toInput.fill('gui'); await page.waitForTimeout(1200); } const menu = await alpha('.glass-control'); record( '⑤ 控件档(地址建议)比正文面更透', menu !== null && card !== null && menu < card - 0.15, `正文面 α=${card} 控件 α=${menu}` ); } finally { await page.close(); await ctx.close(); } } // ── 窄屏 ───────────────────────────────────────────────────────────── { const { page, ctx } = await openApp(PHONE); try { await page.waitForSelector('nav', { timeout: 20000 }); const labels = await page.$$eval('.narrow-nav button', els => els.map(e => e.textContent.replace(/[0-9]+\+?/g, '').trim()).filter(Boolean) ); const stale = labels.filter(t => ['收件', '发件', '授权', '新建', '管理'].includes(t)); record( '① 窄屏底部导航 = 通信/日历/联系人/我的', labels.length === 4 && labels.includes('通信') && labels.includes('我的') && stale.length === 0, `标签=[${labels.join(',')}] 残留=[${stale.join(',')}]` ); const fab = page.locator('[data-testid="compose-fab"]'); const fabInfo = (await fab.count()) ? await page.evaluate(() => { const b = document.querySelector('[data-testid="compose-fab"]'); const r = b.getBoundingClientRect(); const nav = document.querySelector('.narrow-nav')?.getBoundingClientRect(); const el = document.elementFromPoint(r.x + r.width / 2, r.y + r.height / 2); return { w: r.width, h: r.height, bottom: r.bottom, navTop: nav ? nav.top : null, inside: b.contains(el) || b === el }; }) : null; record( '③ 窄屏加号在底部导航之上、命中区达标、没被压住', !!fabInfo && fabInfo.w >= 44 && fabInfo.h >= 44 && fabInfo.inside === true && (fabInfo.navTop === null || fabInfo.bottom <= fabInfo.navTop + 1), fabInfo ? `${Math.round(fabInfo.w)}×${Math.round(fabInfo.h)} bottom=${Math.round(fabInfo.bottom)} navTop=${Math.round(fabInfo.navTop)}` : '没找到' ); if (await fab.count()) { await fab.click(); await page.waitForTimeout(900); const composing = await page.evaluate(() => !!document.querySelector('textarea') && !document.querySelector('[data-testid="compose-fab"]') ); record('② 点加号能进入写信(且加号自己让位)', composing, ''); } // ④ 我的页最下面的管理入口(当前登录账号是否管理员决定可见性,两种都算通过) await page.click('.narrow-nav button:has-text("我的")'); await page.waitForTimeout(900); const adminInfo = await page.evaluate(() => { const btns = [...document.querySelectorAll('button')]; const admin = btns.find(b => b.textContent.includes('用户管理')); const logout = btns.find(b => b.textContent.includes('退出登录')); if (!admin) return { found: false }; return { found: true, afterLogout: !!(logout && admin.getBoundingClientRect().top >= logout.getBoundingClientRect().top), visible: admin.getBoundingClientRect().height > 0 }; }); record( '④ 「我的」页底部有管理入口(且排在退出登录之后)', adminInfo.found && adminInfo.afterLogout && adminInfo.visible, adminInfo.found ? `在退出登录之后=${adminInfo.afterLogout}` : '当前账号非管理员(此判据不适用)' ); } finally { await page.close(); await ctx.close(); } } const failed = results.filter(r => !r.ok); console.log(`\n 导航重构验收:${results.length - failed.length}/${results.length} 通过`); if (failed.length) console.log(' 失败:' + failed.map(f => f.name).join('; ')); process.exitCode = failed.length ? 1 : 0;