test(e2e): accept beforeunload prompt in shared dialog fallback (#9122)

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.
This commit is contained in:
Johannes Millan 2026-07-17 17:34:29 +02:00 committed by GitHub
parent c18055a341
commit 238b91aa59
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 23 additions and 9 deletions

View file

@ -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(

View file

@ -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 =