- bindSearchKeys:Enter 展开搜索并聚焦(搜索框已展开/焦点在可编辑元素时放行), Space 激活焦点所在的 a[href]/[role=button](preventDefault 防滚动) - bindSearchKeys/enablePeekGuard/mountViewSwitch 加幂等守卫: DOM 抖动场景下 HEAD 版泄漏 914 个 keydown 监听器,修复后 0 - MutationObserver 自触发加 selfWrite 计数 + 100ms 节流 - showWorkspaceBlockedModal 的 Escape 从 document 改挂 overlay - 验收脚本:verify-search-key.js(真实按键,页面内计数断言)、 verify-swipe.js(CDP 注入真实触摸,断言分组真的切换) - README/AGENTS:快捷键表、手势验收方法、改样式前先确认设计基准(§9)
106 lines
6.0 KiB
JavaScript
106 lines
6.0 KiB
JavaScript
// 真实手势验收:CDP 注入触摸序列,断言分组真的切换了。
|
||
// AGENTS.md §5 要求:不能只断言"事件到了/监听器挂了",必须断言行为变化。
|
||
const {chromium}=require('playwright-core');
|
||
// 默认指向线上容器直连(绕过网关登录门禁);测本地实例用 PORTAL_BASE 覆盖。
|
||
const BASE = process.env.PORTAL_BASE || 'http://127.0.0.1:18080';
|
||
|
||
|
||
|
||
(async () => {
|
||
const b = await chromium.connectOverCDP(process.env.CDP || 'http://127.0.0.1:9222');
|
||
const ctx = await b.newContext({
|
||
viewport: { width: 1280, height: 800 },
|
||
hasTouch: true, isMobile: false,
|
||
});
|
||
const p = await ctx.newPage();
|
||
const cdp = await ctx.newCDPSession(p);
|
||
await cdp.send('Emulation.setTouchEmulationEnabled', { enabled: true, maxTouchPoints: 5 });
|
||
|
||
const fails = [];
|
||
const ok = (name, pass, detail) => {
|
||
console.log(` ${pass ? '\x1b[32m✅\x1b[0m' : '\x1b[31m❌\x1b[0m'} ${name}${detail ? ' — ' + detail : ''}`);
|
||
if (!pass) fails.push(name);
|
||
};
|
||
|
||
const errors = [];
|
||
p.on('pageerror', e => errors.push(String(e).slice(0, 120)));
|
||
|
||
await p.goto(BASE + '/minimal', { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||
await p.waitForTimeout(4500);
|
||
|
||
const meta = await p.evaluate(() => ({
|
||
swipeFlag: document.documentElement.dataset.naviSwipe || '(无)',
|
||
heads: document.querySelectorAll('.minimal-headings-row .headings').length,
|
||
selected: [...document.querySelectorAll('.minimal-headings-row .headings')].findIndex(e => e.classList.contains('selected')),
|
||
selTitle: (document.querySelector('.minimal-headings-row .headings.selected .section-title') || {}).textContent || '',
|
||
visibleItems: document.querySelectorAll('.minimal-section-inner.selected .item').length,
|
||
touchAction: (() => { const e = document.querySelector('.minimal-home'); return e ? getComputedStyle(e).touchAction : '(无)'; })(),
|
||
}));
|
||
console.log(`\n════ ${BASE}/minimal ════`);
|
||
console.log(` <html data-navi-swipe> = ${meta.swipeFlag}`);
|
||
console.log(` 页签数 = ${meta.heads} 当前选中 = #${meta.selected} (${JSON.stringify(meta.selTitle.trim())})`);
|
||
console.log(` 当前分组可见卡片 = ${meta.visibleItems}`);
|
||
console.log(` .minimal-home touch-action = ${meta.touchAction}`);
|
||
|
||
ok('手势已启用(data-navi-swipe=on)', meta.swipeFlag === 'on', meta.swipeFlag);
|
||
ok('至少两个分组可切换', meta.heads >= 2, `${meta.heads} 个页签`);
|
||
|
||
const before = meta.selected;
|
||
|
||
// ── 真触摸:向左滑(下一个分组)──
|
||
const y = Math.round(800 * 0.55);
|
||
const from = Math.round(1280 * 0.80), to = Math.round(1280 * 0.15);
|
||
const pt = (x) => [{ x, y, radiusX: 12, radiusY: 12, force: 1, id: 1 }];
|
||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: pt(from) });
|
||
for (let i = 1; i <= 12; i++) {
|
||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: pt(from + (to - from) * i / 12) });
|
||
await new Promise(r => setTimeout(r, 14));
|
||
}
|
||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
|
||
await p.waitForTimeout(1200);
|
||
|
||
const after = await p.evaluate(() => ({
|
||
selected: [...document.querySelectorAll('.minimal-headings-row .headings')].findIndex(e => e.classList.contains('selected')),
|
||
selTitle: (document.querySelector('.minimal-headings-row .headings.selected .section-title') || {}).textContent || '',
|
||
visibleItems: document.querySelectorAll('.minimal-section-inner.selected .item').length,
|
||
innerSelected: (() => { const e = document.querySelector('.minimal-section-inner.selected'); return e ? e.className : '(无)'; })(),
|
||
}));
|
||
console.log(`\n 滑后:选中 = #${after.selected} (${JSON.stringify(after.selTitle.trim())}) 可见卡片 = ${after.visibleItems}`);
|
||
ok('★ 真实触摸左滑切换了分组', after.selected !== before,
|
||
`#${before} → #${after.selected}`);
|
||
ok('★ 面板内容跟着换了', after.visibleItems !== meta.visibleItems || after.selected !== before,
|
||
`卡片 ${meta.visibleItems} → ${after.visibleItems}`);
|
||
// ── 再滑回来 ──
|
||
const from2 = Math.round(1280 * 0.20), to2 = Math.round(1280 * 0.85);
|
||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: pt(from2) });
|
||
for (let i = 1; i <= 12; i++) {
|
||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: pt(from2 + (to2 - from2) * i / 12) });
|
||
await new Promise(r => setTimeout(r, 14));
|
||
}
|
||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
|
||
await p.waitForTimeout(1200);
|
||
const back = await p.evaluate(() => ({
|
||
selected: [...document.querySelectorAll('.minimal-headings-row .headings')].findIndex(e => e.classList.contains('selected')),
|
||
}));
|
||
ok('★ 反向右滑能切回', back.selected === before, `#${after.selected} → #${back.selected}`);
|
||
|
||
// ── 竖向滚动不能被误判为横滑 ──
|
||
const xm = Math.round(1280 * 0.5);
|
||
const ptx = (yy) => [{ x: xm, y: yy, radiusX: 12, radiusY: 12, force: 1, id: 1 }];
|
||
const yStart = 600, yEnd = 200;
|
||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: ptx(yStart) });
|
||
for (let i = 1; i <= 10; i++) {
|
||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: ptx(yStart + (yEnd - yStart) * i / 10) });
|
||
await new Promise(r => setTimeout(r, 14));
|
||
}
|
||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
|
||
await p.waitForTimeout(900);
|
||
const afterV = await p.evaluate(() => [...document.querySelectorAll('.minimal-headings-row .headings')].findIndex(e => e.classList.contains('selected')));
|
||
ok('竖滑不误触发横滑切换', afterV === back.selected, `#${back.selected} → #${afterV}`);
|
||
|
||
ok('无 pageerror', errors.length === 0, errors.join(' | ') || '0 个');
|
||
|
||
console.log(fails.length ? `\n\x1b[31m失败 ${fails.length} 项\x1b[0m` : '\n\x1b[32m全部通过\x1b[0m');
|
||
await p.close(); await ctx.close(); await b.close();
|
||
process.exit(fails.length ? 1 : 0);
|
||
})().catch(e => { console.error(e); process.exit(2); }); |