import json import subprocess from pathlib import Path import pytest from tests.dashboard_bundle import dashboard CONTROLLER = Path(__file__).resolve().parents[1] / "frontend" / "mobile-first-task.js" def run_node(body: str) -> dict: script = f""" const createFirstTask = require({json.dumps(str(CONTROLLER))}); class Element {{ constructor() {{ this.listeners = {{}}; this.disabled = false; this.hidden = false; this.open = false; this.textContent = ''; this.focused = false; }} addEventListener(name, callback) {{ this.listeners[name] = callback; }} click() {{ this.listeners.click?.({{preventDefault() {{}}}}); }} showModal() {{ this.open = true; }} close() {{ this.open = false; this.listeners.close?.(); }} focus() {{ this.focused = true; }} }} {body} """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr return json.loads(result.stdout) def test_first_task_activation_is_account_bound_and_completes_only_when_today_is_active(): result = run_node( """ const values = new Map(); const storage = {getItem:key => values.get(key) || null, setItem:(key,value) => values.set(key,value)}; const sheet = new Element(); let login = 'Timmy'; let hasWork = false; let todayActive = false; const controller = createFirstTask({ storage, getLogin:() => login, hasWork:() => hasWork, isTodayActive:() => todayActive, isOnline:() => true, mediaQuery:{matches:true}, sheet, title:new Element(), findButton:new Element(), createButton:new Element(), setupButton:new Element(), closeButton:new Element(), status:new Element(), onFind() {}, onCreate() {}, onSetup() {}, }); const before = [controller.required(), controller.open(), sheet.open]; hasWork = true; const awaitingStart = controller.refresh(); const stillRequired = controller.required(); todayActive = true; const completed = controller.refresh(); const timmyRequired = controller.required(); login = 'alexander'; hasWork = false; todayActive = false; const alexanderRequired = controller.required(); process.stdout.write(JSON.stringify({before, awaitingStart, stillRequired, completed, sheetOpen:sheet.open, timmyRequired, alexanderRequired, values:[...values]})); """ ) assert result == { "before": [True, True, True], "awaitingStart": "awaiting-start", "stillRequired": True, "completed": "completed", "sheetOpen": False, "timmyRequired": False, "alexanderRequired": True, "values": [["stackchain.first-task.v1:timmy", "complete"]], } def test_first_task_activation_resumes_with_finish_starting_guidance(): result = run_node( """ const sheet = new Element(); const title = new Element(); const findButton = new Element(); const createButton = new Element(); const status = new Element(); const controller = createFirstTask({ storage:{getItem:() => null, setItem() {}}, getLogin:() => 'timmy', hasWork:() => true, isTodayActive:() => false, isOnline:() => true, mediaQuery:{matches:true}, sheet, title, findButton, createButton, setupButton:new Element(), closeButton:new Element(), status, onFind() {}, onCreate() {}, onSetup() {}, }); controller.open(); process.stdout.write(JSON.stringify({ required:controller.required(), title:title.textContent, status:status.textContent, find:findButton.textContent, create:createButton.textContent, })); """ ) assert result == { "required": True, "title": "Finish starting your first task", "status": "Your task is ready. Start it from Find or create and start another task.", "find": "Find & start", "create": "Create & start", } def test_first_task_activation_routes_existing_flows_and_keeps_create_available_offline(): result = run_node( """ const calls = []; const sheet = new Element(); const findButton = new Element(); const createButton = new Element(); const setupButton = new Element(); const closeButton = new Element(); const status = new Element(); let online = false; const controller = createFirstTask({ storage:{getItem:() => null, setItem() {}}, getLogin:() => 'timmy', hasWork:() => false, isOnline:() => online, mediaQuery:{matches:true}, sheet, findButton, createButton, setupButton, closeButton, status, onFind:() => calls.push('find'), onCreate:() => calls.push('create'), onSetup:() => calls.push('setup'), }); controller.start(); controller.open(); const offline = {findDisabled:findButton.disabled, createDisabled:createButton.disabled, status:status.textContent, createFocused:createButton.focused}; findButton.click(); createButton.click(); controller.open(); setupButton.click(); controller.open(); online = true; controller.render(); findButton.click(); process.stdout.write(JSON.stringify({offline, calls, sheetOpen:sheet.open})); """ ) assert result == { "offline": { "findDisabled": True, "createDisabled": False, "status": "You are offline. Create a task now and it will stay in Drafts until you reconnect.", "createFocused": True, }, "calls": ["create", "setup", "find"], "sheetOpen": False, } @pytest.mark.anyio async def test_dashboard_renders_and_wires_phone_safe_first_task_activation(): html = await dashboard() assert '' in html assert '

Start your first task

' in html assert 'id="mobile-first-task-find" type="button">Find & start' in html assert 'id="mobile-first-task-create" type="button">Create & start' in html assert 'id="mobile-first-task-setup" type="button">Make this phone work-ready' in html assert '' in html assert "const mobileFirstTask = createMobileFirstTask({" in html assert "isTodayActive: () => workSession.checkpointed()" in html assert "shouldActivate: () => mobileFirstTask.required()" in html assert "openActivation: () => mobileFirstTask.open()" in html assert "mobileFirstTask.refresh()" in html assert '.mobile-first-task { box-sizing:border-box; width:100%;' in html assert 'padding-bottom:calc(16px + env(safe-area-inset-bottom))' in html assert '.mobile-first-task-actions button { min-height:48px;' in html