用户(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)+无模糊。
207 lines
9.5 KiB
JavaScript
207 lines
9.5 KiB
JavaScript
/**
|
||
* 导航重构的真实渲染验收(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('.popup-surface');
|
||
record(
|
||
'⑤ 弹层(地址建议菜单)不透明、也不取背景模糊',
|
||
// 2026-09-15 用户:「你在选择框的模糊逻辑上用力过猛,导致只能显示一条信息」——
|
||
// 菜单浮在表单之上,半透明 + 背景模糊会把背后的输入框/标签糊进候选行里。
|
||
menu !== null && menu >= 0.99 && card !== null && menu > card,
|
||
`正文面 α=${card} 弹层 α=${menu}(弹层必须 ≥0.99)`
|
||
);
|
||
} 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;
|