import { describe, it, expect, vi, beforeEach } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { of, throwError } from 'rxjs'; import { TaskStore } from '@app/features/tasks/data-access/store/task-store'; import { TaskService } from '@app/features/tasks/data-access/services/task'; import { TaskBuilder } from '@tests/builders/task.builder'; describe('TaskStore', () => { let store: TaskStore; let taskService: TaskService; beforeEach(() => { taskService = { getAll: vi.fn(), getById: vi.fn(), create: vi.fn(), update: vi.fn(), delete: vi.fn(), transition: vi.fn(), addNote: vi.fn(), deleteNote: vi.fn(), } as unknown as TaskService; TestBed.configureTestingModule({ providers: [ TaskStore, { provide: TaskService, useValue: taskService }, ], }); store = TestBed.inject(TaskStore); }); describe('loadTasks', () => { it('should load tasks and update signals', () => { const tasks = TaskBuilder.buildMany(3); const pagination = { page: 1, pageSize: 10, total: 3, totalPages: 1 }; vi.mocked(taskService.getAll).mockReturnValue(of({ data: tasks, pagination })); store.loadTasks(); expect(store.tasks()).toEqual(tasks); expect(store.pagination()).toEqual(pagination); expect(store.loading()).toBe(false); expect(store.error()).toBeNull(); }); it('should set loading true while fetching', () => { vi.mocked(taskService.getAll).mockReturnValue(of({ data: [], pagination: { page: 1, pageSize: 10, total: 0, totalPages: 0 } })); store.loadTasks(); expect(store.loading()).toBe(false); }); it('should set error when request fails', () => { vi.mocked(taskService.getAll).mockReturnValue(throwError(() => new Error('Network error'))); store.loadTasks(); expect(store.error()).toBe('Network error'); expect(store.loading()).toBe(false); }); }); describe('loadTask', () => { it('should load single task and set selected task', () => { const task = new TaskBuilder().withId('task-1').build(); vi.mocked(taskService.getById).mockReturnValue(of(task)); store.loadTask('task-1'); expect(store.selectedTask()).toEqual(task); expect(store.loading()).toBe(false); }); it('should set error when loading single task fails', () => { vi.mocked(taskService.getById).mockReturnValue(throwError(() => new Error('Not found'))); store.loadTask('task-1'); expect(store.error()).toBe('Not found'); expect(store.loading()).toBe(false); }); }); describe('setSelectedTask', () => { it('should set the selected task', () => { const task = new TaskBuilder().withId('task-1').build(); store.setSelectedTask(task); expect(store.selectedTask()).toEqual(task); }); }); describe('createTask', () => { it('should add new task to tasks list', () => { const existing = TaskBuilder.buildMany(2); const newTask = new TaskBuilder().withId('task-3').withTitle('New').build(); vi.mocked(taskService.getAll).mockReturnValue(of({ data: existing, pagination: { page: 1, pageSize: 10, total: 2, totalPages: 1 } })); vi.mocked(taskService.create).mockReturnValue(of(newTask)); store.loadTasks(); store.createTask({ title: 'New' }); expect(store.tasks()).toHaveLength(3); expect(store.tasks()).toContainEqual(newTask); expect(store.loading()).toBe(false); }); it('should set error when creation fails', () => { vi.mocked(taskService.create).mockReturnValue(throwError(() => new Error('Create failed'))); store.createTask({ title: 'Fail' }); expect(store.error()).toBe('Create failed'); expect(store.loading()).toBe(false); }); }); describe('updateTask', () => { it('should update existing task in list', () => { const original = TaskBuilder.buildMany(2); const updated = new TaskBuilder().withId('task-1').withTitle('Updated').build(); vi.mocked(taskService.getAll).mockReturnValue(of({ data: original, pagination: { page: 1, pageSize: 10, total: 2, totalPages: 1 } })); vi.mocked(taskService.update).mockReturnValue(of(updated)); store.loadTasks(); store.updateTask('task-1', { title: 'Updated' }); expect(store.tasks().find(t => t.id === 'task-1')?.title).toBe('Updated'); expect(store.loading()).toBe(false); }); it('should update selected task if it matches', () => { const task = new TaskBuilder().withId('task-1').build(); const updated = new TaskBuilder().withId('task-1').withTitle('Updated').build(); vi.mocked(taskService.getById).mockReturnValue(of(task)); vi.mocked(taskService.update).mockReturnValue(of(updated)); store.loadTask('task-1'); store.updateTask('task-1', { title: 'Updated' }); expect(store.selectedTask()?.title).toBe('Updated'); }); it('should set error when update fails', () => { vi.mocked(taskService.update).mockReturnValue(throwError(() => new Error('Update failed'))); store.updateTask('task-1', { title: 'Fail' }); expect(store.error()).toBe('Update failed'); expect(store.loading()).toBe(false); }); }); describe('deleteTask', () => { it('should remove task from list', () => { const tasks = TaskBuilder.buildMany(3); vi.mocked(taskService.getAll).mockReturnValue(of({ data: tasks, pagination: { page: 1, pageSize: 10, total: 3, totalPages: 1 } })); vi.mocked(taskService.delete).mockReturnValue(of(undefined)); store.loadTasks(); store.deleteTask('task-1'); expect(store.tasks()).toHaveLength(2); expect(store.tasks().find(t => t.id === 'task-1')).toBeUndefined(); }); it('should clear selected task if deleted task was selected', () => { const task = new TaskBuilder().withId('task-1').build(); vi.mocked(taskService.getById).mockReturnValue(of(task)); vi.mocked(taskService.delete).mockReturnValue(of(undefined)); store.loadTask('task-1'); store.deleteTask('task-1'); expect(store.selectedTask()).toBeNull(); }); it('should set error when deletion fails', () => { vi.mocked(taskService.delete).mockReturnValue(throwError(() => new Error('Delete failed'))); store.deleteTask('task-1'); expect(store.error()).toBe('Delete failed'); expect(store.loading()).toBe(false); }); }); describe('transitionTask', () => { it('should update task state in list', () => { const tasks = TaskBuilder.buildMany(2, (builder, i) => { if (i === 0) builder.withState('new'); }); const transitioned = new TaskBuilder().withId('task-1').withState('active').build(); vi.mocked(taskService.getAll).mockReturnValue(of({ data: tasks, pagination: { page: 1, pageSize: 10, total: 2, totalPages: 1 } })); vi.mocked(taskService.transition).mockReturnValue(of(transitioned)); store.loadTasks(); store.transitionTask('task-1', 'active'); expect(store.tasks().find(t => t.id === 'task-1')?.stateHistory[0].state).toBe('active'); }); it('should rollback task when transition fails', () => { const tasks = TaskBuilder.buildMany(1, (builder) => { builder.withId('task-1').withState('new'); }); vi.mocked(taskService.getAll).mockReturnValue(of({ data: tasks, pagination: { page: 1, pageSize: 10, total: 1, totalPages: 1 } })); vi.mocked(taskService.transition).mockReturnValue(throwError(() => new Error('Transition failed'))); store.loadTasks(); store.transitionTask('task-1', 'active'); expect(store.tasks().find(t => t.id === 'task-1')?.stateHistory.at(-1)?.state).toBe('new'); expect(store.transitioningState()).toBeNull(); }); }); describe('addNote', () => { it('should update task with new note', () => { const tasks = [new TaskBuilder().withId('task-1').withNotes([]).build()]; const updated = new TaskBuilder().withId('task-1').withNotes(['New note']).build(); vi.mocked(taskService.getAll).mockReturnValue(of({ data: tasks, pagination: { page: 1, pageSize: 10, total: 1, totalPages: 1 } })); vi.mocked(taskService.addNote).mockReturnValue(of(updated)); store.loadTasks(); store.addNote('task-1', 'New note'); expect(store.tasks().find(t => t.id === 'task-1')?.notes).toContain('New note'); }); it('should set error when adding note fails', () => { vi.mocked(taskService.addNote).mockReturnValue(throwError(() => new Error('Note failed'))); store.addNote('task-1', 'Note'); expect(store.error()).toBe('Note failed'); }); }); describe('deleteNote', () => { it('should remove note from task', () => { const tasks = [new TaskBuilder().withId('task-1').withNotes(['Note 1', 'Note 2']).build()]; const updated = new TaskBuilder().withId('task-1').withNotes(['Note 2']).build(); vi.mocked(taskService.getAll).mockReturnValue(of({ data: tasks, pagination: { page: 1, pageSize: 10, total: 1, totalPages: 1 } })); vi.mocked(taskService.deleteNote).mockReturnValue(of(updated)); store.loadTasks(); store.deleteNote('task-1', 0); expect(store.tasks().find(t => t.id === 'task-1')?.notes).toEqual(['Note 2']); }); }); describe('filteredTasks', () => { it('should filter tasks by state', () => { const tasks = [ new TaskBuilder().withId('task-1').withState('active').build(), new TaskBuilder().withId('task-2').withState('new').build(), new TaskBuilder().withId('task-3').withState('active').build(), ]; vi.mocked(taskService.getAll).mockReturnValue(of({ data: tasks, pagination: { page: 1, pageSize: 10, total: 3, totalPages: 1 } })); store.loadTasks(); store.updateFilter({ state: 'active' }); expect(store.filteredTasks()).toHaveLength(2); }); it('should filter tasks by search term', () => { const tasks = [ new TaskBuilder().withId('task-1').withTitle('Angular task').build(), new TaskBuilder().withId('task-2').withTitle('React task').build(), new TaskBuilder().withId('task-3').withTitle('Angular component').build(), ]; vi.mocked(taskService.getAll).mockReturnValue(of({ data: tasks, pagination: { page: 1, pageSize: 10, total: 3, totalPages: 1 } })); store.loadTasks(); store.updateFilter({ search: 'angular' }); expect(store.filteredTasks()).toHaveLength(2); }); it('should return all tasks when filter is all', () => { const tasks = TaskBuilder.buildMany(5); vi.mocked(taskService.getAll).mockReturnValue(of({ data: tasks, pagination: { page: 1, pageSize: 10, total: 5, totalPages: 1 } })); store.loadTasks(); expect(store.filteredTasks()).toHaveLength(5); }); }); describe('updateFilter', () => { it('should merge partial filter with existing filter', () => { store.updateFilter({ state: 'active' }); expect(store.filter().state).toBe('active'); expect(store.filter().search).toBe(''); }); }); describe('updateSort', () => { it('should update sort criteria', () => { store.updateSort({ field: 'title', direction: 'desc' }); expect(store.sort().field).toBe('title'); expect(store.sort().direction).toBe('desc'); }); }); describe('clearError', () => { it('should set error to null', () => { vi.mocked(taskService.getAll).mockReturnValue(throwError(() => new Error('Error'))); store.loadTasks(); expect(store.error()).toBe('Error'); store.clearError(); expect(store.error()).toBeNull(); }); }); describe('reset', () => { it('should reset all signals to default values', () => { const tasks = TaskBuilder.buildMany(3); vi.mocked(taskService.getAll).mockReturnValue(of({ data: tasks, pagination: { page: 1, pageSize: 10, total: 3, totalPages: 1 } })); store.loadTasks(); store.updateFilter({ state: 'active', search: 'test' }); store.reset(); expect(store.tasks()).toEqual([]); expect(store.selectedTask()).toBeNull(); expect(store.filter()).toEqual({ state: 'all', search: '' }); expect(store.sort()).toEqual({ field: 'dueDate', direction: 'asc' }); expect(store.loading()).toBe(false); expect(store.error()).toBeNull(); expect(store.pagination()).toBeNull(); }); }); });