feat: guide first mobile task activation (Closes #1140)
All checks were successful
CI / lint (pull_request) Successful in 3m8s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 3m29s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy 2026-08-19 17:33:26 +00:00
parent 8731682090
commit 304e4a8a24
11 changed files with 343 additions and 1 deletions

View File

@ -1169,6 +1169,15 @@ textarea { resize: vertical; min-height: 120px; }
.mobile-task-dock { position:fixed; inset:auto 0 0; z-index:45; display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); gap:2px; padding:6px 8px; padding-bottom:env(safe-area-inset-bottom); border-top:1px solid #2a496e; background:rgba(11,21,38,.98); backdrop-filter:blur(12px); } .mobile-task-dock { position:fixed; inset:auto 0 0; z-index:45; display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); gap:2px; padding:6px 8px; padding-bottom:env(safe-area-inset-bottom); border-top:1px solid #2a496e; background:rgba(11,21,38,.98); backdrop-filter:blur(12px); }
.mobile-queue-sheet { width:100%; max-width:none; margin:auto 0 0; padding:0; border:0; border-radius:18px 18px 0 0; color:var(--text); background:#102641; } .mobile-queue-sheet { width:100%; max-width:none; margin:auto 0 0; padding:0; border:0; border-radius:18px 18px 0 0; color:var(--text); background:#102641; }
.mobile-first-task { box-sizing:border-box; width:100%; max-width:none; max-height:100dvh; margin:auto 0 0; padding:0; border:0; border-radius:18px 18px 0 0; color:var(--text); background:#102641; }
.mobile-first-task::backdrop { background:rgba(3,9,18,.78); }
.mobile-first-task-panel { display:grid; gap:14px; padding:16px; padding-bottom:calc(16px + env(safe-area-inset-bottom)); }
.mobile-first-task-panel header { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; }
.mobile-first-task-panel h2, .mobile-first-task-panel p { margin:0; }
.mobile-first-task-actions { display:grid; gap:10px; }
.mobile-first-task-actions button { min-height:48px; width:100%; }
.mobile-first-task-actions button:disabled { opacity:.55; }
.mobile-queue-sheet::backdrop { background:rgba(3,9,18,.7); } .mobile-queue-sheet::backdrop { background:rgba(3,9,18,.7); }
.mobile-queue-panel { padding:16px; padding-bottom:calc(16px + env(safe-area-inset-bottom)); } .mobile-queue-panel { padding:16px; padding-bottom:calc(16px + env(safe-area-inset-bottom)); }
.mobile-queue-panel header { display:flex; align-items:center; justify-content:space-between; gap:12px; } .mobile-queue-panel header { display:flex; align-items:center; justify-content:space-between; gap:12px; }

View File

@ -140,6 +140,11 @@
if (!sheet.open) qs('#mobile-queue-sheet').showModal(); if (!sheet.open) qs('#mobile-queue-sheet').showModal();
qs('#mobile-start-day-action').focus(); qs('#mobile-start-day-action').focus();
} }
const mobileFirstTask = createMobileFirstTask({
getLogin: () => confirmedOwnerLogin,
hasWork: () => todayMyWork.length > 0 || activeMyWork.length > 0,
});
mobileFirstTask.start();
const mobileWorkEntry = createMobileWorkEntry({ const mobileWorkEntry = createMobileWorkEntry({
isTodayActive: () => workSession.checkpointed(), isTodayActive: () => workSession.checkpointed(),
isTodayResumable: () => workSession.resumable(), isTodayResumable: () => workSession.resumable(),
@ -152,6 +157,8 @@
startToday: startTodaySession, startToday: startTodaySession,
planToday: () => openPlanToday(mobileTaskButtons.work), planToday: () => openPlanToday(mobileTaskButtons.work),
prepareToday: openMobileStartDay, prepareToday: openMobileStartDay,
shouldActivate: () => mobileFirstTask.required(),
openActivation: () => mobileFirstTask.open(),
findWork: () => qs('#find-work').click(), findWork: () => qs('#find-work').click(),
}); });
const mobileStartDay = createMobileStartDay({ const mobileStartDay = createMobileStartDay({
@ -3225,6 +3232,7 @@
}); });
mobileStartDay.render(); mobileStartDay.render();
mobileTaskDock.updateQueues(counts); mobileTaskDock.updateQueues(counts);
mobileFirstTask.refresh();
mobileTaskDock.updateWork(mobileWorkEntry.mode()); mobileTaskDock.updateWork(mobileWorkEntry.mode());
mobileTaskDock.updateAttention(countMyWork(activeMyWork).attention); mobileTaskDock.updateAttention(countMyWork(activeMyWork).attention);
const activeQueue = qs('[data-work-filter="' + selectedWorkFilter + '"]'); const activeQueue = qs('[data-work-filter="' + selectedWorkFilter + '"]');

View File

@ -1829,6 +1829,22 @@
</section> </section>
</dialog> </dialog>
<dialog class="mobile-first-task" id="mobile-first-task" aria-labelledby="mobile-first-task-title">
<section class="mobile-first-task-panel">
<header>
<div><p class="small muted">Welcome to Work</p><h2 id="mobile-first-task-title">Start your first task</h2></div>
<button id="close-mobile-first-task" type="button">Close</button>
</header>
<p class="small">Choose work, start it, then return to Work whenever you want to continue.</p>
<p id="mobile-first-task-status" class="small" role="status" aria-live="polite"></p>
<div class="mobile-first-task-actions">
<button id="mobile-first-task-find" type="button">Find a task</button>
<button id="mobile-first-task-create" type="button">Create a task</button>
<button id="mobile-first-task-setup" type="button">Make this phone work-ready</button>
</div>
</section>
</dialog>
<nav class="mobile-task-dock" id="mobile-task-dock" aria-label="Primary tasks"> <nav class="mobile-task-dock" id="mobile-task-dock" aria-label="Primary tasks">
<button class="mobile-task-action" data-mobile-task="work" type="button" aria-current="page"><span id="mobile-work-label">Work</span></button> <button class="mobile-task-action" data-mobile-task="work" type="button" aria-current="page"><span id="mobile-work-label">Work</span></button>
<button class="mobile-task-action" data-mobile-task="find" type="button">Find</button> <button class="mobile-task-action" data-mobile-task="find" type="button">Find</button>
@ -1933,6 +1949,7 @@
<script src="static/mobile-today-command-bar.js"></script> <script src="static/mobile-today-command-bar.js"></script>
<script src="static/mobile-task-dock.js"></script> <script src="static/mobile-task-dock.js"></script>
<script src="static/mobile-pull-refresh.js"></script> <script src="static/mobile-pull-refresh.js"></script>
<script src="static/mobile-first-task.js"></script>
<script src="static/mobile-work-entry.js"></script> <script src="static/mobile-work-entry.js"></script>
<script src="static/mobile-queue-launcher.js"></script> <script src="static/mobile-queue-launcher.js"></script>
<script src="static/mobile-delivery-recovery.js"></script> <script src="static/mobile-delivery-recovery.js"></script>

View File

@ -0,0 +1,95 @@
(function (root, factory) {
if (typeof module === 'object' && module.exports) module.exports = factory;
else root.createMobileFirstTask = factory;
})(typeof self !== 'undefined' ? self : this, function createMobileFirstTask(options) {
const doc = typeof document !== 'undefined' ? document : null;
const win = typeof window !== 'undefined' ? window : null;
options = Object.assign({
storage: typeof localStorage !== 'undefined' ? localStorage : null,
isOnline: () => typeof navigator === 'undefined' || navigator.onLine,
mediaQuery: win?.matchMedia('(max-width: 600px)') || {matches:false},
eventTarget: win,
sheet: doc?.querySelector('#mobile-first-task'),
findButton: doc?.querySelector('#mobile-first-task-find'),
createButton: doc?.querySelector('#mobile-first-task-create'),
setupButton: doc?.querySelector('#mobile-first-task-setup'),
closeButton: doc?.querySelector('#close-mobile-first-task'),
status: doc?.querySelector('#mobile-first-task-status'),
onFind: () => doc?.querySelector('#find-work')?.click(),
onCreate: () => doc?.querySelector('#new-issue')?.click(),
onSetup: () => doc?.querySelector('#open-device-setup')?.click(),
}, options);
const prefix = 'stackchain.first-task.v1:';
function account() {
return String(options.getLogin?.() || '').trim().toLowerCase();
}
function key() {
const login = account();
return login ? prefix + login : '';
}
function completed() {
const currentKey = key();
if (!currentKey) return false;
try { return options.storage.getItem(currentKey) === 'complete'; }
catch (_error) { return false; }
}
function required() {
return Boolean(options.mediaQuery.matches && account() && !options.hasWork() && !completed());
}
function markComplete() {
const currentKey = key();
if (!currentKey) return;
try { options.storage.setItem(currentKey, 'complete'); }
catch (_error) {}
}
function render() {
const online = options.isOnline();
options.findButton.disabled = !online;
options.status.textContent = online ?
'Choose a task to claim or create one of your own.' :
'You are offline. Create a task now and it will stay in Drafts until you reconnect.';
}
function open() {
if (!required()) return false;
render();
if (!options.sheet.open) options.sheet.showModal();
(options.findButton.disabled ? options.createButton : options.findButton).focus();
return true;
}
function refresh() {
if (account() && options.hasWork() && !completed()) {
markComplete();
if (options.sheet.open) options.sheet.close();
return 'completed';
}
if (options.sheet.open) render();
return required() ? 'required' : 'inactive';
}
function handoff(callback, requiresOnline = false) {
if (requiresOnline && !options.isOnline()) return;
if (options.sheet.open) options.sheet.close();
callback();
}
function start() {
options.findButton.addEventListener('click', () => handoff(options.onFind, true));
options.createButton.addEventListener('click', () => handoff(options.onCreate));
options.setupButton.addEventListener('click', () => handoff(options.onSetup));
options.closeButton.addEventListener('click', () => {
if (options.sheet.open) options.sheet.close();
});
options.eventTarget?.addEventListener('online', render);
options.eventTarget?.addEventListener('offline', render);
}
return {required, open, refresh, render, start};
});

View File

@ -12,6 +12,7 @@
if (options.getTodayCount() > 0 && options.isTodayResumable()) return 'resume'; if (options.getTodayCount() > 0 && options.isTodayResumable()) return 'resume';
if (options.getTodayCount() > 0) return 'start'; if (options.getTodayCount() > 0) return 'start';
if (options.getEligibleCount() > 0) return 'plan'; if (options.getEligibleCount() > 0) return 'plan';
if (options.shouldActivate?.()) return 'activate';
return 'find'; return 'find';
} }
@ -22,6 +23,7 @@
else if (current === 'resume') options.resumeToday(); else if (current === 'resume') options.resumeToday();
else if (current === 'start') options.startToday(); else if (current === 'start') options.startToday();
else if (current === 'plan') options.planToday(); else if (current === 'plan') options.planToday();
else if (current === 'activate') options.openActivation();
else if (current === 'find') options.findWork(); else if (current === 'find') options.findWork();
else options.queueLauncher.open(current); else options.queueLauncher.open(current);
return current; return current;

