import json import subprocess from pathlib import Path MODULE = Path(__file__).parents[1] / "frontend" / "mobile-device-setup.js" def run_scenario(script: str) -> dict: harness = r""" const createMobileDeviceSetup = require(__MODULE__); class FakeTarget { constructor() { this.listeners = {}; this.hidden = false; this.disabled = false; this.textContent = ''; } addEventListener(name, callback) { (this.listeners[name] ||= []).push(callback); } async dispatch(name, event = {}) { for (const callback of this.listeners[name] || []) await callback(event); } focus() { state.focused = this; } } const state = {installCalls:0, offlineCalls:0, pushCalls:0, focused:null}; const launcher = new FakeTarget(); const closeButton = new FakeTarget(); const sheet = new FakeTarget(); sheet.hidden = true; const installButton = new FakeTarget(); const offlineButton = new FakeTarget(); const pushButton = new FakeTarget(); const installStatus = new FakeTarget(); const offlineStatus = new FakeTarget(); const pushStatus = new FakeTarget(); const readyStatus = new FakeTarget(); const escapeTarget = new FakeTarget(); let readiness = { install:{state:'complete', detail:'Stackchain is installed.'}, offline:{state:'incomplete', detail:'Offline work is off.'}, push:{state:'unavailable', detail:'Notifications are unavailable.'}, }; const setup = createMobileDeviceSetup({ launcher, closeButton, sheet, installButton, offlineButton, pushButton, installStatus, offlineStatus, pushStatus, readyStatus, escapeTarget, getReadiness:() => readiness, install:async () => { state.installCalls += 1; }, enableOffline:async () => { state.offlineCalls += 1; }, enablePush:async () => { state.pushCalls += 1; }, }); (async () => { setup.start(); __SCENARIO__ })().catch(error => { console.error(error); process.exit(1); }); """.replace("__MODULE__", json.dumps(str(MODULE))).replace("__SCENARIO__", script) completed = subprocess.run(["node", "-e", harness], capture_output=True, text=True) assert completed.returncode == 0, completed.stderr return json.loads(completed.stdout) def test_opening_setup_reports_actual_readiness_without_triggering_permissions(): result = run_scenario(""" await launcher.dispatch('click', {currentTarget:launcher}); process.stdout.write(JSON.stringify({ hidden:sheet.hidden, install:installStatus.textContent, offline:offlineStatus.textContent, push:pushStatus.textContent, summary:readyStatus.textContent, calls:[state.installCalls,state.offlineCalls,state.pushCalls], })); """) assert result == { "hidden": False, "install": "Stackchain is installed.", "offline": "Offline work is off.", "push": "Notifications are unavailable.", "summary": "1 of 2 available steps ready.", "calls": [0, 0, 0], } def test_setup_action_rechecks_real_state_before_marking_step_ready(): result = run_scenario(""" await launcher.dispatch('click', {currentTarget:launcher}); readiness.offline = {state:'complete', detail:'Offline work is saved.'}; await offlineButton.dispatch('click'); process.stdout.write(JSON.stringify({ calls:state.offlineCalls, offline:offlineStatus.textContent, hidden:offlineButton.hidden, summary:readyStatus.textContent, })); """) assert result == { "calls": 1, "offline": "Offline work is saved.", "hidden": True, "summary": "This device is ready.", } def test_escape_closes_setup_and_restores_launcher_focus(): result = run_scenario(""" await launcher.dispatch('click', {currentTarget:launcher}); await escapeTarget.dispatch('keydown', {key:'Escape'}); process.stdout.write(JSON.stringify({ hidden:sheet.hidden, launcherFocused:state.focused === launcher, })); """) assert result == {"hidden": True, "launcherFocused": True} def test_mobile_dashboard_mounts_phone_safe_device_setup_flow(): root = MODULE.parents[1] html = (root / "frontend" / "index.html").read_text() dashboard = (root / "frontend" / "dashboard.js").read_text() worker = (root / "frontend" / "service-worker.js").read_text() css = (root / "frontend" / "dashboard.css").read_text() assert 'id="open-device-setup"' in html assert 'id="device-setup-sheet"' in html assert 'aria-labelledby="device-setup-heading"' in html assert all(f'id="device-setup-{step}"' in html for step in ("install", "offline", "push")) assert '' in html assert "createMobileDeviceSetup.mount({" in dashboard assert "BASE + 'static/mobile-device-setup.js'" in worker assert ".device-setup-panel" in css assert "overflow-x:hidden" in css assert "env(safe-area-inset-bottom)" in css assert ".device-setup-action { min-height:44px;" in css