mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
test(archive): add failing tests for legacy import subtask loss
Add E2E and integration tests documenting a bug where tasks with subtasks are lost when: 1. Importing legacy data (pre-operation logs) 2. Archiving via "Finish Day" 3. Exporting again The archiveYoung ends up empty in the export. Tests will pass once the bug is fixed. Files added: - e2e/fixtures/legacy-archive-subtasks-backup.json - e2e/tests/import-export/legacy-archive-subtasks.spec.ts - integration/legacy-archive-subtasks.integration.spec.ts
This commit is contained in:
parent
42b0f4ea1a
commit
6191c782f0
3 changed files with 1101 additions and 0 deletions
332
e2e/fixtures/legacy-archive-subtasks-backup.json
Normal file
332
e2e/fixtures/legacy-archive-subtasks-backup.json
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
{
|
||||
"timestamp": 1733000000000,
|
||||
"lastUpdate": 1733000000000,
|
||||
"crossModelVersion": 4.5,
|
||||
"data": {
|
||||
"project": {
|
||||
"ids": ["INBOX_PROJECT"],
|
||||
"entities": {
|
||||
"INBOX_PROJECT": {
|
||||
"id": "INBOX_PROJECT",
|
||||
"title": "Inbox",
|
||||
"isHiddenFromMenu": false,
|
||||
"taskIds": ["parent-task-1", "subtask-1", "subtask-2", "normal-task-1"],
|
||||
"backlogTaskIds": [],
|
||||
"noteIds": [],
|
||||
"theme": {
|
||||
"primary": "#607d8b",
|
||||
"accent": "#ff4081",
|
||||
"warn": "#e91e63",
|
||||
"isAutoContrast": true
|
||||
},
|
||||
"icon": "inbox",
|
||||
"workStart": {},
|
||||
"workEnd": {},
|
||||
"breakNr": {},
|
||||
"breakTime": {},
|
||||
"isArchived": false,
|
||||
"advancedCfg": {
|
||||
"worklogExportSettings": {
|
||||
"cols": ["DATE", "START", "END", "TIME_CLOCK", "TITLES_INCLUDING_SUB"],
|
||||
"roundWorkTimeTo": null,
|
||||
"roundStartTimeTo": null,
|
||||
"roundEndTimeTo": null,
|
||||
"groupBy": "DATE",
|
||||
"separateTasksBy": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"menuTree": {
|
||||
"tagTree": [],
|
||||
"projectTree": []
|
||||
},
|
||||
"globalConfig": {
|
||||
"misc": {
|
||||
"isConfirmBeforeExit": false,
|
||||
"isConfirmBeforeExitWithoutFinishDay": true,
|
||||
"isNotifyWhenTimeEstimateExceeded": false,
|
||||
"isAutMarkParentAsDone": true,
|
||||
"isAutoStartNextTask": true,
|
||||
"isTurnOffMarkdown": false,
|
||||
"isAutoAddWorkedOnToToday": true,
|
||||
"isDisableInitialDialog": true,
|
||||
"firstDayOfWeek": 1,
|
||||
"startOfNextDay": 0,
|
||||
"daysToShowForPastDates": 14,
|
||||
"isMinimizeToTray": false,
|
||||
"isEnableAdvanced": false,
|
||||
"isTrayShowCurrentTask": true,
|
||||
"taskNotesTpl": "",
|
||||
"isDisableAnimations": false
|
||||
},
|
||||
"appFeatures": {
|
||||
"isTimeTrackingEnabled": true,
|
||||
"isFocusModeEnabled": true,
|
||||
"isSchedulerEnabled": true,
|
||||
"isPlannerEnabled": true,
|
||||
"isBoardsEnabled": true,
|
||||
"isScheduleDayPanelEnabled": true,
|
||||
"isIssuesPanelEnabled": true,
|
||||
"isProjectNotesEnabled": true,
|
||||
"isSyncIconEnabled": true,
|
||||
"isDonatePageEnabled": true,
|
||||
"isEnableUserProfiles": false
|
||||
},
|
||||
"localization": {},
|
||||
"evaluation": {
|
||||
"isHideEvaluationSheet": false
|
||||
},
|
||||
"idle": {
|
||||
"isEnableIdleTimeTracking": false,
|
||||
"isUnTrackedIdleResetsBreakTimer": false,
|
||||
"minIdleTime": 60000,
|
||||
"isOnlyOpenIdleWhenCurrentTask": false
|
||||
},
|
||||
"takeABreak": {
|
||||
"isTakeABreakEnabled": false,
|
||||
"takeABreakMinWorkingTime": 60,
|
||||
"takeABreakSnoozeTime": 15,
|
||||
"motivationalImgs": [],
|
||||
"isLockScreen": false,
|
||||
"isTimedFullScreenBlocker": false,
|
||||
"timedFullScreenBlockerDuration": 8000,
|
||||
"isFocusWindow": false,
|
||||
"takeABreakMessage": "Take a break!"
|
||||
},
|
||||
"pomodoro": {
|
||||
"isEnabled": false,
|
||||
"duration": 1500000,
|
||||
"breakDuration": 300000,
|
||||
"longerBreakDuration": 900000,
|
||||
"cyclesBeforeLongerBreak": 4,
|
||||
"isStopTrackingOnBreak": true,
|
||||
"isStopTrackingOnLongBreak": true,
|
||||
"isManualContinue": false,
|
||||
"isPlaySound": true,
|
||||
"isPlaySoundAfterBreak": false,
|
||||
"isEnabled2": false
|
||||
},
|
||||
"keyboard": {},
|
||||
"localBackup": {
|
||||
"isEnabled": false
|
||||
},
|
||||
"lang": {},
|
||||
"sync": {
|
||||
"isEnabled": false,
|
||||
"isCompressionEnabled": true,
|
||||
"syncInterval": 0,
|
||||
"isEncryptionEnabled": false,
|
||||
"syncProvider": null
|
||||
},
|
||||
"timeline": {
|
||||
"isWorkStartEndEnabled": false,
|
||||
"workStart": "9:00",
|
||||
"workEnd": "17:00"
|
||||
},
|
||||
"reminder": {
|
||||
"isCountdownBannerEnabled": true,
|
||||
"countdownDuration": 600000
|
||||
},
|
||||
"focusMode": {
|
||||
"isAlwaysUseFocusMode": false,
|
||||
"isSkipPreparation": false
|
||||
},
|
||||
"sound": {
|
||||
"volume": 70,
|
||||
"isPlayDoneSound": true,
|
||||
"doneSound": "done2.mp3",
|
||||
"isPlayTickSound": false,
|
||||
"isIncreaseDoneSoundPitch": true,
|
||||
"breakReminderSound": null
|
||||
},
|
||||
"timeTracking": {
|
||||
"trackingInterval": 1000,
|
||||
"defaultEstimate": 0,
|
||||
"defaultEstimateSubTasks": 0,
|
||||
"isNotifyWhenTimeEstimateExceeded": true,
|
||||
"isAutoStartNextTask": false,
|
||||
"isTrackingReminderEnabled": false,
|
||||
"isTrackingReminderShowOnMobile": false,
|
||||
"trackingReminderMinTime": 300000
|
||||
},
|
||||
"quickNotes": {},
|
||||
"dominaMode": {
|
||||
"isEnabled": false,
|
||||
"text": "Your current task is: ${currentTaskTitle}",
|
||||
"interval": 300000,
|
||||
"volume": 75
|
||||
},
|
||||
"shortSyntax": {
|
||||
"isEnableProject": true,
|
||||
"isEnableDue": true,
|
||||
"isEnableTag": true
|
||||
},
|
||||
"schedule": {
|
||||
"isWorkStartEndEnabled": true,
|
||||
"workStart": "9:00",
|
||||
"workEnd": "17:00",
|
||||
"isLunchBreakEnabled": false,
|
||||
"lunchBreakStart": "13:00",
|
||||
"lunchBreakEnd": "14:00"
|
||||
}
|
||||
},
|
||||
"task": {
|
||||
"ids": ["parent-task-1", "subtask-1", "subtask-2", "normal-task-1"],
|
||||
"entities": {
|
||||
"parent-task-1": {
|
||||
"id": "parent-task-1",
|
||||
"title": "Legacy Test - Parent Task With Subtasks",
|
||||
"subTaskIds": ["subtask-1", "subtask-2"],
|
||||
"timeSpentOnDay": {},
|
||||
"timeSpent": 0,
|
||||
"timeEstimate": 7200000,
|
||||
"isDone": false,
|
||||
"dueDay": "2024-12-01",
|
||||
"notes": "This is a parent task with two subtasks",
|
||||
"tagIds": [],
|
||||
"created": 1733000000000,
|
||||
"attachments": [],
|
||||
"projectId": "INBOX_PROJECT"
|
||||
},
|
||||
"subtask-1": {
|
||||
"id": "subtask-1",
|
||||
"title": "Legacy Test - First Subtask",
|
||||
"subTaskIds": [],
|
||||
"parentId": "parent-task-1",
|
||||
"timeSpentOnDay": {},
|
||||
"timeSpent": 0,
|
||||
"timeEstimate": 3600000,
|
||||
"isDone": false,
|
||||
"notes": "First subtask of the parent",
|
||||
"tagIds": [],
|
||||
"created": 1733000000001,
|
||||
"attachments": [],
|
||||
"projectId": "INBOX_PROJECT"
|
||||
},
|
||||
"subtask-2": {
|
||||
"id": "subtask-2",
|
||||
"title": "Legacy Test - Second Subtask",
|
||||
"subTaskIds": [],
|
||||
"parentId": "parent-task-1",
|
||||
"timeSpentOnDay": {},
|
||||
"timeSpent": 0,
|
||||
"timeEstimate": 1800000,
|
||||
"isDone": false,
|
||||
"notes": "Second subtask of the parent",
|
||||
"tagIds": [],
|
||||
"created": 1733000000002,
|
||||
"attachments": [],
|
||||
"projectId": "INBOX_PROJECT"
|
||||
},
|
||||
"normal-task-1": {
|
||||
"id": "normal-task-1",
|
||||
"title": "Legacy Test - Normal Task Without Subtasks",
|
||||
"subTaskIds": [],
|
||||
"timeSpentOnDay": {},
|
||||
"timeSpent": 0,
|
||||
"timeEstimate": 3600000,
|
||||
"isDone": false,
|
||||
"dueDay": "2024-12-01",
|
||||
"notes": "A standalone task",
|
||||
"tagIds": [],
|
||||
"created": 1733000000003,
|
||||
"attachments": [],
|
||||
"projectId": "INBOX_PROJECT"
|
||||
}
|
||||
},
|
||||
"currentTaskId": null,
|
||||
"selectedTaskId": null,
|
||||
"lastCurrentTaskId": null,
|
||||
"isDataLoaded": false
|
||||
},
|
||||
"tag": {
|
||||
"ids": ["TODAY"],
|
||||
"entities": {
|
||||
"TODAY": {
|
||||
"id": "TODAY",
|
||||
"title": "Today",
|
||||
"taskIds": ["parent-task-1", "normal-task-1"],
|
||||
"icon": "wb_sunny",
|
||||
"created": 1733000000000,
|
||||
"theme": {
|
||||
"primary": "#ffa726",
|
||||
"accent": "#ff4081",
|
||||
"warn": "#e91e63",
|
||||
"isAutoContrast": true
|
||||
},
|
||||
"workStart": {},
|
||||
"workEnd": {},
|
||||
"breakNr": {},
|
||||
"breakTime": {},
|
||||
"advancedCfg": {
|
||||
"worklogExportSettings": {
|
||||
"cols": ["DATE", "START", "END", "TIME_CLOCK", "TITLES_INCLUDING_SUB"],
|
||||
"roundWorkTimeTo": null,
|
||||
"roundStartTimeTo": null,
|
||||
"roundEndTimeTo": null,
|
||||
"groupBy": "DATE",
|
||||
"separateTasksBy": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"simpleCounter": {
|
||||
"ids": [],
|
||||
"entities": {}
|
||||
},
|
||||
"taskRepeatCfg": {
|
||||
"ids": [],
|
||||
"entities": {}
|
||||
},
|
||||
"reminders": [],
|
||||
"note": {
|
||||
"ids": [],
|
||||
"entities": {},
|
||||
"todayOrder": []
|
||||
},
|
||||
"metric": {
|
||||
"ids": [],
|
||||
"entities": {}
|
||||
},
|
||||
"planner": {
|
||||
"days": {}
|
||||
},
|
||||
"issueProvider": {
|
||||
"ids": [],
|
||||
"entities": {}
|
||||
},
|
||||
"boards": {
|
||||
"boardCfgs": []
|
||||
},
|
||||
"timeTracking": {
|
||||
"project": {},
|
||||
"tag": {}
|
||||
},
|
||||
"archiveYoung": {
|
||||
"task": {
|
||||
"ids": [],
|
||||
"entities": {}
|
||||
},
|
||||
"timeTracking": {
|
||||
"project": {},
|
||||
"tag": {}
|
||||
},
|
||||
"lastTimeTrackingFlush": 0
|
||||
},
|
||||
"archiveOld": {
|
||||
"task": {
|
||||
"ids": [],
|
||||
"entities": {}
|
||||
},
|
||||
"timeTracking": {
|
||||
"project": {},
|
||||
"tag": {}
|
||||
}
|
||||
},
|
||||
"pluginMetadata": [],
|
||||
"pluginUserData": []
|
||||
}
|
||||
}
|
||||
324
e2e/tests/import-export/legacy-archive-subtasks.spec.ts
Normal file
324
e2e/tests/import-export/legacy-archive-subtasks.spec.ts
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
import { test, expect, type Page, type Download } from '@playwright/test';
|
||||
import { ImportPage } from '../../pages/import.page';
|
||||
import * as fs from 'fs';
|
||||
|
||||
/**
|
||||
* E2E Tests for Legacy Data Import/Export with Archive Subtasks
|
||||
*
|
||||
* BUG: When importing data from before operation logs (legacy data) that contains
|
||||
* tasks with subtasks, then archiving via "Finish Day", the subtasks are lost
|
||||
* when exporting again.
|
||||
*
|
||||
* Test Flow:
|
||||
* 1. Import legacy backup containing active parent task + subtasks + normal task
|
||||
* 2. Mark all tasks as done
|
||||
* 3. Click "Finish Day" to archive tasks
|
||||
* 4. Export the data
|
||||
* 5. Verify exported data contains all archived tasks including subtasks
|
||||
*
|
||||
* Run with: npm run e2e:file e2e/tests/import-export/legacy-archive-subtasks.spec.ts
|
||||
*/
|
||||
|
||||
// Selectors
|
||||
const TASK_SEL = 'task';
|
||||
const TASK_DONE_BTN = '.task-done-btn';
|
||||
const FINISH_DAY_BTN = '.e2e-finish-day';
|
||||
const SAVE_AND_GO_HOME_BTN = 'button[mat-flat-button][color="primary"]:last-of-type';
|
||||
const WELCOME_DIALOG_CLOSE_BTN =
|
||||
'button:has-text("No thanks"), button:has-text("Close Tour")';
|
||||
|
||||
/**
|
||||
* Helper to dismiss welcome tour dialog if present
|
||||
*/
|
||||
const dismissWelcomeDialog = async (page: Page): Promise<void> => {
|
||||
try {
|
||||
const closeBtn = page.locator(WELCOME_DIALOG_CLOSE_BTN).first();
|
||||
await closeBtn.waitFor({ state: 'visible', timeout: 3000 });
|
||||
await closeBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
} catch {
|
||||
// Dialog not present, ignore
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to trigger and capture download
|
||||
*/
|
||||
const captureDownload = async (page: Page): Promise<Download> => {
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
|
||||
// Click the export button (file_upload icon in file-imex)
|
||||
const exportBtn = page.locator(
|
||||
'file-imex button:has(mat-icon:has-text("file_upload"))',
|
||||
);
|
||||
await exportBtn.click();
|
||||
|
||||
return downloadPromise;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to read downloaded file content
|
||||
*/
|
||||
const readDownloadedFile = async (download: Download): Promise<string> => {
|
||||
const downloadPath = await download.path();
|
||||
if (!downloadPath) {
|
||||
throw new Error('Download path is null');
|
||||
}
|
||||
return fs.readFileSync(downloadPath, 'utf-8');
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to mark all visible tasks as done
|
||||
*/
|
||||
const markAllTasksDone = async (page: Page): Promise<void> => {
|
||||
// Wait for tasks to be visible
|
||||
await page.waitForSelector(TASK_SEL, { state: 'visible', timeout: 10000 });
|
||||
|
||||
// Keep marking the first task as done until no undone tasks remain
|
||||
let attempts = 0;
|
||||
const maxAttempts = 10;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
const undoneCount = await page.locator('task:not(.isDone)').count();
|
||||
if (undoneCount === 0) break;
|
||||
|
||||
// Hover and click done on first undone task
|
||||
const firstUndone = page.locator('task:not(.isDone)').first();
|
||||
await firstUndone.hover();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
const doneBtn = firstUndone.locator(TASK_DONE_BTN);
|
||||
if (await doneBtn.isVisible()) {
|
||||
await doneBtn.click();
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
attempts++;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to complete finish day flow
|
||||
*/
|
||||
const finishDay = async (page: Page): Promise<void> => {
|
||||
// Click Finish Day button
|
||||
await page.waitForSelector(FINISH_DAY_BTN, { state: 'visible', timeout: 10000 });
|
||||
await page.click(FINISH_DAY_BTN);
|
||||
|
||||
// Wait for daily summary page
|
||||
await page.waitForSelector('daily-summary', { state: 'visible', timeout: 10000 });
|
||||
|
||||
// Click Save and go home
|
||||
await page.waitForSelector(SAVE_AND_GO_HOME_BTN, { state: 'visible', timeout: 10000 });
|
||||
await page.click(SAVE_AND_GO_HOME_BTN);
|
||||
|
||||
// Wait for navigation back to work view
|
||||
await page.waitForSelector('task-list', { state: 'visible', timeout: 10000 });
|
||||
await page.waitForTimeout(1000);
|
||||
};
|
||||
|
||||
test.describe('@legacy-archive Legacy Archive Subtasks via Finish Day', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Start with fresh state
|
||||
await page.goto('/#/tag/TODAY/tasks');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
});
|
||||
|
||||
/**
|
||||
* Test: Import legacy data, finish day, and verify subtasks are archived
|
||||
*
|
||||
* BUG: This test documents the bug where subtasks are lost after finish day.
|
||||
* Expected: All 4 tasks (parent, 2 subtasks, normal) should be in archiveYoung
|
||||
* Actual (bug): Only parent and normal tasks are in archiveYoung, subtasks missing
|
||||
*/
|
||||
test('should preserve subtasks in archive after finish day', async ({ page }) => {
|
||||
test.setTimeout(120000); // Long test with multiple steps
|
||||
|
||||
const importPage = new ImportPage(page);
|
||||
|
||||
// Step 1: Import legacy backup with active tasks
|
||||
console.log('[Legacy Archive Test] Step 1: Importing legacy backup...');
|
||||
await importPage.navigateToImportPage();
|
||||
const backupPath = ImportPage.getFixturePath('legacy-archive-subtasks-backup.json');
|
||||
await importPage.importBackupFile(backupPath);
|
||||
await expect(page).toHaveURL(/.*tag.*TODAY.*tasks/);
|
||||
console.log('[Legacy Archive Test] Import completed');
|
||||
|
||||
// Dismiss welcome dialog if present
|
||||
await dismissWelcomeDialog(page);
|
||||
|
||||
// Step 2: Navigate to TODAY tag to see tasks
|
||||
console.log('[Legacy Archive Test] Step 2: Navigating to TODAY tag...');
|
||||
await page.goto('/#/tag/TODAY/tasks');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await dismissWelcomeDialog(page);
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Verify tasks are visible (parent tasks - subtasks are nested)
|
||||
await page.waitForSelector(TASK_SEL, { state: 'visible', timeout: 10000 });
|
||||
const taskCount = await page.locator(TASK_SEL).count();
|
||||
console.log(
|
||||
`[Legacy Archive Test] Found ${taskCount} tasks (including subtasks when expanded)`,
|
||||
);
|
||||
|
||||
// Step 3: Mark all tasks as done (parent tasks first, subtasks auto-done by parent)
|
||||
console.log('[Legacy Archive Test] Step 3: Marking all tasks as done...');
|
||||
await markAllTasksDone(page);
|
||||
console.log('[Legacy Archive Test] All tasks marked as done');
|
||||
|
||||
// Step 4: Finish day to archive tasks
|
||||
console.log('[Legacy Archive Test] Step 4: Finishing day...');
|
||||
await finishDay(page);
|
||||
console.log('[Legacy Archive Test] Finish day completed');
|
||||
|
||||
// Step 5: Export and verify
|
||||
console.log('[Legacy Archive Test] Step 5: Exporting data...');
|
||||
await page.reload();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await importPage.navigateToImportPage();
|
||||
const download = await captureDownload(page);
|
||||
const exportedContent = await readDownloadedFile(download);
|
||||
const exportedData = JSON.parse(exportedContent);
|
||||
|
||||
// Step 6: Verify archived tasks
|
||||
console.log('[Legacy Archive Test] Step 6: Verifying exported data...');
|
||||
expect(exportedData).toHaveProperty('data');
|
||||
expect(exportedData.data).toHaveProperty('archiveYoung');
|
||||
|
||||
const archiveYoungTask = exportedData.data.archiveYoung.task;
|
||||
console.log(
|
||||
'[Legacy Archive Test] Exported archiveYoung task IDs:',
|
||||
archiveYoungTask.ids,
|
||||
);
|
||||
console.log(
|
||||
'[Legacy Archive Test] Exported archiveYoung entity keys:',
|
||||
Object.keys(archiveYoungTask.entities),
|
||||
);
|
||||
|
||||
// BUG CHECK: All 4 tasks should be in the archive
|
||||
// This is where the bug manifests - subtasks are missing
|
||||
expect(archiveYoungTask.ids).toContain('parent-task-1');
|
||||
expect(archiveYoungTask.ids).toContain('subtask-1');
|
||||
expect(archiveYoungTask.ids).toContain('subtask-2');
|
||||
expect(archiveYoungTask.ids).toContain('normal-task-1');
|
||||
|
||||
// Verify entities are present
|
||||
expect(archiveYoungTask.entities['parent-task-1']).toBeDefined();
|
||||
expect(archiveYoungTask.entities['subtask-1']).toBeDefined();
|
||||
expect(archiveYoungTask.entities['subtask-2']).toBeDefined();
|
||||
expect(archiveYoungTask.entities['normal-task-1']).toBeDefined();
|
||||
|
||||
// Verify parent-subtask relationships
|
||||
const parentTask = archiveYoungTask.entities['parent-task-1'];
|
||||
expect(parentTask.subTaskIds).toContain('subtask-1');
|
||||
expect(parentTask.subTaskIds).toContain('subtask-2');
|
||||
|
||||
const subtask1 = archiveYoungTask.entities['subtask-1'];
|
||||
expect(subtask1.parentId).toBe('parent-task-1');
|
||||
|
||||
const subtask2 = archiveYoungTask.entities['subtask-2'];
|
||||
expect(subtask2.parentId).toBe('parent-task-1');
|
||||
|
||||
console.log('[Legacy Archive Test] All subtasks preserved in archive!');
|
||||
});
|
||||
|
||||
/**
|
||||
* Test: Verify exact count of archived tasks after finish day
|
||||
*/
|
||||
test('should have exactly 4 archived tasks after finish day', async ({ page }) => {
|
||||
test.setTimeout(120000);
|
||||
|
||||
const importPage = new ImportPage(page);
|
||||
|
||||
// Import legacy backup
|
||||
await importPage.navigateToImportPage();
|
||||
const backupPath = ImportPage.getFixturePath('legacy-archive-subtasks-backup.json');
|
||||
await importPage.importBackupFile(backupPath);
|
||||
await expect(page).toHaveURL(/.*tag.*TODAY.*tasks/);
|
||||
await dismissWelcomeDialog(page);
|
||||
|
||||
// Navigate to TODAY tag and mark all done
|
||||
await page.goto('/#/tag/TODAY/tasks');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await dismissWelcomeDialog(page);
|
||||
await page.waitForTimeout(1000);
|
||||
await markAllTasksDone(page);
|
||||
|
||||
// Finish day
|
||||
await finishDay(page);
|
||||
|
||||
// Export
|
||||
await page.reload();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await importPage.navigateToImportPage();
|
||||
const download = await captureDownload(page);
|
||||
const exportedContent = await readDownloadedFile(download);
|
||||
const exportedData = JSON.parse(exportedContent);
|
||||
|
||||
// Count archived tasks
|
||||
const archiveYoungTaskIds = exportedData.data.archiveYoung.task.ids;
|
||||
const archiveYoungEntityCount = Object.keys(
|
||||
exportedData.data.archiveYoung.task.entities,
|
||||
).length;
|
||||
|
||||
console.log('[Legacy Archive Test] Archive task counts:', {
|
||||
idsCount: archiveYoungTaskIds.length,
|
||||
entitiesCount: archiveYoungEntityCount,
|
||||
expectedCount: 4,
|
||||
});
|
||||
|
||||
// BUG CHECK: Should have 4 tasks (parent, 2 subtasks, normal)
|
||||
expect(archiveYoungTaskIds.length).toBe(4);
|
||||
expect(archiveYoungEntityCount).toBe(4);
|
||||
});
|
||||
|
||||
/**
|
||||
* Test: Verify subtask entity data integrity after archive
|
||||
*/
|
||||
test('should preserve subtask entity data after finish day', async ({ page }) => {
|
||||
test.setTimeout(120000);
|
||||
|
||||
const importPage = new ImportPage(page);
|
||||
|
||||
// Import, mark done, finish day
|
||||
await importPage.navigateToImportPage();
|
||||
const backupPath = ImportPage.getFixturePath('legacy-archive-subtasks-backup.json');
|
||||
await importPage.importBackupFile(backupPath);
|
||||
await expect(page).toHaveURL(/.*tag.*TODAY.*tasks/);
|
||||
await dismissWelcomeDialog(page);
|
||||
|
||||
await page.goto('/#/tag/TODAY/tasks');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await dismissWelcomeDialog(page);
|
||||
await page.waitForTimeout(1000);
|
||||
await markAllTasksDone(page);
|
||||
await finishDay(page);
|
||||
|
||||
// Export
|
||||
await page.reload();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await importPage.navigateToImportPage();
|
||||
const download = await captureDownload(page);
|
||||
const exportedContent = await readDownloadedFile(download);
|
||||
const exportedData = JSON.parse(exportedContent);
|
||||
|
||||
// Get subtask entities
|
||||
const subtask1 = exportedData.data.archiveYoung.task.entities['subtask-1'];
|
||||
const subtask2 = exportedData.data.archiveYoung.task.entities['subtask-2'];
|
||||
|
||||
// BUG CHECK: Subtasks should exist and have correct data
|
||||
expect(subtask1).toBeDefined();
|
||||
expect(subtask1.id).toBe('subtask-1');
|
||||
expect(subtask1.title).toBe('Legacy Test - First Subtask');
|
||||
expect(subtask1.parentId).toBe('parent-task-1');
|
||||
expect(subtask1.isDone).toBe(true);
|
||||
|
||||
expect(subtask2).toBeDefined();
|
||||
expect(subtask2.id).toBe('subtask-2');
|
||||
expect(subtask2.title).toBe('Legacy Test - Second Subtask');
|
||||
expect(subtask2.parentId).toBe('parent-task-1');
|
||||
expect(subtask2.isDone).toBe(true);
|
||||
|
||||
console.log('[Legacy Archive Test] Subtask entity data preserved correctly!');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,445 @@
|
|||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { OperationLogStoreService } from '../store/operation-log-store.service';
|
||||
import { OpType, Operation } from '../operation.types';
|
||||
import { resetTestUuidCounter, TestClient } from './helpers/test-client.helper';
|
||||
import { createMinimalTaskPayload } from './helpers/operation-factory.helper';
|
||||
import { CURRENT_SCHEMA_VERSION } from '../store/schema-migration.service';
|
||||
import { uuidv7 } from '../../../../util/uuid-v7';
|
||||
|
||||
/**
|
||||
* Integration tests for legacy data import/export with archive subtasks.
|
||||
*
|
||||
* BUG: When importing data from before operation logs (legacy data) that contains
|
||||
* tasks with subtasks in archiveYoung, the subtasks are lost when exporting again.
|
||||
*
|
||||
* These tests verify that:
|
||||
* 1. Import creates proper operations preserving parent-subtask relationships
|
||||
* 2. Export includes all archived tasks including subtasks
|
||||
* 3. Round-trip import->export preserves data integrity
|
||||
*/
|
||||
|
||||
// Type definitions for test data
|
||||
interface TestTaskEntity {
|
||||
id: string;
|
||||
title: string;
|
||||
subTaskIds: string[];
|
||||
parentId?: string;
|
||||
isDone: boolean;
|
||||
doneOn?: number;
|
||||
timeSpent: number;
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
interface TestArchiveTask {
|
||||
ids: string[];
|
||||
entities: Record<string, TestTaskEntity>;
|
||||
}
|
||||
|
||||
interface TestArchiveYoung {
|
||||
task: TestArchiveTask;
|
||||
timeTracking: { project: Record<string, unknown>; tag: Record<string, unknown> };
|
||||
lastTimeTrackingFlush: number;
|
||||
}
|
||||
|
||||
interface TestLegacyData {
|
||||
task: {
|
||||
ids: string[];
|
||||
entities: Record<string, unknown>;
|
||||
currentTaskId: null;
|
||||
selectedTaskId: null;
|
||||
lastCurrentTaskId: null;
|
||||
isDataLoaded: boolean;
|
||||
};
|
||||
project: {
|
||||
ids: string[];
|
||||
entities: Record<string, unknown>;
|
||||
};
|
||||
tag: {
|
||||
ids: string[];
|
||||
entities: Record<string, unknown>;
|
||||
};
|
||||
archiveYoung: TestArchiveYoung;
|
||||
archiveOld: {
|
||||
task: { ids: string[]; entities: Record<string, unknown> };
|
||||
timeTracking: { project: Record<string, unknown>; tag: Record<string, unknown> };
|
||||
lastTimeTrackingFlush: number;
|
||||
};
|
||||
timeTracking: { project: Record<string, unknown>; tag: Record<string, unknown> };
|
||||
}
|
||||
|
||||
describe('Legacy Archive Subtasks Integration', () => {
|
||||
let storeService: OperationLogStoreService;
|
||||
|
||||
// Test data: parent task with 2 subtasks + 1 normal task in archiveYoung
|
||||
const createLegacyArchiveData = (): TestLegacyData => ({
|
||||
task: {
|
||||
ids: [],
|
||||
entities: {},
|
||||
currentTaskId: null,
|
||||
selectedTaskId: null,
|
||||
lastCurrentTaskId: null,
|
||||
isDataLoaded: false,
|
||||
},
|
||||
project: {
|
||||
ids: ['INBOX_PROJECT'],
|
||||
entities: {
|
||||
INBOX_PROJECT: {
|
||||
id: 'INBOX_PROJECT',
|
||||
title: 'Inbox',
|
||||
taskIds: [],
|
||||
backlogTaskIds: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
tag: {
|
||||
ids: ['TODAY'],
|
||||
entities: {
|
||||
TODAY: {
|
||||
id: 'TODAY',
|
||||
title: 'Today',
|
||||
taskIds: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
archiveYoung: {
|
||||
task: {
|
||||
ids: [
|
||||
'archived-parent-task',
|
||||
'archived-subtask-1',
|
||||
'archived-subtask-2',
|
||||
'archived-normal-task',
|
||||
],
|
||||
entities: {
|
||||
'archived-parent-task': createMinimalTaskPayload('archived-parent-task', {
|
||||
title: 'Legacy Archive Test - Parent Task With Subtasks',
|
||||
subTaskIds: ['archived-subtask-1', 'archived-subtask-2'],
|
||||
isDone: true,
|
||||
doneOn: Date.now() - 86400000, // 1 day ago
|
||||
timeSpent: 3600000,
|
||||
projectId: 'INBOX_PROJECT',
|
||||
}) as unknown as TestTaskEntity,
|
||||
'archived-subtask-1': createMinimalTaskPayload('archived-subtask-1', {
|
||||
title: 'Legacy Archive Test - First Subtask',
|
||||
parentId: 'archived-parent-task',
|
||||
subTaskIds: [],
|
||||
isDone: true,
|
||||
doneOn: Date.now() - 86400000,
|
||||
timeSpent: 1800000,
|
||||
projectId: 'INBOX_PROJECT',
|
||||
}) as unknown as TestTaskEntity,
|
||||
'archived-subtask-2': createMinimalTaskPayload('archived-subtask-2', {
|
||||
title: 'Legacy Archive Test - Second Subtask',
|
||||
parentId: 'archived-parent-task',
|
||||
subTaskIds: [],
|
||||
isDone: true,
|
||||
doneOn: Date.now() - 86400000,
|
||||
timeSpent: 1200000,
|
||||
projectId: 'INBOX_PROJECT',
|
||||
}) as unknown as TestTaskEntity,
|
||||
'archived-normal-task': createMinimalTaskPayload('archived-normal-task', {
|
||||
title: 'Legacy Archive Test - Normal Task Without Subtasks',
|
||||
subTaskIds: [],
|
||||
isDone: true,
|
||||
doneOn: Date.now() - 86400000,
|
||||
timeSpent: 2400000,
|
||||
projectId: 'INBOX_PROJECT',
|
||||
}) as unknown as TestTaskEntity,
|
||||
},
|
||||
},
|
||||
timeTracking: { project: {}, tag: {} },
|
||||
lastTimeTrackingFlush: Date.now(),
|
||||
},
|
||||
archiveOld: {
|
||||
task: { ids: [], entities: {} },
|
||||
timeTracking: { project: {}, tag: {} },
|
||||
lastTimeTrackingFlush: 0,
|
||||
},
|
||||
timeTracking: { project: {}, tag: {} },
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [OperationLogStoreService],
|
||||
});
|
||||
storeService = TestBed.inject(OperationLogStoreService);
|
||||
|
||||
await storeService.init();
|
||||
await storeService._clearAllDataForTesting();
|
||||
resetTestUuidCounter();
|
||||
});
|
||||
|
||||
describe('SyncImport with Archive Subtasks', () => {
|
||||
/**
|
||||
* Test: Import operation preserves all archived tasks including subtasks
|
||||
*
|
||||
* When legacy data with archived parent+subtask structure is imported,
|
||||
* the SyncImport operation payload should contain all 4 tasks.
|
||||
*/
|
||||
it('should preserve archived subtasks in import operation payload', async () => {
|
||||
const testClient = new TestClient('import-client');
|
||||
const legacyData = createLegacyArchiveData();
|
||||
|
||||
// Create import operation (simulating what PfapiService does)
|
||||
const importOp: Operation = testClient.createOperation({
|
||||
actionType: '[SP_ALL] Load(import) all data',
|
||||
opType: OpType.SyncImport,
|
||||
entityType: 'ALL',
|
||||
entityId: uuidv7(),
|
||||
payload: { appDataComplete: legacyData },
|
||||
});
|
||||
|
||||
await storeService.append(importOp, 'local');
|
||||
|
||||
// Retrieve and verify the stored operation
|
||||
const allOps = await storeService.getOpsAfterSeq(0);
|
||||
expect(allOps.length).toBe(1);
|
||||
|
||||
const storedOp = allOps[0];
|
||||
const payload = storedOp.op.payload as { appDataComplete: TestLegacyData };
|
||||
|
||||
// Verify archiveYoung contains all 4 tasks
|
||||
const archiveYoungTask = payload.appDataComplete.archiveYoung.task;
|
||||
|
||||
expect(archiveYoungTask.ids.length).toBe(4);
|
||||
expect(archiveYoungTask.ids).toContain('archived-parent-task');
|
||||
expect(archiveYoungTask.ids).toContain('archived-subtask-1');
|
||||
expect(archiveYoungTask.ids).toContain('archived-subtask-2');
|
||||
expect(archiveYoungTask.ids).toContain('archived-normal-task');
|
||||
|
||||
// Verify parent-subtask relationships are preserved
|
||||
const parentTask = archiveYoungTask.entities['archived-parent-task'];
|
||||
expect(parentTask.subTaskIds).toContain('archived-subtask-1');
|
||||
expect(parentTask.subTaskIds).toContain('archived-subtask-2');
|
||||
|
||||
const subtask1 = archiveYoungTask.entities['archived-subtask-1'];
|
||||
expect(subtask1.parentId).toBe('archived-parent-task');
|
||||
|
||||
const subtask2 = archiveYoungTask.entities['archived-subtask-2'];
|
||||
expect(subtask2.parentId).toBe('archived-parent-task');
|
||||
});
|
||||
|
||||
/**
|
||||
* Test: State cache after import contains all archived subtasks
|
||||
*
|
||||
* BUG: This test documents the bug where subtasks are lost in the state cache
|
||||
* after importing legacy data. The state cache is used for export.
|
||||
*/
|
||||
it('should include archived subtasks in state cache after import', async () => {
|
||||
const testClient = new TestClient('import-client');
|
||||
const legacyData = createLegacyArchiveData();
|
||||
|
||||
// Create and store import operation
|
||||
const importOp: Operation = testClient.createOperation({
|
||||
actionType: '[SP_ALL] Load(import) all data',
|
||||
opType: OpType.SyncImport,
|
||||
entityType: 'ALL',
|
||||
entityId: uuidv7(),
|
||||
payload: { appDataComplete: legacyData },
|
||||
});
|
||||
|
||||
await storeService.append(importOp, 'local');
|
||||
const lastSeq = await storeService.getLastSeq();
|
||||
|
||||
// Save state cache (simulating what PfapiService does after import)
|
||||
await storeService.saveStateCache({
|
||||
state: legacyData,
|
||||
lastAppliedOpSeq: lastSeq,
|
||||
vectorClock: importOp.vectorClock,
|
||||
compactedAt: Date.now(),
|
||||
schemaVersion: CURRENT_SCHEMA_VERSION,
|
||||
});
|
||||
|
||||
// Load and verify state cache (this is what export would use)
|
||||
const stateCache = await storeService.loadStateCache();
|
||||
expect(stateCache).toBeDefined();
|
||||
|
||||
const cachedState = stateCache!.state as TestLegacyData;
|
||||
const cachedArchiveYoung = cachedState.archiveYoung;
|
||||
|
||||
// This should pass - verifying all 4 tasks are in cache
|
||||
expect(cachedArchiveYoung.task.ids.length).toBe(4);
|
||||
expect(cachedArchiveYoung.task.ids).toContain('archived-parent-task');
|
||||
expect(cachedArchiveYoung.task.ids).toContain('archived-subtask-1');
|
||||
expect(cachedArchiveYoung.task.ids).toContain('archived-subtask-2');
|
||||
expect(cachedArchiveYoung.task.ids).toContain('archived-normal-task');
|
||||
|
||||
// Verify subtask entities are present
|
||||
expect(cachedArchiveYoung.task.entities['archived-subtask-1']).toBeDefined();
|
||||
expect(cachedArchiveYoung.task.entities['archived-subtask-2']).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Import/Export Round-Trip', () => {
|
||||
/**
|
||||
* Test: Round-trip import->export preserves archived subtasks
|
||||
*
|
||||
* BUG: This is the main bug scenario. When legacy data is:
|
||||
* 1. Imported (creating SyncImport operation)
|
||||
* 2. State cache is created
|
||||
* 3. Exported (reading from state cache or operations)
|
||||
*
|
||||
* The exported data should contain all original archived tasks including subtasks.
|
||||
* Currently, subtasks are being lost.
|
||||
*/
|
||||
it('should preserve archived subtasks through import->export cycle', async () => {
|
||||
const testClient = new TestClient('import-client');
|
||||
const originalData = createLegacyArchiveData();
|
||||
|
||||
// Step 1: Import (create SyncImport operation)
|
||||
const importOp: Operation = testClient.createOperation({
|
||||
actionType: '[SP_ALL] Load(import) all data',
|
||||
opType: OpType.SyncImport,
|
||||
entityType: 'ALL',
|
||||
entityId: uuidv7(),
|
||||
payload: { appDataComplete: originalData },
|
||||
});
|
||||
|
||||
await storeService.append(importOp, 'local');
|
||||
const lastSeq = await storeService.getLastSeq();
|
||||
|
||||
// Step 2: Save state cache (simulating post-import state saving)
|
||||
await storeService.saveStateCache({
|
||||
state: originalData,
|
||||
lastAppliedOpSeq: lastSeq,
|
||||
vectorClock: importOp.vectorClock,
|
||||
compactedAt: Date.now(),
|
||||
schemaVersion: CURRENT_SCHEMA_VERSION,
|
||||
});
|
||||
|
||||
// Step 3: Simulate export by reading from stored operation
|
||||
const allOps = await storeService.getOpsAfterSeq(0);
|
||||
const exportedFromOp = allOps[0].op.payload as { appDataComplete: TestLegacyData };
|
||||
|
||||
// Step 4: Also simulate export by reading from state cache
|
||||
const stateCache = await storeService.loadStateCache();
|
||||
const exportedFromCache = stateCache!.state as TestLegacyData;
|
||||
|
||||
// Verify both export sources contain all archived tasks
|
||||
// From operation payload:
|
||||
const opArchiveYoung = exportedFromOp.appDataComplete.archiveYoung.task;
|
||||
expect(opArchiveYoung.ids).toContain('archived-parent-task');
|
||||
expect(opArchiveYoung.ids).toContain('archived-subtask-1');
|
||||
expect(opArchiveYoung.ids).toContain('archived-subtask-2');
|
||||
expect(opArchiveYoung.ids).toContain('archived-normal-task');
|
||||
|
||||
// From state cache:
|
||||
const cacheArchiveYoung = exportedFromCache.archiveYoung.task;
|
||||
expect(cacheArchiveYoung.ids).toContain('archived-parent-task');
|
||||
expect(cacheArchiveYoung.ids).toContain('archived-subtask-1');
|
||||
expect(cacheArchiveYoung.ids).toContain('archived-subtask-2');
|
||||
expect(cacheArchiveYoung.ids).toContain('archived-normal-task');
|
||||
|
||||
// Verify subtask entities are present in exports
|
||||
expect(opArchiveYoung.entities['archived-subtask-1']).toBeDefined();
|
||||
expect(opArchiveYoung.entities['archived-subtask-2']).toBeDefined();
|
||||
expect(cacheArchiveYoung.entities['archived-subtask-1']).toBeDefined();
|
||||
expect(cacheArchiveYoung.entities['archived-subtask-2']).toBeDefined();
|
||||
});
|
||||
|
||||
/**
|
||||
* Test: Multiple imports maintain archive subtask integrity
|
||||
*
|
||||
* When legacy data is imported multiple times (simulating user importing
|
||||
* different backups), archived subtasks should remain intact.
|
||||
*/
|
||||
it('should preserve archived subtasks across multiple import cycles', async () => {
|
||||
const testClient = new TestClient('import-client');
|
||||
|
||||
// First import with subtasks
|
||||
const firstData = createLegacyArchiveData();
|
||||
const firstImportOp: Operation = testClient.createOperation({
|
||||
actionType: '[SP_ALL] Load(import) all data',
|
||||
opType: OpType.SyncImport,
|
||||
entityType: 'ALL',
|
||||
entityId: uuidv7(),
|
||||
payload: { appDataComplete: firstData },
|
||||
});
|
||||
|
||||
await storeService.append(firstImportOp, 'local');
|
||||
|
||||
// Verify first import
|
||||
let allOps = await storeService.getOpsAfterSeq(0);
|
||||
let lastPayload = allOps[allOps.length - 1].op.payload as {
|
||||
appDataComplete: TestLegacyData;
|
||||
};
|
||||
let archiveYoung = lastPayload.appDataComplete.archiveYoung.task;
|
||||
expect(archiveYoung.ids.length).toBe(4);
|
||||
|
||||
// Second import (same data, simulating re-import)
|
||||
const secondImportOp: Operation = testClient.createOperation({
|
||||
actionType: '[SP_ALL] Load(import) all data',
|
||||
opType: OpType.SyncImport,
|
||||
entityType: 'ALL',
|
||||
entityId: uuidv7(),
|
||||
payload: { appDataComplete: firstData },
|
||||
});
|
||||
|
||||
await storeService.append(secondImportOp, 'local');
|
||||
|
||||
// Verify second import still has all subtasks
|
||||
allOps = await storeService.getOpsAfterSeq(0);
|
||||
lastPayload = allOps[allOps.length - 1].op.payload as {
|
||||
appDataComplete: TestLegacyData;
|
||||
};
|
||||
archiveYoung = lastPayload.appDataComplete.archiveYoung.task;
|
||||
|
||||
expect(archiveYoung.ids.length).toBe(4);
|
||||
expect(archiveYoung.ids).toContain('archived-subtask-1');
|
||||
expect(archiveYoung.ids).toContain('archived-subtask-2');
|
||||
expect(archiveYoung.entities['archived-subtask-1']).toBeDefined();
|
||||
expect(archiveYoung.entities['archived-subtask-2']).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Parent-Subtask Relationship Integrity', () => {
|
||||
/**
|
||||
* Test: Parent task subTaskIds match actual subtask parentIds
|
||||
*
|
||||
* This verifies the bidirectional relationship is maintained.
|
||||
*/
|
||||
it('should maintain bidirectional parent-subtask relationships', async () => {
|
||||
const testClient = new TestClient('import-client');
|
||||
const legacyData = createLegacyArchiveData();
|
||||
|
||||
const importOp: Operation = testClient.createOperation({
|
||||
actionType: '[SP_ALL] Load(import) all data',
|
||||
opType: OpType.SyncImport,
|
||||
entityType: 'ALL',
|
||||
entityId: uuidv7(),
|
||||
payload: { appDataComplete: legacyData },
|
||||
});
|
||||
|
||||
await storeService.append(importOp, 'local');
|
||||
|
||||
const allOps = await storeService.getOpsAfterSeq(0);
|
||||
const payload = allOps[0].op.payload as { appDataComplete: TestLegacyData };
|
||||
|
||||
const archiveYoung = payload.appDataComplete.archiveYoung.task;
|
||||
|
||||
// Get parent and subtasks
|
||||
const parent = archiveYoung.entities['archived-parent-task'];
|
||||
const subtask1 = archiveYoung.entities['archived-subtask-1'];
|
||||
const subtask2 = archiveYoung.entities['archived-subtask-2'];
|
||||
const normalTask = archiveYoung.entities['archived-normal-task'];
|
||||
|
||||
// Verify parent has correct subTaskIds
|
||||
expect(parent.subTaskIds).toBeDefined();
|
||||
expect(parent.subTaskIds!.length).toBe(2);
|
||||
expect(parent.subTaskIds).toContain('archived-subtask-1');
|
||||
expect(parent.subTaskIds).toContain('archived-subtask-2');
|
||||
expect(parent.parentId).toBeUndefined();
|
||||
|
||||
// Verify subtasks point to correct parent
|
||||
expect(subtask1.parentId).toBe('archived-parent-task');
|
||||
expect(subtask1.subTaskIds).toEqual([]);
|
||||
|
||||
expect(subtask2.parentId).toBe('archived-parent-task');
|
||||
expect(subtask2.subTaskIds).toEqual([]);
|
||||
|
||||
// Verify normal task has no parent-child relationships
|
||||
expect(normalTask.parentId).toBeUndefined();
|
||||
expect(normalTask.subTaskIds).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue