diff --git a/frontend/dashboard.js b/frontend/dashboard.js index 4463ac8..a0e53a2 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -8472,7 +8472,8 @@ }); let pushControllerReady = Promise.resolve(null); if ('serviceWorker' in navigator) { - pushControllerReady = navigator.serviceWorker.register('service-worker.js').then(async () => { + pushControllerReady = (workspaceLifecycle.serviceWorkerReady || + navigator.serviceWorker.register('service-worker.js')).then(async () => { await issueCaptureFeatures.load('push-notifications'); const controller = createPushNotifications({ control:qs('#push-updates'), diff --git a/frontend/workspace-bootstrap.js b/frontend/workspace-bootstrap.js index b19102b..f3bcebe 100644 --- a/frontend/workspace-bootstrap.js +++ b/frontend/workspace-bootstrap.js @@ -72,6 +72,8 @@ async function loadWorkspace({ retryButton?.addEventListener?.('click', retryFailed); const handleOnline = () => { cameOnline = true; void retryFailed(); }; window?.addEventListener('online', handleOnline); + const serviceWorkerReady = window?.navigator?.serviceWorker?.register ? + window.navigator.serviceWorker.register('service-worker.js') : Promise.resolve(null); try { await retryOnce('work-core'); @@ -105,7 +107,7 @@ async function loadWorkspace({ }; const hydrationSelector = [ - '[data-mobile-task]:not([data-mobile-task="work"]):not([data-mobile-task="queues"])', + '[data-mobile-task]:not([data-mobile-task="queues"])', '[data-progressive-loading="true"]', '#app-menu-toggle', '#work-settings-toggle', @@ -130,6 +132,7 @@ async function loadWorkspace({ deepLinkReady, workspaceReady, markWorkspaceReady, + serviceWorkerReady, get optionalReady() { return hydrateWorkspace(); }, retryFeature(name) { if (!failed.has(name)) return Promise.resolve(true); diff --git a/tests/e2e/test_human_gates_reopen_release.py b/tests/e2e/test_human_gates_reopen_release.py index c5aa656..a3ea15a 100644 --- a/tests/e2e/test_human_gates_reopen_release.py +++ b/tests/e2e/test_human_gates_reopen_release.py @@ -13,7 +13,7 @@ pytest.importorskip("playwright.sync_api") from playwright.sync_api import expect, sync_playwright from fake_gitea import FakeGiteaServer -from test_mobile_offline_issue_release import ACCESS_TOKEN, ROOT, release_server +from test_mobile_offline_issue_release import ACCESS_TOKEN, ROOT, hydrate_workspace, release_server @pytest.mark.parametrize(("width", "height"), [(320, 568), (390, 844)]) @@ -85,6 +85,7 @@ def test_release_artifact_reopens_human_gates_with_one_fresh_mobile_snapshot( page.locator('input[name="access_token"]').fill(ACCESS_TOKEN) page.locator("#submit-sign-in").click() page.wait_for_url(origin + "/", wait_until="networkidle") + hydrate_workspace(page) page.evaluate("document.querySelector('#open-human-gates').click()") expect(page.locator("#human-gates")).to_be_visible() @@ -244,6 +245,7 @@ def test_release_artifact_reviews_live_human_gate_history_and_receipt_on_phone(t page.locator('input[name="access_token"]').fill(ACCESS_TOKEN) page.locator("#submit-sign-in").click() page.wait_for_url(origin + "/", wait_until="networkidle") + hydrate_workspace(page) page.evaluate("document.querySelector('#open-human-gates').click()") expect(page.locator("#human-gates")).to_be_visible() cached_before = page.evaluate("Object.keys(localStorage).sort()") diff --git a/tests/e2e/test_mobile_offline_issue_release.py b/tests/e2e/test_mobile_offline_issue_release.py index a8a2dab..c2e2fe8 100644 --- a/tests/e2e/test_mobile_offline_issue_release.py +++ b/tests/e2e/test_mobile_offline_issue_release.py @@ -114,6 +114,13 @@ def release_server(archive: Path, tmp_path: Path, gitea_url: str): process.communicate() +def hydrate_workspace(page) -> None: + page.evaluate( + "async () => { const lifecycle = await window.stackchainWorkspaceLifecycle; " + "await lifecycle.hydrateWorkspace(); await lifecycle.workspaceReady; }" + ) + + def indexed_issue_records(page: Page) -> list[dict]: return page.evaluate( """async () => { diff --git a/tests/e2e/test_mobile_photo_draft_inbox_release.py b/tests/e2e/test_mobile_photo_draft_inbox_release.py index f77e9b0..1042b4e 100644 --- a/tests/e2e/test_mobile_photo_draft_inbox_release.py +++ b/tests/e2e/test_mobile_photo_draft_inbox_release.py @@ -12,7 +12,7 @@ pytest.importorskip("playwright.sync_api") from playwright.sync_api import expect, sync_playwright from fake_gitea import FakeGiteaServer -from test_mobile_offline_issue_release import ACCESS_TOKEN, ROOT, release_server +from test_mobile_offline_issue_release import ACCESS_TOKEN, ROOT, hydrate_workspace, release_server def test_release_artifact_finds_and_reopens_photo_only_reply_from_mobile_my_work(tmp_path: Path): @@ -60,6 +60,7 @@ def test_release_artifact_finds_and_reopens_photo_only_reply_from_mobile_my_work })""" ) page.reload(wait_until="networkidle") + hydrate_workspace(page) stored = page.evaluate( """() => new Promise((resolve,reject)=>{ const request=indexedDB.open('stackchain-conversation-reply-drafts-v1',1); diff --git a/tests/e2e/test_mobile_today_summary_release.py b/tests/e2e/test_mobile_today_summary_release.py index c432e18..d576f5f 100644 --- a/tests/e2e/test_mobile_today_summary_release.py +++ b/tests/e2e/test_mobile_today_summary_release.py @@ -12,7 +12,7 @@ pytest.importorskip("playwright.sync_api") from playwright.sync_api import expect, sync_playwright from fake_gitea import FakeGiteaServer -from test_mobile_offline_issue_release import ACCESS_TOKEN, ROOT, release_server +from test_mobile_offline_issue_release import ACCESS_TOKEN, ROOT, hydrate_workspace, release_server @pytest.mark.parametrize(("width", "height"), [(320, 568), (390, 844)]) @@ -50,6 +50,7 @@ def test_release_artifact_reviews_and_shares_a_private_mobile_today_summary( page.locator('input[name="access_token"]').fill(ACCESS_TOKEN) page.locator("#submit-sign-in").click() page.wait_for_url(origin + "/", wait_until="networkidle") + hydrate_workspace(page) page.evaluate( """ diff --git a/tests/e2e/test_mobile_today_wrap_up_release.py b/tests/e2e/test_mobile_today_wrap_up_release.py index ee743c9..1322dfe 100644 --- a/tests/e2e/test_mobile_today_wrap_up_release.py +++ b/tests/e2e/test_mobile_today_wrap_up_release.py @@ -12,7 +12,7 @@ pytest.importorskip("playwright.sync_api") from playwright.sync_api import expect, sync_playwright from fake_gitea import FakeGiteaServer -from test_mobile_offline_issue_release import ACCESS_TOKEN, ROOT, release_server +from test_mobile_offline_issue_release import ACCESS_TOKEN, ROOT, hydrate_workspace, release_server @pytest.mark.parametrize(("width", "height"), [(320, 568), (390, 844)]) @@ -47,6 +47,7 @@ def test_release_artifact_renders_and_applies_mobile_today_wrap_up( page.locator('input[name="access_token"]').fill(ACCESS_TOKEN) page.locator("#submit-sign-in").click() page.wait_for_url(origin + "/", wait_until="networkidle") + hydrate_workspace(page) page.evaluate( """ diff --git a/tests/e2e/test_mobile_tomorrow_conflict_release.py b/tests/e2e/test_mobile_tomorrow_conflict_release.py index 086d219..278f33e 100644 --- a/tests/e2e/test_mobile_tomorrow_conflict_release.py +++ b/tests/e2e/test_mobile_tomorrow_conflict_release.py @@ -13,7 +13,7 @@ pytest.importorskip("playwright.sync_api") from playwright.sync_api import expect, sync_playwright from fake_gitea import FakeGiteaServer -from test_mobile_offline_issue_release import ACCESS_TOKEN, ROOT, release_server +from test_mobile_offline_issue_release import ACCESS_TOKEN, ROOT, hydrate_workspace, release_server @pytest.mark.parametrize(("width", "height"), [(320, 568), (390, 844)]) @@ -71,6 +71,7 @@ def test_release_artifact_resolves_cross_device_tomorrow_conflicts_on_mobile( page.locator('input[name="access_token"]').fill(ACCESS_TOKEN) page.locator("#submit-sign-in").click() page.wait_for_url(origin + "/", wait_until="networkidle") + hydrate_workspace(page) def stage_phone_plan(): page.evaluate( diff --git a/tests/e2e/test_mobile_wrap_up_handoff_release.py b/tests/e2e/test_mobile_wrap_up_handoff_release.py index a6d2553..bfa614c 100644 --- a/tests/e2e/test_mobile_wrap_up_handoff_release.py +++ b/tests/e2e/test_mobile_wrap_up_handoff_release.py @@ -12,7 +12,7 @@ pytest.importorskip("playwright.sync_api") from playwright.sync_api import expect, sync_playwright from fake_gitea import FakeGiteaServer -from test_mobile_offline_issue_release import ACCESS_TOKEN, ROOT, release_server +from test_mobile_offline_issue_release import ACCESS_TOKEN, ROOT, hydrate_workspace, release_server def test_release_artifact_reviews_wrap_up_commitments_on_a_phone(tmp_path: Path): @@ -36,6 +36,7 @@ def test_release_artifact_reviews_wrap_up_commitments_on_a_phone(tmp_path: Path) 'issue:acme/mobile:41:', 'issue:acme/mobile:42:' ]))""") page.reload(wait_until="networkidle") + hydrate_workspace(page) dialog = page.locator("#today-handoff-dialog") expect(dialog).to_be_visible() diff --git a/tests/test_workspace_bootstrap.py b/tests/test_workspace_bootstrap.py index ee1481c..fc7476d 100644 --- a/tests/test_workspace_bootstrap.py +++ b/tests/test_workspace_bootstrap.py @@ -72,7 +72,7 @@ const createLoader=()=>({load:async name=>{ let lifecycle=await loadWorkspace({document,createLoader}); let prevented=0,stopped=0,replayed=0,actionReady=false,replayedReady=false; const target={ - closest(selector){return selector.includes('data-mobile-task') ? this : null;}, + closest(selector){return selector.includes(':not([data-mobile-task="work"])') ? null : this;}, click(){replayed++; replayedReady=actionReady;}, }; await listeners.click?.callback({target,preventDefault(){prevented++;},stopImmediatePropagation(){stopped++;}});