From 93cbcd7e449db35b928b4343855fc8f6c10b5b1f Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Fri, 13 Feb 2026 12:47:14 +0100 Subject: [PATCH] test(e2e): fix reliability issues in supersync E2E tests - Replace page.reload() with close-and-reopen pattern after backup imports to prevent hangs with active sync connections - Add proper sync-import-conflict dialog handling via Promise.race - Fix browser context leak in provider-switch test - Replace baseURL! non-null assertions with fallback pattern - Replace fixed timeouts with element-based waits in cascade-delete - Extract duplicate dialog-dismissal loop into dismissBlockingDialogs helper in waits.ts - Add missing WorkViewPage/SuperSyncPage imports in same-client test - Replace any type with proper VectorClockEntry interface --- .../sync/supersync-cascade-delete.spec.ts | 9 +- .../sync/supersync-import-clean-slate.spec.ts | 139 ++++++++++++------ .../supersync-import-other-client-ops.spec.ts | 27 ++-- .../supersync-import-same-client-ops.spec.ts | 28 +++- .../supersync-lastseq-preservation.spec.ts | 40 ++++- .../sync/supersync-provider-switch.spec.ts | 8 +- e2e/utils/waits.ts | 41 +++--- 7 files changed, 194 insertions(+), 98 deletions(-) diff --git a/e2e/tests/sync/supersync-cascade-delete.spec.ts b/e2e/tests/sync/supersync-cascade-delete.spec.ts index ea1a8c92e1..3bc663ae9b 100644 --- a/e2e/tests/sync/supersync-cascade-delete.spec.ts +++ b/e2e/tests/sync/supersync-cascade-delete.spec.ts @@ -33,8 +33,10 @@ const navigateToProject = async ( const projectNavItem = page.locator(`nav-item:has-text("${projectName}")`).first(); await projectNavItem.waitFor({ state: 'visible', timeout: 10000 }); await projectNavItem.click(); - await page.waitForLoadState('networkidle'); - await page.waitForTimeout(500); + await page + .locator('.route-wrapper') + .first() + .waitFor({ state: 'visible', timeout: 10000 }); }; /** @@ -60,7 +62,8 @@ const deleteProjectViaContextMenu = async ( await confirmBtn.waitFor({ state: 'visible', timeout: 5000 }); await confirmBtn.click(); - await page.waitForTimeout(1000); + // Wait for confirmation dialog to close + await page.locator('dialog-confirm').waitFor({ state: 'hidden', timeout: 5000 }); }; test.describe('@supersync Cross-Entity Cascade Delete', () => { diff --git a/e2e/tests/sync/supersync-import-clean-slate.spec.ts b/e2e/tests/sync/supersync-import-clean-slate.spec.ts index 60e3b577e6..d4c41fa808 100644 --- a/e2e/tests/sync/supersync-import-clean-slate.spec.ts +++ b/e2e/tests/sync/supersync-import-clean-slate.spec.ts @@ -8,6 +8,9 @@ import { type SimulatedE2EClient, } from '../../utils/supersync-helpers'; import { ImportPage } from '../../pages/import.page'; +import { SuperSyncPage } from '../../pages/supersync.page'; +import { WorkViewPage } from '../../pages/work-view.page'; +import { waitForAppReady } from '../../utils/waits'; /** * SuperSync Import Clean Slate E2E Tests @@ -58,6 +61,7 @@ test.describe('@supersync @cleanslate Import Clean Slate Semantics', () => { testRunId, }) => { const uniqueId = Date.now(); + const appUrl = baseURL || 'http://localhost:4242'; let clientA: SimulatedE2EClient | null = null; let clientB: SimulatedE2EClient | null = null; @@ -68,10 +72,10 @@ test.describe('@supersync @cleanslate Import Clean Slate Semantics', () => { // ============ PHASE 1: Setup Both Clients ============ console.log('[Clean Slate] Phase 1: Setting up both clients'); - clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId); + clientA = await createSimulatedClient(browser, appUrl, 'A', testRunId); await clientA.sync.setupSuperSync(syncConfig); - clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId); + clientB = await createSimulatedClient(browser, appUrl, 'B', testRunId); await clientB.sync.setupSuperSync(syncConfig); // ============ PHASE 2: Create Pre-Import Tasks on Both Clients ============ @@ -111,17 +115,36 @@ test.describe('@supersync @cleanslate Import Clean Slate Semantics', () => { await importPage.importBackupFile(backupPath); console.log('[Clean Slate] Client A imported backup'); - // Reload page after import to ensure UI reflects the imported state - // (bulk state updates from BACKUP_IMPORT may not trigger component re-renders) - // Use goto instead of reload - more reliable with service workers - await clientA.page.goto(clientA.page.url(), { - waitUntil: 'domcontentloaded', - timeout: 30000, - }); - await clientA.page.waitForLoadState('networkidle'); + // Close and re-open page to pick up imported data with fresh Angular services. + // Using page.reload() can hang when active sync connections prevent navigation. + await clientA.page.close(); + clientA.page = await clientA.context.newPage(); + await clientA.page.goto('/'); + clientA.workView = new WorkViewPage(clientA.page, `A-${testRunId}`); + clientA.sync = new SuperSyncPage(clientA.page); + await waitForAppReady(clientA.page, { ensureRoute: false }); - // Re-enable sync after import (import overwrites globalConfig) - await clientA.sync.setupSuperSync(syncConfig); + // Configure sync WITHOUT waiting for initial sync + // (initial sync will show sync-import-conflict dialog since we have a local BackupImport) + await clientA.sync.setupSuperSync({ ...syncConfig, waitForInitialSync: false }); + + // Wait for either sync import conflict dialog OR sync completion + const syncImportDialog = clientA.sync.syncImportConflictDialog; + const syncResult = await Promise.race([ + syncImportDialog + .waitFor({ state: 'visible', timeout: 30000 }) + .then(() => 'dialog' as const), + clientA.sync.syncCheckIcon + .waitFor({ state: 'visible', timeout: 30000 }) + .then(() => 'complete' as const), + ]); + + if (syncResult === 'dialog') { + // Choose "Use My Data" to preserve the import + await clientA.sync.syncImportUseLocalBtn.click(); + await syncImportDialog.waitFor({ state: 'hidden', timeout: 5000 }); + await clientA.sync.syncCheckIcon.waitFor({ state: 'visible', timeout: 30000 }); + } // Wait for imported task to be visible await waitForTask(clientA.page, 'E2E Import Test - Active Task With Subtask'); @@ -234,6 +257,7 @@ test.describe('@supersync @cleanslate Import Clean Slate Semantics', () => { testRunId, }) => { const uniqueId = Date.now(); + const appUrl = baseURL || 'http://localhost:4242'; let clientA: SimulatedE2EClient | null = null; let clientB: SimulatedE2EClient | null = null; @@ -244,7 +268,7 @@ test.describe('@supersync @cleanslate Import Clean Slate Semantics', () => { // ============ PHASE 1: Client B Creates and Syncs ============ console.log('[Late Joiner] Phase 1: Client B creates and syncs'); - clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId); + clientB = await createSimulatedClient(browser, appUrl, 'B', testRunId); await clientB.sync.setupSuperSync(syncConfig); // Client B creates and syncs a task @@ -260,7 +284,7 @@ test.describe('@supersync @cleanslate Import Clean Slate Semantics', () => { // ============ PHASE 2: Client A Imports (Without Syncing First) ============ console.log('[Late Joiner] Phase 2: Client A imports backup'); - clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId); + clientA = await createSimulatedClient(browser, appUrl, 'A', testRunId); await clientA.sync.setupSuperSync(syncConfig); // DO NOT sync before import - A doesn't know about B's task @@ -273,16 +297,33 @@ test.describe('@supersync @cleanslate Import Clean Slate Semantics', () => { await importPage.importBackupFile(backupPath); console.log('[Late Joiner] Client A imported backup'); - // Reload page after import to ensure UI reflects the imported state - // Use goto instead of reload - more reliable with service workers - await clientA.page.goto(clientA.page.url(), { - waitUntil: 'domcontentloaded', - timeout: 30000, - }); - await clientA.page.waitForLoadState('networkidle'); + // Close and re-open page to pick up imported data with fresh Angular services. + await clientA.page.close(); + clientA.page = await clientA.context.newPage(); + await clientA.page.goto('/'); + clientA.workView = new WorkViewPage(clientA.page, `A-${testRunId}`); + clientA.sync = new SuperSyncPage(clientA.page); + await waitForAppReady(clientA.page, { ensureRoute: false }); - // Re-enable sync after import - await clientA.sync.setupSuperSync(syncConfig); + // Configure sync WITHOUT waiting for initial sync + await clientA.sync.setupSuperSync({ ...syncConfig, waitForInitialSync: false }); + + // Wait for either sync import conflict dialog OR sync completion + const syncImportDialog2 = clientA.sync.syncImportConflictDialog; + const syncResult2 = await Promise.race([ + syncImportDialog2 + .waitFor({ state: 'visible', timeout: 30000 }) + .then(() => 'dialog' as const), + clientA.sync.syncCheckIcon + .waitFor({ state: 'visible', timeout: 30000 }) + .then(() => 'complete' as const), + ]); + + if (syncResult2 === 'dialog') { + await clientA.sync.syncImportUseLocalBtn.click(); + await syncImportDialog2.waitFor({ state: 'hidden', timeout: 5000 }); + await clientA.sync.syncCheckIcon.waitFor({ state: 'visible', timeout: 30000 }); + } // ============ PHASE 3: Both Clients Sync ============ console.log('[Late Joiner] Phase 3: Both clients sync'); @@ -361,6 +402,7 @@ test.describe('@supersync @cleanslate Import Clean Slate Semantics', () => { testRunId, }) => { const uniqueId = Date.now(); + const appUrl = baseURL || 'http://localhost:4242'; let clientA: SimulatedE2EClient | null = null; let clientB: SimulatedE2EClient | null = null; @@ -373,11 +415,11 @@ test.describe('@supersync @cleanslate Import Clean Slate Semantics', () => { '[Pending Invalidation] Phase 1: Setting up both clients with initial sync', ); - clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId); + clientA = await createSimulatedClient(browser, appUrl, 'A', testRunId); await clientA.sync.setupSuperSync(syncConfig); await clientA.sync.syncAndWait(); // Initial sync to establish vector clock - clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId); + clientB = await createSimulatedClient(browser, appUrl, 'B', testRunId); await clientB.sync.setupSuperSync(syncConfig); await clientB.sync.syncAndWait(); // Initial sync to establish vector clock @@ -411,31 +453,32 @@ test.describe('@supersync @cleanslate Import Clean Slate Semantics', () => { await importPage.importBackupFile(backupPath); console.log('[Pending Invalidation] Client A imported backup'); - // Reload page after import to ensure UI reflects the imported state - // Use goto instead of reload - more reliable with service workers - await clientA.page.goto(clientA.page.url(), { - waitUntil: 'domcontentloaded', - timeout: 30000, - }); - await clientA.page.waitForLoadState('networkidle'); + // Close and re-open page to pick up imported data with fresh Angular services. + await clientA.page.close(); + clientA.page = await clientA.context.newPage(); + await clientA.page.goto('/'); + clientA.workView = new WorkViewPage(clientA.page, `A-${testRunId}`); + clientA.sync = new SuperSyncPage(clientA.page); + await waitForAppReady(clientA.page, { ensureRoute: false }); - // Re-enable sync after import (import overwrites globalConfig) - // Use waitForInitialSync: false because a conflict dialog may appear - await clientA.sync.setupSuperSync({ - ...syncConfig, - waitForInitialSync: false, - }); + // Configure sync WITHOUT waiting for initial sync + await clientA.sync.setupSuperSync({ ...syncConfig, waitForInitialSync: false }); - // Handle sync conflict dialog if it appears (server has changes from initial sync) - const conflictDialog = clientA.page.locator( - 'dialog-handle-import-conflict, mat-dialog-container:has-text("Sync Conflict")', - ); - if (await conflictDialog.isVisible({ timeout: 3000 }).catch(() => false)) { - console.log('[Pending Invalidation] Handling sync conflict dialog'); - // Choose "Use My Data" to keep imported data (clean slate) - const useMyDataBtn = conflictDialog.locator('button:has-text("Use My Data")'); - await useMyDataBtn.click(); - await conflictDialog.waitFor({ state: 'hidden', timeout: 10000 }); + // Wait for either sync import conflict dialog OR sync completion + const syncImportDialog3 = clientA.sync.syncImportConflictDialog; + const syncResult3 = await Promise.race([ + syncImportDialog3 + .waitFor({ state: 'visible', timeout: 30000 }) + .then(() => 'dialog' as const), + clientA.sync.syncCheckIcon + .waitFor({ state: 'visible', timeout: 30000 }) + .then(() => 'complete' as const), + ]); + + if (syncResult3 === 'dialog') { + await clientA.sync.syncImportUseLocalBtn.click(); + await syncImportDialog3.waitFor({ state: 'hidden', timeout: 5000 }); + await clientA.sync.syncCheckIcon.waitFor({ state: 'visible', timeout: 30000 }); } // Wait for imported task to be visible diff --git a/e2e/tests/sync/supersync-import-other-client-ops.spec.ts b/e2e/tests/sync/supersync-import-other-client-ops.spec.ts index 27af6f8a40..34657d31e1 100644 --- a/e2e/tests/sync/supersync-import-other-client-ops.spec.ts +++ b/e2e/tests/sync/supersync-import-other-client-ops.spec.ts @@ -62,6 +62,7 @@ test.describe('@supersync @pruning Other client post-import ops sync correctly', testRunId, }) => { const uniqueId = `${testRunId}-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`; + const appUrl = baseURL || 'http://localhost:4242'; let clientA: SimulatedE2EClient | null = null; let clientB: SimulatedE2EClient | null = null; @@ -72,10 +73,10 @@ test.describe('@supersync @pruning Other client post-import ops sync correctly', // ============ PHASE 1: Setup Both Clients ============ console.log('[Other-Client Import] Phase 1: Setting up both clients'); - clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId); + clientA = await createSimulatedClient(browser, appUrl, 'A', testRunId); await clientA.sync.setupSuperSync(syncConfig); - clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId); + clientB = await createSimulatedClient(browser, appUrl, 'B', testRunId); await clientB.sync.setupSuperSync(syncConfig); // Initial sync to establish vector clocks @@ -164,6 +165,11 @@ test.describe('@supersync @pruning Other client post-import ops sync correctly', ); await clientB.page.evaluate(async () => { + interface VectorClockEntry { + clock: Record; + lastUpdate: number; + } + const DB_NAME = 'SUP_OPS'; const VECTOR_CLOCK_STORE = 'vector_clock'; const SINGLETON_KEY = 'current'; @@ -176,13 +182,16 @@ test.describe('@supersync @pruning Other client post-import ops sync correctly', }); // Read current vector clock entry - const currentEntry = await new Promise((resolve, reject) => { - const tx = db.transaction(VECTOR_CLOCK_STORE, 'readonly'); - const store = tx.objectStore(VECTOR_CLOCK_STORE); - const request = store.get(SINGLETON_KEY); - request.onsuccess = (): void => resolve(request.result); - request.onerror = (): void => reject(request.error); - }); + const currentEntry = await new Promise( + (resolve, reject) => { + const tx = db.transaction(VECTOR_CLOCK_STORE, 'readonly'); + const store = tx.objectStore(VECTOR_CLOCK_STORE); + const request = store.get(SINGLETON_KEY); + request.onsuccess = (): void => + resolve(request.result as VectorClockEntry | undefined); + request.onerror = (): void => reject(request.error); + }, + ); const existingClock = currentEntry?.clock ?? {}; console.log( diff --git a/e2e/tests/sync/supersync-import-same-client-ops.spec.ts b/e2e/tests/sync/supersync-import-same-client-ops.spec.ts index a2a950b08a..4004793f6f 100644 --- a/e2e/tests/sync/supersync-import-same-client-ops.spec.ts +++ b/e2e/tests/sync/supersync-import-same-client-ops.spec.ts @@ -8,6 +8,8 @@ import { type SimulatedE2EClient, } from '../../utils/supersync-helpers'; import { ImportPage } from '../../pages/import.page'; +import { SuperSyncPage } from '../../pages/supersync.page'; +import { WorkViewPage } from '../../pages/work-view.page'; import { expectTaskOnAllClients } from '../../utils/supersync-assertions'; import { waitForAppReady } from '../../utils/waits'; @@ -55,6 +57,7 @@ test.describe('@supersync @pruning Import client post-import ops sync correctly' testRunId, }) => { const uniqueId = `${testRunId}-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`; + const appUrl = baseURL || 'http://localhost:4242'; let clientA: SimulatedE2EClient | null = null; let clientB: SimulatedE2EClient | null = null; @@ -65,10 +68,10 @@ test.describe('@supersync @pruning Import client post-import ops sync correctly' // ============ PHASE 1: Setup Both Clients ============ console.log('[Same-Client Import] Phase 1: Setting up both clients'); - clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId); + clientA = await createSimulatedClient(browser, appUrl, 'A', testRunId); await clientA.sync.setupSuperSync(syncConfig); - clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId); + clientB = await createSimulatedClient(browser, appUrl, 'B', testRunId); await clientB.sync.setupSuperSync(syncConfig); // Initial sync to establish vector clocks @@ -86,8 +89,13 @@ test.describe('@supersync @pruning Import client post-import ops sync correctly' await importPage.importBackupFile(backupPath); console.log('[Same-Client Import] Client A imported backup'); - // Reload and re-enable sync - await clientA.page.reload({ timeout: 60000 }); + // Close and re-open page to pick up imported data with fresh Angular services. + // Using page.reload() can hang when active sync connections prevent navigation. + await clientA.page.close(); + clientA.page = await clientA.context.newPage(); + await clientA.page.goto('/'); + clientA.workView = new WorkViewPage(clientA.page, `A-${testRunId}`); + clientA.sync = new SuperSyncPage(clientA.page); await waitForAppReady(clientA.page, { ensureRoute: false }); // Configure sync WITHOUT waiting for initial sync @@ -209,6 +217,7 @@ test.describe('@supersync @pruning Import client post-import ops sync correctly' testRunId, }) => { const uniqueId = `${testRunId}-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`; + const appUrl = baseURL || 'http://localhost:4242'; let clientA: SimulatedE2EClient | null = null; let clientB: SimulatedE2EClient | null = null; @@ -217,10 +226,10 @@ test.describe('@supersync @pruning Import client post-import ops sync correctly' const syncConfig = getSuperSyncConfig(user); // Setup - clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId); + clientA = await createSimulatedClient(browser, appUrl, 'A', testRunId); await clientA.sync.setupSuperSync(syncConfig); - clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId); + clientB = await createSimulatedClient(browser, appUrl, 'B', testRunId); await clientB.sync.setupSuperSync(syncConfig); await clientA.sync.syncAndWait(); @@ -232,7 +241,12 @@ test.describe('@supersync @pruning Import client post-import ops sync correctly' const backupPath = ImportPage.getFixturePath('test-backup.json'); await importPage.importBackupFile(backupPath); - await clientA.page.reload({ timeout: 60000 }); + // Close and re-open page to pick up imported data with fresh Angular services. + await clientA.page.close(); + clientA.page = await clientA.context.newPage(); + await clientA.page.goto('/'); + clientA.workView = new WorkViewPage(clientA.page, `A-${testRunId}`); + clientA.sync = new SuperSyncPage(clientA.page); await waitForAppReady(clientA.page, { ensureRoute: false }); // Configure sync WITHOUT waiting for initial sync (same dialog handling as test 1) diff --git a/e2e/tests/sync/supersync-lastseq-preservation.spec.ts b/e2e/tests/sync/supersync-lastseq-preservation.spec.ts index fe295dd769..33b801257d 100644 --- a/e2e/tests/sync/supersync-lastseq-preservation.spec.ts +++ b/e2e/tests/sync/supersync-lastseq-preservation.spec.ts @@ -8,6 +8,9 @@ import { type SimulatedE2EClient, } from '../../utils/supersync-helpers'; import { ImportPage } from '../../pages/import.page'; +import { SuperSyncPage } from '../../pages/supersync.page'; +import { WorkViewPage } from '../../pages/work-view.page'; +import { waitForAppReady } from '../../utils/waits'; /** * SuperSync lastSeq Preservation Regression Tests @@ -89,15 +92,36 @@ test.describe('@supersync lastSeq Preservation After Import', () => { await importPage.importBackupFile(backupPath); console.log('[lastSeq] Client A imported backup'); - // Reload page after import - await clientA.page.goto(clientA.page.url(), { - waitUntil: 'domcontentloaded', - timeout: 30000, - }); - await clientA.page.waitForLoadState('networkidle'); + // Close and re-open page to pick up imported data with fresh Angular services. + // Using page.reload() can hang when active sync connections prevent navigation. + await clientA.page.close(); + clientA.page = await clientA.context.newPage(); + await clientA.page.goto('/'); + clientA.workView = new WorkViewPage(clientA.page, `A-${testRunId}`); + clientA.sync = new SuperSyncPage(clientA.page); + await waitForAppReady(clientA.page, { ensureRoute: false }); - // Re-enable sync after import (import overwrites globalConfig) - await clientA.sync.setupSuperSync(syncConfig); + // Configure sync WITHOUT waiting for initial sync + // (initial sync will show sync-import-conflict dialog since we have a local BackupImport) + await clientA.sync.setupSuperSync({ ...syncConfig, waitForInitialSync: false }); + + // Wait for either sync import conflict dialog OR sync completion + const syncImportDialog = clientA.sync.syncImportConflictDialog; + const syncResult = await Promise.race([ + syncImportDialog + .waitFor({ state: 'visible', timeout: 30000 }) + .then(() => 'dialog' as const), + clientA.sync.syncCheckIcon + .waitFor({ state: 'visible', timeout: 30000 }) + .then(() => 'complete' as const), + ]); + + if (syncResult === 'dialog') { + // Choose "Use My Data" to preserve the import + await clientA.sync.syncImportUseLocalBtn.click(); + await syncImportDialog.waitFor({ state: 'hidden', timeout: 5000 }); + await clientA.sync.syncCheckIcon.waitFor({ state: 'visible', timeout: 30000 }); + } // Wait for imported task to be visible await waitForTask(clientA.page, 'E2E Import Test - Active Task With Subtask'); diff --git a/e2e/tests/sync/supersync-provider-switch.spec.ts b/e2e/tests/sync/supersync-provider-switch.spec.ts index 3d62c13f0a..8354b270e0 100644 --- a/e2e/tests/sync/supersync-provider-switch.spec.ts +++ b/e2e/tests/sync/supersync-provider-switch.spec.ts @@ -96,8 +96,12 @@ test.describe('@supersync Provider Switch (WebDAV ↔ SuperSync)', () => { // Create WebDAV sync folder const syncFolderName = generateSyncFolderName(`e2e-provswitch-${testRunId}`); - const request = await (await browser.newContext()).request; - await createSyncFolder(request, syncFolderName); + const webdavRequestContext = await browser.newContext(); + try { + await createSyncFolder(webdavRequestContext.request, syncFolderName); + } finally { + await webdavRequestContext.close(); + } // Disable SuperSync and set up WebDAV const syncPageA = new SyncPage(clientA.page); diff --git a/e2e/utils/waits.ts b/e2e/utils/waits.ts index 4f8cc5035e..134507ddc0 100644 --- a/e2e/utils/waits.ts +++ b/e2e/utils/waits.ts @@ -17,6 +17,24 @@ type WaitForAppReadyOptions = { routeRegex?: RegExp; }; +/** + * Dismiss up to `maxAttempts` blocking confirmation dialogs. + * Some app flows show chained dialogs (e.g., pre-migration + data-repair) + * that must be dismissed before the app shell becomes interactive. + */ +const dismissBlockingDialogs = async (page: Page, maxAttempts = 3): Promise => { + for (let i = 0; i < maxAttempts; i++) { + try { + const dialogConfirmBtn = page.locator('dialog-confirm button[e2e="confirmBtn"]'); + await dialogConfirmBtn.waitFor({ state: 'visible', timeout: 2000 }); + await dialogConfirmBtn.click(); + await page.waitForTimeout(500); + } catch { + break; + } + } +}; + /** * Simplified wait that relies on Playwright's auto-waiting. * Previously used Angular testability API to check Zone.js stability. @@ -54,17 +72,7 @@ export const waitForAppReady = async ( // Handle any blocking dialogs (pre-migration, confirmation, etc.) // These dialogs block app until dismissed - for (let i = 0; i < 3; i++) { - try { - const dialogConfirmBtn = page.locator('dialog-confirm button[e2e="confirmBtn"]'); - await dialogConfirmBtn.waitFor({ state: 'visible', timeout: 2000 }); - await dialogConfirmBtn.click(); - await page.waitForTimeout(500); - } catch { - // No dialog visible, break out - break; - } - } + await dismissBlockingDialogs(page); // Wait for the loading screen to disappear (if visible). // The app shows `.loading-full-page-wrapper` while syncing/importing data. @@ -112,16 +120,7 @@ export const waitForAppReady = async ( ); await page.reload(); await page.waitForLoadState('domcontentloaded'); - for (let i = 0; i < 3; i++) { - try { - const btn = page.locator('dialog-confirm button[e2e="confirmBtn"]'); - await btn.waitFor({ state: 'visible', timeout: 2000 }); - await btn.click(); - await page.waitForTimeout(500); - } catch { - break; - } - } + await dismissBlockingDialogs(page); const rl = page.locator('.loading-full-page-wrapper'); if (await rl.isVisible().catch(() => false)) { await rl.waitFor({ state: 'hidden', timeout: 30000 });