mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 00:46:45 +00:00
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.
110 lines
3.5 KiB
TypeScript
110 lines
3.5 KiB
TypeScript
import type { BrowserContext, Page } from '@playwright/test';
|
||
|
||
export type RuntimeBrowserError = {
|
||
type: 'pageerror';
|
||
message: string;
|
||
};
|
||
|
||
export const attachPageErrorCollector = (
|
||
page: Page,
|
||
label: string,
|
||
): RuntimeBrowserError[] => {
|
||
const errors: RuntimeBrowserError[] = [];
|
||
|
||
page.on('pageerror', (error) => {
|
||
const message = error.stack ?? error.message;
|
||
console.error(`[${label}] Page error:`, message);
|
||
errors.push({
|
||
type: 'pageerror',
|
||
message,
|
||
});
|
||
});
|
||
|
||
return errors;
|
||
};
|
||
|
||
// devError() in non-production builds opens window.alert("devERR: …") and then
|
||
// window.confirm("Throw an error for error? ––– …"). Both are page-blocking and
|
||
// will hang any Playwright action (goto/click/waitFor) until handled. Tests
|
||
// that observe these dialogs explicitly can attach their own listener first;
|
||
// this fallback dismisses anything matching the devError shape so a stray call
|
||
// (e.g. selectProjectById on a project that sync just removed) fails the test
|
||
// with its real assertion instead of a 9-minute timeout.
|
||
//
|
||
// Call this on every Page created in E2E. It is wired into the default test
|
||
// fixture, setupSyncClient, createSimulatedClient, and createLegacyMigratedClient;
|
||
// any spec that builds its own context via browser.newContext() must invoke it
|
||
// 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 =
|
||
dialog.type() === 'confirm' && message.startsWith('Throw an error for error?');
|
||
if (!isDevErrorAlert && !isDevErrorConfirm) {
|
||
return;
|
||
}
|
||
console.warn(
|
||
`[${label}] Auto-dismissing devError dialog (${dialog.type()}): ${message}`,
|
||
);
|
||
try {
|
||
await dialog.dismiss();
|
||
} catch {
|
||
// Already handled by another listener — ignore.
|
||
}
|
||
});
|
||
};
|
||
|
||
export const assertNoRuntimeBrowserErrors = (
|
||
errors: RuntimeBrowserError[],
|
||
label: string,
|
||
): void => {
|
||
if (errors.length === 0) {
|
||
return;
|
||
}
|
||
|
||
throw new Error(
|
||
`[${label}] Browser runtime errors were emitted during the test:\n${errors
|
||
.map((error, index) => `${index + 1}. ${error.type}: ${error.message}`)
|
||
.join('\n')}`,
|
||
);
|
||
};
|
||
|
||
export const guardContextCloseWithRuntimeErrorCheck = (
|
||
context: BrowserContext,
|
||
errors: RuntimeBrowserError[],
|
||
label: string,
|
||
): void => {
|
||
const originalClose = context.close.bind(context);
|
||
|
||
context.close = async (...args: Parameters<BrowserContext['close']>) => {
|
||
let closeError: unknown;
|
||
|
||
try {
|
||
await originalClose(...args);
|
||
} catch (error) {
|
||
closeError = error;
|
||
}
|
||
|
||
assertNoRuntimeBrowserErrors(errors, label);
|
||
|
||
if (closeError) {
|
||
throw closeError;
|
||
}
|
||
};
|
||
};
|