- 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>
285 lines
7.5 KiB
TypeScript
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);
|
|
});
|
|
});
|