import { test as base, expect } from '@playwright/test'; import { createTestUser, getSuperSyncConfig, createSimulatedClient, closeClient, waitForTask, isServerHealthy, type SimulatedE2EClient, } from '../../utils/supersync-helpers'; import { ImportPage } from '../../pages/import.page'; import { waitForAppReady } from '../../utils/waits'; /** * Import + Sync E2E Tests * * These tests verify that imported backup data syncs correctly between clients. * This includes active tasks, archived tasks (worklog), and projects. * * Prerequisites: * - super-sync-server running on localhost:1901 with TEST_MODE=true * - Frontend running on localhost:4242 * * Run with: npm run e2e:playwright -- --grep @importsync */ const generateTestRunId = (workerIndex: number): string => { return `${Date.now()}-${workerIndex}`; }; /** Default encryption password used by setupSuperSync's mandatory encryption dialog */ const ENCRYPTION_PASSWORD = 'e2e-default-encryption-pw'; base.describe('@importsync @supersync Import + Sync E2E', () => { let serverHealthy: boolean | null = null; base.beforeEach(async ({}, testInfo) => { if (serverHealthy === null) { serverHealthy = await isServerHealthy(); if (!serverHealthy) { console.warn( 'SuperSync server not healthy at http://localhost:1901 - skipping tests', ); } } testInfo.skip(!serverHealthy, 'SuperSync server not running'); }); /** * Scenario: Import backup file and sync to second client * * This test verifies that importing a backup file creates sync operations * that propagate all data (including archives) to other clients. * * Setup: Client A and B with shared SuperSync account, empty server * * Actions: * 1. Client A imports JSON backup file containing: * - Active tasks (with subtasks) * - Archived tasks in archiveYoung/archiveOld * - Projects and tags * 2. Client A syncs * 3. Client B syncs * * Verify: * - Client B has all active tasks * - Client B has archived tasks in worklog view * - Projects and tags match */ // SKIPPED: Pre-existing issue. After configuring sync and then importing a backup, // the imported state doesn't persist - likely overwritten by sync activity. // The import+sync interaction needs investigation in the app code itself. base.skip( 'Import backup file on Client A, sync to Client B', async ({ browser, baseURL }, testInfo) => { const testRunId = generateTestRunId(testInfo.workerIndex); let clientA: SimulatedE2EClient | null = null; let clientB: SimulatedE2EClient | null = null; try { // Create shared test user const user = await createTestUser(testRunId); const syncConfig = getSuperSyncConfig(user); // ============ PHASE 1: Client A Sets Up Sync and Imports Backup ============ console.log('[Import Test] Phase 1: Client A importing backup'); clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId); await clientA.sync.setupSuperSync(syncConfig); // Navigate to import page const importPage = new ImportPage(clientA.page); await importPage.navigateToImportPage(); // Import the test backup file const backupPath = ImportPage.getFixturePath('test-backup.json'); await importPage.importBackupFile(backupPath); console.log('[Import Test] Client A imported backup successfully'); // Navigate to INBOX project view after import to verify tasks are visible. // The backup tasks have projectId: INBOX_PROJECT and dueDay in the past, // which may not appear in the TODAY virtual tag (depends on date filtering). // Using the project view ensures we see all tasks regardless of dueDay. await clientA.page.goto('/#/project/INBOX_PROJECT/tasks', { waitUntil: 'domcontentloaded', timeout: 30000, }); await waitForAppReady(clientA.page); // Wait for import to complete and verify tasks are visible // The backup contains tasks with "E2E Import Test" in their titles await waitForTask(clientA.page, 'E2E Import Test - Active Task With Subtask'); await waitForTask(clientA.page, 'E2E Import Test - Simple Active Task'); console.log('[Import Test] Client A has imported tasks visible'); // Re-enable sync after import (import overwrites globalConfig including sync settings) console.log('[Import Test] Re-enabling sync after import'); await clientA.sync.setupSuperSync(syncConfig); // ============ PHASE 2: Client A Syncs to Server ============ console.log('[Import Test] Phase 2: Client A syncing to server'); await clientA.sync.syncAndWait(); console.log('[Import Test] Client A sync complete'); // ============ PHASE 3: Client B Downloads via Sync ============ console.log('[Import Test] Phase 3: Client B syncing from server'); clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId); // Client B must use explicit encryption config to enter the password dialog // instead of the enable-encryption dialog (which would wipe server data). await clientB.sync.setupSuperSync({ ...syncConfig, isEncryptionEnabled: true, password: ENCRYPTION_PASSWORD, }); await clientB.sync.syncAndWait(); console.log('[Import Test] Client B sync complete'); // Navigate Client B to INBOX project view to see imported tasks await clientB.page.goto('/#/project/INBOX_PROJECT/tasks', { waitUntil: 'domcontentloaded', timeout: 30000, }); await waitForAppReady(clientB.page); // ============ PHASE 4: Verify Active Tasks on Client B ============ console.log('[Import Test] Phase 4: Verifying active tasks on Client B'); // Check for active tasks await waitForTask(clientB.page, 'E2E Import Test - Active Task With Subtask'); await waitForTask(clientB.page, 'E2E Import Test - Simple Active Task'); // Verify both active tasks are visible const activeTask1 = clientB.page.locator( 'task:has-text("E2E Import Test - Active Task With Subtask")', ); const activeTask2 = clientB.page.locator( 'task:has-text("E2E Import Test - Simple Active Task")', ); await expect(activeTask1).toBeVisible({ timeout: 10000 }); await expect(activeTask2).toBeVisible({ timeout: 10000 }); console.log('[Import Test] Client B has both active tasks'); // Expand parent task to see subtask const expandBtn = activeTask1.locator('.expand-btn'); if (await expandBtn.isVisible()) { await expandBtn.click(); await waitForTask(clientB.page, 'E2E Import Test - Subtask of Active Task'); console.log('[Import Test] Client B has subtask visible'); } // ============ PHASE 5: Verify Archived Tasks in Worklog ============ console.log('[Import Test] Phase 5: Verifying archived tasks in worklog'); // Navigate to worklog on Client B await clientB.page.goto('/#/tag/TODAY/history'); await clientB.page.waitForLoadState('networkidle'); await clientB.page.waitForSelector('history', { timeout: 10000 }); // Expand worklog to see archived tasks const weekRow = clientB.page.locator('.week-row').first(); if (await weekRow.isVisible()) { await weekRow.click(); await clientB.page.waitForTimeout(500); } // Archived tasks should appear in the worklog // The test backup has archived tasks with titles containing "E2E Import Test - Archived" // Note: Archived tasks appear in worklog if they have timeSpentOnDay entries // Our test backup has archived tasks with time entries console.log('[Import Test] Checking for archived tasks in worklog...'); // Try to find archived task entries - they appear under the dates they were worked on const hasArchivedTasks = (await clientB.page .locator('.task-summary-table .task-title') .count() .catch(() => 0)) > 0; if (hasArchivedTasks) { console.log('[Import Test] Client B worklog has archived task entries'); } else { // Archive data may not show in TODAY tag worklog - navigate to project worklog await clientB.page.goto('/#/project/test-project-1/history'); await clientB.page.waitForLoadState('networkidle'); console.log('[Import Test] Checking project worklog for archived tasks'); } // ============ PHASE 6: Final Verification ============ console.log('[Import Test] Phase 6: Final state verification'); // Go back to project view on both clients and wait for app to be ready await clientA.page.goto('/#/project/INBOX_PROJECT/tasks'); await waitForAppReady(clientA.page); await clientB.page.goto('/#/project/INBOX_PROJECT/tasks'); await waitForAppReady(clientB.page); // Wait for tasks to render before counting await waitForTask(clientA.page, 'E2E Import Test - Active Task With Subtask'); await waitForTask(clientB.page, 'E2E Import Test - Active Task With Subtask'); // Count tasks on both clients - should match const taskCountA = await clientA.page.locator('task').count(); const taskCountB = await clientB.page.locator('task').count(); console.log(`[Import Test] Client A task count: ${taskCountA}`); console.log(`[Import Test] Client B task count: ${taskCountB}`); // Both clients should have same number of tasks expect(taskCountA).toBe(taskCountB); expect(taskCountA).toBeGreaterThanOrEqual(2); // At least the 2 active tasks console.log('[Import Test] ✓ Import + Sync test passed!'); } finally { if (clientA) await closeClient(clientA); if (clientB) await closeClient(clientB); } }, ); /** * Scenario: Import with existing data (merge scenario) * * Tests that importing a backup on one client merges with * existing data on another client after sync. * * Actions: * 1. Client A creates a task "Existing Task A" * 2. Client A syncs * 3. Client B syncs (gets Existing Task A) * 4. Client B creates a task "Existing Task B" * 5. Client A imports backup (which adds new tasks) * 6. Client A syncs * 7. Client B syncs * * Verify: * - Client A has: imported tasks + Existing Task A + Existing Task B * - Client B has: imported tasks + Existing Task A + Existing Task B */ // SKIPPED: Pre-existing issue. After backup import on Client A with existing synced data, // the imported tasks don't appear in the TODAY view. The BACKUP_IMPORT operation's // interaction with pre-existing server operations needs investigation. // The simpler "Import backup file" test (above) verifies import+sync on a clean server. base.skip( 'Import merges with existing synced data', async ({ browser, baseURL }, testInfo) => { const testRunId = generateTestRunId(testInfo.workerIndex); const uniqueId = Date.now(); let clientA: SimulatedE2EClient | null = null; let clientB: SimulatedE2EClient | null = null; try { const user = await createTestUser(testRunId); const syncConfig = getSuperSyncConfig(user); // ============ PHASE 1: Setup and Create Initial Tasks ============ console.log('[Merge Test] Phase 1: Creating initial tasks'); clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId); await clientA.sync.setupSuperSync(syncConfig); // Client A creates a task const existingTaskA = `ExistingA-${uniqueId}`; await clientA.workView.addTask(existingTaskA); await clientA.sync.syncAndWait(); console.log(`[Merge Test] Client A created and synced: ${existingTaskA}`); // Client B gets the task. // Must use explicit encryption config so Client B enters the password dialog // instead of the enable-encryption dialog (which would wipe Client A's data). clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId); await clientB.sync.setupSuperSync({ ...syncConfig, isEncryptionEnabled: true, password: ENCRYPTION_PASSWORD, }); await clientB.sync.syncAndWait(); await waitForTask(clientB.page, existingTaskA); // Client B creates another task const existingTaskB = `ExistingB-${uniqueId}`; await clientB.workView.addTask(existingTaskB); await clientB.sync.syncAndWait(); console.log(`[Merge Test] Client B created and synced: ${existingTaskB}`); // ============ PHASE 2: Client A Imports Backup ============ console.log('[Merge Test] Phase 2: Client A importing backup'); // Client A syncs to get Client B's task first await clientA.sync.syncAndWait(); await waitForTask(clientA.page, existingTaskB); // Navigate to import page const importPage = new ImportPage(clientA.page); await importPage.navigateToImportPage(); // Import the backup const backupPath = ImportPage.getFixturePath('test-backup.json'); await importPage.importBackupFile(backupPath); console.log('[Merge Test] Client A imported backup'); // Navigate to tasks view after import. // Use explicit URL instead of page.url() because after import the page may be on // the settings/import route, and the imported state may redirect to /#/config. await clientA.page.goto('/#/tag/TODAY/tasks', { waitUntil: 'domcontentloaded', timeout: 30000, }); await waitForAppReady(clientA.page); // Re-enable sync after import (import overwrites globalConfig including sync settings) console.log('[Merge Test] Re-enabling sync after import'); await clientA.sync.setupSuperSync(syncConfig); // ============ PHASE 3: Sync After Import ============ console.log('[Merge Test] Phase 3: Syncing after import'); await clientA.sync.syncAndWait(); await clientB.sync.syncAndWait(); // Reload Client B after sync to ensure UI reflects synced state // Use goto (current URL) instead of reload - more reliable with service workers await clientB.page.goto(clientB.page.url(), { waitUntil: 'domcontentloaded', timeout: 30000, }); await waitForAppReady(clientB.page); // ============ PHASE 4: Verify Merged Data ============ console.log('[Merge Test] Phase 4: Verifying merged data'); // Client A should have imported tasks await waitForTask(clientA.page, 'E2E Import Test - Active Task With Subtask'); console.log('[Merge Test] Client A has imported tasks'); // Client B should also have imported tasks after sync await waitForTask(clientB.page, 'E2E Import Test - Active Task With Subtask'); console.log('[Merge Test] Client B received imported tasks via sync'); // Note: The import replaces state rather than merging, so existing tasks // may be gone after import. This test documents the actual behavior. // If merge behavior is desired, the app would need to implement it differently. console.log('[Merge Test] ✓ Import merge test passed!'); } finally { if (clientA) await closeClient(clientA); if (clientB) await closeClient(clientB); } }, ); });