test(tasks): ✅ Add test cases for SubtasksInline component and useTasks hook, covering subtask interactions, state updates, and edge cases
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
8333a7a49f
commit
5cae224169
2 changed files with 208 additions and 2 deletions
|
|
@ -11,13 +11,13 @@ import { SubtasksInline } from '../SubtasksInline';
|
|||
|
||||
const mockMutate = vi.fn();
|
||||
|
||||
vi.mock('@features/tasks/frontend/useTasks', () => ({
|
||||
vi.mock('@projects/productivity/tasks/frontend/useTasks', () => ({
|
||||
useTask: vi.fn(),
|
||||
useUpdateTaskStatus: vi.fn(() => ({ mutate: mockMutate })),
|
||||
}));
|
||||
|
||||
// Access the mocked hooks so we can control return values per test
|
||||
import { useTask } from '@features/tasks/frontend/useTasks';
|
||||
import { useTask } from '@projects/productivity/tasks/frontend/useTasks';
|
||||
const mockUseTask = vi.mocked(useTask);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
206
productivity/tasks/frontend/__tests__/useTasks.test.ts
Normal file
206
productivity/tasks/frontend/__tests__/useTasks.test.ts
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { server } from '@/mocks/server';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { type ReactNode, createElement } from 'react';
|
||||
import { useTasks, useCreateTask } from '../useTasks';
|
||||
|
||||
function createWrapper() {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, gcTime: 0 },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
return function Wrapper({ children }: { children: ReactNode }) {
|
||||
return createElement(QueryClientProvider, { client: queryClient }, children);
|
||||
};
|
||||
}
|
||||
|
||||
const MOCK_TASKS = {
|
||||
data: [
|
||||
{
|
||||
id: 'task-1',
|
||||
projectId: 'proj-1',
|
||||
domainId: 'dom-1',
|
||||
goalId: null,
|
||||
sprintId: null,
|
||||
parentId: null,
|
||||
title: 'Write unit tests',
|
||||
description: 'Cover the useTasks hook',
|
||||
status: 'todo',
|
||||
priority: 'medium',
|
||||
energyLevel: 'medium',
|
||||
estimatedMinutes: 30,
|
||||
actualMinutes: null,
|
||||
dueDate: null,
|
||||
scheduledDate: null,
|
||||
isQuickWin: false,
|
||||
recurrenceRule: null,
|
||||
tags: [],
|
||||
sortOrder: 0,
|
||||
createdAt: '2026-01-01T00:00:00Z',
|
||||
updatedAt: '2026-01-01T00:00:00Z',
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
id: 'task-2',
|
||||
projectId: 'proj-1',
|
||||
domainId: 'dom-1',
|
||||
goalId: null,
|
||||
sprintId: null,
|
||||
parentId: null,
|
||||
title: 'Review PR',
|
||||
description: null,
|
||||
status: 'in_progress',
|
||||
priority: 'high',
|
||||
energyLevel: 'high',
|
||||
estimatedMinutes: 15,
|
||||
actualMinutes: null,
|
||||
dueDate: null,
|
||||
scheduledDate: null,
|
||||
isQuickWin: true,
|
||||
recurrenceRule: null,
|
||||
tags: ['review'],
|
||||
sortOrder: 1,
|
||||
createdAt: '2026-01-02T00:00:00Z',
|
||||
updatedAt: '2026-01-02T00:00:00Z',
|
||||
deletedAt: null,
|
||||
},
|
||||
],
|
||||
meta: { page: 1, limit: 20, total: 2, totalPages: 1 },
|
||||
};
|
||||
|
||||
describe('useTasks', () => {
|
||||
beforeEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it('fetches paginated tasks on success', async () => {
|
||||
server.use(
|
||||
http.get('*/tasks', () => {
|
||||
return HttpResponse.json(MOCK_TASKS);
|
||||
}),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useTasks(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
expect(result.current.isLoading).toBe(true);
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(result.current.data).toEqual(MOCK_TASKS);
|
||||
expect(result.current.data!.data).toHaveLength(2);
|
||||
expect(result.current.data!.meta.total).toBe(2);
|
||||
});
|
||||
|
||||
it('passes filter params to the API', async () => {
|
||||
let capturedUrl = '';
|
||||
server.use(
|
||||
http.get('*/tasks', ({ request }) => {
|
||||
capturedUrl = request.url;
|
||||
return HttpResponse.json({
|
||||
data: [MOCK_TASKS.data[0]],
|
||||
meta: { page: 1, limit: 20, total: 1, totalPages: 1 },
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
const filters = { status: 'todo' as const };
|
||||
const { result } = renderHook(() => useTasks(filters), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(capturedUrl).toContain('status=todo');
|
||||
expect(result.current.data!.data).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('reports loading state before data arrives', () => {
|
||||
server.use(
|
||||
http.get('*/tasks', () => {
|
||||
return HttpResponse.json(MOCK_TASKS);
|
||||
}),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useTasks(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
expect(result.current.isLoading).toBe(true);
|
||||
expect(result.current.data).toBeUndefined();
|
||||
});
|
||||
|
||||
it('sets isError when the endpoint returns 500', async () => {
|
||||
server.use(
|
||||
http.get('*/tasks', () => {
|
||||
return new HttpResponse(null, { status: 500 });
|
||||
}),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useTasks(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true), {
|
||||
timeout: 5000,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCreateTask', () => {
|
||||
beforeEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it('creates a task and returns the new entity', async () => {
|
||||
const newTask = { ...MOCK_TASKS.data[0], id: 'task-new', title: 'New task' };
|
||||
|
||||
server.use(
|
||||
http.post('*/tasks', () => {
|
||||
return HttpResponse.json(newTask);
|
||||
}),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useCreateTask(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate({
|
||||
projectId: 'proj-1',
|
||||
domainId: 'dom-1',
|
||||
title: 'New task',
|
||||
} as Parameters<typeof result.current.mutate>[0]);
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(result.current.data).toEqual(newTask);
|
||||
});
|
||||
|
||||
it('sets isError when creation fails', async () => {
|
||||
server.use(
|
||||
http.post('*/tasks', () => {
|
||||
return new HttpResponse(null, { status: 422 });
|
||||
}),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useCreateTask(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate({
|
||||
projectId: 'proj-1',
|
||||
domainId: 'dom-1',
|
||||
title: 'Bad task',
|
||||
} as Parameters<typeof result.current.mutate>[0]);
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true), {
|
||||
timeout: 5000,
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue