Files
MailUI4Agents/client/electron/test/manual/narrow-verify.mjs

175 lines
6.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 窄屏实测验收390px 与 320px 下量真实盒子、真实命中。
*
* 每一条都对应一个曾经真实存在的问题(见 docs/PLAN.md §7.10.1
* 抽屉盖住底部导航、工具按钮只有 16px 高、看不见却按得动的「归档」、
* 对话树缩进把卡片压成竖条、对话树没有返回出口。
*
* 用法ADMIN_PW=<密码> node client/electron/test/manual/narrow-verify.mjs
*/
import {
openApp,
overflowX,
tapTargets,
hitTest,
scrollHealth,
SMALL
} from './narrow-probe-helper.mjs';
const { browser, page, issues } = await openApp();
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, 90)}`);
failed.push(name);
}
}
/**
* 打开收件箱里的第一封邮件。
*
* 邮件行是 `<button class="w-full text-left ...">`,不是带 cursor-pointer 的 div
* —— 按后者找会一直等到超时。
*/
async function openFirstMail(page) {
await page.click('nav button:has-text("收件")');
await page.waitForTimeout(1000);
const rows = page.locator('button.w-full.text-left');
const n = await rows.count();
if (n === 0) throw new Error('收件箱是空的,没有邮件可点');
await rows.first().click();
await page.waitForTimeout(1200);
}
console.log('窄屏实测390px');
// 抽屉已删。它是 fixed z-50 铺满视口高度,把底部导航最左那项盖住点不到。
await check('抽屉入口已移除', async () => {
const n = await page.locator('button[aria-label="打开导航"]').count();
return { ok: n === 0, note: n ? `仍有 ${n}` : '' };
});
// 删抽屉时退出登录是它唯一的独有入口,必须有新去处
await check('「我的」页有退出登录', async () => {
await page.click('nav button:has-text("我的")');
await page.waitForTimeout(1200);
const btn = page.locator('button:has-text("退出登录")');
const n = await btn.count();
const b = n ? await btn.first().boundingBox() : null;
return {
ok: n === 1 && b.height >= 36,
note: b ? `${Math.round(b.width)}x${Math.round(b.height)}` : '找不到'
};
});
// 核心回归:底部导航每一项都要命中自己
await check('底部导航每项都命中自己', async () => {
const r = await hitTest(page, 'nav button');
const miss = r.filter(x => !x.hit);
return {
ok: r.length > 0 && miss.length === 0,
note: miss.length ? '未命中: ' + miss.map(m => m.text).join(',') : `${r.length} 项全部命中`
};
});
// 详情页工具按钮:视觉 15-16px命中区必须补到 44
await check('详情页工具按钮命中区 >= 44px', async () => {
await openFirstMail(page);
const r = await tapTargets(page, ['标记已读', '对话树', '转发', '抄送', '发送', '清空']);
for (const x of r) console.log(' ', JSON.stringify(x));
const small = r.filter(x => x.hitH < 44 || x.hitW < 44);
return {
ok: r.length > 0 && small.length === 0,
note: small.length ? '仍偏小: ' + small.map(s => s.t).join(',') : `${r.length} 个都达标`
};
});
// 次要动作在触摸设备上必须可见(没有 hover 时曾经永远透明却接收点击)
await check('联系人页次要动作默认可见', async () => {
await page.click('nav button:has-text("联系人")');
await page.waitForTimeout(1200);
const r = await page.evaluate(() =>
[...document.querySelectorAll('.reveal')].slice(0, 3).map(d => getComputedStyle(d).opacity)
);
return { ok: r.length > 0 && r.every(o => o === '1'), note: `opacity=${r.join(',')}` };
});
// 对话树:窄屏要有返回出口
await check('对话树有返回出口', async () => {
await openFirstMail(page);
const t = page.locator('button:has-text("对话树")');
if ((await t.count()) === 0) return { ok: false, note: '找不到对话树入口' };
await t.first().click();
await page.waitForTimeout(1600);
const n = await page.locator('button[aria-label="返回"]').count();
return { ok: n >= 1, note: `${n}` };
});
// 各页无横向溢出
for (const label of ['收件', '联系人', '管理', '我的']) {
await check(`${label}页无横向溢出`, async () => {
await page.click(`nav button:has-text("${label}")`);
await page.waitForTimeout(1100);
const of = await overflowX(page);
for (const o of of.offenders) console.log(' 超出:', JSON.stringify(o));
return { ok: of.scrollWidth <= of.docWidth, note: `doc=${of.docWidth} scroll=${of.scrollWidth}` };
});
}
// 每个页面都必须有纵向滚动容器 —— 否则内容一超过视口就被裁掉看不到。
// 「我的」页曾经缺这个390px 下内容需 860px、容器 795px
// 「退出登录」按钮连同下面 65px 一起消失,滚也滚不到。
for (const label of ['收件', '发件', '联系人', '管理', '我的']) {
await check(`${label}页有纵向滚动容器`, async () => {
await page.click(`nav button:has-text("${label}")`);
await page.waitForTimeout(1200);
const h = await scrollHealth(page);
for (const c of h.clipped) console.log(' 被裁:', JSON.stringify(c));
return {
ok: h.hasScroller && h.clipped.length === 0,
note: h.hasScroller
? `${h.scrollers.length} 个容器${h.clipped.length ? ',但有内容被裁' : ''}`
: '没有滚动容器'
};
});
}
console.log('\n最窄320px');
await page.setViewportSize(SMALL);
await page.waitForTimeout(800);
await check('320px 收件箱无横向溢出', async () => {
await page.click('nav button:has-text("收件")');
await page.waitForTimeout(1000);
const of = await overflowX(page);
return { ok: of.scrollWidth <= of.docWidth, note: `doc=${of.docWidth} scroll=${of.scrollWidth}` };
});
await check('320px 模型范围排序按钮命中区达标', async () => {
await page.click('nav button:has-text("管理")');
await page.waitForTimeout(900);
await page.click('button:has-text("模型范围")');
await page.waitForTimeout(1000);
const first = page.locator('button:has-text("dsh")').first();
if ((await first.count()) === 0) return { ok: false, note: '没有 Agent 可展开' };
await first.click();
await page.waitForTimeout(1600);
const r = await tapTargets(page, ['↑', '↓', '×']);
if (r.length === 0) return { ok: true, note: '当前没有已选模型,跳过' };
const small = r.filter(x => x.hitH < 44 || x.hitW < 44);
return { ok: small.length === 0, note: `${r.length} 个,最小 ${Math.min(...r.map(x => x.hitH))}px 高` };
});
console.log('\nissues:', issues.length ? issues : '无');
console.log(failed.length === 0 ? '\n窄屏实测全部通过' : `\n窄屏实测:${failed.length} 项失败 — ${failed.join(', ')}`);
await page.close();
await browser.close();
process.exit(failed.length === 0 ? 0 : 1);