import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { renderHook, act, waitFor } from '@testing-library/react' import { useApi, useApiForm, useApiCache } from './useApi' // Mock API function for testing const mockApiFunction = vi.fn() const mockFormSubmitFunction = vi.fn() const mockCacheFunction = vi.fn() describe('useApi', () => { beforeEach(() => { vi.clearAllMocks() }) afterEach(() => { vi.restoreAllMocks() }) it('should initialize with default state', () => { const { result } = renderHook(() => useApi(mockApiFunction)) expect(result.current.data).toBeNull() expect(result.current.loading).toBe(false) expect(result.current.error).toBeNull() }) it('should execute API function successfully', async () => { const mockData = { id: 1, name: 'Test' } mockApiFunction.mockResolvedValueOnce(mockData) const { result } = renderHook(() => useApi(mockApiFunction)) let executeResult: unknown await act(async () => { executeResult = await result.current.execute('test-arg') }) expect(result.current.data).toEqual(mockData) expect(result.current.loading).toBe(false) expect(result.current.error).toBeNull() expect(executeResult).toEqual(mockData) expect(mockApiFunction).toHaveBeenCalledWith('test-arg') }) it('should handle API function errors', async () => { const errorMessage = 'API Error' mockApiFunction.mockRejectedValueOnce({ message: errorMessage }) const { result } = renderHook(() => useApi(mockApiFunction)) let executeResult: unknown await act(async () => { executeResult = await result.current.execute() }) expect(result.current.data).toBeNull() expect(result.current.loading).toBe(false) expect(result.current.error).toBe(errorMessage) expect(executeResult).toBeNull() }) it('should call onSuccess callback', async () => { const mockData = { id: 1 } const onSuccess = vi.fn() mockApiFunction.mockResolvedValueOnce(mockData) const { result } = renderHook(() => useApi(mockApiFunction, { onSuccess })) await act(async () => { await result.current.execute() }) expect(onSuccess).toHaveBeenCalledWith(mockData) }) it('should call onError callback', async () => { const errorMessage = 'Test Error' const onError = vi.fn() mockApiFunction.mockRejectedValueOnce({ message: errorMessage }) const { result } = renderHook(() => useApi(mockApiFunction, { onError })) await act(async () => { await result.current.execute() }) expect(onError).toHaveBeenCalledWith(errorMessage) }) it('should execute immediately when immediate option is true', async () => { const mockData = { id: 1 } mockApiFunction.mockResolvedValueOnce(mockData) renderHook(() => useApi(mockApiFunction, { immediate: true })) await waitFor(() => { expect(mockApiFunction).toHaveBeenCalled() }) }) it('should reset state', async () => { const mockData = { id: 1 } mockApiFunction.mockResolvedValueOnce(mockData) const { result } = renderHook(() => useApi(mockApiFunction)) await act(async () => { await result.current.execute() }) expect(result.current.data).toEqual(mockData) act(() => { result.current.reset() }) expect(result.current.data).toBeNull() expect(result.current.loading).toBe(false) expect(result.current.error).toBeNull() }) it('should clear error', async () => { mockApiFunction.mockRejectedValueOnce({ message: 'Test Error' }) const { result } = renderHook(() => useApi(mockApiFunction)) await act(async () => { await result.current.execute() }) expect(result.current.error).toBeTruthy() act(() => { result.current.clearError() }) expect(result.current.error).toBeNull() }) it('should cancel previous requests', async () => { let resolveFirst: (value: unknown) => void let resolveSecond: (value: unknown) => void const firstPromise = new Promise(resolve => { resolveFirst = resolve }) const secondPromise = new Promise(resolve => { resolveSecond = resolve }) mockApiFunction .mockReturnValueOnce(firstPromise) .mockReturnValueOnce(secondPromise) const { result } = renderHook(() => useApi(mockApiFunction)) // Start first request act(() => { result.current.execute('first') }) // Start second request before first completes act(() => { result.current.execute('second') }) // Resolve first request (should be cancelled) await act(async () => { resolveFirst({ data: 'first' }) await new Promise(resolve => setTimeout(resolve, 0)) }) // Resolve second request await act(async () => { resolveSecond({ data: 'second' }) await new Promise(resolve => setTimeout(resolve, 0)) }) expect(result.current.data).toEqual({ data: 'second' }) }) }) describe('useApiForm', () => { beforeEach(() => { vi.clearAllMocks() }) it('should handle form submission', async () => { const formData = { name: 'Test' } const responseData = { id: 1, name: 'Test' } mockFormSubmitFunction.mockResolvedValueOnce(responseData) const { result } = renderHook(() => useApiForm(mockFormSubmitFunction)) let submitResult: unknown await act(async () => { submitResult = await result.current.handleSubmit(formData) }) expect(submitResult).toEqual(responseData) expect(mockFormSubmitFunction).toHaveBeenCalledWith(formData) expect(result.current.data).toEqual(responseData) }) it('should reset on success when option is enabled', async () => { const formData = { name: 'Test' } const responseData = { id: 1, name: 'Test' } mockFormSubmitFunction.mockResolvedValueOnce(responseData) const { result } = renderHook(() => useApiForm(mockFormSubmitFunction, { resetOnSuccess: true }) ) await act(async () => { await result.current.handleSubmit(formData) }) expect(result.current.data).toBeNull() }) }) describe('useApiCache', () => { beforeEach(() => { vi.clearAllMocks() vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() }) it('should fetch and cache data', async () => { const mockData = { id: 1, name: 'Test' } mockCacheFunction.mockResolvedValueOnce(mockData) const { result } = renderHook(() => useApiCache('test-key', mockCacheFunction) ) let fetchResult: unknown await act(async () => { fetchResult = await result.current.fetchData() }) expect(fetchResult).toEqual(mockData) expect(result.current.data).toEqual(mockData) expect(mockCacheFunction).toHaveBeenCalledTimes(1) }) it('should return cached data when fresh', async () => { const mockData = { id: 1, name: 'Test' } mockCacheFunction.mockResolvedValueOnce(mockData) const { result } = renderHook(() => useApiCache('test-key', mockCacheFunction, { staleTime: 60000 }) ) // First fetch await act(async () => { await result.current.fetchData() }) // Second fetch should return cached data let secondResult: unknown await act(async () => { secondResult = await result.current.fetchData() }) expect(secondResult).toEqual(mockData) expect(mockCacheFunction).toHaveBeenCalledTimes(1) // Should not call API again }) it('should fetch fresh data when stale', async () => { const mockData1 = { id: 1, name: 'Test 1' } const mockData2 = { id: 2, name: 'Test 2' } mockCacheFunction .mockResolvedValueOnce(mockData1) .mockResolvedValueOnce(mockData2) const { result } = renderHook(() => useApiCache('test-key', mockCacheFunction, { staleTime: 1000 }) ) // First fetch await act(async () => { await result.current.fetchData() }) expect(result.current.data).toEqual(mockData1) // Advance time past stale time act(() => { vi.advanceTimersByTime(2000) }) // Second fetch should get fresh data await act(async () => { await result.current.fetchData() }) expect(result.current.data).toEqual(mockData2) expect(mockCacheFunction).toHaveBeenCalledTimes(2) }) it('should clear cache', async () => { const mockData = { id: 1, name: 'Test' } mockCacheFunction.mockResolvedValueOnce(mockData) const { result } = renderHook(() => useApiCache('test-key', mockCacheFunction) ) await act(async () => { await result.current.fetchData() }) expect(result.current.data).toEqual(mockData) await act(async () => { result.current.clearCache() }) expect(result.current.data).toBeNull() }) it('should indicate stale status', async () => { const mockData = { id: 1, name: 'Test' } mockCacheFunction.mockResolvedValueOnce(mockData) const { result } = renderHook(() => useApiCache('test-key', mockCacheFunction, { staleTime: 1000 }) ) // Initially stale (no data) expect(result.current.isStale).toBe(true) // Fetch data await act(async () => { await result.current.fetchData() }) // Should be fresh expect(result.current.isStale).toBe(false) // Advance time past stale time act(() => { vi.advanceTimersByTime(2000) }) // Should be stale again expect(result.current.isStale).toBe(true) }) })