feat: pause Today during mobile Insights (Closes #1082)
This commit is contained in:
1 parent
d7b8cd5e71
commit
472371d5a8
8 files changed
+168
-19
No files matched your search
@@ -351,7 +351,7 @@ def test_release_artifact_plans_hands_off_and_opens_next_mobile_issue(tmp_path:
|
||||
fake_thread.join(timeout=5)
|
||||
|
||||
|
||||
def test_release_artifact_pauses_today_across_find_and_queue_detours(tmp_path: Path):
|
||||
def test_release_artifact_pauses_today_across_mobile_work_and_insights_detours(tmp_path: Path):
|
||||
archives = sorted((ROOT / "dist").glob("stackchain-dashboard-*.tar.gz"))
|
||||
assert len(archives) == 1
|
||||
fake = FakeGiteaServer(("127.0.0.1", 0))
|
||||
@@ -381,7 +381,7 @@ def test_release_artifact_pauses_today_across_find_and_queue_detours(tmp_path: P
|
||||
missing.nth(0).press("Tab")
|
||||
page.locator("#save-and-start-today").click()
|
||||
expect(page.locator("#issue-sheet-title")).to_have_text("Ship mobile capture")
|
||||
page.locator("#close-issue-sheet").click()
|
||||
page.locator("#close-issue-sheet").click(force=True)
|
||||
if page.locator("#plan-today-sheet").is_visible():
|
||||
page.locator("#cancel-plan-today").click()
|
||||
|
||||
@@ -421,6 +421,31 @@ def test_release_artifact_pauses_today_across_find_and_queue_detours(tmp_path: P
|
||||
const timer = JSON.parse(localStorage.getItem(key));
|
||||
return timer.entries[timer.active_identity].running === true && !timer.detour_interruption;
|
||||
}""")
|
||||
|
||||
page.locator("#close-issue-sheet").click()
|
||||
page.locator("#app-menu-toggle").click()
|
||||
page.locator("#open-insights").click()
|
||||
insights_pause = page.locator("#insights-sheet > [data-today-detour]")
|
||||
expect(insights_pause).to_be_visible()
|
||||
expect(insights_pause).to_contain_text("Today paused · Ship mobile capture")
|
||||
bounds = insights_pause.locator("[data-return-from-detour]").bounding_box()
|
||||
assert bounds and bounds["height"] >= 44
|
||||
assert page.evaluate("""() => {
|
||||
const key = Object.keys(localStorage).find(value => value.startsWith('stackchain.today-timer.v1.'));
|
||||
const timer = JSON.parse(localStorage.getItem(key));
|
||||
return timer.entries[timer.active_identity].running === false && timer.detour_interruption?.reason === 'insights';
|
||||
}""")
|
||||
page.reload(wait_until="networkidle")
|
||||
restored_pause = page.locator("#insights-sheet > [data-today-detour]")
|
||||
expect(restored_pause).to_be_visible()
|
||||
expect(restored_pause).to_contain_text("Today paused · Ship mobile capture")
|
||||
restored_pause.locator("[data-return-from-detour]").click()
|
||||
expect(page.locator("#issue-sheet-title")).to_have_text("Ship mobile capture")
|
||||
assert page.evaluate("""() => {
|
||||
const key = Object.keys(localStorage).find(value => value.startsWith('stackchain.today-timer.v1.'));
|
||||
const timer = JSON.parse(localStorage.getItem(key));
|
||||
return timer.entries[timer.active_identity].running === true && !timer.detour_interruption;
|
||||
}""")
|
||||
browser.close()
|
||||
finally:
|
||||
fake.shutdown()
|
||||
|
||||
@@ -150,6 +150,70 @@ process.stdout.write(JSON.stringify({{backs:history.backs,prevented}}));
|
||||
assert run_node(script) == {"backs": 1, "prevented": 1}
|
||||
|
||||
|
||||
def test_insights_route_owns_the_today_detour_for_open_back_and_reload():
|
||||
script = f"""
|
||||
const createInsights=require({json.dumps(str(CONTROLLER))});
|
||||
const listeners={{}};
|
||||
const element=()=>({{hidden:false,inert:false,attributes:{{}},listeners:{{}},setAttribute(n,v){{this.attributes[n]=String(v);}},removeAttribute(n){{delete this.attributes[n];}},addEventListener(n,f){{this.listeners[n]=f;}},focus(){{}}}});
|
||||
const root=element(),launcher=element(),closeButton=element(),returnButton=element(),dock=element(),hud=element();
|
||||
const location={{pathname:'/dashboard/',search:'',hash:'#/my-work/today'}};
|
||||
const calls=[];
|
||||
const detour={{beginDetour(reason){{calls.push('begin:'+reason);return {{identity:'issue:r:42:',reason}};}},finishDetour(){{calls.push('finish');return {{identity:'issue:r:42:'}};}}}};
|
||||
const history={{state:null,pushState(state,unused,url){{this.state=state;location.hash=url.slice(url.indexOf('#'));}},replaceState(){{}},back(){{}}}};
|
||||
const controller=createInsights({{root,launcher,closeButton,returnButton,dock,hud,backgrounds:[],history,location,detour:()=>detour,eventTarget:{{addEventListener(n,f){{listeners[n]=f;}}}},mediaQuery:{{matches:true,addEventListener(){{}}}}}});
|
||||
controller.start();
|
||||
launcher.listeners.click();
|
||||
location.hash='#/my-work/today'; history.state=null; listeners.popstate();
|
||||
listeners.popstate();
|
||||
location.hash='#/insights'; listeners.hashchange();
|
||||
process.stdout.write(JSON.stringify({{calls,current:controller.current()}}));
|
||||
"""
|
||||
|
||||
assert run_node(script) == {
|
||||
"calls": ["begin:insights", "finish", "begin:insights"],
|
||||
"current": True,
|
||||
}
|
||||
|
||||
|
||||
def test_return_to_today_closes_insights_and_finishes_the_detour_once():
|
||||
script = f"""
|
||||
const createInsights=require({json.dumps(str(CONTROLLER))});
|
||||
const element=()=>({{hidden:false,inert:false,attributes:{{}},listeners:{{}},setAttribute(n,v){{this.attributes[n]=String(v);}},removeAttribute(n){{delete this.attributes[n];}},addEventListener(n,f){{this.listeners[n]=f;}},focus(){{}}}});
|
||||
const root=element(),launcher=element(),closeButton=element(),returnButton=element(),dock=element(),hud=element();
|
||||
const location={{pathname:'/dashboard/',search:'',hash:'#/my-work/today'}};
|
||||
const calls=[];
|
||||
const detour={{beginDetour(reason){{calls.push('begin:'+reason);}},finishDetour(){{calls.push('finish');}}}};
|
||||
const history={{state:null,pushState(state,unused,url){{this.state=state;location.hash=url.slice(url.indexOf('#'));}},replaceState(state,unused,url){{this.state=state;location.hash=url.slice(url.indexOf('#'));}},back(){{}}}};
|
||||
const controller=createInsights({{root,launcher,closeButton,returnButton,dock,hud,backgrounds:[],history,location,detour:()=>detour,eventTarget:{{addEventListener(){{}}}},mediaQuery:{{matches:true,addEventListener(){{}}}}}});
|
||||
controller.start(); launcher.listeners.click(); returnButton.listeners.click();
|
||||
process.stdout.write(JSON.stringify({{calls,current:controller.current(),hash:location.hash}}));
|
||||
"""
|
||||
|
||||
assert run_node(script) == {
|
||||
"calls": ["begin:insights", "finish"],
|
||||
"current": False,
|
||||
"hash": "#/my-work/today",
|
||||
}
|
||||
|
||||
|
||||
def test_direct_insights_route_retries_detour_after_identity_restores():
|
||||
script = f"""
|
||||
const createInsights=require({json.dumps(str(CONTROLLER))});
|
||||
const element=()=>({{hidden:false,inert:false,attributes:{{}},listeners:{{}},setAttribute(n,v){{this.attributes[n]=String(v);}},removeAttribute(n){{delete this.attributes[n];}},addEventListener(n,f){{this.listeners[n]=f;}},focus(){{}}}});
|
||||
const root=element(),launcher=element(),closeButton=element(),returnButton=element(),dock=element(),hud=element();
|
||||
const location={{pathname:'/dashboard/',search:'',hash:'#/insights'}};
|
||||
let attempts=0; let retry;
|
||||
global.setTimeout=callback=>{{retry=callback;}};
|
||||
const detour={{beginDetour(){{attempts+=1;return attempts > 1 ? {{identity:'issue:r:42:'}} : null;}},finishDetour(){{}}}};
|
||||
const history={{state:null,replaceState(){{}},pushState(){{}},back(){{}}}};
|
||||
const controller=createInsights({{root,launcher,closeButton,returnButton,dock,hud,backgrounds:[],history,location,detour:()=>detour,eventTarget:{{addEventListener(){{}}}},mediaQuery:{{matches:true,addEventListener(){{}}}}}});
|
||||
controller.start(); retry();
|
||||
process.stdout.write(JSON.stringify({{attempts,current:controller.current()}}));
|
||||
"""
|
||||
|
||||
assert run_node(script) == {"attempts": 2, "current": True}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_mobile_home_progressively_discloses_secondary_panels_as_insights():
|
||||
html = await dashboard()
|
||||
@@ -157,6 +221,11 @@ async def test_mobile_home_progressively_discloses_secondary_panels_as_insights(
|
||||
assert 'id="open-insights"' in html
|
||||
assert 'id="insights-sheet"' in html
|
||||
assert 'id="close-insights"' in html
|
||||
insights = html[html.index('id="insights-sheet"'):html.index('</section>', html.index('id="insights-sheet"'))]
|
||||
assert 'data-today-detour' in insights
|
||||
assert 'data-return-from-detour' in insights
|
||||
assert "detour:() => timerView" in html
|
||||
assert html.index("const timerView = createTodayTimerView({") < html.index("mobileInsights.start();")
|
||||
assert html.index('id="my-work"') < html.index('id="insights-sheet"')
|
||||
assert html.index('id="insights-sheet"') < html.index('data-panel-key="context"')
|
||||
assert html.index('data-panel-key="markdown"') < html.index('</section>\n</main>')
|
||||
|
||||
@@ -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"') == 3
|
||||
assert html.count('data-return-from-detour type="button"') == 3
|
||||
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
|
||||
|
||||
@@ -260,6 +260,35 @@ process.stdout.write(JSON.stringify({interruption, paused, durable, returned, re
|
||||
}
|
||||
|
||||
|
||||
def test_insights_detour_excludes_review_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('insights');
|
||||
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": "insights"},
|
||||
"restored": {"identity": "issue:r:42:", "resume": True, "reason": "insights"},
|
||||
"paused": {"identity": "issue:r:42:", "elapsed_ms": 5000, "running": False},
|
||||
"returned": {"identity": "issue:r:42:", "resumed": True, "reason": "insights"},
|
||||
"resumed": {"identity": "issue:r:42:", "elapsed_ms": 7000, "running": True},
|
||||
}
|
||||
|
||||
|
||||
def test_mobile_detour_never_resumes_manually_paused_or_replaced_work():
|
||||
script = SOURCE.read_text() + r"""
|
||||
const values = new Map();
|
||||
|
||||
Reference in new issue
Block a user