From ba3f930e4dee2302d0df1c5464ee3f15d30b2f12 Mon Sep 17 00:00:00 2001 From: CCS Admin Date: Thu, 26 Feb 2026 21:32:30 +0000 Subject: [PATCH] test: add page tests for AdminUsers, Reports, Login Co-Authored-By: Claude Opus 4.6 --- .../pages/__tests__/AdminUsersPage.test.tsx | 94 +++++++++++++++++++ .../src/pages/__tests__/LoginPage.test.tsx | 94 +++++++++++++++++++ .../src/pages/__tests__/ReportsPage.test.tsx | 86 +++++++++++++++++ 3 files changed, 274 insertions(+) create mode 100644 frontend/src/pages/__tests__/AdminUsersPage.test.tsx create mode 100644 frontend/src/pages/__tests__/LoginPage.test.tsx create mode 100644 frontend/src/pages/__tests__/ReportsPage.test.tsx diff --git a/frontend/src/pages/__tests__/AdminUsersPage.test.tsx b/frontend/src/pages/__tests__/AdminUsersPage.test.tsx new file mode 100644 index 0000000..c592b62 --- /dev/null +++ b/frontend/src/pages/__tests__/AdminUsersPage.test.tsx @@ -0,0 +1,94 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { screen, waitFor } 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 { AdminUsersPage } from '@/pages/AdminUsersPage' + +describe('AdminUsersPage', () => { + beforeEach(() => { + localStorage.setItem('access_token', 'test-token') + }) + + it('renders user list with user data', async () => { + renderWithProviders(, { initialRoute: '/admin/users' }) + + await waitFor(() => { + // mockUserResponse has username 'admin_user', email 'admin@dak.de', role 'admin' + expect(screen.getByText('admin_user')).toBeInTheDocument() + }) + + expect(screen.getByText('admin@dak.de')).toBeInTheDocument() + expect(screen.getByText('Admin')).toBeInTheDocument() + // "Aktiv" appears both as table header and badge; check for at least 2 + const aktivElements = screen.getAllByText('Aktiv') + expect(aktivElements.length).toBeGreaterThanOrEqual(2) + }) + + it('shows "Neuen Benutzer erstellen" button and opens dialog', async () => { + const user = userEvent.setup() + renderWithProviders(, { initialRoute: '/admin/users' }) + + await waitFor(() => { + expect(screen.getByText('Neuen Benutzer erstellen')).toBeInTheDocument() + }) + + // Click the create button to open the dialog + await user.click(screen.getByText('Neuen Benutzer erstellen')) + + // Dialog should open with form fields + await waitFor(() => { + expect(screen.getByLabelText('Benutzername')).toBeInTheDocument() + }) + expect(screen.getByLabelText('E-Mail')).toBeInTheDocument() + expect(screen.getByLabelText('Passwort')).toBeInTheDocument() + }) + + it('handles empty user list', async () => { + server.use( + http.get('/api/admin/users', () => { + return HttpResponse.json([]) + }), + ) + + renderWithProviders(, { initialRoute: '/admin/users' }) + + await waitFor(() => { + expect(screen.getByText('Keine Benutzer gefunden.')).toBeInTheDocument() + }) + }) + + it('shows loading state', () => { + server.use( + http.get('/api/admin/users', async () => { + await new Promise((resolve) => setTimeout(resolve, 500)) + return HttpResponse.json([]) + }), + ) + + renderWithProviders(, { initialRoute: '/admin/users' }) + + // Heading is always visible + expect(screen.getByText('Benutzer')).toBeInTheDocument() + + // During loading, user data should not be visible yet + expect(screen.queryByText('admin_user')).not.toBeInTheDocument() + expect(screen.queryByText('Keine Benutzer gefunden.')).not.toBeInTheDocument() + }) + + it('shows error state on API failure', async () => { + server.use( + http.get('/api/admin/users', () => { + return new HttpResponse(null, { status: 500 }) + }), + ) + + renderWithProviders(, { initialRoute: '/admin/users' }) + + // When the query fails, users defaults to [] so it should show empty state + await waitFor(() => { + expect(screen.getByText('Keine Benutzer gefunden.')).toBeInTheDocument() + }) + }) +}) diff --git a/frontend/src/pages/__tests__/LoginPage.test.tsx b/frontend/src/pages/__tests__/LoginPage.test.tsx new file mode 100644 index 0000000..3faba8e --- /dev/null +++ b/frontend/src/pages/__tests__/LoginPage.test.tsx @@ -0,0 +1,94 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { screen, waitFor } 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 { LoginPage } from '@/pages/LoginPage' + +// Mock window.matchMedia for useTheme hook +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation((query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), +}) + +describe('LoginPage', () => { + beforeEach(() => { + // LoginPage is for unauthenticated users — do NOT set access_token + localStorage.removeItem('access_token') + localStorage.removeItem('refresh_token') + + // Override /api/auth/me to return 401 for unauthenticated state + server.use( + http.get('/api/auth/me', () => { + return new HttpResponse(null, { status: 401 }) + }), + ) + }) + + it('renders email and password input fields', () => { + renderWithProviders(, { initialRoute: '/login' }) + + expect(screen.getByLabelText('E-Mail')).toBeInTheDocument() + expect(screen.getByLabelText('Passwort')).toBeInTheDocument() + }) + + it('renders submit button', () => { + renderWithProviders(, { initialRoute: '/login' }) + + expect(screen.getByRole('button', { name: 'Anmelden' })).toBeInTheDocument() + }) + + it('shows error message on failed login', async () => { + const user = userEvent.setup() + + // Override login endpoint to return 401 + server.use( + http.post('/api/auth/login', () => { + return HttpResponse.json( + { detail: 'Invalid credentials' }, + { status: 401 }, + ) + }), + ) + + renderWithProviders(, { initialRoute: '/login' }) + + // Fill in valid form data + await user.type(screen.getByLabelText('E-Mail'), 'wrong@dak.de') + await user.type(screen.getByLabelText('Passwort'), 'wrongpassword') + + // Submit the form + await user.click(screen.getByRole('button', { name: 'Anmelden' })) + + // Should show error message for 401 + await waitFor(() => { + expect(screen.getByText('Ungueltige Anmeldedaten')).toBeInTheDocument() + }) + }) + + it('shows validation errors for empty form submission', async () => { + const user = userEvent.setup() + + renderWithProviders(, { initialRoute: '/login' }) + + // Click submit without filling in any fields + await user.click(screen.getByRole('button', { name: 'Anmelden' })) + + // Zod validation errors should appear + await waitFor(() => { + expect(screen.getByText('Bitte geben Sie eine gueltige E-Mail-Adresse ein')).toBeInTheDocument() + }) + + expect(screen.getByText('Passwort ist erforderlich')).toBeInTheDocument() + }) +}) diff --git a/frontend/src/pages/__tests__/ReportsPage.test.tsx b/frontend/src/pages/__tests__/ReportsPage.test.tsx new file mode 100644 index 0000000..b65d9bb --- /dev/null +++ b/frontend/src/pages/__tests__/ReportsPage.test.tsx @@ -0,0 +1,86 @@ +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 { ReportsPage } from '@/pages/ReportsPage' + +describe('ReportsPage', () => { + beforeEach(() => { + localStorage.setItem('access_token', 'test-token') + }) + + it('renders report list with report metadata', async () => { + renderWithProviders(, { initialRoute: '/reports' }) + + await waitFor(() => { + // mockReportMeta has jahr: 2026, kw: 6 + expect(screen.getByText('2026')).toBeInTheDocument() + }) + + expect(screen.getByText('KW 6')).toBeInTheDocument() + + // Total count should show in the header "Bisherige Berichte (1)" + expect(screen.getByText('Bisherige Berichte (1)')).toBeInTheDocument() + }) + + it('shows "Bericht generieren" button and form fields', async () => { + renderWithProviders(, { initialRoute: '/reports' }) + + await waitFor(() => { + // "Bericht generieren" appears as card title and button text; use button role + expect(screen.getByRole('button', { name: /Bericht generieren/ })).toBeInTheDocument() + }) + + // Admin user sees the generation form with Jahr and KW inputs + expect(screen.getByLabelText('Jahr')).toBeInTheDocument() + expect(screen.getByLabelText('Kalenderwoche')).toBeInTheDocument() + }) + + it('handles empty report list', async () => { + server.use( + http.get('/api/reports/list', () => { + return HttpResponse.json({ items: [], total: 0 }) + }), + ) + + renderWithProviders(, { initialRoute: '/reports' }) + + await waitFor(() => { + expect(screen.getByText('Keine Berichte vorhanden.')).toBeInTheDocument() + }) + }) + + it('shows loading state', () => { + server.use( + http.get('/api/reports/list', async () => { + await new Promise((resolve) => setTimeout(resolve, 500)) + return HttpResponse.json({ items: [], total: 0 }) + }), + ) + + renderWithProviders(, { initialRoute: '/reports' }) + + // The heading is always visible + expect(screen.getByText('Berichte')).toBeInTheDocument() + + // During loading, the empty state message or report data should not be visible + expect(screen.queryByText('Keine Berichte vorhanden.')).not.toBeInTheDocument() + expect(screen.queryByText('KW 6')).not.toBeInTheDocument() + }) + + it('shows error on API failure', async () => { + server.use( + http.get('/api/reports/list', () => { + return new HttpResponse(null, { status: 500 }) + }), + ) + + renderWithProviders(, { initialRoute: '/reports' }) + + // When query fails, reports defaults to [] so empty state appears + await waitFor(() => { + expect(screen.getByText('Keine Berichte vorhanden.')).toBeInTheDocument() + }) + }) +})