import { renderHook, waitFor } from '@testing-library/react'; import useUserData from '../useUserData'; // Mock fetch const mockFetch = jest.fn(); global.fetch = mockFetch as any; describe('useUserData', () => { beforeEach(() => { jest.clearAllMocks(); mockFetch.mockClear(); }); afterEach(() => { jest.restoreAllMocks(); }); describe('initial state', () => { test('should start with loading true, userData null, and error null', () => { mockFetch.mockImplementation( () => new Promise(() => { // Never resolves - used for testing initial state }) ); const { result } = renderHook(() => useUserData()); expect(result.current.loading).toBe(true); expect(result.current.userData).toBe(null); expect(result.current.error).toBe(null); }); }); describe('successful data fetching', () => { test('should fetch user data successfully', async () => { const mockUserData = { id: '123', name: 'John Doe', email: 'john@example.com', profile: { avatar: 'avatar.jpg', bio: 'Software developer', }, }; mockFetch.mockResolvedValueOnce({ json: jest.fn().mockResolvedValue(mockUserData), }); const { result } = renderHook(() => useUserData()); expect(result.current.loading).toBe(true); expect(mockFetch).toHaveBeenCalledWith('/api/user/profile'); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.userData).toEqual(mockUserData); expect(result.current.error).toBe(null); }); test('should handle empty user data response', async () => { const emptyUserData = {}; mockFetch.mockResolvedValueOnce({ json: jest.fn().mockResolvedValue(emptyUserData), }); const { result } = renderHook(() => useUserData()); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.userData).toEqual(emptyUserData); expect(result.current.error).toBe(null); }); test('should handle null user data response', async () => { mockFetch.mockResolvedValueOnce({ json: jest.fn().mockResolvedValue(null), }); const { result } = renderHook(() => useUserData()); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.userData).toBe(null); expect(result.current.error).toBe(null); }); }); describe('error handling', () => { test('should handle fetch errors', async () => { const fetchError = new Error('Network error'); mockFetch.mockRejectedValueOnce(fetchError); const { result } = renderHook(() => useUserData()); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.userData).toBe(null); expect(result.current.error).toBe(fetchError); }); test('should handle JSON parsing errors', async () => { const jsonError = new Error('Invalid JSON'); mockFetch.mockResolvedValueOnce({ json: jest.fn().mockRejectedValue(jsonError), }); const { result } = renderHook(() => useUserData()); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.userData).toBe(null); expect(result.current.error).toBe(jsonError); }); test('should handle HTTP error responses', async () => { const httpError = new Error('HTTP 404'); mockFetch.mockRejectedValueOnce(httpError); const { result } = renderHook(() => useUserData()); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.error).toBe(httpError); }); test('should handle server errors gracefully', async () => { const serverError = new Error('Internal Server Error'); mockFetch.mockRejectedValueOnce(serverError); const { result } = renderHook(() => useUserData()); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.userData).toBe(null); expect(result.current.error).toBe(serverError); expect(result.current.loading).toBe(false); }); }); describe('API endpoint', () => { test('should call the correct API endpoint', async () => { mockFetch.mockResolvedValueOnce({ json: jest.fn().mockResolvedValue({}), }); renderHook(() => useUserData()); expect(mockFetch).toHaveBeenCalledTimes(1); expect(mockFetch).toHaveBeenCalledWith('/api/user/profile'); }); }); describe('loading state management', () => { test('should set loading to false after successful fetch', async () => { mockFetch.mockResolvedValueOnce({ json: jest.fn().mockResolvedValue({ id: '123' }), }); const { result } = renderHook(() => useUserData()); expect(result.current.loading).toBe(true); await waitFor(() => { expect(result.current.loading).toBe(false); }); }); test('should set loading to false after failed fetch', async () => { mockFetch.mockRejectedValueOnce(new Error('Fetch failed')); const { result } = renderHook(() => useUserData()); expect(result.current.loading).toBe(true); await waitFor(() => { expect(result.current.loading).toBe(false); }); }); }); describe('effect behavior', () => { test('should only fetch data once on mount', async () => { mockFetch.mockResolvedValue({ json: jest.fn().mockResolvedValue({ id: '123' }), }); const { rerender } = renderHook(() => useUserData()); await waitFor(() => { expect(mockFetch).toHaveBeenCalledTimes(1); }); // Rerender the hook rerender(); // Should still only be called once expect(mockFetch).toHaveBeenCalledTimes(1); }); test('should not fetch data again on re-renders', async () => { mockFetch.mockResolvedValue({ json: jest.fn().mockResolvedValue({ id: '123' }), }); const { result, rerender } = renderHook(() => useUserData()); await waitFor(() => { expect(result.current.loading).toBe(false); }); const firstCallCount = mockFetch.mock.calls.length; // Force multiple re-renders rerender(); rerender(); rerender(); expect(mockFetch).toHaveBeenCalledTimes(firstCallCount); }); }); describe('return value structure', () => { test('should return object with userData, loading, and error properties', () => { mockFetch.mockImplementation( () => new Promise(() => { // Never resolves - used for testing return value structure }) ); const { result } = renderHook(() => useUserData()); expect(result.current).toHaveProperty('userData'); expect(result.current).toHaveProperty('loading'); expect(result.current).toHaveProperty('error'); expect(Object.keys(result.current)).toHaveLength(3); }); test('should maintain consistent return value structure through state changes', async () => { mockFetch.mockResolvedValueOnce({ json: jest.fn().mockResolvedValue({ id: '123' }), }); const { result } = renderHook(() => useUserData()); // During loading expect(result.current).toHaveProperty('userData'); expect(result.current).toHaveProperty('loading'); expect(result.current).toHaveProperty('error'); await waitFor(() => { expect(result.current.loading).toBe(false); }); // After loading expect(result.current).toHaveProperty('userData'); expect(result.current).toHaveProperty('loading'); expect(result.current).toHaveProperty('error'); }); }); describe('complex user data scenarios', () => { test('should handle complex nested user data', async () => { const complexUserData = { id: '123', personal: { firstName: 'John', lastName: 'Doe', contact: { email: 'john@example.com', phone: '+1234567890', address: { street: '123 Main St', city: 'Anytown', country: 'USA', }, }, }, preferences: { theme: 'dark', notifications: { email: true, push: false, sms: true, }, }, medications: [ { id: 'med1', name: 'Aspirin', dosage: '100mg' }, { id: 'med2', name: 'Vitamin D', dosage: '1000IU' }, ], }; mockFetch.mockResolvedValueOnce({ json: jest.fn().mockResolvedValue(complexUserData), }); const { result } = renderHook(() => useUserData()); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.userData).toEqual(complexUserData); expect(result.current.error).toBe(null); }); test('should handle arrays in user data', async () => { const userDataWithArrays = { id: '123', roles: ['user', 'admin'], tags: [], history: [ { action: 'login', timestamp: '2023-01-01' }, { action: 'update_profile', timestamp: '2023-01-02' }, ], }; mockFetch.mockResolvedValueOnce({ json: jest.fn().mockResolvedValue(userDataWithArrays), }); const { result } = renderHook(() => useUserData()); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.userData).toEqual(userDataWithArrays); }); }); describe('memory management', () => { test('should not cause memory leaks on unmount during pending request', () => { let resolvePromise: (value: any) => void; const pendingPromise = new Promise(resolve => { resolvePromise = resolve; }); mockFetch.mockReturnValueOnce(pendingPromise); const { unmount } = renderHook(() => useUserData()); // Unmount while request is pending unmount(); // Resolve the promise after unmount resolvePromise!({ json: jest.fn().mockResolvedValue({ id: '123' }), }); // Test should complete without errors expect(mockFetch).toHaveBeenCalledTimes(1); }); }); });