diff --git a/docs/plans/2026-02-26-frontend-testing-implementation.md b/docs/plans/2026-02-26-frontend-testing-implementation.md
new file mode 100644
index 0000000..e1a1e5e
--- /dev/null
+++ b/docs/plans/2026-02-26-frontend-testing-implementation.md
@@ -0,0 +1,1558 @@
+# Frontend Testing Implementation Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** Implement comprehensive frontend testing with Vitest + MSW for unit/integration tests and Playwright for E2E browser tests (~155 tests total).
+
+**Architecture:** Vitest with jsdom environment for hooks/services/pages, MSW to intercept HTTP requests at network level, `renderWithProviders()` helper for page tests (wraps QueryClient + Auth + Router), Playwright for critical user journeys against live app.
+
+**Tech Stack:** Vitest, @testing-library/react, @testing-library/jest-dom, @testing-library/user-event, MSW 2.x, Playwright, React 19, TanStack Query v5, Axios
+
+---
+
+### Task 1: Install dependencies and configure Vitest
+
+**Files:**
+- Modify: `frontend/package.json`
+- Create: `frontend/vitest.config.ts`
+- Modify: `frontend/tsconfig.app.json` (add vitest types)
+
+**Step 1: Install test dependencies**
+
+```bash
+cd /home/frontend/dak_c2s/frontend
+pnpm add -D vitest @vitest/coverage-v8 jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event msw
+```
+
+**Step 2: Create vitest.config.ts**
+
+Create `frontend/vitest.config.ts`:
+
+```ts
+import { defineConfig } from 'vitest/config'
+import react from '@vitejs/plugin-react'
+import path from 'path'
+
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
+ },
+ },
+ test: {
+ globals: true,
+ environment: 'jsdom',
+ setupFiles: ['./src/test/setup.ts'],
+ include: ['src/**/*.test.{ts,tsx}'],
+ coverage: {
+ provider: 'v8',
+ include: ['src/hooks/**', 'src/services/**', 'src/pages/**', 'src/context/**', 'src/components/layout/**'],
+ exclude: ['src/components/ui/**', 'src/test/**'],
+ },
+ },
+})
+```
+
+**Step 3: Add vitest types to tsconfig.app.json**
+
+In `frontend/tsconfig.app.json`, change the `types` field:
+
+```json
+"types": ["vite/client", "vitest/globals"]
+```
+
+**Step 4: Add test scripts to package.json**
+
+Add to `scripts` in `frontend/package.json`:
+
+```json
+"test": "vitest run",
+"test:watch": "vitest",
+"test:coverage": "vitest run --coverage"
+```
+
+**Step 5: Verify install**
+
+```bash
+cd /home/frontend/dak_c2s/frontend && npx vitest --version
+```
+
+**Step 6: Commit**
+
+```bash
+cd /home/frontend/dak_c2s
+git add frontend/package.json frontend/pnpm-lock.yaml frontend/vitest.config.ts frontend/tsconfig.app.json
+git commit -m "feat: add Vitest testing infrastructure"
+```
+
+---
+
+### Task 2: Create MSW mock server, handlers, and mock data
+
+**Files:**
+- Create: `frontend/src/test/setup.ts`
+- Create: `frontend/src/test/mocks/server.ts`
+- Create: `frontend/src/test/mocks/data.ts`
+- Create: `frontend/src/test/mocks/handlers.ts`
+
+**Step 1: Create mock data**
+
+Create `frontend/src/test/mocks/data.ts`:
+
+```ts
+import type {
+ User, Case, CaseListResponse, DashboardResponse, DashboardKPIs,
+ WeeklyDataPoint, Notification, NotificationList, ReportMeta,
+ UserResponse, AuditLogEntry, DisclosureRequest, TokenResponse,
+} from '@/types'
+
+export const mockUser: User = {
+ id: 1,
+ username: 'admin',
+ email: 'admin@test.de',
+ first_name: 'Test',
+ last_name: 'Admin',
+ display_name: 'Test Admin',
+ avatar_url: null,
+ role: 'admin',
+ mfa_enabled: false,
+ is_active: true,
+ last_login: '2026-02-26T10:00:00Z',
+ created_at: '2026-01-01T00:00:00Z',
+}
+
+export const mockDakUser: User = {
+ ...mockUser,
+ id: 2,
+ username: 'dak_user',
+ email: 'dak@test.de',
+ role: 'dak_mitarbeiter',
+ first_name: 'DAK',
+ last_name: 'Mitarbeiter',
+ display_name: 'DAK Mitarbeiter',
+}
+
+export const mockTokenResponse: TokenResponse = {
+ access_token: 'mock-access-token',
+ refresh_token: 'mock-refresh-token',
+ token_type: 'bearer',
+ user: mockUser,
+}
+
+export const mockCase: Case = {
+ id: 1,
+ fall_id: '2026-06-onko-A123456789',
+ crm_ticket_id: 'CRM-001',
+ jahr: 2026,
+ kw: 6,
+ datum: '2026-02-09',
+ anrede: 'Herr',
+ vorname: 'Max',
+ nachname: 'Mustermann',
+ geburtsdatum: '1980-01-15',
+ kvnr: 'A123456789',
+ versicherung: 'DAK',
+ icd: 'C50.9',
+ fallgruppe: 'onko',
+ strasse: 'Musterstr. 1',
+ plz: '20095',
+ ort: 'Hamburg',
+ email: 'max@test.de',
+ ansprechpartner: null,
+ telefonnummer: null,
+ mobiltelefon: null,
+ unterlagen: true,
+ unterlagen_verschickt: '2026-02-10',
+ erhalten: true,
+ unterlagen_erhalten: '2026-02-12',
+ unterlagen_an_gutachter: '2026-02-13',
+ gutachten: true,
+ gutachter: 'Dr. Schmidt',
+ gutachten_erstellt: '2026-02-20',
+ gutachten_versendet: '2026-02-21',
+ schweigepflicht: true,
+ ablehnung: false,
+ abbruch: false,
+ abbruch_datum: null,
+ gutachten_typ: 'Erstgutachten',
+ therapieaenderung: 'Ja',
+ ta_diagnosekorrektur: false,
+ ta_unterversorgung: true,
+ ta_uebertherapie: false,
+ kurzbeschreibung: 'Onkologische Zweitmeinung',
+ fragestellung: 'Therapieempfehlung',
+ kommentar: null,
+ sonstiges: null,
+ abgerechnet: false,
+ abrechnung_datum: null,
+ import_source: 'excel_import',
+ imported_at: '2026-02-09T08:00:00Z',
+ updated_at: '2026-02-20T14:30:00Z',
+}
+
+export const mockCaseNoIcd: Case = {
+ ...mockCase,
+ id: 2,
+ fall_id: '2026-06-kardio-B987654321',
+ icd: null,
+ fallgruppe: 'kardio',
+ kvnr: 'B987654321',
+ gutachten: false,
+ gutachter: null,
+ gutachten_erstellt: null,
+ gutachten_versendet: null,
+}
+
+export const mockCaseListResponse: CaseListResponse = {
+ items: [mockCase, mockCaseNoIcd],
+ total: 2,
+ page: 1,
+ per_page: 50,
+}
+
+export const mockEmptyCaseList: CaseListResponse = {
+ items: [],
+ total: 0,
+ page: 1,
+ per_page: 50,
+}
+
+export const mockDashboardKPIs: DashboardKPIs = {
+ total_cases: 150,
+ pending_icd: 12,
+ pending_coding: 8,
+ total_gutachten: 95,
+ fallgruppen: { onko: 80, kardio: 40, intensiv: 20, galle: 10, sd: 0 },
+}
+
+export const mockWeeklyData: WeeklyDataPoint[] = [
+ { kw: 1, erstberatungen: 5, unterlagen: 3, ablehnungen: 1, keine_rm: 0, gutachten: 2 },
+ { kw: 2, erstberatungen: 8, unterlagen: 6, ablehnungen: 0, keine_rm: 1, gutachten: 4 },
+]
+
+export const mockDashboardResponse: DashboardResponse = {
+ kpis: mockDashboardKPIs,
+ weekly: mockWeeklyData,
+}
+
+export const mockNotification: Notification = {
+ id: 1,
+ notification_type: 'disclosure_approved',
+ title: 'Freigabe genehmigt',
+ message: 'Ihre Freigabe-Anfrage wurde genehmigt.',
+ is_read: false,
+ created_at: '2026-02-26T09:00:00Z',
+}
+
+export const mockNotificationRead: Notification = {
+ ...mockNotification,
+ id: 2,
+ is_read: true,
+ title: 'Alter Hinweis',
+ notification_type: 'info',
+}
+
+export const mockNotificationList: NotificationList = {
+ items: [mockNotification, mockNotificationRead],
+ unread_count: 1,
+}
+
+export const mockReportMeta: ReportMeta = {
+ id: 1,
+ jahr: 2026,
+ kw: 6,
+ report_date: '2026-02-09',
+ generated_at: '2026-02-26T10:00:00Z',
+}
+
+export const mockUserResponse: UserResponse = {
+ id: 1,
+ username: 'admin',
+ email: 'admin@test.de',
+ first_name: 'Test',
+ last_name: 'Admin',
+ display_name: 'Test Admin',
+ avatar_url: null,
+ role: 'admin',
+ is_active: true,
+ mfa_enabled: false,
+ last_login: '2026-02-26T10:00:00Z',
+ created_at: '2026-01-01T00:00:00Z',
+}
+
+export const mockAuditLogEntry: AuditLogEntry = {
+ id: 1,
+ user_id: 1,
+ action: 'case.update',
+ entity_type: 'case',
+ entity_id: '1',
+ old_values: { icd: null },
+ new_values: { icd: 'C50.9' },
+ ip_address: '10.10.181.104',
+ created_at: '2026-02-26T10:00:00Z',
+}
+
+export const mockDisclosureRequest: DisclosureRequest = {
+ id: 1,
+ case_id: 1,
+ requester_id: 2,
+ requester_username: 'dak_user',
+ fall_id: '2026-06-onko-A123456789',
+ reason: 'KVNR-Fehler, Identifikation nötig',
+ status: 'pending',
+ reviewed_by: null,
+ reviewed_at: null,
+ expires_at: null,
+ created_at: '2026-02-26T08:00:00Z',
+}
+
+export const mockDisclosureApproved: DisclosureRequest = {
+ ...mockDisclosureRequest,
+ id: 2,
+ status: 'approved',
+ reviewed_by: 1,
+ reviewed_at: '2026-02-26T09:00:00Z',
+ expires_at: '2026-02-27T09:00:00Z',
+}
+```
+
+**Step 2: Create MSW handlers**
+
+Create `frontend/src/test/mocks/handlers.ts`:
+
+```ts
+import { http, HttpResponse } from 'msw'
+import {
+ mockDashboardResponse, mockCaseListResponse, mockCase,
+ mockNotificationList, mockReportMeta, mockUserResponse,
+ mockAuditLogEntry, mockDisclosureRequest, mockUser, mockTokenResponse,
+} from './data'
+
+export const handlers = [
+ // Auth
+ http.post('/api/auth/login', () => HttpResponse.json(mockTokenResponse)),
+ http.post('/api/auth/register', () => HttpResponse.json({ user: mockUser })),
+ http.post('/api/auth/logout', () => new HttpResponse(null, { status: 204 })),
+ http.post('/api/auth/refresh', () => HttpResponse.json({ access_token: 'new-token' })),
+ http.get('/api/auth/me', () => HttpResponse.json(mockUser)),
+ http.put('/api/auth/profile', () => HttpResponse.json(mockUser)),
+ http.post('/api/auth/avatar', () => HttpResponse.json(mockUser)),
+ http.delete('/api/auth/avatar', () => HttpResponse.json(mockUser)),
+ http.post('/api/auth/change-password', () => new HttpResponse(null, { status: 204 })),
+ http.post('/api/auth/mfa/setup', () => HttpResponse.json({ secret: 'ABCDEF', qr_uri: 'otpauth://totp/test' })),
+ http.post('/api/auth/mfa/verify', () => new HttpResponse(null, { status: 204 })),
+ http.delete('/api/auth/mfa', () => new HttpResponse(null, { status: 204 })),
+
+ // Dashboard
+ http.get('/api/reports/dashboard', () => HttpResponse.json(mockDashboardResponse)),
+
+ // Cases
+ http.get('/api/cases/', () => HttpResponse.json(mockCaseListResponse)),
+ http.get('/api/cases/pending-icd', () => HttpResponse.json(mockCaseListResponse)),
+ http.put('/api/cases/:id', () => HttpResponse.json(mockCase)),
+ http.put('/api/cases/:id/kvnr', () => HttpResponse.json(mockCase)),
+ http.put('/api/cases/:id/icd', () => HttpResponse.json(mockCase)),
+
+ // Notifications
+ http.get('/api/notifications', () => HttpResponse.json(mockNotificationList)),
+ http.put('/api/notifications/:id/read', () => new HttpResponse(null, { status: 204 })),
+ http.put('/api/notifications/read-all', () => HttpResponse.json({ marked_read: 1 })),
+
+ // Reports
+ http.get('/api/reports/list', () => HttpResponse.json({ items: [mockReportMeta], total: 1 })),
+ http.post('/api/reports/generate', () => HttpResponse.json(mockReportMeta)),
+ http.delete('/api/reports/delete', () => new HttpResponse(null, { status: 204 })),
+
+ // Admin Users
+ http.get('/api/admin/users', () => HttpResponse.json([mockUserResponse])),
+ http.post('/api/admin/users', () => HttpResponse.json(mockUserResponse)),
+ http.put('/api/admin/users/:id', () => HttpResponse.json(mockUserResponse)),
+
+ // Audit Log
+ http.get('/api/admin/audit-log', () => HttpResponse.json([mockAuditLogEntry])),
+
+ // Disclosure Requests
+ http.get('/api/admin/disclosure-requests', () => HttpResponse.json([mockDisclosureRequest])),
+ http.post('/api/cases/:id/disclosure-request', () => HttpResponse.json(mockDisclosureRequest)),
+ http.put('/api/admin/disclosure-requests/:id', () => HttpResponse.json({ ...mockDisclosureRequest, status: 'approved' })),
+ http.get('/api/admin/disclosure-requests/count', () => HttpResponse.json({ pending_count: 1 })),
+]
+```
+
+**Step 3: Create MSW server**
+
+Create `frontend/src/test/mocks/server.ts`:
+
+```ts
+import { setupServer } from 'msw/node'
+import { handlers } from './handlers'
+
+export const server = setupServer(...handlers)
+```
+
+**Step 4: Create test setup**
+
+Create `frontend/src/test/setup.ts`:
+
+```ts
+import '@testing-library/jest-dom/vitest'
+import { cleanup } from '@testing-library/react'
+import { afterAll, afterEach, beforeAll } from 'vitest'
+import { server } from './mocks/server'
+
+beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
+afterEach(() => {
+ cleanup()
+ server.resetHandlers()
+})
+afterAll(() => server.close())
+```
+
+**Step 5: Run vitest to verify setup**
+
+```bash
+cd /home/frontend/dak_c2s/frontend && pnpm test
+```
+
+Expected: 0 tests found, no errors (just "No test files found").
+
+**Step 6: Commit**
+
+```bash
+cd /home/frontend/dak_c2s
+git add frontend/src/test/
+git commit -m "feat: add MSW mock server, handlers, and test data"
+```
+
+---
+
+### Task 3: Create test utilities
+
+**Files:**
+- Create: `frontend/src/test/utils.tsx`
+
+**Step 1: Create renderWithProviders helper**
+
+Create `frontend/src/test/utils.tsx`:
+
+```tsx
+import { render, type RenderOptions } from '@testing-library/react'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { MemoryRouter } from 'react-router-dom'
+import { AuthProvider } from '@/context/AuthContext'
+import type { ReactElement, ReactNode } from 'react'
+
+function createTestQueryClient() {
+ return new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ gcTime: 0,
+ },
+ mutations: {
+ retry: false,
+ },
+ },
+ })
+}
+
+interface ProvidersProps {
+ children: ReactNode
+ initialRoute?: string
+}
+
+function Providers({ children, initialRoute = '/' }: ProvidersProps) {
+ const queryClient = createTestQueryClient()
+ return (
+
+
+
+ {children}
+
+
+
+ )
+}
+
+export function renderWithProviders(
+ ui: ReactElement,
+ options?: Omit & { initialRoute?: string },
+) {
+ const { initialRoute, ...renderOptions } = options ?? {}
+ return render(ui, {
+ wrapper: ({ children }) => {children},
+ ...renderOptions,
+ })
+}
+
+export function createTestQueryClient() {
+ return new QueryClient({
+ defaultOptions: {
+ queries: { retry: false, gcTime: 0 },
+ mutations: { retry: false },
+ },
+ })
+}
+
+export { createTestQueryClient as createQueryClient }
+```
+
+Wait — there's a duplicate `createTestQueryClient`. Let me fix:
+
+Create `frontend/src/test/utils.tsx`:
+
+```tsx
+import { render, type RenderOptions } from '@testing-library/react'
+import { renderHook, type RenderHookOptions } from '@testing-library/react'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { MemoryRouter } from 'react-router-dom'
+import { AuthProvider } from '@/context/AuthContext'
+import type { ReactElement, ReactNode } from 'react'
+
+export function createTestQueryClient() {
+ return new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ gcTime: 0,
+ },
+ mutations: {
+ retry: false,
+ },
+ },
+ })
+}
+
+interface ProvidersProps {
+ children: ReactNode
+ initialRoute?: string
+}
+
+function Providers({ children, initialRoute = '/' }: ProvidersProps) {
+ const queryClient = createTestQueryClient()
+ return (
+
+
+
+ {children}
+
+
+
+ )
+}
+
+export function renderWithProviders(
+ ui: ReactElement,
+ options?: Omit & { initialRoute?: string },
+) {
+ const { initialRoute, ...renderOptions } = options ?? {}
+ return render(ui, {
+ wrapper: ({ children }) => (
+ {children}
+ ),
+ ...renderOptions,
+ })
+}
+
+export function renderHookWithProviders(
+ hook: () => TResult,
+ options?: { initialRoute?: string },
+) {
+ const queryClient = createTestQueryClient()
+ const wrapper = ({ children }: { children: ReactNode }) => (
+
+
+ {children}
+
+
+ )
+ return { ...renderHook(hook, { wrapper }), queryClient }
+}
+```
+
+**Step 2: Verify build still works**
+
+```bash
+cd /home/frontend/dak_c2s/frontend && pnpm build
+```
+
+**Step 3: Commit**
+
+```bash
+cd /home/frontend/dak_c2s
+git add frontend/src/test/utils.tsx
+git commit -m "feat: add test utilities (renderWithProviders, renderHookWithProviders)"
+```
+
+---
+
+### Task 4: Service tests — api.ts, authService.ts, disclosureService.ts
+
+**Files:**
+- Create: `frontend/src/services/__tests__/api.test.ts`
+- Create: `frontend/src/services/__tests__/authService.test.ts`
+- Create: `frontend/src/services/__tests__/disclosureService.test.ts`
+
+**Step 1: Write api.ts tests**
+
+Create `frontend/src/services/__tests__/api.test.ts`:
+
+```ts
+import { describe, it, expect, beforeEach } from 'vitest'
+import { http, HttpResponse } from 'msw'
+import { server } from '@/test/mocks/server'
+import api from '@/services/api'
+
+describe('api service', () => {
+ beforeEach(() => {
+ localStorage.clear()
+ })
+
+ it('attaches Authorization header when access_token exists', async () => {
+ localStorage.setItem('access_token', 'test-token')
+ let capturedAuth = ''
+ server.use(
+ http.get('/api/test', ({ request }) => {
+ capturedAuth = request.headers.get('Authorization') ?? ''
+ return HttpResponse.json({ ok: true })
+ }),
+ )
+ await api.get('/test')
+ expect(capturedAuth).toBe('Bearer test-token')
+ })
+
+ it('does not attach Authorization header when no token', async () => {
+ let capturedAuth: string | null = null
+ server.use(
+ http.get('/api/test', ({ request }) => {
+ capturedAuth = request.headers.get('Authorization')
+ return HttpResponse.json({ ok: true })
+ }),
+ )
+ await api.get('/test')
+ expect(capturedAuth).toBeNull()
+ })
+
+ it('refreshes token on 401 and retries request', async () => {
+ localStorage.setItem('access_token', 'expired-token')
+ localStorage.setItem('refresh_token', 'valid-refresh')
+ let callCount = 0
+ server.use(
+ http.get('/api/test', () => {
+ callCount++
+ if (callCount === 1) return new HttpResponse(null, { status: 401 })
+ return HttpResponse.json({ ok: true })
+ }),
+ http.post('/api/auth/refresh', () =>
+ HttpResponse.json({ access_token: 'new-token' }),
+ ),
+ )
+ const res = await api.get('/test')
+ expect(res.data).toEqual({ ok: true })
+ expect(localStorage.getItem('access_token')).toBe('new-token')
+ })
+
+ it('redirects to /login when refresh fails', async () => {
+ localStorage.setItem('access_token', 'expired-token')
+ localStorage.setItem('refresh_token', 'invalid-refresh')
+ const originalLocation = window.location.href
+ // Mock window.location.href
+ Object.defineProperty(window, 'location', {
+ value: { ...window.location, href: originalLocation },
+ writable: true,
+ })
+ server.use(
+ http.get('/api/test', () => new HttpResponse(null, { status: 401 })),
+ http.post('/api/auth/refresh', () => new HttpResponse(null, { status: 401 })),
+ )
+ await expect(api.get('/test')).rejects.toThrow()
+ expect(localStorage.getItem('access_token')).toBeNull()
+ expect(localStorage.getItem('refresh_token')).toBeNull()
+ })
+
+ it('passes through non-401 errors', async () => {
+ server.use(
+ http.get('/api/test', () => new HttpResponse(null, { status: 500 })),
+ )
+ await expect(api.get('/test')).rejects.toThrow()
+ })
+
+ it('does not retry 401 twice', async () => {
+ localStorage.setItem('access_token', 'token')
+ localStorage.setItem('refresh_token', 'refresh')
+ let callCount = 0
+ server.use(
+ http.get('/api/test', () => {
+ callCount++
+ return new HttpResponse(null, { status: 401 })
+ }),
+ http.post('/api/auth/refresh', () =>
+ HttpResponse.json({ access_token: 'new-token' }),
+ ),
+ )
+ await expect(api.get('/test')).rejects.toThrow()
+ expect(callCount).toBe(2) // original + 1 retry
+ })
+})
+```
+
+**Step 2: Write authService.ts tests**
+
+Create `frontend/src/services/__tests__/authService.test.ts`:
+
+```ts
+import { describe, it, expect, beforeEach } from 'vitest'
+import { http, HttpResponse } from 'msw'
+import { server } from '@/test/mocks/server'
+import * as authService from '@/services/authService'
+import { mockUser, mockTokenResponse } from '@/test/mocks/data'
+
+describe('authService', () => {
+ beforeEach(() => {
+ localStorage.clear()
+ })
+
+ describe('login', () => {
+ it('stores tokens and returns response', async () => {
+ const result = await authService.login({ email: 'admin@test.de', password: 'pass' })
+ expect(result.access_token).toBe('mock-access-token')
+ expect(result.user.username).toBe('admin')
+ expect(localStorage.getItem('access_token')).toBe('mock-access-token')
+ expect(localStorage.getItem('refresh_token')).toBe('mock-refresh-token')
+ })
+
+ it('throws on invalid credentials', async () => {
+ server.use(
+ http.post('/api/auth/login', () => HttpResponse.json({ detail: 'Invalid credentials' }, { status: 401 })),
+ )
+ await expect(authService.login({ email: 'bad@test.de', password: 'wrong' })).rejects.toThrow()
+ })
+ })
+
+ describe('register', () => {
+ it('returns user', async () => {
+ const result = await authService.register({ username: 'new', email: 'new@test.de', password: 'pass' })
+ expect(result.user.username).toBe('admin')
+ })
+ })
+
+ describe('logout', () => {
+ it('clears tokens from localStorage', async () => {
+ localStorage.setItem('access_token', 'token')
+ localStorage.setItem('refresh_token', 'refresh')
+ await authService.logout()
+ expect(localStorage.getItem('access_token')).toBeNull()
+ expect(localStorage.getItem('refresh_token')).toBeNull()
+ })
+
+ it('clears tokens even if API call fails', async () => {
+ localStorage.setItem('access_token', 'token')
+ localStorage.setItem('refresh_token', 'refresh')
+ server.use(
+ http.post('/api/auth/logout', () => new HttpResponse(null, { status: 500 })),
+ )
+ await authService.logout()
+ expect(localStorage.getItem('access_token')).toBeNull()
+ })
+ })
+
+ describe('getMe', () => {
+ it('returns user profile', async () => {
+ const user = await authService.getMe()
+ expect(user.id).toBe(1)
+ expect(user.email).toBe('admin@test.de')
+ })
+ })
+
+ describe('updateProfile', () => {
+ it('returns updated user', async () => {
+ const user = await authService.updateProfile({ first_name: 'Updated' })
+ expect(user.id).toBe(1)
+ })
+ })
+
+ describe('uploadAvatar', () => {
+ it('sends FormData and returns user', async () => {
+ const file = new File(['data'], 'avatar.png', { type: 'image/png' })
+ const user = await authService.uploadAvatar(file)
+ expect(user.id).toBe(1)
+ })
+ })
+
+ describe('deleteAvatar', () => {
+ it('returns user', async () => {
+ const user = await authService.deleteAvatar()
+ expect(user.id).toBe(1)
+ })
+ })
+
+ describe('changePassword', () => {
+ it('succeeds without error', async () => {
+ await expect(
+ authService.changePassword({ old_password: 'old', new_password: 'new' }),
+ ).resolves.not.toThrow()
+ })
+
+ it('throws on wrong old password', async () => {
+ server.use(
+ http.post('/api/auth/change-password', () =>
+ HttpResponse.json({ detail: 'Wrong password' }, { status: 400 }),
+ ),
+ )
+ await expect(
+ authService.changePassword({ old_password: 'wrong', new_password: 'new' }),
+ ).rejects.toThrow()
+ })
+ })
+
+ describe('MFA', () => {
+ it('setupMFA returns secret and qr_uri', async () => {
+ const result = await authService.setupMFA()
+ expect(result.secret).toBe('ABCDEF')
+ expect(result.qr_uri).toContain('otpauth://')
+ })
+
+ it('verifyMFA succeeds', async () => {
+ await expect(
+ authService.verifyMFA({ secret: 'ABCDEF', code: '123456' }),
+ ).resolves.not.toThrow()
+ })
+
+ it('disableMFA succeeds', async () => {
+ await expect(authService.disableMFA('password')).resolves.not.toThrow()
+ })
+ })
+})
+```
+
+**Step 3: Write disclosureService.ts tests**
+
+Create `frontend/src/services/__tests__/disclosureService.test.ts`:
+
+```ts
+import { describe, it, expect } from 'vitest'
+import { http, HttpResponse } from 'msw'
+import { server } from '@/test/mocks/server'
+import * as disclosureService from '@/services/disclosureService'
+import { mockDisclosureRequest } from '@/test/mocks/data'
+
+describe('disclosureService', () => {
+ describe('requestDisclosure', () => {
+ it('sends request and returns disclosure', async () => {
+ const result = await disclosureService.requestDisclosure(1, 'KVNR-Fehler')
+ expect(result.id).toBe(1)
+ expect(result.status).toBe('pending')
+ })
+
+ it('throws on error', async () => {
+ server.use(
+ http.post('/api/cases/:id/disclosure-request', () =>
+ HttpResponse.json({ detail: 'Already requested' }, { status: 400 }),
+ ),
+ )
+ await expect(disclosureService.requestDisclosure(1, 'reason')).rejects.toThrow()
+ })
+ })
+
+ describe('getDisclosureRequests', () => {
+ it('returns list of disclosures', async () => {
+ const result = await disclosureService.getDisclosureRequests('pending')
+ expect(result).toHaveLength(1)
+ expect(result[0].status).toBe('pending')
+ })
+
+ it('passes status parameter', async () => {
+ let capturedUrl = ''
+ server.use(
+ http.get('/api/admin/disclosure-requests', ({ request }) => {
+ capturedUrl = request.url
+ return HttpResponse.json([])
+ }),
+ )
+ await disclosureService.getDisclosureRequests('approved')
+ expect(capturedUrl).toContain('status=approved')
+ })
+
+ it('works without status filter', async () => {
+ const result = await disclosureService.getDisclosureRequests()
+ expect(Array.isArray(result)).toBe(true)
+ })
+ })
+
+ describe('getDisclosureCount', () => {
+ it('returns pending count', async () => {
+ const count = await disclosureService.getDisclosureCount()
+ expect(count).toBe(1)
+ })
+ })
+
+ describe('reviewDisclosure', () => {
+ it('approves disclosure', async () => {
+ const result = await disclosureService.reviewDisclosure(1, 'approved')
+ expect(result.status).toBe('approved')
+ })
+
+ it('rejects disclosure', async () => {
+ server.use(
+ http.put('/api/admin/disclosure-requests/:id', () =>
+ HttpResponse.json({ ...mockDisclosureRequest, status: 'rejected' }),
+ ),
+ )
+ const result = await disclosureService.reviewDisclosure(1, 'rejected')
+ expect(result.status).toBe('rejected')
+ })
+ })
+})
+```
+
+**Step 4: Run tests**
+
+```bash
+cd /home/frontend/dak_c2s/frontend && pnpm test
+```
+
+Expected: All service tests pass (~25 tests).
+
+**Step 5: Commit**
+
+```bash
+cd /home/frontend/dak_c2s
+git add frontend/src/services/__tests__/
+git commit -m "test: add service tests (api, authService, disclosureService)"
+```
+
+---
+
+### Task 5: Hook tests — useDashboard, useCases
+
+**Files:**
+- Create: `frontend/src/hooks/__tests__/useDashboard.test.ts`
+- Create: `frontend/src/hooks/__tests__/useCases.test.ts`
+
+**Step 1: Write useDashboard tests**
+
+Create `frontend/src/hooks/__tests__/useDashboard.test.ts`:
+
+```ts
+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 { useDashboard } from '@/hooks/useDashboard'
+import { mockDashboardResponse } from '@/test/mocks/data'
+
+describe('useDashboard', () => {
+ it('fetches dashboard data for given year', async () => {
+ const { result } = renderHookWithProviders(() => useDashboard(2026))
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
+ expect(result.current.data).toEqual(mockDashboardResponse)
+ })
+
+ it('includes year in query key (refetches on change)', 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', async () => {
+ server.use(
+ http.get('/api/reports/dashboard', () => new HttpResponse(null, { status: 500 })),
+ )
+ const { result } = renderHookWithProviders(() => useDashboard(2026))
+ await waitFor(() => expect(result.current.isError).toBe(true))
+ })
+})
+```
+
+**Step 2: Write useCases tests**
+
+Create `frontend/src/hooks/__tests__/useCases.test.ts`:
+
+```ts
+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 { useCases, usePendingIcdCases, useCaseUpdate, useKvnrUpdate, useIcdUpdate } from '@/hooks/useCases'
+import { mockCaseListResponse, mockCase, mockEmptyCaseList } from '@/test/mocks/data'
+
+describe('useCases', () => {
+ it('fetches cases with filters', async () => {
+ const { result } = renderHookWithProviders(() =>
+ useCases({ page: 1, per_page: 50 }),
+ )
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
+ expect(result.current.data?.items).toHaveLength(2)
+ expect(result.current.data?.total).toBe(2)
+ })
+
+ it('passes filter params to API', async () => {
+ let capturedParams: Record = {}
+ server.use(
+ http.get('/api/cases/', ({ request }) => {
+ const url = new URL(request.url)
+ url.searchParams.forEach((v, k) => { capturedParams[k] = v })
+ return HttpResponse.json(mockCaseListResponse)
+ }),
+ )
+ const { result } = renderHookWithProviders(() =>
+ useCases({ page: 1, per_page: 50, search: 'test', jahr: 2026, fallgruppe: 'onko', has_icd: 'true' }),
+ )
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
+ expect(capturedParams.search).toBe('test')
+ expect(capturedParams.jahr).toBe('2026')
+ expect(capturedParams.fallgruppe).toBe('onko')
+ expect(capturedParams.has_icd).toBe('true')
+ })
+
+ it('respects enabled: false', async () => {
+ const { result } = renderHookWithProviders(() =>
+ useCases({ page: 1, per_page: 50 }, { enabled: false }),
+ )
+ expect(result.current.fetchStatus).toBe('idle')
+ })
+
+ it('handles empty response', async () => {
+ server.use(
+ http.get('/api/cases/', () => HttpResponse.json(mockEmptyCaseList)),
+ )
+ const { result } = renderHookWithProviders(() =>
+ useCases({ page: 1, per_page: 50 }),
+ )
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
+ expect(result.current.data?.items).toHaveLength(0)
+ })
+
+ it('handles server error', async () => {
+ server.use(
+ http.get('/api/cases/', () => new HttpResponse(null, { status: 500 })),
+ )
+ const { result } = renderHookWithProviders(() =>
+ useCases({ page: 1, per_page: 50 }),
+ )
+ await waitFor(() => expect(result.current.isError).toBe(true))
+ })
+})
+
+describe('usePendingIcdCases', () => {
+ it('fetches pending ICD cases', async () => {
+ const { result } = renderHookWithProviders(() =>
+ usePendingIcdCases(1, 50),
+ )
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
+ expect(result.current.data?.items).toHaveLength(2)
+ })
+
+ it('respects enabled: false', async () => {
+ const { result } = renderHookWithProviders(() =>
+ usePendingIcdCases(1, 50, { enabled: false }),
+ )
+ expect(result.current.fetchStatus).toBe('idle')
+ })
+})
+
+describe('useCaseUpdate', () => {
+ it('updates case and invalidates cache', async () => {
+ const { result, queryClient } = renderHookWithProviders(() => useCaseUpdate())
+ let updatedCase
+ await act(async () => {
+ updatedCase = await result.current.mutateAsync({ id: 1, data: { kommentar: 'Test' } })
+ })
+ expect(updatedCase).toBeDefined()
+ expect(updatedCase!.id).toBe(1)
+ })
+
+ it('handles error', async () => {
+ server.use(
+ http.put('/api/cases/:id', () => HttpResponse.json({ detail: 'Forbidden' }, { status: 403 })),
+ )
+ const { result } = renderHookWithProviders(() => useCaseUpdate())
+ await expect(
+ act(() => result.current.mutateAsync({ id: 1, data: {} })),
+ ).rejects.toThrow()
+ })
+})
+
+describe('useKvnrUpdate', () => {
+ it('updates KVNR', async () => {
+ const { result } = renderHookWithProviders(() => useKvnrUpdate())
+ let updatedCase
+ await act(async () => {
+ updatedCase = await result.current.mutateAsync({ id: 1, kvnr: 'A123456789' })
+ })
+ expect(updatedCase!.id).toBe(1)
+ })
+
+ it('handles null KVNR', async () => {
+ const { result } = renderHookWithProviders(() => useKvnrUpdate())
+ await act(async () => {
+ await result.current.mutateAsync({ id: 1, kvnr: null })
+ })
+ expect(result.current.isSuccess).toBe(true)
+ })
+})
+
+describe('useIcdUpdate', () => {
+ it('updates ICD and invalidates cases + dashboard', async () => {
+ const { result } = renderHookWithProviders(() => useIcdUpdate())
+ await act(async () => {
+ await result.current.mutateAsync({ id: 1, icd: 'C50.9' })
+ })
+ expect(result.current.isSuccess).toBe(true)
+ })
+
+ it('handles error', async () => {
+ server.use(
+ http.put('/api/cases/:id/icd', () =>
+ HttpResponse.json({ detail: 'Invalid ICD' }, { status: 422 })),
+ )
+ const { result } = renderHookWithProviders(() => useIcdUpdate())
+ await expect(
+ act(() => result.current.mutateAsync({ id: 1, icd: 'INVALID' })),
+ ).rejects.toThrow()
+ })
+})
+```
+
+**Step 3: Run tests**
+
+```bash
+cd /home/frontend/dak_c2s/frontend && pnpm test
+```
+
+**Step 4: Commit**
+
+```bash
+cd /home/frontend/dak_c2s
+git add frontend/src/hooks/__tests__/
+git commit -m "test: add hook tests for useDashboard and useCases"
+```
+
+---
+
+### Task 6: Hook tests — useDisclosures, useAuditLog, useUsers
+
+**Files:**
+- Create: `frontend/src/hooks/__tests__/useDisclosures.test.ts`
+- Create: `frontend/src/hooks/__tests__/useAuditLog.test.ts`
+- Create: `frontend/src/hooks/__tests__/useUsers.test.ts`
+
+The subagent should read the existing hook implementations and the mock data/handlers, then write comprehensive tests covering:
+
+**useDisclosures:** query with status filter, empty list, useReviewDisclosure approve/reject/error, cache invalidation of `['disclosures']` and `['notifications']`
+
+**useAuditLog:** query with various filters (skip, limit, user_id, action, date_from, date_to), empty response, error
+
+**useUsers:** useUsers query success/empty, useCreateUser success/validation error/cache invalidation, useUpdateUser success/error/cache invalidation
+
+Follow the same pattern as Task 5 — use `renderHookWithProviders`, `waitFor`, `act`, MSW server overrides for error cases.
+
+**Run tests, then commit:**
+
+```bash
+cd /home/frontend/dak_c2s/frontend && pnpm test
+git add frontend/src/hooks/__tests__/ && git commit -m "test: add hook tests for useDisclosures, useAuditLog, useUsers"
+```
+
+---
+
+### Task 7: Hook tests — useNotifications, useReports
+
+**Files:**
+- Create: `frontend/src/hooks/__tests__/useNotifications.test.ts`
+- Create: `frontend/src/hooks/__tests__/useReports.test.ts`
+
+**useNotifications:** query returns notifications + unreadCount, markAsRead mutation, markAllAsRead mutation, refresh invalidates cache, loading state, refetchInterval is configured (check via `vi.advanceTimersByTime` with `vi.useFakeTimers`)
+
+**useReports:** useReports query success/empty, useGenerateReport success/error/cache invalidation, useDeleteReports success/error/cache invalidation
+
+Follow same patterns. Run tests, then commit:
+
+```bash
+cd /home/frontend/dak_c2s/frontend && pnpm test
+git add frontend/src/hooks/__tests__/ && git commit -m "test: add hook tests for useNotifications and useReports"
+```
+
+---
+
+### Task 8: Page integration tests — DashboardPage, DisclosuresPage, AdminAuditPage
+
+**Files:**
+- Create: `frontend/src/pages/__tests__/DashboardPage.test.tsx`
+- Create: `frontend/src/pages/__tests__/DisclosuresPage.test.tsx`
+- Create: `frontend/src/pages/__tests__/AdminAuditPage.test.tsx`
+
+These are simpler pages. Use `renderWithProviders` and `screen` from `@testing-library/react`. MSW handles API responses.
+
+**DashboardPage tests:**
+- Renders KPI cards with correct values (150, 12, 8, 95)
+- Shows year selector
+- Renders loading skeletons initially
+- Filters 0-value Fallgruppen from pie chart data
+- Handles error state
+
+**DisclosuresPage tests:**
+- Renders pending disclosure requests
+- Shows approve/reject buttons
+- Handles empty list message
+- Shows loading skeletons
+- Disables buttons during mutation (isPending)
+
+**AdminAuditPage tests:**
+- Renders audit log entries
+- Shows filter controls (user_id, action, date_from, date_to)
+- Handles empty list
+- Shows loading state
+
+NOTE: The MSW handlers from `handlers.ts` already mock the auth endpoint (`GET /api/auth/me`), which AuthProvider calls on mount. The mock returns `mockUser` (admin role). For pages that need `isAdmin`, this is already handled. For tests needing a non-admin user, override the handler in the test.
+
+**Run tests, then commit:**
+
+```bash
+cd /home/frontend/dak_c2s/frontend && pnpm test
+git add frontend/src/pages/__tests__/ && git commit -m "test: add page tests for Dashboard, Disclosures, AdminAudit"
+```
+
+---
+
+### Task 9: Page integration tests — AdminUsersPage, ReportsPage, LoginPage
+
+**Files:**
+- Create: `frontend/src/pages/__tests__/AdminUsersPage.test.tsx`
+- Create: `frontend/src/pages/__tests__/ReportsPage.test.tsx`
+- Create: `frontend/src/pages/__tests__/LoginPage.test.tsx`
+
+**AdminUsersPage tests:**
+- Renders user list
+- Create dialog opens and submits
+- Edit dialog opens and submits
+- Handles validation errors
+- Shows loading state
+
+**ReportsPage tests:**
+- Renders report list
+- Generate report form works
+- Delete with checkbox selection
+- Shows isPending states on buttons
+- Handles errors
+
+**LoginPage tests:**
+- Renders email/password fields
+- Shows validation errors for empty fields
+- Submits and redirects on success
+- Shows error message on failed login
+
+**Run tests, then commit:**
+
+```bash
+cd /home/frontend/dak_c2s/frontend && pnpm test
+git add frontend/src/pages/__tests__/ && git commit -m "test: add page tests for AdminUsers, Reports, Login"
+```
+
+---
+
+### Task 10: Page integration tests — CasesPage (most complex)
+
+**Files:**
+- Create: `frontend/src/pages/__tests__/CasesPage.test.tsx`
+
+This is the most complex page. Test:
+
+- Renders case list table with fall_id, datum, fallgruppe columns
+- Search input triggers debounced filter
+- Year/Fallgruppe/ICD select filters work
+- Pagination controls (prev/next, page display)
+- Clicking a row opens the detail Sheet
+- Detail sheet shows case fields (fall_id, CRM-Ticket, Datum, etc.)
+- Edit button enables inline editing
+- Save button calls mutation
+- ICD input + save works
+- pendingIcdOnly mode fetches from different endpoint
+- Empty list shows message
+- Admin sees Nachname/Vorname columns, non-admin does not
+
+For the Sheet/inline-edit tests, use `@testing-library/user-event` for realistic user interactions (click, type, tab).
+
+**Run tests, then commit:**
+
+```bash
+cd /home/frontend/dak_c2s/frontend && pnpm test
+git add frontend/src/pages/__tests__/ && git commit -m "test: add page tests for CasesPage (list + detail + inline edit)"
+```
+
+---
+
+### Task 11: Page integration tests — ProtectedRoute, remaining pages
+
+**Files:**
+- Create: `frontend/src/components/layout/__tests__/ProtectedRoute.test.tsx`
+
+**ProtectedRoute tests:**
+- Redirects to /login when not authenticated (no token)
+- Shows children when authenticated
+- `requireAdmin` blocks non-admin users
+- `requireAdmin` allows admin users
+- Shows loading state while auth is loading
+
+Read the actual ProtectedRoute component first to understand its logic.
+
+Also add any remaining page tests if ImportPage or AccountPage have been skipped. If time allows, add basic render tests for those.
+
+**Run tests, then commit:**
+
+```bash
+cd /home/frontend/dak_c2s/frontend && pnpm test
+git add frontend/src/ && git commit -m "test: add ProtectedRoute tests and remaining page tests"
+```
+
+---
+
+### Task 12: Install Playwright and create E2E tests
+
+**Files:**
+- Modify: `frontend/package.json`
+- Create: `frontend/playwright.config.ts`
+- Create: `frontend/e2e/auth.spec.ts`
+- Create: `frontend/e2e/dashboard.spec.ts`
+- Create: `frontend/e2e/cases.spec.ts`
+- Create: `frontend/e2e/admin.spec.ts`
+
+**Step 1: Install Playwright**
+
+```bash
+cd /home/frontend/dak_c2s/frontend
+pnpm add -D @playwright/test
+npx playwright install chromium
+```
+
+**Step 2: Create playwright.config.ts**
+
+```ts
+import { defineConfig } from '@playwright/test'
+
+export default defineConfig({
+ testDir: './e2e',
+ timeout: 30_000,
+ retries: 1,
+ use: {
+ baseURL: 'http://localhost:5173',
+ headless: true,
+ screenshot: 'only-on-failure',
+ },
+ webServer: {
+ command: 'pnpm dev',
+ port: 5173,
+ reuseExistingServer: true,
+ },
+})
+```
+
+**Step 3: Create E2E test files**
+
+Create `frontend/e2e/auth.spec.ts`:
+
+```ts
+import { test, expect } from '@playwright/test'
+
+test.describe('Authentication', () => {
+ test('login with valid credentials redirects to dashboard', async ({ page }) => {
+ await page.goto('/login')
+ await page.fill('input[type="email"]', 'admin@dak-portal.de')
+ await page.fill('input[type="password"]', 'admin123')
+ await page.click('button[type="submit"]')
+ await expect(page).toHaveURL(/dashboard/)
+ await expect(page.locator('h1')).toContainText('Dashboard')
+ })
+
+ test('login with invalid credentials shows error', async ({ page }) => {
+ await page.goto('/login')
+ await page.fill('input[type="email"]', 'wrong@test.de')
+ await page.fill('input[type="password"]', 'wrongpass')
+ await page.click('button[type="submit"]')
+ await expect(page.locator('[role="alert"], .text-red-600, .text-destructive')).toBeVisible()
+ })
+
+ test('protected route redirects to login without auth', async ({ page }) => {
+ await page.goto('/dashboard')
+ await expect(page).toHaveURL(/login/)
+ })
+
+ test('logout redirects to login', async ({ page }) => {
+ // Login first
+ await page.goto('/login')
+ await page.fill('input[type="email"]', 'admin@dak-portal.de')
+ await page.fill('input[type="password"]', 'admin123')
+ await page.click('button[type="submit"]')
+ await expect(page).toHaveURL(/dashboard/)
+ // Logout via user menu
+ await page.click('[data-testid="user-menu"], button:has(span.sr-only:has-text("Abmelden"))')
+ // This may need adjustment based on actual DOM structure
+ })
+})
+```
+
+Create `frontend/e2e/dashboard.spec.ts`:
+
+```ts
+import { test, expect } from '@playwright/test'
+
+test.describe('Dashboard', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/login')
+ await page.fill('input[type="email"]', 'admin@dak-portal.de')
+ await page.fill('input[type="password"]', 'admin123')
+ await page.click('button[type="submit"]')
+ await expect(page).toHaveURL(/dashboard/)
+ })
+
+ test('shows KPI cards', async ({ page }) => {
+ await expect(page.getByText('Fälle gesamt')).toBeVisible()
+ await expect(page.getByText('Offene ICD')).toBeVisible()
+ await expect(page.getByText('Offene Codierung')).toBeVisible()
+ await expect(page.getByText('Gutachten gesamt')).toBeVisible()
+ })
+
+ test('year selector changes data', async ({ page }) => {
+ await page.click('button:has-text("2026")')
+ await page.click('[role="option"]:has-text("2025")')
+ // Data should reload (check network or wait for content change)
+ await expect(page.getByText('Fälle gesamt')).toBeVisible()
+ })
+
+ test('shows Fallgruppen chart', async ({ page }) => {
+ await expect(page.getByText('Fallgruppen')).toBeVisible()
+ })
+})
+```
+
+Create `frontend/e2e/cases.spec.ts`:
+
+```ts
+import { test, expect } from '@playwright/test'
+
+test.describe('Cases', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/login')
+ await page.fill('input[type="email"]', 'admin@dak-portal.de')
+ await page.fill('input[type="password"]', 'admin123')
+ await page.click('button[type="submit"]')
+ await expect(page).toHaveURL(/dashboard/)
+ await page.goto('/cases')
+ })
+
+ test('shows case list table', async ({ page }) => {
+ await expect(page.getByText('Fälle')).toBeVisible()
+ await expect(page.locator('table')).toBeVisible()
+ await expect(page.getByText('Fall-ID')).toBeVisible()
+ })
+
+ test('search filters cases', async ({ page }) => {
+ await page.fill('input[placeholder*="Suche"]', 'onko')
+ // Wait for debounce (300ms) + API response
+ await page.waitForTimeout(500)
+ await expect(page.locator('table')).toBeVisible()
+ })
+
+ test('clicking a case opens detail sheet', async ({ page }) => {
+ await page.locator('table tbody tr').first().click()
+ await expect(page.locator('[role="dialog"], [data-state="open"]')).toBeVisible()
+ })
+})
+```
+
+Create `frontend/e2e/admin.spec.ts`:
+
+```ts
+import { test, expect } from '@playwright/test'
+
+test.describe('Admin Pages', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/login')
+ await page.fill('input[type="email"]', 'admin@dak-portal.de')
+ await page.fill('input[type="password"]', 'admin123')
+ await page.click('button[type="submit"]')
+ await expect(page).toHaveURL(/dashboard/)
+ })
+
+ test('admin users page loads', async ({ page }) => {
+ await page.goto('/admin/users')
+ await expect(page.getByText('Benutzer')).toBeVisible()
+ })
+
+ test('audit log page loads', async ({ page }) => {
+ await page.goto('/admin/audit')
+ await expect(page.getByText('Audit-Log')).toBeVisible()
+ })
+
+ test('disclosures page loads', async ({ page }) => {
+ await page.goto('/admin/disclosures')
+ await expect(page.getByText('Freigabe-Anfragen')).toBeVisible()
+ })
+})
+```
+
+**Step 4: Add E2E script to package.json**
+
+Add to `scripts`: `"test:e2e": "playwright test"`
+
+**Step 5: Commit**
+
+```bash
+cd /home/frontend/dak_c2s
+git add frontend/playwright.config.ts frontend/e2e/ frontend/package.json frontend/pnpm-lock.yaml
+git commit -m "test: add Playwright E2E tests (auth, dashboard, cases, admin)"
+```
+
+---
+
+### Task 13: Final verification, coverage, and deploy
+
+**Step 1: Run all unit/integration tests**
+
+```bash
+cd /home/frontend/dak_c2s/frontend && pnpm test
+```
+
+Expected: All ~135 unit/integration tests pass.
+
+**Step 2: Run coverage report**
+
+```bash
+cd /home/frontend/dak_c2s/frontend && pnpm test:coverage
+```
+
+Review coverage output — hooks and services should have >90% coverage.
+
+**Step 3: Verify build still works**
+
+```bash
+cd /home/frontend/dak_c2s/frontend && pnpm build
+```
+
+**Step 4: Push and deploy**
+
+```bash
+cd /home/frontend/dak_c2s
+git push origin develop
+git checkout main && git pull origin main && git merge develop && git push origin main
+git checkout develop
+```
+
+Deploy to Hetzner 1:
+
+```bash
+ssh hetzner1 "cd /opt/dak-portal && git pull origin main && cd frontend && pnpm install && pnpm build && cp -r dist/* /var/www/vhosts/complexcaresolutions.de/dak.complexcaresolutions.de/dist/"
+```
+
+Note: E2E tests (`pnpm test:e2e`) require a running dev server + backend and should be run manually on the dev machine, not during deployment.