emi-challenge-fe/tests/app/features/tasks/data-access/store/task-store.spec.ts

346 lines
12 KiB
TypeScript

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();
});
});
});