mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 00:46:45 +00:00
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
This commit is contained in:
parent
41987c94ae
commit
93cbcd7e44
7 changed files with 194 additions and 98 deletions
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<string, number>;
|
||||
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<any>((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<VectorClockEntry | undefined>(
|
||||
(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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<void> => {
|
||||
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 });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue