diff --git a/frontend/src/pages/__tests__/AdminAuditPage.test.tsx b/frontend/src/pages/__tests__/AdminAuditPage.test.tsx new file mode 100644 index 0000000..d7688c4 --- /dev/null +++ b/frontend/src/pages/__tests__/AdminAuditPage.test.tsx @@ -0,0 +1,80 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { screen, waitFor } from '@testing-library/react' +import { http, HttpResponse } from 'msw' +import { server } from '@/test/mocks/server' +import { renderWithProviders } from '@/test/utils' +import { AdminAuditPage } from '@/pages/AdminAuditPage' + +describe('AdminAuditPage', () => { + beforeEach(() => { + localStorage.setItem('access_token', 'test-token') + }) + + it('renders audit log entries', async () => { + renderWithProviders() + + await waitFor(() => { + // mockAuditLogEntry has action 'case.update' + expect(screen.getByText('case.update')).toBeInTheDocument() + }) + + // Should show entity info: entity_type 'case' + entity_id '1' + expect(screen.getByText('case #1')).toBeInTheDocument() + + // Should show user_id = 1 + expect(screen.getByText('1')).toBeInTheDocument() + + // Should show the IP address + expect(screen.getByText('10.10.181.104')).toBeInTheDocument() + }) + + it('shows filter controls', async () => { + renderWithProviders() + + // Filter section title + expect(screen.getByText('Filter')).toBeInTheDocument() + + // Filter input labels + expect(screen.getByLabelText('Benutzer-ID')).toBeInTheDocument() + expect(screen.getByLabelText('Aktion')).toBeInTheDocument() + expect(screen.getByLabelText('Datum von')).toBeInTheDocument() + expect(screen.getByLabelText('Datum bis')).toBeInTheDocument() + + // Filter buttons + expect(screen.getByText('Filtern')).toBeInTheDocument() + expect(screen.getByText('Zurücksetzen')).toBeInTheDocument() + }) + + it('handles empty audit log list', async () => { + server.use( + http.get('/api/admin/audit-log', () => { + return HttpResponse.json([]) + }), + ) + + renderWithProviders() + + await waitFor(() => { + expect(screen.getByText('Keine Audit-Einträge gefunden.')).toBeInTheDocument() + }) + }) + + it('shows loading state', () => { + // Delay the API response to observe loading state + server.use( + http.get('/api/admin/audit-log', async () => { + await new Promise((resolve) => setTimeout(resolve, 500)) + return HttpResponse.json([]) + }), + ) + + renderWithProviders() + + // The heading is always visible + expect(screen.getByText('Audit-Log')).toBeInTheDocument() + + // During loading, neither the empty message nor table data should appear + expect(screen.queryByText('Keine Audit-Einträge gefunden.')).not.toBeInTheDocument() + expect(screen.queryByText('case.update')).not.toBeInTheDocument() + }) +}) diff --git a/frontend/src/pages/__tests__/DashboardPage.test.tsx b/frontend/src/pages/__tests__/DashboardPage.test.tsx new file mode 100644 index 0000000..1c10020 --- /dev/null +++ b/frontend/src/pages/__tests__/DashboardPage.test.tsx @@ -0,0 +1,148 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { screen, waitFor } from '@testing-library/react' +import { http, HttpResponse } from 'msw' +import { server } from '@/test/mocks/server' +import { renderWithProviders } from '@/test/utils' +import { DashboardPage } from '@/pages/DashboardPage' + +// Recharts ResponsiveContainer needs ResizeObserver as a proper constructor +class ResizeObserverStub { + observe() {} + unobserve() {} + disconnect() {} +} +vi.stubGlobal('ResizeObserver', ResizeObserverStub) + +// Mock recharts to avoid SVG rendering issues in jsdom +// We render a simplified version that outputs the data as text +vi.mock('recharts', async () => { + const actual = await vi.importActual('recharts') + return { + ...actual, + ResponsiveContainer: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + PieChart: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + BarChart: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + Pie: ({ data }: { data?: Array<{ name: string; value: number }> }) => ( +
+ {data?.map((entry) => ( + {entry.name} + ))} +
+ ), + Bar: () =>
, + Cell: () =>
, + XAxis: () =>
, + YAxis: () =>
, + CartesianGrid: () =>
, + Tooltip: () =>
, + Legend: () =>
, + } +}) + +describe('DashboardPage', () => { + beforeEach(() => { + localStorage.setItem('access_token', 'test-token') + }) + + it('renders KPI cards with correct values once loaded', async () => { + renderWithProviders() + + await waitFor(() => { + expect(screen.getByText('Fälle gesamt')).toBeInTheDocument() + }) + + // Check all four KPI card titles + expect(screen.getByText('Offene ICD')).toBeInTheDocument() + expect(screen.getByText('Offene Codierung')).toBeInTheDocument() + expect(screen.getByText('Gutachten gesamt')).toBeInTheDocument() + + // Check KPI values from mockDashboardKPIs (142, 18, 7, 89) + expect(screen.getByText('142')).toBeInTheDocument() + expect(screen.getByText('18')).toBeInTheDocument() + expect(screen.getByText('7')).toBeInTheDocument() + expect(screen.getByText('89')).toBeInTheDocument() + }) + + it('shows year selector', async () => { + renderWithProviders() + + // The Select trigger should show the current year + const currentYear = new Date().getFullYear() + await waitFor(() => { + expect(screen.getByText(String(currentYear))).toBeInTheDocument() + }) + + // The heading should be present + expect(screen.getByText('Dashboard')).toBeInTheDocument() + }) + + it('shows loading skeleton initially', () => { + // Delay the API response so we can observe the loading state + server.use( + http.get('/api/reports/dashboard', async () => { + await new Promise((resolve) => setTimeout(resolve, 500)) + return HttpResponse.json({ kpis: {}, weekly: [] }) + }), + ) + + renderWithProviders() + + // During loading, the KPI titles should not be visible yet + expect(screen.queryByText('Fälle gesamt')).not.toBeInTheDocument() + + // The heading is always visible + expect(screen.getByText('Dashboard')).toBeInTheDocument() + }) + + it('filters 0-value Fallgruppen from pie chart data', async () => { + // The mock data has: onko: 65, ortho: 42, kardio: 20, neuro: 15 + // FALLGRUPPEN_LABELS maps onko->Onkologie, kardio->Kardiologie + // ortho and neuro are not in FALLGRUPPEN_LABELS so they use the raw key + // All values are > 0, so all should appear in the pie chart + // sd, intensiv, galle are NOT in mock data, so they should not appear + renderWithProviders() + + await waitFor(() => { + expect(screen.getByText('Fälle gesamt')).toBeInTheDocument() + }) + + // The chart section title should appear + expect(screen.getByText('Fallgruppen')).toBeInTheDocument() + + // Onkologie and Kardiologie should appear (mapped from FALLGRUPPEN_LABELS) + expect(screen.getByText('Onkologie')).toBeInTheDocument() + expect(screen.getByText('Kardiologie')).toBeInTheDocument() + + // ortho and neuro use raw keys since they are not in FALLGRUPPEN_LABELS + expect(screen.getByText('ortho')).toBeInTheDocument() + expect(screen.getByText('neuro')).toBeInTheDocument() + + // Schilddrüse should NOT appear (sd is not in mock data) + expect(screen.queryByText('Schilddrüse')).not.toBeInTheDocument() + // Intensivmedizin should NOT appear (intensiv is not in mock data) + expect(screen.queryByText('Intensivmedizin')).not.toBeInTheDocument() + // Gallenblase should NOT appear (galle is not in mock data) + expect(screen.queryByText('Gallenblase')).not.toBeInTheDocument() + }) + + it('handles error state when API returns 500', async () => { + server.use( + http.get('/api/reports/dashboard', () => { + return new HttpResponse(null, { status: 500 }) + }), + ) + + renderWithProviders() + + // When data is null and loading is false, it shows the fallback message + await waitFor(() => { + expect(screen.getByText('Keine Daten verfügbar.')).toBeInTheDocument() + }) + }) +}) diff --git a/frontend/src/pages/__tests__/DisclosuresPage.test.tsx b/frontend/src/pages/__tests__/DisclosuresPage.test.tsx new file mode 100644 index 0000000..5ca8797 --- /dev/null +++ b/frontend/src/pages/__tests__/DisclosuresPage.test.tsx @@ -0,0 +1,74 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { screen, waitFor } from '@testing-library/react' +import { http, HttpResponse } from 'msw' +import { server } from '@/test/mocks/server' +import { renderWithProviders } from '@/test/utils' +import { DisclosuresPage } from '@/pages/DisclosuresPage' + +describe('DisclosuresPage', () => { + beforeEach(() => { + localStorage.setItem('access_token', 'test-token') + }) + + it('renders disclosure requests list', async () => { + renderWithProviders() + + await waitFor(() => { + // The mock handler returns [mockDisclosureRequest] with fall_id '2026-06-onko-A123456789' + expect(screen.getByText('Fall 2026-06-onko-A123456789')).toBeInTheDocument() + }) + + // Should show requester username + expect(screen.getByText('dak_user')).toBeInTheDocument() + + // Should show the reason + expect(screen.getByText('Benötige vollständige Patientendaten für Rückruf.')).toBeInTheDocument() + + // Should show the status badge + expect(screen.getByText('pending')).toBeInTheDocument() + }) + + it('shows approve and reject buttons', async () => { + renderWithProviders() + + await waitFor(() => { + expect(screen.getByText('Genehmigen')).toBeInTheDocument() + }) + + expect(screen.getByText('Ablehnen')).toBeInTheDocument() + }) + + it('handles empty disclosure list', async () => { + server.use( + http.get('/api/admin/disclosure-requests', () => { + return HttpResponse.json([]) + }), + ) + + renderWithProviders() + + await waitFor(() => { + expect(screen.getByText('Keine offenen Anfragen.')).toBeInTheDocument() + }) + }) + + it('shows loading state', () => { + // Delay the API response to observe loading state + server.use( + http.get('/api/admin/disclosure-requests', async () => { + await new Promise((resolve) => setTimeout(resolve, 500)) + return HttpResponse.json([]) + }), + ) + + renderWithProviders() + + // The heading is always visible + expect(screen.getByText('Freigabe-Anfragen')).toBeInTheDocument() + + // During loading, the "Keine offenen Anfragen." message should not be visible yet + expect(screen.queryByText('Keine offenen Anfragen.')).not.toBeInTheDocument() + // Nor should any disclosure item be visible + expect(screen.queryByText('Genehmigen')).not.toBeInTheDocument() + }) +})