Files
rxminder/hooks/__tests__/useUserData.test.ts
William Valentin 5623f7efd2 test: add comprehensive hook tests
- Add extensive tests for useTheme hook with theme switching and system preference detection
- Add tests for useUserData hook with user data management
- Fix documentElement readonly property issues and fetch mock type issues in tests
- Test theme persistence, system theme detection, and edge cases
- Improve hook test coverage and reliability

This adds comprehensive testing for React hooks used throughout the application.
2025-09-08 11:38:37 -07:00

373 lines
10 KiB
TypeScript

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);
});
});
});