import json import subprocess from pathlib import Path import pytest from tests.dashboard_bundle import dashboard GESTURE = Path(__file__).resolve().parents[1] / "frontend" / "update-triage-gesture.js" def run_gesture(script): source = f"const createGesture = require({json.dumps(str(GESTURE))});\n" + script result = subprocess.run(["node", "-e", source], capture_output=True, text=True) assert result.returncode == 0, result.stderr return json.loads(result.stdout) def test_committed_horizontal_swipes_decide_once_and_lock_while_pending(): result = run_gesture(r""" class Surface { constructor() { this.listeners = {}; this.dataset = {}; } addEventListener(name, callback) { this.listeners[name] = callback; } removeEventListener(name) { delete this.listeners[name]; } setPointerCapture() {} releasePointerCapture() {} } const surface = new Surface(); const calls = []; let enabled = true; let resolveRead; let lockedCalls; const gesture = createGesture({ surface, enabled: () => enabled, keepUnread: () => calls.push('keep'), markRead: () => new Promise(resolve => { calls.push('read'); resolveRead = resolve; }), viewportWidth: () => 390, }); const target = {closest: () => null, parentElement: null, scrollWidth: 100, clientWidth: 100}; const event = (x, y, id=1) => ({pointerId:id, pointerType:'touch', clientX:x, clientY:y, target, preventDefault(){}}); surface.listeners.pointerdown(event(180, 200)); surface.listeners.pointermove(event(250, 205)); surface.listeners.pointerup(event(270, 206)); surface.listeners.pointerdown(event(280, 200)); surface.listeners.pointermove(event(205, 204)); surface.listeners.pointerup(event(190, 205)); surface.listeners.pointerdown(event(280, 200)); surface.listeners.pointermove(event(190, 203)); surface.listeners.pointerup(event(180, 203)); Promise.resolve().then(() => { lockedCalls = [...calls]; resolveRead(); return Promise.resolve(); }).then(() => process.stdout.write(JSON.stringify({calls, lockedCalls, direction:surface.dataset.triageGesture || null}))); """) assert result == { "calls": ["keep", "read"], "lockedCalls": ["keep", "read"], "direction": None, } def test_scrolls_short_drags_edges_and_interactive_targets_never_decide(): result = run_gesture(r""" class Surface { constructor() { this.listeners = {}; this.dataset = {}; } addEventListener(name, callback) { this.listeners[name] = callback; } removeEventListener(name) { delete this.listeners[name]; } setPointerCapture() {} releasePointerCapture() {} } const surface = new Surface(); const calls = []; createGesture({surface, enabled:()=>true, keepUnread:()=>calls.push('keep'), markRead:()=>calls.push('read'), viewportWidth:()=>390}); const plain = {closest: () => null, parentElement: null, scrollWidth:100, clientWidth:100}; const interactive = {closest: selector => selector.includes('button') ? {} : null, parentElement:null, scrollWidth:100, clientWidth:100}; const scroller = {closest: () => null, parentElement:null, scrollWidth:400, clientWidth:100}; const event = (x,y,target=plain,id=1) => ({pointerId:id,pointerType:'touch',clientX:x,clientY:y,target,preventDefault(){}}); function drag(fromX, fromY, toX, toY, target=plain) { surface.listeners.pointerdown(event(fromX,fromY,target)); surface.listeners.pointermove(event(toX,toY,target)); surface.listeners.pointerup(event(toX,toY,target)); } drag(180,100,205,104); drag(180,100,220,190); drag(8,100,120,104); drag(180,100,280,104,interactive); drag(180,100,280,104,scroller); surface.listeners.pointerdown(event(180,100)); surface.listeners.pointercancel(event(280,104)); process.stdout.write(JSON.stringify({calls, direction:surface.dataset.triageGesture || null})); """) assert result == {"calls": [], "direction": None} @pytest.mark.anyio async def test_dashboard_wires_accessible_phone_only_update_swipe_triage(): html = await dashboard() assert '' in html assert 'id="update-gesture-status"' in html assert "createUpdateTriageGesture({" in html assert "qs('#keep-update-unread').click()" in html assert "qs('#mark-update-read-next').click()" in html assert "data-triage-gesture" in html assert "prefers-reduced-motion: reduce" in html