import json import subprocess from pathlib import Path import pytest from tests.dashboard_bundle import dashboard DOCK = Path(__file__).resolve().parents[1] / "frontend" / "mobile-task-dock.js" ENTRY = Path(__file__).resolve().parents[1] / "frontend" / "mobile-work-entry.js" TIMER = Path(__file__).resolve().parents[1] / "frontend" / "today-timer.js" def test_mobile_work_entry_prioritizes_continue_resume_start_then_fallback(): script = f""" const createEntry = require({json.dumps(str(ENTRY))}); const state = {{active:true, resumable:true, today:2}}; const calls = []; const entry = createEntry({{ isTodayActive: () => state.active, isTodayResumable: () => state.resumable, getTodayCount: () => state.today, continueToday: () => calls.push('continue'), resumeToday: () => calls.push('resume'), startToday: () => calls.push('start'), openFallback: () => calls.push('fallback'), }}); const modes = []; modes.push(entry.open()); state.active = false; modes.push(entry.open()); state.resumable = false; modes.push(entry.open()); state.today = 0; modes.push(entry.open()); process.stdout.write(JSON.stringify({{modes, calls}})); """ result = subprocess.run( ["node", "-e", script], capture_output=True, text=True ) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "modes": ["continue", "resume", "start", "work"], "calls": ["continue", "resume", "start", "fallback"], } def test_mobile_task_dock_routes_actions_hides_for_overlays_and_restores_focus(): script = f""" const createDock = require({json.dumps(str(DOCK))}); class FakeElement {{ constructor() {{ this.listeners = {{}}; this.attributes = {{}}; this.classList = {{ values:new Set(), contains:value => this.classList.values.has(value) }}; this.hidden = false; this.focuses = 0; }} addEventListener(name, callback) {{ this.listeners[name] = callback; }} click() {{ this.listeners.click({{currentTarget:this}}); }} setAttribute(name, value) {{ this.attributes[name] = value; }} removeAttribute(name) {{ delete this.attributes[name]; }} focus() {{ this.focuses += 1; }} }} const nav = new FakeElement(); const sessionHud = new FakeElement(); const buttons = Object.fromEntries(['work','attention','find','new','search','drafts'].map(name => [name, new FakeElement()])); buttons.attention.hidden = true; const overlay = new FakeElement(); const calls = []; let observerCallback; const dock = createDock({{ nav, sessionHud, buttons, overlays:[overlay], actions: Object.fromEntries(Object.keys(buttons).map(name => [name, () => calls.push(name)])), observe(callback) {{ observerCallback = callback; return {{disconnect() {{}}}}; }}, }}); dock.start(); buttons.work.click(); buttons.find.click(); overlay.classList.values.add('open'); observerCallback(); const hiddenWhileOpen = nav.hidden; const hudHiddenWhileOpen = sessionHud.attributes['data-overlay-hidden']; overlay.classList.values.delete('open'); observerCallback(); buttons.drafts.click(); process.stdout.write(JSON.stringify({{ calls, hiddenWhileOpen, hiddenAfterClose:nav.hidden, hudHiddenWhileOpen, hudHiddenAfterClose:sessionHud.attributes['data-overlay-hidden'] || null, attentionHidden:buttons.attention.hidden, findFocuses:buttons.find.focuses, current:Object.fromEntries(Object.entries(buttons).map(([name, button]) => [name, button.attributes['aria-current'] || null])), }})); """ result = subprocess.run( ["node", "-e", script], capture_output=True, text=True ) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "calls": ["work", "find", "drafts"], "hiddenWhileOpen": True, "hiddenAfterClose": False, "hudHiddenWhileOpen": "true", "hudHiddenAfterClose": None, "attentionHidden": True, "findFocuses": 1, "current": { "work": None, "attention": None, "find": None, "new": None, "search": None, "drafts": "page", }, } def test_mobile_task_dock_keeps_today_label_separate_from_attention_count(): script = f""" const createDock = require({json.dumps(str(DOCK))}); const work = {{ attributes: {{}}, setAttribute(name, value) {{ this.attributes[name] = value; }} }}; const workLabel = {{ textContent:'' }}; const dock = createDock({{ nav:{{}}, buttons:{{work}}, workLabel }}); dock.updateWork('continue', 3); const continuing = {{ text:workLabel.textContent, label:work.attributes['aria-label'] }}; dock.updateWork('resume', 0); const resuming = {{ text:workLabel.textContent, label:work.attributes['aria-label'] }}; dock.updateWork('start', 0); const starting = {{ text:workLabel.textContent, label:work.attributes['aria-label'] }}; dock.updateWork('work', 0); process.stdout.write(JSON.stringify({{ continuing, resuming, starting, fallback:{{ text:workLabel.textContent, label:work.attributes['aria-label'] }}, }})); """ result = subprocess.run( ["node", "-e", script], capture_output=True, text=True ) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "continuing": { "text": "Continue", "label": "Continue Today", }, "resuming": {"text": "Resume", "label": "Resume Today"}, "starting": {"text": "Start", "label": "Start Today"}, "fallback": {"text": "Work", "label": "Work"}, } def test_mobile_today_hud_completes_current_item_and_labels_final_item_for_recap(): script = f""" const createView = require({json.dumps(str(TIMER))}).createView; class FakeElement {{ constructor() {{ this.listeners = {{}}; this.hidden = true; this.textContent = ''; this.attributes = {{}}; }} addEventListener(name, callback) {{ this.listeners[name] = callback; }} click() {{ return this.listeners.click?.(); }} setAttribute(name, value) {{ this.attributes[name] = value; }} }} const hud = new FakeElement(); const open = new FakeElement(); const toggle = new FakeElement(); const complete = new FakeElement(); const progress = new FakeElement(); const elements = {{ '[data-mobile-today-hud]':[hud], '[data-mobile-today-open]':[open], '[data-mobile-today-toggle]':[toggle], '[data-mobile-today-complete]':[complete], '[data-work-session-progress]':[progress], '[data-work-session-adjust-plan]':[], '[data-work-session-timer-toggle]':[toggle], }}; let active = true; const snapshot = {{identity:'issue:r:1:', elapsed_ms:0, running:true}}; const completed = []; const view = createView({{ timer:{{snapshot:()=>snapshot,totalElapsed:()=>0,pause:()=>true,resume:()=>true}}, isActive:()=>active, queryAll:selector=>elements[selector] || [], formatEstimate:value=>value + 'm', getItem:()=>({{title:'Ship mobile flow'}}), onComplete:identity=>{{ completed.push(identity); return false; }}, }}); view.update({{index:1,total:2}}, null); const next = {{hidden:complete.hidden,label:complete.textContent,aria:complete.attributes['aria-label']}}; complete.click(); const preservedAfterFailure = {{hudHidden:hud.hidden,identity:snapshot.identity}}; view.update({{index:2,total:2}}, null); const recap = {{hidden:complete.hidden,label:complete.textContent}}; active = false; view.render(); process.stdout.write(JSON.stringify({{next,recap,completed,preservedAfterFailure,hiddenWhenInactive:complete.hidden}})); """ result = subprocess.run( ["node", "-e", script], capture_output=True, text=True ) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "next": { "hidden": False, "label": "Done & next", "aria": "Complete Ship mobile flow and open next Today item", }, "recap": {"hidden": False, "label": "Done & recap"}, "completed": ["issue:r:1:"], "preservedAfterFailure": {"hudHidden": False, "identity": "issue:r:1:"}, "hiddenWhenInactive": True, } def test_mobile_task_dock_exposes_independent_attention_action_and_six_column_state(): script = f""" const createDock = require({json.dumps(str(DOCK))}); const nav = {{ attributes: {{}}, setAttribute(name, value) {{ this.attributes[name] = value; }}, removeAttribute(name) {{ delete this.attributes[name]; }} }}; const attention = {{ attributes: {{}}, hidden:true, setAttribute(name, value) {{ this.attributes[name] = value; }} }}; const badge = {{ textContent:'', hidden:true }}; const dock = createDock({{ nav, buttons:{{attention}}, attentionBadge:badge }}); dock.updateAttention(3); const pending = {{ count:badge.textContent, badgeHidden:badge.hidden, actionHidden:attention.hidden, label:attention.attributes['aria-label'], layout:nav.attributes['data-attention'] }}; dock.updateAttention(0); process.stdout.write(JSON.stringify({{ pending, empty:{{ count:badge.textContent, badgeHidden:badge.hidden, actionHidden:attention.hidden, label:attention.attributes['aria-label'], layout:nav.attributes['data-attention'] || null }}, }})); """ result = subprocess.run( ["node", "-e", script], capture_output=True, text=True ) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "pending": {"count": "3", "badgeHidden": False, "actionHidden": False, "label": "Attention, 3 items", "layout": "true"}, "empty": {"count": "0", "badgeHidden": True, "actionHidden": True, "label": "Attention, 0 items", "layout": None}, } @pytest.mark.anyio async def test_dashboard_wires_mobile_work_dock_into_today_session_lifecycle(): html = await dashboard() assert 'id="mobile-work-label">Work' in html assert '' in html assert "const mobileWorkEntry = createMobileWorkEntry({" in html assert "isTodayActive: () => workSession.checkpointed()" in html assert "isTodayResumable: () => workSession.resumable()" in html assert "getTodayCount: () => todayMyWork.length" in html assert "continueToday: continueTodaySession" in html assert "resumeToday: resumeTodaySession" in html assert "startToday: startTodaySession" in html assert "work: () => mobileWorkEntry.open()" in html assert "mobileTaskDock.updateWork(mobileWorkEntry.mode())" in html assert "mobileTaskDock.updateAttention(countMyWork(activeMyWork).attention)" in html assert "workSession.reopen(item)" in html assert "runTodayTransition('continue')" in html @pytest.mark.anyio async def test_dashboard_renders_and_wires_phone_safe_task_dock(): html = await dashboard() assert '