diff --git a/frontend/dashboard.js b/frontend/dashboard.js index 25e67e2..0c5bf3e 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -1,5 +1,5 @@ (async function(){ - await loadWorkspace({ document }); + const workspaceLifecycle = await loadWorkspace({ document, window }); const qs = (s, el=document) => el.querySelector(s); const fmt = (d) => new Date(d).toLocaleString(); const cardPlanning = createCardPlanning(document); @@ -7389,6 +7389,7 @@ if (!navigator.onLine) await showOfflineStatus(); window.addEventListener('offline', showOfflineStatus); window.addEventListener('online', reconnectOutboxes); + workspaceLifecycle.replayOnline(reconnectOutboxes); qs('#refresh').addEventListener('click', load); qs('#plan-today').addEventListener('click', event => openPlanToday(event.currentTarget)); diff --git a/frontend/workspace-bootstrap.js b/frontend/workspace-bootstrap.js index 0d07105..7698292 100644 --- a/frontend/workspace-bootstrap.js +++ b/frontend/workspace-bootstrap.js @@ -1,4 +1,8 @@ -async function loadWorkspace({ document, createLoader = createFeatureLoader }) { +async function loadWorkspace({ document, window = null, createLoader = createFeatureLoader }) { + let cameOnline = false; + let replayed = false; + const captureOnline = () => { cameOnline = true; }; + window?.addEventListener('online', captureOnline); const status = document.querySelector('#my-work-action-status'); const url = document.querySelector( 'meta[name="stackchain-feature-today-timer"]' @@ -11,7 +15,16 @@ async function loadWorkspace({ document, createLoader = createFeatureLoader }) { try { await loader.load('today-timer'); if (status) status.textContent = ''; + return { + replayOnline(callback) { + if (replayed) return; + replayed = true; + window?.removeEventListener('online', captureOnline); + if (cameOnline) callback(); + }, + }; } catch (error) { + window?.removeEventListener('online', captureOnline); if (status) { status.textContent = 'Workspace could not load. Check your connection, then reload to retry.'; diff --git a/src/frontend_bundle.py b/src/frontend_bundle.py index 38f0858..d2c8ede 100644 --- a/src/frontend_bundle.py +++ b/src/frontend_bundle.py @@ -31,7 +31,7 @@ FEATURE_SOURCES = { "security-center": ("static/security-center.js",), "today-timer": ( "static/conversation.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/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/my-work.js", "static/protect-today.js", "static/mobile-task-dock.js", "static/mobile-queue-launcher.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-recap.js", "static/today-wrap-up.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/my-work.js", "static/protect-today.js", "static/mobile-task-dock.js", "static/mobile-work-entry.js", "static/mobile-queue-launcher.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-recap.js", "static/today-wrap-up.js", "static/today-handoff.js", "static/today-rollover.js", "static/later-work.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/draft-filing-session.js", "static/draft-capacity-dialog.js", "static/work-selection.js", diff --git a/tests/e2e/test_mobile_offline_issue_release.py b/tests/e2e/test_mobile_offline_issue_release.py index 8d1c357..ae20ea2 100644 --- a/tests/e2e/test_mobile_offline_issue_release.py +++ b/tests/e2e/test_mobile_offline_issue_release.py @@ -270,9 +270,8 @@ def test_release_artifact_files_one_mobile_issue_exactly_once_after_offline_relo context.set_offline(False) page.evaluate("window.dispatchEvent(new Event('online'))") # Containerized Actions runners can take longer than a local browser to wake the - # service worker, finish optional workspace activation, and complete its identity - # handshake after reconnect. - for _ in range(240): + # service worker and complete its identity handshake after reconnect. + for _ in range(160): if fake.created_issues: break page.wait_for_timeout(250) diff --git a/tests/test_workspace_bootstrap.py b/tests/test_workspace_bootstrap.py index 50640cd..6751822 100644 --- a/tests/test_workspace_bootstrap.py +++ b/tests/test_workspace_bootstrap.py @@ -53,4 +53,26 @@ console.log(JSON.stringify({message,status:status.textContent})); assert result == { "message": "network failed", "status": "Workspace could not load. Check your connection, then reload to retry.", - } \ No newline at end of file + } + + +def test_workspace_bootstrap_replays_online_event_after_dashboard_registers_handlers(): + result = run_bootstrap(""" +const status={textContent:''}; const listeners={}; +const document={querySelector(selector) { + if (selector.startsWith('meta[')) return {content:'feature-workspace-abc.js'}; + if (selector === '#my-work-action-status') return status; + return null; +}}; +const window={ + addEventListener(name,callback) { listeners[name]=callback; }, + removeEventListener(name,callback) { if (listeners[name] === callback) delete listeners[name]; }, +}; +const createLoader=()=>({load:async()=>{listeners.online();}}); +const lifecycle=await loadWorkspace({document,window,createLoader}); +let reconnects=0; +lifecycle.replayOnline(()=>{reconnects++;}); +lifecycle.replayOnline(()=>{reconnects++;}); +console.log(JSON.stringify({reconnects,listening:Boolean(listeners.online)})); +""") + assert result == {"reconnects": 1, "listening": False} \ No newline at end of file