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>
This commit is contained in:
@@ -1,31 +1,27 @@
|
|||||||
---
|
---
|
||||||
# nuzlocke-tracker-ee9s
|
# nuzlocke-tracker-ee9s
|
||||||
title: Unit tests for frontend utilities and hooks
|
title: Unit tests for frontend utilities and hooks
|
||||||
status: draft
|
status: completed
|
||||||
type: task
|
type: task
|
||||||
|
priority: normal
|
||||||
created_at: 2026-02-10T09:33:38Z
|
created_at: 2026-02-10T09:33:38Z
|
||||||
updated_at: 2026-02-10T09:33:38Z
|
updated_at: 2026-02-21T12:47:19Z
|
||||||
parent: nuzlocke-tracker-yzpb
|
parent: nuzlocke-tracker-yzpb
|
||||||
---
|
---
|
||||||
|
|
||||||
Write unit tests for the frontend utility functions and custom React hooks.
|
Write unit tests for the frontend utility functions and custom React hooks.
|
||||||
|
|
||||||
|
All API modules are mocked with `vi.mock`. Hooks are tested with `renderHook` from @testing-library/react, wrapped in `QueryClientProvider`. Mutation tests spy on `queryClient.invalidateQueries` to verify cache invalidation.
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
- [ ] Test `utils/formatEvolution.ts` — evolution chain formatting logic
|
- [x] Test `utils/formatEvolution.ts` — done in smoke test
|
||||||
- [ ] Test `utils/download.ts` — file download utility
|
- [x] Test `utils/download.ts` — blob URL creation, filename, cleanup
|
||||||
- [ ] Test `hooks/useRuns.ts` — run CRUD hook with mocked API
|
- [x] Test `hooks/useGames.ts` — query hooks and disabled state
|
||||||
- [ ] Test `hooks/useGames.ts` — game fetching hook
|
- [x] Test `hooks/useRuns.ts` — query hooks + mutations with cache invalidation
|
||||||
- [ ] Test `hooks/useEncounters.ts` — encounter operations hook
|
- [x] Test `hooks/useEncounters.ts` — mutations and conditional queries
|
||||||
- [ ] Test `hooks/usePokemon.ts` — pokemon data hook
|
- [x] Test `hooks/usePokemon.ts` — conditional queries
|
||||||
- [ ] Test `hooks/useGenlockes.ts` — genlocke operations hook
|
- [x] Test `hooks/useGenlockes.ts` — queries and mutations
|
||||||
- [ ] Test `hooks/useBosses.ts` — boss operations hook
|
- [x] Test `hooks/useBosses.ts` — queries and mutations
|
||||||
- [ ] Test `hooks/useStats.ts` — stats fetching hook
|
- [x] Test `hooks/useStats.ts` — single query hook
|
||||||
- [ ] Test `hooks/useAdmin.ts` — admin operations hook
|
- [x] Test `hooks/useAdmin.ts` — representative subset (usePokemonList, useCreateGame, useDeleteGame)
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Utility functions are pure functions — straightforward to test
|
|
||||||
- Hooks wrap React Query — test that they call the right API endpoints, handle loading/error states, and invalidate queries correctly
|
|
||||||
- Use `@testing-library/react`'s `renderHook` for hook testing
|
|
||||||
- Mock the API client (from `src/api/`) rather than individual fetch calls
|
|
||||||
|
|||||||
@@ -24,5 +24,5 @@ Add comprehensive unit and integration test coverage to both the backend (FastAP
|
|||||||
- [ ] Backend schemas and services have unit test coverage
|
- [ ] Backend schemas and services have unit test coverage
|
||||||
- [x] Backend API endpoints have integration test coverage
|
- [x] Backend API endpoints have integration test coverage
|
||||||
- [x] Frontend test infrastructure is set up (Vitest, RTL)
|
- [x] Frontend test infrastructure is set up (Vitest, RTL)
|
||||||
- [ ] Frontend utilities and hooks have unit test coverage
|
- [x] Frontend utilities and hooks have unit test coverage
|
||||||
- [ ] Frontend components have basic render/interaction tests
|
- [ ] Frontend components have basic render/interaction tests
|
||||||
148
frontend/src/hooks/useAdmin.test.tsx
Normal file
148
frontend/src/hooks/useAdmin.test.tsx
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import { QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import { renderHook, waitFor, act } from '@testing-library/react'
|
||||||
|
import { createTestQueryClient } from '../test/utils'
|
||||||
|
import { usePokemonList, useCreateGame, useUpdateGame, useDeleteGame } from './useAdmin'
|
||||||
|
|
||||||
|
vi.mock('../api/admin')
|
||||||
|
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }))
|
||||||
|
|
||||||
|
import * as adminApi from '../api/admin'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
|
function createWrapper() {
|
||||||
|
const queryClient = createTestQueryClient()
|
||||||
|
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||||
|
)
|
||||||
|
return { queryClient, wrapper }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('usePokemonList', () => {
|
||||||
|
it('calls listPokemon with defaults', async () => {
|
||||||
|
vi.mocked(adminApi.listPokemon).mockResolvedValue({ results: [], total: 0 } as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
const { result } = renderHook(() => usePokemonList(), { wrapper })
|
||||||
|
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||||
|
|
||||||
|
expect(adminApi.listPokemon).toHaveBeenCalledWith(undefined, 50, 0, undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('passes search and filter params to listPokemon', async () => {
|
||||||
|
vi.mocked(adminApi.listPokemon).mockResolvedValue({ results: [], total: 0 } as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
renderHook(() => usePokemonList('pika', 10, 20, 'electric'), { wrapper })
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(adminApi.listPokemon).toHaveBeenCalledWith('pika', 10, 20, 'electric')
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useCreateGame', () => {
|
||||||
|
it('calls createGame with the provided input', async () => {
|
||||||
|
vi.mocked(adminApi.createGame).mockResolvedValue({ id: 1 } as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useCreateGame(), { wrapper })
|
||||||
|
const input = { name: 'FireRed', slug: 'firered', generation: 3, region: 'kanto', vgId: 1 }
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.mutateAsync(input as never)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(adminApi.createGame).toHaveBeenCalledWith(input)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('invalidates the games query on success', async () => {
|
||||||
|
vi.mocked(adminApi.createGame).mockResolvedValue({} as never)
|
||||||
|
const { queryClient, wrapper } = createWrapper()
|
||||||
|
const spy = vi.spyOn(queryClient, 'invalidateQueries')
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useCreateGame(), { wrapper })
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.mutateAsync({} as never)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalledWith({ queryKey: ['games'] })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows a success toast after creating a game', async () => {
|
||||||
|
vi.mocked(adminApi.createGame).mockResolvedValue({} as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useCreateGame(), { wrapper })
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.mutateAsync({} as never)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(toast.success).toHaveBeenCalledWith('Game created')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows an error toast on failure', async () => {
|
||||||
|
vi.mocked(adminApi.createGame).mockRejectedValue(new Error('Conflict'))
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useCreateGame(), { wrapper })
|
||||||
|
await act(async () => {
|
||||||
|
result.current.mutate({} as never)
|
||||||
|
})
|
||||||
|
|
||||||
|
await waitFor(() => expect(toast.error).toHaveBeenCalledWith('Failed to create game: Conflict'))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useUpdateGame', () => {
|
||||||
|
it('calls updateGame with id and data', async () => {
|
||||||
|
vi.mocked(adminApi.updateGame).mockResolvedValue({} as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useUpdateGame(), { wrapper })
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.mutateAsync({ id: 7, data: { name: 'Renamed' } } as never)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(adminApi.updateGame).toHaveBeenCalledWith(7, { name: 'Renamed' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('invalidates games and shows a toast on success', async () => {
|
||||||
|
vi.mocked(adminApi.updateGame).mockResolvedValue({} as never)
|
||||||
|
const { queryClient, wrapper } = createWrapper()
|
||||||
|
const spy = vi.spyOn(queryClient, 'invalidateQueries')
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useUpdateGame(), { wrapper })
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.mutateAsync({ id: 1, data: {} } as never)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalledWith({ queryKey: ['games'] })
|
||||||
|
expect(toast.success).toHaveBeenCalledWith('Game updated')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useDeleteGame', () => {
|
||||||
|
it('calls deleteGame with the given id', async () => {
|
||||||
|
vi.mocked(adminApi.deleteGame).mockResolvedValue(undefined as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useDeleteGame(), { wrapper })
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.mutateAsync(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(adminApi.deleteGame).toHaveBeenCalledWith(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('invalidates games and shows a toast on success', async () => {
|
||||||
|
vi.mocked(adminApi.deleteGame).mockResolvedValue(undefined as never)
|
||||||
|
const { queryClient, wrapper } = createWrapper()
|
||||||
|
const spy = vi.spyOn(queryClient, 'invalidateQueries')
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useDeleteGame(), { wrapper })
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.mutateAsync(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalledWith({ queryKey: ['games'] })
|
||||||
|
expect(toast.success).toHaveBeenCalledWith('Game deleted')
|
||||||
|
})
|
||||||
|
})
|
||||||
118
frontend/src/hooks/useBosses.test.tsx
Normal file
118
frontend/src/hooks/useBosses.test.tsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import { renderHook, waitFor, act } from '@testing-library/react'
|
||||||
|
import { createTestQueryClient } from '../test/utils'
|
||||||
|
import {
|
||||||
|
useGameBosses,
|
||||||
|
useBossResults,
|
||||||
|
useCreateBossResult,
|
||||||
|
useDeleteBossResult,
|
||||||
|
} from './useBosses'
|
||||||
|
|
||||||
|
vi.mock('../api/bosses')
|
||||||
|
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }))
|
||||||
|
|
||||||
|
import { getGameBosses, getBossResults, createBossResult, deleteBossResult } from '../api/bosses'
|
||||||
|
|
||||||
|
function createWrapper() {
|
||||||
|
const queryClient = createTestQueryClient()
|
||||||
|
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||||
|
)
|
||||||
|
return { queryClient, wrapper }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('useGameBosses', () => {
|
||||||
|
it('is disabled when gameId is null', () => {
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
const { result } = renderHook(() => useGameBosses(null), { wrapper })
|
||||||
|
expect(result.current.fetchStatus).toBe('idle')
|
||||||
|
expect(getGameBosses).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fetches bosses for a given game', async () => {
|
||||||
|
const bosses = [{ id: 1, name: 'Brock' }]
|
||||||
|
vi.mocked(getGameBosses).mockResolvedValue(bosses as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useGameBosses(1), { wrapper })
|
||||||
|
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||||
|
|
||||||
|
expect(getGameBosses).toHaveBeenCalledWith(1, undefined)
|
||||||
|
expect(result.current.data).toEqual(bosses)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('passes the all flag to the API', async () => {
|
||||||
|
vi.mocked(getGameBosses).mockResolvedValue([] as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
renderHook(() => useGameBosses(2, true), { wrapper })
|
||||||
|
await waitFor(() => expect(getGameBosses).toHaveBeenCalledWith(2, true))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useBossResults', () => {
|
||||||
|
it('fetches boss results for a given run', async () => {
|
||||||
|
vi.mocked(getBossResults).mockResolvedValue([] as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useBossResults(10), { wrapper })
|
||||||
|
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||||
|
|
||||||
|
expect(getBossResults).toHaveBeenCalledWith(10)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useCreateBossResult', () => {
|
||||||
|
it('calls createBossResult with the run id and input', async () => {
|
||||||
|
vi.mocked(createBossResult).mockResolvedValue({} as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useCreateBossResult(5), { wrapper })
|
||||||
|
const input = { bossId: 1, won: true }
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.mutateAsync(input as never)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(createBossResult).toHaveBeenCalledWith(5, input)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('invalidates boss results for the run on success', async () => {
|
||||||
|
vi.mocked(createBossResult).mockResolvedValue({} as never)
|
||||||
|
const { queryClient, wrapper } = createWrapper()
|
||||||
|
const spy = vi.spyOn(queryClient, 'invalidateQueries')
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useCreateBossResult(5), { wrapper })
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.mutateAsync({} as never)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalledWith({ queryKey: ['runs', 5, 'boss-results'] })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useDeleteBossResult', () => {
|
||||||
|
it('calls deleteBossResult with the run id and result id', async () => {
|
||||||
|
vi.mocked(deleteBossResult).mockResolvedValue(undefined as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useDeleteBossResult(5), { wrapper })
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.mutateAsync(99)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(deleteBossResult).toHaveBeenCalledWith(5, 99)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('invalidates boss results for the run on success', async () => {
|
||||||
|
vi.mocked(deleteBossResult).mockResolvedValue(undefined as never)
|
||||||
|
const { queryClient, wrapper } = createWrapper()
|
||||||
|
const spy = vi.spyOn(queryClient, 'invalidateQueries')
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useDeleteBossResult(5), { wrapper })
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.mutateAsync(99)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalledWith({ queryKey: ['runs', 5, 'boss-results'] })
|
||||||
|
})
|
||||||
|
})
|
||||||
161
frontend/src/hooks/useEncounters.test.tsx
Normal file
161
frontend/src/hooks/useEncounters.test.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import { QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import { renderHook, waitFor, act } from '@testing-library/react'
|
||||||
|
import { createTestQueryClient } from '../test/utils'
|
||||||
|
import {
|
||||||
|
useCreateEncounter,
|
||||||
|
useUpdateEncounter,
|
||||||
|
useDeleteEncounter,
|
||||||
|
useEvolutions,
|
||||||
|
useForms,
|
||||||
|
useBulkRandomize,
|
||||||
|
} from './useEncounters'
|
||||||
|
|
||||||
|
vi.mock('../api/encounters')
|
||||||
|
|
||||||
|
import {
|
||||||
|
createEncounter,
|
||||||
|
updateEncounter,
|
||||||
|
deleteEncounter,
|
||||||
|
fetchEvolutions,
|
||||||
|
fetchForms,
|
||||||
|
bulkRandomizeEncounters,
|
||||||
|
} from '../api/encounters'
|
||||||
|
|
||||||
|
function createWrapper() {
|
||||||
|
const queryClient = createTestQueryClient()
|
||||||
|
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||||
|
)
|
||||||
|
return { queryClient, wrapper }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('useCreateEncounter', () => {
|
||||||
|
it('calls createEncounter with the run id and input', async () => {
|
||||||
|
vi.mocked(createEncounter).mockResolvedValue({} as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useCreateEncounter(3), { wrapper })
|
||||||
|
const input = { routeId: 1, pokemonId: 25, status: 'caught' }
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.mutateAsync(input as never)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(createEncounter).toHaveBeenCalledWith(3, input)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('invalidates the run query on success', async () => {
|
||||||
|
vi.mocked(createEncounter).mockResolvedValue({} as never)
|
||||||
|
const { queryClient, wrapper } = createWrapper()
|
||||||
|
const spy = vi.spyOn(queryClient, 'invalidateQueries')
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useCreateEncounter(3), { wrapper })
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.mutateAsync({} as never)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalledWith({ queryKey: ['runs', 3] })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useUpdateEncounter', () => {
|
||||||
|
it('calls updateEncounter with id and data', async () => {
|
||||||
|
vi.mocked(updateEncounter).mockResolvedValue({} as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useUpdateEncounter(3), { wrapper })
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.mutateAsync({ id: 42, data: { status: 'dead' } } as never)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(updateEncounter).toHaveBeenCalledWith(42, { status: 'dead' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('invalidates the run query on success', async () => {
|
||||||
|
vi.mocked(updateEncounter).mockResolvedValue({} as never)
|
||||||
|
const { queryClient, wrapper } = createWrapper()
|
||||||
|
const spy = vi.spyOn(queryClient, 'invalidateQueries')
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useUpdateEncounter(3), { wrapper })
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.mutateAsync({ id: 1, data: {} } as never)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalledWith({ queryKey: ['runs', 3] })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useDeleteEncounter', () => {
|
||||||
|
it('calls deleteEncounter with the encounter id', async () => {
|
||||||
|
vi.mocked(deleteEncounter).mockResolvedValue(undefined as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useDeleteEncounter(3), { wrapper })
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.mutateAsync(55)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(deleteEncounter).toHaveBeenCalledWith(55)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('invalidates the run query on success', async () => {
|
||||||
|
vi.mocked(deleteEncounter).mockResolvedValue(undefined as never)
|
||||||
|
const { queryClient, wrapper } = createWrapper()
|
||||||
|
const spy = vi.spyOn(queryClient, 'invalidateQueries')
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useDeleteEncounter(3), { wrapper })
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.mutateAsync(55)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalledWith({ queryKey: ['runs', 3] })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useEvolutions', () => {
|
||||||
|
it('is disabled when pokemonId is null', () => {
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
const { result } = renderHook(() => useEvolutions(null), { wrapper })
|
||||||
|
expect(result.current.fetchStatus).toBe('idle')
|
||||||
|
expect(fetchEvolutions).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fetches evolutions for a given pokemon', async () => {
|
||||||
|
vi.mocked(fetchEvolutions).mockResolvedValue([] as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
renderHook(() => useEvolutions(25, 'kanto'), { wrapper })
|
||||||
|
await waitFor(() => expect(fetchEvolutions).toHaveBeenCalledWith(25, 'kanto'))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useForms', () => {
|
||||||
|
it('is disabled when pokemonId is null', () => {
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
const { result } = renderHook(() => useForms(null), { wrapper })
|
||||||
|
expect(result.current.fetchStatus).toBe('idle')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fetches forms for a given pokemon', async () => {
|
||||||
|
vi.mocked(fetchForms).mockResolvedValue([] as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
renderHook(() => useForms(133), { wrapper })
|
||||||
|
await waitFor(() => expect(fetchForms).toHaveBeenCalledWith(133))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useBulkRandomize', () => {
|
||||||
|
it('calls bulkRandomizeEncounters and invalidates the run', async () => {
|
||||||
|
vi.mocked(bulkRandomizeEncounters).mockResolvedValue([] as never)
|
||||||
|
const { queryClient, wrapper } = createWrapper()
|
||||||
|
const spy = vi.spyOn(queryClient, 'invalidateQueries')
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useBulkRandomize(4), { wrapper })
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.mutateAsync()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(bulkRandomizeEncounters).toHaveBeenCalledWith(4)
|
||||||
|
expect(spy).toHaveBeenCalledWith({ queryKey: ['runs', 4] })
|
||||||
|
})
|
||||||
|
})
|
||||||
89
frontend/src/hooks/useGames.test.tsx
Normal file
89
frontend/src/hooks/useGames.test.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import { renderHook, waitFor } from '@testing-library/react'
|
||||||
|
import { createTestQueryClient } from '../test/utils'
|
||||||
|
import { useGames, useGame, useGameRoutes, useRoutePokemon } from './useGames'
|
||||||
|
|
||||||
|
vi.mock('../api/games')
|
||||||
|
|
||||||
|
import { getGames, getGame, getGameRoutes, getRoutePokemon } from '../api/games'
|
||||||
|
|
||||||
|
function createWrapper() {
|
||||||
|
const queryClient = createTestQueryClient()
|
||||||
|
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||||
|
)
|
||||||
|
return { queryClient, wrapper }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('useGames', () => {
|
||||||
|
it('calls getGames and returns data', async () => {
|
||||||
|
const games = [{ id: 1, name: 'Red' }]
|
||||||
|
vi.mocked(getGames).mockResolvedValue(games as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useGames(), { wrapper })
|
||||||
|
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||||
|
|
||||||
|
expect(getGames).toHaveBeenCalledOnce()
|
||||||
|
expect(result.current.data).toEqual(games)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useGame', () => {
|
||||||
|
it('calls getGame with the given id', async () => {
|
||||||
|
const game = { id: 2, name: 'Blue' }
|
||||||
|
vi.mocked(getGame).mockResolvedValue(game as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useGame(2), { wrapper })
|
||||||
|
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||||
|
|
||||||
|
expect(getGame).toHaveBeenCalledWith(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useGameRoutes', () => {
|
||||||
|
it('is disabled when gameId is null', () => {
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
const { result } = renderHook(() => useGameRoutes(null), { wrapper })
|
||||||
|
expect(result.current.fetchStatus).toBe('idle')
|
||||||
|
expect(getGameRoutes).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fetches routes when gameId is provided', async () => {
|
||||||
|
const routes = [{ id: 10, name: 'Route 1' }]
|
||||||
|
vi.mocked(getGameRoutes).mockResolvedValue(routes as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useGameRoutes(1), { wrapper })
|
||||||
|
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||||
|
|
||||||
|
expect(getGameRoutes).toHaveBeenCalledWith(1, undefined)
|
||||||
|
expect(result.current.data).toEqual(routes)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('passes allowedTypes to the API', async () => {
|
||||||
|
vi.mocked(getGameRoutes).mockResolvedValue([] as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
renderHook(() => useGameRoutes(5, ['grass', 'water']), { wrapper })
|
||||||
|
await waitFor(() => expect(getGameRoutes).toHaveBeenCalledWith(5, ['grass', 'water']))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useRoutePokemon', () => {
|
||||||
|
it('is disabled when routeId is null', () => {
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
const { result } = renderHook(() => useRoutePokemon(null), { wrapper })
|
||||||
|
expect(result.current.fetchStatus).toBe('idle')
|
||||||
|
expect(getRoutePokemon).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fetches pokemon for a given route', async () => {
|
||||||
|
vi.mocked(getRoutePokemon).mockResolvedValue([] as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
renderHook(() => useRoutePokemon(3, 1), { wrapper })
|
||||||
|
await waitFor(() => expect(getRoutePokemon).toHaveBeenCalledWith(3, 1))
|
||||||
|
})
|
||||||
|
})
|
||||||
178
frontend/src/hooks/useGenlockes.test.tsx
Normal file
178
frontend/src/hooks/useGenlockes.test.tsx
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import { QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import { renderHook, waitFor, act } from '@testing-library/react'
|
||||||
|
import { createTestQueryClient } from '../test/utils'
|
||||||
|
import {
|
||||||
|
useGenlockes,
|
||||||
|
useGenlocke,
|
||||||
|
useGenlockeGraveyard,
|
||||||
|
useGenlockeLineages,
|
||||||
|
useRegions,
|
||||||
|
useCreateGenlocke,
|
||||||
|
useLegSurvivors,
|
||||||
|
useAdvanceLeg,
|
||||||
|
} from './useGenlockes'
|
||||||
|
|
||||||
|
vi.mock('../api/genlockes')
|
||||||
|
|
||||||
|
import {
|
||||||
|
getGenlockes,
|
||||||
|
getGenlocke,
|
||||||
|
getGenlockeGraveyard,
|
||||||
|
getGenlockeLineages,
|
||||||
|
getGamesByRegion,
|
||||||
|
createGenlocke,
|
||||||
|
getLegSurvivors,
|
||||||
|
advanceLeg,
|
||||||
|
} from '../api/genlockes'
|
||||||
|
|
||||||
|
function createWrapper() {
|
||||||
|
const queryClient = createTestQueryClient()
|
||||||
|
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||||
|
)
|
||||||
|
return { queryClient, wrapper }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('useGenlockes', () => {
|
||||||
|
it('calls getGenlockes and returns data', async () => {
|
||||||
|
const genlockes = [{ id: 1, name: 'Gen 1 Run' }]
|
||||||
|
vi.mocked(getGenlockes).mockResolvedValue(genlockes as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useGenlockes(), { wrapper })
|
||||||
|
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||||
|
|
||||||
|
expect(getGenlockes).toHaveBeenCalledOnce()
|
||||||
|
expect(result.current.data).toEqual(genlockes)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useGenlocke', () => {
|
||||||
|
it('calls getGenlocke with the given id', async () => {
|
||||||
|
vi.mocked(getGenlocke).mockResolvedValue({ id: 2 } as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useGenlocke(2), { wrapper })
|
||||||
|
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||||
|
|
||||||
|
expect(getGenlocke).toHaveBeenCalledWith(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useGenlockeGraveyard', () => {
|
||||||
|
it('calls getGenlockeGraveyard with the given id', async () => {
|
||||||
|
vi.mocked(getGenlockeGraveyard).mockResolvedValue([] as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
renderHook(() => useGenlockeGraveyard(3), { wrapper })
|
||||||
|
await waitFor(() => expect(getGenlockeGraveyard).toHaveBeenCalledWith(3))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useGenlockeLineages', () => {
|
||||||
|
it('calls getGenlockeLineages with the given id', async () => {
|
||||||
|
vi.mocked(getGenlockeLineages).mockResolvedValue([] as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
renderHook(() => useGenlockeLineages(3), { wrapper })
|
||||||
|
await waitFor(() => expect(getGenlockeLineages).toHaveBeenCalledWith(3))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useRegions', () => {
|
||||||
|
it('calls getGamesByRegion', async () => {
|
||||||
|
vi.mocked(getGamesByRegion).mockResolvedValue([] as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useRegions(), { wrapper })
|
||||||
|
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||||
|
|
||||||
|
expect(getGamesByRegion).toHaveBeenCalledOnce()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useCreateGenlocke', () => {
|
||||||
|
it('calls createGenlocke with the provided input', async () => {
|
||||||
|
vi.mocked(createGenlocke).mockResolvedValue({ id: 10 } as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useCreateGenlocke(), { wrapper })
|
||||||
|
const input = { name: 'New Genlocke', gameIds: [1, 2] }
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.mutateAsync(input as never)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(createGenlocke).toHaveBeenCalledWith(input)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('invalidates both runs and genlockes on success', async () => {
|
||||||
|
vi.mocked(createGenlocke).mockResolvedValue({} as never)
|
||||||
|
const { queryClient, wrapper } = createWrapper()
|
||||||
|
const spy = vi.spyOn(queryClient, 'invalidateQueries')
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useCreateGenlocke(), { wrapper })
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.mutateAsync({} as never)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalledWith({ queryKey: ['runs'] })
|
||||||
|
expect(spy).toHaveBeenCalledWith({ queryKey: ['genlockes'] })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useLegSurvivors', () => {
|
||||||
|
it('is disabled when enabled is false', () => {
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
const { result } = renderHook(() => useLegSurvivors(1, 1, false), { wrapper })
|
||||||
|
expect(result.current.fetchStatus).toBe('idle')
|
||||||
|
expect(getLegSurvivors).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fetches survivors when enabled', async () => {
|
||||||
|
vi.mocked(getLegSurvivors).mockResolvedValue([] as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
renderHook(() => useLegSurvivors(1, 2, true), { wrapper })
|
||||||
|
await waitFor(() => expect(getLegSurvivors).toHaveBeenCalledWith(1, 2))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useAdvanceLeg', () => {
|
||||||
|
it('calls advanceLeg with genlocke id and leg order', async () => {
|
||||||
|
vi.mocked(advanceLeg).mockResolvedValue({} as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAdvanceLeg(), { wrapper })
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.mutateAsync({ genlockeId: 1, legOrder: 1 })
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(advanceLeg).toHaveBeenCalledWith(1, 1, undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('passes transferEncounterIds when provided', async () => {
|
||||||
|
vi.mocked(advanceLeg).mockResolvedValue({} as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAdvanceLeg(), { wrapper })
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.mutateAsync({ genlockeId: 2, legOrder: 3, transferEncounterIds: [4, 5] })
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(advanceLeg).toHaveBeenCalledWith(2, 3, { transferEncounterIds: [4, 5] })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('invalidates runs and genlockes on success', async () => {
|
||||||
|
vi.mocked(advanceLeg).mockResolvedValue({} as never)
|
||||||
|
const { queryClient, wrapper } = createWrapper()
|
||||||
|
const spy = vi.spyOn(queryClient, 'invalidateQueries')
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAdvanceLeg(), { wrapper })
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.mutateAsync({ genlockeId: 1, legOrder: 1 })
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalledWith({ queryKey: ['runs'] })
|
||||||
|
expect(spy).toHaveBeenCalledWith({ queryKey: ['genlockes'] })
|
||||||
|
})
|
||||||
|
})
|
||||||
93
frontend/src/hooks/usePokemon.test.tsx
Normal file
93
frontend/src/hooks/usePokemon.test.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import { renderHook, waitFor } from '@testing-library/react'
|
||||||
|
import { createTestQueryClient } from '../test/utils'
|
||||||
|
import {
|
||||||
|
usePokemon,
|
||||||
|
usePokemonFamilies,
|
||||||
|
usePokemonEncounterLocations,
|
||||||
|
usePokemonEvolutionChain,
|
||||||
|
} from './usePokemon'
|
||||||
|
|
||||||
|
vi.mock('../api/pokemon')
|
||||||
|
|
||||||
|
import {
|
||||||
|
getPokemon,
|
||||||
|
fetchPokemonFamilies,
|
||||||
|
fetchPokemonEncounterLocations,
|
||||||
|
fetchPokemonEvolutionChain,
|
||||||
|
} from '../api/pokemon'
|
||||||
|
|
||||||
|
function createWrapper() {
|
||||||
|
const queryClient = createTestQueryClient()
|
||||||
|
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||||
|
)
|
||||||
|
return { queryClient, wrapper }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('usePokemon', () => {
|
||||||
|
it('is disabled when id is null', () => {
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
const { result } = renderHook(() => usePokemon(null), { wrapper })
|
||||||
|
expect(result.current.fetchStatus).toBe('idle')
|
||||||
|
expect(getPokemon).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fetches a pokemon by id', async () => {
|
||||||
|
const mon = { id: 25, name: 'pikachu' }
|
||||||
|
vi.mocked(getPokemon).mockResolvedValue(mon as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
const { result } = renderHook(() => usePokemon(25), { wrapper })
|
||||||
|
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||||
|
|
||||||
|
expect(getPokemon).toHaveBeenCalledWith(25)
|
||||||
|
expect(result.current.data).toEqual(mon)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('usePokemonFamilies', () => {
|
||||||
|
it('calls fetchPokemonFamilies and returns data', async () => {
|
||||||
|
const families = [{ id: 1, members: [] }]
|
||||||
|
vi.mocked(fetchPokemonFamilies).mockResolvedValue(families as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
const { result } = renderHook(() => usePokemonFamilies(), { wrapper })
|
||||||
|
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||||
|
|
||||||
|
expect(fetchPokemonFamilies).toHaveBeenCalledOnce()
|
||||||
|
expect(result.current.data).toEqual(families)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('usePokemonEncounterLocations', () => {
|
||||||
|
it('is disabled when pokemonId is null', () => {
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
const { result } = renderHook(() => usePokemonEncounterLocations(null), { wrapper })
|
||||||
|
expect(result.current.fetchStatus).toBe('idle')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fetches encounter locations for a given pokemon', async () => {
|
||||||
|
vi.mocked(fetchPokemonEncounterLocations).mockResolvedValue([] as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
renderHook(() => usePokemonEncounterLocations(25), { wrapper })
|
||||||
|
await waitFor(() => expect(fetchPokemonEncounterLocations).toHaveBeenCalledWith(25))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('usePokemonEvolutionChain', () => {
|
||||||
|
it('is disabled when pokemonId is null', () => {
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
const { result } = renderHook(() => usePokemonEvolutionChain(null), { wrapper })
|
||||||
|
expect(result.current.fetchStatus).toBe('idle')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fetches the evolution chain for a given pokemon', async () => {
|
||||||
|
vi.mocked(fetchPokemonEvolutionChain).mockResolvedValue([] as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
renderHook(() => usePokemonEvolutionChain(4), { wrapper })
|
||||||
|
await waitFor(() => expect(fetchPokemonEvolutionChain).toHaveBeenCalledWith(4))
|
||||||
|
})
|
||||||
|
})
|
||||||
181
frontend/src/hooks/useRuns.test.tsx
Normal file
181
frontend/src/hooks/useRuns.test.tsx
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
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')
|
||||||
|
})
|
||||||
|
})
|
||||||
38
frontend/src/hooks/useStats.test.tsx
Normal file
38
frontend/src/hooks/useStats.test.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import { renderHook, waitFor } from '@testing-library/react'
|
||||||
|
import { createTestQueryClient } from '../test/utils'
|
||||||
|
import { useStats } from './useStats'
|
||||||
|
|
||||||
|
vi.mock('../api/stats')
|
||||||
|
|
||||||
|
import { getStats } from '../api/stats'
|
||||||
|
|
||||||
|
function createWrapper() {
|
||||||
|
const queryClient = createTestQueryClient()
|
||||||
|
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||||
|
)
|
||||||
|
return { queryClient, wrapper }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('useStats', () => {
|
||||||
|
it('calls getStats and returns data', async () => {
|
||||||
|
const stats = { totalRuns: 5, activeRuns: 2 }
|
||||||
|
vi.mocked(getStats).mockResolvedValue(stats as never)
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useStats(), { wrapper })
|
||||||
|
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
||||||
|
|
||||||
|
expect(getStats).toHaveBeenCalledOnce()
|
||||||
|
expect(result.current.data).toEqual(stats)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reflects loading state before data resolves', () => {
|
||||||
|
vi.mocked(getStats).mockReturnValue(new Promise(() => undefined))
|
||||||
|
const { wrapper } = createWrapper()
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useStats(), { wrapper })
|
||||||
|
expect(result.current.isLoading).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
47
frontend/src/utils/download.test.ts
Normal file
47
frontend/src/utils/download.test.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { downloadJson } from './download'
|
||||||
|
|
||||||
|
describe('downloadJson', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:mock-url')
|
||||||
|
vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates a blob URL from the JSON data', () => {
|
||||||
|
downloadJson({ x: 1 }, 'export.json')
|
||||||
|
expect(URL.createObjectURL).toHaveBeenCalledOnce()
|
||||||
|
const blob = vi.mocked(URL.createObjectURL).mock.calls[0]?.[0] as Blob
|
||||||
|
expect(blob.type).toBe('application/json')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('revokes the blob URL after triggering the download', () => {
|
||||||
|
downloadJson({ x: 1 }, 'export.json')
|
||||||
|
expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:mock-url')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets the correct download filename on the anchor', () => {
|
||||||
|
const spy = vi.spyOn(document, 'createElement')
|
||||||
|
downloadJson({ x: 1 }, 'my-data.json')
|
||||||
|
const anchor = spy.mock.results[0]?.value as HTMLAnchorElement
|
||||||
|
expect(anchor.download).toBe('my-data.json')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('appends and removes the anchor from the document body', () => {
|
||||||
|
const appendSpy = vi.spyOn(document.body, 'appendChild')
|
||||||
|
const removeSpy = vi.spyOn(document.body, 'removeChild')
|
||||||
|
downloadJson({}, 'empty.json')
|
||||||
|
expect(appendSpy).toHaveBeenCalledOnce()
|
||||||
|
expect(removeSpy).toHaveBeenCalledOnce()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('serializes the data as formatted JSON', () => {
|
||||||
|
downloadJson({ a: 1, b: [2, 3] }, 'data.json')
|
||||||
|
const blob = vi.mocked(URL.createObjectURL).mock.calls[0]?.[0] as Blob
|
||||||
|
// Blob is constructed but content can't be read synchronously in jsdom;
|
||||||
|
// verifying type and that createObjectURL was called with a Blob is enough.
|
||||||
|
expect(blob).toBeInstanceOf(Blob)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user