/** * Tests for the useApi hook * @jest-environment jsdom */ import { renderHook, act, waitFor } from '@testing-library/react'; import { useApi, useFetchData } from '@/hooks/useApi'; import { api } from '@/services/api'; // Mock the api module jest.mock('@/services/api', () => ({ api: { get: jest.fn(), post: jest.fn(), put: jest.fn(), patch: jest.fn(), delete: jest.fn(), }, ApiError: class ApiError extends Error { constructor(public status: number, message: string, public data?: unknown) { super(message); this.name = 'ApiError'; } }, })); // Mock react-hot-toast - define mock inside factory to avoid hoisting issues jest.mock('react-hot-toast', () => { const mockToast = { error: jest.fn(), success: jest.fn(), }; return { __esModule: true, default: mockToast, toast: mockToast, error: mockToast.error, success: mockToast.success, }; }); // Import to access the mock for assertions import { toast } from 'react-hot-toast'; describe('useApi Hook', () => { beforeEach(() => { jest.clearAllMocks(); }); describe('initial state', () => { it('should have correct initial state', () => { const { result } = renderHook(() => useApi()); expect(result.current.data).toBeNull(); expect(result.current.loading).toBe(false); expect(result.current.error).toBeNull(); }); }); describe('execute', () => { it('should execute GET request successfully', async () => { const mockData = { id: 1, name: 'Test' }; (api.get as jest.Mock).mockResolvedValueOnce(mockData); const { result } = renderHook(() => useApi()); let response: typeof mockData | null = null; await act(async () => { response = await result.current.execute('get', '/test'); }); expect(api.get).toHaveBeenCalledWith('/test'); expect(result.current.data).toEqual(mockData); expect(result.current.loading).toBe(false); expect(result.current.error).toBeNull(); expect(response).toEqual(mockData); }); it('should execute POST request with data', async () => { const postData = { email: 'test@test.com' }; const mockResponse = { success: true }; (api.post as jest.Mock).mockResolvedValueOnce(mockResponse); const { result } = renderHook(() => useApi()); await act(async () => { await result.current.execute('post', '/auth/login', postData); }); expect(api.post).toHaveBeenCalledWith('/auth/login', postData); expect(result.current.data).toEqual(mockResponse); }); it('should execute PUT request', async () => { const putData = { name: 'Updated' }; (api.put as jest.Mock).mockResolvedValueOnce({ success: true }); const { result } = renderHook(() => useApi()); await act(async () => { await result.current.execute('put', '/users/1', putData); }); expect(api.put).toHaveBeenCalledWith('/users/1', putData); }); it('should execute PATCH request', async () => { const patchData = { status: 'active' }; (api.patch as jest.Mock).mockResolvedValueOnce({ success: true }); const { result } = renderHook(() => useApi()); await act(async () => { await result.current.execute('patch', '/users/1', patchData); }); expect(api.patch).toHaveBeenCalledWith('/users/1', patchData); }); it('should execute DELETE request', async () => { (api.delete as jest.Mock).mockResolvedValueOnce({ success: true }); const { result } = renderHook(() => useApi()); await act(async () => { await result.current.execute('delete', '/users/1'); }); expect(api.delete).toHaveBeenCalledWith('/users/1'); }); it('should handle errors and show toast', async () => { const errorMessage = 'Something went wrong'; (api.get as jest.Mock).mockRejectedValueOnce(new Error(errorMessage)); const { result } = renderHook(() => useApi()); await act(async () => { await result.current.execute('get', '/test'); }); expect(result.current.data).toBeNull(); expect(result.current.loading).toBe(false); expect(result.current.error).toBe(errorMessage); expect(toast.error).toHaveBeenCalledWith(errorMessage); }); it('should return null on error', async () => { (api.get as jest.Mock).mockRejectedValueOnce(new Error('Error')); const { result } = renderHook(() => useApi()); let response: unknown = 'initial'; await act(async () => { response = await result.current.execute('get', '/test'); }); expect(response).toBeNull(); }); it('should set loading state during request', async () => { let resolvePromise: (value: unknown) => void; const promise = new Promise((resolve) => { resolvePromise = resolve; }); (api.get as jest.Mock).mockReturnValueOnce(promise); const { result } = renderHook(() => useApi()); act(() => { result.current.execute('get', '/test'); }); // Should be loading immediately after calling execute expect(result.current.loading).toBe(true); // Resolve the promise await act(async () => { resolvePromise!({ data: 'test' }); await promise; }); expect(result.current.loading).toBe(false); }); }); describe('reset', () => { it('should reset state to initial values', async () => { const mockData = { id: 1 }; (api.get as jest.Mock).mockResolvedValueOnce(mockData); const { result } = renderHook(() => useApi()); await act(async () => { await result.current.execute('get', '/test'); }); expect(result.current.data).toEqual(mockData); act(() => { result.current.reset(); }); expect(result.current.data).toBeNull(); expect(result.current.loading).toBe(false); expect(result.current.error).toBeNull(); }); }); }); describe('useFetchData Hook', () => { beforeEach(() => { jest.clearAllMocks(); }); it('should have correct initial state before refetch', () => { const { result } = renderHook(() => useFetchData('/test')); // Before calling refetch, data should be null and loading should be true expect(result.current.data).toBeNull(); expect(result.current.loading).toBe(true); expect(result.current.error).toBeNull(); expect(typeof result.current.refetch).toBe('function'); }); it('should extract data from response.data', async () => { const mockData = { id: 1, name: 'Test' }; (api.get as jest.Mock).mockResolvedValue({ data: mockData }); const { result } = renderHook(() => useFetchData('/test')); await act(async () => { await result.current.refetch(); }); expect(result.current.data).toEqual(mockData); expect(result.current.loading).toBe(false); }); it('should use response directly if no data field', async () => { const mockData = { id: 1, name: 'Test' }; (api.get as jest.Mock).mockResolvedValue(mockData); const { result } = renderHook(() => useFetchData('/test')); await act(async () => { await result.current.refetch(); }); expect(result.current.data).toEqual(mockData); }); it('should call onSuccess callback', async () => { const mockData = { id: 1 }; const onSuccess = jest.fn(); (api.get as jest.Mock).mockResolvedValue({ data: mockData }); const { result } = renderHook(() => useFetchData('/test', { onSuccess }) ); await act(async () => { await result.current.refetch(); }); expect(onSuccess).toHaveBeenCalledWith(mockData); }); it('should call onError callback on error', async () => { const errorMessage = 'Fetch failed'; const onError = jest.fn(); (api.get as jest.Mock).mockRejectedValue(new Error(errorMessage)); const { result } = renderHook(() => useFetchData('/test', { onError, showErrorToast: false }) ); await act(async () => { await result.current.refetch(); }); expect(onError).toHaveBeenCalledWith(errorMessage); expect(result.current.error).toBe(errorMessage); }); it('should show error toast by default', async () => { (api.get as jest.Mock).mockRejectedValue(new Error('Error')); const { result } = renderHook(() => useFetchData('/test')); await act(async () => { await result.current.refetch(); }); expect(toast.error).toHaveBeenCalled(); }); it('should not show error toast when showErrorToast is false', async () => { (api.get as jest.Mock).mockRejectedValue(new Error('Error')); const { result } = renderHook(() => useFetchData('/test', { showErrorToast: false }) ); await act(async () => { await result.current.refetch(); }); expect(toast.error).not.toHaveBeenCalled(); }); it('should allow refetching data', async () => { const mockData1 = { id: 1 }; const mockData2 = { id: 2 }; (api.get as jest.Mock) .mockResolvedValueOnce({ data: mockData1 }) .mockResolvedValueOnce({ data: mockData2 }); const { result } = renderHook(() => useFetchData('/test')); await act(async () => { await result.current.refetch(); }); expect(result.current.data).toEqual(mockData1); await act(async () => { await result.current.refetch(); }); expect(result.current.data).toEqual(mockData2); }); });