View File

@ -113,6 +113,7 @@ const SHELL = [
BASE + 'static/live-data-status.js', BASE + 'static/live-data-status.js',
BASE + 'static/mobile-today-command-bar.js', BASE + 'static/mobile-today-command-bar.js',
BASE + 'static/mobile-task-dock.js', BASE + 'static/mobile-task-dock.js',
BASE + 'static/mobile-first-task.js',
BASE + 'static/mobile-work-entry.js', BASE + 'static/mobile-work-entry.js',
BASE + 'static/mobile-queue-launcher.js', BASE + 'static/mobile-queue-launcher.js',
BASE + 'static/mobile-delivery-recovery.js', BASE + 'static/mobile-delivery-recovery.js',

View File

@ -35,7 +35,7 @@ FEATURE_SOURCES = {
"security-center": ("static/security-center.js",), "security-center": ("static/security-center.js",),
"today-timer": ( "today-timer": (
"static/conversation.js", "static/widgets.js", "static/voice-transcript-store.js", "static/voice-conversation-capture.js", "static/mobile-launch.js", "static/mobile-insights.js", "static/mobile-app-shortcuts.js", "static/mobile-plan-today-nav.js", "static/mobile-find-work-nav.js", "static/mobile-pull-refresh.js", "static/live-data-status.js", "static/conversation.js", "static/widgets.js", "static/voice-transcript-store.js", "static/voice-conversation-capture.js", "static/mobile-launch.js", "static/mobile-insights.js", "static/mobile-app-shortcuts.js", "static/mobile-plan-today-nav.js", "static/mobile-find-work-nav.js", "static/mobile-pull-refresh.js", "static/live-data-status.js",
"static/today-completion.js", "static/card-planning.js", "static/work-detail-position.js", "static/work-route.js", "static/commands.js", "static/saved-searches.js", "static/task-overlay-history.js", "static/search-preview.js", "static/mobile-search-preview-nav.js", "static/search-reply-draft-store.js", "static/conversation-reply-draft-store.js", "static/conversation-photo-drafts.js", "static/search-defer.js", "static/mobile-search-viewport.js", "static/agenda-replan.js", "static/agenda-calendar.js", "static/my-work.js", "static/protect-today.js", "static/mobile-today-command-bar.js", "static/mobile-task-dock.js", "static/mobile-work-entry.js", "static/mobile-queue-launcher.js", "static/mobile-delivery-recovery.js", "static/mobile-start-day.js", "static/update-triage-session.js", "static/update-review-handoff.js", "static/update-triage-launcher.js", "static/update-triage-gesture.js", "static/notification-undo.js", "static/today-timer.js", "static/today-break.js", "static/today-progress.js", "static/today-lock-screen.js", "static/today-session-sync.js", "static/today-recap.js", "static/today-wrap-up.js", "static/today-summary.js", "static/today-handoff.js", "static/today-completion.js", "static/card-planning.js", "static/work-detail-position.js", "static/work-route.js", "static/commands.js", "static/saved-searches.js", "static/task-overlay-history.js", "static/search-preview.js", "static/mobile-search-preview-nav.js", "static/search-reply-draft-store.js", "static/conversation-reply-draft-store.js", "static/conversation-photo-drafts.js", "static/search-defer.js", "static/mobile-search-viewport.js", "static/agenda-replan.js", "static/agenda-calendar.js", "static/my-work.js", "static/protect-today.js", "static/mobile-today-command-bar.js", "static/mobile-task-dock.js", "static/mobile-first-task.js", "static/mobile-work-entry.js", "static/mobile-queue-launcher.js", "static/mobile-delivery-recovery.js", "static/mobile-start-day.js", "static/update-triage-session.js", "static/update-review-handoff.js", "static/update-triage-launcher.js", "static/update-triage-gesture.js", "static/notification-undo.js", "static/today-timer.js", "static/today-break.js", "static/today-progress.js", "static/today-lock-screen.js", "static/today-session-sync.js", "static/today-recap.js", "static/today-wrap-up.js", "static/today-summary.js", "static/today-handoff.js",
"static/today-rollover.js", "static/later-work.js", "static/detail-defer.js", "static/later-picker.js", "static/drafts.js", "static/unfiled-captures.js", "static/unfiled-draft-sync.js", "static/today-rollover.js", "static/later-work.js", "static/detail-defer.js", "static/later-picker.js", "static/drafts.js", "static/unfiled-captures.js", "static/unfiled-draft-sync.js",
"static/assign-and-start.js", "static/filed-claim.js", "static/queue-today.js", "static/create-and-start.js", "static/assign-and-start.js", "static/filed-claim.js", "static/queue-today.js", "static/create-and-start.js",
"static/draft-filing-session.js", "static/draft-capacity-dialog.js", "static/work-selection.js", "static/draft-filing-session.js", "static/draft-capacity-dialog.js", "static/work-selection.js",

View File

@ -16,6 +16,61 @@ from fake_gitea import AVAILABLE_ISSUES, FakeGiteaServer
from test_mobile_offline_issue_release import ACCESS_TOKEN, ROOT, release_server from test_mobile_offline_issue_release import ACCESS_TOKEN, ROOT, release_server
@pytest.mark.parametrize(("width", "height"), [(390, 844)])
def test_release_artifact_guides_an_empty_mobile_account_to_first_work(
tmp_path: Path, width: int, height: int
):
archives = sorted((ROOT / "dist").glob("stackchain-dashboard-*.tar.gz"))
assert len(archives) == 1, "browser job must download exactly one assembled release archive"
fake = FakeGiteaServer(("127.0.0.1", 0))
fake.assigned_issue_numbers = []
fake_thread = threading.Thread(target=fake.serve_forever, daemon=True)
fake_thread.start()
fake_url = f"http://127.0.0.1:{fake.server_port}"
try:
with release_server(archives[0], tmp_path, fake_url) as origin, sync_playwright() as playwright:
browser = playwright.chromium.launch(args=["--ignore-certificate-errors"])
context = browser.new_context(
viewport={"width": width, "height": height}, ignore_https_errors=True
)
page = context.new_page()
page.goto(origin + "/", wait_until="networkidle")
page.locator('input[name="device_label"]').fill("First task release phone")
page.locator('input[name="access_token"]').fill(ACCESS_TOKEN)
page.locator("#submit-sign-in").click()
page.wait_for_url(origin + "/", wait_until="networkidle")
expect(page.locator("#my-work-status")).to_contain_text("No assigned work")
page.locator('[data-mobile-task="work"]').click()
sheet = page.locator("#mobile-first-task")
expect(sheet).to_be_visible()
expect(page.locator("#mobile-first-task-find")).to_be_focused()
for selector in ("#mobile-first-task-find", "#mobile-first-task-create", "#mobile-first-task-setup"):
bounds = page.locator(selector).bounding_box()
assert bounds and bounds["height"] >= 44
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
page.locator("#mobile-first-task-find").click()
expect(sheet).to_be_hidden()
expect(page.locator("#find-work-sheet")).to_be_visible()
page.locator("#close-find-work").click()
context.set_offline(True)
page.locator('[data-mobile-task="work"]').click()
expect(sheet).to_be_visible()
expect(page.locator("#mobile-first-task-find")).to_be_disabled()
expect(page.locator("#mobile-first-task-status")).to_contain_text("Create a task now")
expect(page.locator("#mobile-first-task-create")).to_be_enabled()
expect(page.locator("#mobile-first-task-create")).to_be_focused()
browser.close()
finally:
fake.shutdown()
fake.server_close()
fake_thread.join(timeout=5)
@pytest.mark.parametrize(("width", "height"), [(320, 568), (390, 844)]) @pytest.mark.parametrize(("width", "height"), [(320, 568), (390, 844)])
def test_release_artifact_bootstraps_mobile_home_and_returns_from_insights( def test_release_artifact_bootstraps_mobile_home_and_returns_from_insights(
tmp_path: Path, width: int, height: int tmp_path: Path, width: int, height: int

View File

@ -0,0 +1,125 @@
import json
import subprocess
from pathlib import Path
import pytest
from tests.dashboard_bundle import dashboard
CONTROLLER = Path(__file__).resolve().parents[1] / "frontend" / "mobile-first-task.js"
def run_node(body: str) -> dict:
script = f"""
const createFirstTask = require({json.dumps(str(CONTROLLER))});
class Element {{
constructor() {{ this.listeners = {{}}; this.disabled = false; this.hidden = false; this.open = false; this.textContent = ''; this.focused = false; }}
addEventListener(name, callback) {{ this.listeners[name] = callback; }}
click() {{ this.listeners.click?.({{preventDefault() {{}}}}); }}
showModal() {{ this.open = true; }}
close() {{ this.open = false; this.listeners.close?.(); }}
focus() {{ this.focused = true; }}
}}
{body}
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
return json.loads(result.stdout)
def test_first_task_activation_is_account_bound_and_completes_when_work_appears():
result = run_node(
"""
const values = new Map();
const storage = {getItem:key => values.get(key) || null, setItem:(key,value) => values.set(key,value)};
const sheet = new Element();
let login = 'Timmy';
let hasWork = false;
const controller = createFirstTask({
storage, getLogin:() => login, hasWork:() => hasWork, isOnline:() => true,
mediaQuery:{matches:true}, sheet, findButton:new Element(), createButton:new Element(),
setupButton:new Element(), closeButton:new Element(), status:new Element(),
onFind() {}, onCreate() {}, onSetup() {},
});
const before = [controller.required(), controller.open(), sheet.open];
hasWork = true;
const completed = controller.refresh();
const timmyRequired = controller.required();
login = 'alexander'; hasWork = false;
const alexanderRequired = controller.required();
process.stdout.write(JSON.stringify({before, completed, sheetOpen:sheet.open, timmyRequired, alexanderRequired, values:[...values]}));
"""
)
assert result == {
"before": [True, True, True],
"completed": "completed",
"sheetOpen": False,
"timmyRequired": False,
"alexanderRequired": True,
"values": [["stackchain.first-task.v1:timmy", "complete"]],
}
def test_first_task_activation_routes_existing_flows_and_keeps_create_available_offline():
result = run_node(
"""
const calls = [];
const sheet = new Element();
const findButton = new Element();
const createButton = new Element();
const setupButton = new Element();
const closeButton = new Element();
const status = new Element();
let online = false;
const controller = createFirstTask({
storage:{getItem:() => null, setItem() {}}, getLogin:() => 'timmy', hasWork:() => false,
isOnline:() => online, mediaQuery:{matches:true}, sheet, findButton, createButton,
setupButton, closeButton, status,
onFind:() => calls.push('find'), onCreate:() => calls.push('create'), onSetup:() => calls.push('setup'),
});
controller.start();
controller.open();
const offline = {findDisabled:findButton.disabled, createDisabled:createButton.disabled, status:status.textContent, createFocused:createButton.focused};
findButton.click();
createButton.click();
controller.open();
setupButton.click();
controller.open();
online = true;
controller.render();
findButton.click();
process.stdout.write(JSON.stringify({offline, calls, sheetOpen:sheet.open}));
"""
)
assert result == {
"offline": {
"findDisabled": True,
"createDisabled": False,
"status": "You are offline. Create a task now and it will stay in Drafts until you reconnect.",
"createFocused": True,
},
"calls": ["create", "setup", "find"],
"sheetOpen": False,
}
@pytest.mark.anyio
async def test_dashboard_renders_and_wires_phone_safe_first_task_activation():
html = await dashboard()
assert '<dialog class="mobile-first-task" id="mobile-first-task" aria-labelledby="mobile-first-task-title">' in html
assert '<h2 id="mobile-first-task-title">Start your first task</h2>' in html
assert 'id="mobile-first-task-find" type="button">Find a task</button>' in html
assert 'id="mobile-first-task-create" type="button">Create a task</button>' in html
assert 'id="mobile-first-task-setup" type="button">Make this phone work-ready</button>' in html
assert '<script src="static/mobile-first-task.js"></script>' in html
assert "const mobileFirstTask = createMobileFirstTask({" in html
assert "shouldActivate: () => mobileFirstTask.required()" in html
assert "openActivation: () => mobileFirstTask.open()" in html
assert "mobileFirstTask.refresh()" in html
assert '.mobile-first-task { box-sizing:border-box; width:100%;' in html
assert 'padding-bottom:calc(16px + env(safe-area-inset-bottom))' in html
assert '.mobile-first-task-actions button { min-height:48px;' in html

View File

@ -140,6 +140,35 @@ process.stdout.write(JSON.stringify({{mode:entry.open(), calls}}));
assert json.loads(result.stdout) == {"mode": "plan", "calls": ["plan"]} 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(): def test_mobile_task_dock_routes_actions_hides_for_overlays_and_restores_focus():
script = f""" script = f"""
const createDock = require({json.dumps(str(DOCK))}); const createDock = require({json.dumps(str(DOCK))});

View File

@ -1257,6 +1257,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
"/dashboard/static/live-data-status.js", "/dashboard/static/live-data-status.js",
"/dashboard/static/mobile-today-command-bar.js", "/dashboard/static/mobile-today-command-bar.js",
"/dashboard/static/mobile-task-dock.js", "/dashboard/static/mobile-task-dock.js",
"/dashboard/static/mobile-first-task.js",
"/dashboard/static/mobile-work-entry.js", "/dashboard/static/mobile-work-entry.js",
"/dashboard/static/mobile-queue-launcher.js", "/dashboard/static/mobile-queue-launcher.js",
"/dashboard/static/mobile-delivery-recovery.js", "/dashboard/static/mobile-delivery-recovery.js",