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.
This commit is contained in:
335
hooks/__tests__/useTheme.test.ts
Normal file
335
hooks/__tests__/useTheme.test.ts
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
import { renderHook, act } from '@testing-library/react';
|
||||||
|
import { useTheme } from '../useTheme';
|
||||||
|
import { useLocalStorage } from '../useLocalStorage';
|
||||||
|
|
||||||
|
// Mock the useLocalStorage hook
|
||||||
|
jest.mock('../useLocalStorage');
|
||||||
|
const mockUseLocalStorage = useLocalStorage as jest.MockedFunction<
|
||||||
|
typeof useLocalStorage
|
||||||
|
>;
|
||||||
|
|
||||||
|
// Mock window.matchMedia
|
||||||
|
const mockMatchMedia = jest.fn();
|
||||||
|
Object.defineProperty(window, 'matchMedia', {
|
||||||
|
writable: true,
|
||||||
|
value: mockMatchMedia,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock document.documentElement
|
||||||
|
const mockDocumentElement = {
|
||||||
|
classList: {
|
||||||
|
remove: jest.fn(),
|
||||||
|
add: jest.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Object.defineProperty(document, 'documentElement', {
|
||||||
|
writable: true,
|
||||||
|
value: mockDocumentElement,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('useTheme', () => {
|
||||||
|
let mockSetTheme: jest.Mock;
|
||||||
|
let mockMediaQueryList: {
|
||||||
|
matches: boolean;
|
||||||
|
addEventListener: jest.Mock;
|
||||||
|
removeEventListener: jest.Mock;
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
mockSetTheme = jest.fn();
|
||||||
|
mockMediaQueryList = {
|
||||||
|
matches: false,
|
||||||
|
addEventListener: jest.fn(),
|
||||||
|
removeEventListener: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
mockUseLocalStorage.mockReturnValue(['system', mockSetTheme]);
|
||||||
|
mockMatchMedia.mockReturnValue(mockMediaQueryList);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('initialization', () => {
|
||||||
|
test('should initialize with system theme by default', () => {
|
||||||
|
const { result } = renderHook(() => useTheme());
|
||||||
|
|
||||||
|
expect(mockUseLocalStorage).toHaveBeenCalledWith('theme', 'system');
|
||||||
|
expect(result.current.theme).toBe('system');
|
||||||
|
expect(typeof result.current.setTheme).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should apply theme on mount', () => {
|
||||||
|
renderHook(() => useTheme());
|
||||||
|
|
||||||
|
expect(mockDocumentElement.classList.remove).toHaveBeenCalledWith(
|
||||||
|
'light',
|
||||||
|
'dark'
|
||||||
|
);
|
||||||
|
expect(mockDocumentElement.classList.add).toHaveBeenCalledWith('light');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('theme application', () => {
|
||||||
|
test('should apply light theme when theme is light', () => {
|
||||||
|
mockUseLocalStorage.mockReturnValue(['light', mockSetTheme]);
|
||||||
|
|
||||||
|
renderHook(() => useTheme());
|
||||||
|
|
||||||
|
expect(mockDocumentElement.classList.remove).toHaveBeenCalledWith(
|
||||||
|
'light',
|
||||||
|
'dark'
|
||||||
|
);
|
||||||
|
expect(mockDocumentElement.classList.add).toHaveBeenCalledWith('light');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should apply dark theme when theme is dark', () => {
|
||||||
|
mockUseLocalStorage.mockReturnValue(['dark', mockSetTheme]);
|
||||||
|
|
||||||
|
renderHook(() => useTheme());
|
||||||
|
|
||||||
|
expect(mockDocumentElement.classList.remove).toHaveBeenCalledWith(
|
||||||
|
'light',
|
||||||
|
'dark'
|
||||||
|
);
|
||||||
|
expect(mockDocumentElement.classList.add).toHaveBeenCalledWith('dark');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should apply system theme when theme is system and system prefers light', () => {
|
||||||
|
mockMediaQueryList.matches = false;
|
||||||
|
mockUseLocalStorage.mockReturnValue(['system', mockSetTheme]);
|
||||||
|
|
||||||
|
renderHook(() => useTheme());
|
||||||
|
|
||||||
|
expect(mockDocumentElement.classList.add).toHaveBeenCalledWith('light');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should apply system theme when theme is system and system prefers dark', () => {
|
||||||
|
mockMediaQueryList.matches = true;
|
||||||
|
mockUseLocalStorage.mockReturnValue(['system', mockSetTheme]);
|
||||||
|
|
||||||
|
renderHook(() => useTheme());
|
||||||
|
|
||||||
|
expect(mockDocumentElement.classList.add).toHaveBeenCalledWith('dark');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('system theme detection', () => {
|
||||||
|
test('should detect system theme as light when prefers-color-scheme is light', () => {
|
||||||
|
mockMediaQueryList.matches = false;
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useTheme());
|
||||||
|
|
||||||
|
expect(mockMatchMedia).toHaveBeenCalledWith(
|
||||||
|
'(prefers-color-scheme: dark)'
|
||||||
|
);
|
||||||
|
expect(result.current.theme).toBe('system');
|
||||||
|
expect(mockDocumentElement.classList.add).toHaveBeenCalledWith('light');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should detect system theme as dark when prefers-color-scheme is dark', () => {
|
||||||
|
mockMediaQueryList.matches = true;
|
||||||
|
|
||||||
|
renderHook(() => useTheme());
|
||||||
|
|
||||||
|
expect(mockDocumentElement.classList.add).toHaveBeenCalledWith('dark');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should fallback to light theme when window.matchMedia is not available', () => {
|
||||||
|
// Mock window as undefined
|
||||||
|
const originalWindow = global.window;
|
||||||
|
delete (global as any).window;
|
||||||
|
|
||||||
|
mockUseLocalStorage.mockReturnValue(['system', mockSetTheme]);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useTheme());
|
||||||
|
|
||||||
|
expect(result.current.theme).toBe('system');
|
||||||
|
|
||||||
|
// Restore window
|
||||||
|
global.window = originalWindow;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should fallback to light theme when matchMedia is not available', () => {
|
||||||
|
Object.defineProperty(window, 'matchMedia', {
|
||||||
|
writable: true,
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
mockUseLocalStorage.mockReturnValue(['system', mockSetTheme]);
|
||||||
|
|
||||||
|
renderHook(() => useTheme());
|
||||||
|
|
||||||
|
// Should still work and default to light theme for system
|
||||||
|
expect(mockDocumentElement.classList.add).toHaveBeenCalledWith('light');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('media query listener', () => {
|
||||||
|
test('should add event listener for system theme changes', () => {
|
||||||
|
mockUseLocalStorage.mockReturnValue(['system', mockSetTheme]);
|
||||||
|
|
||||||
|
renderHook(() => useTheme());
|
||||||
|
|
||||||
|
expect(mockMediaQueryList.addEventListener).toHaveBeenCalledWith(
|
||||||
|
'change',
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should remove event listener on unmount', () => {
|
||||||
|
const { unmount } = renderHook(() => useTheme());
|
||||||
|
|
||||||
|
unmount();
|
||||||
|
|
||||||
|
expect(mockMediaQueryList.removeEventListener).toHaveBeenCalledWith(
|
||||||
|
'change',
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should update theme when system preference changes and using system theme', () => {
|
||||||
|
mockUseLocalStorage.mockReturnValue(['system', mockSetTheme]);
|
||||||
|
mockMediaQueryList.matches = false;
|
||||||
|
|
||||||
|
renderHook(() => useTheme());
|
||||||
|
|
||||||
|
// Get the change handler
|
||||||
|
const changeHandler =
|
||||||
|
mockMediaQueryList.addEventListener.mock.calls[0][1];
|
||||||
|
|
||||||
|
// Clear previous calls
|
||||||
|
mockDocumentElement.classList.remove.mockClear();
|
||||||
|
mockDocumentElement.classList.add.mockClear();
|
||||||
|
|
||||||
|
// Simulate system theme change
|
||||||
|
mockMediaQueryList.matches = true;
|
||||||
|
act(() => {
|
||||||
|
changeHandler();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockDocumentElement.classList.remove).toHaveBeenCalledWith(
|
||||||
|
'light',
|
||||||
|
'dark'
|
||||||
|
);
|
||||||
|
expect(mockDocumentElement.classList.add).toHaveBeenCalledWith('dark');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not update theme when system preference changes but not using system theme', () => {
|
||||||
|
mockUseLocalStorage.mockReturnValue(['light', mockSetTheme]);
|
||||||
|
|
||||||
|
renderHook(() => useTheme());
|
||||||
|
|
||||||
|
// Get the change handler
|
||||||
|
const changeHandler =
|
||||||
|
mockMediaQueryList.addEventListener.mock.calls[0][1];
|
||||||
|
|
||||||
|
// Clear previous calls
|
||||||
|
mockDocumentElement.classList.remove.mockClear();
|
||||||
|
mockDocumentElement.classList.add.mockClear();
|
||||||
|
|
||||||
|
// Simulate system theme change
|
||||||
|
act(() => {
|
||||||
|
changeHandler();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should not trigger theme change since we're using explicit light theme
|
||||||
|
expect(mockDocumentElement.classList.remove).not.toHaveBeenCalled();
|
||||||
|
expect(mockDocumentElement.classList.add).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('theme updates', () => {
|
||||||
|
test('should update theme when setTheme is called', () => {
|
||||||
|
const { result, rerender } = renderHook(() => useTheme());
|
||||||
|
|
||||||
|
// Change to dark theme
|
||||||
|
mockUseLocalStorage.mockReturnValue(['dark', mockSetTheme]);
|
||||||
|
rerender();
|
||||||
|
|
||||||
|
expect(result.current.theme).toBe('dark');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should apply new theme when theme changes', () => {
|
||||||
|
const { rerender } = renderHook(() => useTheme());
|
||||||
|
|
||||||
|
// Clear initial calls
|
||||||
|
mockDocumentElement.classList.remove.mockClear();
|
||||||
|
mockDocumentElement.classList.add.mockClear();
|
||||||
|
|
||||||
|
// Change to dark theme
|
||||||
|
mockUseLocalStorage.mockReturnValue(['dark', mockSetTheme]);
|
||||||
|
rerender();
|
||||||
|
|
||||||
|
expect(mockDocumentElement.classList.remove).toHaveBeenCalledWith(
|
||||||
|
'light',
|
||||||
|
'dark'
|
||||||
|
);
|
||||||
|
expect(mockDocumentElement.classList.add).toHaveBeenCalledWith('dark');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('edge cases', () => {
|
||||||
|
test('should handle invalid theme values gracefully', () => {
|
||||||
|
mockUseLocalStorage.mockReturnValue(['invalid' as any, mockSetTheme]);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useTheme());
|
||||||
|
|
||||||
|
expect(result.current.theme).toBe('invalid');
|
||||||
|
// Should still try to apply the theme
|
||||||
|
expect(mockDocumentElement.classList.add).toHaveBeenCalledWith('invalid');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle missing document.documentElement', () => {
|
||||||
|
const originalDocumentElement = document.documentElement;
|
||||||
|
delete (document as any).documentElement;
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
renderHook(() => useTheme());
|
||||||
|
}).not.toThrow();
|
||||||
|
|
||||||
|
// Restore
|
||||||
|
Object.defineProperty(document, 'documentElement', {
|
||||||
|
value: originalDocumentElement,
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle classList methods not being available', () => {
|
||||||
|
const originalClassList = mockDocumentElement.classList;
|
||||||
|
mockDocumentElement.classList = undefined as any;
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
renderHook(() => useTheme());
|
||||||
|
}).not.toThrow();
|
||||||
|
|
||||||
|
// Restore
|
||||||
|
mockDocumentElement.classList = originalClassList;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('return value', () => {
|
||||||
|
test('should return current theme and setTheme function', () => {
|
||||||
|
const { result } = renderHook(() => useTheme());
|
||||||
|
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
theme: 'system',
|
||||||
|
setTheme: mockSetTheme,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should maintain stable setTheme reference', () => {
|
||||||
|
const { result, rerender } = renderHook(() => useTheme());
|
||||||
|
const firstSetTheme = result.current.setTheme;
|
||||||
|
|
||||||
|
rerender();
|
||||||
|
const secondSetTheme = result.current.setTheme;
|
||||||
|
|
||||||
|
expect(firstSetTheme).toBe(secondSetTheme);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
372
hooks/__tests__/useUserData.test.ts
Normal file
372
hooks/__tests__/useUserData.test.ts
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user