/** * Unit tests for race condition prevention logic * Tests the core mechanism without component dependencies */ describe('Beneficiary Race Condition Prevention', () => { test('should track loading beneficiary ID and ignore stale responses', async () => { // Simulate the race condition prevention mechanism let loadingBeneficiaryIdRef: string | null = null; const responses: Array<{ id: string; data: any; delay: number }> = []; // Mock API function that tracks which ID is being loaded const mockLoadBeneficiary = async (id: string, delay: number) => { const currentLoadingId = id; loadingBeneficiaryIdRef = currentLoadingId; // Simulate API delay await new Promise(resolve => setTimeout(resolve, delay)); // Check if this request is still current if (loadingBeneficiaryIdRef !== currentLoadingId) { return { ignored: true, id }; } responses.push({ id, data: `Data for ${id}`, delay }); return { ignored: false, id, data: `Data for ${id}` }; }; // Simulate rapid switching: Load 1, then quickly switch to 2 const request1 = mockLoadBeneficiary('1', 100); // Slower await new Promise(resolve => setTimeout(resolve, 10)); // Quick switch const request2 = mockLoadBeneficiary('2', 20); // Faster const [result1, result2] = await Promise.all([request1, request2]); // Result 1 should be ignored (stale) expect(result1.ignored).toBe(true); // Result 2 should be accepted (current) expect(result2.ignored).toBe(false); // Only beneficiary 2's data should be in responses expect(responses).toHaveLength(1); expect(responses[0].id).toBe('2'); }); test('should cancel AbortController when new request starts', () => { const abortControllers: Array<{ id: string; controller: AbortController }> = []; const mockLoadWithAbort = (id: string) => { // Cancel previous request if (abortControllers.length > 0) { const prev = abortControllers[abortControllers.length - 1]; prev.controller.abort(); } // Create new AbortController const controller = new AbortController(); abortControllers.push({ id, controller }); return controller; }; // Simulate loading multiple beneficiaries const controller1 = mockLoadWithAbort('1'); const controller2 = mockLoadWithAbort('2'); const controller3 = mockLoadWithAbort('3'); // First two should be aborted expect(controller1.signal.aborted).toBe(true); expect(controller2.signal.aborted).toBe(true); // Last one should still be active expect(controller3.signal.aborted).toBe(false); expect(abortControllers).toHaveLength(3); }); test('should handle concurrent requests with different completion times', async () => { interface RequestResult { id: string; timestamp: number; accepted: boolean; } const results: RequestResult[] = []; let currentId: string | null = null; const mockConcurrentLoad = async (id: string, delay: number) => { const requestId = id; currentId = requestId; const startTime = Date.now(); await new Promise(resolve => setTimeout(resolve, delay)); const isStillCurrent = currentId === requestId; const result: RequestResult = { id: requestId, timestamp: Date.now() - startTime, accepted: isStillCurrent, }; results.push(result); return result; }; // Start 3 requests with delays: 300ms, 200ms, 100ms // They complete in reverse order: 3, 2, 1 const promises = [ mockConcurrentLoad('1', 300), mockConcurrentLoad('2', 200), mockConcurrentLoad('3', 100), ]; await Promise.all(promises); // Sort by timestamp to see completion order const sortedResults = [...results].sort((a, b) => a.timestamp - b.timestamp); // Request 3 completes first and should be accepted expect(sortedResults[0].id).toBe('3'); expect(sortedResults[0].accepted).toBe(true); // Requests 1 and 2 complete later and should be rejected expect(sortedResults[1].accepted).toBe(false); expect(sortedResults[2].accepted).toBe(false); }); test('should allow reloading the same beneficiary', async () => { const loadedIds: string[] = []; let currentId: string | null = null; const mockReload = async (id: string, delay: number) => { const requestId = id; currentId = requestId; await new Promise(resolve => setTimeout(resolve, delay)); if (currentId === requestId) { loadedIds.push(id); return { accepted: true, id }; } return { accepted: false, id }; }; // Load beneficiary 1 await mockReload('1', 50); expect(loadedIds).toEqual(['1']); // Reload the same beneficiary (e.g., pull to refresh) await mockReload('1', 50); expect(loadedIds).toEqual(['1', '1']); // Both requests should be accepted expect(loadedIds.filter(id => id === '1')).toHaveLength(2); }); test('should handle errors without affecting race condition logic', async () => { let currentId: string | null = null; const results: Array<{ id: string; error?: boolean }> = []; const mockLoadWithError = async (id: string, shouldError: boolean, delay: number) => { const requestId = id; currentId = requestId; await new Promise(resolve => setTimeout(resolve, delay)); // Check if still current if (currentId !== requestId) { return { ignored: true, id }; } if (shouldError) { results.push({ id, error: true }); throw new Error(`Failed to load ${id}`); } results.push({ id }); return { id, data: `Data for ${id}` }; }; // Request 1 will error after 100ms const request1 = mockLoadWithError('1', true, 100).catch(e => ({ error: true, id: '1' })); // Quickly switch to request 2 (succeeds after 50ms) await new Promise(resolve => setTimeout(resolve, 10)); const request2 = mockLoadWithError('2', false, 50); await Promise.all([request1, request2]); // Only successful request 2 should be in results expect(results.filter(r => !r.error)).toHaveLength(1); expect(results.filter(r => !r.error)[0].id).toBe('2'); }); });