/** * Tests for Offline-Aware API Wrapper */ import { api } from '@/services/api'; import { withOfflineCheck, isNetworkError, getNetworkErrorMessage, NETWORK_ERROR_MESSAGES, NETWORK_ERROR_CODES, offlineAwareApi, } from '@/services/offlineAwareApi'; import * as networkStatus from '@/utils/networkStatus'; // Mock network status utilities jest.mock('@/utils/networkStatus', () => ({ isOnline: jest.fn(), retryWithBackoff: jest.fn((fn) => fn()), DEFAULT_RETRY_CONFIG: { maxAttempts: 3, delayMs: 1000, backoffMultiplier: 2, }, })); // Mock API service jest.mock('@/services/api', () => ({ api: { getAllBeneficiaries: jest.fn(), createBeneficiary: jest.fn(), }, })); describe('Offline-Aware API', () => { beforeEach(() => { jest.clearAllMocks(); }); describe('isNetworkError', () => { it('should identify network error by code', () => { expect(isNetworkError({ message: 'Test', code: 'NETWORK_ERROR' })).toBe(true); expect(isNetworkError({ message: 'Test', code: 'NETWORK_OFFLINE' })).toBe(true); expect(isNetworkError({ message: 'Test', code: 'NETWORK_TIMEOUT' })).toBe(true); }); it('should identify network error by message', () => { expect(isNetworkError({ message: 'Network connection lost' })).toBe(true); expect(isNetworkError({ message: 'Request timeout' })).toBe(true); expect(isNetworkError({ message: 'Fetch failed' })).toBe(true); expect(isNetworkError({ message: 'You are offline' })).toBe(true); }); it('should return false for non-network errors', () => { expect(isNetworkError({ message: 'Invalid input', code: 'VALIDATION_ERROR' })).toBe(false); expect(isNetworkError({ message: 'Not found', code: 'NOT_FOUND' })).toBe(false); }); it('should handle null/undefined errors', () => { expect(isNetworkError(null as any)).toBe(false); expect(isNetworkError(undefined as any)).toBe(false); expect(isNetworkError({} as any)).toBe(false); }); }); describe('getNetworkErrorMessage', () => { it('should return specific message for offline error', () => { const error = { message: 'Test', code: 'NETWORK_OFFLINE' }; expect(getNetworkErrorMessage(error)).toBe(NETWORK_ERROR_MESSAGES.OFFLINE); }); it('should return specific message for timeout error', () => { const error = { message: 'Test', code: 'NETWORK_TIMEOUT' }; expect(getNetworkErrorMessage(error)).toBe(NETWORK_ERROR_MESSAGES.TIMEOUT); }); it('should return generic message for network error without specific code', () => { const error = { message: 'Network failed', code: 'NETWORK_ERROR' }; expect(getNetworkErrorMessage(error)).toBe(NETWORK_ERROR_MESSAGES.GENERIC); }); it('should return original message for non-network errors', () => { const error = { message: 'Validation failed', code: 'VALIDATION_ERROR' }; expect(getNetworkErrorMessage(error)).toBe('Validation failed'); }); }); describe('withOfflineCheck', () => { it('should return offline error when not online', async () => { (networkStatus.isOnline as jest.Mock).mockResolvedValue(false); const result = await withOfflineCheck(() => Promise.resolve({ ok: true, data: 'test' })); expect(result.ok).toBe(false); expect(result.error?.code).toBe(NETWORK_ERROR_CODES.OFFLINE); expect(result.error?.message).toBe(NETWORK_ERROR_MESSAGES.OFFLINE); }); it('should use custom offline message', async () => { (networkStatus.isOnline as jest.Mock).mockResolvedValue(false); const customMessage = 'Custom offline message'; const result = await withOfflineCheck( () => Promise.resolve({ ok: true, data: 'test' }), { offlineMessage: customMessage } ); expect(result.ok).toBe(false); expect(result.error?.message).toBe(customMessage); }); it('should execute API call when online', async () => { (networkStatus.isOnline as jest.Mock).mockResolvedValue(true); const mockResponse = { ok: true, data: { id: 1, name: 'Test' } }; const apiCall = jest.fn().mockResolvedValue(mockResponse); const result = await withOfflineCheck(apiCall); expect(apiCall).toHaveBeenCalled(); expect(result).toEqual(mockResponse); }); it('should retry when retry option is enabled', async () => { (networkStatus.isOnline as jest.Mock).mockResolvedValue(true); const mockResponse = { ok: true, data: 'test' }; const apiCall = jest.fn().mockResolvedValue(mockResponse); await withOfflineCheck(apiCall, { retry: true }); expect(networkStatus.retryWithBackoff).toHaveBeenCalledWith(apiCall, expect.any(Object)); }); it('should convert exceptions to ApiResponse format', async () => { (networkStatus.isOnline as jest.Mock).mockResolvedValue(true); const apiCall = jest.fn().mockRejectedValue(new Error('Something went wrong')); const result = await withOfflineCheck(apiCall); expect(result.ok).toBe(false); expect(result.error?.message).toBe('Something went wrong'); expect(result.error?.code).toBe('NETWORK_ERROR'); }); }); describe('offlineAwareApi', () => { beforeEach(() => { (networkStatus.isOnline as jest.Mock).mockResolvedValue(true); }); it('should wrap getAllBeneficiaries with offline check', async () => { const mockData = [{ id: 1, name: 'Test' }]; (api.getAllBeneficiaries as jest.Mock).mockResolvedValue({ ok: true, data: mockData, }); const result = await offlineAwareApi.getAllBeneficiaries(); expect(api.getAllBeneficiaries).toHaveBeenCalled(); expect(result.ok).toBe(true); expect(result.data).toEqual(mockData); }); it('should wrap createBeneficiary with offline check', async () => { (api.createBeneficiary as jest.Mock).mockResolvedValue({ ok: true, data: { id: 2, name: 'New Beneficiary' }, }); const result = await offlineAwareApi.createBeneficiary({ name: 'New Beneficiary' }); expect(api.createBeneficiary).toHaveBeenCalledWith({ name: 'New Beneficiary' }); expect(result.ok).toBe(true); }); it('should return offline error when offline', async () => { (networkStatus.isOnline as jest.Mock).mockResolvedValue(false); const result = await offlineAwareApi.getAllBeneficiaries(); expect(result.ok).toBe(false); expect(result.error?.code).toBe(NETWORK_ERROR_CODES.OFFLINE); expect(api.getAllBeneficiaries).not.toHaveBeenCalled(); }); it('should use custom offline messages for write operations', async () => { (networkStatus.isOnline as jest.Mock).mockResolvedValue(false); const result = await offlineAwareApi.createBeneficiary({ name: 'Test' }); expect(result.ok).toBe(false); expect(result.error?.message).toContain('offline'); expect(result.error?.message.toLowerCase()).toContain('cannot'); }); }); });