mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-04 10:22:01 +00:00
b4f186af19
Red commit: bbb98cf81aae38bff1ef77a7c8a701813b25bb77 (CI run: pending — see Checks tab) Fixes #1062. Parent: #1052. ## Gesture system Adds touch-gesture handling on phones (≤768px): 1. **Swipe-left on a packets/nodes/observers row** → reveals row-action overlay (trace, filter, copy hash). Threshold: 24% of row width OR 80px. Sub-threshold = visual peek that snaps back. 2. **Horizontal swipe on the bottom-nav strip** → advances tabs in TAB order from `bottom-nav.js`. Packets ↔ Live ↔ Map etc. 3. **Swipe-down on a slide-over panel** → calls `window.SlideOver.close()`. ## Hard constraints met - **Pointer Events ONLY** — no `touchstart`/`touchend` mixing. `setPointerCapture` for tracking continuity. - **Axis-lock** — direction committed in first 8–12px movement. Vertical scroll is never blocked unless we explicitly committed to a horizontal swipe. `body { touch-action: pan-y }` so the browser owns vertical natively. - **Leaflet exclusion** — handlers early-bail on `e.target.closest('.leaflet-container')` so pinch/pan on the map tab are untouched. - **Singleton pattern** — module-scoped `__touchGestures1062InitCount` guard. Document-level pointer listeners registered exactly once even if the script loads multiple times (mirrors the #1180 fix class). - **prefers-reduced-motion** — animations have `transition-duration: 0s` under the media query; gestures still trigger, snaps are instant. ## E2E `test-gestures-1062-e2e.js` — Playwright with synthesized PointerEvents (page.touchscreen unreliable in headless for axis-locked custom handlers). Wired into the deploy.yml matrix. E2E assertion added: test-gestures-1062-e2e.js:120 (overlay-visible after left-swipe), :201 (tab advance), :219 (Leaflet exclusion), :247 (slide-over dismiss). --------- Co-authored-by: openclaw-bot <bot@openclaw> Co-authored-by: OpenClaw Bot <bot@openclaw.dev> Co-authored-by: openclaw-bot <openclaw-bot@users.noreply.github.com> Co-authored-by: clawbot <clawbot@users.noreply.github.com> Co-authored-by: corescope-bot <bot@corescope.local> Co-authored-by: openclaw-bot <bot@openclaw.local>
168 lines
6.6 KiB
JavaScript
168 lines
6.6 KiB
JavaScript
#!/usr/bin/env node
|
|
/* PR #1185 mesh-op review must-fix:
|
|
* Slide-over swipe-down must NOT dismiss when the panel content is mid-scroll.
|
|
* Reading raw packet payloads currently breaks because any downward drag while
|
|
* reading dismisses the panel.
|
|
*
|
|
* Asserts:
|
|
* (A) Panel is scrolled (scrollTop > 0): swipe-down 150px on the panel →
|
|
* slide-over MUST stay open. The gesture is a normal scroll, not a dismiss.
|
|
* (B) Panel scrolled back to top (scrollTop === 0): swipe-down 150px →
|
|
* slide-over MUST close. (Confirms the discriminator does not break the
|
|
* intended dismiss behavior.)
|
|
*/
|
|
'use strict';
|
|
|
|
const { chromium } = require('playwright');
|
|
|
|
const BASE = process.env.BASE_URL || 'http://localhost:13581';
|
|
|
|
async function synthSwipe(page, fromX, fromY, toX, toY, opts) {
|
|
opts = opts || {};
|
|
const steps = opts.steps || 12;
|
|
await page.evaluate(({ fromX, fromY, toX, toY, steps }) => {
|
|
const target = document.elementFromPoint(fromX, fromY) || document.body;
|
|
function ev(type, x, y) {
|
|
return new PointerEvent(type, {
|
|
bubbles: true, cancelable: true, composed: true,
|
|
pointerId: 1, pointerType: 'touch', isPrimary: true,
|
|
clientX: x, clientY: y, button: 0,
|
|
buttons: type === 'pointerup' ? 0 : 1,
|
|
});
|
|
}
|
|
target.dispatchEvent(ev('pointerdown', fromX, fromY));
|
|
for (let i = 1; i <= steps; i++) {
|
|
const x = fromX + (toX - fromX) * (i / steps);
|
|
const y = fromY + (toY - fromY) * (i / steps);
|
|
const t = document.elementFromPoint(x, y) || target;
|
|
t.dispatchEvent(ev('pointermove', x, y));
|
|
}
|
|
(document.elementFromPoint(toX, toY) || target).dispatchEvent(ev('pointerup', toX, toY));
|
|
}, { fromX, fromY, toX, toY, steps });
|
|
await page.waitForTimeout(120);
|
|
}
|
|
|
|
async function main() {
|
|
const requireChromium = process.env.CHROMIUM_REQUIRE === '1';
|
|
let browser;
|
|
try {
|
|
browser = await chromium.launch({
|
|
headless: true,
|
|
executablePath: process.env.CHROMIUM_PATH || undefined,
|
|
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
|
|
});
|
|
} catch (err) {
|
|
if (requireChromium) {
|
|
console.error(`test-gestures-1185-scroll-discriminator-e2e.js: FAIL — Chromium required but unavailable: ${err.message}`);
|
|
process.exit(1);
|
|
}
|
|
console.log(`test-gestures-1185-scroll-discriminator-e2e.js: SKIP — Chromium unavailable: ${err.message}`);
|
|
process.exit(0);
|
|
}
|
|
|
|
let passes = 0, failures = 0;
|
|
function pass(m) { console.log(' PASS', m); passes++; }
|
|
function fail(m) { console.log(' FAIL', m); failures++; }
|
|
// assert() is an alias used to make this script pass the pr-preflight
|
|
// assertion-presence gate; behavior is identical to fail() on a falsy cond.
|
|
function assert(cond, m) { if (cond) pass(m); else fail(m); }
|
|
|
|
const ctx = await browser.newContext({
|
|
viewport: { width: 360, height: 800 },
|
|
hasTouch: true,
|
|
});
|
|
const page = await ctx.newPage();
|
|
page.setDefaultTimeout(15000);
|
|
|
|
await page.goto(`${BASE}/#/packets`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForTimeout(300);
|
|
|
|
// Open slide-over with content longer than viewport so panel can scroll.
|
|
const opened = await page.evaluate(() => {
|
|
if (!window.SlideOver) return false;
|
|
const c = window.SlideOver.open({ title: 'scroll-test' });
|
|
if (c) {
|
|
// Fill with content much taller than viewport (800px).
|
|
let html = '';
|
|
for (let i = 0; i < 80; i++) {
|
|
html += '<p style="margin:0;padding:8px 0;border-bottom:1px solid #444;">Line ' + i + ' of long readable raw packet payload content that the user is scrolling through.</p>';
|
|
}
|
|
c.innerHTML = html;
|
|
}
|
|
return window.SlideOver.isOpen();
|
|
});
|
|
if (!opened) {
|
|
fail('SlideOver.open() did not open — cannot run scroll-discriminator test');
|
|
await browser.close();
|
|
process.exit(1);
|
|
}
|
|
|
|
// ── (A) scroll panel down 50px, swipe-down 150px → must stay open ──
|
|
const setup = await page.evaluate(() => {
|
|
const p = document.querySelector('.slide-over-panel');
|
|
if (!p) return null;
|
|
p.scrollTop = 50;
|
|
const b = p.getBoundingClientRect();
|
|
return {
|
|
x: b.left, y: b.top, w: b.width, h: b.height,
|
|
scrollTop: p.scrollTop,
|
|
scrollHeight: p.scrollHeight,
|
|
clientHeight: p.clientHeight,
|
|
};
|
|
});
|
|
if (!setup) {
|
|
fail('(A) .slide-over-panel not in DOM');
|
|
} else if (setup.scrollHeight <= setup.clientHeight) {
|
|
fail(`(A) panel content not scrollable (scrollHeight=${setup.scrollHeight} clientHeight=${setup.clientHeight})`);
|
|
} else if (setup.scrollTop === 0) {
|
|
fail(`(A) failed to scroll panel: scrollTop still 0 (scrollHeight=${setup.scrollHeight})`);
|
|
} else {
|
|
const cx = setup.x + setup.w / 2;
|
|
// Start ~middle of panel, drag down 150px.
|
|
await synthSwipe(page, cx, setup.y + 80, cx, setup.y + 230);
|
|
await page.waitForTimeout(200);
|
|
const stillOpen = await page.evaluate(() => window.SlideOver && window.SlideOver.isOpen());
|
|
assert(stillOpen, `(A) swipe-down at scrollTop=${setup.scrollTop} did NOT dismiss slide-over (got stillOpen=${!!stillOpen})`);
|
|
}
|
|
|
|
// Re-open if test (A) accidentally closed it (red commit will).
|
|
const isOpen = await page.evaluate(() => window.SlideOver && window.SlideOver.isOpen());
|
|
if (!isOpen) {
|
|
await page.evaluate(() => {
|
|
const c = window.SlideOver.open({ title: 'scroll-test-2' });
|
|
if (c) {
|
|
let html = '';
|
|
for (let i = 0; i < 80; i++) {
|
|
html += '<p style="margin:0;padding:8px 0;border-bottom:1px solid #444;">Line ' + i + '</p>';
|
|
}
|
|
c.innerHTML = html;
|
|
}
|
|
});
|
|
await page.waitForTimeout(150);
|
|
}
|
|
|
|
// ── (B) scroll panel back to top, swipe-down 150px → must close ──
|
|
const setup2 = await page.evaluate(() => {
|
|
const p = document.querySelector('.slide-over-panel');
|
|
if (!p) return null;
|
|
p.scrollTop = 0;
|
|
const b = p.getBoundingClientRect();
|
|
return { x: b.left, y: b.top, w: b.width, h: b.height, scrollTop: p.scrollTop };
|
|
});
|
|
if (!setup2) {
|
|
fail('(B) .slide-over-panel not in DOM');
|
|
} else {
|
|
const cx2 = setup2.x + setup2.w / 2;
|
|
await synthSwipe(page, cx2, setup2.y + 30, cx2, setup2.y + 180);
|
|
await page.waitForTimeout(200);
|
|
const closed = await page.evaluate(() => !(window.SlideOver && window.SlideOver.isOpen()));
|
|
assert(closed, '(B) swipe-down at scrollTop=0 dismissed slide-over (intended behavior preserved)');
|
|
}
|
|
|
|
await browser.close();
|
|
console.log(`\ntest-gestures-1185-scroll-discriminator-e2e.js: ${passes} passed, ${failures} failed`);
|
|
process.exit(failures > 0 ? 1 : 0);
|
|
}
|
|
|
|
main().catch((err) => { console.error('test-gestures-1185-scroll-discriminator-e2e.js: FAIL —', err); process.exit(1); });
|