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" QUEUE_LAUNCHER = Path(__file__).resolve().parents[1] / "frontend" / "mobile-queue-launcher.js" TIMER = Path(__file__).resolve().parents[1] / "frontend" / "today-timer.js" def test_mobile_work_entry_prioritizes_continue_resume_start_plan_then_find(): script = f""" const createEntry = require({json.dumps(str(ENTRY))}); const state = {{active:true, resumable:true, today:2, eligible:3}}; const calls = []; const entry = createEntry({{ isTodayActive: () => state.active, isTodayResumable: () => state.resumable, getTodayCount: () => state.today, getEligibleCount: () => state.eligible, continueToday: () => calls.push('continue'), resumeToday: () => calls.push('resume'), startToday: () => calls.push('start'), planToday: () => calls.push('plan'), findWork: () => calls.push('find'), }}); 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()); state.eligible = 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", "plan", "find"], "calls": ["continue", "resume", "start", "plan", "find"], } def test_mobile_work_entry_preserves_active_today_then_launches_highest_priority_queue(): script = f""" const createEntry = require({json.dumps(str(ENTRY))}); const calls = []; const state = {{active:true, today:2, resumable:true, eligible:3, next:{{name:'attention'}}}}; const entry = createEntry({{ isTodayActive: () => state.active, isTodayResumable: () => state.resumable, getTodayCount: () => state.today, getEligibleCount: () => state.eligible, queueLauncher: {{recommend: () => state.next, open: name => calls.push('queue:' + name)}}, continueToday: () => calls.push('continue'), resumeToday: () => calls.push('resume'), startToday: () => calls.push('start'), planToday: () => calls.push('plan'), findWork: () => calls.push('find'), }}); const modes = [entry.open()]; state.active = false; modes.push(entry.open()); state.next = {{name:'today'}}; modes.push(entry.open()); state.today = 0; state.eligible = 0; state.next = {{name:'update'}}; modes.push(entry.open()); state.next = {{name:'agenda'}}; modes.push(entry.open()); state.next = {{name:'later'}}; modes.push(entry.open()); state.next = {{name:'draft'}}; modes.push(entry.open()); state.next = {{name:'find'}}; 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", "attention", "resume", "update", "agenda", "later", "draft", "find"], "calls": ["continue", "queue:attention", "resume", "queue:update", "queue:agenda", "queue:later", "queue:draft", "find"], } def test_mobile_work_entry_opens_prepare_today_before_direct_queue_work(): script = f""" const createEntry = require({json.dumps(str(ENTRY))}); const calls = []; const state = {{active:true, preparation:{{active:false, next:'attention'}}}}; const entry = createEntry({{ isTodayActive: () => state.active, isTodayResumable: () => false, getTodayCount: () => 0, getEligibleCount: () => 3, getPreparationState: () => state.preparation, queueLauncher: {{recommend: () => ({{name:'attention'}}), open: name => calls.push('queue:' + name)}}, continueToday: () => calls.push('continue'), prepareToday: () => calls.push('prepare'), planToday: () => calls.push('plan'), findWork: () => calls.push('find'), }}); const modes = [entry.open()]; state.active = false; modes.push(entry.open()); state.preparation.active = true; 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", "prepare", "prepare-resume"], "calls": ["continue", "prepare", "prepare"], } def test_mobile_work_entry_plans_eligible_work_before_find_fallback(): script = f""" const createEntry = require({json.dumps(str(ENTRY))}); const calls = []; const entry = createEntry({{ isTodayActive: () => false, isTodayResumable: () => false, getTodayCount: () => 0, getEligibleCount: () => 2, queueLauncher: {{recommend: () => ({{name:'find'}}), open: () => {{}}}}, continueToday: () => {{}}, resumeToday: () => {{}}, startToday: () => {{}}, planToday: () => calls.push('plan'), findWork: () => calls.push('find'), }}); process.stdout.write(JSON.stringify({{mode:entry.open(), calls}})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == {"mode": "plan", "calls": ["plan"]} 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_pauses_before_queues_and_distinguishes_close_from_selection(): script = f""" const createDock = require({json.dumps(str(DOCK))}); class FakeElement {{ constructor() {{ this.listeners = {{}}; this.open = false; this.attributes = {{}}; }} addEventListener(name, callback) {{ this.listeners[name] = callback; }} setAttribute(name, value) {{ this.attributes[name] = value; }} removeAttribute(name) {{ delete this.attributes[name]; }} showModal() {{ this.open = true; }} close() {{ this.open = false; this.listeners.close?.(); }} focus() {{}} }} const nav = new FakeElement(); nav.hidden = false; const queues = new FakeElement(); const close = new FakeElement(); const row = new FakeElement(); const button = new FakeElement(); const calls = []; const dock = createDock({{ nav, buttons:{{queues:button}}, queueSheet:queues, queueClose:close, queueRows:{{update:row}}, overlays:[], detour:()=>({{beginDetour:reason=>calls.push('pause:' + reason), finishDetour:()=>calls.push('return')}}), onSelectQueue:name=>calls.push('select:' + name), observe:()=>({{disconnect(){{}}}}), }}); dock.start(); button.listeners.click({{currentTarget:button}}); close.listeners.click(); button.listeners.click({{currentTarget:button}}); row.listeners.click(); process.stdout.write(JSON.stringify({{calls, open:queues.open}})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "calls": ["pause:queues", "return", "pause:queues", "select:update"], "open": False, } @pytest.mark.anyio async def test_find_and_queue_detours_are_visible_and_wired_to_today_timing(): html = await dashboard() assert html.count('data-today-detour role="status"') == 4 assert html.count('data-return-from-detour type="button"') == 4 assert "detour:() => timerView" in html timer_source = TIMER.read_text() assert "createTodayDetourInterruption" in timer_source assert "queryAll('#find-work-sheet')" in timer_source assert '.today-detour-interruption button { min-height:44px;' in html def test_mobile_task_dock_programmatically_selects_destination_without_running_action(): script = f""" const createDock = require({json.dumps(str(DOCK))}); const calls = []; const makeButton = () => ({{ attributes: {{}}, setAttribute(name, value) {{ this.attributes[name] = value; }}, removeAttribute(name) {{ delete this.attributes[name]; }}, }}); const buttons = {{work:makeButton(), drafts:makeButton()}}; const dock = createDock({{ nav:{{}}, buttons, actions:{{work:()=>calls.push('work'), drafts:()=>calls.push('drafts')}}, }}); const selected = dock.select('drafts'); process.stdout.write(JSON.stringify({{ selected, calls, work:buttons.work.attributes['aria-current'] || null, drafts:buttons.drafts.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) == { "selected": True, "calls": [], "work": 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('plan', 0); const planning = {{ text:workLabel.textContent, label:work.attributes['aria-label'] }}; dock.updateWork('find', 0); process.stdout.write(JSON.stringify({{ continuing, resuming, starting, planning, finding:{{ 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"}, "planning": {"text": "Plan", "label": "Plan Today"}, "finding": {"text": "Find", "label": "Find 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}, } def test_mobile_task_dock_opens_stable_queue_switcher_and_routes_each_queue(): script = f""" const createDock = require({json.dumps(str(DOCK))}); class FakeElement {{ constructor() {{ this.listeners = {{}}; this.attributes = {{}}; this.hidden = false; this.textContent = ''; this.focuses = 0; this.classList = {{contains:()=>false}}; }} addEventListener(name, callback) {{ this.listeners[name] = callback; }} click() {{ return 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 queues = new FakeElement(); const sheet = new FakeElement(); sheet.open = false; sheet.showModal = function () {{ this.open = true; }}; sheet.close = function () {{ this.open = false; this.listeners.close?.(); }}; const close = new FakeElement(); const badge = new FakeElement(); const deadline = new FakeElement(); const rows = Object.fromEntries(['today','agenda','delivery','attention','update','filed','later','draft','recaps'].map(name => [name, new FakeElement()])); const counts = Object.fromEntries(Object.keys(rows).map(name => [name, new FakeElement()])); const selected = []; const utilities = []; const dock = createDock({{ nav, buttons:{{queues}}, actions:{{queues:()=>{{}}}}, overlays:[sheet], queueSheet:sheet, queueClose:close, queueRows:rows, queueCounts:counts, queueBadge:badge, deadlineBadge:deadline, onSelectQueue:(name,row)=>name === 'recaps' ? utilities.push(row === rows.recaps ? 'recaps:trigger' : 'recaps:missing-trigger') : selected.push(name), observe() {{}}, }}); dock.start(); dock.updateQueues({{today:2, agenda:5, delivery:1, attention:1, update:5, filed:2, later:3, draft:4}}); queues.click(); const opened = sheet.open; rows.update.click(); const closedAfterSelect = !sheet.open; queues.click(); rows.recaps.click(); const closedAfterUtility = !sheet.open; queues.click(); close.click(); const populated = {{ badge:badge.textContent, badgeHidden:badge.hidden, deadline:deadline.textContent, deadlineHidden:deadline.hidden, agendaDue:rows.agenda.attributes['data-deadlines'] || null, badgeLabel:queues.attributes['aria-label'], }}; dock.updateQueues({{today:0, agenda:0, attention:0, update:7, later:0, draft:0}}); const updatesOnly = {{ badge:badge.textContent, badgeHidden:badge.hidden, deadlineHidden:deadline.hidden, badgeLabel:queues.attributes['aria-label'], }}; dock.updateQueues({{today:0, agenda:0, attention:0, update:0, later:0, draft:0}}); process.stdout.write(JSON.stringify({{ opened, closedAfterSelect, closedAfterUtility, selected, utilities, badge:badge.textContent, populated, updatesOnly, clearedBadgeHidden:badge.hidden, clearedDeadlineHidden:deadline.hidden, clearedAgendaDue:rows.agenda.attributes['data-deadlines'] || null, clearedBadgeLabel:queues.attributes['aria-label'], counts:Object.fromEntries(Object.entries(counts).map(([name, node]) => [name, node.textContent])), updateLabel:rows.update.attributes['aria-label'], queueFocuses:queues.focuses, columnState: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) == { "opened": True, "closedAfterSelect": True, "closedAfterUtility": True, "selected": ["update"], "utilities": ["recaps:trigger"], "badge": "0 active", "populated": { "badge": "7 active", "badgeHidden": False, "badgeLabel": "Queues: Today 2, Agenda 5 due, Delivery 1, Attention 1, Updates 5, Filed 2, Later 3, Drafts 4; 7 active queues", "deadline": "5 due", "deadlineHidden": False, "agendaDue": "true", }, "updatesOnly": { "badge": "1 active", "badgeHidden": False, "deadlineHidden": True, "badgeLabel": "Queues: Today 0, Agenda 0 due, Delivery 0, Attention 0, Updates 7, Filed 0, Later 0, Drafts 0; 1 active queue", }, "clearedBadgeHidden": True, "clearedDeadlineHidden": True, "clearedAgendaDue": None, "clearedBadgeLabel": "Queues: no active queues; no upcoming deadlines", "counts": {"today": "0", "agenda": "0", "delivery": "0", "attention": "0", "update": "0", "filed": "0", "later": "0", "draft": "0", "recaps": "0"}, "updateLabel": "Updates, 0 unread conversations", "queueFocuses": 3, "columnState": None, } def test_mobile_queue_launcher_opens_first_actionable_item_after_selecting_queue(): script = f""" const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))}); const calls = []; const launcher = createLauncher({{ selectFilter: name => calls.push('filter:' + name), firstAction: name => name === 'attention' ? {{click() {{ calls.push('open:first'); }}}} : null, announce: message => calls.push('announce:' + message), }}); const opened = launcher.open('attention'); const empty = launcher.open('later'); process.stdout.write(JSON.stringify({{opened, empty, calls}})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "opened": "opened", "empty": "empty", "calls": [ "filter:attention", "open:first", "filter:later", "announce:No deferred work is ready to open.", ], } def test_mobile_queue_launcher_uses_dedicated_filed_follow_up_flow(): script = f""" const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))}); const calls = []; const launcher = createLauncher({{ openFiled: () => {{ calls.push('filed-follow-up'); return 'opened-update'; }}, selectFilter: name => calls.push('generic-filter:' + name), firstAction: () => {{ throw new Error('generic card launch must not run'); }}, announce: message => calls.push('announce:' + message), }}); const result = launcher.open('filed'); process.stdout.write(JSON.stringify({{result, calls}})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "result": "opened-update", "calls": ["filed-follow-up"], } def test_mobile_queue_launcher_uses_dedicated_delivery_recovery_flow(): script = f""" const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))}); const calls = []; const launcher = createLauncher({{ openDelivery: () => {{ calls.push('delivery-recovery'); return 'opened'; }}, selectFilter: name => calls.push('generic-filter:' + name), firstAction: () => {{ throw new Error('generic card launch must not run'); }}, }}); const result = launcher.open('delivery'); process.stdout.write(JSON.stringify({{result, calls}})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == {"result": "opened", "calls": ["delivery-recovery"]} def test_mobile_work_prioritizes_and_revalidates_actionable_delivery_recovery(): script = f""" const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))}); const calls = []; let counts = {{delivery:2, attention:3, today:1, update:4}}; const launcher = createLauncher({{ getCounts: () => counts, openDelivery: () => {{ calls.push('delivery-recovery'); return 'opened-delivery'; }}, openUpdates: () => {{ calls.push('updates'); return 'opened-updates'; }}, selectFilter: name => calls.push('filter:' + name), firstAction: () => null, announce: () => {{}}, openFindWork: () => calls.push('find'), }}); const delivery = launcher.recommend(); const opened = launcher.continueWork(); counts = {{delivery:0, attention:0, today:0, update:4}}; const afterRecovery = launcher.recommend(); process.stdout.write(JSON.stringify({{delivery, opened, afterRecovery, calls}})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "delivery": {"name": "delivery", "count": 2, "label": "Recover Delivery (2)"}, "opened": "opened-delivery", "afterRecovery": {"name": "update", "count": 4, "label": "Resume Updates (4)"}, "calls": ["delivery-recovery"], } def test_mobile_work_continues_into_filed_follow_up_before_later_work(): script = f""" const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))}); const calls = []; let counts = {{attention:0, today:0, update:0, agenda:0, filed:2, later:4, draft:3}}; const launcher = createLauncher({{ getCounts: () => counts, openFiled: () => {{ calls.push('filed-follow-up'); return 'opened-filed'; }}, selectFilter: name => calls.push('filter:' + name), firstAction: () => null, announce: () => {{}}, openFindWork: () => calls.push('find'), }}); const recommendation = launcher.recommend(); const opened = launcher.continueWork(); process.stdout.write(JSON.stringify({{recommendation, opened, calls}})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "recommendation": {"name": "filed", "count": 2, "label": "Review Filed (2)"}, "opened": "opened-filed", "calls": ["filed-follow-up"], } def test_mobile_queue_launcher_recommends_and_revalidates_cross_queue_continuation(): script = f""" const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))}); const calls = []; let counts = {{attention: 2, today: 1, agenda: 5, later: 4, draft: 3}}; const launcher = createLauncher({{ openToday: () => calls.push('today'), openAgenda: () => {{ calls.push('agenda'); return 'opened'; }}, selectFilter: name => calls.push('filter:' + name), firstAction: name => (counts[name] || 0) ? {{click() {{ calls.push('open:' + name); }}}} : null, announce: message => calls.push('announce:' + message), getCounts: () => counts, openFindWork: () => calls.push('find'), }}); const first = launcher.recommend(); counts = {{attention: 0, today: 0, agenda: 5, later: 4, draft: 3}}; const agenda = launcher.recommend(); const opened = launcher.continueWork(); counts = {{attention: 0, today: 0, agenda: 0, later: 0, draft: 0}}; const fallback = launcher.recommend(); launcher.continueWork(); process.stdout.write(JSON.stringify({{first, agenda, opened, fallback, calls}})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "first": {"name": "attention", "count": 2, "label": "Start Attention (2)"}, "agenda": {"name": "agenda", "count": 5, "label": "Open Agenda (5)"}, "opened": "opened", "fallback": {"name": "find", "count": 0, "label": "Find Work"}, "calls": ["agenda", "find"], } def test_mobile_queue_launcher_prioritizes_updates_before_agenda_and_resumes_launcher(): script = f""" const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))}); const calls = []; let counts = {{attention:0, today:0, update:6, agenda:2, later:1, draft:1}}; const launcher = createLauncher({{ getCounts: () => counts, openUpdates: () => {{ calls.push('updates'); return 'resumed'; }}, openAgenda: () => calls.push('agenda'), selectFilter: name => calls.push('filter:' + name), firstAction: () => null, announce: () => {{}}, openFindWork: () => calls.push('find'), }}); const recommendation = launcher.recommend(); const opened = launcher.continueWork(); process.stdout.write(JSON.stringify({{recommendation, opened, calls}})); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "recommendation": {"name": "update", "count": 6, "label": "Resume Updates (6)"}, "opened": "resumed", "calls": ["updates"], } def test_mobile_task_dock_labels_each_recommended_work_destination_accessibly(): 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}}); const labels = {{}}; for (const mode of ['delivery','attention','update','agenda','filed','later','draft']) {{ dock.updateWork(mode); labels[mode] = [workLabel.textContent, work.attributes['aria-label']]; }} process.stdout.write(JSON.stringify(labels)); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "delivery": ["Delivery", "Open Delivery"], "attention": ["Attention", "Open Attention"], "update": ["Updates", "Resume Updates"], "agenda": ["Agenda", "Open Agenda"], "filed": ["Filed", "Open Filed"], "later": ["Later", "Open Later"], "draft": ["Drafts", "Open Drafts"], } def test_mobile_task_dock_labels_prepare_today_as_the_primary_action(): 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}}); const labels = {{}}; for (const mode of ['prepare','prepare-resume']) {{ dock.updateWork(mode); labels[mode] = [workLabel.textContent, work.attributes['aria-label']]; }} process.stdout.write(JSON.stringify(labels)); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == { "prepare": ["Prepare", "Prepare Today"], "prepare-resume": ["Resume prep", "Resume preparation"], } @pytest.mark.anyio async def test_dashboard_renders_accessible_mobile_queue_completion_handoff(): html = await dashboard() assert 'id="mobile-queue-heading"' in html assert 'data-mobile-queue="find"' in html assert "mobileQueueLauncher.recommend()" in html assert "showMobileQueueCompletion('Updates')" in html assert "row.setAttribute('data-recommended', 'true')" in html assert "row.focus()" in html @pytest.mark.anyio async def test_dashboard_renders_and_wires_mobile_queue_switcher(): html = await dashboard() assert 'data-mobile-task="queues"' in html assert 'id="mobile-queue-sheet"' in html assert 'aria-labelledby="mobile-queue-heading"' in html assert 'data-mobile-queue="today"' in html assert 'data-mobile-queue="agenda"' in html assert 'AgendaUpcoming deadlines' in html assert 'data-work-filter="agenda"' in html assert "const agendaItems = agendaMyWork(activeMyWork);" in html assert "counts.agenda = agendaItems.length;" in html assert 'data-agenda-group' in html assert 'data-mobile-queue="attention"' in html assert 'data-mobile-queue="delivery"' in html assert 'DeliveryNeeds recovery' in html assert 'data-mobile-queue-count="delivery"' in html assert 'data-mobile-queue="update"' in html assert 'UpdatesUnread conversations' in html assert 'data-mobile-queue="filed"' in html assert 'FiledIssues you delegated' in html assert 'data-mobile-queue-count="filed"' in html assert "openFiled: openFiledFollowUp" in html assert "filedFollowUpTarget(completedFiledReview.visible(lastMyWork))" in html assert 'data-mobile-queue="later"' in html assert 'data-mobile-queue="draft"' in html assert 'data-mobile-queue="recaps"' in html assert 'RecapsHistory' in html assert 'id="mobile-queue-count"' in html assert 'id="mobile-deadline-count"' in html assert 'mobileTaskDock.updateQueues(counts)' in html assert '' in html assert "const mobileQueueLauncher = createMobileQueueLauncher({" in html assert "firstAction: name => qs('#my-work-list .my-work-card-main, #my-work-list .draft-resume, #my-work-list .draft-continue, #my-work-list .draft-edit')" in html assert "name === 'find' ? qs('#find-work').click() : mobileQueueLauncher.open(name)" in html assert '.mobile-queue-sheet' in html assert '.my-work-actions { display:none;' in html assert 'padding-bottom:calc(16px + env(safe-area-inset-bottom))' in html assert '.mobile-task-deadline' in html assert '[data-deadlines="true"]' in html assert '.mobile-task-dock[data-attention="true"]' not in html @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 "getEligibleCount: () => activeMyWork.length" in html assert "continueToday: continueTodaySession" in html assert "resumeToday: resumeTodaySession" in html assert "startToday: startTodaySession" in html assert "planToday: () => openPlanToday(mobileTaskButtons.work)" 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_wires_work_dock_and_app_shortcut_to_live_queue_recommendation(): html = await dashboard() assert "queueLauncher: mobileQueueLauncher" in html assert "work: () => mobileWorkEntry.open()" in html assert "continueWork: () => mobileWorkEntry.open()" in html assert "mobileQueueCounts = counts;" in html assert "mobileTaskDock.updateWork(mobileWorkEntry.mode())" in html @pytest.mark.anyio async def test_dashboard_renders_and_wires_phone_safe_task_dock(): html = await dashboard() assert '