Add unit tests for API client, stores, and utils
This commit is contained in:
286
src/lib/__tests__/api.test.ts
Normal file
286
src/lib/__tests__/api.test.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// We need to test the ApiClient class, so we import the module fresh
|
||||
// The api.ts uses import.meta.env.PROD which is false in test
|
||||
// So API_BASE = '/api' and AUTH_BASE = ''
|
||||
|
||||
let ApiClient: any;
|
||||
let api: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
vi.stubGlobal('fetch', vi.fn());
|
||||
const mod = await import('@/lib/api');
|
||||
api = mod.api;
|
||||
// Get the class from the singleton
|
||||
ApiClient = (api as any).constructor;
|
||||
});
|
||||
|
||||
function mockFetchResponse(data: any, ok = true, status = 200) {
|
||||
(fetch as any).mockResolvedValueOnce({
|
||||
ok,
|
||||
status,
|
||||
json: () => Promise.resolve(data),
|
||||
});
|
||||
}
|
||||
|
||||
describe('ApiClient', () => {
|
||||
describe('login', () => {
|
||||
it('sends login request and returns data on success', async () => {
|
||||
const responseData = { token: 'abc123', user: { id: '1', email: 'test@test.com' } };
|
||||
mockFetchResponse(responseData);
|
||||
|
||||
const result = await api.login('test@test.com', 'password');
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith('/api/auth/sign-in/email', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: 'test@test.com', password: 'password' }),
|
||||
credentials: 'include',
|
||||
});
|
||||
expect(result).toEqual(responseData);
|
||||
});
|
||||
|
||||
it('throws on failed login', async () => {
|
||||
(fetch as any).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ message: 'Invalid credentials' }),
|
||||
});
|
||||
|
||||
await expect(api.login('bad@test.com', 'wrong')).rejects.toThrow('Invalid credentials');
|
||||
});
|
||||
});
|
||||
|
||||
describe('logout', () => {
|
||||
it('sends POST to sign-out', async () => {
|
||||
mockFetchResponse({});
|
||||
|
||||
await api.logout();
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith('/api/auth/sign-out', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSession', () => {
|
||||
it('returns session data when authenticated', async () => {
|
||||
const session = { user: { id: '1', email: 'test@test.com', name: 'Test' } };
|
||||
mockFetchResponse(session);
|
||||
|
||||
const result = await api.getSession();
|
||||
|
||||
expect(result).toEqual(session);
|
||||
expect(fetch).toHaveBeenCalledWith('/api/auth/get-session', {
|
||||
credentials: 'include',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null when not authenticated', async () => {
|
||||
(fetch as any).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
json: () => Promise.resolve({}),
|
||||
});
|
||||
|
||||
const result = await api.getSession();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null on network error', async () => {
|
||||
(fetch as any).mockRejectedValueOnce(new Error('Network error'));
|
||||
|
||||
const result = await api.getSession();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Projects', () => {
|
||||
it('getProjects fetches project list', async () => {
|
||||
const projects = [{ id: '1', name: 'Project 1' }];
|
||||
mockFetchResponse(projects);
|
||||
|
||||
const result = await api.getProjects();
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith('/api/projects', expect.objectContaining({
|
||||
headers: expect.objectContaining({ 'Content-Type': 'application/json' }),
|
||||
credentials: 'include',
|
||||
}));
|
||||
expect(result).toEqual(projects);
|
||||
});
|
||||
|
||||
it('createProject sends POST with data', async () => {
|
||||
const project = { id: '1', name: 'New Project', color: '#ff0000' };
|
||||
mockFetchResponse(project);
|
||||
|
||||
const result = await api.createProject({ name: 'New Project', color: '#ff0000' });
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith('/api/projects', expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name: 'New Project', color: '#ff0000' }),
|
||||
}));
|
||||
expect(result).toEqual(project);
|
||||
});
|
||||
|
||||
it('updateProject sends PATCH', async () => {
|
||||
const updated = { id: '1', name: 'Updated' };
|
||||
mockFetchResponse(updated);
|
||||
|
||||
const result = await api.updateProject('1', { name: 'Updated' });
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith('/api/projects/1', expect.objectContaining({
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ name: 'Updated' }),
|
||||
}));
|
||||
expect(result).toEqual(updated);
|
||||
});
|
||||
|
||||
it('deleteProject sends DELETE', async () => {
|
||||
mockFetchResponse(undefined);
|
||||
|
||||
await api.deleteProject('1');
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith('/api/projects/1', expect.objectContaining({
|
||||
method: 'DELETE',
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tasks', () => {
|
||||
it('getTasks fetches tasks without params', async () => {
|
||||
const tasks = [{ id: '1', title: 'Task 1' }];
|
||||
mockFetchResponse(tasks);
|
||||
|
||||
const result = await api.getTasks();
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith('/api/tasks', expect.objectContaining({
|
||||
credentials: 'include',
|
||||
}));
|
||||
expect(result).toEqual(tasks);
|
||||
});
|
||||
|
||||
it('getTasks includes query params', async () => {
|
||||
const tasks = [{ id: '1', title: 'Task 1' }];
|
||||
mockFetchResponse(tasks);
|
||||
|
||||
await api.getTasks({ projectId: 'proj1', completed: false, today: true });
|
||||
|
||||
const calledUrl = (fetch as any).mock.calls[0][0] as string;
|
||||
expect(calledUrl).toContain('/api/tasks?');
|
||||
expect(calledUrl).toContain('projectId=proj1');
|
||||
expect(calledUrl).toContain('completed=false');
|
||||
expect(calledUrl).toContain('today=true');
|
||||
});
|
||||
|
||||
it('createTask sends POST', async () => {
|
||||
const task = { id: '1', title: 'New Task' };
|
||||
mockFetchResponse(task);
|
||||
|
||||
const result = await api.createTask({ title: 'New Task' });
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith('/api/tasks', expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ title: 'New Task' }),
|
||||
}));
|
||||
expect(result).toEqual(task);
|
||||
});
|
||||
|
||||
it('updateTask sends PATCH', async () => {
|
||||
const updated = { id: '1', title: 'Updated Task' };
|
||||
mockFetchResponse(updated);
|
||||
|
||||
const result = await api.updateTask('1', { title: 'Updated Task' });
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith('/api/tasks/1', expect.objectContaining({
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ title: 'Updated Task' }),
|
||||
}));
|
||||
expect(result).toEqual(updated);
|
||||
});
|
||||
|
||||
it('deleteTask sends DELETE', async () => {
|
||||
mockFetchResponse(undefined);
|
||||
|
||||
await api.deleteTask('1');
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith('/api/tasks/1', expect.objectContaining({
|
||||
method: 'DELETE',
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Labels', () => {
|
||||
it('getLabels fetches label list', async () => {
|
||||
const labels = [{ id: '1', name: 'Bug' }];
|
||||
mockFetchResponse(labels);
|
||||
|
||||
const result = await api.getLabels();
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith('/api/labels', expect.objectContaining({
|
||||
credentials: 'include',
|
||||
}));
|
||||
expect(result).toEqual(labels);
|
||||
});
|
||||
|
||||
it('createLabel sends POST', async () => {
|
||||
const label = { id: '1', name: 'Feature', color: '#00ff00' };
|
||||
mockFetchResponse(label);
|
||||
|
||||
const result = await api.createLabel({ name: 'Feature', color: '#00ff00' });
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith('/api/labels', expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name: 'Feature', color: '#00ff00' }),
|
||||
}));
|
||||
expect(result).toEqual(label);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Auth header', () => {
|
||||
it('sends Authorization header when token is set', async () => {
|
||||
api.setToken('my-token');
|
||||
mockFetchResponse([]);
|
||||
|
||||
await api.getProjects();
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith('/api/projects', expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
'Authorization': 'Bearer my-token',
|
||||
}),
|
||||
}));
|
||||
|
||||
// Clean up
|
||||
api.setToken(null);
|
||||
});
|
||||
|
||||
it('does not send Authorization header when token is null', async () => {
|
||||
api.setToken(null);
|
||||
mockFetchResponse([]);
|
||||
|
||||
await api.getProjects();
|
||||
|
||||
const headers = (fetch as any).mock.calls[0][1].headers;
|
||||
expect(headers['Authorization']).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('throws error with message from non-200 response', async () => {
|
||||
(fetch as any).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ error: 'Not found' }),
|
||||
});
|
||||
|
||||
await expect(api.getProjects()).rejects.toThrow('Not found');
|
||||
});
|
||||
|
||||
it('throws "Request failed" when error body is unparseable', async () => {
|
||||
(fetch as any).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
json: () => Promise.reject(new Error('parse error')),
|
||||
});
|
||||
|
||||
await expect(api.getProjects()).rejects.toThrow('Unknown error');
|
||||
});
|
||||
});
|
||||
});
|
||||
115
src/lib/__tests__/utils.test.ts
Normal file
115
src/lib/__tests__/utils.test.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { formatDate, isOverdue, getPriorityColor, getPriorityLabel } from '@/lib/utils';
|
||||
|
||||
describe('formatDate', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(2025, 5, 15, 12, 0, 0)); // June 15, 2025
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('returns empty string for undefined', () => {
|
||||
expect(formatDate(undefined)).toBe('');
|
||||
});
|
||||
|
||||
it('returns "Today" for today\'s date', () => {
|
||||
expect(formatDate('2025-06-15')).toBe('Today');
|
||||
});
|
||||
|
||||
it('returns "Tomorrow" for tomorrow\'s date', () => {
|
||||
expect(formatDate('2025-06-16')).toBe('Tomorrow');
|
||||
});
|
||||
|
||||
it('returns "Yesterday" for yesterday\'s date', () => {
|
||||
expect(formatDate('2025-06-14')).toBe('Yesterday');
|
||||
});
|
||||
|
||||
it('returns weekday name for dates within this week', () => {
|
||||
// June 18 is Wednesday, within 7 days from June 15
|
||||
const result = formatDate('2025-06-18');
|
||||
expect(result).toBe('Wednesday');
|
||||
});
|
||||
|
||||
it('returns "Mon DD" for same-year dates beyond this week', () => {
|
||||
const result = formatDate('2025-08-20');
|
||||
expect(result).toBe('Aug 20');
|
||||
});
|
||||
|
||||
it('returns "Mon DD, YYYY" for different-year dates', () => {
|
||||
const result = formatDate('2024-03-10');
|
||||
expect(result).toBe('Mar 10, 2024');
|
||||
});
|
||||
|
||||
it('accepts Date objects', () => {
|
||||
expect(formatDate(new Date(2025, 5, 15))).toBe('Today');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isOverdue', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(2025, 5, 15, 12, 0, 0));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('returns false for undefined', () => {
|
||||
expect(isOverdue(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true for past dates', () => {
|
||||
expect(isOverdue('2025-06-10')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for future dates', () => {
|
||||
expect(isOverdue('2025-06-20')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for today', () => {
|
||||
// Today at midnight is not < today at midnight
|
||||
expect(isOverdue('2025-06-15')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPriorityColor', () => {
|
||||
it('returns red for p1', () => {
|
||||
expect(getPriorityColor('p1')).toBe('#ef4444');
|
||||
});
|
||||
|
||||
it('returns amber for p2', () => {
|
||||
expect(getPriorityColor('p2')).toBe('#f59e0b');
|
||||
});
|
||||
|
||||
it('returns blue for p3', () => {
|
||||
expect(getPriorityColor('p3')).toBe('#3b82f6');
|
||||
});
|
||||
|
||||
it('returns gray for p4 or unknown', () => {
|
||||
expect(getPriorityColor('p4')).toBe('#6b7280');
|
||||
expect(getPriorityColor('unknown')).toBe('#6b7280');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPriorityLabel', () => {
|
||||
it('returns "Priority 1" for p1', () => {
|
||||
expect(getPriorityLabel('p1')).toBe('Priority 1');
|
||||
});
|
||||
|
||||
it('returns "Priority 2" for p2', () => {
|
||||
expect(getPriorityLabel('p2')).toBe('Priority 2');
|
||||
});
|
||||
|
||||
it('returns "Priority 3" for p3', () => {
|
||||
expect(getPriorityLabel('p3')).toBe('Priority 3');
|
||||
});
|
||||
|
||||
it('returns "Priority 4" for p4 or unknown', () => {
|
||||
expect(getPriorityLabel('p4')).toBe('Priority 4');
|
||||
expect(getPriorityLabel('unknown')).toBe('Priority 4');
|
||||
});
|
||||
});
|
||||
104
src/stores/__tests__/auth.test.ts
Normal file
104
src/stores/__tests__/auth.test.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { api } from '@/lib/api';
|
||||
|
||||
vi.mock('@/lib/api', () => ({
|
||||
api: {
|
||||
login: vi.fn(),
|
||||
logout: vi.fn(),
|
||||
getSession: vi.fn(),
|
||||
setToken: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockApi = vi.mocked(api);
|
||||
|
||||
describe('useAuthStore', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Reset store state
|
||||
useAuthStore.setState({
|
||||
user: null,
|
||||
isLoading: true,
|
||||
isAuthenticated: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('has correct initial state', () => {
|
||||
const state = useAuthStore.getState();
|
||||
expect(state.user).toBeNull();
|
||||
expect(state.isAuthenticated).toBe(false);
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
it('sets user and isAuthenticated on success', async () => {
|
||||
const user = { id: '1', email: 'test@test.com', name: 'Test', role: 'user' as const, createdAt: '2025-01-01' };
|
||||
mockApi.login.mockResolvedValueOnce({ token: 'abc' });
|
||||
mockApi.getSession.mockResolvedValueOnce({ user });
|
||||
|
||||
await useAuthStore.getState().login('test@test.com', 'password');
|
||||
|
||||
const state = useAuthStore.getState();
|
||||
expect(state.user).toEqual(user);
|
||||
expect(state.isAuthenticated).toBe(true);
|
||||
expect(state.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
it('throws on failure', async () => {
|
||||
mockApi.login.mockRejectedValueOnce(new Error('Invalid credentials'));
|
||||
|
||||
await expect(useAuthStore.getState().login('bad@test.com', 'wrong'))
|
||||
.rejects.toThrow('Invalid credentials');
|
||||
|
||||
const state = useAuthStore.getState();
|
||||
expect(state.user).toBeNull();
|
||||
expect(state.isAuthenticated).toBe(false);
|
||||
expect(state.isLoading).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logout', () => {
|
||||
it('clears user and sets isAuthenticated to false', async () => {
|
||||
useAuthStore.setState({
|
||||
user: { id: '1', email: 'test@test.com', name: 'Test', role: 'user', createdAt: '2025-01-01' },
|
||||
isAuthenticated: true,
|
||||
});
|
||||
mockApi.logout.mockResolvedValueOnce(undefined);
|
||||
|
||||
await useAuthStore.getState().logout();
|
||||
|
||||
const state = useAuthStore.getState();
|
||||
expect(state.user).toBeNull();
|
||||
expect(state.isAuthenticated).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkSession', () => {
|
||||
it('sets user when session exists', async () => {
|
||||
const user = { id: '1', email: 'test@test.com', name: 'Test', role: 'user' as const, createdAt: '2025-01-01' };
|
||||
mockApi.getSession.mockResolvedValueOnce({ user });
|
||||
|
||||
await useAuthStore.getState().checkSession();
|
||||
|
||||
const state = useAuthStore.getState();
|
||||
expect(state.user).toEqual(user);
|
||||
expect(state.isAuthenticated).toBe(true);
|
||||
expect(state.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
it('clears user when no session', async () => {
|
||||
useAuthStore.setState({
|
||||
user: { id: '1', email: 'test@test.com', name: 'Test', role: 'user', createdAt: '2025-01-01' },
|
||||
isAuthenticated: true,
|
||||
});
|
||||
mockApi.getSession.mockResolvedValueOnce(null);
|
||||
|
||||
await useAuthStore.getState().checkSession();
|
||||
|
||||
const state = useAuthStore.getState();
|
||||
expect(state.user).toBeNull();
|
||||
expect(state.isAuthenticated).toBe(false);
|
||||
expect(state.isLoading).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
188
src/stores/__tests__/tasks.test.ts
Normal file
188
src/stores/__tests__/tasks.test.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { useTasksStore } from '@/stores/tasks';
|
||||
import { api } from '@/lib/api';
|
||||
import type { Task, Project, Label } from '@/types';
|
||||
|
||||
vi.mock('@/lib/api', () => ({
|
||||
api: {
|
||||
getTasks: vi.fn(),
|
||||
getProjects: vi.fn(),
|
||||
getLabels: vi.fn(),
|
||||
createTask: vi.fn(),
|
||||
updateTask: vi.fn(),
|
||||
deleteTask: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockApi = vi.mocked(api);
|
||||
|
||||
const makeTask = (overrides: Partial<Task> = {}): Task => ({
|
||||
id: '1',
|
||||
userId: 'u1',
|
||||
projectId: 'p1',
|
||||
title: 'Test Task',
|
||||
priority: 'p4',
|
||||
isCompleted: false,
|
||||
sortOrder: 0,
|
||||
createdAt: '2025-01-01',
|
||||
updatedAt: '2025-01-01',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const makeProject = (overrides: Partial<Project> = {}): Project => ({
|
||||
id: 'p1',
|
||||
userId: 'u1',
|
||||
name: 'Project 1',
|
||||
color: '#000',
|
||||
isInbox: false,
|
||||
isFavorite: false,
|
||||
isArchived: false,
|
||||
sortOrder: 0,
|
||||
createdAt: '2025-01-01',
|
||||
updatedAt: '2025-01-01',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const makeLabel = (overrides: Partial<Label> = {}): Label => ({
|
||||
id: 'l1',
|
||||
userId: 'u1',
|
||||
name: 'Bug',
|
||||
color: '#ff0000',
|
||||
isFavorite: false,
|
||||
sortOrder: 0,
|
||||
createdAt: '2025-01-01',
|
||||
updatedAt: '2025-01-01',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('useTasksStore', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useTasksStore.setState({
|
||||
tasks: [],
|
||||
projects: [],
|
||||
labels: [],
|
||||
isLoading: false,
|
||||
selectedTask: null,
|
||||
activeProjectId: null,
|
||||
activeView: 'inbox',
|
||||
});
|
||||
});
|
||||
|
||||
it('has correct initial state', () => {
|
||||
const state = useTasksStore.getState();
|
||||
expect(state.tasks).toEqual([]);
|
||||
expect(state.projects).toEqual([]);
|
||||
expect(state.labels).toEqual([]);
|
||||
expect(state.isLoading).toBe(false);
|
||||
expect(state.selectedTask).toBeNull();
|
||||
expect(state.activeProjectId).toBeNull();
|
||||
expect(state.activeView).toBe('inbox');
|
||||
});
|
||||
|
||||
describe('fetchTasks', () => {
|
||||
it('populates tasks', async () => {
|
||||
const tasks = [makeTask({ id: '1' }), makeTask({ id: '2', title: 'Task 2' })];
|
||||
mockApi.getTasks.mockResolvedValueOnce(tasks);
|
||||
|
||||
await useTasksStore.getState().fetchTasks();
|
||||
|
||||
expect(useTasksStore.getState().tasks).toEqual(tasks);
|
||||
expect(useTasksStore.getState().isLoading).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchProjects', () => {
|
||||
it('populates projects', async () => {
|
||||
const projects = [makeProject()];
|
||||
mockApi.getProjects.mockResolvedValueOnce(projects);
|
||||
|
||||
await useTasksStore.getState().fetchProjects();
|
||||
|
||||
expect(useTasksStore.getState().projects).toEqual(projects);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchLabels', () => {
|
||||
it('populates labels', async () => {
|
||||
const labels = [makeLabel()];
|
||||
mockApi.getLabels.mockResolvedValueOnce(labels);
|
||||
|
||||
await useTasksStore.getState().fetchLabels();
|
||||
|
||||
expect(useTasksStore.getState().labels).toEqual(labels);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createTask', () => {
|
||||
it('adds task to list', async () => {
|
||||
const newTask = makeTask({ id: '2', title: 'New Task' });
|
||||
mockApi.createTask.mockResolvedValueOnce(newTask);
|
||||
|
||||
const result = await useTasksStore.getState().createTask({ title: 'New Task' });
|
||||
|
||||
expect(result).toEqual(newTask);
|
||||
expect(useTasksStore.getState().tasks).toContainEqual(newTask);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTask', () => {
|
||||
it('updates task in list', async () => {
|
||||
const task = makeTask({ id: '1', title: 'Original' });
|
||||
useTasksStore.setState({ tasks: [task] });
|
||||
|
||||
const updated = { ...task, title: 'Updated' };
|
||||
mockApi.updateTask.mockResolvedValueOnce(updated);
|
||||
|
||||
await useTasksStore.getState().updateTask('1', { title: 'Updated' });
|
||||
|
||||
expect(useTasksStore.getState().tasks[0].title).toBe('Updated');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteTask', () => {
|
||||
it('removes task from list', async () => {
|
||||
const task = makeTask({ id: '1' });
|
||||
useTasksStore.setState({ tasks: [task] });
|
||||
mockApi.deleteTask.mockResolvedValueOnce(undefined);
|
||||
|
||||
await useTasksStore.getState().deleteTask('1');
|
||||
|
||||
expect(useTasksStore.getState().tasks).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleComplete', () => {
|
||||
it('flips isCompleted', async () => {
|
||||
const task = makeTask({ id: '1', isCompleted: false });
|
||||
useTasksStore.setState({ tasks: [task] });
|
||||
mockApi.updateTask.mockResolvedValueOnce({ ...task, isCompleted: true, completedAt: '2025-06-15' });
|
||||
|
||||
await useTasksStore.getState().toggleComplete('1');
|
||||
|
||||
expect(useTasksStore.getState().tasks[0].isCompleted).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setSelectedTask', () => {
|
||||
it('sets selected task', () => {
|
||||
const task = makeTask();
|
||||
useTasksStore.getState().setSelectedTask(task);
|
||||
expect(useTasksStore.getState().selectedTask).toEqual(task);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setActiveProject', () => {
|
||||
it('sets active project id', () => {
|
||||
useTasksStore.getState().setActiveProject('p1');
|
||||
expect(useTasksStore.getState().activeProjectId).toBe('p1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setActiveView', () => {
|
||||
it('sets active view', () => {
|
||||
useTasksStore.getState().setActiveView('today');
|
||||
expect(useTasksStore.getState().activeView).toBe('today');
|
||||
});
|
||||
});
|
||||
});
|
||||
1
src/test/setup.ts
Normal file
1
src/test/setup.ts
Normal file
@@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom';
|
||||
Reference in New Issue
Block a user