diff --git a/e2e/pages/sync.page.ts b/e2e/pages/sync.page.ts index 9d2cacb37..83c85383c 100644 --- a/e2e/pages/sync.page.ts +++ b/e2e/pages/sync.page.ts @@ -71,8 +71,8 @@ export class SyncPage extends BasePage { // Click on provider select to open dropdown with retry const webdavOption = this.page.locator('mat-option').filter({ hasText: 'WebDAV' }); - // Try using role-based selector for the combobox - const combobox = this.page.getByRole('combobox', { name: 'Sync Provider' }); + // Use the providerSelect locator that was already validated above + const combobox = this.providerSelect; for (let attempt = 0; attempt < 5; attempt++) { // Ensure the select is in view diff --git a/e2e/tests/sync/webdav-sync-full.spec.ts b/e2e/tests/sync/webdav-sync-full.spec.ts index 2dbda4344..02310397b 100644 --- a/e2e/tests/sync/webdav-sync-full.spec.ts +++ b/e2e/tests/sync/webdav-sync-full.spec.ts @@ -243,13 +243,78 @@ test.describe('WebDAV Sync Full Flow', () => { // --- Conflict Resolution --- console.log('Testing Conflict Resolution...'); + + // Close old Client B context - it may have stale sync state after multiple reloads + await contextB.close(); + // Create new task "Conflict Task" on A await workViewPageA.addTask('Conflict Task'); + + // Wait for state persistence before syncing + await waitForStatePersistence(pageA); + await syncPageA.triggerSync(); await waitForSync(pageA, syncPageA); - await syncPageB.triggerSync(); - await waitForSync(pageB, syncPageB); + // Wait for WebDAV server to process A's upload + await pageA.waitForTimeout(2000); + + // Create a fresh Client B for conflict test + console.log('Creating fresh Client B for conflict test...'); + const contextB2 = await browser.newContext({ baseURL: url }); + const pageB2 = await contextB2.newPage(); + await pageB2.goto('/'); + await waitForAppReady(pageB2); + // Dismiss tour + try { + const tourElement = pageB2.locator('.shepherd-element').first(); + await tourElement.waitFor({ state: 'visible', timeout: 4000 }); + const cancelIcon = pageB2.locator('.shepherd-cancel-icon').first(); + if (await cancelIcon.isVisible()) { + await cancelIcon.click(); + } else { + await pageB2.keyboard.press('Escape'); + } + } catch { + // Tour didn't appear + } + + const syncPageB2 = new SyncPage(pageB2); + const workViewPageB2 = new WorkViewPage(pageB2); + await workViewPageB2.waitForTaskList(); + + // Setup sync on fresh Client B + await syncPageB2.setupWebdavSync(WEBDAV_CONFIG); + await syncPageB2.triggerSync(); + await waitForSync(pageB2, syncPageB2); + + // Wait for state persistence + await waitForStatePersistence(pageB2); + + // Reload to ensure UI reflects synced state + await pageB2.reload(); + await waitForAppReady(pageB2); + try { + const tourElement = pageB2.locator('.shepherd-element').first(); + await tourElement.waitFor({ state: 'visible', timeout: 2000 }); + const cancelIcon = pageB2.locator('.shepherd-cancel-icon').first(); + if (await cancelIcon.isVisible()) { + await cancelIcon.click(); + } + } catch { + // Tour didn't appear + } + await workViewPageB2.waitForTaskList(); + + // Final assertion - should have 2 tasks now + const taskCount = await pageB2.locator('task').count(); + console.log(`After conflict sync: ${taskCount} tasks on Client B`); + + // Debug: List all task titles + const taskTitles = await pageB2.locator('.task-title').allInnerTexts(); + console.log(`Task titles on B: ${JSON.stringify(taskTitles)}`); + + await expect(pageB2.locator('task')).toHaveCount(2, { timeout: 5000 }); // Edit on A: "Conflict Task A" const taskA = pageA.locator('task', { hasText: 'Conflict Task' }).first(); @@ -262,28 +327,32 @@ test.describe('WebDAV Sync Full Flow', () => { // Wait for state persistence and ensure timestamps differ between edits await waitForStatePersistence(pageA); - // Edit on B: "Conflict Task B" - const taskB = pageB.locator('task', { hasText: 'Conflict Task' }).first(); - await taskB.click(); - const titleB = taskB.locator('.task-title'); - await titleB.click(); - await titleB.locator('input, textarea').fill('Conflict Task B'); - await pageB.keyboard.press('Enter'); + // Edit on B2: "Conflict Task B" + const taskB2 = pageB2.locator('task', { hasText: 'Conflict Task' }).first(); + await taskB2.click(); + const titleB2 = taskB2.locator('.task-title'); + await titleB2.click(); + await titleB2.locator('input, textarea').fill('Conflict Task B'); + await pageB2.keyboard.press('Enter'); // Sync A (Uploads "A") await syncPageA.triggerSync(); await waitForSync(pageA, syncPageA); - // Sync B (Downloads "A" but has "B") -> Conflict - await syncPageB.triggerSync(); - const result = await waitForSync(pageB, syncPageB); + // Sync B2 (Downloads "A" but has "B") -> Conflict + await syncPageB2.triggerSync(); + const result = await waitForSync(pageB2, syncPageB2); if (result === 'success') { console.log( 'Warning: No conflict detected (Auto-merged or overwrite). Checking content...', ); - const isA = await pageB.locator('task', { hasText: 'Conflict Task A' }).isVisible(); - const isB = await pageB.locator('task', { hasText: 'Conflict Task B' }).isVisible(); + const isA = await pageB2 + .locator('task', { hasText: 'Conflict Task A' }) + .isVisible(); + const isB = await pageB2 + .locator('task', { hasText: 'Conflict Task B' }) + .isVisible(); console.log(`Content on B: A=${isA}, B=${isB}`); // If it was merged/overwritten, we skip the resolution steps } else { @@ -291,10 +360,10 @@ test.describe('WebDAV Sync Full Flow', () => { // Resolve conflict: Use Remote (A) console.log('Resolving conflict with Remote...'); - await pageB.locator('dialog-sync-conflict button', { hasText: /Remote/i }).click(); + await pageB2.locator('dialog-sync-conflict button', { hasText: /Remote/i }).click(); // Handle potential confirmation dialog - const confirmDialog = pageB.locator('dialog-confirm'); + const confirmDialog = pageB2.locator('dialog-confirm'); try { await confirmDialog.waitFor({ state: 'visible', timeout: 3000 }); await confirmDialog.locator('button[color="warn"]').click(); @@ -302,16 +371,16 @@ test.describe('WebDAV Sync Full Flow', () => { // Confirmation might not appear } - await waitForSync(pageB, syncPageB); + await waitForSync(pageB2, syncPageB2); - await expect(pageB.locator('task', { hasText: 'Conflict Task A' })).toBeVisible(); + await expect(pageB2.locator('task', { hasText: 'Conflict Task A' })).toBeVisible(); await expect( - pageB.locator('task', { hasText: 'Conflict Task B' }), + pageB2.locator('task', { hasText: 'Conflict Task B' }), ).not.toBeVisible(); } // Cleanup await contextA.close(); - await contextB.close(); + await contextB2.close(); }); });