stackchain-dashboard/tests/test_mobile_task_dock.py
timmy dbabd8640b
All checks were successful
CI / lint (pull_request) Successful in 3m53s
CI / build-release (pull_request) Successful in 7s
CI / browser-journey (pull_request) Successful in 7m39s
CI / release-candidate (pull_request) Has been skipped
feat: personalize adaptive mobile queue priority (Closes #1460)
2026-08-27 07:09:06 +00:00

1405 lines
59 KiB
Python

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"
QUEUE_PRIORITY = Path(__file__).resolve().parents[1] / "frontend" / "mobile-queue-priority.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_queue_priority_persists_complete_account_scoped_routine_order():
script = f"""
const createPriority = require({json.dumps(str(QUEUE_PRIORITY))});
const values = new Map();
const storage = {{
getItem:key => values.has(key) ? values.get(key) : null,
setItem:(key, value) => values.set(key, value),
removeItem:key => values.delete(key),
}};
let login = 'alice';
const priority = createPriority({{storage, getLogin:() => login}});
const original = priority.getOrder();
priority.move('following', -1);
priority.move('following', -1);
priority.move('following', -1);
const alice = priority.getOrder();
login = 'bob';
const bob = priority.getOrder();
login = '';
const anonymous = priority.getOrder();
login = 'alice';
priority.reset();
process.stdout.write(JSON.stringify({{
original, alice, bob, anonymous, reset:priority.getOrder(), keys:Array.from(values.keys()),
}}));
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
default = ["attention", "today", "update", "agenda", "following", "authored", "filed", "later", "draft"]
assert json.loads(result.stdout) == {
"original": default,
"alice": ["attention", "following", "today", "update", "agenda", "authored", "filed", "later", "draft"],
"bob": default,
"anonymous": default,
"reset": default,
"keys": [],
}
def test_mobile_queue_priority_renders_keyboard_controls_and_updates_immediately():
script = f"""
const createPriority = require({json.dumps(str(QUEUE_PRIORITY))});
class Element {{
constructor(tag='div') {{ this.tag=tag; this.children=[]; this.listeners={{}}; this.attributes={{}}; this.disabled=false; this.textContent=''; }}
append(...items) {{ this.children.push(...items); }}
replaceChildren(...items) {{ this.children=[...items]; }}
addEventListener(name, callback) {{ this.listeners[name]=callback; }}
setAttribute(name, value) {{ this.attributes[name]=value; }}
click() {{ this.listeners.click?.(); }}
}}
const values = new Map();
const list = new Element(); const resetButton = new Element('button'); const status = new Element();
const priority = createPriority({{
storage:{{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}},
getLogin:()=>'alice', document:{{createElement:tag=>new Element(tag)}}, list, resetButton, status,
labels:{{attention:'Attention',today:'Today',update:'Updates',agenda:'Agenda',following:'Following',authored:'My PRs',filed:'Filed',later:'Later',draft:'Drafts'}},
}});
priority.start();
for (let index=0; index<3; index += 1) {{
const row = list.children.find(item => item.attributes['data-queue-priority'] === 'following');
row.children[1].children[0].click();
}}
const following = list.children.find(item => item.attributes['data-queue-priority'] === 'following');
process.stdout.write(JSON.stringify({{
order:list.children.map(item => item.attributes['data-queue-priority']),
earlierLabel:following.children[1].children[0].attributes['aria-label'],
laterLabel:following.children[1].children[1].attributes['aria-label'],
status:status.textContent,
}}));
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"order": ["attention", "following", "today", "update", "agenda", "authored", "filed", "later", "draft"],
"earlierLabel": "Move Following earlier",
"laterLabel": "Move Following later",
"status": "Following moved earlier.",
}
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_work_entry_guides_an_empty_first_workspace_before_find():
script = f"""
const createEntry = require({json.dumps(str(ENTRY))});
const calls = [];
let activationRequired = true;
const entry = createEntry({{
isTodayActive: () => false,
isTodayResumable: () => false,
getTodayCount: () => 0,
getEligibleCount: () => 0,
shouldActivate: () => activationRequired,
queueLauncher: {{recommend: () => ({{name:'find'}}), open: () => {{}}}},
openActivation: () => calls.push('activate'),
findWork: () => calls.push('find'),
}});
const modes = [entry.open()];
activationRequired = false;
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": ["activate", "find"],
"calls": ["activate", "find"],
}
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_mobile_queue_sheet_prioritizes_next_active_and_planning_without_duplicate_rows():
html = await dashboard()
assert 'aria-labelledby="mobile-queue-next-heading"' in html
assert 'id="mobile-queue-next-action"' in html
assert 'aria-labelledby="mobile-queue-active-heading"' in html
assert 'id="mobile-queue-active-list"' in html
assert 'aria-labelledby="mobile-queue-planning-heading"' in html
assert 'id="mobile-queue-planning-list"' in html
assert '<details class="mobile-queue-all"' in html
assert 'id="mobile-queue-all-list"' in html
assert "nextAction: qs('#mobile-queue-next-action')" in html
assert "activeList: qs('#mobile-queue-active-list')" in html
assert "mobileQueueLauncher.renderPresentation();" in html
assert "queueCounts.followingUnavailable = status === 'error';\n renderMobileQueuePresentation();" in html
assert "mobileQueueLauncher.continueWork()" in html
for name in ("today", "tomorrow", "week", "agenda", "delivery", "gate", "attention", "update", "following", "filed", "authored", "later", "draft", "find", "recaps"):
assert html.count(f'<button data-mobile-queue="{name}"') == 1
@pytest.mark.anyio
async def test_mobile_queue_priority_is_packaged_account_scoped_and_touch_safe():
html = await dashboard()
assert 'id="mobile-queue-priority"' in html
assert 'id="mobile-queue-priority-list"' in html
assert 'id="reset-mobile-queue-priority"' in html
assert 'id="mobile-queue-priority-status" role="status" aria-live="polite"' in html
assert '<script src="static/mobile-queue-priority.js"></script>' in html
assert "getRoutineOrder: () => mobileQueuePriority?.getOrder()" in html
assert "createMobileQueuePriority({" in html
assert "getLogin: () => confirmedOwnerLogin" in html
assert "mobileQueuePriority.render();\n renderMobileQueuePresentation();" in html
assert ".mobile-queue-priority-controls button { min-height:44px;" in html
@pytest.mark.anyio
async def test_adaptive_mobile_queue_wiring_recommends_again_when_connectivity_changes():
html = await dashboard()
assert "let offlineWorkMode = false;" in html
assert "isOnline: () => !offlineWorkMode" in html
assert (
"offlineWorkMode = value;\n"
" renderMobileQueuePresentation();"
) in html
@pytest.mark.anyio
async def test_following_hydration_refreshes_the_persistent_mobile_queue_census():
html = await dashboard()
assert (
"preparationItems.following = items.filter(item => item.has_unseen_change === true);\n"
" renderMobileQueuePresentation();\n"
" mobileTaskDock.updateQueues(queueCounts);\n"
" mobileStartDay.render();"
) in html
assert (
"queueCounts.followingUnavailable = status === 'error';\n"
" renderMobileQueuePresentation();\n"
" mobileTaskDock.updateQueues(queueCounts);\n"
" mobileStartDay.render();"
) in html
@pytest.mark.anyio
async def test_my_work_refresh_preserves_the_hydrated_following_queue():
html = await dashboard()
assert "counts.following = queueCounts.following;" in html
assert "counts.followingUnavailable = queueCounts.followingUnavailable;" in html
assert "following:preparationItems.following || []," in html
@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"') == 6
assert html.count('data-return-from-detour type="button"') == 6
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','gate','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, gate:2, 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": "8 active",
"badgeHidden": False,
"badgeLabel": "Queues: Today 2, Agenda 5 due, Delivery 1, Human Gates 2, Attention 1, Updates 5, Following 0, Filed 2, My PRs 0, Later 3, Drafts 4; 8 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, Human Gates 0, Attention 0, Updates 7, Following 0, Filed 0, My PRs 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", "gate": "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_task_dock_counts_following_and_authored_work_truthfully():
script = f"""
const createDock = require({json.dumps(str(DOCK))});
const node = () => ({{
textContent:'', hidden:false, attributes:{{}},
setAttribute(name, value) {{ this.attributes[name] = value; }},
removeAttribute(name) {{ delete this.attributes[name]; }},
}});
const queues = node();
const badge = node();
const deadline = node();
const following = node();
const authored = node();
const dock = createDock({{
nav:node(), buttons:{{queues}}, queueBadge:badge, deadlineBadge:deadline,
queueRows:{{following, authored}},
queueCounts:{{following:node(), authored:node()}},
}});
dock.updateQueues({{following:3, authored:2}});
const active = {{
badge:badge.textContent,
badgeHidden:badge.hidden,
summary:queues.attributes['aria-label'],
followingCount:dock ? following.attributes['aria-label'] : null,
}};
dock.updateQueues({{following:0, authored:0}});
process.stdout.write(JSON.stringify({{
active,
cleared:{{
badgeHidden:badge.hidden,
summary:queues.attributes['aria-label'],
followingLabel:following.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) == {
"active": {
"badge": "2 active",
"badgeHidden": False,
"summary": "Queues: Today 0, Agenda 0 due, Delivery 0, Human Gates 0, Attention 0, Updates 0, Following 3, Filed 0, My PRs 2, Later 0, Drafts 0; 2 active queues",
"followingCount": "Following, 3 unseen changes",
},
"cleared": {
"badgeHidden": True,
"summary": "Queues: no active queues; no upcoming deadlines",
"followingLabel": "Following, 0 unseen changes",
},
}
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_existing_human_gate_review_flow():
script = f"""
const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))});
const calls = [];
const launcher = createLauncher({{
openHumanGates: () => {{ calls.push('human-gates'); return 'opened-gates'; }},
selectFilter: name => calls.push('generic-filter:' + name),
firstAction: () => {{ throw new Error('generic card launch must not run'); }},
announce: message => calls.push('announce:' + message),
}});
process.stdout.write(JSON.stringify({{result:launcher.open('gate'), 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-gates",
"calls": ["human-gates"],
}
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, gate:1, 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, gate:1, 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": "gate", "count": 1, "label": "Review Human Gates (1)"},
"calls": ["delivery-recovery"],
}
def test_adaptive_mobile_work_skips_online_only_queues_offline_and_restores_them_online():
script = f"""
const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))});
const calls = [];
let online = false;
const launcher = createLauncher({{
getCounts: () => ({{delivery:2, gate:1, today:3, update:4}}),
isOnline: () => online,
openToday: () => {{ calls.push('today'); return 'opened-today'; }},
selectFilter: name => calls.push('filter:' + name),
firstAction: () => null,
announce: () => {{}},
}});
const offline = launcher.recommend();
const opened = launcher.continueWork();
online = true;
const reconnected = launcher.recommend();
process.stdout.write(JSON.stringify({{offline, opened, reconnected, calls}}));
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"offline": {"name": "today", "count": 3, "label": "Continue Today (3)"},
"opened": "opened-today",
"reconnected": {"name": "delivery", "count": 2, "label": "Recover Delivery (2)"},
"calls": ["today"],
}
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_work_continues_following_then_authored_before_claiming_new_work():
script = f"""
const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))});
const calls = [];
let counts = {{following:3, authored:2, filed:1, later:4}};
const launcher = createLauncher({{
getCounts: () => counts,
openFollowing: () => {{ calls.push('following'); return 'opened-following'; }},
selectFilter: name => calls.push('filter:' + name),
firstAction: name => name === 'authored' ? {{click() {{ calls.push('open:authored'); }}}} : null,
announce: message => calls.push('announce:' + message),
openFindWork: () => calls.push('find'),
}});
const following = launcher.recommend();
const openedFollowing = launcher.continueWork();
counts = {{following:0, authored:2, filed:1, later:4}};
const authored = launcher.recommend();
const openedAuthored = launcher.continueWork();
counts = {{following:0, authored:0, filed:0, later:0}};
const fallback = launcher.recommend();
launcher.continueWork();
process.stdout.write(JSON.stringify({{following, openedFollowing, authored, openedAuthored, fallback, calls}}));
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"following": {"name": "following", "count": 3, "label": "Review Following (3)"},
"openedFollowing": "opened-following",
"authored": {"name": "authored", "count": 2, "label": "Open My PRs (2)"},
"openedAuthored": "opened",
"fallback": {"name": "find", "count": 0, "label": "Find Work"},
"calls": ["following", "filter:authored", "open:authored", "find"],
}
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_applies_routine_priority_without_demoting_safety_queues():
script = f"""
const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))});
let online = true;
const launcher = createLauncher({{
getCounts:() => ({{delivery:1, gate:2, attention:3, following:4, authored:5}}),
isOnline:() => online,
getRoutineOrder:() => ['following','authored','attention','today','update','agenda','filed','later','draft'],
}});
const onlineView = launcher.presentation();
online = false;
const offlineView = launcher.presentation();
process.stdout.write(JSON.stringify({{
onlineNext:onlineView.nextUp.name,
onlineActive:onlineView.active.map(item => item.name),
offlineNext:offlineView.nextUp.name,
offlineActive:offlineView.active.map(item => item.name),
}}));
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"onlineNext": "delivery",
"onlineActive": ["delivery", "gate", "following", "authored", "attention"],
"offlineNext": "following",
"offlineActive": ["delivery", "gate", "following", "authored", "attention"],
}
def test_mobile_queue_launcher_uses_one_adaptive_action_for_daily_preparation():
script = f"""
const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))});
const calls = [];
let preparation = {{active:true, total:2, label:'Review Human Gates'}};
const nextAction = {{textContent:'', dataset:{{}}, attributes:{{}}, setAttribute(name, value) {{ this.attributes[name]=value; }}}};
const launcher = createLauncher({{
getCounts: () => ({{delivery:4}}),
getPreparation: () => preparation,
openPreparation: () => calls.push('prepare'),
nextAction,
}});
launcher.renderPresentation();
const resumed = [nextAction.textContent, nextAction.dataset.queue, nextAction.attributes['aria-label']];
launcher.continueWork();
preparation = {{active:false, total:2, label:'Review Agenda'}};
launcher.renderPresentation();
const started = [nextAction.textContent, nextAction.dataset.queue, nextAction.attributes['aria-label']];
launcher.continueWork();
process.stdout.write(JSON.stringify({{resumed, started, calls}}));
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"resumed": [
"Resume preparation · Review Human Gates",
"prepare",
"Start or continue: Resume preparation · Review Human Gates",
],
"started": [
"Start day · Review Agenda",
"prepare",
"Start or continue: Start day · Review Agenda",
],
"calls": ["prepare", "prepare"],
}
def test_mobile_queue_launcher_builds_truthful_next_up_and_active_sections():
script = f"""
const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))});
const counts = {{delivery:1, gate:2, today:3, authored:4, update:0, attentionUnavailable:true, followingUnavailable:true}};
const launcher = createLauncher({{getCounts:() => counts}});
process.stdout.write(JSON.stringify(launcher.presentation()));
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"nextUp": {"name": "delivery", "count": 1, "label": "Recover Delivery (1)"},
"active": [
{"name": "delivery", "count": 1},
{"name": "gate", "count": 2},
{"name": "today", "count": 3},
{"name": "authored", "count": 4},
{"name": "attention", "unavailable": True},
{"name": "following", "unavailable": True},
],
"planning": ["today", "tomorrow", "week"],
"all": [
"today", "tomorrow", "week", "agenda", "delivery", "gate",
"attention", "update", "following", "filed", "authored", "later",
"draft", "find", "recaps",
],
}
def test_mobile_queue_launcher_renders_one_set_of_rows_into_adaptive_groups():
script = f"""
const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))});
class Box {{
constructor(name) {{ this.name=name; this.children=[]; this.hidden=false; this.attributes={{}}; this.textContent=''; this.dataset={{}}; }}
append(row) {{ if (row.parent) row.parent.children=row.parent.children.filter(item => item !== row); this.children.push(row); row.parent=this; }}
setAttribute(name, value) {{ this.attributes[name]=value; }}
removeAttribute(name) {{ delete this.attributes[name]; }}
}}
const names=['today','tomorrow','week','agenda','delivery','gate','attention','update','following','filed','authored','later','draft','find','recaps'];
const rows=Object.fromEntries(names.map(name => [name,new Box(name)]));
const nextAction=new Box('next');
const activeList=new Box('active'); const planningList=new Box('planning'); const allList=new Box('all');
const activeSection=new Box('active-section');
const launcher=createLauncher({{
getCounts:()=>({{delivery:1,gate:2,today:3,attentionUnavailable:true}}), rows,
nextAction, activeList, planningList, allList, activeSection,
}});
launcher.renderPresentation();
process.stdout.write(JSON.stringify({{
next:[nextAction.textContent,nextAction.dataset.queue,nextAction.attributes['aria-label']],
active:activeList.children.map(row => [row.name,row.attributes['data-unavailable'] || null]),
planning:planningList.children.map(row => row.name),
all:allList.children.map(row => row.name), activeHidden:activeSection.hidden,
}}));
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
"next": ["Recover Delivery (1)", "delivery", "Next up: Recover Delivery (1)"],
"active": [["delivery", None], ["gate", None], ["attention", "true"]],
"planning": ["today", "tomorrow", "week"],
"all": ["agenda", "update", "following", "filed", "authored", "later", "draft", "find", "recaps"],
"activeHidden": False,
}
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','following','authored','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"],
"following": ["Following", "Open Following"],
"authored": ["My PRs", "Open My PRs"],
"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 '<strong>Agenda</strong><small>Upcoming deadlines</small>' 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 '<strong>Delivery</strong><small>Needs recovery</small>' in html
assert 'data-mobile-queue-count="delivery"' in html
assert 'data-mobile-queue="update"' in html
assert '<strong>Updates</strong><small>Unread conversations</small>' in html
assert 'data-mobile-queue="filed"' in html
assert '<strong>Filed</strong><small>Issues you delegated</small>' 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 '<strong>Recaps</strong><small>History</small>' in html
assert 'id="mobile-queue-count"' in html
assert 'id="mobile-deadline-count"' in html
assert 'mobileTaskDock.updateQueues(counts)' in html
assert '<script src="static/mobile-queue-launcher.js"></script>' 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() :" in html
assert "queueCounts.followingUnavailable ? followingQueue.open()" 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</span>' in html
assert '<script src="static/mobile-work-entry.js"></script>' 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 "queueCounts = 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 '<nav class="mobile-task-dock" id="mobile-task-dock" aria-label="Primary tasks">' in html
assert html.count('class="mobile-task-action" data-mobile-task=') == 5
for task in ("work", "find", "new", "search", "queues"):
assert f'data-mobile-task="{task}"' in html
assert 'id="mobile-queue-count"' in html
assert 'data-work-filter="attention"' in html
assert '.mobile-task-dock { display:none;' in html
assert 'grid-template-columns:repeat(5,minmax(0,1fr))' in html
assert 'padding-bottom:env(safe-area-inset-bottom)' in html
assert '.mobile-task-action { min-width:0; min-height:44px;' in html
assert '<script src="static/mobile-task-dock.js"></script>' in html
assert "createMobileTaskDock({" in html
assert "work: () => mobileWorkEntry.open()" in html
assert "name === 'find' ? qs('#find-work').click() :" in html
assert "queueCounts.followingUnavailable ? followingQueue.open()" in html
assert "qs('[data-work-filter=\"' + name + '\"]').click()" in html
assert "find: () => qs('#find-work').click()" in html
assert "new: () => qs('#new-issue').click()" in html
assert "search: () => qs('#open-palette').click()" in html
assert "detour:() => timerView" in html
assert "mobileTaskDock.updateQueues(counts)" in html
assert "mobileTaskDock.updateWork(mobileWorkEntry.mode())" in html
assert "mobileTaskDock.updateAttention(countMyWork(activeMyWork).attention)" in html
@pytest.mark.anyio
async def test_mobile_update_reader_keeps_primary_decisions_within_thumb_reach():
html = await dashboard()
assert 'class="update-decision-bar" aria-label="Update decisions"' in html
assert 'id="keep-update-unread" type="button" aria-label="Keep unread and open next update"' in html
assert 'id="mark-update-read-next" type="button" aria-label="Mark read and open next update"' in html
assert 'id="focus-update-reply" type="button">Reply</button>' in html
assert 'class="update-more-actions"' in html
assert "qs('#focus-update-reply').addEventListener('click'" in html
assert "qs('#update-reply').focus({ preventScroll:true })" in html
assert '.update-decision-bar { position:fixed;' in html
assert 'padding-bottom:calc(8px + env(safe-area-inset-bottom))' in html
assert '.update-decision-bar button, .update-decision-bar summary { min-height:44px;' in html
assert 'padding-bottom:calc(82px + env(safe-area-inset-bottom))' in html
@pytest.mark.anyio
async def test_dashboard_renders_phone_safe_today_session_hud_above_task_dock():
html = await dashboard()
assert 'class="mobile-today-hud" data-mobile-today-hud' in html
assert 'data-mobile-today-open' in html
assert 'data-work-session-progress' in html
assert 'data-work-session-timer-toggle' in html
assert 'data-work-session-adjust-plan' in html
assert 'data-mobile-today-complete' in html
assert 'Done &amp; next' in html
assert "getItem: identity => [...todayMyWork, ...activeMyWork].find" in html
assert "onReopen: identity =>" in html
assert "onComplete: identity =>" in html
assert "completeTodayItem(item)" in html
assert "workSession.reopen(item)" in html
assert '.mobile-today-hud { display:none;' in html
assert 'data-mobile-today-more' in html
assert 'class="mobile-today-actions"' in html
assert 'grid-template-columns:repeat(6,minmax(0,1fr));' in html
assert '[data-mobile-today-complete] { grid-column:1 / 3;' in html
assert 'bottom:calc(56px + env(safe-area-inset-bottom))' in html
assert '.mobile-today-hud button { min-width:0; min-height:44px;' in html
assert 'var(--mobile-today-clearance, 166px)' in html
assert '.mobile-task-action[hidden] { display:none;' in html
assert 'max-width:100%;' in html
assert 'overflow:hidden;' in html
@pytest.mark.anyio
async def test_mobile_attention_pauses_today_and_offers_phone_safe_return():
html = await dashboard()
assert 'class="attention-interruption" id="attention-interruption"' in html
assert 'id="return-to-today" type="button">Return to Today</button>' in html
assert "if (name === 'attention' && workSession.checkpointed()) timer.beginAttention();" in html
assert 'timer.returnFromAttention()' in html
assert "workSession.reopen(item)" in html
assert "attentionInterruption.hidden = !pending" in html
assert '.attention-interruption:not([hidden]) {' in html
assert '.attention-interruption button { min-height:44px;' in html
assert 'max-width:100%;' in html