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.
343 lines
12 KiB
TypeScript
343 lines
12 KiB
TypeScript
import { test, expect } from '../../fixtures/supersync.fixture';
|
|
import {
|
|
createTestUser,
|
|
getSuperSyncConfig,
|
|
createSimulatedClient,
|
|
closeClient,
|
|
markTaskDone,
|
|
type SimulatedE2EClient,
|
|
} from '../../utils/supersync-helpers';
|
|
import { waitForAppReady } from '../../utils/waits';
|
|
|
|
/**
|
|
* SuperSync Worklog/Archive E2E Tests
|
|
*
|
|
* Tests worklog and archive query scenarios:
|
|
* - Archive tasks, query on other client
|
|
* - Worklog data structure sync
|
|
* - Multiple archives across clients
|
|
*/
|
|
|
|
test.describe('@supersync Worklog Sync', () => {
|
|
/**
|
|
* Test: Archived tasks appear in worklog on synced client
|
|
*
|
|
* Actions:
|
|
* 1. Client A creates and completes tasks
|
|
* 2. Client A archives via Finish Day
|
|
* 3. Client A syncs
|
|
* 4. Client B syncs
|
|
* 5. Client B verifies tasks in worklog
|
|
*/
|
|
test('Archived tasks appear in worklog on synced client', 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: Client A Creates and Completes Tasks ============
|
|
clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
|
|
await clientA.sync.setupSuperSync(syncConfig);
|
|
|
|
const task1Name = `Worklog1-${uniqueId}`;
|
|
const task2Name = `Worklog2-${uniqueId}`;
|
|
|
|
await clientA.workView.addTask(task1Name);
|
|
await clientA.workView.addTask(task2Name);
|
|
console.log('[Worklog Test] Client A created 2 tasks');
|
|
|
|
// Mark both as done
|
|
await markTaskDone(clientA, task1Name);
|
|
await markTaskDone(clientA, task2Name);
|
|
console.log('[Worklog Test] Client A marked tasks done');
|
|
|
|
// ============ PHASE 2: Client A Archives ============
|
|
const finishDayBtn = clientA.page.locator('.e2e-finish-day');
|
|
await finishDayBtn.waitFor({ state: 'visible', timeout: 10000 });
|
|
await finishDayBtn.click();
|
|
|
|
await clientA.page.waitForURL(/daily-summary/, { timeout: 10000 });
|
|
await clientA.page.waitForLoadState('networkidle');
|
|
|
|
const saveBtn = clientA.page.locator(
|
|
'daily-summary button[mat-flat-button]:has(mat-icon:has-text("wb_sunny"))',
|
|
);
|
|
await saveBtn.waitFor({ state: 'visible', timeout: 10000 });
|
|
await saveBtn.click();
|
|
|
|
await clientA.page.waitForURL(/tag\/TODAY/, { timeout: 10000 });
|
|
console.log('[Worklog Test] Client A archived tasks');
|
|
|
|
// ============ PHASE 3: Sync ============
|
|
await clientA.sync.syncAndWait();
|
|
console.log('[Worklog Test] Client A synced');
|
|
|
|
// ============ PHASE 4: Client B Downloads and Checks Worklog ============
|
|
clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
|
|
await clientB.sync.setupSuperSync(syncConfig);
|
|
await clientB.sync.syncAndWait();
|
|
console.log('[Worklog Test] Client B synced');
|
|
|
|
// Navigate to worklog
|
|
await clientB.page.goto('/#/tag/TODAY/history');
|
|
await clientB.page.waitForLoadState('networkidle');
|
|
await clientB.page.waitForSelector('history', { timeout: 10000 });
|
|
console.log('[Worklog Test] Client B navigated to worklog');
|
|
|
|
// Expand week to see tasks
|
|
const weekRow = clientB.page.locator('.week-row').first();
|
|
if (await weekRow.isVisible()) {
|
|
await weekRow.click();
|
|
await clientB.page.waitForTimeout(500);
|
|
}
|
|
|
|
// Verify both tasks appear in worklog
|
|
const task1InWorklog = clientB.page.locator(
|
|
`.task-summary-table .task-title:has-text("${task1Name}")`,
|
|
);
|
|
const task2InWorklog = clientB.page.locator(
|
|
`.task-summary-table .task-title:has-text("${task2Name}")`,
|
|
);
|
|
|
|
await expect(task1InWorklog).toBeVisible({ timeout: 10000 });
|
|
await expect(task2InWorklog).toBeVisible({ timeout: 10000 });
|
|
console.log('[Worklog Test] Both tasks visible in worklog on Client B');
|
|
} finally {
|
|
if (clientA) await closeClient(clientA);
|
|
if (clientB) await closeClient(clientB);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Test: Worklog shows correct time data after sync
|
|
*
|
|
* Actions:
|
|
* 1. Client A creates task, tracks time, completes
|
|
* 2. Client A archives
|
|
* 3. Client B syncs and checks worklog for time data
|
|
*/
|
|
test('Worklog shows correct time data after sync', 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: Client A Creates, Tracks, Completes ============
|
|
clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
|
|
await clientA.sync.setupSuperSync(syncConfig);
|
|
|
|
const taskName = `TimedWorklog-${uniqueId}`;
|
|
await clientA.workView.addTask(taskName);
|
|
|
|
// Track time using helper functions (need to add imports)
|
|
const task = clientA.page.locator(`task:has-text("${taskName}")`);
|
|
await task.hover();
|
|
await task.locator('.start-task-btn').click();
|
|
await clientA.page.waitForTimeout(3000);
|
|
await task.hover();
|
|
const pauseBtn = task.locator('button:has(mat-icon:has-text("pause"))');
|
|
await pauseBtn.click();
|
|
console.log('[Timed Worklog Test] Tracked 3 seconds');
|
|
|
|
// Complete task
|
|
await markTaskDone(clientA, taskName);
|
|
|
|
// ============ PHASE 2: Archive ============
|
|
const finishDayBtn = clientA.page.locator('.e2e-finish-day');
|
|
await finishDayBtn.waitFor({ state: 'visible', timeout: 10000 });
|
|
await finishDayBtn.click();
|
|
|
|
await clientA.page.waitForURL(/daily-summary/, { timeout: 10000 });
|
|
|
|
const saveBtn = clientA.page.locator(
|
|
'daily-summary button[mat-flat-button]:has(mat-icon:has-text("wb_sunny"))',
|
|
);
|
|
await saveBtn.waitFor({ state: 'visible', timeout: 10000 });
|
|
await saveBtn.click();
|
|
|
|
await clientA.page.waitForURL(/tag\/TODAY/, { timeout: 10000 });
|
|
|
|
// ============ PHASE 3: Sync ============
|
|
await clientA.sync.syncAndWait();
|
|
|
|
// ============ PHASE 4: Client B Checks Worklog ============
|
|
clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
|
|
await clientB.sync.setupSuperSync(syncConfig);
|
|
await clientB.sync.syncAndWait();
|
|
|
|
await clientB.page.goto('/#/tag/TODAY/history');
|
|
await clientB.page.waitForLoadState('networkidle');
|
|
await clientB.page.waitForSelector('history', { timeout: 10000 });
|
|
|
|
// Expand week
|
|
const weekRow = clientB.page.locator('.week-row').first();
|
|
if (await weekRow.isVisible()) {
|
|
await weekRow.click();
|
|
await clientB.page.waitForTimeout(500);
|
|
}
|
|
|
|
// Verify task appears in worklog
|
|
const taskInWorklog = clientB.page.locator(`text=${taskName}`);
|
|
await expect(taskInWorklog).toBeVisible({ timeout: 10000 });
|
|
|
|
// The task with tracked time should appear in the worklog
|
|
// Time data is stored but exact UI format varies
|
|
console.log('[Timed Worklog Test] Task with time data visible in worklog');
|
|
} finally {
|
|
if (clientA) await closeClient(clientA);
|
|
if (clientB) await closeClient(clientB);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Test: Multiple archive operations sync correctly
|
|
*
|
|
* Actions:
|
|
* 1. Client A creates and archives tasks day 1
|
|
* 2. Client A syncs
|
|
* 3. Client B syncs
|
|
* 4. Client B creates and archives more tasks
|
|
* 5. Both sync
|
|
* 6. Verify all archived tasks in worklog on both
|
|
*/
|
|
test('Multiple archive operations sync 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: Client A Archives First Batch ============
|
|
clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
|
|
await clientA.sync.setupSuperSync(syncConfig);
|
|
|
|
const taskA1 = `BatchA1-${uniqueId}`;
|
|
await clientA.workView.addTask(taskA1);
|
|
|
|
await markTaskDone(clientA, taskA1);
|
|
|
|
// Archive
|
|
const finishDayBtnA = clientA.page.locator('.e2e-finish-day');
|
|
await finishDayBtnA.click();
|
|
await clientA.page.waitForURL(/daily-summary/, { timeout: 10000 });
|
|
|
|
const saveBtnA = clientA.page.locator(
|
|
'daily-summary button[mat-flat-button]:has(mat-icon:has-text("wb_sunny"))',
|
|
);
|
|
await saveBtnA.click();
|
|
await clientA.page.waitForURL(/tag\/TODAY/, { timeout: 10000 });
|
|
console.log('[Multi Archive Test] Client A archived first batch');
|
|
|
|
await clientA.sync.syncAndWait();
|
|
|
|
// ============ PHASE 2: Client B Archives Second Batch ============
|
|
clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
|
|
await clientB.sync.setupSuperSync(syncConfig);
|
|
await clientB.sync.syncAndWait();
|
|
|
|
const taskB1 = `BatchB1-${uniqueId}`;
|
|
await clientB.workView.addTask(taskB1);
|
|
|
|
await markTaskDone(clientB, taskB1);
|
|
|
|
// Archive
|
|
const finishDayBtnB = clientB.page.locator('.e2e-finish-day');
|
|
await finishDayBtnB.click();
|
|
await clientB.page.waitForURL(/daily-summary/, { timeout: 10000 });
|
|
|
|
const saveBtnB = clientB.page.locator(
|
|
'daily-summary button[mat-flat-button]:has(mat-icon:has-text("wb_sunny"))',
|
|
);
|
|
await saveBtnB.click();
|
|
await clientB.page.waitForURL(/tag\/TODAY/, { timeout: 10000 });
|
|
console.log('[Multi Archive Test] Client B archived second batch');
|
|
|
|
// ============ PHASE 3: Sync Both ============
|
|
await clientB.sync.syncAndWait();
|
|
await clientA.sync.syncAndWait();
|
|
console.log('[Multi Archive Test] Both synced');
|
|
|
|
// Reload both clients to ensure archive data from sync is loaded
|
|
// Archive data is stored in a separate IndexedDB store and may need reload to appear in UI
|
|
await clientA.page.goto(clientA.page.url(), {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: 30000,
|
|
});
|
|
await waitForAppReady(clientA.page);
|
|
await clientB.page.goto(clientB.page.url(), {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: 30000,
|
|
});
|
|
await waitForAppReady(clientB.page);
|
|
console.log('[Multi Archive Test] Both clients reloaded after sync');
|
|
|
|
// ============ PHASE 4: Verify All Archives in Worklog ============
|
|
// Check Client A worklog
|
|
await clientA.page.goto('/#/tag/TODAY/history');
|
|
await clientA.page.waitForLoadState('networkidle');
|
|
await clientA.page.waitForSelector('history', { timeout: 10000 });
|
|
|
|
const weekRowA = clientA.page.locator('.week-row').first();
|
|
if (await weekRowA.isVisible()) {
|
|
await weekRowA.click();
|
|
await clientA.page.waitForTimeout(500);
|
|
}
|
|
|
|
const taskA1InWorklog = clientA.page.locator(
|
|
`.task-summary-table .task-title:has-text("${taskA1}")`,
|
|
);
|
|
const taskB1InWorklogA = clientA.page.locator(
|
|
`.task-summary-table .task-title:has-text("${taskB1}")`,
|
|
);
|
|
|
|
await expect(taskA1InWorklog).toBeVisible({ timeout: 10000 });
|
|
await expect(taskB1InWorklogA).toBeVisible({ timeout: 10000 });
|
|
console.log('[Multi Archive Test] Client A has both archives in worklog');
|
|
|
|
// Check Client B worklog
|
|
await clientB.page.goto('/#/tag/TODAY/history');
|
|
await clientB.page.waitForLoadState('networkidle');
|
|
await clientB.page.waitForSelector('history', { timeout: 10000 });
|
|
|
|
const weekRowB = clientB.page.locator('.week-row').first();
|
|
if (await weekRowB.isVisible()) {
|
|
await weekRowB.click();
|
|
await clientB.page.waitForTimeout(500);
|
|
}
|
|
|
|
const taskA1InWorklogB = clientB.page.locator(
|
|
`.task-summary-table .task-title:has-text("${taskA1}")`,
|
|
);
|
|
const taskB1InWorklog = clientB.page.locator(
|
|
`.task-summary-table .task-title:has-text("${taskB1}")`,
|
|
);
|
|
|
|
await expect(taskA1InWorklogB).toBeVisible({ timeout: 10000 });
|
|
await expect(taskB1InWorklog).toBeVisible({ timeout: 10000 });
|
|
console.log('[Multi Archive Test] Client B has both archives in worklog');
|
|
} finally {
|
|
if (clientA) await closeClient(clientA);
|
|
if (clientB) await closeClient(clientB);
|
|
}
|
|
});
|
|
});
|