diff --git a/frontend/src/hooks/__tests__/useCases.test.ts b/frontend/src/hooks/__tests__/useCases.test.ts new file mode 100644 index 0000000..4b7694c --- /dev/null +++ b/frontend/src/hooks/__tests__/useCases.test.ts @@ -0,0 +1,291 @@ +import { describe, it, expect } from 'vitest' +import { waitFor, act } from '@testing-library/react' +import { http, HttpResponse } from 'msw' +import { server } from '@/test/mocks/server' +import { renderHookWithProviders } from '@/test/utils' +import { + mockCase, + mockCaseListResponse, + mockEmptyCaseList, +} from '@/test/mocks/data' +import { + useCases, + usePendingIcdCases, + useCaseUpdate, + useKvnrUpdate, + useIcdUpdate, +} from '@/hooks/useCases' +import type { CaseFilters } from '@/hooks/useCases' + +// --------------------------------------------------------------------------- +// useCases +// --------------------------------------------------------------------------- + +describe('useCases', () => { + const defaultFilters: CaseFilters = { page: 1, per_page: 20 } + + it('fetches cases with default filters (success)', async () => { + const { result } = renderHookWithProviders(() => useCases(defaultFilters)) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + + expect(result.current.data).toEqual(mockCaseListResponse) + expect(result.current.data!.items).toHaveLength(2) + expect(result.current.data!.total).toBe(2) + }) + + it('passes filter params to the API (search, jahr, fallgruppe, has_icd)', async () => { + const capturedParams: Record = {} + + server.use( + http.get('/api/cases/', ({ request }) => { + const url = new URL(request.url) + capturedParams.search = url.searchParams.get('search') + capturedParams.jahr = url.searchParams.get('jahr') + capturedParams.fallgruppe = url.searchParams.get('fallgruppe') + capturedParams.has_icd = url.searchParams.get('has_icd') + capturedParams.page = url.searchParams.get('page') + capturedParams.per_page = url.searchParams.get('per_page') + return HttpResponse.json(mockCaseListResponse) + }), + ) + + const filters: CaseFilters = { + page: 2, + per_page: 10, + search: 'Meier', + jahr: 2026, + fallgruppe: 'onko', + has_icd: 'true', + } + + const { result } = renderHookWithProviders(() => useCases(filters)) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + + expect(capturedParams.search).toBe('Meier') + expect(capturedParams.jahr).toBe('2026') + expect(capturedParams.fallgruppe).toBe('onko') + expect(capturedParams.has_icd).toBe('true') + expect(capturedParams.page).toBe('2') + expect(capturedParams.per_page).toBe('10') + }) + + it('does not send optional params when they are undefined', async () => { + const capturedParams: Record = {} + + server.use( + http.get('/api/cases/', ({ request }) => { + const url = new URL(request.url) + capturedParams.search = url.searchParams.get('search') + capturedParams.jahr = url.searchParams.get('jahr') + capturedParams.fallgruppe = url.searchParams.get('fallgruppe') + capturedParams.has_icd = url.searchParams.get('has_icd') + return HttpResponse.json(mockCaseListResponse) + }), + ) + + const { result } = renderHookWithProviders(() => useCases(defaultFilters)) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + + expect(capturedParams.search).toBeNull() + expect(capturedParams.jahr).toBeNull() + expect(capturedParams.fallgruppe).toBeNull() + expect(capturedParams.has_icd).toBeNull() + }) + + it('respects enabled: false (fetchStatus === "idle")', () => { + const { result } = renderHookWithProviders(() => + useCases(defaultFilters, { enabled: false }), + ) + + expect(result.current.fetchStatus).toBe('idle') + expect(result.current.data).toBeUndefined() + }) + + it('handles empty response', async () => { + server.use( + http.get('/api/cases/', () => { + return HttpResponse.json(mockEmptyCaseList) + }), + ) + + const { result } = renderHookWithProviders(() => useCases(defaultFilters)) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + + expect(result.current.data!.items).toHaveLength(0) + expect(result.current.data!.total).toBe(0) + }) + + it('handles server error (500)', async () => { + server.use( + http.get('/api/cases/', () => { + return new HttpResponse(null, { status: 500 }) + }), + ) + + const { result } = renderHookWithProviders(() => useCases(defaultFilters)) + + await waitFor(() => expect(result.current.isError).toBe(true)) + + expect(result.current.data).toBeUndefined() + }) +}) + +// --------------------------------------------------------------------------- +// usePendingIcdCases +// --------------------------------------------------------------------------- + +describe('usePendingIcdCases', () => { + it('fetches pending ICD cases (success)', async () => { + const { result } = renderHookWithProviders(() => usePendingIcdCases(1, 20)) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + + expect(result.current.data!.items).toHaveLength(1) + expect(result.current.data!.items[0].icd).toBeNull() + expect(result.current.data!.total).toBe(1) + }) + + it('respects enabled: false', () => { + const { result } = renderHookWithProviders(() => + usePendingIcdCases(1, 20, { enabled: false }), + ) + + expect(result.current.fetchStatus).toBe('idle') + expect(result.current.data).toBeUndefined() + }) +}) + +// --------------------------------------------------------------------------- +// useCaseUpdate +// --------------------------------------------------------------------------- + +describe('useCaseUpdate', () => { + it('updates a case and returns the result', async () => { + const { result } = renderHookWithProviders(() => useCaseUpdate()) + + let updated: unknown + await act(async () => { + updated = await result.current.mutateAsync({ + id: 1, + data: { kommentar: 'Neuer Kommentar' }, + }) + }) + + expect((updated as { id: number }).id).toBe(1) + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + }) + + it('handles error (403 Forbidden)', async () => { + server.use( + http.put('/api/cases/:id', () => { + return HttpResponse.json( + { detail: 'Forbidden' }, + { status: 403 }, + ) + }), + ) + + const { result } = renderHookWithProviders(() => useCaseUpdate()) + + await act(async () => { + try { + await result.current.mutateAsync({ id: 1, data: { kommentar: 'x' } }) + } catch { + // expected + } + }) + + await waitFor(() => expect(result.current.isError).toBe(true)) + }) +}) + +// --------------------------------------------------------------------------- +// useKvnrUpdate +// --------------------------------------------------------------------------- + +describe('useKvnrUpdate', () => { + it('updates KVNR and returns the updated case', async () => { + const { result } = renderHookWithProviders(() => useKvnrUpdate()) + + let updated: unknown + await act(async () => { + updated = await result.current.mutateAsync({ + id: 1, + kvnr: 'C111222333', + }) + }) + + expect((updated as { kvnr: string }).kvnr).toBe('C111222333') + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + }) + + it('handles null KVNR', async () => { + server.use( + http.put('/api/cases/:id/kvnr', ({ params }) => { + const id = Number(params.id) + return HttpResponse.json({ ...mockCase, id, kvnr: null }) + }), + ) + + const { result } = renderHookWithProviders(() => useKvnrUpdate()) + + let updated: unknown + await act(async () => { + updated = await result.current.mutateAsync({ + id: 1, + kvnr: null, + }) + }) + + expect((updated as { kvnr: string | null }).kvnr).toBeNull() + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + }) +}) + +// --------------------------------------------------------------------------- +// useIcdUpdate +// --------------------------------------------------------------------------- + +describe('useIcdUpdate', () => { + it('updates ICD and returns the updated case', async () => { + const { result } = renderHookWithProviders(() => useIcdUpdate()) + + let updated: unknown + await act(async () => { + updated = await result.current.mutateAsync({ + id: 1, + icd: 'C61', + }) + }) + + expect((updated as { icd: string }).icd).toBe('C61') + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + }) + + it('handles error (422 Unprocessable Entity)', async () => { + server.use( + http.put('/api/cases/:id/icd', () => { + return HttpResponse.json( + { detail: 'Invalid ICD code' }, + { status: 422 }, + ) + }), + ) + + const { result } = renderHookWithProviders(() => useIcdUpdate()) + + await act(async () => { + try { + await result.current.mutateAsync({ id: 1, icd: 'INVALID' }) + } catch { + // expected + } + }) + + await waitFor(() => expect(result.current.isError).toBe(true)) + }) +}) diff --git a/frontend/src/hooks/__tests__/useDashboard.test.ts b/frontend/src/hooks/__tests__/useDashboard.test.ts new file mode 100644 index 0000000..388818b --- /dev/null +++ b/frontend/src/hooks/__tests__/useDashboard.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect } from 'vitest' +import { waitFor } from '@testing-library/react' +import { http, HttpResponse } from 'msw' +import { server } from '@/test/mocks/server' +import { renderHookWithProviders } from '@/test/utils' +import { mockDashboardResponse } from '@/test/mocks/data' +import { useDashboard } from '@/hooks/useDashboard' + +describe('useDashboard', () => { + it('fetches dashboard data for the given year', async () => { + const { result } = renderHookWithProviders(() => useDashboard(2026)) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + + expect(result.current.data).toEqual(mockDashboardResponse) + expect(result.current.data!.kpis.total_cases).toBe(142) + expect(result.current.data!.weekly).toHaveLength(6) + }) + + it('includes the year parameter in the API call', async () => { + let capturedJahr: string | null = null + + server.use( + http.get('/api/reports/dashboard', ({ request }) => { + const url = new URL(request.url) + capturedJahr = url.searchParams.get('jahr') + return HttpResponse.json(mockDashboardResponse) + }), + ) + + const { result } = renderHookWithProviders(() => useDashboard(2025)) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + + expect(capturedJahr).toBe('2025') + }) + + it('handles server error (500)', async () => { + server.use( + http.get('/api/reports/dashboard', () => { + return new HttpResponse(null, { status: 500 }) + }), + ) + + const { result } = renderHookWithProviders(() => useDashboard(2026)) + + await waitFor(() => expect(result.current.isError).toBe(true)) + + expect(result.current.data).toBeUndefined() + }) +})