#!/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 += '

Line ' + i + ' of long readable raw packet payload content that the user is scrolling through.

'; } 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 += '

Line ' + i + '

'; } 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); });