feat: pause Today during mobile Insights (Closes #1082)
This commit is contained in:
parent
d7b8cd5e71
commit
472371d5a8
|
|
@ -254,16 +254,14 @@
|
||||||
root: qs('#insights-sheet'),
|
root: qs('#insights-sheet'),
|
||||||
launcher: qs('#open-insights'),
|
launcher: qs('#open-insights'),
|
||||||
closeButton: qs('#close-insights'),
|
closeButton: qs('#close-insights'),
|
||||||
|
|
||||||
dock: qs('#mobile-task-dock'),
|
dock: qs('#mobile-task-dock'),
|
||||||
hud: qs('[data-mobile-today-hud]'),
|
hud: qs('[data-mobile-today-hud]'),
|
||||||
backgrounds: [qs('header'), qs('#my-work')],
|
backgrounds: [qs('header'), qs('#my-work')],
|
||||||
menu: qs('.app-menu'),
|
|
||||||
history: window.history,
|
history: window.history,
|
||||||
location: window.location,
|
|
||||||
eventTarget: window,
|
|
||||||
mediaQuery: window.matchMedia('(max-width: 600px)'),
|
mediaQuery: window.matchMedia('(max-width: 600px)'),
|
||||||
|
detour:() => timerView,
|
||||||
});
|
});
|
||||||
mobileInsights.start();
|
|
||||||
qs('#empty-work-find').addEventListener('click', () => qs('#find-work').click());
|
qs('#empty-work-find').addEventListener('click', () => qs('#find-work').click());
|
||||||
qs('#empty-work-create').addEventListener('click', () => qs('#new-issue').click());
|
qs('#empty-work-create').addEventListener('click', () => qs('#new-issue').click());
|
||||||
let liveMode = true;
|
let liveMode = true;
|
||||||
|
|
@ -1841,7 +1839,8 @@
|
||||||
onReopen: identity => {
|
onReopen: identity => {
|
||||||
taskOverlayHistory.leave();
|
taskOverlayHistory.leave();
|
||||||
selectTodayWork();
|
selectTodayWork();
|
||||||
workSession.reopen(todayMyWork.find(item => todayWork.identity(item) === identity));
|
const item = [...todayMyWork, ...activeMyWork].find(item => todayWork.identity(item) === identity);
|
||||||
|
if (!workSession.reopen(item)) workSession.resume(item);
|
||||||
},
|
},
|
||||||
onResume: identity => {
|
onResume: identity => {
|
||||||
selectTodayWork();
|
selectTodayWork();
|
||||||
|
|
@ -1854,6 +1853,7 @@
|
||||||
},
|
},
|
||||||
onCapture:() => openCreateIssueSheet(),
|
onCapture:() => openCreateIssueSheet(),
|
||||||
});
|
});
|
||||||
|
mobileInsights.start();
|
||||||
todaySessionSync = attachTodaySessionHandoff({
|
todaySessionSync = attachTodaySessionHandoff({
|
||||||
fetchJson:fetchReviewJson, storage:localStorage, timer, qs,
|
fetchJson:fetchReviewJson, storage:localStorage, timer, qs,
|
||||||
items:() => [...todayMyWork, ...activeMyWork],
|
items:() => [...todayMyWork, ...activeMyWork],
|
||||||
|
|
|
||||||
|
|
@ -309,6 +309,10 @@
|
||||||
<div><h2 id="insights-heading">Insights</h2><p class="small muted">Context, activity, live signals, and workspace tools.</p></div>
|
<div><h2 id="insights-heading">Insights</h2><p class="small muted">Context, activity, live signals, and workspace tools.</p></div>
|
||||||
<button id="close-insights" type="button">Close</button>
|
<button id="close-insights" type="button">Close</button>
|
||||||
</div>
|
</div>
|
||||||
|
<aside class="today-detour-interruption" data-today-detour role="status" aria-live="polite" hidden>
|
||||||
|
<strong data-today-detour-label>Today paused</strong>
|
||||||
|
<button data-return-from-detour type="button">Return to Today</button>
|
||||||
|
</aside>
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<details class="panel stack" data-panel-key="context" open>
|
<details class="panel stack" data-panel-key="context" open>
|
||||||
<summary><h2>Context & view</h2></summary>
|
<summary><h2>Context & view</h2></summary>
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,11 @@
|
||||||
else root.createMobileInsights = factory;
|
else root.createMobileInsights = factory;
|
||||||
})(typeof self !== 'undefined' ? self : this, function (o) {
|
})(typeof self !== 'undefined' ? self : this, function (o) {
|
||||||
const route = '#/insights';
|
const route = '#/insights';
|
||||||
|
const events = o.eventTarget || self;
|
||||||
|
const location = o.location || self.location;
|
||||||
let active = false;
|
let active = false;
|
||||||
let previous = '#/my-work';
|
let previous = '#/my-work';
|
||||||
|
let retries = 0;
|
||||||
|
|
||||||
function attr(element, name, value) {
|
function attr(element, name, value) {
|
||||||
if (value === null) element.removeAttribute(name);
|
if (value === null) element.removeAttribute(name);
|
||||||
|
|
@ -12,7 +15,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function render(open, focus = false) {
|
function render(open, focus = false) {
|
||||||
|
const wasActive = active;
|
||||||
active = Boolean(open && o.mediaQuery.matches);
|
active = Boolean(open && o.mediaQuery.matches);
|
||||||
|
const detour = active ? o.detour?.() : null;
|
||||||
|
const started = detour?.beginDetour('insights');
|
||||||
|
if (detour && !started && retries++ < 40) setTimeout(sync, 250);
|
||||||
|
else if (started && retries) { retries = 0; setTimeout(sync, 500); }
|
||||||
|
else retries = 0;
|
||||||
|
if (wasActive && !active) o.detour?.()?.finishDetour();
|
||||||
attr(o.root, 'data-mobile-open', active ? 'true' : null);
|
attr(o.root, 'data-mobile-open', active ? 'true' : null);
|
||||||
attr(o.root, 'role', active ? 'dialog' : null);
|
attr(o.root, 'role', active ? 'dialog' : null);
|
||||||
attr(o.root, 'aria-modal', active ? 'true' : null);
|
attr(o.root, 'aria-modal', active ? 'true' : null);
|
||||||
|
|
@ -26,20 +36,21 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function url(hash) {
|
function url(hash) {
|
||||||
return (o.location.pathname || '') + (o.location.search || '') + hash;
|
return (location.pathname || '') + (location.search || '') + hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
if (!o.mediaQuery.matches || active) return false;
|
if (!o.mediaQuery.matches || active) return false;
|
||||||
previous = o.location.hash && o.location.hash !== route ? o.location.hash : '#/my-work';
|
previous = location.hash && location.hash !== route ? location.hash : '#/my-work';
|
||||||
if (o.menu) o.menu.open = false;
|
const menu = o.menu || o.launcher.closest?.('details');
|
||||||
|
if (menu) menu.open = false;
|
||||||
o.history.pushState({ ...(o.history.state || {}), mobileInsights:true, previousHash:previous }, '', url(route));
|
o.history.pushState({ ...(o.history.state || {}), mobileInsights:true, previousHash:previous }, '', url(route));
|
||||||
render(true);
|
render(true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sync() {
|
function sync() {
|
||||||
render(o.location.hash === route, active && o.location.hash !== route);
|
render(location.hash === route, active && location.hash !== route);
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
|
|
@ -55,12 +66,23 @@
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function returnToToday() {
|
||||||
|
if (!active) return false;
|
||||||
|
const state = { ...(o.history.state || {}) };
|
||||||
|
delete state.mobileInsights;
|
||||||
|
delete state.previousHash;
|
||||||
|
o.history.replaceState(state, '', url(previous));
|
||||||
|
render(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function start() {
|
function start() {
|
||||||
o.launcher.addEventListener('click', open);
|
o.launcher.addEventListener('click', open);
|
||||||
o.closeButton.addEventListener('click', close);
|
o.closeButton.addEventListener('click', close);
|
||||||
o.eventTarget.addEventListener('popstate', sync);
|
(o.returnButton || o.root.querySelector?.('[data-return-from-detour]'))?.addEventListener('click', returnToToday);
|
||||||
o.eventTarget.addEventListener('hashchange', sync);
|
events.addEventListener('popstate', sync);
|
||||||
o.eventTarget.addEventListener('keydown', event => {
|
events.addEventListener('hashchange', sync);
|
||||||
|
events.addEventListener('keydown', event => {
|
||||||
if (active && event.key === 'Escape') {
|
if (active && event.key === 'Escape') {
|
||||||
event.preventDefault?.();
|
event.preventDefault?.();
|
||||||
close();
|
close();
|
||||||
|
|
@ -70,5 +92,5 @@
|
||||||
sync();
|
sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
return { start, open, close, current:() => active };
|
return { start, open, close, sync, current:() => active };
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ function createTodayTimer({ storage, getLogin, now = () => Date.now(), onChange
|
||||||
const validDetour = state => {
|
const validDetour = state => {
|
||||||
const pending = state.detour_interruption;
|
const pending = state.detour_interruption;
|
||||||
return pending && typeof pending.identity === 'string' && pending.identity &&
|
return pending && typeof pending.identity === 'string' && pending.identity &&
|
||||||
typeof pending.resume === 'boolean' && ['find', 'queues'].includes(pending.reason) ?
|
typeof pending.resume === 'boolean' && ['find', 'queues', 'insights'].includes(pending.reason) ?
|
||||||
{ identity:pending.identity, resume:pending.resume, reason:pending.reason } : null;
|
{ identity:pending.identity, resume:pending.resume, reason:pending.reason } : null;
|
||||||
};
|
};
|
||||||
const validBreak = state => {
|
const validBreak = state => {
|
||||||
|
|
@ -289,7 +289,7 @@ function createTodayTimer({ storage, getLogin, now = () => Date.now(), onChange
|
||||||
return write(state) ? { identity:pending.identity, resumed } : null;
|
return write(state) ? { identity:pending.identity, resumed } : null;
|
||||||
},
|
},
|
||||||
beginDetour(reason) {
|
beginDetour(reason) {
|
||||||
if (!['find', 'queues'].includes(reason)) return null;
|
if (!['find', 'queues', 'insights'].includes(reason)) return null;
|
||||||
const state = read();
|
const state = read();
|
||||||
const existing = validDetour(state);
|
const existing = validDetour(state);
|
||||||
if (existing) return existing;
|
if (existing) return existing;
|
||||||
|
|
|
||||||
|
|
@ -351,7 +351,7 @@ def test_release_artifact_plans_hands_off_and_opens_next_mobile_issue(tmp_path:
|
||||||
fake_thread.join(timeout=5)
|
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"))
|
archives = sorted((ROOT / "dist").glob("stackchain-dashboard-*.tar.gz"))
|
||||||
assert len(archives) == 1
|
assert len(archives) == 1
|
||||||
fake = FakeGiteaServer(("127.0.0.1", 0))
|
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")
|
missing.nth(0).press("Tab")
|
||||||
page.locator("#save-and-start-today").click()
|
page.locator("#save-and-start-today").click()
|
||||||
expect(page.locator("#issue-sheet-title")).to_have_text("Ship mobile capture")
|
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():
|
if page.locator("#plan-today-sheet").is_visible():
|
||||||
page.locator("#cancel-plan-today").click()
|
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));
|
const timer = JSON.parse(localStorage.getItem(key));
|
||||||
return timer.entries[timer.active_identity].running === true && !timer.detour_interruption;
|
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()
|
browser.close()
|
||||||
finally:
|
finally:
|
||||||
fake.shutdown()
|
fake.shutdown()
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,70 @@ process.stdout.write(JSON.stringify({{backs:history.backs,prevented}}));
|
||||||
assert run_node(script) == {"backs": 1, "prevented": 1}
|
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
|
@pytest.mark.anyio
|
||||||
async def test_mobile_home_progressively_discloses_secondary_panels_as_insights():
|
async def test_mobile_home_progressively_discloses_secondary_panels_as_insights():
|
||||||
html = await dashboard()
|
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="open-insights"' in html
|
||||||
assert 'id="insights-sheet"' in html
|
assert 'id="insights-sheet"' in html
|
||||||
assert 'id="close-insights"' 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="my-work"') < html.index('id="insights-sheet"')
|
||||||
assert html.index('id="insights-sheet"') < html.index('data-panel-key="context"')
|
assert html.index('id="insights-sheet"') < html.index('data-panel-key="context"')
|
||||||
assert html.index('data-panel-key="markdown"') < html.index('</section>\n</main>')
|
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():
|
async def test_find_and_queue_detours_are_visible_and_wired_to_today_timing():
|
||||||
html = await dashboard()
|
html = await dashboard()
|
||||||
|
|
||||||
assert html.count('data-today-detour role="status"') == 3
|
assert html.count('data-today-detour role="status"') == 4
|
||||||
assert html.count('data-return-from-detour type="button"') == 3
|
assert html.count('data-return-from-detour type="button"') == 4
|
||||||
assert "detour:() => timerView" in html
|
assert "detour:() => timerView" in html
|
||||||
timer_source = TIMER.read_text()
|
timer_source = TIMER.read_text()
|
||||||
assert "createTodayDetourInterruption" in timer_source
|
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():
|
def test_mobile_detour_never_resumes_manually_paused_or_replaced_work():
|
||||||
script = SOURCE.read_text() + r"""
|
script = SOURCE.read_text() + r"""
|
||||||
const values = new Map();
|
const values = new Map();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user