Files
HomeDash/deploy/verify-swipe.js
JianFeeeee 270445a516 feat(navi): Enter 唤起搜索框 + Space 激活焦点卡片;监听器幂等守卫
- 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)
2026-10-02 11:24:55 +08:00

106 lines
6.0 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.

// 真实手势验收: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); });