346 lines
12 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|