test(e2e): dismiss devError native dialogs so sync tests don't hang

devError() in non-production builds opens window.alert("devERR: …") then
window.confirm("Throw an error for error? ––– …"). Both are page-blocking,
so any Playwright call (goto/click/waitFor) hangs until the dialog is
handled. After the project-task-page signal subscription added in
34af7b103, selectProjectById fires devError whenever sync removes the
project the user is currently viewing — which masters' webdav delete-
cascade and supersync legacy-migration tests trigger by design.

Add installDevErrorDialogHandler that matches the exact two devError
shapes and dismisses them, then wire it into the four E2E page-creation
sites: the default test fixture, setupSyncClient, createSimulatedClient,
and createLegacyMigratedClient. Production code is untouched.

Also carve out the devError confirm in setupSyncClient's strict confirm
validator so it doesn't raise an unhandled rejection alongside our
dismiss, and drop the unused createLegacyMigratedClientNoDialogHandler
wrapper (it now lies — the underlying helper does install a handler).
This commit is contained in:
johannesjo 2026-05-23 23:06:21 +02:00
parent ebf29ed01a
commit f1d9161d13
5 changed files with 45 additions and 20 deletions

View file

@ -14,6 +14,7 @@ import { skipOnboardingForE2E, waitForAppReady } from '../utils/waits';
import {
assertNoRuntimeBrowserErrors,
attachPageErrorCollector,
installDevErrorDialogHandler,
} from '../utils/runtime-errors';
type TestFixtures = {
@ -73,6 +74,7 @@ export const test = base.extend<TestFixtures>({
// when the test fails for another reason), then aggregated and thrown at teardown
// if the test otherwise passed.
const runtimeErrors = attachPageErrorCollector(page, 'page');
installDevErrorDialogHandler(page, 'page');
// Skip onboarding, hints, and example tasks before the app boots.
// This runs before any page JavaScript, so Angular sees the flags immediately.

View file

@ -1,6 +1,7 @@
import { expect, type Browser, type BrowserContext, type Page } from '@playwright/test';
import { skipOnboardingForE2E, waitForAppReady } from './waits';
import { MIGRATION_BACKUP_PREFIX } from '../../electron/shared-with-frontend/get-backup-timestamp';
import { installDevErrorDialogHandler } from './runtime-errors';
/**
* Legacy Migration E2E Test Helpers
@ -96,6 +97,7 @@ export const createLegacyMigratedClient = async (
page.on('pageerror', (error) => {
console.error(`[Legacy Client ${clientName}] Page error:`, error.message);
});
installDevErrorDialogHandler(page, `Legacy Client ${clientName}`);
// Block JS to seed database before app initializes
await page.route('**/*.js', async (route) => {
@ -151,26 +153,6 @@ export const createLegacyMigratedClient = async (
return { context, page };
};
/**
* Create a legacy-migrated client without auto-accepting dialogs.
* Use this when you need to interact with conflict dialogs manually.
*
* @param browser - Playwright browser instance
* @param baseURL - App base URL
* @param legacyData - Legacy data to seed
* @param clientName - Human-readable name for debugging
*/
export const createLegacyMigratedClientNoDialogHandler = async (
browser: Browser,
baseURL: string,
legacyData: Record<string, unknown>,
clientName: string,
): Promise<{ context: BrowserContext; page: Page }> => {
// Same as createLegacyMigratedClient but doesn't add dialog handlers
// This is useful for conflict tests where we need to observe dialogs
return createLegacyMigratedClient(browser, baseURL, legacyData, clientName);
};
/**
* Close a legacy-migrated client and clean up resources.
* Safely handles already-closed contexts.

View file

@ -23,6 +23,38 @@ export const attachPageErrorCollector = (
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) => {
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,

View file

@ -25,6 +25,7 @@ import {
import {
assertNoRuntimeBrowserErrors,
attachPageErrorCollector,
installDevErrorDialogHandler,
type RuntimeBrowserError,
} from './runtime-errors';
@ -223,6 +224,7 @@ export const createSimulatedClient = async (
const page = await context.newPage();
const runtimeErrors = attachPageErrorCollector(page, `Client ${clientName}`);
installDevErrorDialogHandler(page, `Client ${clientName}`);
// Skip onboarding, hints, and example tasks before the app boots.
// This runs before any page JavaScript, so Angular sees the flags immediately.

View file

@ -10,6 +10,7 @@ import type { SyncPage } from '../pages/sync.page';
import {
attachPageErrorCollector,
guardContextCloseWithRuntimeErrorCheck,
installDevErrorDialogHandler,
} from './runtime-errors';
/**
@ -107,6 +108,7 @@ export const setupSyncClient = async (
const context = await browser.newContext({ baseURL });
const page = await context.newPage();
const pageErrors = attachPageErrorCollector(page, 'WebDAV sync client');
installDevErrorDialogHandler(page, 'WebDAV sync client');
guardContextCloseWithRuntimeErrorCheck(context, pageErrors, 'WebDAV sync client');
// Skip onboarding, hints, and example tasks before the app boots.
@ -123,6 +125,11 @@ export const setupSyncClient = async (
page.on('dialog', async (dialog) => {
if (dialog.type() === 'confirm') {
const message = dialog.message();
// devError's "Throw an error for error? …" confirm is handled by
// installDevErrorDialogHandler above; don't trip the strict validator.
if (message.startsWith('Throw an error for error?')) {
return;
}
// Validate this is the expected fresh client sync confirmation
const expectedPatterns = [/fresh/i, /remote/i, /sync/i, /operations/i];
const isExpectedDialog = expectedPatterns.some((pattern) => pattern.test(message));