349 lines
11 KiB
TypeScript
349 lines
11 KiB
TypeScript
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
|
|
});
|
|
});
|
|
});
|