Frontend API structure.
This commit is contained in:
parent
7d2b7fc90c
commit
c443a13a14
10 changed files with 1810 additions and 4 deletions
359
frontend/app/hooks/useApi.test.ts
Normal file
359
frontend/app/hooks/useApi.test.ts
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
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)
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue