272 lines
6.9 KiB
TypeScript
272 lines
6.9 KiB
TypeScript
import { renderHook, waitFor, act } from '@testing-library/react';
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { useCreateProject, useDeleteProject } from './use-project-mutations';
|
|
import type { Project } from '@/api/types/projects';
|
|
import type { CreateProjectRequest } from '@/api/types/projects';
|
|
|
|
vi.mock('@/hooks/ui/use-toast', () => ({
|
|
useToast: () => ({
|
|
toast: vi.fn(),
|
|
}),
|
|
}));
|
|
|
|
const mockAPI = {
|
|
createProject: vi.fn(),
|
|
deleteProject: vi.fn(),
|
|
};
|
|
|
|
vi.mock('@/api/interface', () => ({
|
|
getAPI: () => mockAPI,
|
|
}));
|
|
|
|
const createMockProject = (overrides?: Partial<Project>): Project => ({
|
|
id: 'project-123',
|
|
workspace_id: 'workspace-456',
|
|
name: 'Test Project',
|
|
description: 'Test description',
|
|
is_default: false,
|
|
is_archived: false,
|
|
created_at: Date.now() / 1000,
|
|
updated_at: Date.now() / 1000,
|
|
...overrides,
|
|
});
|
|
|
|
describe('useCreateProject', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('should call API with correct request', async () => {
|
|
const { result } = renderHook(() => useCreateProject());
|
|
|
|
const request: CreateProjectRequest = {
|
|
workspace_id: 'w1',
|
|
name: 'New Project',
|
|
description: 'A new project',
|
|
};
|
|
|
|
const project = createMockProject({
|
|
id: 'p1',
|
|
...request,
|
|
});
|
|
|
|
mockAPI.createProject.mockResolvedValue(project);
|
|
|
|
await result.current.mutate(request);
|
|
|
|
await waitFor(() => {
|
|
expect(mockAPI.createProject).toHaveBeenCalledWith(request);
|
|
});
|
|
});
|
|
|
|
it('should return project on success', async () => {
|
|
const { result } = renderHook(() => useCreateProject());
|
|
|
|
const request: CreateProjectRequest = {
|
|
workspace_id: 'w1',
|
|
name: 'New Project',
|
|
};
|
|
|
|
const project = createMockProject({
|
|
id: 'p1',
|
|
...request,
|
|
});
|
|
|
|
mockAPI.createProject.mockResolvedValue(project);
|
|
|
|
await result.current.mutate(request);
|
|
|
|
expect(mockAPI.createProject).toHaveBeenCalledWith(request);
|
|
});
|
|
|
|
it('should expose loading state', async () => {
|
|
const { result } = renderHook(() => useCreateProject());
|
|
|
|
expect(result.current.isLoading).toBe(false);
|
|
|
|
mockAPI.createProject.mockImplementation(
|
|
() =>
|
|
new Promise((resolve) =>
|
|
setTimeout(
|
|
() =>
|
|
resolve(
|
|
createMockProject({
|
|
id: 'p1',
|
|
workspace_id: 'w1',
|
|
})
|
|
),
|
|
50
|
|
)
|
|
)
|
|
);
|
|
|
|
const mutatePromise = result.current.mutate({
|
|
workspace_id: 'w1',
|
|
name: 'New Project',
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isLoading).toBe(true);
|
|
});
|
|
|
|
await act(async () => {
|
|
await mutatePromise;
|
|
});
|
|
|
|
expect(result.current.isLoading).toBe(false);
|
|
});
|
|
|
|
it('should expose error state', async () => {
|
|
const { result } = renderHook(() => useCreateProject());
|
|
|
|
const error = new Error('API Error');
|
|
mockAPI.createProject.mockRejectedValue(error);
|
|
|
|
await act(async () => {
|
|
await result.current.mutate({
|
|
workspace_id: 'w1',
|
|
name: 'New Project',
|
|
});
|
|
});
|
|
|
|
expect(result.current.error).toBeTruthy();
|
|
expect(result.current.error?.message).toBe('API Error');
|
|
});
|
|
|
|
it('should clear error on successful mutation', async () => {
|
|
const { result } = renderHook(() => useCreateProject());
|
|
|
|
mockAPI.createProject.mockRejectedValueOnce(new Error('First error'));
|
|
await act(async () => {
|
|
await result.current.mutate({
|
|
workspace_id: 'w1',
|
|
name: 'Project 1',
|
|
});
|
|
});
|
|
expect(result.current.error).toBeTruthy();
|
|
|
|
mockAPI.createProject.mockResolvedValueOnce(
|
|
createMockProject({ id: 'p1', workspace_id: 'w1' })
|
|
);
|
|
await act(async () => {
|
|
await result.current.mutate({
|
|
workspace_id: 'w1',
|
|
name: 'Project 2',
|
|
});
|
|
});
|
|
expect(result.current.error).toBeNull();
|
|
});
|
|
|
|
it('should handle workspace_id correctly', async () => {
|
|
const { result } = renderHook(() => useCreateProject());
|
|
|
|
const request: CreateProjectRequest = {
|
|
workspace_id: 'workspace-789',
|
|
name: 'New Project',
|
|
description: 'Test',
|
|
};
|
|
|
|
const project = createMockProject({
|
|
id: 'p1',
|
|
...request,
|
|
});
|
|
|
|
mockAPI.createProject.mockResolvedValue(project);
|
|
|
|
await result.current.mutate(request);
|
|
|
|
expect(mockAPI.createProject).toHaveBeenCalledWith(request);
|
|
});
|
|
});
|
|
|
|
describe('useDeleteProject', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('should call API with project ID', async () => {
|
|
const { result } = renderHook(() => useDeleteProject());
|
|
|
|
mockAPI.deleteProject.mockResolvedValue(true);
|
|
|
|
await result.current.mutate('project-123');
|
|
|
|
await waitFor(() => {
|
|
expect(mockAPI.deleteProject).toHaveBeenCalledWith('project-123');
|
|
});
|
|
});
|
|
|
|
it('should return true on success', async () => {
|
|
const { result } = renderHook(() => useDeleteProject());
|
|
|
|
mockAPI.deleteProject.mockResolvedValue(true);
|
|
|
|
await result.current.mutate('project-123');
|
|
|
|
expect(mockAPI.deleteProject).toHaveBeenCalledWith('project-123');
|
|
});
|
|
|
|
it('should expose loading state', async () => {
|
|
const { result } = renderHook(() => useDeleteProject());
|
|
|
|
expect(result.current.isLoading).toBe(false);
|
|
|
|
mockAPI.deleteProject.mockImplementation(
|
|
() =>
|
|
new Promise((resolve) => setTimeout(() => resolve(true), 50))
|
|
);
|
|
|
|
const mutatePromise = result.current.mutate('project-123');
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isLoading).toBe(true);
|
|
});
|
|
|
|
await act(async () => {
|
|
await mutatePromise;
|
|
});
|
|
|
|
expect(result.current.isLoading).toBe(false);
|
|
});
|
|
|
|
it('should expose error state', async () => {
|
|
const { result } = renderHook(() => useDeleteProject());
|
|
|
|
const error = new Error('Delete failed');
|
|
mockAPI.deleteProject.mockRejectedValue(error);
|
|
|
|
await act(async () => {
|
|
await result.current.mutate('project-123');
|
|
});
|
|
|
|
expect(result.current.error).toBeTruthy();
|
|
expect(result.current.error?.message).toBe('Delete failed');
|
|
});
|
|
|
|
it('should handle API returning false (not found)', async () => {
|
|
const { result } = renderHook(() => useDeleteProject());
|
|
|
|
mockAPI.deleteProject.mockResolvedValue(false);
|
|
|
|
await result.current.mutate('missing-project');
|
|
|
|
expect(mockAPI.deleteProject).toHaveBeenCalledWith('missing-project');
|
|
});
|
|
|
|
it('should clear error on successful mutation', async () => {
|
|
const { result } = renderHook(() => useDeleteProject());
|
|
|
|
mockAPI.deleteProject.mockRejectedValueOnce(new Error('First error'));
|
|
await act(async () => {
|
|
await result.current.mutate('project-123');
|
|
});
|
|
expect(result.current.error).toBeTruthy();
|
|
|
|
mockAPI.deleteProject.mockResolvedValueOnce(true);
|
|
await act(async () => {
|
|
await result.current.mutate('project-456');
|
|
});
|
|
expect(result.current.error).toBeNull();
|
|
});
|
|
});
|