auto-claude: subtask-3-1 - Create tests for useAsync hook (loading states, er

This commit is contained in:
2026-01-25 11:54:54 +01:00
parent 30d2b76056
commit ef62a67534
+348
View File
@@ -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<string>());
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<string>());
expect(result.current.execute).toBeInstanceOf(Function);
});
});
describe('loading states', () => {
it('should set loading to true when executing', async () => {
const { result } = renderHook(() => useAsync<string>());
const asyncFn = vi.fn(() => new Promise<string>((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<string>());
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<string>());
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<string>());
// 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<string>((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<string>());
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<number>());
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<string[]>());
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<string>());
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<string | null>());
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<string>());
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<string>());
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<string>());
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<string>());
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<string>());
// 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<string>());
// 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<string>());
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<string>());
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<string>());
const slowAsyncFn = vi.fn(() => new Promise<string>((resolve) => {
setTimeout(() => resolve('slow result'), 100);
}));
const fastAsyncFn = vi.fn(() => new Promise<string>((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
});
});
});