WellNuo/hooks/__tests__/useLoadingState.test.ts
Sergei 2b36f801f1 Add comprehensive loading state management system
- Add useLoadingState hook with data/loading/error state management
- Add useSimpleLoading hook for basic boolean loading state
- Add useMultipleLoadingStates hook for tracking multiple items
- Create Skeleton component with shimmer animation for placeholders
- Create specialized skeletons: SkeletonText, SkeletonAvatar, SkeletonCard,
  SkeletonListItem, SkeletonBeneficiaryCard, SkeletonSensorCard
- Create LoadingOverlay components: modal, inline, and card variants
- Create ScreenLoading wrapper with loading/error/content states
- Create RefreshableScreen with built-in pull-to-refresh
- Create EmptyState and LoadingButtonState utility components
- Add comprehensive tests for all components and hooks (61 tests passing)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-01 10:11:14 -08:00

285 lines
7.5 KiB
TypeScript

import { renderHook, act, cleanup } from '@testing-library/react-native';
import {
useLoadingState,
useSimpleLoading,
useMultipleLoadingStates,
} from '@/hooks/useLoadingState';
// Cleanup after each test to prevent unmounted renderer issues
afterEach(() => {
cleanup();
});
describe('useLoadingState', () => {
it('initializes with default state', () => {
const { result } = renderHook(() => useLoadingState());
expect(result.current.data).toBeNull();
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBeNull();
expect(result.current.isRefreshing).toBe(false);
});
it('initializes with provided initial data', () => {
const { result } = renderHook(() =>
useLoadingState({ initialData: { id: 1, name: 'Test' } })
);
expect(result.current.data).toEqual({ id: 1, name: 'Test' });
});
it('setData updates data and clears error', () => {
const { result } = renderHook(() => useLoadingState<string>());
act(() => {
result.current.setError('Previous error');
});
act(() => {
result.current.setData('new data');
});
expect(result.current.data).toBe('new data');
expect(result.current.error).toBeNull();
});
it('setLoading updates loading state', () => {
const { result } = renderHook(() => useLoadingState());
act(() => {
result.current.setLoading(true);
});
expect(result.current.isLoading).toBe(true);
act(() => {
result.current.setLoading(false);
});
expect(result.current.isLoading).toBe(false);
});
it('setError updates error and clears loading states', () => {
const { result } = renderHook(() => useLoadingState());
act(() => {
result.current.setLoading(true);
});
act(() => {
result.current.setError('Something went wrong');
});
expect(result.current.error).toBe('Something went wrong');
expect(result.current.isLoading).toBe(false);
expect(result.current.isRefreshing).toBe(false);
});
it('setRefreshing updates refreshing state', () => {
const { result } = renderHook(() => useLoadingState());
act(() => {
result.current.setRefreshing(true);
});
expect(result.current.isRefreshing).toBe(true);
});
it('reset restores initial state', () => {
const { result } = renderHook(() =>
useLoadingState({ initialData: 'initial' })
);
act(() => {
result.current.setData('modified');
result.current.setLoading(true);
result.current.setError('error');
});
act(() => {
result.current.reset();
});
expect(result.current.data).toBe('initial');
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBeNull();
expect(result.current.isRefreshing).toBe(false);
});
it('execute handles successful async function', async () => {
const { result } = renderHook(() => useLoadingState<string>());
await act(async () => {
await result.current.execute(async () => {
return 'success';
});
});
expect(result.current.data).toBe('success');
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBeNull();
});
it('execute handles async function error', async () => {
const { result } = renderHook(() => useLoadingState<string>());
await act(async () => {
await result.current.execute(async () => {
throw new Error('Network error');
});
});
expect(result.current.error).toBe('Network error');
expect(result.current.isLoading).toBe(false);
});
it('execute with refresh option returns data', async () => {
const { result } = renderHook(() => useLoadingState<string>());
await act(async () => {
await result.current.execute(
async () => {
return 'refreshed data';
},
{ refresh: true }
);
});
expect(result.current.data).toBe('refreshed data');
expect(result.current.isRefreshing).toBe(false);
});
it('execute uses transform function', async () => {
const { result } = renderHook(() => useLoadingState<string>());
await act(async () => {
await result.current.execute(
async () => {
return { value: 'raw' };
},
{ transform: (res) => res.value.toUpperCase() }
);
});
expect(result.current.data).toBe('RAW');
});
});
describe('useSimpleLoading', () => {
it('initializes with default state', () => {
const { result } = renderHook(() => useSimpleLoading());
expect(result.current.isLoading).toBe(false);
});
it('initializes with provided initial state', () => {
const { result } = renderHook(() => useSimpleLoading(true));
expect(result.current.isLoading).toBe(true);
});
it('startLoading sets isLoading to true', () => {
const { result } = renderHook(() => useSimpleLoading());
act(() => {
result.current.startLoading();
});
expect(result.current.isLoading).toBe(true);
});
it('stopLoading sets isLoading to false', () => {
const { result } = renderHook(() => useSimpleLoading(true));
act(() => {
result.current.stopLoading();
});
expect(result.current.isLoading).toBe(false);
});
it('toggleLoading toggles isLoading', () => {
const { result } = renderHook(() => useSimpleLoading(false));
act(() => {
result.current.toggleLoading();
});
expect(result.current.isLoading).toBe(true);
act(() => {
result.current.toggleLoading();
});
expect(result.current.isLoading).toBe(false);
});
it('withLoading wraps async function', async () => {
const { result } = renderHook(() => useSimpleLoading());
await act(async () => {
const returnValue = await result.current.withLoading(async () => {
return 'done';
});
expect(returnValue).toBe('done');
});
expect(result.current.isLoading).toBe(false);
});
});
describe('useMultipleLoadingStates', () => {
it('initializes with empty loading states', () => {
const { result } = renderHook(() => useMultipleLoadingStates());
expect(result.current.loadingStates).toEqual({});
expect(result.current.anyLoading).toBe(false);
});
it('setLoading updates individual loading state', () => {
const { result } = renderHook(() => useMultipleLoadingStates());
act(() => {
result.current.setLoading('item-1', true);
});
expect(result.current.isLoading('item-1')).toBe(true);
expect(result.current.isLoading('item-2')).toBe(false);
expect(result.current.anyLoading).toBe(true);
});
it('tracks multiple loading states', () => {
const { result } = renderHook(() => useMultipleLoadingStates());
act(() => {
result.current.setLoading('item-1', true);
result.current.setLoading('item-2', true);
});
expect(result.current.isLoading('item-1')).toBe(true);
expect(result.current.isLoading('item-2')).toBe(true);
expect(result.current.anyLoading).toBe(true);
act(() => {
result.current.setLoading('item-1', false);
});
expect(result.current.isLoading('item-1')).toBe(false);
expect(result.current.isLoading('item-2')).toBe(true);
expect(result.current.anyLoading).toBe(true);
});
it('clearAll clears all loading states', () => {
const { result } = renderHook(() => useMultipleLoadingStates());
act(() => {
result.current.setLoading('item-1', true);
result.current.setLoading('item-2', true);
});
act(() => {
result.current.clearAll();
});
expect(result.current.loadingStates).toEqual({});
expect(result.current.anyLoading).toBe(false);
});
});