From fb7f925e9269ff374f05ab08c13f8ffd573e7ca0 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Mon, 28 Jul 2025 19:35:37 +0200 Subject: [PATCH] fix: error when using global add task shortcut Closes #4859 --- electron/ipc-handler.ts | 2 +- electron/protocol-handler.ts | 2 +- .../shared-with-frontend/ipc-events.const.ts | 3 +- src/app/core-ui/shortcut/shortcut.service.ts | 2 +- src/app/core/ipc-events.ts | 10 +- .../tasks/store/task-electron.effects.spec.ts | 110 ++++++++++++++++++ .../tasks/store/task-electron.effects.ts | 10 +- 7 files changed, 127 insertions(+), 12 deletions(-) create mode 100644 src/app/features/tasks/store/task-electron.effects.spec.ts diff --git a/electron/ipc-handler.ts b/electron/ipc-handler.ts index 0f0236453..2234a16f1 100644 --- a/electron/ipc-handler.ts +++ b/electron/ipc-handler.ts @@ -155,7 +155,7 @@ export const initIpcInterfaces = (): void => { actionFn = () => { showOrFocus(mainWin); // NOTE: delay slightly to make sure app is ready - mainWin.webContents.send(IPC.ADD_TASK); + mainWin.webContents.send(IPC.SHOW_ADD_TASK_BAR); }; break; diff --git a/electron/protocol-handler.ts b/electron/protocol-handler.ts index b9a72e6e5..ae80cc94a 100644 --- a/electron/protocol-handler.ts +++ b/electron/protocol-handler.ts @@ -40,7 +40,7 @@ export const processProtocolUrl = (url: string, mainWin: BrowserWindow | null): // Send IPC message to create task if (mainWin && mainWin.webContents) { - mainWin.webContents.send(IPC.ADD_TASK, { title: taskTitle }); + mainWin.webContents.send(IPC.ADD_TASK_FROM_APP_URI, { title: taskTitle }); } } break; diff --git a/electron/shared-with-frontend/ipc-events.const.ts b/electron/shared-with-frontend/ipc-events.const.ts index 1a40a153d..4b7ac3375 100644 --- a/electron/shared-with-frontend/ipc-events.const.ts +++ b/electron/shared-with-frontend/ipc-events.const.ts @@ -22,7 +22,8 @@ export enum IPC { TASK_MARK_AS_DONE = 'TASK_MARK_AS_DONE', TASK_START = 'TASK_START', TASK_TOGGLE_START = 'TASK_TOGGLE_START', - ADD_TASK = 'ADD_TASK', + SHOW_ADD_TASK_BAR = 'SHOW_ADD_TASK_BAR', + ADD_TASK_FROM_APP_URI = 'ADD_TASK_FROM_APP_URI', ADD_NOTE = 'ADD_NOTE', TASK_PAUSE = 'TASK_PAUSE', diff --git a/src/app/core-ui/shortcut/shortcut.service.ts b/src/app/core-ui/shortcut/shortcut.service.ts index 718c5d25b..75f8044d0 100644 --- a/src/app/core-ui/shortcut/shortcut.service.ts +++ b/src/app/core-ui/shortcut/shortcut.service.ts @@ -57,7 +57,7 @@ export class ShortcutService { window.ea.on(IPC.TASK_TOGGLE_START, () => { this._taskService.toggleStartTask(); }); - window.ea.on(IPC.ADD_TASK, () => { + window.ea.on(IPC.SHOW_ADD_TASK_BAR, () => { this._layoutService.showAddTaskBar(); }); window.ea.on(IPC.ADD_NOTE, () => { diff --git a/src/app/core/ipc-events.ts b/src/app/core/ipc-events.ts index eaba728a8..3f9dc9d9f 100644 --- a/src/app/core/ipc-events.ts +++ b/src/app/core/ipc-events.ts @@ -25,6 +25,12 @@ export const ipcSuspend$: Observable = IS_ELECTRON ? ipcEvent$(IPC.SUSPEND).pipe() : EMPTY; -export const ipcAddTask$: Observable<{ title: string }> = IS_ELECTRON - ? ipcEvent$(IPC.ADD_TASK).pipe(map(([ev, data]: any) => data as { title: string })) +export const ipcShowAddTaskBar$: Observable = IS_ELECTRON + ? ipcEvent$(IPC.SHOW_ADD_TASK_BAR).pipe() + : EMPTY; + +export const ipcAddTaskFromAppUri$: Observable<{ title: string }> = IS_ELECTRON + ? ipcEvent$(IPC.ADD_TASK_FROM_APP_URI).pipe( + map(([ev, data]: any) => data as { title: string }), + ) : EMPTY; diff --git a/src/app/features/tasks/store/task-electron.effects.spec.ts b/src/app/features/tasks/store/task-electron.effects.spec.ts new file mode 100644 index 000000000..1a41618eb --- /dev/null +++ b/src/app/features/tasks/store/task-electron.effects.spec.ts @@ -0,0 +1,110 @@ +import { TestBed } from '@angular/core/testing'; +import { provideMockActions } from '@ngrx/effects/testing'; +import { Observable, of, Subject } from 'rxjs'; +import { TaskElectronEffects } from './task-electron.effects'; +import { TaskService } from '../task.service'; +import { provideMockStore } from '@ngrx/store/testing'; +import { GlobalConfigService } from '../../config/global-config.service'; +import { PomodoroService } from '../../pomodoro/pomodoro.service'; +import { FocusModeService } from '../../focus-mode/focus-mode.service'; +import { tap } from 'rxjs/operators'; + +describe('TaskElectronEffects', () => { + let effects: TaskElectronEffects; + let actions$: Observable; + let taskService: jasmine.SpyObj; + let mockIpcAddTaskFromAppUri$: Subject<{ title: string }>; + + beforeEach(() => { + const taskServiceSpy = jasmine.createSpyObj('TaskService', ['add']); + const globalConfigServiceSpy = jasmine.createSpyObj('GlobalConfigService', [], { + cfg$: of({}), + }); + const pomodoroServiceSpy = jasmine.createSpyObj('PomodoroService', [], { + isEnabled$: of(false), + currentSessionTime$: of(0), + }); + const focusModeServiceSpy = jasmine.createSpyObj('FocusModeService', [], { + currentSessionTime$: of(0), + }); + + // Mock window.ea + (window as any).ea = { + on: jasmine.createSpy('on'), + updateCurrentTask: jasmine.createSpy('updateCurrentTask'), + setProgressBar: jasmine.createSpy('setProgressBar'), + }; + + actions$ = new Subject(); + mockIpcAddTaskFromAppUri$ = new Subject<{ title: string }>(); + + TestBed.configureTestingModule({ + providers: [ + { + provide: TaskElectronEffects, + useFactory: ( + taskServiceInj: TaskService, + // Other dependencies could be injected here if needed + ) => { + const effectsInstance = new TaskElectronEffects(); + // Manually inject dependencies that are used in the effect + (effectsInstance as any)._taskService = taskServiceInj; + + // Override the effect with our mock observable + effectsInstance.handleAddTaskFromProtocol$ = mockIpcAddTaskFromAppUri$.pipe( + tap((data) => { + taskServiceInj.add(data.title); + }), + ) as any; + + return effectsInstance; + }, + deps: [TaskService], + }, + provideMockActions(() => actions$), + provideMockStore(), + { provide: TaskService, useValue: taskServiceSpy }, + { provide: GlobalConfigService, useValue: globalConfigServiceSpy }, + { provide: PomodoroService, useValue: pomodoroServiceSpy }, + { provide: FocusModeService, useValue: focusModeServiceSpy }, + ], + }); + + effects = TestBed.inject(TaskElectronEffects); + taskService = TestBed.inject(TaskService) as jasmine.SpyObj; + }); + + describe('handleAddTaskFromProtocol$', () => { + it('should add task when receiving data with title', (done) => { + const mockData = { title: 'Test Task' }; + + // Subscribe to the effect + effects.handleAddTaskFromProtocol$.subscribe(() => { + expect(taskService.add).toHaveBeenCalledWith('Test Task'); + done(); + }); + + // Emit data through the mocked observable + mockIpcAddTaskFromAppUri$.next(mockData); + }); + + it('should handle multiple tasks', (done) => { + let callCount = 0; + const expectedCalls = 2; + + effects.handleAddTaskFromProtocol$.subscribe(() => { + callCount++; + if (callCount === expectedCalls) { + expect(taskService.add).toHaveBeenCalledTimes(2); + expect(taskService.add).toHaveBeenCalledWith('Task 1'); + expect(taskService.add).toHaveBeenCalledWith('Task 2'); + done(); + } + }); + + // Emit multiple tasks + mockIpcAddTaskFromAppUri$.next({ title: 'Task 1' }); + mockIpcAddTaskFromAppUri$.next({ title: 'Task 2' }); + }); + }); +}); diff --git a/src/app/features/tasks/store/task-electron.effects.ts b/src/app/features/tasks/store/task-electron.effects.ts index 53feef68f..cee727b2f 100644 --- a/src/app/features/tasks/store/task-electron.effects.ts +++ b/src/app/features/tasks/store/task-electron.effects.ts @@ -19,7 +19,7 @@ import { unPauseFocusSession, } from '../../focus-mode/store/focus-mode.actions'; import { IPC } from '../../../../../electron/shared-with-frontend/ipc-events.const'; -import { ipcAddTask$ } from '../../../core/ipc-events'; +import { ipcAddTaskFromAppUri$ } from '../../../core/ipc-events'; import { TaskService } from '../task.service'; // TODO send message to electron when current task changes here @@ -159,11 +159,9 @@ export class TaskElectronEffects { handleAddTaskFromProtocol$ = createEffect( () => - ipcAddTask$.pipe( - tap(({ title }) => { - if (title) { - this._taskService.add(title); - } + ipcAddTaskFromAppUri$.pipe( + tap((data) => { + this._taskService.add(data.title); }), ), { dispatch: false },