mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-20 18:08:55 +00:00
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.
311 lines
9.5 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
});
|