mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-21 02:20:12 +00:00
- Replace page.reload() with close-and-reopen pattern after backup imports to prevent hangs with active sync connections - Add proper sync-import-conflict dialog handling via Promise.race - Fix browser context leak in provider-switch test - Replace baseURL! non-null assertions with fallback pattern - Replace fixed timeouts with element-based waits in cascade-delete - Extract duplicate dialog-dismissal loop into dismissBlockingDialogs helper in waits.ts - Add missing WorkViewPage/SuperSyncPage imports in same-client test - Replace any type with proper VectorClockEntry interface
191 lines
7.4 KiB
TypeScript
191 lines
7.4 KiB
TypeScript
import { test, expect } from '../../fixtures/supersync.fixture';
|
|
import {
|
|
createTestUser,
|
|
getSuperSyncConfig,
|
|
createSimulatedClient,
|
|
closeClient,
|
|
waitForTask,
|
|
createProjectReliably,
|
|
type SimulatedE2EClient,
|
|
} from '../../utils/supersync-helpers';
|
|
|
|
/**
|
|
* SuperSync Cross-Entity Cascade Delete E2E Tests
|
|
*
|
|
* Tests that project deletion propagates correctly across clients.
|
|
* When Client A deletes a project (with tasks), the deletion should
|
|
* propagate to Client B so the project and its tasks disappear.
|
|
*
|
|
* Prerequisites:
|
|
* - super-sync-server running on localhost:1901 with TEST_MODE=true
|
|
* - Frontend running on localhost:4242
|
|
*
|
|
* Run with: npm run e2e:supersync:file e2e/tests/sync/supersync-cascade-delete.spec.ts
|
|
*/
|
|
|
|
/**
|
|
* Navigate to project work view by clicking the project in the sidebar.
|
|
*/
|
|
const navigateToProject = async (
|
|
page: import('@playwright/test').Page,
|
|
projectName: string,
|
|
): Promise<void> => {
|
|
const projectNavItem = page.locator(`nav-item:has-text("${projectName}")`).first();
|
|
await projectNavItem.waitFor({ state: 'visible', timeout: 10000 });
|
|
await projectNavItem.click();
|
|
await page
|
|
.locator('.route-wrapper')
|
|
.first()
|
|
.waitFor({ state: 'visible', timeout: 10000 });
|
|
};
|
|
|
|
/**
|
|
* Delete a project via the context menu on the sidebar nav-item.
|
|
*/
|
|
const deleteProjectViaContextMenu = async (
|
|
page: import('@playwright/test').Page,
|
|
projectName: string,
|
|
): Promise<void> => {
|
|
const projectNavItem = page.locator(`nav-item:has-text("${projectName}")`).first();
|
|
await projectNavItem.waitFor({ state: 'visible', timeout: 10000 });
|
|
|
|
await projectNavItem.hover();
|
|
const moreBtn = projectNavItem.locator('.additional-btn');
|
|
await moreBtn.waitFor({ state: 'visible', timeout: 5000 });
|
|
await moreBtn.click();
|
|
|
|
const deleteMenuItem = page.locator('button[mat-menu-item]:has-text("Delete project")');
|
|
await deleteMenuItem.waitFor({ state: 'visible', timeout: 5000 });
|
|
await deleteMenuItem.click();
|
|
|
|
const confirmBtn = page.locator('dialog-confirm button[e2e="confirmBtn"]');
|
|
await confirmBtn.waitFor({ state: 'visible', timeout: 5000 });
|
|
await confirmBtn.click();
|
|
|
|
// Wait for confirmation dialog to close
|
|
await page.locator('dialog-confirm').waitFor({ state: 'hidden', timeout: 5000 });
|
|
};
|
|
|
|
test.describe('@supersync Cross-Entity Cascade Delete', () => {
|
|
/**
|
|
* Scenario: Project deletion propagates to another synced client
|
|
*
|
|
* Flow:
|
|
* 1. Client A creates a project with tasks
|
|
* 2. Client A syncs
|
|
* 3. Client B syncs (gets the project with tasks)
|
|
* 4. Verify Client B can see the project and tasks
|
|
* 5. Client A deletes the project (cascade deletes tasks locally)
|
|
* 6. Client A syncs (uploads deletion)
|
|
* 7. Client B syncs (receives deletion)
|
|
* 8. Verify: Both clients have no project and no tasks
|
|
*/
|
|
test('Project deletion cascades correctly across clients', async ({
|
|
browser,
|
|
baseURL,
|
|
testRunId,
|
|
}) => {
|
|
test.setTimeout(180000);
|
|
const appUrl = baseURL || 'http://localhost:4242';
|
|
let clientA: SimulatedE2EClient | null = null;
|
|
let clientB: SimulatedE2EClient | null = null;
|
|
|
|
try {
|
|
const user = await createTestUser(testRunId);
|
|
const syncConfig = getSuperSyncConfig(user);
|
|
|
|
// ============ PHASE 1: Client A creates project with tasks ============
|
|
console.log('[CascadeDelete] Phase 1: Client A creates project with tasks');
|
|
|
|
clientA = await createSimulatedClient(browser, appUrl, 'A', testRunId);
|
|
await clientA.sync.setupSuperSync(syncConfig);
|
|
|
|
const projectName = `CascadeProject-${testRunId}`;
|
|
await createProjectReliably(clientA.page, projectName);
|
|
|
|
const taskA1 = `TaskA1-${testRunId}`;
|
|
const taskA2 = `TaskA2-${testRunId}`;
|
|
await clientA.workView.addTask(taskA1);
|
|
await clientA.workView.addTask(taskA2);
|
|
await waitForTask(clientA.page, taskA1);
|
|
await waitForTask(clientA.page, taskA2);
|
|
console.log(
|
|
`[CascadeDelete] Client A created project "${projectName}" with 2 tasks`,
|
|
);
|
|
|
|
// Sync to upload project and tasks
|
|
await clientA.sync.syncAndWait();
|
|
console.log('[CascadeDelete] Client A synced');
|
|
|
|
// ============ PHASE 2: Client B syncs and verifies project ============
|
|
console.log('[CascadeDelete] Phase 2: Client B syncs and verifies project');
|
|
|
|
clientB = await createSimulatedClient(browser, appUrl, 'B', testRunId);
|
|
await clientB.sync.setupSuperSync(syncConfig);
|
|
|
|
await clientB.sync.syncAndWait();
|
|
|
|
// Verify Client B has the project in sidebar
|
|
const projectOnB = clientB.page.locator(`nav-item:has-text("${projectName}")`);
|
|
await expect(projectOnB).toBeVisible({ timeout: 10000 });
|
|
|
|
// Navigate to the project and verify tasks
|
|
await navigateToProject(clientB.page, projectName);
|
|
await waitForTask(clientB.page, taskA1);
|
|
await waitForTask(clientB.page, taskA2);
|
|
console.log('[CascadeDelete] Client B has project and tasks');
|
|
|
|
// Navigate Client B to Today before deletion sync to avoid stale view
|
|
await clientB.page.evaluate(() => {
|
|
window.location.hash = '#/tag/TODAY/tasks';
|
|
});
|
|
await clientB.page.waitForURL(/tag\/TODAY/, { timeout: 15000 });
|
|
|
|
// ============ PHASE 3: Client A deletes the project ============
|
|
console.log('[CascadeDelete] Phase 3: Client A deletes the project');
|
|
|
|
await deleteProjectViaContextMenu(clientA.page, projectName);
|
|
console.log('[CascadeDelete] Client A deleted project');
|
|
|
|
// Sync the deletion
|
|
await clientA.sync.syncAndWait();
|
|
console.log('[CascadeDelete] Client A synced deletion');
|
|
|
|
// ============ PHASE 4: Client B syncs to receive deletion ============
|
|
console.log('[CascadeDelete] Phase 4: Client B syncs to receive deletion');
|
|
|
|
await clientB.sync.syncAndWait();
|
|
// Extra convergence round
|
|
await clientB.sync.syncAndWait();
|
|
console.log('[CascadeDelete] Client B synced');
|
|
|
|
// ============ PHASE 5: Verify deletion propagated ============
|
|
console.log('[CascadeDelete] Phase 5: Verifying deletion propagated');
|
|
|
|
// Verify project is gone from sidebar on both clients
|
|
const projectOnA = clientA.page.locator(`nav-item:has-text("${projectName}")`);
|
|
await expect(projectOnA).not.toBeVisible({ timeout: 10000 });
|
|
await expect(projectOnB).not.toBeVisible({ timeout: 10000 });
|
|
console.log('[CascadeDelete] Project gone from both sidebars');
|
|
|
|
// Verify tasks are not visible on Client A (which is already on Today after deletion)
|
|
const taskA1OnA = clientA.page.locator(`task:has-text("${taskA1}")`);
|
|
const taskA2OnA = clientA.page.locator(`task:has-text("${taskA2}")`);
|
|
await expect(taskA1OnA).not.toBeVisible({ timeout: 10000 });
|
|
await expect(taskA2OnA).not.toBeVisible({ timeout: 10000 });
|
|
console.log('[CascadeDelete] Tasks gone from Client A');
|
|
|
|
// Verify tasks are not visible on Client B (which is on Today)
|
|
const taskA1OnB = clientB.page.locator(`task:has-text("${taskA1}")`);
|
|
const taskA2OnB = clientB.page.locator(`task:has-text("${taskA2}")`);
|
|
await expect(taskA1OnB).not.toBeVisible({ timeout: 10000 });
|
|
await expect(taskA2OnB).not.toBeVisible({ timeout: 10000 });
|
|
console.log('[CascadeDelete] Tasks gone from Client B');
|
|
|
|
console.log('[CascadeDelete] Cascade delete test PASSED!');
|
|
} finally {
|
|
if (clientA) await closeClient(clientA);
|
|
if (clientB) await closeClient(clientB);
|
|
}
|
|
});
|
|
});
|