super-productivity/e2e/tests/sync/webdav-single-client-rapid-sync.spec.ts
Johannes Millan d32f7037a3 fix(sync): preserve own vector clock counter across full-state op resets
When a full-state op (SYNC_IMPORT/BACKUP_IMPORT) arrived from another
client, mergeRemoteOpClocks reset the local clock to a "minimal" form
but only preserved the current client's counter from the incoming op's
clock. If the current client had issued ops (e.g. GLOBAL_CONFIG) not
reflected in the incoming full-state op's clock, its counter was dropped,
causing subsequent ops to reuse the same counter value. Downstream clients
then saw these ops as EQUAL (duplicate) and skipped them silently.

Fix: take max(mergedClock[clientId], currentClock[clientId]) when
rebuilding the clock after a full-state op reset.

Also add __SP_E2E_BLOCK_WS_DOWNLOAD flag to WsTriggeredDownloadService
to allow E2E tests to block automatic WS-triggered downloads during
concurrent conflict scenarios.

Fix archive conflict test by blocking WS downloads on Client A during
the concurrent edit phase so it doesn't auto-receive B's rename via
WebSocket before archiving (restoring the intended conflict scenario).

Fix LWW singleton test to assert convergence rather than specific winner.
Fix renameTask helper to avoid Playwright/Angular re-render races.
Fix shepherd.js import paths that broke the Angular dev server build.
2026-04-01 15:41:13 +02:00

311 lines
9.5 KiB
TypeScript

