Files
nuzlocke-tracker/frontend/src/hooks/useRuns.test.tsx
Julian Tabel 0d2f419c6a Add unit tests for frontend utilities and hooks
82 tests covering download.ts and all React Query hooks. API modules are
mocked with vi.mock; mutation tests spy on queryClient.invalidateQueries
to verify cache invalidation. Conditional queries (null id) are verified
to stay idle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 13:47:55 +01:00

182 lines
5.9 KiB
TypeScript

import { QueryClientProvider } from '@tanstack/react-query'
import { renderHook, waitFor, act } from '@testing-library/react'
import { createTestQueryClient } from '../test/utils'
import {
useRuns,
useRun,
useCreateRun,
useUpdateRun,
useDeleteRun,
useNamingCategories,
useNameSuggestions,
} from './useRuns'
vi.mock('../api/runs')
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }))
import { getRuns, getRun, createRun, updateRun, deleteRun, getNamingCategories } from '../api/runs'
import { toast } from 'sonner'
function createWrapper() {
const queryClient = createTestQueryClient()
const wrapper = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
return { queryClient, wrapper }
}
describe('useRuns', () => {
it('calls getRuns and returns data', async () => {
const runs = [{ id: 1, name: 'My Run' }]
vi.mocked(getRuns).mockResolvedValue(runs as never)
const { wrapper } = createWrapper()
const { result } = renderHook(() => useRuns(), { wrapper })
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(getRuns).toHaveBeenCalledOnce()
expect(result.current.data).toEqual(runs)
})
})
describe('useRun', () => {
it('calls getRun with the given id', async () => {
const run = { id: 3, name: 'Specific Run' }
vi.mocked(getRun).mockResolvedValue(run as never)
const { wrapper } = createWrapper()
const { result } = renderHook(() => useRun(3), { wrapper })
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(getRun).toHaveBeenCalledWith(3)
})
})
describe('useCreateRun', () => {
it('calls createRun with the provided input', async () => {
vi.mocked(createRun).mockResolvedValue({ id: 10 } as never)
const { wrapper } = createWrapper()
const { result } = renderHook(() => useCreateRun(), { wrapper })
await act(async () => {
await result.current.mutateAsync({ name: 'New Run', gameId: 1, status: 'active' } as never)
})
expect(createRun).toHaveBeenCalledWith({ name: 'New Run', gameId: 1, status: 'active' })
})
it('invalidates the runs query on success', async () => {
vi.mocked(createRun).mockResolvedValue({} as never)
const { queryClient, wrapper } = createWrapper()
const spy = vi.spyOn(queryClient, 'invalidateQueries')
const { result } = renderHook(() => useCreateRun(), { wrapper })
await act(async () => {
await result.current.mutateAsync({} as never)
})
expect(spy).toHaveBeenCalledWith({ queryKey: ['runs'] })
})
})
describe('useUpdateRun', () => {
it('calls updateRun with the given id and data', async () => {
vi.mocked(updateRun).mockResolvedValue({} as never)
const { wrapper } = createWrapper()
const { result } = renderHook(() => useUpdateRun(5), { wrapper })
await act(async () => {
await result.current.mutateAsync({ name: 'Updated' } as never)
})
expect(updateRun).toHaveBeenCalledWith(5, { name: 'Updated' })
})
it('invalidates both the list and individual run query on success', async () => {
vi.mocked(updateRun).mockResolvedValue({} as never)
const { queryClient, wrapper } = createWrapper()
const spy = vi.spyOn(queryClient, 'invalidateQueries')
const { result } = renderHook(() => useUpdateRun(5), { wrapper })
await act(async () => {
await result.current.mutateAsync({} as never)
})
expect(spy).toHaveBeenCalledWith({ queryKey: ['runs'] })
expect(spy).toHaveBeenCalledWith({ queryKey: ['runs', 5] })
})
it('shows a toast when status is set to completed', async () => {
vi.mocked(updateRun).mockResolvedValue({} as never)
const { wrapper } = createWrapper()
const { result } = renderHook(() => useUpdateRun(1), { wrapper })
await act(async () => {
await result.current.mutateAsync({ status: 'completed' } as never)
})
expect(toast.success).toHaveBeenCalledWith('Run marked as completed!')
})
it('shows an error toast on failure', async () => {
vi.mocked(updateRun).mockRejectedValue(new Error('Network error'))
const { wrapper } = createWrapper()
const { result } = renderHook(() => useUpdateRun(1), { wrapper })
await act(async () => {
await result.current.mutate({} as never)
})
await waitFor(() =>
expect(toast.error).toHaveBeenCalledWith('Failed to update run: Network error')
)
})
})
describe('useDeleteRun', () => {
it('calls deleteRun with the given id', async () => {
vi.mocked(deleteRun).mockResolvedValue(undefined as never)
const { wrapper } = createWrapper()
const { result } = renderHook(() => useDeleteRun(), { wrapper })
await act(async () => {
await result.current.mutateAsync(7)
})
expect(deleteRun).toHaveBeenCalledWith(7)
})
it('invalidates the runs query on success', async () => {
vi.mocked(deleteRun).mockResolvedValue(undefined as never)
const { queryClient, wrapper } = createWrapper()
const spy = vi.spyOn(queryClient, 'invalidateQueries')
const { result } = renderHook(() => useDeleteRun(), { wrapper })
await act(async () => {
await result.current.mutateAsync(7)
})
expect(spy).toHaveBeenCalledWith({ queryKey: ['runs'] })
})
})
describe('useNamingCategories', () => {
it('calls getNamingCategories', async () => {
vi.mocked(getNamingCategories).mockResolvedValue([] as never)
const { wrapper } = createWrapper()
const { result } = renderHook(() => useNamingCategories(), { wrapper })
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(getNamingCategories).toHaveBeenCalledOnce()
})
})
describe('useNameSuggestions', () => {
it('is disabled when runId is null', () => {
const { wrapper } = createWrapper()
const { result } = renderHook(() => useNameSuggestions(null), { wrapper })
expect(result.current.fetchStatus).toBe('idle')
})
})