/** * Tests for useApiWithErrorHandling hooks */ import { renderHook, act, waitFor } from '@testing-library/react-native'; import { useApiCall, useMutation, useMultipleApiCalls } from '../useApiWithErrorHandling'; import { ApiResponse } from '@/types'; import { AppError } from '@/types/errors'; // Mock ErrorContext const mockShowError = jest.fn(); jest.mock('@/contexts/ErrorContext', () => ({ useShowErrorSafe: () => mockShowError, })); // Mock errorHandler jest.mock('@/services/errorHandler', () => ({ createErrorFromApi: jest.fn((error) => ({ message: error.message, code: error.code || 'API_ERROR', severity: 'error', category: 'unknown', retry: { isRetryable: false }, userMessage: error.message, timestamp: new Date(), errorId: 'test-error-id', })), createNetworkError: jest.fn((message, isTimeout = false) => ({ message, code: isTimeout ? 'NETWORK_TIMEOUT' : 'NETWORK_ERROR', severity: 'warning', category: 'network', retry: { isRetryable: true }, userMessage: message, timestamp: new Date(), errorId: 'test-network-error-id', })), })); // Mock networkErrorRecovery jest.mock('@/utils/networkErrorRecovery', () => ({ isNetworkError: jest.fn(() => false), isTimeoutError: jest.fn(() => false), toApiError: jest.fn((error) => ({ message: error instanceof Error ? error.message : String(error), code: 'UNKNOWN_ERROR', })), })); describe('useApiCall', () => { beforeEach(() => { jest.clearAllMocks(); }); it('should start with initial state', () => { const mockApiCall = jest.fn(); const { result } = renderHook(() => useApiCall(mockApiCall)); expect(result.current.isLoading).toBe(false); expect(result.current.error).toBe(null); expect(result.current.data).toBe(null); }); it('should handle successful API call', async () => { const mockData = { id: 1, name: 'Test' }; const mockApiCall = jest.fn().mockResolvedValue({ ok: true, data: mockData, } as ApiResponse); const onSuccess = jest.fn(); const { result } = renderHook(() => useApiCall(mockApiCall, { onSuccess }) ); await act(async () => { await result.current.execute(); }); expect(result.current.isLoading).toBe(false); expect(result.current.data).toEqual(mockData); expect(result.current.error).toBe(null); expect(onSuccess).toHaveBeenCalled(); expect(mockShowError).not.toHaveBeenCalled(); }); it('should handle API error response', async () => { const mockApiCall = jest.fn().mockResolvedValue({ ok: false, error: { message: 'Not found', code: 'NOT_FOUND' }, } as ApiResponse); const onError = jest.fn(); const { result } = renderHook(() => useApiCall(mockApiCall, { onError }) ); await act(async () => { await result.current.execute(); }); expect(result.current.isLoading).toBe(false); expect(result.current.data).toBe(null); expect(result.current.error).not.toBe(null); expect(result.current.error?.message).toBe('Not found'); expect(onError).toHaveBeenCalled(); expect(mockShowError).toHaveBeenCalled(); }); it('should not show error when showError is false', async () => { const mockApiCall = jest.fn().mockResolvedValue({ ok: false, error: { message: 'Error' }, } as ApiResponse); const { result } = renderHook(() => useApiCall(mockApiCall, { showError: false }) ); await act(async () => { await result.current.execute(); }); expect(result.current.error).not.toBe(null); expect(mockShowError).not.toHaveBeenCalled(); }); it('should handle thrown exceptions', async () => { const mockApiCall = jest.fn().mockRejectedValue(new Error('Network failed')); const { result } = renderHook(() => useApiCall(mockApiCall)); await act(async () => { await result.current.execute(); }); expect(result.current.isLoading).toBe(false); expect(result.current.error).not.toBe(null); expect(mockShowError).toHaveBeenCalled(); }); it('should reset state', async () => { const mockData = { id: 1 }; const mockApiCall = jest.fn().mockResolvedValue({ ok: true, data: mockData, }); const { result } = renderHook(() => useApiCall(mockApiCall)); await act(async () => { await result.current.execute(); }); expect(result.current.data).toEqual(mockData); act(() => { result.current.reset(); }); expect(result.current.isLoading).toBe(false); expect(result.current.error).toBe(null); expect(result.current.data).toBe(null); }); it('should retry last call', async () => { let callCount = 0; const mockApiCall = jest.fn().mockImplementation(async () => { callCount++; if (callCount === 1) { return { ok: false, error: { message: 'First failed' } }; } return { ok: true, data: { success: true } }; }); const { result } = renderHook(() => useApiCall(mockApiCall)); // First call fails await act(async () => { await result.current.execute(); }); expect(result.current.error).not.toBe(null); // Retry succeeds await act(async () => { await result.current.retry(); }); expect(result.current.data).toEqual({ success: true }); expect(result.current.error).toBe(null); }); it('should set loading state during execution', async () => { let resolvePromise: (value: ApiResponse) => void; const mockApiCall = jest.fn().mockReturnValue( new Promise(resolve => { resolvePromise = resolve; }) ); const { result } = renderHook(() => useApiCall(mockApiCall)); // Start execution let executePromise: Promise; act(() => { executePromise = result.current.execute(); }); expect(result.current.isLoading).toBe(true); // Resolve the promise await act(async () => { resolvePromise!({ ok: true, data: {} }); await executePromise; }); expect(result.current.isLoading).toBe(false); }); }); describe('useMutation', () => { beforeEach(() => { jest.clearAllMocks(); }); it('should start with initial state', () => { const mockMutationFn = jest.fn(); const { result } = renderHook(() => useMutation(mockMutationFn)); expect(result.current.isLoading).toBe(false); expect(result.current.error).toBe(null); expect(result.current.data).toBe(null); expect(result.current.isError).toBe(false); expect(result.current.isSuccess).toBe(false); }); it('should handle successful mutation', async () => { const mockData = { id: 1, created: true }; const mockMutationFn = jest.fn().mockResolvedValue({ ok: true, data: mockData, }); const onMutate = jest.fn(); const onSuccess = jest.fn(); const onSettled = jest.fn(); const { result } = renderHook(() => useMutation(mockMutationFn, { onMutate, onSuccess, onSettled }) ); await act(async () => { await result.current.mutate({ name: 'Test' }); }); expect(onMutate).toHaveBeenCalledWith({ name: 'Test' }); expect(onSuccess).toHaveBeenCalledWith(mockData, { name: 'Test' }); expect(onSettled).toHaveBeenCalledWith(mockData, null, { name: 'Test' }); expect(result.current.data).toEqual(mockData); expect(result.current.isSuccess).toBe(true); expect(result.current.isError).toBe(false); }); it('should handle mutation error', async () => { const mockMutationFn = jest.fn().mockResolvedValue({ ok: false, error: { message: 'Mutation failed' }, }); const onError = jest.fn(); const onSettled = jest.fn(); const { result } = renderHook(() => useMutation(mockMutationFn, { onError, onSettled }) ); await act(async () => { await result.current.mutate({ name: 'Test' }); }); expect(onError).toHaveBeenCalled(); expect(onSettled).toHaveBeenCalled(); expect(result.current.isError).toBe(true); expect(result.current.isSuccess).toBe(false); }); it('should call onMutate for optimistic updates', async () => { const callOrder: string[] = []; const mockMutationFn = jest.fn().mockImplementation(async () => { callOrder.push('mutation'); return { ok: true, data: {} }; }); const onMutate = jest.fn().mockImplementation(async () => { callOrder.push('onMutate'); }); const { result } = renderHook(() => useMutation(mockMutationFn, { onMutate }) ); await act(async () => { await result.current.mutate({ id: 1 }); }); expect(onMutate).toHaveBeenCalledWith({ id: 1 }); expect(callOrder).toEqual(['onMutate', 'mutation']); }); it('should reset state', async () => { const mockMutationFn = jest.fn().mockResolvedValue({ ok: true, data: { id: 1 }, }); const { result } = renderHook(() => useMutation(mockMutationFn)); await act(async () => { await result.current.mutate({}); }); expect(result.current.data).not.toBe(null); act(() => { result.current.reset(); }); expect(result.current.isLoading).toBe(false); expect(result.current.error).toBe(null); expect(result.current.data).toBe(null); }); }); describe('useMultipleApiCalls', () => { beforeEach(() => { jest.clearAllMocks(); }); it('should execute multiple calls in parallel', async () => { const call1 = jest.fn().mockResolvedValue({ ok: true, data: { id: 1 } }); const call2 = jest.fn().mockResolvedValue({ ok: true, data: { id: 2 } }); const call3 = jest.fn().mockResolvedValue({ ok: true, data: { id: 3 } }); const { result } = renderHook(() => useMultipleApiCalls()); let results: any[]; await act(async () => { results = await result.current.executeAll([call1, call2, call3]); }); expect(results!).toHaveLength(3); expect(results![0].data).toEqual({ id: 1 }); expect(results![1].data).toEqual({ id: 2 }); expect(results![2].data).toEqual({ id: 3 }); expect(result.current.hasErrors).toBe(false); }); it('should collect errors from failed calls', async () => { const call1 = jest.fn().mockResolvedValue({ ok: true, data: { id: 1 } }); const call2 = jest.fn().mockResolvedValue({ ok: false, error: { message: 'Failed' } }); const call3 = jest.fn().mockResolvedValue({ ok: true, data: { id: 3 } }); const onError = jest.fn(); const { result } = renderHook(() => useMultipleApiCalls()); await act(async () => { await result.current.executeAll([call1, call2, call3], { onError }); }); expect(result.current.hasErrors).toBe(true); expect(result.current.errors).toHaveLength(1); expect(onError).toHaveBeenCalled(); }); it('should reset state', async () => { const call1 = jest.fn().mockResolvedValue({ ok: false, error: { message: 'Error' } }); const { result } = renderHook(() => useMultipleApiCalls()); await act(async () => { await result.current.executeAll([call1]); }); expect(result.current.hasErrors).toBe(true); act(() => { result.current.reset(); }); expect(result.current.isLoading).toBe(false); expect(result.current.errors).toEqual([]); expect(result.current.hasErrors).toBe(false); }); it('should handle thrown exceptions', async () => { const call1 = jest.fn().mockRejectedValue(new Error('Exception')); const { result } = renderHook(() => useMultipleApiCalls()); await act(async () => { await result.current.executeAll([call1]); }); expect(result.current.hasErrors).toBe(true); }); });