super-productivity/e2e/tests/sync/import-sync.spec.ts
Johannes Millan 755cd705f5
Fix/run 22683814946 (#6740)
test(e2e): fix e2e tests
2026-03-05 19:37:48 +01:00

369 lines
16 KiB
TypeScript

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/worklog');
await clientB.page.waitForLoadState('networkidle');
await clientB.page.waitForSelector('worklog', { 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/worklog');
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);
}
},
);
});