/** * Tests for useOnlineStatus hook */ import React from 'react'; import { renderHook, act, waitFor } from '@testing-library/react-native'; import { AppState, AppStateStatus } from 'react-native'; import { useOnlineStatus, useMultipleOnlineStatus } from '../useOnlineStatus'; import { onlineStatusService, BeneficiaryOnlineStatus } from '@/services/onlineStatusService'; // Mock the onlineStatusService jest.mock('@/services/onlineStatusService', () => ({ onlineStatusService: { checkBeneficiaryStatus: jest.fn(), checkMultipleBeneficiaries: jest.fn(), }, })); // Mock AppState jest.mock('react-native', () => { const listeners: ((state: AppStateStatus) => void)[] = []; return { AppState: { addEventListener: jest.fn((event: string, callback: (state: AppStateStatus) => void) => { listeners.push(callback); return { remove: jest.fn(() => { const index = listeners.indexOf(callback); if (index > -1) listeners.splice(index, 1); }), }; }), // Helper to trigger state change in tests __triggerStateChange: (state: AppStateStatus) => { listeners.forEach(cb => cb(state)); }, }, }; }); const mockService = onlineStatusService as jest.Mocked; // Helper to create mock status data const createMockStatus = (overrides: Partial = {}): BeneficiaryOnlineStatus => ({ beneficiaryId: 'ben-123', totalDevices: 2, onlineCount: 1, warningCount: 0, offlineCount: 1, overallStatus: 'warning', devices: [ { deviceId: 'dev-1', deviceName: 'Device 1', status: 'online', lastSeen: new Date(), lastSeenMinutesAgo: 2, lastSeenFormatted: '2 min ago', }, { deviceId: 'dev-2', deviceName: 'Device 2', status: 'offline', lastSeen: new Date(Date.now() - 2 * 60 * 60 * 1000), lastSeenMinutesAgo: 120, lastSeenFormatted: '2 hours ago', }, ], lastChecked: new Date(), ...overrides, }); describe('useOnlineStatus', () => { beforeEach(() => { jest.clearAllMocks(); jest.useFakeTimers(); }); afterEach(() => { jest.useRealTimers(); }); describe('initial loading', () => { it('should start in loading state', () => { mockService.checkBeneficiaryStatus.mockResolvedValue({ ok: true, data: createMockStatus(), }); const { result } = renderHook(() => useOnlineStatus('ben-123')); expect(result.current.isLoading).toBe(true); }); it('should fetch data on mount by default', async () => { mockService.checkBeneficiaryStatus.mockResolvedValue({ ok: true, data: createMockStatus(), }); renderHook(() => useOnlineStatus('ben-123')); await waitFor(() => { expect(mockService.checkBeneficiaryStatus).toHaveBeenCalledWith('ben-123', true); }); }); it('should not fetch on mount when fetchOnMount is false', async () => { const { result } = renderHook(() => useOnlineStatus('ben-123', { fetchOnMount: false }) ); expect(mockService.checkBeneficiaryStatus).not.toHaveBeenCalled(); expect(result.current.isLoading).toBe(false); }); it('should handle null beneficiaryId', async () => { const { result } = renderHook(() => useOnlineStatus(null)); expect(result.current.isLoading).toBe(false); expect(result.current.status).toBeNull(); expect(mockService.checkBeneficiaryStatus).not.toHaveBeenCalled(); }); }); describe('successful data fetch', () => { it('should populate status data', async () => { const mockStatus = createMockStatus(); mockService.checkBeneficiaryStatus.mockResolvedValue({ ok: true, data: mockStatus, }); const { result } = renderHook(() => useOnlineStatus('ben-123')); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(result.current.status).toEqual(mockStatus); expect(result.current.overallStatus).toBe('warning'); expect(result.current.devices).toHaveLength(2); expect(result.current.onlineCount).toBe(1); expect(result.current.offlineCount).toBe(1); expect(result.current.totalDevices).toBe(2); }); it('should provide device lookup functions', async () => { mockService.checkBeneficiaryStatus.mockResolvedValue({ ok: true, data: createMockStatus(), }); const { result } = renderHook(() => useOnlineStatus('ben-123')); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); // isDeviceOnline expect(result.current.isDeviceOnline('dev-1')).toBe(true); expect(result.current.isDeviceOnline('dev-2')).toBe(false); expect(result.current.isDeviceOnline('non-existent')).toBe(false); // getDeviceStatus const device1 = result.current.getDeviceStatus('dev-1'); expect(device1?.status).toBe('online'); expect(result.current.getDeviceStatus('non-existent')).toBeUndefined(); }); }); describe('error handling', () => { it('should handle API errors', async () => { mockService.checkBeneficiaryStatus.mockResolvedValue({ ok: false, error: 'Network error', }); const { result } = renderHook(() => useOnlineStatus('ben-123')); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(result.current.error).toBe('Network error'); expect(result.current.status).toBeNull(); }); it('should handle exceptions', async () => { mockService.checkBeneficiaryStatus.mockRejectedValue(new Error('Unexpected error')); const { result } = renderHook(() => useOnlineStatus('ben-123')); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(result.current.error).toBe('Unexpected error'); }); }); describe('refresh', () => { it('should allow manual refresh', async () => { mockService.checkBeneficiaryStatus.mockResolvedValue({ ok: true, data: createMockStatus(), }); const { result } = renderHook(() => useOnlineStatus('ben-123')); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(mockService.checkBeneficiaryStatus).toHaveBeenCalledTimes(1); await act(async () => { await result.current.refresh(); }); expect(mockService.checkBeneficiaryStatus).toHaveBeenCalledTimes(2); }); it('should set isRefreshing during refresh', async () => { let resolvePromise: (value: any) => void; mockService.checkBeneficiaryStatus.mockImplementation( () => new Promise(resolve => { resolvePromise = resolve; }) ); const { result } = renderHook(() => useOnlineStatus('ben-123', { fetchOnMount: false }) ); // Start refresh let refreshPromise: Promise; act(() => { refreshPromise = result.current.refresh(); }); await waitFor(() => { expect(result.current.isRefreshing).toBe(true); }); // Resolve the API call act(() => { resolvePromise!({ ok: true, data: createMockStatus() }); }); await act(async () => { await refreshPromise; }); expect(result.current.isRefreshing).toBe(false); }); }); describe('polling', () => { it('should poll when enabled', async () => { mockService.checkBeneficiaryStatus.mockResolvedValue({ ok: true, data: createMockStatus(), }); renderHook(() => useOnlineStatus('ben-123', { enablePolling: true, pollingInterval: 5000, }) ); // Initial fetch await waitFor(() => { expect(mockService.checkBeneficiaryStatus).toHaveBeenCalledTimes(1); }); // Advance timer by polling interval act(() => { jest.advanceTimersByTime(5000); }); await waitFor(() => { expect(mockService.checkBeneficiaryStatus).toHaveBeenCalledTimes(2); }); // Advance again act(() => { jest.advanceTimersByTime(5000); }); await waitFor(() => { expect(mockService.checkBeneficiaryStatus).toHaveBeenCalledTimes(3); }); }); it('should stop polling on unmount', async () => { mockService.checkBeneficiaryStatus.mockResolvedValue({ ok: true, data: createMockStatus(), }); const { unmount } = renderHook(() => useOnlineStatus('ben-123', { enablePolling: true, pollingInterval: 5000, }) ); // Initial fetch await waitFor(() => { expect(mockService.checkBeneficiaryStatus).toHaveBeenCalledTimes(1); }); // Unmount unmount(); // Advance timer - should not trigger new fetch act(() => { jest.advanceTimersByTime(10000); }); expect(mockService.checkBeneficiaryStatus).toHaveBeenCalledTimes(1); }); }); describe('beneficiaryId changes', () => { it('should refetch when beneficiaryId changes', async () => { mockService.checkBeneficiaryStatus.mockResolvedValue({ ok: true, data: createMockStatus(), }); const { rerender } = renderHook( ({ id }: { id: string }) => useOnlineStatus(id), { initialProps: { id: 'ben-123' } } ); await waitFor(() => { expect(mockService.checkBeneficiaryStatus).toHaveBeenCalledWith('ben-123', true); }); // Change beneficiaryId rerender({ id: 'ben-456' }); await waitFor(() => { expect(mockService.checkBeneficiaryStatus).toHaveBeenCalledWith('ben-456', true); }); }); }); }); describe('useMultipleOnlineStatus', () => { beforeEach(() => { jest.clearAllMocks(); jest.useFakeTimers(); }); afterEach(() => { jest.useRealTimers(); }); it('should fetch statuses for multiple beneficiaries', async () => { const results = new Map([ ['ben-1', { ok: true, data: createMockStatus({ beneficiaryId: 'ben-1' }) }], ['ben-2', { ok: true, data: createMockStatus({ beneficiaryId: 'ben-2' }) }], ]); mockService.checkMultipleBeneficiaries.mockResolvedValue(results); const { result } = renderHook(() => useMultipleOnlineStatus(['ben-1', 'ben-2']) ); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(result.current.statuses.size).toBe(2); expect(result.current.getStatus('ben-1')?.beneficiaryId).toBe('ben-1'); expect(result.current.getStatus('ben-2')?.beneficiaryId).toBe('ben-2'); }); it('should handle errors for individual beneficiaries', async () => { const results = new Map([ ['ben-1', { ok: true, data: createMockStatus({ beneficiaryId: 'ben-1' }) }], ['ben-2', { ok: false, error: 'Failed to fetch' }], ]); mockService.checkMultipleBeneficiaries.mockResolvedValue(results); const { result } = renderHook(() => useMultipleOnlineStatus(['ben-1', 'ben-2']) ); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(result.current.statuses.size).toBe(1); expect(result.current.errors.get('ben-2')).toBe('Failed to fetch'); }); it('should allow manual refresh', async () => { mockService.checkMultipleBeneficiaries.mockResolvedValue(new Map()); const { result } = renderHook(() => useMultipleOnlineStatus(['ben-1', 'ben-2']) ); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(mockService.checkMultipleBeneficiaries).toHaveBeenCalledTimes(1); await act(async () => { await result.current.refresh(); }); expect(mockService.checkMultipleBeneficiaries).toHaveBeenCalledTimes(2); }); it('should handle empty beneficiary array', async () => { const { result } = renderHook(() => useMultipleOnlineStatus([])); expect(result.current.isLoading).toBe(false); expect(result.current.statuses.size).toBe(0); expect(mockService.checkMultipleBeneficiaries).not.toHaveBeenCalled(); }); });