import json import subprocess from pathlib import Path import pytest from tests.dashboard_bundle import dashboard MOBILE_LAUNCH = Path(__file__).resolve().parents[1] / "frontend" / "mobile-launch.js" APP_SHORTCUTS = Path(__file__).resolve().parents[1] / "frontend" / "mobile-app-shortcuts.js" def test_installed_app_shortcut_accepts_only_bounded_actions_and_runs_once(): script = f""" const shortcuts = require({json.dumps(str(APP_SHORTCUTS))}); const calls = []; const controller = shortcuts.createController({{ search:'?launch=continue', continueWork:() => calls.push('continue'), newIssue:() => calls.push('new'), agenda:() => calls.push('agenda'), }}); Promise.resolve(controller.run()).then(() => controller.run()).then(() => {{ process.stdout.write(JSON.stringify({{ action:controller.action(), calls, invalid:shortcuts.parse('?launch=delete-account'), shared:shortcuts.parse('?launch=new&title=Shared'), }})); }}); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "action": "continue", "calls": ["continue"], "invalid": None, "shared": "new" } def test_mobile_launch_selects_priority_queue_without_overriding_saved_choice(): script = f""" const mobileLaunch = require({json.dumps(str(MOBILE_LAUNCH))}); process.stdout.write(JSON.stringify({{ today: mobileLaunch.chooseFilter({{saved:null, today:2, attention:4, agenda:6}}), attention: mobileLaunch.chooseFilter({{saved:null, today:0, attention:4, agenda:6}}), agenda: mobileLaunch.chooseFilter({{saved:null, today:0, attention:0, agenda:6}}), all: mobileLaunch.chooseFilter({{saved:null, today:0, attention:0, agenda:0}}), saved: mobileLaunch.chooseFilter({{saved:'agenda', today:2, attention:4, agenda:6}}), }})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "today": "today", "attention": "attention", "agenda": "agenda", "all": "all", "saved": "agenda", } def test_mobile_disclosure_closes_on_escape_and_restores_launcher_focus(): script = f""" const mobileLaunch = require({json.dumps(str(MOBILE_LAUNCH))}); const listeners = {{}}; const disclosure = {{open:true, addEventListener(name, fn) {{ listeners[name] = fn; }}}}; const launcher = {{focuses:0, focus() {{ this.focuses += 1; }}}}; const controller = mobileLaunch.createDisclosure({{disclosure, launcher}}); controller.start(); let prevented = false; listeners.keydown({{key:'Escape', preventDefault() {{ prevented = true; }}}}); process.stdout.write(JSON.stringify({{open:disclosure.open, focuses:launcher.focuses, prevented}})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == {"open": False, "focuses": 1, "prevented": True} @pytest.mark.anyio async def test_mobile_launch_progressively_discloses_secondary_controls(): html = await dashboard() assert 'class="app-brand"' in html assert 'class="app-live-status"' in html assert 'class="app-menu"' in html assert 'id="app-menu-toggle"' in html assert 'class="work-settings"' in html assert 'id="work-settings-toggle"' in html assert 'id="active-work-queue"' in html assert '' in html assert '' in html service_worker = (Path(__file__).resolve().parents[1] / "frontend" / "service-worker.js").read_text() assert "BASE + 'static/mobile-launch.js'" in service_worker assert "BASE + 'static/mobile-app-shortcuts.js'" in service_worker bootstrap = (Path(__file__).resolve().parents[1] / "frontend" / "dashboard.js").read_text() assert "mobileAppShortcuts.createController" in bootstrap adopted = bootstrap.index( "let adoptedProgressiveSnapshot = contextPoller.adopt(progressiveWorkHandoff?.liveSnapshot);" ) fallback = bootstrap.index("if (!adoptedProgressiveSnapshot) await load();") shortcuts = bootstrap.index("await appShortcut.run();") assert adopted < fallback < shortcuts assert "contextPoller.start();" not in bootstrap assert "mobileLaunch.chooseFilter" in html assert "agenda: counts.agenda" in html assert "mobileLaunch.createDisclosure" in html assert "@media (max-width: 600px)" in html assert "header { min-height:56px; max-height:64px;" in html assert ".work-settings:not([open]) > .work-settings-panel { display:none; }" in html assert ".app-menu:not([open]) > .app-menu-panel { display:none; }" in html assert ".my-work-actions { display:none; }" in html