mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-19 01:17:31 +00:00
Remove the full welcome tour (Welcome, AddTask, DeleteTask, Projects, Sync, IssueProviders, ProductivityHelper, FinalCongrats, StartTourAgain) and keep only the KeyboardNav tour, triggered from the Help menu. - Delete ShepherdComponent (auto-start on load) - Strip shepherd-steps.const.ts to KeyboardNav steps only - Simplify ShepherdService with _initPromise guard and fresh tour on re-open - Remove 3 tour menu items from Help menu, keep keyboard tour - Remove waitForEl/waitForElObs$ helpers (unused by KeyboardNav) - Delete e2e/utils/tour-helpers.ts and all tour dismissal calls - Clean up stale tour comments across e2e tests - Fix pre-existing build error: add isFinishDayEnabled to AppFeaturesConfig type, defaults, form toggle, component signal, and translations
460 lines
16 KiB
TypeScript
460 lines
16 KiB
TypeScript
import { expect } from '@playwright/test';
|
|
import { test } from '../../fixtures/webdav.fixture';
|
|
import { SyncPage } from '../../pages/sync.page';
|
|
import { WorkViewPage } from '../../pages/work-view.page';
|
|
import { TaskPage } from '../../pages/task.page';
|
|
import { TagPage } from '../../pages/tag.page';
|
|
import { ProjectPage } from '../../pages/project.page';
|
|
import { waitForStatePersistence, waitForAppReady } from '../../utils/waits';
|
|
import {
|
|
WEBDAV_CONFIG_TEMPLATE,
|
|
setupSyncClient,
|
|
createSyncFolder,
|
|
waitForSyncComplete,
|
|
generateSyncFolderName,
|
|
waitForArchivePersistence,
|
|
closeContextsSafely,
|
|
} from '../../utils/sync-helpers';
|
|
import { Page } from 'playwright';
|
|
|
|
/**
|
|
* WebDAV Delete Cascade E2E Tests
|
|
*
|
|
* These tests verify that deleting entities (tags/projects) properly cascades
|
|
* to archived tasks across multiple clients:
|
|
* - Delete tag with archived tasks syncs to other client
|
|
* - Delete project with archived tasks syncs to other client
|
|
* - Concurrent delete tag + archive task
|
|
*/
|
|
|
|
/**
|
|
* Archive done tasks via Daily Summary flow
|
|
* Adapted from webdav-sync-archive.spec.ts
|
|
*/
|
|
const archiveDoneTasks = async (page: Page): Promise<void> => {
|
|
const finishDayBtn = page.locator('.e2e-finish-day');
|
|
await finishDayBtn.waitFor({ state: 'visible', timeout: 10000 });
|
|
await finishDayBtn.click();
|
|
|
|
await page.waitForURL(/daily-summary/, { timeout: 15000 });
|
|
|
|
const saveAndGoHomeBtn = page.locator(
|
|
'daily-summary button[mat-flat-button]:has(mat-icon:has-text("wb_sunny"))',
|
|
);
|
|
await saveAndGoHomeBtn.waitFor({ state: 'visible', timeout: 10000 });
|
|
await saveAndGoHomeBtn.click();
|
|
|
|
await page.waitForURL(/(active\/tasks|tag\/TODAY\/tasks)/, { timeout: 15000 });
|
|
};
|
|
|
|
test.describe('@webdav WebDAV Delete Cascade Sync', () => {
|
|
test.describe.configure({ mode: 'serial' });
|
|
|
|
/**
|
|
* Test 1.1: Delete tag with archived tasks syncs to other client
|
|
*
|
|
* Setup:
|
|
* - Client A creates Tag1
|
|
* - Client A creates Task1 with Tag1
|
|
* - Client A marks Task1 done, archives it
|
|
* - Client A syncs
|
|
* - Client B syncs (downloads archived Task1)
|
|
*
|
|
* Test:
|
|
* 1. Client A deletes Tag1
|
|
* 2. Client A syncs
|
|
* 3. Client B syncs
|
|
* 4. Verify: No errors, tag cascade completes successfully
|
|
*/
|
|
test('Delete tag with archived tasks syncs to other client', async ({
|
|
browser,
|
|
baseURL,
|
|
request,
|
|
}) => {
|
|
test.slow();
|
|
const SYNC_FOLDER_NAME = generateSyncFolderName('e2e-del-tag-archive');
|
|
const WEBDAV_CONFIG = {
|
|
...WEBDAV_CONFIG_TEMPLATE,
|
|
syncFolderPath: `/${SYNC_FOLDER_NAME}`,
|
|
};
|
|
const url = baseURL || 'http://localhost:4242';
|
|
|
|
await createSyncFolder(request, SYNC_FOLDER_NAME);
|
|
|
|
// --- Setup Client A ---
|
|
const { context: contextA, page: pageA } = await setupSyncClient(browser, url);
|
|
const syncPageA = new SyncPage(pageA);
|
|
const workViewPageA = new WorkViewPage(pageA);
|
|
const taskPageA = new TaskPage(pageA);
|
|
const tagPageA = new TagPage(pageA);
|
|
|
|
await workViewPageA.waitForTaskList();
|
|
await syncPageA.setupWebdavSync(WEBDAV_CONFIG);
|
|
console.log('[Delete Tag] Client A configured');
|
|
|
|
// Client A creates a tag
|
|
const tagName = 'TestTag-ToDelete';
|
|
await tagPageA.createTag(tagName);
|
|
console.log('[Delete Tag] Client A created tag');
|
|
|
|
// Client A creates task and assigns tag
|
|
const taskName = 'Task-WithTag';
|
|
await workViewPageA.addTask(taskName);
|
|
const task = taskPageA.getTaskByText(taskName).first();
|
|
await tagPageA.assignTagToTask(task, tagName);
|
|
await expect(tagPageA.getTagOnTask(task, tagName)).toBeVisible();
|
|
console.log('[Delete Tag] Client A created task with tag');
|
|
|
|
// Mark task done and archive
|
|
await taskPageA.markTaskAsDone(task);
|
|
await pageA.waitForTimeout(300);
|
|
await archiveDoneTasks(pageA);
|
|
await waitForArchivePersistence(pageA);
|
|
await expect(pageA.locator('task')).toHaveCount(0);
|
|
console.log('[Delete Tag] Client A archived task');
|
|
|
|
// Client A syncs
|
|
await waitForStatePersistence(pageA);
|
|
await syncPageA.triggerSync();
|
|
await waitForSyncComplete(pageA, syncPageA);
|
|
console.log('[Delete Tag] Client A initial sync complete');
|
|
|
|
// --- Setup Client B ---
|
|
const { context: contextB, page: pageB } = await setupSyncClient(browser, url);
|
|
const syncPageB = new SyncPage(pageB);
|
|
const workViewPageB = new WorkViewPage(pageB);
|
|
const tagPageB = new TagPage(pageB);
|
|
|
|
await workViewPageB.waitForTaskList();
|
|
await syncPageB.setupWebdavSync(WEBDAV_CONFIG);
|
|
console.log('[Delete Tag] Client B configured');
|
|
|
|
// Client B syncs (downloads archived task with tag)
|
|
await syncPageB.triggerSync();
|
|
await waitForSyncComplete(pageB, syncPageB);
|
|
console.log('[Delete Tag] Client B synced, downloaded archived task');
|
|
|
|
// Verify tag exists on Client B
|
|
const tagExistsB = await tagPageB.tagExistsInSidebar(tagName);
|
|
expect(tagExistsB).toBe(true);
|
|
console.log('[Delete Tag] Verified tag exists on Client B');
|
|
|
|
// --- Client A deletes the tag ---
|
|
await tagPageA.deleteTag(tagName);
|
|
console.log('[Delete Tag] Client A deleted tag');
|
|
|
|
// Client A syncs
|
|
await waitForStatePersistence(pageA);
|
|
await syncPageA.triggerSync();
|
|
await waitForSyncComplete(pageA, syncPageA);
|
|
console.log('[Delete Tag] Client A synced deletion');
|
|
|
|
// Client B syncs
|
|
await syncPageB.triggerSync();
|
|
await waitForSyncComplete(pageB, syncPageB);
|
|
console.log('[Delete Tag] Client B synced');
|
|
|
|
// --- Verify final state ---
|
|
// Tag should be gone on both clients
|
|
const tagExistsAFinal = await tagPageA.tagExistsInSidebar(tagName);
|
|
expect(tagExistsAFinal).toBe(false);
|
|
console.log('[Delete Tag] Tag deleted on Client A');
|
|
|
|
const tagExistsBFinal = await tagPageB.tagExistsInSidebar(tagName);
|
|
expect(tagExistsBFinal).toBe(false);
|
|
console.log('[Delete Tag] Tag deleted on Client B');
|
|
|
|
// Reload Client B and verify no errors (archive should be cleaned)
|
|
await pageB.reload();
|
|
await waitForAppReady(pageB);
|
|
await workViewPageB.waitForTaskList();
|
|
|
|
// Check for no global errors
|
|
const globalError = pageB.locator('.global-error-alert');
|
|
await expect(globalError).not.toBeVisible({ timeout: 3000 });
|
|
console.log('[Delete Tag] No errors after reload on Client B');
|
|
|
|
console.log('[Delete Tag] ✓ Delete tag with archived tasks synced correctly');
|
|
|
|
// Cleanup
|
|
await closeContextsSafely(contextA, contextB);
|
|
});
|
|
|
|
/**
|
|
* Test 1.2: Delete project with archived tasks syncs to other client
|
|
*
|
|
* Setup:
|
|
* - Client A creates Project1
|
|
* - Client A creates Task1 in Project1
|
|
* - Client A marks Task1 done, archives it
|
|
* - Client A syncs
|
|
* - Client B syncs (downloads archived Task1)
|
|
*
|
|
* Test:
|
|
* 1. Client A deletes Project1
|
|
* 2. Client A syncs
|
|
* 3. Client B syncs
|
|
* 4. Verify: Archived task is deleted on both clients
|
|
*/
|
|
test('Delete project with archived tasks syncs to other client', async ({
|
|
browser,
|
|
baseURL,
|
|
request,
|
|
}) => {
|
|
test.slow();
|
|
const SYNC_FOLDER_NAME = generateSyncFolderName('e2e-del-proj-archive');
|
|
const WEBDAV_CONFIG = {
|
|
...WEBDAV_CONFIG_TEMPLATE,
|
|
syncFolderPath: `/${SYNC_FOLDER_NAME}`,
|
|
};
|
|
const url = baseURL || 'http://localhost:4242';
|
|
|
|
await createSyncFolder(request, SYNC_FOLDER_NAME);
|
|
|
|
// --- Setup Client A ---
|
|
const { context: contextA, page: pageA } = await setupSyncClient(browser, url);
|
|
const syncPageA = new SyncPage(pageA);
|
|
const workViewPageA = new WorkViewPage(pageA);
|
|
const taskPageA = new TaskPage(pageA);
|
|
const projectPageA = new ProjectPage(pageA);
|
|
|
|
await workViewPageA.waitForTaskList();
|
|
await syncPageA.setupWebdavSync(WEBDAV_CONFIG);
|
|
console.log('[Delete Project] Client A configured');
|
|
|
|
// Client A creates a project
|
|
const projectName = 'TestProject-ToDelete';
|
|
await projectPageA.createProject(projectName);
|
|
await projectPageA.navigateToProjectByName(projectName);
|
|
console.log('[Delete Project] Client A created and navigated to project');
|
|
|
|
// Client A creates task in project
|
|
const taskName = 'Task-InProject';
|
|
await workViewPageA.addTask(taskName);
|
|
await expect(pageA.locator('task')).toHaveCount(1);
|
|
console.log('[Delete Project] Client A created task in project');
|
|
|
|
// Mark task done
|
|
const task = taskPageA.getTaskByText(taskName).first();
|
|
await taskPageA.markTaskAsDone(task);
|
|
await pageA.waitForTimeout(500);
|
|
|
|
// For project view, use "Move done to archive" button instead of Daily Summary
|
|
const moveToArchiveBtn = pageA.locator('.e2e-move-done-to-archive');
|
|
await moveToArchiveBtn.waitFor({ state: 'visible', timeout: 5000 });
|
|
await moveToArchiveBtn.click();
|
|
await pageA.waitForTimeout(500);
|
|
await expect(pageA.locator('task')).toHaveCount(0);
|
|
console.log('[Delete Project] Client A archived task');
|
|
|
|
// Navigate to TODAY view before syncing
|
|
await pageA.goto(`${url}/#/tag/TODAY/tasks`);
|
|
await workViewPageA.waitForTaskList();
|
|
|
|
// Client A syncs
|
|
await waitForStatePersistence(pageA);
|
|
await syncPageA.triggerSync();
|
|
await waitForSyncComplete(pageA, syncPageA);
|
|
console.log('[Delete Project] Client A initial sync complete');
|
|
|
|
// --- Setup Client B ---
|
|
const { context: contextB, page: pageB } = await setupSyncClient(browser, url);
|
|
const syncPageB = new SyncPage(pageB);
|
|
const workViewPageB = new WorkViewPage(pageB);
|
|
const projectPageB = new ProjectPage(pageB);
|
|
|
|
await workViewPageB.waitForTaskList();
|
|
await syncPageB.setupWebdavSync(WEBDAV_CONFIG);
|
|
console.log('[Delete Project] Client B configured');
|
|
|
|
// Client B syncs (downloads project and archived task)
|
|
await syncPageB.triggerSync();
|
|
await waitForSyncComplete(pageB, syncPageB);
|
|
console.log('[Delete Project] Client B synced');
|
|
|
|
// Navigate to the project directly to verify it exists on Client B
|
|
await projectPageB.navigateToProjectByName(projectName);
|
|
console.log('[Delete Project] Verified project exists on Client B (navigated to it)');
|
|
|
|
// --- Client A deletes the project ---
|
|
await projectPageA.deleteProject(projectName);
|
|
console.log('[Delete Project] Client A deleted project');
|
|
|
|
// Client A syncs
|
|
await waitForStatePersistence(pageA);
|
|
await syncPageA.triggerSync();
|
|
await waitForSyncComplete(pageA, syncPageA);
|
|
console.log('[Delete Project] Client A synced deletion');
|
|
|
|
// Client B syncs
|
|
await syncPageB.triggerSync();
|
|
await waitForSyncComplete(pageB, syncPageB);
|
|
console.log('[Delete Project] Client B synced');
|
|
|
|
// --- Verify final state ---
|
|
// Navigate to TODAY view on Client B
|
|
await pageB.goto(`${url}/#/tag/TODAY/tasks`);
|
|
await workViewPageB.waitForTaskList();
|
|
|
|
// Check for no global errors
|
|
const globalError = pageB.locator('.global-error-alert');
|
|
await expect(globalError).not.toBeVisible({ timeout: 3000 });
|
|
|
|
// Project should not exist in sidebar
|
|
const projectInSidebar = pageB
|
|
.locator('magic-side-nav')
|
|
.locator(`text=${projectName}`);
|
|
await expect(projectInSidebar).not.toBeVisible({ timeout: 3000 });
|
|
|
|
console.log('[Delete Project] Project deleted on Client B');
|
|
console.log('[Delete Project] ✓ Delete project with archived tasks synced correctly');
|
|
|
|
// Cleanup
|
|
await closeContextsSafely(contextA, contextB);
|
|
});
|
|
|
|
/**
|
|
* Test 1.3: Concurrent delete tag + archive task
|
|
*
|
|
* Setup:
|
|
* - Client A creates Tag1
|
|
* - Client A creates Task1 with Tag1, Task2 with Tag1
|
|
* - Both sync
|
|
*
|
|
* Test:
|
|
* 1. Client A deletes Tag1 (before sync)
|
|
* 2. Client B marks Task1 done, archives it (before sync)
|
|
* 3. Client A syncs
|
|
* 4. Client B syncs
|
|
* 5. Verify: Task1 is archived but without Tag1 reference
|
|
* 6. Verify: Consistent state on both clients
|
|
*/
|
|
test('Concurrent delete tag and archive task', async ({
|
|
browser,
|
|
baseURL,
|
|
request,
|
|
}) => {
|
|
test.slow();
|
|
const SYNC_FOLDER_NAME = generateSyncFolderName('e2e-del-tag-concurrent');
|
|
const WEBDAV_CONFIG = {
|
|
...WEBDAV_CONFIG_TEMPLATE,
|
|
syncFolderPath: `/${SYNC_FOLDER_NAME}`,
|
|
};
|
|
const url = baseURL || 'http://localhost:4242';
|
|
|
|
await createSyncFolder(request, SYNC_FOLDER_NAME);
|
|
|
|
// --- Setup Client A ---
|
|
const { context: contextA, page: pageA } = await setupSyncClient(browser, url);
|
|
const syncPageA = new SyncPage(pageA);
|
|
const workViewPageA = new WorkViewPage(pageA);
|
|
const taskPageA = new TaskPage(pageA);
|
|
const tagPageA = new TagPage(pageA);
|
|
|
|
await workViewPageA.waitForTaskList();
|
|
await syncPageA.setupWebdavSync(WEBDAV_CONFIG);
|
|
console.log('[Concurrent] Client A configured');
|
|
|
|
// Client A creates a tag
|
|
const tagName = 'TestTag-Concurrent';
|
|
await tagPageA.createTag(tagName);
|
|
console.log('[Concurrent] Client A created tag');
|
|
|
|
// Client A creates two tasks and assigns tag
|
|
const task1Name = 'Task1-ToArchive';
|
|
const task2Name = 'Task2-Remains';
|
|
await workViewPageA.addTask(task1Name);
|
|
await workViewPageA.addTask(task2Name);
|
|
|
|
const task1 = taskPageA.getTaskByText(task1Name).first();
|
|
const task2 = taskPageA.getTaskByText(task2Name).first();
|
|
await tagPageA.assignTagToTask(task1, tagName);
|
|
await tagPageA.assignTagToTask(task2, tagName);
|
|
console.log('[Concurrent] Client A created 2 tasks with tag');
|
|
|
|
// Client A syncs
|
|
await waitForStatePersistence(pageA);
|
|
await syncPageA.triggerSync();
|
|
await waitForSyncComplete(pageA, syncPageA);
|
|
console.log('[Concurrent] Client A initial sync complete');
|
|
|
|
// --- Setup Client B ---
|
|
const { context: contextB, page: pageB } = await setupSyncClient(browser, url);
|
|
const syncPageB = new SyncPage(pageB);
|
|
const workViewPageB = new WorkViewPage(pageB);
|
|
const taskPageB = new TaskPage(pageB);
|
|
|
|
await workViewPageB.waitForTaskList();
|
|
await syncPageB.setupWebdavSync(WEBDAV_CONFIG);
|
|
console.log('[Concurrent] Client B configured');
|
|
|
|
// Client B syncs (downloads tasks with tag)
|
|
await syncPageB.triggerSync();
|
|
await waitForSyncComplete(pageB, syncPageB);
|
|
await expect(pageB.locator('task')).toHaveCount(2);
|
|
console.log('[Concurrent] Client B downloaded 2 tasks');
|
|
|
|
// --- Concurrent operations ---
|
|
|
|
// Client A deletes the tag (before sync)
|
|
await tagPageA.deleteTag(tagName);
|
|
console.log('[Concurrent] Client A deleted tag (not synced yet)');
|
|
|
|
// Client B archives Task1 (before sync)
|
|
const task1OnB = taskPageB.getTaskByText(task1Name).first();
|
|
await taskPageB.markTaskAsDone(task1OnB);
|
|
await pageB.waitForTimeout(300);
|
|
await archiveDoneTasks(pageB);
|
|
await waitForArchivePersistence(pageB);
|
|
await expect(pageB.locator('task')).toHaveCount(1); // Only Task2 visible
|
|
console.log('[Concurrent] Client B archived Task1 (not synced yet)');
|
|
|
|
// --- Both clients sync ---
|
|
await waitForStatePersistence(pageA);
|
|
await syncPageA.triggerSync();
|
|
await waitForSyncComplete(pageA, syncPageA);
|
|
console.log('[Concurrent] Client A synced tag deletion');
|
|
|
|
await waitForStatePersistence(pageB);
|
|
await syncPageB.triggerSync();
|
|
await waitForSyncComplete(pageB, syncPageB);
|
|
console.log('[Concurrent] Client B synced archive');
|
|
|
|
// Sync again for eventual consistency
|
|
await syncPageA.triggerSync();
|
|
await waitForSyncComplete(pageA, syncPageA);
|
|
await syncPageB.triggerSync();
|
|
await waitForSyncComplete(pageB, syncPageB);
|
|
console.log('[Concurrent] Both synced again for consistency');
|
|
|
|
// --- Verify final state ---
|
|
// Tag should be deleted on both clients
|
|
const tagExistsAFinal = await tagPageA.tagExistsInSidebar(tagName);
|
|
expect(tagExistsAFinal).toBe(false);
|
|
|
|
// Reload Client B and verify no errors
|
|
await pageB.reload();
|
|
await waitForAppReady(pageB);
|
|
await workViewPageB.waitForTaskList();
|
|
|
|
const globalError = pageB.locator('.global-error-alert');
|
|
await expect(globalError).not.toBeVisible({ timeout: 3000 });
|
|
|
|
// Task2 should still be visible (but without tag since it was deleted)
|
|
await expect(taskPageB.getTaskByText(task2Name).first()).toBeVisible();
|
|
console.log('[Concurrent] Task2 still visible on Client B');
|
|
|
|
// The tag should not be on Task2 anymore
|
|
const task2AfterReload = taskPageB.getTaskByText(task2Name).first();
|
|
const tagOnTask2 = tagPageA.getTagOnTask(task2AfterReload, tagName);
|
|
await expect(tagOnTask2).not.toBeVisible({ timeout: 2000 });
|
|
console.log('[Concurrent] Tag no longer on Task2');
|
|
|
|
console.log(
|
|
'[Concurrent] ✓ Concurrent delete tag and archive task resolved correctly',
|
|
);
|
|
|
|
// Cleanup
|
|
await closeContextsSafely(contextA, contextB);
|
|
});
|
|
});
|