import { describe, it, expect, vi, beforeEach } from 'vitest'; import { renderHook, waitFor } from '@testing-library/react'; import { useAsync } from './useAsync'; import * as errorHandling from '../utils/errorHandling'; // Mock the errorHandling module vi.mock('../utils/errorHandling', async () => { const actual = await vi.importActual('../utils/errorHandling'); return { ...actual, handleError: vi.fn((error: unknown) => { if (error instanceof Error) { return error; } return new Error('Handled error'); }) }; }); describe('useAsync', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('initial state', () => { it('should initialize with null data, null error, and loading false', () => { const { result } = renderHook(() => useAsync()); expect(result.current.data).toBeNull(); expect(result.current.error).toBeNull(); expect(result.current.loading).toBe(false); }); it('should provide an execute function', () => { const { result } = renderHook(() => useAsync()); expect(result.current.execute).toBeInstanceOf(Function); }); }); describe('loading states', () => { it('should set loading to true when executing', async () => { const { result } = renderHook(() => useAsync()); const asyncFn = vi.fn(() => new Promise((resolve) => { setTimeout(() => resolve('test data'), 100); })); result.current.execute(asyncFn); // Loading should be true immediately after execute is called expect(result.current.loading).toBe(true); expect(result.current.data).toBeNull(); expect(result.current.error).toBeNull(); await waitFor(() => { expect(result.current.loading).toBe(false); }); }); it('should set loading to false after successful execution', async () => { const { result } = renderHook(() => useAsync()); const asyncFn = vi.fn(async () => 'test data'); await result.current.execute(asyncFn); expect(result.current.loading).toBe(false); expect(result.current.data).toBe('test data'); expect(result.current.error).toBeNull(); }); it('should set loading to false after failed execution', async () => { const { result } = renderHook(() => useAsync()); const error = new Error('Test error'); const asyncFn = vi.fn(async () => { throw error; }); try { await result.current.execute(asyncFn); } catch (e) { // Expected to throw } expect(result.current.loading).toBe(false); expect(result.current.data).toBeNull(); expect(result.current.error).toBe(error); }); it('should reset data and error when starting new execution', async () => { const { result } = renderHook(() => useAsync()); // First execution with data await result.current.execute(async () => 'first data'); expect(result.current.data).toBe('first data'); expect(result.current.error).toBeNull(); // Second execution that will take some time const slowAsyncFn = vi.fn(() => new Promise((resolve) => { setTimeout(() => resolve('second data'), 100); })); result.current.execute(slowAsyncFn); // Immediately after calling execute, state should be reset expect(result.current.data).toBeNull(); expect(result.current.error).toBeNull(); expect(result.current.loading).toBe(true); await waitFor(() => { expect(result.current.loading).toBe(false); }); }); }); describe('data flow', () => { it('should set data on successful execution', async () => { const { result } = renderHook(() => useAsync()); const testData = 'success data'; const asyncFn = vi.fn(async () => testData); await result.current.execute(asyncFn); expect(result.current.data).toBe(testData); expect(result.current.error).toBeNull(); expect(result.current.loading).toBe(false); expect(asyncFn).toHaveBeenCalledTimes(1); }); it('should handle different data types', async () => { // Test with number const { result: numberResult } = renderHook(() => useAsync()); await numberResult.current.execute(async () => 42); expect(numberResult.current.data).toBe(42); // Test with object const { result: objectResult } = renderHook(() => useAsync<{ id: number; name: string }>()); const testObject = { id: 1, name: 'test' }; await objectResult.current.execute(async () => testObject); expect(objectResult.current.data).toEqual(testObject); // Test with array const { result: arrayResult } = renderHook(() => useAsync()); const testArray = ['a', 'b', 'c']; await arrayResult.current.execute(async () => testArray); expect(arrayResult.current.data).toEqual(testArray); }); it('should return data from execute function', async () => { const { result } = renderHook(() => useAsync()); const testData = 'returned data'; const asyncFn = vi.fn(async () => testData); const returnedData = await result.current.execute(asyncFn); expect(returnedData).toBe(testData); }); it('should handle null data', async () => { const { result } = renderHook(() => useAsync()); const asyncFn = vi.fn(async () => null); await result.current.execute(asyncFn); expect(result.current.data).toBeNull(); expect(result.current.error).toBeNull(); expect(result.current.loading).toBe(false); }); it('should replace previous data with new data', async () => { const { result } = renderHook(() => useAsync()); await result.current.execute(async () => 'first'); expect(result.current.data).toBe('first'); await result.current.execute(async () => 'second'); expect(result.current.data).toBe('second'); await result.current.execute(async () => 'third'); expect(result.current.data).toBe('third'); }); }); describe('error handling', () => { it('should set error on failed execution', async () => { const { result } = renderHook(() => useAsync()); const error = new Error('Test error'); const asyncFn = vi.fn(async () => { throw error; }); try { await result.current.execute(asyncFn); } catch (e) { // Expected to throw } expect(result.current.data).toBeNull(); expect(result.current.error).toBe(error); expect(result.current.loading).toBe(false); }); it('should call handleError with error and context', async () => { const { result } = renderHook(() => useAsync()); const error = new Error('Test error'); const asyncFn = vi.fn(async () => { throw error; }); try { await result.current.execute(asyncFn); } catch (e) { // Expected to throw } expect(errorHandling.handleError).toHaveBeenCalledWith(error, 'Async Operation'); }); it('should re-throw the handled error', async () => { const { result } = renderHook(() => useAsync()); const originalError = new Error('Test error'); const asyncFn = vi.fn(async () => { throw originalError; }); await expect(result.current.execute(asyncFn)).rejects.toThrow(); }); it('should handle errors of different types', async () => { const { result } = renderHook(() => useAsync()); // Test with Error object const error1 = new Error('Error object'); await expect(result.current.execute(async () => { throw error1; })).rejects.toThrow(); // Test with string error await expect(result.current.execute(async () => { throw 'String error'; })).rejects.toThrow(); // Test with number error await expect(result.current.execute(async () => { throw 404; })).rejects.toThrow(); }); it('should clear error on successful subsequent execution', async () => { const { result } = renderHook(() => useAsync()); // First execution fails try { await result.current.execute(async () => { throw new Error('First error'); }); } catch (e) { // Expected to throw } expect(result.current.error).not.toBeNull(); // Second execution succeeds await result.current.execute(async () => 'success'); expect(result.current.error).toBeNull(); expect(result.current.data).toBe('success'); }); it('should replace previous error with new error', async () => { const { result } = renderHook(() => useAsync()); const error1 = new Error('First error'); const error2 = new Error('Second error'); // First execution fails try { await result.current.execute(async () => { throw error1; }); } catch (e) { // Expected to throw } expect(result.current.error).toBe(error1); // Second execution also fails try { await result.current.execute(async () => { throw error2; }); } catch (e) { // Expected to throw } expect(result.current.error).toBe(error2); }); }); describe('execute function stability', () => { it('should maintain the same execute function reference', () => { const { result, rerender } = renderHook(() => useAsync()); const firstExecute = result.current.execute; rerender(); const secondExecute = result.current.execute; expect(firstExecute).toBe(secondExecute); }); }); describe('concurrent executions', () => { it('should handle multiple concurrent executions', async () => { const { result } = renderHook(() => useAsync()); const slowAsyncFn = vi.fn(() => new Promise((resolve) => { setTimeout(() => resolve('slow result'), 100); })); const fastAsyncFn = vi.fn(() => new Promise((resolve) => { setTimeout(() => resolve('fast result'), 10); })); // Start slow execution const slowPromise = result.current.execute(slowAsyncFn); // Start fast execution (will reset state) const fastPromise = result.current.execute(fastAsyncFn); // Fast one should complete await fastPromise; expect(result.current.data).toBe('fast result'); // Slow one should also complete, but will override the fast result await slowPromise; // Note: In the current implementation, the last executed function will set the final state }); }); });