feat: pause Today during mobile device setup (Closes #1084)
CI / lint (pull_request) Successful in 3m24s
CI / build-release (pull_request) Successful in 9s
CI / browser-journey (pull_request) Successful in 3m0s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-18 15:30:55 +00:00
1 parent a6f34fbfde
commit 4a53a95b2d
7 files changed
+93 -7

No files matched your search

+44 -2
View File
@@ -15,7 +15,10 @@ class FakeTarget {
async dispatch(name, event = {}) { for (const callback of this.listeners[name] || []) await callback(event); }
focus() { state.focused = this; }
}
const state = {installCalls:0, offlineCalls:0, pushCalls:0, deadlineCalls:0, focused:null};
const state = {
installCalls:0, offlineCalls:0, pushCalls:0, deadlineCalls:0, focused:null,
events:[], beginCalls:[], finishCalls:0,
};
const launcher = new FakeTarget();
const closeButton = new FakeTarget();
const sheet = new FakeTarget(); sheet.hidden = true;
@@ -33,6 +36,7 @@ const promptCard = new FakeTarget(); promptCard.hidden = true;
const promptSummary = new FakeTarget();
const promptLauncher = new FakeTarget();
const promptDismiss = new FakeTarget();
const returnButton = new FakeTarget();
let now = 1000;
const promptStorage = {
values:{},
@@ -49,8 +53,13 @@ const setup = createMobileDeviceSetup({
launcher, closeButton, sheet, installButton, offlineButton, pushButton, deadlineButton,
installStatus, offlineStatus, pushStatus, deadlineStatus, readyStatus, escapeTarget,
promptCard, promptSummary, promptLauncher, promptDismiss,
returnButton, isMobile:() => true,
timerView:{
beginDetour:reason => { state.events.push('pause'); state.beginCalls.push(reason); return {reason}; },
finishDetour:() => { state.events.push('resume'); state.finishCalls += 1; },
},
promptStorage, now:() => now,
getReadiness:() => readiness,
getReadiness:() => { state.events.push('readiness'); return readiness; },
install:async () => { state.installCalls += 1; },
enableOffline:async () => { state.offlineCalls += 1; },
enablePush:async () => { state.pushCalls += 1; },
@@ -170,6 +179,35 @@ process.stdout.write(JSON.stringify({
assert result == {"hidden": True, "launcherFocused": True}
def test_mobile_setup_pauses_before_readiness_and_each_exit_resumes_once():
result = run_scenario("""
state.events = [];
await launcher.dispatch('click', {currentTarget:launcher});
const firstOpen = {events:[...state.events], reasons:[...state.beginCalls]};
await sheet.dispatch('click', {target:sheet});
const backdropFinish = state.finishCalls;
await promptLauncher.dispatch('click', {currentTarget:promptLauncher});
await escapeTarget.dispatch('keydown', {key:'Escape'});
const escapeFinish = state.finishCalls;
await launcher.dispatch('click', {currentTarget:launcher});
returnButton.addEventListener('click', () => { state.events.push('shared-resume'); state.finishCalls += 1; });
await returnButton.dispatch('click');
process.stdout.write(JSON.stringify({
firstOpen, backdropFinish, escapeFinish,
returnFinish:state.finishCalls,
hidden:sheet.hidden,
}));
""")
assert result == {
"firstOpen": {"events": ["pause", "readiness"], "reasons": ["device-setup"]},
"backdropFinish": 1,
"escapeFinish": 2,
"returnFinish": 3,
"hidden": True,
}
def test_deadline_step_runs_one_setup_action_and_uses_confirmed_readiness():
result = run_scenario("""
readiness.push = {state:'complete', detail:'New update notifications are enabled.'};
@@ -207,6 +245,8 @@ def test_mobile_dashboard_mounts_phone_safe_device_setup_flow():
assert 'id="dismiss-device-readiness"' in html
assert html.index('id="device-readiness-card"') < html.index('id="my-work-list"')
assert 'id="device-setup-sheet"' in html
assert 'id="device-setup-today-detour"' in html
assert 'id="return-from-device-setup"' in html
assert 'aria-labelledby="device-setup-heading"' in html
assert all(f'id="device-setup-{step}"' in html for step in ("install", "offline", "push", "deadline"))
assert 'id="device-storage-summary"' in html
@@ -218,6 +258,8 @@ def test_mobile_dashboard_mounts_phone_safe_device_setup_flow():
assert 'id="device-setup-deadline-hour"' in html
assert '<script src="static/mobile-device-setup.js"></script>' in html
assert "createMobileDeviceSetup.mount({" in dashboard
assert "timerView," in dashboard
assert "isMobile:options.isMobile || (() => innerWidth <= 600)" in MODULE.read_text()
assert "createDeviceStorage.mount(document)" in dashboard
assert "clearPrivateDeviceData:root.stackchainPrivateDeviceData" in storage_module
assert "promptStorage:localStorage" in dashboard
+2 -2
View File
@@ -254,8 +254,8 @@ process.stdout.write(JSON.stringify({{calls, open:queues.open}}));
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 html.count('data-today-detour role="status"') == 5
assert html.count('data-return-from-detour type="button"') == 5
assert "detour:() => timerView" in html
timer_source = TIMER.read_text()
assert "createTodayDetourInterruption" in timer_source
+29
View File
@@ -313,6 +313,35 @@ process.stdout.write(JSON.stringify({pausedDetour, runningDetour, replacedReturn
}
def test_device_setup_detour_excludes_permission_time_and_survives_reload():
script = SOURCE.read_text() + r"""
const values = new Map();
const storage = {getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)};
let now = 1000;
let timer = createTodayTimer({storage, getLogin:()=> 'timmy', now:()=>now});
timer.activate('issue:r:42:');
now = 6000;
const interruption = timer.beginDetour('device-setup');
now = 26000;
timer = createTodayTimer({storage, getLogin:()=> 'timmy', now:()=>now});
const restored = timer.detourInterruption();
const paused = timer.snapshot();
const returned = timer.returnFromDetour();
now = 28000;
process.stdout.write(JSON.stringify({interruption, restored, paused, returned, resumed:timer.snapshot()}));
"""
completed = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert completed.returncode == 0, completed.stderr
assert json.loads(completed.stdout) == {
"interruption": {"identity": "issue:r:42:", "resume": True, "reason": "device-setup"},
"restored": {"identity": "issue:r:42:", "resume": True, "reason": "device-setup"},
"paused": {"identity": "issue:r:42:", "elapsed_ms": 5000, "running": False},
"returned": {"identity": "issue:r:42:", "resumed": True, "reason": "device-setup"},
"resumed": {"identity": "issue:r:42:", "elapsed_ms": 7000, "running": True},
}
def test_mobile_detour_view_names_work_and_returns_to_today():
script = SOURCE.read_text() + r"""
const values = new Map();