diff --git a/frontend/dashboard.js b/frontend/dashboard.js index 5d28da4..02de798 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -5480,7 +5480,7 @@ activeFlushLogin = contextIdentityFresh ? String(snapshot.context.user?.login || '').trim() : ''; if (activeFlushLogin) { confirmedOwnerLogin = activeFlushLogin; - timerView.render(); + timerView.restore(todaySync.flush()); restoreReleaseReceipt(); updateDeliveryReceiptControls(); interruptionPrompt.restore(); diff --git a/frontend/today-timer.js b/frontend/today-timer.js index a525ffa..958900d 100644 --- a/frontend/today-timer.js +++ b/frontend/today-timer.js @@ -542,6 +542,10 @@ function createTodayTimerView({ timer, isActive, queryAll, formatEstimate, getRu }, finishCapture() { return captureView.finish(); }, transferCapture() { return captureView.transfer(); }, + restore(recovery) { + render(); + return Promise.resolve(recovery).catch(() => false).then(() => { render(); return !!timer.breakSnapshot?.(); }); + }, render, }; queryAll('#new-issue').forEach(button => button.addEventListener('click', () => { diff --git a/tests/test_today_break.py b/tests/test_today_break.py index df0e49c..ec465c6 100644 --- a/tests/test_today_break.py +++ b/tests/test_today_break.py @@ -185,12 +185,86 @@ process.stdout.write(JSON.stringify({{opened,cancelled,started,expired,final:{{c } -def test_confirming_the_account_renders_a_persisted_break_immediately(): - dashboard = (ROOT / "frontend" / "dashboard.js").read_text() - confirmation = dashboard.split("if (activeFlushLogin) {", 1)[1].split("}", 1)[0] +def test_identity_ready_restore_renders_break_again_after_sync_recovery_settles(): + script = f""" +const createBreak = require({json.dumps(str(BREAK))}); +global.createTodayBreak = createBreak; +global.setInterval = () => 1; +global.clearInterval = () => {{}}; +Date.now = () => 100000; +const createTimer = require({json.dumps(str(TIMER))}); +const createSync = require({json.dumps(str(ROOT / "frontend" / "today-sync.js"))}); +const values = new Map(); +const storage = {{ + get length(){{return values.size;}}, + key:index=>Array.from(values.keys())[index]??null, + getItem:key=>values.get(key)||null, + setItem:(key,value)=>values.set(key,value), + removeItem:key=>values.delete(key), +}}; +let login = ''; +values.set('stackchain.today-timer.v1.timmy', JSON.stringify({{ + version:1, active_identity:'issue:r:42:', + entries:{{'issue:r:42:':{{elapsed_ms:12000,started_at:null,running:false}}}}, + away_at:null,pending_interruption:null,attention_interruption:null,capture_interruption:null, + search_interruption:null,detour_interruption:null, + timed_break:{{identity:'issue:r:42:',deadline_at:400000}}, +}})); +const status = {{hidden:false,textContent:''}}, resume = {{hidden:false,addEventListener(){{}}}}; +const opener = {{hidden:false,addEventListener(){{}}}}; +const elements = {{'#today-break-status':[status],'#resume-today-break':[resume],'[data-today-break-open]':[opener]}}; +const timer = createTimer({{storage,getLogin:()=>login,now:()=>100000}}); +const view = createTimer.createView({{ + timer,isActive:()=>true,queryAll:selector=>elements[selector]||[], + formatEstimate:value=>String(value),getItem:()=>({{title:'Ship mobile capture'}}), +}}); +login = 'timmy'; +values.set('stackchain.today-sync.v1.timmy.operation.broken', '{{not-json'); +const statuses = []; +const sync = createSync({{ + storage,getLogin:()=>login,now:()=>100000, + fetchJson:async()=>({{revision:1,ids:['issue:r:42:'],capacity_minutes:null,estimates:{{}}}}), + onStatus:(state,detail)=>statuses.push([state,detail.discarded||0]), +}}); +const recovery = sync.flush(); +const restoring = view.restore(recovery); +const immediate = {{hidden:status.hidden,text:status.textContent,openerHidden:opener.hidden}}; +status.hidden = true; status.textContent = ''; opener.hidden = false; +restoring.then(result => {{ + const settled = {{hidden:status.hidden,text:status.textContent,openerHidden:opener.hidden}}; + status.hidden = true; status.textContent = ''; opener.hidden = false; + return view.restore(Promise.reject(new Error('coordinator unavailable'))).then(rejected => + process.stdout.write(JSON.stringify({{ + immediate, settled, rejectedSettled:{{hidden:status.hidden,text:status.textContent,openerHidden:opener.hidden}}, + result,rejected,statuses, + }})) + ); +}}); +""" - assert "confirmedOwnerLogin = activeFlushLogin;" in confirmation - assert "timerView.render();" in confirmation + assert run_node(script) == { + "immediate": { + "hidden": False, + "text": "On break · resume in 5:00", + "openerHidden": True, + }, + "settled": { + "hidden": False, + "text": "On break · resume in 5:00", + "openerHidden": True, + }, + "rejectedSettled": { + "hidden": False, + "text": "On break · resume in 5:00", + "openerHidden": True, + }, + "result": True, + "rejected": True, + "statuses": [["recovered", 1]], + } + + dashboard = (ROOT / "frontend" / "dashboard.js").read_text() + assert "timerView.restore(todaySync.flush());" in dashboard def test_timed_break_is_packaged_as_an_accessible_mobile_flow():