Implement a comprehensive online status monitoring system: - OnlineStatusService: Centralized service for checking device/sensor online status with caching, configurable thresholds, and batch operations - useOnlineStatus hook: React hook for easy component integration with auto-polling, loading states, and app state handling - Support for status levels: online (<5 min), warning (5-60 min), offline (>60 min) - Utility functions for status colors, labels, and formatting Includes 49 passing tests covering service and hook functionality. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
433 lines
12 KiB
TypeScript
433 lines
12 KiB
TypeScript
/**
|
|
* 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<typeof onlineStatusService>;
|
|
|
|
// Helper to create mock status data
|
|
const createMockStatus = (overrides: Partial<BeneficiaryOnlineStatus> = {}): 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<void>;
|
|
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();
|
|
});
|
|
});
|