From ef62a67534604712b73634c8d96769141c9172ae Mon Sep 17 00:00:00 2001 From: Damjan Savic Date: Sun, 25 Jan 2026 11:54:54 +0100 Subject: [PATCH] auto-claude: subtask-3-1 - Create tests for useAsync hook (loading states, er --- src/hooks/useAsync.test.tsx | 348 ++++++++++++++++++++++++++++++++++++ 1 file changed, 348 insertions(+) create mode 100644 src/hooks/useAsync.test.tsx diff --git a/src/hooks/useAsync.test.tsx b/src/hooks/useAsync.test.tsx new file mode 100644 index 0000000..e9eae3c --- /dev/null +++ b/src/hooks/useAsync.test.tsx @@ -0,0 +1,348 @@ +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 + }); + }); +});