From 10524a471df83c7b326ef1849a00ed669ffeadea Mon Sep 17 00:00:00 2001 From: CCS Admin Date: Thu, 26 Feb 2026 21:36:12 +0000 Subject: [PATCH] test: add CasesPage integration tests (list, filter, detail) Co-Authored-By: Claude Opus 4.6 --- .../src/pages/__tests__/CasesPage.test.tsx | 266 ++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 frontend/src/pages/__tests__/CasesPage.test.tsx diff --git a/frontend/src/pages/__tests__/CasesPage.test.tsx b/frontend/src/pages/__tests__/CasesPage.test.tsx new file mode 100644 index 0000000..b49c6ce --- /dev/null +++ b/frontend/src/pages/__tests__/CasesPage.test.tsx @@ -0,0 +1,266 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { screen, waitFor, within } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { http, HttpResponse } from 'msw' +import { server } from '@/test/mocks/server' +import { renderWithProviders } from '@/test/utils' +import { CasesPage } from '@/pages/CasesPage' +import { mockCase, mockCaseNoIcd, mockEmptyCaseList } from '@/test/mocks/data' + +describe('CasesPage', () => { + beforeEach(() => { + localStorage.setItem('access_token', 'test-token') + }) + + // ==================== List rendering ==================== + + it('renders the page heading', async () => { + renderWithProviders(, { initialRoute: '/cases' }) + + await waitFor(() => { + expect(screen.getByText('Fälle')).toBeInTheDocument() + }) + }) + + it('renders case list table with fall_id, datum, and fallgruppe columns', async () => { + renderWithProviders(, { initialRoute: '/cases' }) + + // Wait for the table to load with data + await waitFor(() => { + expect(screen.getByText(mockCase.fall_id!)).toBeInTheDocument() + }) + + // Check table headers + expect(screen.getByText('Fall-ID')).toBeInTheDocument() + expect(screen.getByText('Datum')).toBeInTheDocument() + expect(screen.getByText('Fallgruppe')).toBeInTheDocument() + expect(screen.getByText('KVNR')).toBeInTheDocument() + expect(screen.getByText('ICD')).toBeInTheDocument() + // "Gutachten" appears both as table header and as a status badge, so use getAllByText + expect(screen.getAllByText('Gutachten').length).toBeGreaterThanOrEqual(1) + expect(screen.getByText('Status')).toBeInTheDocument() + + // Admin-only columns should be visible (default mockUser is admin) + expect(screen.getByText('Nachname')).toBeInTheDocument() + expect(screen.getByText('Vorname')).toBeInTheDocument() + }) + + it('shows correct number of cases from mock data', async () => { + renderWithProviders(, { initialRoute: '/cases' }) + + // Wait for data to load + await waitFor(() => { + expect(screen.getByText(mockCase.fall_id!)).toBeInTheDocument() + }) + + // Both mock cases should be rendered + expect(screen.getByText(mockCase.fall_id!)).toBeInTheDocument() + expect(screen.getByText(mockCaseNoIcd.fall_id!)).toBeInTheDocument() + + // Total count shown in pagination area + expect(screen.getByText('2 Fälle insgesamt')).toBeInTheDocument() + }) + + it('renders case data correctly in table rows', async () => { + renderWithProviders(, { initialRoute: '/cases' }) + + await waitFor(() => { + expect(screen.getByText(mockCase.fall_id!)).toBeInTheDocument() + }) + + // Check first case data + expect(screen.getByText('Onkologie')).toBeInTheDocument() + expect(screen.getByText(mockCase.kvnr!)).toBeInTheDocument() + expect(screen.getByText(mockCase.icd!)).toBeInTheDocument() + expect(screen.getByText(mockCase.nachname!)).toBeInTheDocument() + + // Second case should also be present + expect(screen.getByText(mockCaseNoIcd.fall_id!)).toBeInTheDocument() + expect(screen.getByText(mockCaseNoIcd.kvnr!)).toBeInTheDocument() + }) + + it('handles empty list and shows empty message', async () => { + server.use( + http.get('/api/cases/', () => { + return HttpResponse.json(mockEmptyCaseList) + }), + ) + + renderWithProviders(, { initialRoute: '/cases' }) + + await waitFor(() => { + expect(screen.getByText('Keine Fälle gefunden.')).toBeInTheDocument() + }) + }) + + it('shows loading state with skeleton placeholders', () => { + // Delay the API response to observe loading state + server.use( + http.get('/api/cases/', async () => { + await new Promise((resolve) => setTimeout(resolve, 500)) + return HttpResponse.json(mockEmptyCaseList) + }), + ) + + renderWithProviders(, { initialRoute: '/cases' }) + + // During loading, the heading is always visible + expect(screen.getByText('Fälle')).toBeInTheDocument() + + // Table data should not be visible yet + expect(screen.queryByText('Fall-ID')).not.toBeInTheDocument() + }) + + // ==================== Filtering ==================== + + it('renders search input', async () => { + renderWithProviders(, { initialRoute: '/cases' }) + + await waitFor(() => { + expect(screen.getByText(mockCase.fall_id!)).toBeInTheDocument() + }) + + // Search input with admin placeholder + const searchInput = screen.getByPlaceholderText('Suche nach Name, Fall-ID, KVNR...') + expect(searchInput).toBeInTheDocument() + }) + + it('allows typing into the search input', async () => { + const user = userEvent.setup() + renderWithProviders(, { initialRoute: '/cases' }) + + await waitFor(() => { + expect(screen.getByText(mockCase.fall_id!)).toBeInTheDocument() + }) + + const searchInput = screen.getByPlaceholderText('Suche nach Name, Fall-ID, KVNR...') + await user.type(searchInput, 'onko') + expect(searchInput).toHaveValue('onko') + }) + + it('renders year and fallgruppe filter selects', async () => { + renderWithProviders(, { initialRoute: '/cases' }) + + await waitFor(() => { + expect(screen.getByText(mockCase.fall_id!)).toBeInTheDocument() + }) + + // Year select trigger shows "Alle Jahre" by default + expect(screen.getByText('Alle Jahre')).toBeInTheDocument() + + // Fallgruppe select trigger shows "Alle Fallgruppen" by default + expect(screen.getByText('Alle Fallgruppen')).toBeInTheDocument() + + // ICD filter shows "Alle" by default + // There are multiple "Alle" texts, so we verify the ICD filter trigger exists + const alleTriggers = screen.getAllByText('Alle') + expect(alleTriggers.length).toBeGreaterThanOrEqual(1) + }) + + // ==================== Detail view ==================== + + it('opens detail sheet when clicking a table row', async () => { + const user = userEvent.setup() + renderWithProviders(, { initialRoute: '/cases' }) + + // Wait for data to load + await waitFor(() => { + expect(screen.getByText(mockCase.fall_id!)).toBeInTheDocument() + }) + + // Click on the first case's fall_id cell (which is inside a table row) + await user.click(screen.getByText(mockCase.fall_id!)) + + // The Sheet (Radix Dialog) should open with case details + await waitFor(() => { + const dialog = screen.getByRole('dialog') + expect(dialog).toBeInTheDocument() + }) + + // SheetTitle shows the fall_id + const dialog = screen.getByRole('dialog') + expect(within(dialog).getByText(`Fall ${mockCase.fall_id}`)).toBeInTheDocument() + }) + + it('shows case detail information in the sheet', async () => { + const user = userEvent.setup() + renderWithProviders(, { initialRoute: '/cases' }) + + await waitFor(() => { + expect(screen.getByText(mockCase.fall_id!)).toBeInTheDocument() + }) + + // Click on the first case + await user.click(screen.getByText(mockCase.fall_id!)) + + // Wait for sheet to open + await waitFor(() => { + expect(screen.getByRole('dialog')).toBeInTheDocument() + }) + + const dialog = screen.getByRole('dialog') + + // SheetDescription shows fallgruppe and KW/year + expect(within(dialog).getByText(/Onkologie/)).toBeInTheDocument() + + // Read-only metadata fields + expect(within(dialog).getByText('Fall-ID')).toBeInTheDocument() + expect(within(dialog).getByText('CRM-Ticket')).toBeInTheDocument() + + // Editable section headers from fieldConfig + expect(within(dialog).getByText('Persönliche Daten')).toBeInTheDocument() + expect(within(dialog).getByText('Kontakt')).toBeInTheDocument() + expect(within(dialog).getByText('Falldetails')).toBeInTheDocument() + + // ICD section + expect(within(dialog).getByText('ICD-Code')).toBeInTheDocument() + + // Edit button should be present + expect(within(dialog).getByText('Bearbeiten')).toBeInTheDocument() + }) + + // ==================== Error handling ==================== + + it('shows empty state when API returns 500', async () => { + server.use( + http.get('/api/cases/', () => { + return new HttpResponse(null, { status: 500 }) + }), + ) + + renderWithProviders(, { initialRoute: '/cases' }) + + // When the query fails, data is null, so the empty message is shown + await waitFor(() => { + expect(screen.getByText('Keine Fälle gefunden.')).toBeInTheDocument() + }) + }) + + // ==================== Pagination ==================== + + it('shows pagination controls', async () => { + renderWithProviders(, { initialRoute: '/cases' }) + + await waitFor(() => { + expect(screen.getByText(mockCase.fall_id!)).toBeInTheDocument() + }) + + // Pagination text + expect(screen.getByText('Seite 1 von 1')).toBeInTheDocument() + expect(screen.getByText('2 Fälle insgesamt')).toBeInTheDocument() + }) + + // ==================== pendingIcdOnly mode ==================== + + it('renders ICD-Eingabe heading when pendingIcdOnly is true', async () => { + renderWithProviders(, { initialRoute: '/cases/pending-icd' }) + + await waitFor(() => { + expect(screen.getByText('ICD-Eingabe')).toBeInTheDocument() + }) + + // The filter bar should NOT be rendered in pendingIcdOnly mode + expect(screen.queryByPlaceholderText('Suche nach Name, Fall-ID, KVNR...')).not.toBeInTheDocument() + expect(screen.queryByText('Alle Fallgruppen')).not.toBeInTheDocument() + }) +})