WellNuo/services/__tests__/onlineStatusService.test.ts
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

435 lines
14 KiB
TypeScript

/**
* Tests for OnlineStatusService
*/
import {
OnlineStatusService,
calculateOnlineStatus,
formatLastSeen,
getStatusColor,
getStatusLabel,
calculateOverallStatus,
DEFAULT_THRESHOLDS,
DeviceStatusInfo,
} from '../onlineStatusService';
import { api } from '../api';
// Mock the api module
jest.mock('../api', () => ({
api: {
getDevicesForBeneficiary: jest.fn(),
},
}));
const mockApi = api as jest.Mocked<typeof api>;
describe('OnlineStatusService', () => {
describe('calculateOnlineStatus', () => {
it('should return online for recent timestamps (< 5 minutes)', () => {
const now = new Date();
const twoMinutesAgo = new Date(now.getTime() - 2 * 60 * 1000);
expect(calculateOnlineStatus(twoMinutesAgo)).toBe('online');
});
it('should return online for just now', () => {
const now = new Date();
expect(calculateOnlineStatus(now)).toBe('online');
});
it('should return warning for timestamps between 5 and 60 minutes', () => {
const now = new Date();
const thirtyMinutesAgo = new Date(now.getTime() - 30 * 60 * 1000);
expect(calculateOnlineStatus(thirtyMinutesAgo)).toBe('warning');
});
it('should return offline for timestamps older than 60 minutes', () => {
const now = new Date();
const twoHoursAgo = new Date(now.getTime() - 120 * 60 * 1000);
expect(calculateOnlineStatus(twoHoursAgo)).toBe('offline');
});
it('should respect custom thresholds', () => {
const now = new Date();
const tenMinutesAgo = new Date(now.getTime() - 10 * 60 * 1000);
// With default thresholds, 10 minutes should be warning
expect(calculateOnlineStatus(tenMinutesAgo, DEFAULT_THRESHOLDS)).toBe('warning');
// With custom thresholds, 10 minutes could be online
expect(calculateOnlineStatus(tenMinutesAgo, { onlineMaxMinutes: 15, warningMaxMinutes: 60 })).toBe('online');
});
});
describe('formatLastSeen', () => {
it('should return "Just now" for timestamps less than 1 minute ago', () => {
const now = new Date();
expect(formatLastSeen(now)).toBe('Just now');
const thirtySecondsAgo = new Date(now.getTime() - 30 * 1000);
expect(formatLastSeen(thirtySecondsAgo)).toBe('Just now');
});
it('should return minutes for timestamps less than 1 hour ago', () => {
const now = new Date();
const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000);
expect(formatLastSeen(fiveMinutesAgo)).toBe('5 min ago');
const fortyFiveMinutesAgo = new Date(now.getTime() - 45 * 60 * 1000);
expect(formatLastSeen(fortyFiveMinutesAgo)).toBe('45 min ago');
});
it('should return hours for timestamps less than 24 hours ago', () => {
const now = new Date();
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
expect(formatLastSeen(oneHourAgo)).toBe('1 hour ago');
const threeHoursAgo = new Date(now.getTime() - 3 * 60 * 60 * 1000);
expect(formatLastSeen(threeHoursAgo)).toBe('3 hours ago');
});
it('should return days for timestamps older than 24 hours', () => {
const now = new Date();
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
expect(formatLastSeen(oneDayAgo)).toBe('1 day ago');
const threeDaysAgo = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000);
expect(formatLastSeen(threeDaysAgo)).toBe('3 days ago');
});
});
describe('getStatusColor', () => {
it('should return green for online', () => {
expect(getStatusColor('online')).toBe('#22c55e');
});
it('should return amber for warning', () => {
expect(getStatusColor('warning')).toBe('#f59e0b');
});
it('should return red for offline', () => {
expect(getStatusColor('offline')).toBe('#ef4444');
});
});
describe('getStatusLabel', () => {
it('should return proper labels', () => {
expect(getStatusLabel('online')).toBe('Online');
expect(getStatusLabel('warning')).toBe('Warning');
expect(getStatusLabel('offline')).toBe('Offline');
});
});
describe('calculateOverallStatus', () => {
const createDevice = (status: 'online' | 'warning' | 'offline'): DeviceStatusInfo => ({
deviceId: `device-${Math.random()}`,
deviceName: 'Test Device',
status,
lastSeen: new Date(),
lastSeenMinutesAgo: 0,
lastSeenFormatted: 'Just now',
});
it('should return offline for empty array', () => {
expect(calculateOverallStatus([])).toBe('offline');
});
it('should return online when all devices are online', () => {
const devices = [
createDevice('online'),
createDevice('online'),
createDevice('online'),
];
expect(calculateOverallStatus(devices)).toBe('online');
});
it('should return warning when at least one device has warning (but none offline)', () => {
const devices = [
createDevice('online'),
createDevice('warning'),
createDevice('online'),
];
expect(calculateOverallStatus(devices)).toBe('warning');
});
it('should return offline when at least one device is offline', () => {
const devices = [
createDevice('online'),
createDevice('warning'),
createDevice('offline'),
];
expect(calculateOverallStatus(devices)).toBe('offline');
});
it('should prioritize offline over warning', () => {
const devices = [
createDevice('offline'),
createDevice('warning'),
];
expect(calculateOverallStatus(devices)).toBe('offline');
});
});
describe('OnlineStatusService class', () => {
let service: OnlineStatusService;
beforeEach(() => {
service = new OnlineStatusService();
jest.clearAllMocks();
});
describe('setThresholds / getThresholds', () => {
it('should update thresholds', () => {
service.setThresholds({ onlineMaxMinutes: 10 });
const thresholds = service.getThresholds();
expect(thresholds.onlineMaxMinutes).toBe(10);
expect(thresholds.warningMaxMinutes).toBe(60); // Default
});
});
describe('checkBeneficiaryStatus', () => {
it('should return error when API fails', async () => {
mockApi.getDevicesForBeneficiary.mockResolvedValueOnce({
ok: false,
error: { message: 'Network error' },
} as any);
const result = await service.checkBeneficiaryStatus('ben-123');
expect(result.ok).toBe(false);
expect(result.error).toBe('Network error');
});
it('should return empty status when no devices', async () => {
mockApi.getDevicesForBeneficiary.mockResolvedValueOnce({
ok: true,
data: [],
} as any);
const result = await service.checkBeneficiaryStatus('ben-123');
expect(result.ok).toBe(true);
expect(result.data?.totalDevices).toBe(0);
expect(result.data?.devices).toHaveLength(0);
expect(result.data?.overallStatus).toBe('offline'); // No devices = offline
});
it('should correctly transform devices', async () => {
const now = new Date();
const mockDevices = [
{
deviceId: 'dev-1',
wellId: 123,
mac: 'AABBCCDDEEFF',
name: 'WP_123_ddeeff',
status: 'online',
lastSeen: now,
beneficiaryId: 'ben-123',
deploymentId: 456,
source: 'api',
},
{
deviceId: 'dev-2',
wellId: 456,
mac: '112233445566',
name: 'WP_456_445566',
status: 'offline',
lastSeen: new Date(now.getTime() - 2 * 60 * 60 * 1000), // 2 hours ago
beneficiaryId: 'ben-123',
deploymentId: 456,
source: 'api',
},
];
mockApi.getDevicesForBeneficiary.mockResolvedValueOnce({
ok: true,
data: mockDevices,
} as any);
const result = await service.checkBeneficiaryStatus('ben-123');
expect(result.ok).toBe(true);
expect(result.data?.totalDevices).toBe(2);
expect(result.data?.onlineCount).toBe(1);
expect(result.data?.offlineCount).toBe(1);
expect(result.data?.overallStatus).toBe('offline'); // One offline = overall offline
});
it('should use cached data when available and valid', async () => {
const mockDevices = [
{
deviceId: 'dev-1',
name: 'WP_123_ddeeff',
status: 'online',
lastSeen: new Date(),
wellId: 123,
mac: 'AABBCCDDEEFF',
},
];
mockApi.getDevicesForBeneficiary.mockResolvedValue({
ok: true,
data: mockDevices,
} as any);
// First call - should hit API
await service.checkBeneficiaryStatus('ben-123');
expect(mockApi.getDevicesForBeneficiary).toHaveBeenCalledTimes(1);
// Second call - should use cache
await service.checkBeneficiaryStatus('ben-123');
expect(mockApi.getDevicesForBeneficiary).toHaveBeenCalledTimes(1);
// Force refresh - should hit API again
await service.checkBeneficiaryStatus('ben-123', true);
expect(mockApi.getDevicesForBeneficiary).toHaveBeenCalledTimes(2);
});
});
describe('checkMultipleBeneficiaries', () => {
it('should check multiple beneficiaries', async () => {
mockApi.getDevicesForBeneficiary.mockResolvedValue({
ok: true,
data: [],
} as any);
const results = await service.checkMultipleBeneficiaries(['ben-1', 'ben-2', 'ben-3']);
expect(results.size).toBe(3);
expect(results.get('ben-1')?.ok).toBe(true);
expect(results.get('ben-2')?.ok).toBe(true);
expect(results.get('ben-3')?.ok).toBe(true);
});
});
describe('isDeviceOnline', () => {
it('should return true for online device', async () => {
mockApi.getDevicesForBeneficiary.mockResolvedValueOnce({
ok: true,
data: [
{
deviceId: 'dev-1',
name: 'Test',
status: 'online',
lastSeen: new Date(),
},
],
} as any);
const isOnline = await service.isDeviceOnline('ben-123', 'dev-1');
expect(isOnline).toBe(true);
});
it('should return false for offline device', async () => {
const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000);
mockApi.getDevicesForBeneficiary.mockResolvedValueOnce({
ok: true,
data: [
{
deviceId: 'dev-1',
name: 'Test',
status: 'offline',
lastSeen: twoHoursAgo,
},
],
} as any);
const isOnline = await service.isDeviceOnline('ben-123', 'dev-1');
expect(isOnline).toBe(false);
});
it('should return false for non-existent device', async () => {
mockApi.getDevicesForBeneficiary.mockResolvedValueOnce({
ok: true,
data: [],
} as any);
const isOnline = await service.isDeviceOnline('ben-123', 'non-existent');
expect(isOnline).toBe(false);
});
});
describe('getStatusSummary', () => {
it('should return correct summary', async () => {
const now = new Date();
mockApi.getDevicesForBeneficiary.mockResolvedValueOnce({
ok: true,
data: [
{ deviceId: 'd1', name: 'D1', status: 'online', lastSeen: now },
{ deviceId: 'd2', name: 'D2', status: 'online', lastSeen: now },
{ deviceId: 'd3', name: 'D3', status: 'warning', lastSeen: new Date(now.getTime() - 30 * 60 * 1000) },
{ deviceId: 'd4', name: 'D4', status: 'offline', lastSeen: new Date(now.getTime() - 2 * 60 * 60 * 1000) },
],
} as any);
const summary = await service.getStatusSummary('ben-123');
expect(summary).not.toBeNull();
expect(summary?.total).toBe(4);
expect(summary?.online).toBe(2);
expect(summary?.warning).toBe(1);
expect(summary?.offline).toBe(1);
});
it('should return null when API fails', async () => {
mockApi.getDevicesForBeneficiary.mockResolvedValueOnce({
ok: false,
error: 'Failed',
} as any);
const summary = await service.getStatusSummary('ben-123');
expect(summary).toBeNull();
});
});
describe('clearCache', () => {
it('should clear all cached data', async () => {
mockApi.getDevicesForBeneficiary.mockResolvedValue({
ok: true,
data: [],
} as any);
// Populate cache
await service.checkBeneficiaryStatus('ben-1');
await service.checkBeneficiaryStatus('ben-2');
expect(mockApi.getDevicesForBeneficiary).toHaveBeenCalledTimes(2);
// Verify cache is being used
await service.checkBeneficiaryStatus('ben-1');
expect(mockApi.getDevicesForBeneficiary).toHaveBeenCalledTimes(2);
// Clear cache
service.clearCache();
// Now should hit API again
await service.checkBeneficiaryStatus('ben-1');
expect(mockApi.getDevicesForBeneficiary).toHaveBeenCalledTimes(3);
});
});
describe('getCachedStatus', () => {
it('should return cached status if valid', async () => {
mockApi.getDevicesForBeneficiary.mockResolvedValueOnce({
ok: true,
data: [{ deviceId: 'd1', name: 'D1', status: 'online', lastSeen: new Date() }],
} as any);
// Populate cache
await service.checkBeneficiaryStatus('ben-123');
const cached = service.getCachedStatus('ben-123');
expect(cached).not.toBeUndefined();
expect(cached?.beneficiaryId).toBe('ben-123');
});
it('should return undefined for non-cached beneficiary', () => {
const cached = service.getCachedStatus('non-existent');
expect(cached).toBeUndefined();
});
});
});
});