Sets up API methods and types. Sets up a colorscheme. Sets up a homepage. Removes tailwind in favor of mui for now. Reviewed-on: #7 Co-authored-by: Drew Galbraith <drew@tiramisu.one> Co-committed-by: Drew Galbraith <drew@tiramisu.one>
359 lines
9.3 KiB
TypeScript
359 lines
9.3 KiB
TypeScript
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)
|
|
})
|
|
})
|