WellNuo/hooks/__tests__/useOnlineStatus.test.tsx
Sergei 290b0e218b Add online status check service and hook for device monitoring
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>
2026-02-01 09:35:45 -08:00

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();
});
});