mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
test: add comprehensive tests for TaskService and StartupService
TaskService (45 tests): - Task selection, lifecycle, updates, ordering - Subtask management, archiving, project transfer - Scheduling, time tracking, task creation StartupService (16 tests): - Initialization flow with BroadcastChannel mocking - App rating logic (day tracking, dialog triggers) - Tour detection, plugin init, storage persistence
This commit is contained in:
parent
f0f536671b
commit
a7c780a444
2 changed files with 1003 additions and 89 deletions
369
src/app/core/startup/startup.service.spec.ts
Normal file
369
src/app/core/startup/startup.service.spec.ts
Normal file
|
|
@ -0,0 +1,369 @@
|
|||
import { TestBed, fakeAsync, tick, flush } from '@angular/core/testing';
|
||||
import { StartupService } from './startup.service';
|
||||
import { PfapiService } from '../../pfapi/pfapi.service';
|
||||
import { ImexViewService } from '../../imex/imex-meta/imex-view.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { LocalBackupService } from '../../imex/local-backup/local-backup.service';
|
||||
import { GlobalConfigService } from '../../features/config/global-config.service';
|
||||
import { SnackService } from '../snack/snack.service';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { PluginService } from '../../plugins/plugin.service';
|
||||
import { SyncWrapperService } from '../../imex/sync/sync-wrapper.service';
|
||||
import { BannerService } from '../banner/banner.service';
|
||||
import { UiHelperService } from '../../features/ui-helper/ui-helper.service';
|
||||
import { ChromeExtensionInterfaceService } from '../chrome-extension-interface/chrome-extension-interface.service';
|
||||
import { ProjectService } from '../../features/project/project.service';
|
||||
import { TrackingReminderService } from '../../features/tracking-reminder/tracking-reminder.service';
|
||||
import { SyncSafetyBackupService } from '../../imex/sync/sync-safety-backup.service';
|
||||
import { of } from 'rxjs';
|
||||
import { signal } from '@angular/core';
|
||||
import { LS } from '../persistence/storage-keys.const';
|
||||
|
||||
describe('StartupService', () => {
|
||||
let service: StartupService;
|
||||
let pfapiService: jasmine.SpyObj<PfapiService>;
|
||||
let matDialog: jasmine.SpyObj<MatDialog>;
|
||||
let pluginService: jasmine.SpyObj<PluginService>;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock localStorage
|
||||
const localStorageMock: { [key: string]: string } = {};
|
||||
spyOn(localStorage, 'getItem').and.callFake(
|
||||
(key: string) => localStorageMock[key] || null,
|
||||
);
|
||||
spyOn(localStorage, 'setItem').and.callFake(
|
||||
(key: string, value: string) => (localStorageMock[key] = value),
|
||||
);
|
||||
|
||||
// Create spies for all dependencies
|
||||
const pfapiServiceSpy = jasmine.createSpyObj('PfapiService', [
|
||||
'isCheckForStrayLocalTmpDBBackupAndImport',
|
||||
]);
|
||||
pfapiServiceSpy.isCheckForStrayLocalTmpDBBackupAndImport.and.returnValue(
|
||||
Promise.resolve(),
|
||||
);
|
||||
pfapiServiceSpy.pf = {
|
||||
metaModel: {
|
||||
load: jasmine.createSpy().and.returnValue(Promise.resolve(null)),
|
||||
},
|
||||
};
|
||||
|
||||
const imexViewServiceSpy = jasmine.createSpyObj('ImexViewService', ['']);
|
||||
const translateServiceSpy = jasmine.createSpyObj('TranslateService', ['']);
|
||||
|
||||
const localBackupServiceSpy = jasmine.createSpyObj('LocalBackupService', [
|
||||
'askForFileStoreBackupIfAvailable',
|
||||
'init',
|
||||
]);
|
||||
localBackupServiceSpy.askForFileStoreBackupIfAvailable.and.returnValue(
|
||||
Promise.resolve(),
|
||||
);
|
||||
|
||||
const globalConfigServiceSpy = jasmine.createSpyObj('GlobalConfigService', [''], {
|
||||
cfg: signal({
|
||||
misc: {
|
||||
isConfirmBeforeExit: false,
|
||||
defaultProjectId: null,
|
||||
isShowProductivityTipLonger: false,
|
||||
},
|
||||
}),
|
||||
misc: signal({
|
||||
isShowProductivityTipLonger: false,
|
||||
}),
|
||||
});
|
||||
|
||||
const snackServiceSpy = jasmine.createSpyObj('SnackService', ['open']);
|
||||
const matDialogSpy = jasmine.createSpyObj('MatDialog', ['open']);
|
||||
|
||||
const pluginServiceSpy = jasmine.createSpyObj('PluginService', ['initializePlugins']);
|
||||
pluginServiceSpy.initializePlugins.and.returnValue(Promise.resolve());
|
||||
|
||||
const syncWrapperServiceSpy = jasmine.createSpyObj('SyncWrapperService', [
|
||||
'isSyncInProgressSync',
|
||||
]);
|
||||
syncWrapperServiceSpy.isSyncInProgressSync.and.returnValue(false);
|
||||
syncWrapperServiceSpy.afterCurrentSyncDoneOrSyncDisabled$ = of(undefined);
|
||||
|
||||
const bannerServiceSpy = jasmine.createSpyObj('BannerService', [
|
||||
'open',
|
||||
'dismissAll',
|
||||
]);
|
||||
|
||||
const uiHelperServiceSpy = jasmine.createSpyObj('UiHelperService', ['initElectron']);
|
||||
|
||||
const chromeExtensionInterfaceServiceSpy = jasmine.createSpyObj(
|
||||
'ChromeExtensionInterfaceService',
|
||||
['init'],
|
||||
);
|
||||
|
||||
const projectServiceSpy = jasmine.createSpyObj('ProjectService', [''], {
|
||||
list: signal([{ id: 'project-1' }, { id: 'project-2' }, { id: 'project-3' }]),
|
||||
});
|
||||
|
||||
const trackingReminderServiceSpy = jasmine.createSpyObj('TrackingReminderService', [
|
||||
'init',
|
||||
]);
|
||||
|
||||
const syncSafetyBackupServiceSpy = jasmine.createSpyObj('SyncSafetyBackupService', [
|
||||
'',
|
||||
]);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
StartupService,
|
||||
{ provide: PfapiService, useValue: pfapiServiceSpy },
|
||||
{ provide: ImexViewService, useValue: imexViewServiceSpy },
|
||||
{ provide: TranslateService, useValue: translateServiceSpy },
|
||||
{ provide: LocalBackupService, useValue: localBackupServiceSpy },
|
||||
{ provide: GlobalConfigService, useValue: globalConfigServiceSpy },
|
||||
{ provide: SnackService, useValue: snackServiceSpy },
|
||||
{ provide: MatDialog, useValue: matDialogSpy },
|
||||
{ provide: PluginService, useValue: pluginServiceSpy },
|
||||
{ provide: SyncWrapperService, useValue: syncWrapperServiceSpy },
|
||||
{ provide: BannerService, useValue: bannerServiceSpy },
|
||||
{ provide: UiHelperService, useValue: uiHelperServiceSpy },
|
||||
{
|
||||
provide: ChromeExtensionInterfaceService,
|
||||
useValue: chromeExtensionInterfaceServiceSpy,
|
||||
},
|
||||
{ provide: ProjectService, useValue: projectServiceSpy },
|
||||
{ provide: TrackingReminderService, useValue: trackingReminderServiceSpy },
|
||||
{ provide: SyncSafetyBackupService, useValue: syncSafetyBackupServiceSpy },
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(StartupService);
|
||||
pfapiService = TestBed.inject(PfapiService) as jasmine.SpyObj<PfapiService>;
|
||||
matDialog = TestBed.inject(MatDialog) as jasmine.SpyObj<MatDialog>;
|
||||
pluginService = TestBed.inject(PluginService) as jasmine.SpyObj<PluginService>;
|
||||
});
|
||||
|
||||
describe('service creation', () => {
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('init', () => {
|
||||
// Note: Full init() testing requires complex BroadcastChannel mocking
|
||||
// These tests cover the testable parts
|
||||
|
||||
it('should check for stray backups during initialization', fakeAsync(() => {
|
||||
// Mock BroadcastChannel to prevent multi-instance blocking
|
||||
const mockChannel = {
|
||||
postMessage: jasmine.createSpy(),
|
||||
addEventListener: jasmine.createSpy(),
|
||||
removeEventListener: jasmine.createSpy(),
|
||||
close: jasmine.createSpy(),
|
||||
};
|
||||
const originalBroadcastChannel = (window as any).BroadcastChannel;
|
||||
(window as any).BroadcastChannel = jasmine
|
||||
.createSpy('BroadcastChannel')
|
||||
.and.returnValue(mockChannel);
|
||||
|
||||
service.init();
|
||||
tick(200); // Wait for single instance check
|
||||
|
||||
expect(pfapiService.isCheckForStrayLocalTmpDBBackupAndImport).toHaveBeenCalled();
|
||||
|
||||
flush();
|
||||
|
||||
// Restore
|
||||
(window as any).BroadcastChannel = originalBroadcastChannel;
|
||||
}));
|
||||
});
|
||||
|
||||
describe('_handleAppStartRating (private, tested via init)', () => {
|
||||
it('should increment app start count on new day', () => {
|
||||
// Set up initial state - different day
|
||||
(localStorage.getItem as jasmine.Spy).and.callFake((key: string) => {
|
||||
if (key === LS.APP_START_COUNT) return '5';
|
||||
if (key === LS.APP_START_COUNT_LAST_START_DAY) return '2026-01-04'; // Different day
|
||||
return null;
|
||||
});
|
||||
|
||||
// Call the private method via reflection for unit testing
|
||||
(service as any)._handleAppStartRating();
|
||||
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith(LS.APP_START_COUNT, '6');
|
||||
});
|
||||
|
||||
it('should show rating dialog at 32 app starts', () => {
|
||||
(localStorage.getItem as jasmine.Spy).and.callFake((key: string) => {
|
||||
if (key === LS.APP_START_COUNT) return '32';
|
||||
if (key === LS.APP_START_COUNT_LAST_START_DAY) return '2026-01-04';
|
||||
return null;
|
||||
});
|
||||
|
||||
(service as any)._handleAppStartRating();
|
||||
|
||||
expect(matDialog.open).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show rating dialog at 96 app starts', () => {
|
||||
(localStorage.getItem as jasmine.Spy).and.callFake((key: string) => {
|
||||
if (key === LS.APP_START_COUNT) return '96';
|
||||
if (key === LS.APP_START_COUNT_LAST_START_DAY) return '2026-01-04';
|
||||
return null;
|
||||
});
|
||||
|
||||
(service as any)._handleAppStartRating();
|
||||
|
||||
expect(matDialog.open).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not show rating dialog at other counts', () => {
|
||||
(localStorage.getItem as jasmine.Spy).and.callFake((key: string) => {
|
||||
if (key === LS.APP_START_COUNT) return '50';
|
||||
if (key === LS.APP_START_COUNT_LAST_START_DAY) return '2026-01-04';
|
||||
return null;
|
||||
});
|
||||
|
||||
(service as any)._handleAppStartRating();
|
||||
|
||||
expect(matDialog.open).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not increment count if same day', () => {
|
||||
const todayStr = new Date().toISOString().split('T')[0];
|
||||
(localStorage.getItem as jasmine.Spy).and.callFake((key: string) => {
|
||||
if (key === LS.APP_START_COUNT) return '10';
|
||||
if (key === LS.APP_START_COUNT_LAST_START_DAY) return todayStr;
|
||||
return null;
|
||||
});
|
||||
|
||||
(service as any)._handleAppStartRating();
|
||||
|
||||
// Should not have set a new count since it's the same day
|
||||
// (the method only sets when lastStartDay !== todayStr)
|
||||
const setItemCalls = (localStorage.setItem as jasmine.Spy).calls.all();
|
||||
const countSetCalls = setItemCalls.filter(
|
||||
(call) => call.args[0] === LS.APP_START_COUNT,
|
||||
);
|
||||
expect(countSetCalls.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_isTourLikelyToBeShown (private)', () => {
|
||||
it('should return false if IS_SKIP_TOUR is set', () => {
|
||||
(localStorage.getItem as jasmine.Spy).and.callFake((key: string) => {
|
||||
if (key === LS.IS_SKIP_TOUR) return 'true';
|
||||
return null;
|
||||
});
|
||||
|
||||
const result = (service as any)._isTourLikelyToBeShown();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for NIGHTWATCH user agent', () => {
|
||||
const originalUserAgent = navigator.userAgent;
|
||||
Object.defineProperty(navigator, 'userAgent', {
|
||||
value: 'NIGHTWATCH',
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const result = (service as any)._isTourLikelyToBeShown();
|
||||
|
||||
expect(result).toBe(false);
|
||||
|
||||
// Restore
|
||||
Object.defineProperty(navigator, 'userAgent', {
|
||||
value: originalUserAgent,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false for PLAYWRIGHT user agent', () => {
|
||||
const originalUserAgent = navigator.userAgent;
|
||||
Object.defineProperty(navigator, 'userAgent', {
|
||||
value: 'Something PLAYWRIGHT Something',
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const result = (service as any)._isTourLikelyToBeShown();
|
||||
|
||||
expect(result).toBe(false);
|
||||
|
||||
// Restore
|
||||
Object.defineProperty(navigator, 'userAgent', {
|
||||
value: originalUserAgent,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false when more than 2 projects exist', () => {
|
||||
// projectService.list returns signal with 3 projects in setup
|
||||
const result = (service as any)._isTourLikelyToBeShown();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_initPlugins (private)', () => {
|
||||
it('should initialize plugins after sync completes', async () => {
|
||||
await (service as any)._initPlugins();
|
||||
|
||||
expect(pluginService.initializePlugins).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle plugin initialization errors gracefully', async () => {
|
||||
pluginService.initializePlugins.and.returnValue(
|
||||
Promise.reject(new Error('Plugin init failed')),
|
||||
);
|
||||
|
||||
// Should not throw
|
||||
await expectAsync((service as any)._initPlugins()).toBeResolved();
|
||||
});
|
||||
});
|
||||
|
||||
describe('_requestPersistence (private)', () => {
|
||||
it('should request persistent storage', fakeAsync(() => {
|
||||
const mockStorage = {
|
||||
persisted: jasmine.createSpy().and.returnValue(Promise.resolve(false)),
|
||||
persist: jasmine.createSpy().and.returnValue(Promise.resolve(true)),
|
||||
estimate: jasmine.createSpy(),
|
||||
};
|
||||
Object.defineProperty(navigator, 'storage', {
|
||||
value: mockStorage,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
(service as any)._requestPersistence();
|
||||
tick();
|
||||
|
||||
expect(mockStorage.persisted).toHaveBeenCalled();
|
||||
expect(mockStorage.persist).toHaveBeenCalled();
|
||||
|
||||
flush();
|
||||
}));
|
||||
|
||||
it('should not request persistence if already persisted', fakeAsync(() => {
|
||||
const mockStorage = {
|
||||
persisted: jasmine.createSpy().and.returnValue(Promise.resolve(true)),
|
||||
persist: jasmine.createSpy(),
|
||||
estimate: jasmine.createSpy(),
|
||||
};
|
||||
Object.defineProperty(navigator, 'storage', {
|
||||
value: mockStorage,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
(service as any)._requestPersistence();
|
||||
tick();
|
||||
|
||||
expect(mockStorage.persisted).toHaveBeenCalled();
|
||||
expect(mockStorage.persist).not.toHaveBeenCalled();
|
||||
|
||||
flush();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('_initOfflineBanner (private)', () => {
|
||||
// Note: This requires mocking isOnline$ which is complex
|
||||
// Basic test to ensure the method can be called
|
||||
it('should set up offline banner subscription', () => {
|
||||
expect(() => (service as any)._initOfflineBanner()).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,30 +1,47 @@
|
|||
// This test file has been disabled due to complex dependency injection issues
|
||||
// The move-to-archive.spec.ts file provides a cleaner test that demonstrates the issue
|
||||
/*
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { TaskService } from './task.service';
|
||||
import { MockStore, provideMockStore } from '@ngrx/store/testing';
|
||||
import { WorkContextService } from '../work-context/work-context.service';
|
||||
import { SnackService } from '../../core/snack/snack.service';
|
||||
import { TaskRepeatCfgService } from '../task-repeat-cfg/task-repeat-cfg.service';
|
||||
import { GlobalTrackingIntervalService } from '../../core/global-tracking-interval/global-tracking-interval.service';
|
||||
import { DateService } from '../../core/date/date.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { ArchiveService } from '../time-tracking/archive.service';
|
||||
import { TaskArchiveService } from '../time-tracking/task-archive.service';
|
||||
import { GlobalConfigService } from '../config/global-config.service';
|
||||
import { TaskFocusService } from './task-focus.service';
|
||||
import { ImexViewService } from '../../imex/imex-meta/imex-view.service';
|
||||
import { DEFAULT_TASK, Task, TaskWithSubTasks } from './task.model';
|
||||
import { WorkContextType } from '../work-context/work-context.model';
|
||||
import { of } from 'rxjs';
|
||||
import { of, Subject } from 'rxjs';
|
||||
import { TaskSharedActions } from '../../root-store/meta/task-shared.actions';
|
||||
import {
|
||||
setCurrentTask,
|
||||
unsetCurrentTask,
|
||||
setSelectedTask,
|
||||
addSubTask,
|
||||
moveSubTaskToTop,
|
||||
moveSubTaskToBottom,
|
||||
} from './store/task.actions';
|
||||
import { TaskDetailTargetPanel, TaskReminderOptionId } from './task.model';
|
||||
import { TODAY_TAG } from '../tag/tag.const';
|
||||
import { INBOX_PROJECT } from '../project/project.const';
|
||||
import { signal } from '@angular/core';
|
||||
|
||||
describe('TaskService', () => {
|
||||
let service: TaskService;
|
||||
let store: MockStore;
|
||||
let workContextService: jasmine.SpyObj<WorkContextService>;
|
||||
let archiveService: jasmine.SpyObj<ArchiveService>;
|
||||
let tickSubject: Subject<{ duration: number; date: string }>;
|
||||
|
||||
const createMockTask = (id: string, isDone: boolean, parentId?: string): Task => ({
|
||||
...DEFAULT_TASK,
|
||||
id,
|
||||
title: id + ' title',
|
||||
isDone,
|
||||
parentId,
|
||||
projectId: 'test-project',
|
||||
tagIds: [],
|
||||
});
|
||||
const createMockTask = (id: string, overrides: Partial<Task> = {}): Task =>
|
||||
({
|
||||
...DEFAULT_TASK,
|
||||
id,
|
||||
title: `Task ${id}`,
|
||||
created: Date.now(),
|
||||
projectId: 'test-project',
|
||||
...overrides,
|
||||
}) as Task;
|
||||
|
||||
const createMockTaskWithSubTasks = (
|
||||
task: Task,
|
||||
|
|
@ -35,114 +52,642 @@ describe('TaskService', () => {
|
|||
});
|
||||
|
||||
beforeEach(() => {
|
||||
tickSubject = new Subject();
|
||||
|
||||
const workContextServiceSpy = jasmine.createSpyObj('WorkContextService', [''], {
|
||||
activeWorkContextType: WorkContextType.PROJECT,
|
||||
activeWorkContextId: 'test-project',
|
||||
mainListTaskIds$: of(['task-1', 'task-2']),
|
||||
backlogTaskIds$: of([]),
|
||||
doneTaskIds$: of([]),
|
||||
doneBacklogTaskIds$: of([]),
|
||||
startableTasksForActiveContext$: of([]),
|
||||
startableTasksForActiveContext: signal([]),
|
||||
activeWorkContext$: of({
|
||||
id: 'test-project',
|
||||
type: WorkContextType.PROJECT,
|
||||
taskIds: ['task-1', 'task-2'],
|
||||
}),
|
||||
});
|
||||
const snackServiceSpy = jasmine.createSpyObj('SnackService', ['open']);
|
||||
const taskRepeatCfgServiceSpy = jasmine.createSpyObj('TaskRepeatCfgService', [
|
||||
'getTaskRepeatCfgById$',
|
||||
|
||||
const globalTrackingIntervalServiceSpy = jasmine.createSpyObj(
|
||||
'GlobalTrackingIntervalService',
|
||||
[''],
|
||||
{
|
||||
tick$: tickSubject.asObservable(),
|
||||
},
|
||||
);
|
||||
|
||||
const dateServiceSpy = jasmine.createSpyObj('DateService', ['todayStr']);
|
||||
dateServiceSpy.todayStr.and.returnValue('2026-01-05');
|
||||
|
||||
const routerSpy = jasmine.createSpyObj('Router', ['navigate']);
|
||||
routerSpy.navigate.and.returnValue(Promise.resolve(true));
|
||||
|
||||
const archiveServiceSpy = jasmine.createSpyObj('ArchiveService', [
|
||||
'moveTasksToArchiveAndFlushArchiveIfDue',
|
||||
]);
|
||||
taskRepeatCfgServiceSpy.getTaskRepeatCfgById$.and.returnValue(of(null));
|
||||
archiveServiceSpy.moveTasksToArchiveAndFlushArchiveIfDue.and.returnValue(
|
||||
Promise.resolve(),
|
||||
);
|
||||
|
||||
const taskArchiveServiceSpy = jasmine.createSpyObj('TaskArchiveService', [
|
||||
'load',
|
||||
'updateTask',
|
||||
'updateTasks',
|
||||
'getById',
|
||||
'roundTimeSpent',
|
||||
]);
|
||||
taskArchiveServiceSpy.load.and.returnValue(
|
||||
Promise.resolve({ ids: [], entities: {} }),
|
||||
);
|
||||
taskArchiveServiceSpy.getById.and.returnValue(Promise.resolve(null));
|
||||
|
||||
const globalConfigServiceSpy = jasmine.createSpyObj('GlobalConfigService', [''], {
|
||||
cfg: signal({
|
||||
misc: { defaultProjectId: null },
|
||||
reminder: { defaultTaskRemindOption: 'AT_START' },
|
||||
appFeatures: { isTimeTrackingEnabled: true },
|
||||
}),
|
||||
misc: signal({ isShowProductivityTipLonger: false }),
|
||||
});
|
||||
|
||||
const taskFocusServiceSpy = jasmine.createSpyObj('TaskFocusService', [''], {
|
||||
lastFocusedTaskComponent: signal(null),
|
||||
});
|
||||
|
||||
const imexViewServiceSpy = jasmine.createSpyObj('ImexViewService', [''], {
|
||||
isDataImportInProgress$: of(false),
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
TaskService,
|
||||
provideMockStore({
|
||||
initialState: {
|
||||
task: {
|
||||
ids: [],
|
||||
entities: {},
|
||||
tasks: {
|
||||
ids: ['task-1', 'task-2'],
|
||||
entities: {
|
||||
['task-1']: createMockTask('task-1'),
|
||||
['task-2']: createMockTask('task-2'),
|
||||
},
|
||||
currentTaskId: null,
|
||||
selectedTaskId: null,
|
||||
taskDetailTargetPanel: null,
|
||||
isDataLoaded: true,
|
||||
},
|
||||
},
|
||||
selectors: [],
|
||||
}),
|
||||
{ provide: WorkContextService, useValue: workContextServiceSpy },
|
||||
{ provide: SnackService, useValue: snackServiceSpy },
|
||||
{ provide: TaskRepeatCfgService, useValue: taskRepeatCfgServiceSpy },
|
||||
{
|
||||
provide: GlobalTrackingIntervalService,
|
||||
useValue: globalTrackingIntervalServiceSpy,
|
||||
},
|
||||
{ provide: DateService, useValue: dateServiceSpy },
|
||||
{ provide: Router, useValue: routerSpy },
|
||||
{ provide: ArchiveService, useValue: archiveServiceSpy },
|
||||
{ provide: TaskArchiveService, useValue: taskArchiveServiceSpy },
|
||||
{ provide: GlobalConfigService, useValue: globalConfigServiceSpy },
|
||||
{ provide: TaskFocusService, useValue: taskFocusServiceSpy },
|
||||
{ provide: ImexViewService, useValue: imexViewServiceSpy },
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(TaskService);
|
||||
store = TestBed.inject(MockStore);
|
||||
workContextService = TestBed.inject(
|
||||
WorkContextService,
|
||||
) as jasmine.SpyObj<WorkContextService>;
|
||||
archiveService = TestBed.inject(ArchiveService) as jasmine.SpyObj<ArchiveService>;
|
||||
|
||||
spyOn(store, 'dispatch').and.callThrough();
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
afterEach(() => {
|
||||
store.resetSelectors();
|
||||
});
|
||||
|
||||
describe('moveToArchive', () => {
|
||||
it('should successfully archive tasks without subtasks in project context', () => {
|
||||
// Arrange
|
||||
const task1 = createMockTask('task-1', true);
|
||||
const task2 = createMockTask('task-2', true);
|
||||
const tasksWithSubTasks: TaskWithSubTasks[] = [
|
||||
createMockTaskWithSubTasks(task1),
|
||||
createMockTaskWithSubTasks(task2),
|
||||
];
|
||||
describe('setCurrentId', () => {
|
||||
it('should dispatch setCurrentTask when id is provided', () => {
|
||||
service.setCurrentId('task-1');
|
||||
|
||||
spyOn(store, 'dispatch');
|
||||
|
||||
// Act
|
||||
service.moveToArchive(tasksWithSubTasks);
|
||||
|
||||
// Assert
|
||||
expect(store.dispatch).toHaveBeenCalled();
|
||||
expect(store.dispatch).toHaveBeenCalledWith(setCurrentTask({ id: 'task-1' }));
|
||||
});
|
||||
|
||||
it('should throw error when trying to archive subtasks in project context', () => {
|
||||
// Arrange
|
||||
const parentTask = createMockTask('parent-1', true);
|
||||
const subTask1 = createMockTask('sub-1', true, 'parent-1');
|
||||
const subTask2 = createMockTask('sub-2', true, 'parent-1');
|
||||
it('should dispatch unsetCurrentTask when id is null', () => {
|
||||
service.setCurrentId(null);
|
||||
|
||||
// Include both parent and subtasks in the array (simulating the bug)
|
||||
const tasksWithSubTasks: TaskWithSubTasks[] = [
|
||||
createMockTaskWithSubTasks(parentTask, [subTask1, subTask2]),
|
||||
createMockTaskWithSubTasks(subTask1),
|
||||
createMockTaskWithSubTasks(subTask2),
|
||||
];
|
||||
expect(store.dispatch).toHaveBeenCalledWith(unsetCurrentTask());
|
||||
});
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
expect(() => service.moveToArchive(tasksWithSubTasks)).toThrowError(
|
||||
'Trying to move sub tasks into archive for project',
|
||||
describe('setSelectedId', () => {
|
||||
it('should dispatch setSelectedTask with default panel', () => {
|
||||
service.setSelectedId('task-1');
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
setSelectedTask({
|
||||
id: 'task-1',
|
||||
taskDetailTargetPanel: TaskDetailTargetPanel.Default,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should successfully handle subtasks in tag context by removing tag', () => {
|
||||
// Arrange - Switch to TAG context
|
||||
(workContextService as any).activeWorkContextType = WorkContextType.TAG;
|
||||
(workContextService as any).activeWorkContextId = 'TODAY';
|
||||
it('should dispatch setSelectedTask with specified panel', () => {
|
||||
service.setSelectedId('task-1', TaskDetailTargetPanel.Attachments);
|
||||
|
||||
const parentTask = createMockTask('parent-1', true);
|
||||
const subTask1 = {
|
||||
...createMockTask('sub-1', true, 'parent-1'),
|
||||
tagIds: ['TODAY', 'other-tag'],
|
||||
};
|
||||
const subTask2 = {
|
||||
...createMockTask('sub-2', true, 'parent-1'),
|
||||
tagIds: ['TODAY'],
|
||||
};
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
setSelectedTask({
|
||||
id: 'task-1',
|
||||
taskDetailTargetPanel: TaskDetailTargetPanel.Attachments,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const tasksWithSubTasks: TaskWithSubTasks[] = [
|
||||
createMockTaskWithSubTasks(parentTask, [subTask1, subTask2]),
|
||||
createMockTaskWithSubTasks(subTask1),
|
||||
createMockTaskWithSubTasks(subTask2),
|
||||
];
|
||||
it('should handle null id', () => {
|
||||
service.setSelectedId(null);
|
||||
|
||||
spyOn(service, 'updateTags');
|
||||
spyOn(store, 'dispatch');
|
||||
|
||||
// Act
|
||||
service.moveToArchive(tasksWithSubTasks);
|
||||
|
||||
// Assert - subtasks should have tag removed, parent task should be archived
|
||||
expect(service.updateTags).toHaveBeenCalledWith(subTask1, ['other-tag']);
|
||||
expect(service.updateTags).toHaveBeenCalledWith(subTask2, []);
|
||||
expect(store.dispatch).toHaveBeenCalled();
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
setSelectedTask({
|
||||
id: null,
|
||||
taskDetailTargetPanel: TaskDetailTargetPanel.Default,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pauseCurrent', () => {
|
||||
it('should dispatch unsetCurrentTask', () => {
|
||||
service.pauseCurrent();
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(unsetCurrentTask());
|
||||
});
|
||||
});
|
||||
|
||||
describe('add', () => {
|
||||
it('should dispatch addTask with correct payload', () => {
|
||||
const id = service.add('New Task');
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
jasmine.objectContaining({
|
||||
type: TaskSharedActions.addTask.type,
|
||||
}),
|
||||
);
|
||||
expect(id).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should create task with project context', () => {
|
||||
service.add('New Task');
|
||||
|
||||
const dispatchCall = (store.dispatch as jasmine.Spy).calls.mostRecent();
|
||||
const action = dispatchCall.args[0] as ReturnType<typeof TaskSharedActions.addTask>;
|
||||
|
||||
expect(action.task.projectId).toBe('test-project');
|
||||
expect(action.workContextType).toBe(WorkContextType.PROJECT);
|
||||
});
|
||||
|
||||
it('should pass isAddToBacklog flag', () => {
|
||||
service.add('New Task', true);
|
||||
|
||||
const dispatchCall = (store.dispatch as jasmine.Spy).calls.mostRecent();
|
||||
const action = dispatchCall.args[0] as ReturnType<typeof TaskSharedActions.addTask>;
|
||||
|
||||
expect(action.isAddToBacklog).toBe(true);
|
||||
});
|
||||
|
||||
it('should pass isAddToBottom flag', () => {
|
||||
service.add('New Task', false, {}, true);
|
||||
|
||||
const dispatchCall = (store.dispatch as jasmine.Spy).calls.mostRecent();
|
||||
const action = dispatchCall.args[0] as ReturnType<typeof TaskSharedActions.addTask>;
|
||||
|
||||
expect(action.isAddToBottom).toBe(true);
|
||||
});
|
||||
|
||||
it('should merge additional fields', () => {
|
||||
service.add('New Task', false, { notes: 'Test notes' });
|
||||
|
||||
const dispatchCall = (store.dispatch as jasmine.Spy).calls.mostRecent();
|
||||
const action = dispatchCall.args[0] as ReturnType<typeof TaskSharedActions.addTask>;
|
||||
|
||||
expect(action.task.notes).toBe('Test notes');
|
||||
});
|
||||
});
|
||||
|
||||
describe('addToToday', () => {
|
||||
it('should dispatch planTasksForToday', () => {
|
||||
const task = createMockTaskWithSubTasks(createMockTask('task-1'));
|
||||
|
||||
service.addToToday(task);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
TaskSharedActions.planTasksForToday({ taskIds: ['task-1'] }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should dispatch deleteTask', () => {
|
||||
const task = createMockTaskWithSubTasks(createMockTask('task-1'));
|
||||
|
||||
service.remove(task);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(TaskSharedActions.deleteTask({ task }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeMultipleTasks', () => {
|
||||
it('should dispatch deleteTasks', () => {
|
||||
service.removeMultipleTasks(['task-1', 'task-2']);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
TaskSharedActions.deleteTasks({ taskIds: ['task-1', 'task-2'] }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should dispatch updateTask with changes', () => {
|
||||
service.update('task-1', { title: 'Updated Title' });
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
TaskSharedActions.updateTask({
|
||||
task: { id: 'task-1', changes: { title: 'Updated Title' } },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTags', () => {
|
||||
it('should dispatch updateTask with unique tagIds', () => {
|
||||
const task = createMockTask('task-1');
|
||||
|
||||
service.updateTags(task, ['tag-1', 'tag-2', 'tag-1']);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
TaskSharedActions.updateTask({
|
||||
task: { id: 'task-1', changes: { tagIds: ['tag-1', 'tag-2'] } },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeTagsForAllTask', () => {
|
||||
it('should dispatch removeTagsForAllTasks', () => {
|
||||
service.removeTagsForAllTask(['tag-1', 'tag-2']);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
TaskSharedActions.removeTagsForAllTasks({ tagIdsToRemove: ['tag-1', 'tag-2'] }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setDone', () => {
|
||||
it('should update task with isDone: true', () => {
|
||||
service.setDone('task-1');
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
TaskSharedActions.updateTask({
|
||||
task: { id: 'task-1', changes: { isDone: true } },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setUnDone', () => {
|
||||
it('should update task with isDone: false', () => {
|
||||
service.setUnDone('task-1');
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
TaskSharedActions.updateTask({
|
||||
task: { id: 'task-1', changes: { isDone: false } },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('markIssueUpdatesAsRead', () => {
|
||||
it('should update task with issueWasUpdated: false', () => {
|
||||
service.markIssueUpdatesAsRead('task-1');
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
TaskSharedActions.updateTask({
|
||||
task: { id: 'task-1', changes: { issueWasUpdated: false } },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('moveToTop', () => {
|
||||
it('should dispatch moveSubTaskToTop for subtask', () => {
|
||||
service.moveToTop('subtask-1', 'parent-1', false);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
moveSubTaskToTop({ id: 'subtask-1', parentId: 'parent-1' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('moveToBottom', () => {
|
||||
it('should dispatch moveSubTaskToBottom for subtask', () => {
|
||||
service.moveToBottom('subtask-1', 'parent-1', false);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
moveSubTaskToBottom({ id: 'subtask-1', parentId: 'parent-1' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addSubTaskTo', () => {
|
||||
it('should dispatch addSubTask', () => {
|
||||
const id = service.addSubTaskTo('parent-1', { title: 'Subtask' });
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
jasmine.objectContaining({
|
||||
type: addSubTask.type,
|
||||
parentId: 'parent-1',
|
||||
}),
|
||||
);
|
||||
expect(id).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('moveToArchive', () => {
|
||||
it('should dispatch moveToArchive and call archive service for parent tasks', async () => {
|
||||
const task = createMockTaskWithSubTasks(
|
||||
createMockTask('task-1', { projectId: 'test-project' }),
|
||||
);
|
||||
|
||||
await service.moveToArchive(task);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
TaskSharedActions.moveToArchive({ tasks: [task] }),
|
||||
);
|
||||
expect(archiveService.moveTasksToArchiveAndFlushArchiveIfDue).toHaveBeenCalledWith([
|
||||
task,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle array of tasks', async () => {
|
||||
const tasks = [
|
||||
createMockTaskWithSubTasks(createMockTask('task-1')),
|
||||
createMockTaskWithSubTasks(createMockTask('task-2')),
|
||||
];
|
||||
|
||||
await service.moveToArchive(tasks);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
TaskSharedActions.moveToArchive({ tasks }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should only archive parent tasks (subtasks are handled separately)', async () => {
|
||||
// When archiving both parent and subtasks, only parent tasks are dispatched
|
||||
// Subtasks trigger a devError in PROJECT context (tested behavior)
|
||||
const parentTask = createMockTaskWithSubTasks(createMockTask('parent-1'));
|
||||
|
||||
await service.moveToArchive([parentTask]);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
TaskSharedActions.moveToArchive({ tasks: [parentTask] }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle null/undefined gracefully', async () => {
|
||||
await service.moveToArchive(null as any);
|
||||
|
||||
expect(store.dispatch).not.toHaveBeenCalledWith(
|
||||
jasmine.objectContaining({ type: TaskSharedActions.moveToArchive.type }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('moveToProject', () => {
|
||||
it('should dispatch moveToOtherProject', () => {
|
||||
const task = createMockTaskWithSubTasks(createMockTask('task-1'));
|
||||
|
||||
service.moveToProject(task, 'new-project');
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
TaskSharedActions.moveToOtherProject({
|
||||
task,
|
||||
targetProjectId: 'new-project',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for subtask', () => {
|
||||
const subtask = createMockTaskWithSubTasks(
|
||||
createMockTask('subtask-1', { parentId: 'parent-1' }),
|
||||
);
|
||||
|
||||
expect(() => service.moveToProject(subtask, 'new-project')).toThrowError(
|
||||
'Wrong task model',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('restoreTask', () => {
|
||||
it('should dispatch restoreTask', () => {
|
||||
const task = createMockTask('task-1');
|
||||
const subTasks = [createMockTask('subtask-1', { parentId: 'task-1' })];
|
||||
|
||||
service.restoreTask(task, subTasks);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
TaskSharedActions.restoreTask({ task, subTasks }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('scheduleTask', () => {
|
||||
it('should dispatch scheduleTaskWithTime', () => {
|
||||
const task = createMockTask('task-1');
|
||||
const due = Date.now() + 3600000;
|
||||
|
||||
service.scheduleTask(task, due, TaskReminderOptionId.AtStart);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
jasmine.objectContaining({
|
||||
type: TaskSharedActions.scheduleTaskWithTime.type,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reScheduleTask', () => {
|
||||
it('should dispatch reScheduleTaskWithTime', () => {
|
||||
const task = createMockTask('task-1');
|
||||
const due = Date.now() + 3600000;
|
||||
|
||||
service.reScheduleTask({
|
||||
task,
|
||||
due,
|
||||
remindCfg: TaskReminderOptionId.AtStart,
|
||||
isMoveToBacklog: false,
|
||||
});
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
jasmine.objectContaining({
|
||||
type: TaskSharedActions.reScheduleTaskWithTime.type,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createNewTaskWithDefaults', () => {
|
||||
it('should create task with default values', () => {
|
||||
const task = service.createNewTaskWithDefaults({ title: 'Test Task' });
|
||||
|
||||
expect(task.title).toBe('Test Task');
|
||||
expect(task.id).toBeTruthy();
|
||||
expect(task.created).toBeTruthy();
|
||||
expect(task.projectId).toBe('test-project');
|
||||
});
|
||||
|
||||
it('should set projectId for project context', () => {
|
||||
const task = service.createNewTaskWithDefaults({
|
||||
title: 'Test',
|
||||
workContextType: WorkContextType.PROJECT,
|
||||
workContextId: 'my-project',
|
||||
});
|
||||
|
||||
expect(task.projectId).toBe('my-project');
|
||||
});
|
||||
|
||||
it('should set tagIds for tag context (non-TODAY)', () => {
|
||||
const task = service.createNewTaskWithDefaults({
|
||||
title: 'Test',
|
||||
workContextType: WorkContextType.TAG,
|
||||
workContextId: 'my-tag',
|
||||
});
|
||||
|
||||
expect(task.tagIds).toContain('my-tag');
|
||||
});
|
||||
|
||||
it('should NOT set tagIds for TODAY tag context', () => {
|
||||
const task = service.createNewTaskWithDefaults({
|
||||
title: 'Test',
|
||||
workContextType: WorkContextType.TAG,
|
||||
workContextId: TODAY_TAG.id,
|
||||
});
|
||||
|
||||
expect(task.tagIds).not.toContain(TODAY_TAG.id);
|
||||
});
|
||||
|
||||
it('should set dueDay for TODAY tag context', () => {
|
||||
const task = service.createNewTaskWithDefaults({
|
||||
title: 'Test',
|
||||
workContextType: WorkContextType.TAG,
|
||||
workContextId: TODAY_TAG.id,
|
||||
});
|
||||
|
||||
expect(task.dueDay).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should use custom id if provided', () => {
|
||||
const task = service.createNewTaskWithDefaults({
|
||||
title: 'Test',
|
||||
id: 'custom-id',
|
||||
});
|
||||
|
||||
expect(task.id).toBe('custom-id');
|
||||
});
|
||||
|
||||
it('should merge additional fields', () => {
|
||||
const task = service.createNewTaskWithDefaults({
|
||||
title: 'Test',
|
||||
additional: {
|
||||
notes: 'Some notes',
|
||||
timeEstimate: 3600000,
|
||||
},
|
||||
});
|
||||
|
||||
expect(task.notes).toBe('Some notes');
|
||||
expect(task.timeEstimate).toBe(3600000);
|
||||
});
|
||||
|
||||
it('should use INBOX_PROJECT if no projectId available', () => {
|
||||
// Create a task with TAG context but no default project configured
|
||||
const task = service.createNewTaskWithDefaults({
|
||||
title: 'Test',
|
||||
workContextType: WorkContextType.TAG,
|
||||
workContextId: 'some-tag',
|
||||
});
|
||||
|
||||
// Should fallback to INBOX_PROJECT since no default is configured
|
||||
expect(task.projectId).toBe(INBOX_PROJECT.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getByIdOnce$', () => {
|
||||
it('should return observable that completes after one emission', (done) => {
|
||||
store.overrideSelector('selectTaskById', createMockTask('task-1'));
|
||||
|
||||
let emissionCount = 0;
|
||||
service.getByIdOnce$('task-1').subscribe({
|
||||
next: (task) => {
|
||||
emissionCount++;
|
||||
expect(task).toBeTruthy();
|
||||
},
|
||||
complete: () => {
|
||||
expect(emissionCount).toBe(1);
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addTimeSpent', () => {
|
||||
it('should dispatch addTimeSpent action', () => {
|
||||
const task = createMockTask('task-1');
|
||||
|
||||
service.addTimeSpent(task, 60000);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
jasmine.objectContaining({
|
||||
type: '[TimeTracking] Add time spent',
|
||||
task,
|
||||
duration: 60000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use provided date', () => {
|
||||
const task = createMockTask('task-1');
|
||||
|
||||
service.addTimeSpent(task, 60000, '2026-01-01');
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
jasmine.objectContaining({
|
||||
date: '2026-01-01',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeTimeSpent', () => {
|
||||
it('should dispatch removeTimeSpent action', () => {
|
||||
service.removeTimeSpent('task-1', 30000);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
jasmine.objectContaining({
|
||||
type: '[Task] Remove time spent',
|
||||
id: 'task-1',
|
||||
duration: 30000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleStartTask', () => {
|
||||
it('should dispatch toggleStart when time tracking is enabled', () => {
|
||||
service.toggleStartTask();
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
jasmine.objectContaining({ type: '[Task] Toggle start' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Note: convertToMainTask requires complex selector mocking that doesn't work well
|
||||
// with the current test setup. It's better tested via integration tests.
|
||||
});
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue