stackchain-dashboard/tests/test_update_triage_gesture.py
timmy 931f656f78
All checks were successful
CI / lint (pull_request) Successful in 1m41s
CI / build-release (pull_request) Successful in 6s
CI / release-candidate (pull_request) Has been skipped
feat: add mobile update swipe triage (Closes #769)
2026-08-13 20:30:15 +00:00

111 lines
4.2 KiB
Python

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 '<script src="static/update-triage-gesture.js"></script>' 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