mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 08:56:41 +00:00
* feat(history): merge Quick History and Worklog into one view - single /history route; legacy worklog & quick-history redirect to it - one History menu entry; navigate-to-task targets history - show per-day work start-end as its own column in the day table - show enabled habits/simple-counters (icon + tooltip) in the expanded day - extract HistoryTaskRowComponent for archived task rows - remove WorklogComponent and QuickHistoryComponent * fix(history): restore quick history behavior * refactor(history): collapse to a single unified view - remove the Worklog/Quick History toggle; show one full all-time history tree (year -> month -> week -> day) - drop the quick-history mode, significance filter and ?view= param - extract shared HistoryDayMetaComponent; reuse HistoryTaskRowComponent in the Daily Summary "This Week" widget - convert project colors + dateStr auto-expand to signals - update e2e specs to the single-view selectors * refactor(history): remove dead quick-history code and i18n keys After merging Quick History into the unified History view, the quick-history data pipeline had no consumers. Remove it: - _quickHistoryData$, quickHistoryWeeks$, _loadQuickHistoryForWorkContext - map-archive-to-worklog-weeks util (+ tz spec) and WorklogYearsWithWeeks - orphaned F.QUICK_HISTORY, MH.QUICK_HISTORY, MH.WORKLOG translation keys Keep WorklogWeekSimple (base of the still-used WorklogWeek). * docs(history): reflect Quick History merge into unified History The quick-history and worklog routes are now legacy aliases for the single unified History view; drop the stale 'current-year mode/toggle' framing. Concepts-note prose flagged for human review. * feat(history): improve a11y and i18n of the unified history view Accessibility: - week table uses <thead> + <th scope=col>; icon-only work-times header gets a translated aria-label - expandable day row: real <button> disclosure control carrying aria-expanded/aria-controls (keyboard + AT path) instead of a role=button <tr>; row keeps a pointer click - archived-task title is now a <button> (was a non-focusable <span>) i18n: translate previously hardcoded aria-labels (export/restore) and the week-range tooltip; reuse VIEW_TASK_DETAILS/RESTORE_TASK_FROM_ARCHIVE, add EXPORT_DATA + WEEK_RANGE_TOOLTIP keys. Polish (carried in): convert worklogData$ to a toSignal signal (drop AsyncPipe + dead animations), relocate the shared row to features/worklog/worklog-task-row (WorklogTaskRowComponent) to break a worklog<->history cycle, make template-unused services private, tighten projectColor input type. SCSS focus rings use focus-ring tokens. e2e: locate task titles via td.title button after the span->button change.
1054 lines
38 KiB
TypeScript
1054 lines
38 KiB
TypeScript
import { test, expect } from '../../fixtures/supersync.fixture';
|
|
import {
|
|
createTestUser,
|
|
getSuperSyncConfig,
|
|
createSimulatedClient,
|
|
closeClient,
|
|
waitForTask,
|
|
getTaskElement,
|
|
getParentTaskElement,
|
|
markTaskDone,
|
|
markSubtaskDone,
|
|
expandTask,
|
|
deleteTask,
|
|
renameTask,
|
|
startTimeTracking,
|
|
stopTimeTracking,
|
|
waitForTaskTimeSpent,
|
|
getTaskCount,
|
|
hasTaskOnClient,
|
|
type SimulatedE2EClient,
|
|
} from '../../utils/supersync-helpers';
|
|
import {
|
|
expectTaskVisible,
|
|
expectTaskNotVisible,
|
|
expectTaskDone,
|
|
expectTaskOnAllClients,
|
|
expectSubtaskVisible,
|
|
expectSubtaskDone,
|
|
expectSubtaskNotDone,
|
|
expectEqualTaskCount,
|
|
expectTaskCount,
|
|
} from '../../utils/supersync-assertions';
|
|
import { waitForAppReady } from '../../utils/waits';
|
|
|
|
/**
|
|
* SuperSync E2E Tests
|
|
*
|
|
* These tests verify multi-client sync using the real super-sync-server.
|
|
* They mirror scenarios from sync-scenarios.integration.spec.ts but test
|
|
* the full stack including UI, network, and real IndexedDB isolation.
|
|
*
|
|
* Prerequisites:
|
|
* - super-sync-server running on localhost:1901 with TEST_MODE=true
|
|
* - Frontend running on localhost:4242
|
|
*
|
|
* Run with: npm run e2e:supersync
|
|
*/
|
|
|
|
test.describe('@supersync SuperSync E2E', () => {
|
|
/**
|
|
* Scenario 2.1: Client A Creates, Client B Downloads
|
|
*
|
|
* This is the simplest sync scenario - one client creates data,
|
|
* another client downloads it.
|
|
*
|
|
* Setup: Client A and B, empty server
|
|
*
|
|
* Actions:
|
|
* 1. Client A creates "Task 1", syncs
|
|
* 2. Client B syncs (download)
|
|
*
|
|
* Expected:
|
|
* - Client A: has Task 1
|
|
* - Client B: has Task 1 (received via sync)
|
|
* - Server: has 1 operation
|
|
*/
|
|
test('2.1 Client A creates task, Client B downloads it', async ({
|
|
browser,
|
|
baseURL,
|
|
testRunId,
|
|
}) => {
|
|
let clientA: SimulatedE2EClient | null = null;
|
|
let clientB: SimulatedE2EClient | null = null;
|
|
|
|
try {
|
|
// Create shared test user (both clients use same account)
|
|
const user = await createTestUser(testRunId);
|
|
const syncConfig = getSuperSyncConfig(user);
|
|
|
|
// Set up Client A
|
|
clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
|
|
await clientA.sync.setupSuperSync(syncConfig);
|
|
|
|
// Step 1: Client A creates a task
|
|
const taskName = `Task-${testRunId}-from-A`;
|
|
await clientA.workView.addTask(taskName);
|
|
|
|
// Step 2: Client A syncs (upload)
|
|
await clientA.sync.syncAndWait();
|
|
|
|
// Verify Client A still has the task
|
|
await waitForTask(clientA.page, taskName);
|
|
|
|
// Set up Client B (fresh context = isolated IndexedDB)
|
|
clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
|
|
await clientB.sync.setupSuperSync(syncConfig);
|
|
|
|
// Step 3: Client B syncs (download)
|
|
await clientB.sync.syncAndWait();
|
|
|
|
// Verify Client B has the task from Client A
|
|
await waitForTask(clientB.page, taskName);
|
|
|
|
// Final assertions
|
|
await expectTaskOnAllClients([clientA, clientB], taskName);
|
|
} finally {
|
|
// Cleanup
|
|
if (clientA) await closeClient(clientA);
|
|
if (clientB) await closeClient(clientB);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Scenario 2.2: Both Clients Create Different Tasks
|
|
*
|
|
* Setup: A and B connected, empty server
|
|
*
|
|
* Actions:
|
|
* 1. Client A creates "Task A", syncs
|
|
* 2. Client B creates "Task B", syncs
|
|
* 3. Client A syncs (download)
|
|
*
|
|
* Expected: Both clients have both tasks
|
|
*/
|
|
test('2.2 Both clients create different tasks', async ({
|
|
browser,
|
|
baseURL,
|
|
testRunId,
|
|
}) => {
|
|
let clientA: SimulatedE2EClient | null = null;
|
|
let clientB: SimulatedE2EClient | null = null;
|
|
|
|
try {
|
|
const user = await createTestUser(testRunId);
|
|
const syncConfig = getSuperSyncConfig(user);
|
|
|
|
// Set up both clients
|
|
clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
|
|
await clientA.sync.setupSuperSync(syncConfig);
|
|
|
|
clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
|
|
await clientB.sync.setupSuperSync(syncConfig);
|
|
|
|
// Step 1: Client A creates Task A
|
|
const taskA = `TaskA-${testRunId}`;
|
|
await clientA.workView.addTask(taskA);
|
|
// Wait for task to be fully created in store
|
|
await waitForTask(clientA.page, taskA);
|
|
|
|
// Step 2: Client A syncs (upload Task A)
|
|
await clientA.sync.syncAndWait();
|
|
console.log('[Test] Client A synced Task A');
|
|
|
|
// Step 3: Client B creates Task B
|
|
const taskB = `TaskB-${testRunId}`;
|
|
await clientB.workView.addTask(taskB);
|
|
// Wait for task to be fully created in store
|
|
await waitForTask(clientB.page, taskB);
|
|
|
|
// Step 4: Client B syncs (upload Task B, download Task A)
|
|
await clientB.sync.syncAndWait();
|
|
console.log('[Test] Client B synced (uploaded Task B, downloaded Task A)');
|
|
|
|
// Step 5: Client A syncs (download Task B)
|
|
await clientA.sync.syncAndWait();
|
|
console.log('[Test] Client A synced (downloaded Task B)');
|
|
|
|
// Wait for UI to settle after sync
|
|
await clientA.page.waitForTimeout(500);
|
|
await clientB.page.waitForTimeout(500);
|
|
|
|
// Verify both clients have both tasks
|
|
await waitForTask(clientA.page, taskA);
|
|
await waitForTask(clientA.page, taskB);
|
|
await waitForTask(clientB.page, taskA);
|
|
await waitForTask(clientB.page, taskB);
|
|
|
|
await expectTaskOnAllClients([clientA, clientB], taskA);
|
|
await expectTaskOnAllClients([clientA, clientB], taskB);
|
|
} finally {
|
|
if (clientA) await closeClient(clientA);
|
|
if (clientB) await closeClient(clientB);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Scenario 1.3: Update Task and Sync
|
|
*
|
|
* Setup: Client A with existing task
|
|
*
|
|
* Actions:
|
|
* 1. Client A creates "Task 1", syncs
|
|
* 2. Client B syncs (download)
|
|
* 3. Client A marks task as done, syncs
|
|
* 4. Client B syncs
|
|
*
|
|
* Expected: Both clients see task as done
|
|
*/
|
|
test('1.3 Update propagates between clients', async ({
|
|
browser,
|
|
baseURL,
|
|
testRunId,
|
|
}) => {
|
|
let clientA: SimulatedE2EClient | null = null;
|
|
let clientB: SimulatedE2EClient | null = null;
|
|
|
|
try {
|
|
const user = await createTestUser(testRunId);
|
|
const syncConfig = getSuperSyncConfig(user);
|
|
|
|
// Set up Client A and create task
|
|
clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
|
|
await clientA.sync.setupSuperSync(syncConfig);
|
|
|
|
const taskName = `Task-${testRunId}-update`;
|
|
await clientA.workView.addTask(taskName);
|
|
await clientA.sync.syncAndWait();
|
|
|
|
// Set up Client B and sync to get the task
|
|
clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
|
|
await clientB.sync.setupSuperSync(syncConfig);
|
|
await clientB.sync.syncAndWait();
|
|
|
|
// Verify Client B has the task
|
|
await waitForTask(clientB.page, taskName);
|
|
|
|
// Client A marks task as done
|
|
await markTaskDone(clientA, taskName);
|
|
|
|
// Client A syncs the update
|
|
await clientA.sync.syncAndWait();
|
|
|
|
// Client B syncs to receive the update
|
|
await clientB.sync.syncAndWait();
|
|
|
|
// Verify both show task as done
|
|
await expectTaskDone(clientA, taskName);
|
|
await expectTaskDone(clientB, taskName);
|
|
} finally {
|
|
if (clientA) await closeClient(clientA);
|
|
if (clientB) await closeClient(clientB);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Scenario 1.4: Delete Task and Sync
|
|
*
|
|
* Actions:
|
|
* 1. Client A creates task, syncs
|
|
* 2. Client B syncs (download)
|
|
* 3. Client A deletes task, syncs
|
|
* 4. Client B syncs
|
|
*
|
|
* Expected: Task removed from both clients
|
|
*/
|
|
test('1.4 Delete propagates between clients', async ({
|
|
browser,
|
|
baseURL,
|
|
testRunId,
|
|
}) => {
|
|
let clientA: SimulatedE2EClient | null = null;
|
|
let clientB: SimulatedE2EClient | null = null;
|
|
|
|
try {
|
|
const user = await createTestUser(testRunId);
|
|
const syncConfig = getSuperSyncConfig(user);
|
|
|
|
// Set up Client A and create task
|
|
clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
|
|
await clientA.sync.setupSuperSync(syncConfig);
|
|
|
|
const taskName = `Task-${testRunId}-delete`;
|
|
await clientA.workView.addTask(taskName);
|
|
await clientA.sync.syncAndWait();
|
|
|
|
// Set up Client B and sync
|
|
clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
|
|
await clientB.sync.setupSuperSync(syncConfig);
|
|
await clientB.sync.syncAndWait();
|
|
|
|
// Verify Client B has the task
|
|
await waitForTask(clientB.page, taskName);
|
|
|
|
// Client A deletes the task
|
|
await deleteTask(clientA, taskName);
|
|
|
|
// Client A syncs the deletion
|
|
await clientA.sync.syncAndWait();
|
|
|
|
// Client B syncs to receive the deletion
|
|
await clientB.sync.syncAndWait();
|
|
|
|
// Verify task is removed from both clients
|
|
await expectTaskNotVisible(clientA, taskName);
|
|
await expectTaskNotVisible(clientB, taskName);
|
|
} finally {
|
|
if (clientA) await closeClient(clientA);
|
|
if (clientB) await closeClient(clientB);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Scenario 3.1: Concurrent Edits on Same Task
|
|
*
|
|
* This tests basic conflict handling - both clients edit the same
|
|
* task without seeing each other's changes first.
|
|
*
|
|
* Actions:
|
|
* 1. Client A creates task, syncs
|
|
* 2. Client B syncs (download)
|
|
* 3. Client A marks task done (no sync yet)
|
|
* 4. Client B adds notes to task (no sync yet)
|
|
* 5. Client A syncs
|
|
* 6. Client B syncs
|
|
*
|
|
* Expected: Conflict detected or auto-merged, final state consistent
|
|
*/
|
|
test('3.1 Concurrent edits handled gracefully', async ({
|
|
browser,
|
|
baseURL,
|
|
testRunId,
|
|
}) => {
|
|
let clientA: SimulatedE2EClient | null = null;
|
|
let clientB: SimulatedE2EClient | null = null;
|
|
|
|
try {
|
|
const user = await createTestUser(testRunId);
|
|
const syncConfig = getSuperSyncConfig(user);
|
|
|
|
// Set up both clients
|
|
clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
|
|
await clientA.sync.setupSuperSync(syncConfig);
|
|
|
|
const taskName = `Task-${testRunId}-conflict`;
|
|
await clientA.workView.addTask(taskName);
|
|
await clientA.sync.syncAndWait();
|
|
|
|
clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
|
|
await clientB.sync.setupSuperSync(syncConfig);
|
|
await clientB.sync.syncAndWait();
|
|
|
|
// Both clients now have the task
|
|
await waitForTask(clientA.page, taskName);
|
|
await waitForTask(clientB.page, taskName);
|
|
|
|
// Client A marks done (creates local op)
|
|
await markTaskDone(clientA, taskName);
|
|
|
|
// Client B marks done too (concurrent edit)
|
|
await markTaskDone(clientB, taskName);
|
|
|
|
// Client A syncs first
|
|
await clientA.sync.syncAndWait();
|
|
|
|
// Client B syncs (may detect concurrent edit)
|
|
await clientB.sync.syncAndWait();
|
|
|
|
// Client A syncs again to converge
|
|
await clientA.sync.syncAndWait();
|
|
|
|
// Verify both clients have consistent state
|
|
await expectEqualTaskCount([clientA, clientB]);
|
|
|
|
// Task should exist on both
|
|
await expectTaskOnAllClients([clientA, clientB], taskName);
|
|
} finally {
|
|
if (clientA) await closeClient(clientA);
|
|
if (clientB) await closeClient(clientB);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Scenario 2.3: Client A Creates Parent, Client B Creates Subtask
|
|
*
|
|
* Tests parent-child task relationships syncing correctly.
|
|
*
|
|
* Actions:
|
|
* 1. Client A creates parent task, syncs
|
|
* 2. Client B syncs (downloads parent)
|
|
* 3. Client B creates subtask under parent, syncs
|
|
* 4. Client A syncs (downloads subtask)
|
|
*
|
|
* Expected: Both clients have parent with subtask
|
|
*/
|
|
test('2.3 Client A creates parent, Client B creates subtask', async ({
|
|
browser,
|
|
baseURL,
|
|
testRunId,
|
|
}) => {
|
|
let clientA: SimulatedE2EClient | null = null;
|
|
let clientB: SimulatedE2EClient | null = null;
|
|
|
|
try {
|
|
const user = await createTestUser(testRunId);
|
|
const syncConfig = getSuperSyncConfig(user);
|
|
|
|
// Set up Client A and create parent task
|
|
clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
|
|
await clientA.sync.setupSuperSync(syncConfig);
|
|
|
|
const parentTaskName = `Parent-${testRunId}`;
|
|
await clientA.workView.addTask(parentTaskName);
|
|
await clientA.sync.syncAndWait();
|
|
|
|
// Set up Client B and sync to get the parent task
|
|
clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
|
|
await clientB.sync.setupSuperSync(syncConfig);
|
|
await clientB.sync.syncAndWait();
|
|
|
|
// Verify Client B has the parent task
|
|
await waitForTask(clientB.page, parentTaskName);
|
|
|
|
// Client B creates a subtask under the parent
|
|
// Use a name that won't match the parent (no testRunId overlap)
|
|
const subtaskName = `ChildOfParent-${Date.now()}`;
|
|
const parentTaskB = getTaskElement(clientB, parentTaskName);
|
|
await clientB.workView.addSubTask(parentTaskB, subtaskName);
|
|
|
|
// Client B syncs (uploads subtask)
|
|
await clientB.sync.syncAndWait();
|
|
|
|
// Client A syncs (downloads subtask)
|
|
await clientA.sync.syncAndWait();
|
|
|
|
// Verify both clients have parent and subtask
|
|
// First expand the parent task to see subtasks
|
|
await expandTask(clientA, parentTaskName);
|
|
await expandTask(clientB, parentTaskName);
|
|
|
|
// Wait for subtasks to be visible
|
|
await waitForTask(clientA.page, subtaskName);
|
|
await waitForTask(clientB.page, subtaskName);
|
|
|
|
// Verify subtask exists on both
|
|
await expectSubtaskVisible(clientA, subtaskName);
|
|
await expectSubtaskVisible(clientB, subtaskName);
|
|
} finally {
|
|
if (clientA) await closeClient(clientA);
|
|
if (clientB) await closeClient(clientB);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Scenario 4.1: Complex Chain of Actions
|
|
*
|
|
* This test simulates a realistic workflow where two clients
|
|
* perform various actions in sequence, syncing between each step.
|
|
*
|
|
* Chain of operations:
|
|
* 1. Client A: Create parent task "Project X"
|
|
* 2. Sync A → Server → Sync B
|
|
* 3. Client B: Add subtask "Research" under "Project X"
|
|
* 4. Sync B → Server → Sync A
|
|
* 5. Client A: Mark "Research" as done
|
|
* 6. Client A: Add subtask "Implementation"
|
|
* 7. Sync A → Server → Sync B
|
|
* 8. Client B: Add subtask "Testing"
|
|
* 9. Client B: Mark "Testing" as done
|
|
* 10. Sync B → Server → Sync A
|
|
* 11. Verify final state matches on both clients
|
|
*
|
|
* Expected: Both clients have identical final state with:
|
|
* - Parent task "Project X"
|
|
* - "Research" (done)
|
|
* - "Implementation" (not done)
|
|
* - "Testing" (done)
|
|
*/
|
|
test('4.1 Complex chain of actions syncs correctly', async ({
|
|
browser,
|
|
baseURL,
|
|
testRunId,
|
|
}) => {
|
|
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: Initial Setup ============
|
|
clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
|
|
await clientA.sync.setupSuperSync(syncConfig);
|
|
|
|
clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
|
|
await clientB.sync.setupSuperSync(syncConfig);
|
|
|
|
// ============ PHASE 2: Client A Creates Initial Task ============
|
|
const parentTaskName = `ProjectX-${uniqueId}`;
|
|
await clientA.workView.addTask(parentTaskName);
|
|
console.log(`[Chain Test] Client A created task: ${parentTaskName}`);
|
|
|
|
// Sync: A → Server → B
|
|
await clientA.sync.syncAndWait();
|
|
await clientB.sync.syncAndWait();
|
|
|
|
// Verify B has the task
|
|
await waitForTask(clientB.page, parentTaskName);
|
|
console.log('[Chain Test] Client B received initial task');
|
|
|
|
// ============ PHASE 3: Client B Adds First Subtask ============
|
|
const subtask1Name = `Research-${uniqueId}`;
|
|
const parentTaskLocatorB = getTaskElement(clientB, parentTaskName);
|
|
await clientB.workView.addSubTask(parentTaskLocatorB, subtask1Name);
|
|
console.log(`[Chain Test] Client B added subtask: ${subtask1Name}`);
|
|
|
|
// Sync: B → Server → A
|
|
await clientB.sync.syncAndWait();
|
|
await clientA.sync.syncAndWait();
|
|
|
|
// Verify A has the parent task with subtask
|
|
await expandTask(clientA, parentTaskName);
|
|
await waitForTask(clientA.page, subtask1Name);
|
|
console.log('[Chain Test] Client A received subtask from B');
|
|
|
|
// ============ PHASE 4: Client A Marks Subtask Done and Adds Another ============
|
|
await markSubtaskDone(clientA, subtask1Name);
|
|
console.log(`[Chain Test] Client A marked ${subtask1Name} as done`);
|
|
|
|
const subtask2Name = `Implementation-${uniqueId}`;
|
|
const parentTaskLocatorA = getTaskElement(clientA, parentTaskName);
|
|
await clientA.workView.addSubTask(parentTaskLocatorA, subtask2Name);
|
|
console.log(`[Chain Test] Client A added subtask: ${subtask2Name}`);
|
|
|
|
// Sync: A → Server → B
|
|
await clientA.sync.syncAndWait();
|
|
await clientB.sync.syncAndWait();
|
|
|
|
// Verify B has the updates
|
|
await expandTask(clientB, parentTaskName);
|
|
await waitForTask(clientB.page, subtask2Name);
|
|
console.log('[Chain Test] Client B received done status and new subtask');
|
|
|
|
// ============ PHASE 5: Client B Adds Third Subtask and Marks It Done ============
|
|
const subtask3Name = `Testing-${uniqueId}`;
|
|
await clientB.workView.addSubTask(
|
|
getTaskElement(clientB, parentTaskName),
|
|
subtask3Name,
|
|
);
|
|
console.log(`[Chain Test] Client B added subtask: ${subtask3Name}`);
|
|
|
|
await markSubtaskDone(clientB, subtask3Name);
|
|
console.log(`[Chain Test] Client B marked ${subtask3Name} as done`);
|
|
|
|
// Sync: B → Server → A
|
|
await clientB.sync.syncAndWait();
|
|
await clientA.sync.syncAndWait();
|
|
|
|
// ============ PHASE 6: Final Verification ============
|
|
console.log('[Chain Test] Verifying final state...');
|
|
|
|
// Both clients should have the parent task
|
|
const finalParentA = getParentTaskElement(clientA, parentTaskName);
|
|
const finalParentB = getParentTaskElement(clientB, parentTaskName);
|
|
await expect(finalParentA).toBeVisible({ timeout: 10000 });
|
|
await expect(finalParentB).toBeVisible({ timeout: 10000 });
|
|
|
|
// Expand parents to see subtasks
|
|
await expandTask(clientA, parentTaskName);
|
|
await expandTask(clientB, parentTaskName);
|
|
|
|
// Verify all three subtasks exist on both clients
|
|
// Research (done - marked by Client A in Phase 4)
|
|
await expectSubtaskDone(clientA, subtask1Name);
|
|
await expectSubtaskDone(clientB, subtask1Name);
|
|
|
|
// Implementation (not done)
|
|
await expectSubtaskNotDone(clientA, subtask2Name);
|
|
await expectSubtaskNotDone(clientB, subtask2Name);
|
|
|
|
// Testing (done - marked by Client B in Phase 5)
|
|
await expectSubtaskDone(clientA, subtask3Name);
|
|
await expectSubtaskDone(clientB, subtask3Name);
|
|
|
|
// Count total tasks should match
|
|
await expectEqualTaskCount([clientA, clientB]);
|
|
await expectTaskCount(clientA, 4); // 1 parent + 3 subtasks
|
|
|
|
console.log('[Chain Test] ✓ All verifications passed!');
|
|
} finally {
|
|
if (clientA) await closeClient(clientA);
|
|
if (clientB) await closeClient(clientB);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Scenario 5.1: Task Archiving and Worklog Sync
|
|
*
|
|
* Tests that archived tasks appear in worklog on both clients after sync.
|
|
*
|
|
* Setup: Client A and B with shared account
|
|
*
|
|
* Actions:
|
|
* 1. Client A creates tasks and marks them done
|
|
* 2. Client A syncs
|
|
* 3. Client B syncs (receives tasks)
|
|
* 4. Client B archives tasks via "Finish Day"
|
|
* 5. Client B syncs (uploads archive operation)
|
|
* 6. Client A syncs (receives archive)
|
|
* 7. Both clients verify tasks appear in worklog
|
|
*
|
|
* Expected:
|
|
* - Both clients show archived tasks in worklog
|
|
* - Task titles visible in worklog entries
|
|
*/
|
|
test('5.1 Archived tasks appear in worklog on both clients', async ({
|
|
browser,
|
|
baseURL,
|
|
testRunId,
|
|
}) => {
|
|
const uniqueId = Date.now();
|
|
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 Creates and Completes Tasks ============
|
|
clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
|
|
await clientA.sync.setupSuperSync(syncConfig);
|
|
|
|
// Create two tasks
|
|
const task1Name = `Archive-Task1-${uniqueId}`;
|
|
const task2Name = `Archive-Task2-${uniqueId}`;
|
|
|
|
await clientA.workView.addTask(task1Name);
|
|
console.log(`[Archive Test] Client A created task: ${task1Name}`);
|
|
|
|
await clientA.workView.addTask(task2Name);
|
|
console.log(`[Archive Test] Client A created task: ${task2Name}`);
|
|
|
|
// Mark both tasks as done
|
|
await markTaskDone(clientA, task1Name);
|
|
console.log(`[Archive Test] Client A marked ${task1Name} as done`);
|
|
|
|
await markTaskDone(clientA, task2Name);
|
|
console.log(`[Archive Test] Client A marked ${task2Name} as done`);
|
|
|
|
// ============ PHASE 2: Sync Tasks to Server ============
|
|
await clientA.sync.syncAndWait();
|
|
console.log('[Archive Test] Client A synced tasks');
|
|
|
|
// ============ PHASE 3: Client B Downloads Tasks ============
|
|
clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
|
|
await clientB.sync.setupSuperSync(syncConfig);
|
|
await clientB.sync.syncAndWait();
|
|
console.log('[Archive Test] Client B synced (downloaded tasks)');
|
|
|
|
// Verify Client B has the tasks
|
|
await waitForTask(clientB.page, task1Name);
|
|
await waitForTask(clientB.page, task2Name);
|
|
console.log('[Archive Test] Client B received both tasks');
|
|
|
|
// ============ PHASE 4: Client B Archives Tasks via Finish Day ============
|
|
// Click "Finish Day" button to go to daily summary
|
|
const finishDayBtn = clientB.page.locator('.e2e-finish-day');
|
|
await finishDayBtn.waitFor({ state: 'visible', timeout: 10000 });
|
|
await finishDayBtn.click();
|
|
console.log('[Archive Test] Client B clicked Finish Day');
|
|
|
|
// Wait for daily summary page
|
|
await clientB.page.waitForURL(/daily-summary/, { timeout: 10000 });
|
|
await clientB.page.waitForLoadState('networkidle');
|
|
console.log('[Archive Test] Client B on daily summary page');
|
|
|
|
// Click the "Save and go home" button to archive tasks
|
|
const saveAndGoHomeBtn = clientB.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();
|
|
console.log('[Archive Test] Client B clicked Save and go home (archiving)');
|
|
|
|
// Wait for navigation back to work view.
|
|
// Use negative lookahead to avoid matching /tag/TODAY/daily-summary prematurely.
|
|
await clientB.page.waitForURL(/(tag\/TODAY(?!\/daily-summary))/, {
|
|
timeout: 10000,
|
|
});
|
|
await clientB.page.waitForLoadState('networkidle');
|
|
console.log('[Archive Test] Client B back on work view after archiving');
|
|
|
|
// ============ PHASE 5: Sync Archive Operation ============
|
|
await clientB.sync.syncAndWait();
|
|
console.log('[Archive Test] Client B synced (uploaded archive)');
|
|
|
|
// Client A syncs to receive archive
|
|
await clientA.sync.syncAndWait();
|
|
console.log('[Archive Test] Client A synced (downloaded archive)');
|
|
|
|
// ============ PHASE 6: Verify Worklog on Both Clients ============
|
|
// Navigate Client A to worklog
|
|
await clientA.page.goto('/#/tag/TODAY/history');
|
|
await clientA.page.waitForLoadState('networkidle');
|
|
await clientA.page.waitForSelector('history', { timeout: 10000 });
|
|
console.log('[Archive Test] Client A navigated to worklog');
|
|
|
|
// Navigate Client B to worklog
|
|
await clientB.page.goto('/#/tag/TODAY/history');
|
|
await clientB.page.waitForLoadState('networkidle');
|
|
await clientB.page.waitForSelector('history', { timeout: 10000 });
|
|
console.log('[Archive Test] Client B navigated to worklog');
|
|
|
|
// Expand the current day's worklog to see tasks
|
|
// Click on the week row to expand it
|
|
const expandWorklogA = async (): Promise<void> => {
|
|
const weekRow = clientA.page.locator('.week-row').first();
|
|
if (await weekRow.isVisible()) {
|
|
await weekRow.click();
|
|
await clientA.page.waitForTimeout(500);
|
|
}
|
|
};
|
|
|
|
const expandWorklogB = async (): Promise<void> => {
|
|
const weekRow = clientB.page.locator('.week-row').first();
|
|
if (await weekRow.isVisible()) {
|
|
await weekRow.click();
|
|
await clientB.page.waitForTimeout(500);
|
|
}
|
|
};
|
|
|
|
await expandWorklogA();
|
|
await expandWorklogB();
|
|
|
|
// Verify tasks appear in worklog on both clients
|
|
// Tasks are shown in .task-title within the worklog table
|
|
const task1InWorklogA = clientA.page.locator(
|
|
`.task-summary-table .task-title:has-text("${task1Name}")`,
|
|
);
|
|
const task2InWorklogA = clientA.page.locator(
|
|
`.task-summary-table .task-title:has-text("${task2Name}")`,
|
|
);
|
|
const task1InWorklogB = clientB.page.locator(
|
|
`.task-summary-table .task-title:has-text("${task1Name}")`,
|
|
);
|
|
const task2InWorklogB = clientB.page.locator(
|
|
`.task-summary-table .task-title:has-text("${task2Name}")`,
|
|
);
|
|
|
|
await expect(task1InWorklogA).toBeVisible({ timeout: 10000 });
|
|
await expect(task2InWorklogA).toBeVisible({ timeout: 10000 });
|
|
console.log('[Archive Test] Client A worklog has both tasks');
|
|
|
|
await expect(task1InWorklogB).toBeVisible({ timeout: 10000 });
|
|
await expect(task2InWorklogB).toBeVisible({ timeout: 10000 });
|
|
console.log('[Archive Test] Client B worklog has both tasks');
|
|
|
|
console.log('[Archive Test] ✓ All verifications passed!');
|
|
} finally {
|
|
if (clientA) await closeClient(clientA);
|
|
if (clientB) await closeClient(clientB);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Scenario 6.1: Time Tracking Sync
|
|
*
|
|
* Tests that time tracked on one client syncs to another client.
|
|
*
|
|
* Setup: Client A and B with shared account
|
|
*
|
|
* Actions:
|
|
* 1. Client A creates a task
|
|
* 2. Client A starts time tracking on the task
|
|
* 3. Wait for time to accumulate (3 seconds)
|
|
* 4. Client A stops time tracking
|
|
* 5. Client A syncs
|
|
* 6. Client B syncs
|
|
* 7. Verify Client B sees the tracked time
|
|
*
|
|
* Expected:
|
|
* - Client B shows timeSpent > 0 for the task
|
|
* - Both clients have matching time values
|
|
*/
|
|
test('6.1 Time tracking syncs between clients', async ({
|
|
browser,
|
|
baseURL,
|
|
testRunId,
|
|
}) => {
|
|
const uniqueId = Date.now();
|
|
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 Creates Task ============
|
|
clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
|
|
await clientA.sync.setupSuperSync(syncConfig);
|
|
|
|
const taskName = `TimeTrack-${uniqueId}`;
|
|
await clientA.workView.addTask(taskName);
|
|
console.log(`[TimeTrack Test] Client A created task: ${taskName}`);
|
|
|
|
// ============ PHASE 2: Start Time Tracking ============
|
|
await startTimeTracking(clientA, taskName);
|
|
console.log('[TimeTrack Test] Client A started time tracking');
|
|
|
|
// Verify tracking started
|
|
const taskLocatorA = getTaskElement(clientA, taskName);
|
|
const playIndicator = taskLocatorA.locator('.play-indicator');
|
|
await expect(playIndicator).toBeVisible({ timeout: 5000 });
|
|
console.log('[TimeTrack Test] Time tracking active');
|
|
|
|
// ============ PHASE 3: Accumulate Time ============
|
|
// Wait for the first global tracking tick before stopping. A fixed sleep can
|
|
// race the app timer under CI load and leave the task with 0 recorded time.
|
|
await waitForTaskTimeSpent(clientA, taskName, 10000);
|
|
console.log('[TimeTrack Test] Time accumulated');
|
|
|
|
// ============ PHASE 4: Stop Time Tracking ============
|
|
await stopTimeTracking(clientA, taskName);
|
|
console.log('[TimeTrack Test] Client A stopped time tracking');
|
|
|
|
// Wait for tracking to stop
|
|
await expect(playIndicator).not.toBeVisible({ timeout: 5000 });
|
|
|
|
// Verify time was recorded on Client A via persisted state. The row can
|
|
// hide the visual time while hover controls are mounted, and sub-minute
|
|
// values render as "-" in the UI.
|
|
const timeSpentA = await waitForTaskTimeSpent(clientA, taskName, 5000);
|
|
console.log(`[TimeTrack Test] Client A recorded time: ${timeSpentA}ms`);
|
|
|
|
// ============ PHASE 5: Sync to Server ============
|
|
await clientA.sync.syncAndWait();
|
|
console.log('[TimeTrack Test] Client A synced');
|
|
|
|
// ============ PHASE 6: Client B Downloads ============
|
|
clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
|
|
await clientB.sync.setupSuperSync(syncConfig);
|
|
// Add delay to ensure any auto-sync from setup has time to start/finish (reduced from 2000ms)
|
|
// or to avoid race conditions with "Sync already in progress"
|
|
await clientB.page.waitForTimeout(500);
|
|
await clientB.sync.syncAndWait();
|
|
console.log('[TimeTrack Test] Client B synced');
|
|
|
|
// Reload to ensure UI reflects DB state (in case of sync UI glitch)
|
|
// Use goto instead of reload - more reliable with service workers
|
|
await clientB.page.goto(clientB.page.url(), {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: 30000,
|
|
});
|
|
await waitForAppReady(clientB.page);
|
|
|
|
// ============ PHASE 7: Verify Time on Client B ============
|
|
// Wait for task to appear
|
|
try {
|
|
await waitForTask(clientB.page, taskName);
|
|
} catch (e) {
|
|
const tasks = await clientB.page.locator('task .task-title').allTextContents();
|
|
console.log('[TimeTrack Test] Client B tasks found:', tasks);
|
|
throw e;
|
|
}
|
|
|
|
await expectTaskVisible(clientB, taskName);
|
|
|
|
// Verify time synced to Client B
|
|
const timeSpentB = await waitForTaskTimeSpent(clientB, taskName, 10000);
|
|
console.log(`[TimeTrack Test] Client B shows time: ${timeSpentB}ms`);
|
|
|
|
// Both clients should have the same synced timeSpent value
|
|
expect(timeSpentB).toBe(timeSpentA);
|
|
console.log('[TimeTrack Test] Time values match on both clients');
|
|
|
|
console.log('[TimeTrack Test] ✓ All verifications passed!');
|
|
} finally {
|
|
if (clientA) await closeClient(clientA);
|
|
if (clientB) await closeClient(clientB);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Scenario 7.1: Three-Client Eventual Consistency
|
|
*
|
|
* Tests that three clients all converge to the same state after
|
|
* concurrent operations and multiple sync rounds. This simulates
|
|
* a real-world scenario with phone, laptop, and desktop.
|
|
*
|
|
* Setup: Clients A, B, C with shared account
|
|
*
|
|
* Timeline:
|
|
* 1. Client A creates Task-1, syncs
|
|
* 2. Client B syncs (gets Task-1), creates Task-2
|
|
* 3. Client C syncs (gets Task-1), creates Task-3, renames Task-1
|
|
* 4. Client B syncs (uploads Task-2)
|
|
* 5. Client C syncs (uploads Task-3 + rename, downloads Task-2)
|
|
* 6. Client A syncs (downloads Task-2, Task-3, rename)
|
|
* 7. Client B syncs (downloads Task-3, rename)
|
|
* 8. All three clients should have identical state
|
|
*
|
|
* Expected:
|
|
* - All clients have Task-1 (renamed), Task-2, Task-3
|
|
* - No data loss from concurrent operations
|
|
* - System achieves eventual consistency
|
|
*/
|
|
test('7.1 Three clients achieve eventual consistency', async ({
|
|
browser,
|
|
baseURL,
|
|
testRunId,
|
|
}) => {
|
|
const uniqueId = Date.now();
|
|
let clientA: SimulatedE2EClient | null = null;
|
|
let clientB: SimulatedE2EClient | null = null;
|
|
let clientC: SimulatedE2EClient | null = null;
|
|
|
|
try {
|
|
// Create shared test user
|
|
const user = await createTestUser(testRunId);
|
|
const syncConfig = getSuperSyncConfig(user);
|
|
|
|
// Task names
|
|
const task1Name = `Task1-${uniqueId}`;
|
|
const task1Renamed = `Task1-Renamed-${uniqueId}`;
|
|
const task2Name = `Task2-${uniqueId}`;
|
|
const task3Name = `Task3-${uniqueId}`;
|
|
|
|
// ============ PHASE 1: Client A Creates Task-1 ============
|
|
console.log('[3-Client Test] Phase 1: Client A creates Task-1');
|
|
clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
|
|
await clientA.sync.setupSuperSync(syncConfig);
|
|
|
|
await clientA.workView.addTask(task1Name);
|
|
console.log(`[3-Client Test] Client A created: ${task1Name}`);
|
|
|
|
await clientA.sync.syncAndWait();
|
|
console.log('[3-Client Test] Client A synced');
|
|
|
|
// ============ PHASE 2: Client B Gets Task-1, Creates Task-2 ============
|
|
console.log('[3-Client Test] Phase 2: Client B syncs and creates Task-2');
|
|
clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
|
|
await clientB.sync.setupSuperSync(syncConfig);
|
|
await clientB.sync.syncAndWait();
|
|
|
|
// Verify B has Task-1
|
|
await waitForTask(clientB.page, task1Name);
|
|
console.log('[3-Client Test] Client B received Task-1');
|
|
|
|
// B creates Task-2
|
|
await clientB.workView.addTask(task2Name);
|
|
console.log(`[3-Client Test] Client B created: ${task2Name}`);
|
|
|
|
// ============ PHASE 3: Client C Gets Task-1, Creates Task-3, Renames Task-1 ============
|
|
console.log(
|
|
'[3-Client Test] Phase 3: Client C syncs, creates Task-3, renames Task-1',
|
|
);
|
|
clientC = await createSimulatedClient(browser, baseURL!, 'C', testRunId);
|
|
await clientC.sync.setupSuperSync(syncConfig);
|
|
await clientC.sync.syncAndWait();
|
|
|
|
// Verify C has Task-1 (but NOT Task-2 yet - B hasn't synced)
|
|
await waitForTask(clientC.page, task1Name);
|
|
console.log('[3-Client Test] Client C received Task-1');
|
|
|
|
// C creates Task-3
|
|
await clientC.workView.addTask(task3Name);
|
|
console.log(`[3-Client Test] Client C created: ${task3Name}`);
|
|
|
|
// C renames Task-1
|
|
await renameTask(clientC, task1Name, task1Renamed);
|
|
console.log(`[3-Client Test] Client C renamed Task-1 to: ${task1Renamed}`);
|
|
|
|
// ============ PHASE 4: Client B Syncs (Uploads Task-2) ============
|
|
console.log('[3-Client Test] Phase 4: Client B syncs (uploads Task-2)');
|
|
await clientB.sync.syncAndWait();
|
|
console.log('[3-Client Test] Client B synced');
|
|
|
|
// ============ PHASE 5: Client C Syncs (Uploads Task-3 + Rename, Downloads Task-2) ============
|
|
console.log('[3-Client Test] Phase 5: Client C syncs');
|
|
await clientC.sync.syncAndWait();
|
|
|
|
// Wait for DOM to settle after sync
|
|
await clientC.page.waitForLoadState('domcontentloaded');
|
|
|
|
// Verify C now has Task-2
|
|
await waitForTask(clientC.page, task2Name);
|
|
console.log('[3-Client Test] Client C received Task-2');
|
|
|
|
// ============ PHASE 6: Client A Syncs (Downloads Task-2, Task-3, Rename) ============
|
|
console.log('[3-Client Test] Phase 6: Client A syncs');
|
|
await clientA.sync.syncAndWait();
|
|
|
|
// Wait for DOM to settle after sync
|
|
await clientA.page.waitForLoadState('domcontentloaded');
|
|
|
|
// Verify A has all tasks with correct state
|
|
await waitForTask(clientA.page, task1Renamed);
|
|
await waitForTask(clientA.page, task2Name);
|
|
await waitForTask(clientA.page, task3Name);
|
|
console.log('[3-Client Test] Client A received all updates');
|
|
|
|
// ============ PHASE 7: Client B Syncs (Downloads Task-3, Rename) ============
|
|
console.log('[3-Client Test] Phase 7: Client B syncs');
|
|
await clientB.sync.syncAndWait();
|
|
|
|
// Wait for DOM to settle after sync
|
|
await clientB.page.waitForLoadState('domcontentloaded');
|
|
await clientB.page.waitForTimeout(500);
|
|
|
|
// Verify B has all tasks with correct state
|
|
await waitForTask(clientB.page, task1Renamed);
|
|
await waitForTask(clientB.page, task3Name);
|
|
console.log('[3-Client Test] Client B received all updates');
|
|
|
|
// ============ PHASE 8: Final Verification - All Clients Identical ============
|
|
console.log('[3-Client Test] Phase 8: Verifying eventual consistency');
|
|
|
|
const countA = await getTaskCount(clientA);
|
|
const countB = await getTaskCount(clientB);
|
|
const countC = await getTaskCount(clientC);
|
|
|
|
console.log(`[3-Client Test] Client A task count: ${countA}`);
|
|
console.log(`[3-Client Test] Client B task count: ${countB}`);
|
|
console.log(`[3-Client Test] Client C task count: ${countC}`);
|
|
|
|
// All clients should have exactly 3 tasks
|
|
await expectTaskCount(clientA, 3);
|
|
await expectTaskCount(clientB, 3);
|
|
await expectTaskCount(clientC, 3);
|
|
|
|
// Verify renamed Task-1 exists on all clients
|
|
expect(await hasTaskOnClient(clientA, task1Renamed)).toBe(true);
|
|
expect(await hasTaskOnClient(clientB, task1Renamed)).toBe(true);
|
|
expect(await hasTaskOnClient(clientC, task1Renamed)).toBe(true);
|
|
console.log('[3-Client Test] Task-1 (renamed) exists on all clients');
|
|
|
|
// Verify Task-2 exists on all clients
|
|
expect(await hasTaskOnClient(clientA, task2Name)).toBe(true);
|
|
expect(await hasTaskOnClient(clientB, task2Name)).toBe(true);
|
|
expect(await hasTaskOnClient(clientC, task2Name)).toBe(true);
|
|
console.log('[3-Client Test] Task-2 exists on all clients');
|
|
|
|
// Verify Task-3 exists on all clients
|
|
expect(await hasTaskOnClient(clientA, task3Name)).toBe(true);
|
|
expect(await hasTaskOnClient(clientB, task3Name)).toBe(true);
|
|
expect(await hasTaskOnClient(clientC, task3Name)).toBe(true);
|
|
console.log('[3-Client Test] Task-3 exists on all clients');
|
|
|
|
// Verify original Task-1 name no longer exists (it was renamed)
|
|
// Skip checking on C since C did the rename and might still have the old element in DOM
|
|
expect(await hasTaskOnClient(clientA, task1Name)).toBe(false);
|
|
expect(await hasTaskOnClient(clientB, task1Name)).toBe(false);
|
|
|
|
console.log('[3-Client Test] ✓ All three clients have identical state!');
|
|
console.log('[3-Client Test] ✓ Eventual consistency achieved!');
|
|
} finally {
|
|
if (clientA) await closeClient(clientA);
|
|
if (clientB) await closeClient(clientB);
|
|
if (clientC) await closeClient(clientC);
|
|
}
|
|
});
|
|
});
|