import { test, expect } from '../../fixtures/webdav.fixture';
import { SyncPage } from '../../pages/sync.page';
import { WorkViewPage } from '../../pages/work-view.page';
import {
WEBDAV_CONFIG_TEMPLATE,
setupSyncClient,
createSyncFolder,
waitForSyncComplete,
generateSyncFolderName,
closeContextsSafely,
} from '../../utils/sync-helpers';
/**
* WebDAV Single Client Rapid Sync E2E Tests
*
* These tests verify that rapid successive syncs from a single client
* complete without errors. Conflict detection uses content hashing (MD5)
* to compare the remote file before uploading, so timing precision
* is not a concern.
*
* Prerequisites:
* - WebDAV server running at http://127.0.0.1:2345/
* - Frontend running on localhost:4242
*
* Run with: npm run e2e:file e2e/tests/sync/webdav-single-client-rapid-sync.spec.ts
*/
test.describe('@webdav Rapid Sync (Single Client)', () => {
// Run sync tests serially to avoid WebDAV server contention
test.describe.configure({ mode: 'serial' });
// Use a unique folder for each test run
const SYNC_FOLDER_NAME = generateSyncFolderName('e2e-rapid');
const WEBDAV_CONFIG = {
...WEBDAV_CONFIG_TEMPLATE,
syncFolderPath: `/${SYNC_FOLDER_NAME}`,
};
/**
* Scenario: Single client rapid syncs complete without errors
*
* Setup:
* - Client A with WebDAV sync configured
*
* Actions:
* 1. Create task, sync
* 2. Immediately create another task, sync
* 3. Repeat 5 times in rapid succession
*
* Verify:
* - All 5 syncs complete successfully
* - All 5 tasks are present
*/
test('Single client rapid syncs do not cause 412 errors', async ({
browser,
baseURL,
request,
webdavServerUp,
}) => {
test.slow(); // Sync tests take longer
// Create the sync folder on WebDAV server
await createSyncFolder(request, SYNC_FOLDER_NAME);
const { context, page } = await setupSyncClient(browser, baseURL);
const syncPage = new SyncPage(page);
const workViewPage = new WorkViewPage(page);
// Track any sync errors — use specific patterns to avoid false positives
// from unrelated console output that happens to contain "412" (e.g. body sizes)
const syncErrors: string[] = [];
page.on('console', (msg) => {
const text = msg.text();
if (/HTTP 412|status[:\s]+412|Precondition Failed/i.test(text)) {
syncErrors.push(text);
}
});
try {
await workViewPage.waitForTaskList();
// Configure WebDAV sync
await syncPage.setupWebdavSync(WEBDAV_CONFIG);
await expect(syncPage.syncBtn).toBeVisible();
console.log('[RapidSync] WebDAV sync configured');
// Initial sync to establish baseline
await syncPage.triggerSync();
await waitForSyncComplete(page, syncPage);
console.log('[RapidSync] Initial sync complete');
// Perform 5 rapid create-then-sync cycles
const taskNames: string[] = [];
for (let i = 1; i <= 5; i++) {
const taskName = `RapidTask${i}-${Date.now()}`;
taskNames.push(taskName);
// Create task
await workViewPage.addTask(taskName);
await expect(page.locator(`task:has-text("${taskName}")`)).toBeVisible();
// Immediately sync (within the same second if possible)
await syncPage.triggerSync();
const result = await waitForSyncComplete(page, syncPage);
if (result === 'conflict') {
throw new Error(`Unexpected conflict on task ${i}`);
}
console.log(`[RapidSync] Cycle ${i}/5 complete: ${taskName}`);
}
// Verify all tasks are present (allow extra time for Angular to settle after rapid syncing)
for (const taskName of taskNames) {
await expect(page.locator(`task:has-text("${taskName}")`)).toBeVisible({
timeout: 10000,
});
}
await expect(page.locator('task')).toHaveCount(5, { timeout: 10000 });
// Verify no 412 errors occurred
expect(syncErrors.length).toBe(0);
console.log('[RapidSync] ✓ All 5 rapid syncs successful');
console.log('[RapidSync] ✓ No 412 errors');
} finally {
await closeContextsSafely(context);
}
});
/**
* Scenario: Rapid task modifications sync without errors
*
* Tests that rapidly modifying tasks (mark done, rename) and syncing
* doesn't cause 412 errors.
*
* Setup:
* - Client with existing synced task
*
* Actions:
* 1. Sync
* 2. Mark task done, immediately sync
* 3. Add new task, immediately sync
* 4. Rename task, immediately sync
*
* Verify:
* - All syncs succeed without 412 errors
*/
test('Rapid task modifications sync without errors', async ({
browser,
baseURL,
request,
webdavServerUp,
}) => {
test.slow();
const folderName = generateSyncFolderName('e2e-modify');
await createSyncFolder(request, folderName);
const config = {
...WEBDAV_CONFIG_TEMPLATE,
syncFolderPath: `/${folderName}`,
};
const { context, page } = await setupSyncClient(browser, baseURL);
const syncPage = new SyncPage(page);
const workViewPage = new WorkViewPage(page);
const syncErrors: string[] = [];
page.on('console', (msg) => {
const text = msg.text();
if (/HTTP 412|status[:\s]+412|Precondition Failed/i.test(text)) {
syncErrors.push(text);
}
});
try {
await workViewPage.waitForTaskList();
await syncPage.setupWebdavSync(config);
console.log('[ModifySync] WebDAV sync configured');
// Create initial task
const taskName = `ModifyTask-${Date.now()}`;
await workViewPage.addTask(taskName);
await expect(page.locator(`task:has-text("${taskName}")`)).toBeVisible();
// Sync initial task
await syncPage.triggerSync();
await waitForSyncComplete(page, syncPage);
console.log('[ModifySync] Initial task synced');
// Step 1: Mark task done, immediately sync
const task = page.locator(`task:has-text("${taskName}")`).first();
await task.hover();
await task.locator('done-toggle').click();
await expect(task).toHaveClass(/isDone/);
await syncPage.triggerSync();
await waitForSyncComplete(page, syncPage);
console.log('[ModifySync] Done state synced');
// Step 2: Add another task, immediately sync
const task2Name = `ModifyTask2-${Date.now()}`;
await workViewPage.addTask(task2Name);
await expect(page.locator(`task:has-text("${task2Name}")`)).toBeVisible();
await syncPage.triggerSync();
await waitForSyncComplete(page, syncPage);
console.log('[ModifySync] Second task synced');
// Step 3: Rename second task, immediately sync
const task2 = page.locator(`task:has-text("${task2Name}")`).first();
await task2.click();
const titleElement = task2.locator('.task-title');
await titleElement.click();
const input = task2.locator('input, textarea');
await input.fill(`${task2Name}-Renamed`);
await page.keyboard.press('Tab');
await syncPage.triggerSync();
await waitForSyncComplete(page, syncPage);
console.log('[ModifySync] Rename synced');
// Verify no 412 errors
expect(syncErrors.length).toBe(0);
console.log('[ModifySync] ✓ All modifications synced without 412 errors');
} finally {
await closeContextsSafely(context);
}
});
/**
* Scenario: Multiple syncs within same second succeed
*
* Stress test that tries to trigger the timestamp precision issue
* by syncing as fast as possible.
*
* Setup:
* - Client with WebDAV configured
*
* Actions:
* 1. Sync 10 times as fast as possible
*
* Verify:
* - All syncs complete
* - No 412 errors
*/
test('Multiple syncs within same second succeed', async ({
browser,
baseURL,
request,
webdavServerUp,
}) => {
test.slow();
const folderName = generateSyncFolderName('e2e-burst');
await createSyncFolder(request, folderName);
const config = {
...WEBDAV_CONFIG_TEMPLATE,
syncFolderPath: `/${folderName}`,
};
const { context, page } = await setupSyncClient(browser, baseURL);
const syncPage = new SyncPage(page);
const workViewPage = new WorkViewPage(page);
const syncErrors: string[] = [];
page.on('console', (msg) => {
const text = msg.text();
if (/HTTP 412|status[:\s]+412|Precondition Failed/i.test(text)) {
syncErrors.push(text);
}
});
try {
await workViewPage.waitForTaskList();
await syncPage.setupWebdavSync(config);
// Create a task so there's data to sync
await workViewPage.addTask(`BurstTask-${Date.now()}`);
console.log('[BurstSync] Task created');
// Sync rapidly 10 times
let successCount = 0;
for (let i = 1; i <= 10; i++) {
try {
await syncPage.triggerSync();
// Brief wait to let the new sync cycle start before checking completion
await page.waitForTimeout(500);
await waitForSyncComplete(page, syncPage, 15000);
successCount++;
} catch (e) {
// If sync times out, that's still better than 412
console.log(`[BurstSync] Sync ${i} timed out (not 412)`);
}
}
// At least some syncs should succeed (some might overlap/skip/dedup)
expect(successCount).toBeGreaterThanOrEqual(3);
expect(syncErrors.length).toBe(0);
console.log(`[BurstSync] ✓ ${successCount}/10 syncs completed`);
console.log('[BurstSync] ✓ No 412 errors in burst sync');
} finally {
await closeContextsSafely(context);
}
});
});