mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-19 17:36:25 +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.
230 lines
8.9 KiB
TypeScript
230 lines
8.9 KiB
TypeScript
import { test, expect } from '../../fixtures/supersync.fixture';
|
|
import {
|
|
createTestUser,
|
|
getSuperSyncConfig,
|
|
createSimulatedClient,
|
|
closeClient,
|
|
waitForTask,
|
|
type SimulatedE2EClient,
|
|
} from '../../utils/supersync-helpers';
|
|
|
|
/**
|
|
* SuperSync Archive Data Sync E2E Tests - Server Migration Scenario
|
|
*
|
|
* This test specifically verifies the bug fix in ServerMigrationService.handleServerMigration()
|
|
* which was using getStateSnapshot() (returns empty archives) instead of
|
|
* getStateSnapshotAsync() (loads real archives from IndexedDB).
|
|
*
|
|
* The bug caused archived tasks to be lost when:
|
|
* 1. A client switches sync providers (triggers server migration)
|
|
* 2. Another client syncs from the new provider
|
|
* 3. The SYNC_IMPORT operation had empty archives
|
|
*
|
|
* This test triggers server migration by having a client switch sync providers.
|
|
*
|
|
* 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-archive-data-sync.spec.ts
|
|
*/
|
|
|
|
/**
|
|
* Helper to mark a task as done using keyboard shortcut
|
|
*/
|
|
const markTaskDone = async (
|
|
page: SimulatedE2EClient['page'],
|
|
taskName: string,
|
|
): Promise<void> => {
|
|
const task = page.locator(`task:not(.ng-animating):has-text("${taskName}")`).first();
|
|
await task.waitFor({ state: 'visible', timeout: 10000 });
|
|
|
|
await task.focus();
|
|
await page.waitForTimeout(100);
|
|
await task.press('d');
|
|
|
|
await expect(task).toHaveClass(/isDone/, { timeout: 10000 });
|
|
};
|
|
|
|
/**
|
|
* Helper to archive done tasks via Daily Summary
|
|
*/
|
|
const archiveDoneTasks = async (page: SimulatedE2EClient['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/);
|
|
|
|
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)/);
|
|
};
|
|
|
|
/**
|
|
* Helper to check worklog for archived task entries
|
|
*/
|
|
const checkWorklogForArchivedTasks = async (
|
|
page: SimulatedE2EClient['page'],
|
|
): Promise<number> => {
|
|
await page.goto('/#/tag/TODAY/history');
|
|
await page.waitForLoadState('networkidle');
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Try to expand week rows to see task entries
|
|
const weekRows = page.locator('.week-row');
|
|
const weekCount = await weekRows.count();
|
|
for (let i = 0; i < Math.min(weekCount, 3); i++) {
|
|
const row = weekRows.nth(i);
|
|
if (await row.isVisible()) {
|
|
await row.click().catch(() => {});
|
|
await page.waitForTimeout(300);
|
|
}
|
|
}
|
|
|
|
// Count task entries in worklog
|
|
const taskEntries = await page
|
|
.locator('.task-summary-table .task-title, .worklog-task, worklog-task')
|
|
.count()
|
|
.catch(() => 0);
|
|
|
|
return taskEntries;
|
|
};
|
|
|
|
test.describe('@supersync Server Migration Archive Sync', () => {
|
|
/**
|
|
* Scenario: Server migration should include archive data in SYNC_IMPORT
|
|
*
|
|
* This test triggers the exact code path that had the bug:
|
|
* ServerMigrationService.handleServerMigration() creates a SYNC_IMPORT
|
|
* when a client with existing synced data connects to a new/empty server.
|
|
*
|
|
* Steps:
|
|
* 1. Client A connects to Provider 1 (user1)
|
|
* 2. Client A creates a task, marks it done, and archives it
|
|
* 3. Client A syncs to Provider 1
|
|
* 4. Client A switches to Provider 2 (user2) - TRIGGERS SERVER MIGRATION
|
|
* - lastServerSeq is 0 for new provider
|
|
* - Server (user2) is empty
|
|
* - Client A has previously synced ops (from user1)
|
|
* - handleServerMigration() creates SYNC_IMPORT with full state
|
|
* 5. Client A syncs to Provider 2 (uploads SYNC_IMPORT)
|
|
* 6. Client B connects to Provider 2 and syncs
|
|
* 7. Verify Client B has the archived task in worklog
|
|
*
|
|
* Before the fix: SYNC_IMPORT had empty archives (DEFAULT_ARCHIVE)
|
|
* After the fix: SYNC_IMPORT includes real archives from IndexedDB
|
|
*/
|
|
test('Server migration SYNC_IMPORT should include archived tasks', async ({
|
|
browser,
|
|
baseURL,
|
|
testRunId,
|
|
}) => {
|
|
let clientA: SimulatedE2EClient | null = null;
|
|
let clientB: SimulatedE2EClient | null = null;
|
|
|
|
try {
|
|
// Create TWO different sync providers (users)
|
|
const user1 = await createTestUser(testRunId + '-provider1');
|
|
const user2 = await createTestUser(testRunId + '-provider2');
|
|
const syncConfig1 = getSuperSyncConfig(user1);
|
|
const syncConfig2 = getSuperSyncConfig(user2);
|
|
|
|
// ============ PHASE 1: Client A connects to Provider 1 ============
|
|
console.log('[Migration Archive] Phase 1: Client A connecting to Provider 1');
|
|
|
|
clientA = await createSimulatedClient(browser, baseURL!, 'A', testRunId);
|
|
await clientA.sync.setupSuperSync(syncConfig1);
|
|
await clientA.sync.syncAndWait();
|
|
console.log('[Migration Archive] Client A connected to Provider 1');
|
|
|
|
// ============ PHASE 2: Client A creates and archives a task ============
|
|
console.log('[Migration Archive] Phase 2: Creating and archiving task');
|
|
|
|
const archivedTaskName = `MigrationArchiveTest-${testRunId}`;
|
|
await clientA.workView.addTask(archivedTaskName);
|
|
await waitForTask(clientA.page, archivedTaskName);
|
|
console.log(`[Migration Archive] Created task: ${archivedTaskName}`);
|
|
|
|
// Mark task as done
|
|
await markTaskDone(clientA.page, archivedTaskName);
|
|
console.log('[Migration Archive] Marked task as done');
|
|
|
|
// Archive the task via Daily Summary
|
|
await archiveDoneTasks(clientA.page);
|
|
console.log('[Migration Archive] Archived task');
|
|
|
|
// Verify task is no longer in work view (archived)
|
|
const taskInWorkView = clientA.page.locator(`task:has-text("${archivedTaskName}")`);
|
|
await expect(taskInWorkView).not.toBeVisible({ timeout: 5000 });
|
|
console.log('[Migration Archive] Verified task is archived (not in work view)');
|
|
|
|
// ============ PHASE 3: Client A syncs to Provider 1 ============
|
|
console.log('[Migration Archive] Phase 3: Syncing to Provider 1');
|
|
|
|
await clientA.sync.syncAndWait();
|
|
console.log('[Migration Archive] Client A synced to Provider 1');
|
|
|
|
// Verify archived task appears in worklog on Client A
|
|
const worklogCountA = await checkWorklogForArchivedTasks(clientA.page);
|
|
console.log(`[Migration Archive] Client A worklog entries: ${worklogCountA}`);
|
|
expect(worklogCountA).toBeGreaterThan(0);
|
|
|
|
// ============ PHASE 4: Client A switches to Provider 2 (TRIGGERS MIGRATION) ============
|
|
console.log(
|
|
'[Migration Archive] Phase 4: Switching to Provider 2 (server migration)',
|
|
);
|
|
|
|
// This is the KEY step that triggers handleServerMigration():
|
|
// - Client A has previously synced ops (from Provider 1)
|
|
// - Provider 2 is empty (lastServerSeq = 0)
|
|
// - handleServerMigration() creates SYNC_IMPORT with full state
|
|
await clientA.sync.setupSuperSync(syncConfig2);
|
|
console.log('[Migration Archive] Client A configured for Provider 2');
|
|
|
|
// Sync to Provider 2 - this uploads the SYNC_IMPORT
|
|
await clientA.sync.syncAndWait();
|
|
console.log(
|
|
'[Migration Archive] Client A synced to Provider 2 (SYNC_IMPORT uploaded)',
|
|
);
|
|
|
|
// ============ PHASE 5: Client B connects to Provider 2 and syncs ============
|
|
console.log('[Migration Archive] Phase 5: Client B joining Provider 2');
|
|
|
|
clientB = await createSimulatedClient(browser, baseURL!, 'B', testRunId);
|
|
await clientB.sync.setupSuperSync(syncConfig2);
|
|
await clientB.sync.syncAndWait();
|
|
console.log('[Migration Archive] Client B synced from Provider 2');
|
|
|
|
// Wait for state to settle
|
|
await clientB.page.waitForTimeout(2000);
|
|
|
|
// ============ PHASE 6: Verify Client B has archived task in worklog ============
|
|
console.log('[Migration Archive] Phase 6: Verifying Client B has archived task');
|
|
|
|
// Check worklog on Client B for archived tasks
|
|
const worklogCountB = await checkWorklogForArchivedTasks(clientB.page);
|
|
console.log(`[Migration Archive] Client B worklog entries: ${worklogCountB}`);
|
|
|
|
// CRITICAL ASSERTION: Client B should have archived task entries
|
|
// Before the fix: This would be 0 because SYNC_IMPORT had empty archives
|
|
// After the fix: This should be > 0 because SYNC_IMPORT includes real archives
|
|
expect(worklogCountB).toBeGreaterThan(
|
|
0,
|
|
'Client B should have archived task entries from server migration SYNC_IMPORT',
|
|
);
|
|
|
|
console.log('[Migration Archive] ✓ Server migration archive test PASSED!');
|
|
console.log(
|
|
'[Migration Archive] Archives were correctly included in SYNC_IMPORT during server migration',
|
|
);
|
|
} finally {
|
|
if (clientA) await closeClient(clientA);
|
|
if (clientB) await closeClient(clientB);
|
|
}
|
|
});
|
|
});
|