From 238b91aa597f0d31a95e6736f6c7b6a29768aed6 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Fri, 17 Jul 2026 17:34:29 +0200 Subject: [PATCH] test(e2e): accept beforeunload prompt in shared dialog fallback (#9122) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The USE_REMOTE crash-resume spec reloads Client B mid-sync. While isSyncInProgress is set, startup.service's beforeunload handler calls preventDefault(), so Chrome raises a beforeunload prompt (the preceding button click supplies the user activation it requires). Playwright auto-accepts such prompts only when no 'dialog' listener is registered — but installDevErrorDialogHandler registers one on every page and early- returned on beforeunload, leaving the prompt unanswered. The navigation then never starts and reload() times out (observed on SuperSync shard 6/6; the earlier load→domcontentloaded de-flake could not help because navigation is blocked before any lifecycle event). Accept beforeunload in the shared fallback, restoring Playwright's default; other dialog types keep flowing to spec-specific handlers. Corrects the two crash-resume comments that encoded the wrong theory. --- .../supersync-use-remote-crash-resume.spec.ts | 17 ++++++++--------- e2e/utils/runtime-errors.ts | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/e2e/tests/sync/supersync-use-remote-crash-resume.spec.ts b/e2e/tests/sync/supersync-use-remote-crash-resume.spec.ts index 78d00acdd8..29e180fd2e 100644 --- a/e2e/tests/sync/supersync-use-remote-crash-resume.spec.ts +++ b/e2e/tests/sync/supersync-use-remote-crash-resume.spec.ts @@ -97,11 +97,8 @@ test.describe('@supersync USE_REMOTE interrupted rebuild recovery', () => { rebuildCommittedLog: REBUILD_COMMITTED_LOG, }, ); - // Wait only for `domcontentloaded`, not the default `load`: an active - // SuperSync WebSocket/sync connection can keep the page "loading" so the - // `load` event never fires and `reload()` times out (flaky). `reload()` - // preserves sessionStorage (unlike close()+newPage()), which this test - // needs, and `waitForAppReady` below is the real readiness gate. + // `reload()` preserves sessionStorage (unlike close()+newPage()), which + // this test needs; `waitForAppReady` below is the real readiness gate. await clientB.page.reload({ waitUntil: 'domcontentloaded', timeout: 60000 }); await waitForAppReady(clientB.page); @@ -136,10 +133,12 @@ test.describe('@supersync USE_REMOTE interrupted rebuild recovery', () => { await clientB.sync.syncImportUseRemoteBtn.click(); await crashObserved; - // domcontentloaded (not the default `load`) — see the note on the first - // reload above: an active sync connection can block `load` and hang - // `reload()`. sessionStorage survives the reload; the assertion below - // depends on it. + // This reload races the crashed sync's unwind: while `isSyncInProgress` is + // still set, startup.service's beforeunload handler prevents the unload and + // Chrome prompts (the click above supplies the required user activation). + // installDevErrorDialogHandler accepts that prompt — without it the + // navigation never starts and this hangs. sessionStorage survives the + // reload; the assertion below depends on it. await clientB.page.reload({ waitUntil: 'domcontentloaded', timeout: 60000 }); await waitForAppReady(clientB.page); expect( diff --git a/e2e/utils/runtime-errors.ts b/e2e/utils/runtime-errors.ts index bddcf68870..031f45b8f8 100644 --- a/e2e/utils/runtime-errors.ts +++ b/e2e/utils/runtime-errors.ts @@ -37,6 +37,21 @@ export const attachPageErrorCollector = ( // explicitly, or it will be vulnerable to the same hang. export const installDevErrorDialogHandler = (page: Page, label: string): void => { page.on('dialog', async (dialog) => { + // Registering ANY 'dialog' listener switches off Playwright's built-in + // handling (accept for beforeunload, dismiss for everything else). This + // fallback is installed on every page, so it must restore that default for + // beforeunload: startup.service raises one whenever a sync is in flight, and + // an unanswered prompt blocks the navigation before it starts — reload() + // then times out on any waitUntil, however early. + if (dialog.type() === 'beforeunload') { + try { + await dialog.accept(); + } catch { + // Already handled by another listener — ignore. + } + return; + } + const message = dialog.message(); const isDevErrorAlert = dialog.type() === 'alert' && message.startsWith('devERR:'); const isDevErrorConfirm =