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" 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 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, 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; overlay.classList.values.delete('open'); observerCallback(); buttons.drafts.click(); process.stdout.write(JSON.stringify({{ calls, hiddenWhileOpen, hiddenAfterClose:nav.hidden, 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, "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_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 '