import { DependencyList } from 'react'; import { renderHook, waitFor } from '@testing-library/react'; import { useAsync } from './useAsync'; describe('useAsync', () => { const renderAsyncHook = (callback: () => any | [], deps: DependencyList, ignorePrevious = false) => { return renderHook( ({ deps }: { deps: DependencyList }) => useAsync({ callback, defaultValue: 'Initial state', ignorePrevious }, deps), { initialProps: { deps } }, ); }; it('Should invoke callback when hook is initially rendered', async () => { const callback = jest.fn().mockResolvedValue('Resolved data'); const { result } = renderAsyncHook(callback, ['initial_dependency_value']); expect(result.current.isLoading).toBe(true); expect(result.current.error).toBe(null); expect(result.current.data).toBe('Initial state'); await waitFor(() => expect(result.current.isLoading).toBe(false)); expect(result.current.isLoading).toBe(false); expect(result.current.error).toBe(null); expect(result.current.data).toBe('Resolved data'); }); it('Should not invoke callback when hook is re-rendered with the same dependencies', async () => { const callback = jest.fn().mockResolvedValue('Resolved data'); const { result, rerender } = renderAsyncHook(callback, ['initial_dependency_value']); await waitFor(() => expect(result.current.isLoading).toBe(false)); // If we re-render with the same "deps" we should not attempt to re-load data. rerender({ deps: ['initial_dependency_value'] }); await waitFor(() => expect(result.current.isLoading).toBe(false)); expect(result.current.isLoading).toBe(false); expect(result.current.error).toBe(null); expect(result.current.data).toBe('Resolved data'); expect(callback).toBeCalledTimes(1); }); it('Should invoke callback when hook is re-rendered with different dependencies', async () => { const callback = jest.fn().mockResolvedValueOnce('Resolved data').mockResolvedValueOnce('Updated resolved data'); const { result, rerender } = renderAsyncHook(callback, ['initial_dependency_value']); await waitFor(() => expect(result.current.isLoading).toBe(false)); // If we re-render with the same "deps" we should not attempt to re-load data. rerender({ deps: ['updated_dependency_value'] }); await waitFor(() => expect(result.current.isLoading).toBe(false)); expect(result.current.isLoading).toBe(false); expect(result.current.error).toBe(null); expect(result.current.data).toBe('Updated resolved data'); expect(callback).toBeCalledTimes(2); }); it.each([ ['Error object thrown', new Error('Callback failed'), 'Callback failed'], ['Non-error object thrown', 'Callback failed', 'Callback failed'], ])( 'Should retain the error if the callback rejects - %s', async (description: string, error: any, expectedErrorMessage: string) => { const callback = jest.fn().mockRejectedValue(error); const { result } = renderAsyncHook(callback, ['initial_dependency_value']); await waitFor(() => expect(result.current.isLoading).toBe(false)); expect(result.current.isLoading).toBe(false); expect(result.current.error).toBeInstanceOf(Error); expect(result.current.error?.message).toBe(expectedErrorMessage); expect(result.current.data).toBe('Initial state'); }, ); it('Should handle non-async return data returned from callback', async () => { const callback = jest.fn(() => 'Returned data'); const { result } = renderAsyncHook(callback, ['initial_dependency_value']); expect(result.current.isLoading).toBe(false); expect(result.current.error).toBe(null); expect(result.current.data).toBe('Returned data'); }); it.each([ ['Error object thrown', new Error('Callback failed'), 'Callback failed'], ['Non-error object thrown', 'Callback failed', 'Callback failed'], ])( 'Should handle non-async thrown errors from callback - %s', async (description: string, error: any, expectedErrorMessage: string) => { const callback = jest.fn(() => { throw error; }); const { result } = renderAsyncHook(callback, ['initial_dependency_value']); expect(result.current.isLoading).toBe(false); expect(result.current.error).toBeInstanceOf(Error); expect(result.current.error?.message).toBe(expectedErrorMessage); expect(result.current.data).toBe('Initial state'); }, ); test('useAsync handles multiple asynchronous callbacks', async () => { const asyncCallbackOne = jest.fn(() => Promise.resolve('First Async Data')); const asyncCallbackTwo = jest.fn(() => Promise.resolve('Second Async Data')); const { result } = renderHook(() => useAsync( { callback: [asyncCallbackOne, asyncCallbackTwo], defaultValue: 'Default Value', }, [], ), ); // Initial state expect(result.current.data).toBe('Default Value'); expect(result.current.isLoading).toBe(true); expect(result.current.error).toBe(null); await waitFor(() => { expect(result.current.data).toStrictEqual(['First Async Data', 'Second Async Data']); expect(result.current.isLoading).toBe(false); expect(result.current.error).toBe(null); }); expect(asyncCallbackOne).toHaveBeenCalled(); expect(asyncCallbackTwo).toHaveBeenCalled(); }); });