test: add Playwright E2E tests (auth, dashboard, cases, admin)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
CCS Admin 2026-02-26 21:44:25 +00:00
parent edf30c02ec
commit 77805191cf
7 changed files with 254 additions and 1 deletions

View file

@ -0,0 +1,46 @@
import { test, expect } from '@playwright/test'
test.describe('Admin pages', () => {
test.beforeEach(async ({ page }) => {
// Log in as admin before each test
await page.goto('/login')
await page.getByLabel('E-Mail').fill('admin@dak-portal.de')
await page.getByLabel('Passwort').fill('admin123')
await page.getByRole('button', { name: 'Anmelden' }).click()
await expect(page).toHaveURL(/\/dashboard/)
})
test('admin users page loads', async ({ page }) => {
await page.goto('/admin/users')
// Page heading should be visible
await expect(page.getByRole('heading', { name: 'Benutzer' })).toBeVisible()
// The create button should be present
await expect(page.getByRole('button', { name: /neuen benutzer erstellen/i })).toBeVisible()
// Table headers should be visible
await expect(page.getByRole('columnheader', { name: 'Benutzername' })).toBeVisible()
await expect(page.getByRole('columnheader', { name: 'E-Mail' })).toBeVisible()
await expect(page.getByRole('columnheader', { name: 'Rolle' })).toBeVisible()
})
test('audit log page loads', async ({ page }) => {
await page.goto('/admin/audit')
// Page heading should be visible
await expect(page.getByRole('heading', { name: 'Audit-Log' })).toBeVisible()
// Filter section should be present
await expect(page.getByText('Filter')).toBeVisible()
await expect(page.getByLabel('Benutzer-ID')).toBeVisible()
await expect(page.getByLabel('Aktion')).toBeVisible()
})
test('disclosures page loads', async ({ page }) => {
await page.goto('/admin/disclosures')
// Page heading should be visible
await expect(page.getByRole('heading', { name: 'Freigabe-Anfragen' })).toBeVisible()
})
})

53
frontend/e2e/auth.spec.ts Normal file
View file

@ -0,0 +1,53 @@
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.getByLabel('E-Mail').fill('admin@dak-portal.de')
await page.getByLabel('Passwort').fill('admin123')
await page.getByRole('button', { name: 'Anmelden' }).click()
// Should redirect to dashboard after successful login
await expect(page).toHaveURL(/\/dashboard/)
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible()
})
test('login with invalid credentials shows error message', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('E-Mail').fill('invalid@example.de')
await page.getByLabel('Passwort').fill('wrongpassword')
await page.getByRole('button', { name: 'Anmelden' }).click()
// Should show error alert
await expect(page.getByText('Ungueltige Anmeldedaten')).toBeVisible()
// Should remain on login page
await expect(page).toHaveURL(/\/login/)
})
test('protected route redirects to login without auth', async ({ page }) => {
// Try to access dashboard directly without logging in
await page.goto('/dashboard')
// Should be redirected to login page
await expect(page).toHaveURL(/\/login/)
await expect(page.getByText('DAK Portal')).toBeVisible()
})
test('logout redirects to login', async ({ page }) => {
// First log in
await page.goto('/login')
await page.getByLabel('E-Mail').fill('admin@dak-portal.de')
await page.getByLabel('Passwort').fill('admin123')
await page.getByRole('button', { name: 'Anmelden' }).click()
await expect(page).toHaveURL(/\/dashboard/)
// Click the logout button in the sidebar/header
await page.getByRole('button', { name: /abmelden|logout/i }).click()
// Should be redirected to login page
await expect(page).toHaveURL(/\/login/)
})
})

View file

@ -0,0 +1,52 @@
import { test, expect } from '@playwright/test'
test.describe('Cases', () => {
test.beforeEach(async ({ page }) => {
// Log in before each test
await page.goto('/login')
await page.getByLabel('E-Mail').fill('admin@dak-portal.de')
await page.getByLabel('Passwort').fill('admin123')
await page.getByRole('button', { name: 'Anmelden' }).click()
await expect(page).toHaveURL(/\/dashboard/)
// Navigate to cases page
await page.getByRole('link', { name: /fälle/i }).click()
await expect(page).toHaveURL(/\/cases/)
})
test('case list table is visible', async ({ page }) => {
// Table headers should be visible
await expect(page.getByRole('columnheader', { name: 'Fall-ID' })).toBeVisible()
await expect(page.getByRole('columnheader', { name: 'Datum' })).toBeVisible()
await expect(page.getByRole('columnheader', { name: 'KVNR' })).toBeVisible()
await expect(page.getByRole('columnheader', { name: 'Fallgruppe' })).toBeVisible()
})
test('search input filters cases', async ({ page }) => {
// The search input should be present
const searchInput = page.getByPlaceholder(/suche/i)
await expect(searchInput).toBeVisible()
// Type a search query
await searchInput.fill('onko')
// Wait for debounce and results to update
await page.waitForTimeout(500)
// The page should still show the cases heading
await expect(page.getByRole('heading', { name: 'Fälle' })).toBeVisible()
})
test('clicking a row opens detail sheet', async ({ page }) => {
// Wait for table rows to appear
const firstRow = page.locator('tbody tr').first()
await expect(firstRow).toBeVisible()
// Click the first row
await firstRow.click()
// The detail sheet should open with case information
await expect(page.getByText('Fall-ID')).toBeVisible()
await expect(page.getByRole('button', { name: /bearbeiten/i })).toBeVisible()
})
})

View file

@ -0,0 +1,45 @@
import { test, expect } from '@playwright/test'
test.describe('Dashboard', () => {
test.beforeEach(async ({ page }) => {
// Log in before each test
await page.goto('/login')
await page.getByLabel('E-Mail').fill('admin@dak-portal.de')
await page.getByLabel('Passwort').fill('admin123')
await page.getByRole('button', { name: 'Anmelden' }).click()
await expect(page).toHaveURL(/\/dashboard/)
})
test('KPI cards are visible after login', async ({ page }) => {
// All four KPI cards should be displayed
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 exists and can be changed', async ({ page }) => {
// The year selector trigger should be visible
const yearSelector = page.locator('button').filter({ hasText: /^\d{4}$/ })
await expect(yearSelector).toBeVisible()
// Click it to open the dropdown
await yearSelector.click()
// Previous years should be available in the dropdown
const currentYear = new Date().getFullYear()
const previousYear = currentYear - 1
await expect(page.getByRole('option', { name: String(previousYear) })).toBeVisible()
// Select the previous year
await page.getByRole('option', { name: String(previousYear) }).click()
})
test('Fallgruppen chart section is visible', async ({ page }) => {
await expect(page.getByText('Fallgruppen')).toBeVisible()
})
test('weekly chart section is visible', async ({ page }) => {
await expect(page.getByText('Wöchentliche Übersicht')).toBeVisible()
})
})

View file

@ -10,7 +10,8 @@
"preview": "vite preview", "preview": "vite preview",
"test": "vitest run", "test": "vitest run",
"test:watch": "vitest", "test:watch": "vitest",
"test:coverage": "vitest run --coverage" "test:coverage": "vitest run --coverage",
"test:e2e": "playwright test"
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.2",
@ -35,6 +36,7 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",
"@playwright/test": "^1.58.2",
"@tailwindcss/vite": "^4.2.1", "@tailwindcss/vite": "^4.2.1",
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",

View file

@ -0,0 +1,17 @@
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,
},
})

View file

@ -54,6 +54,9 @@ importers:
'@eslint/js': '@eslint/js':
specifier: ^9.39.1 specifier: ^9.39.1
version: 9.39.3 version: 9.39.3
'@playwright/test':
specifier: ^1.58.2
version: 1.58.2
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.2.1 specifier: ^4.2.1
version: 4.2.1(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)) version: 4.2.1(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1))
@ -680,6 +683,11 @@ packages:
'@open-draft/until@2.1.0': '@open-draft/until@2.1.0':
resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
'@playwright/test@1.58.2':
resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==}
engines: {node: '>=18'}
hasBin: true
'@radix-ui/number@1.1.1': '@radix-ui/number@1.1.1':
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
@ -2483,6 +2491,11 @@ packages:
resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==}
engines: {node: '>=14.14'} engines: {node: '>=14.14'}
fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
fsevents@2.3.3: fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@ -3167,6 +3180,16 @@ packages:
resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==}
engines: {node: '>=16.20.0'} engines: {node: '>=16.20.0'}
playwright-core@1.58.2:
resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==}
engines: {node: '>=18'}
hasBin: true
playwright@1.58.2:
resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==}
engines: {node: '>=18'}
hasBin: true
postcss-selector-parser@7.1.1: postcss-selector-parser@7.1.1:
resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -4422,6 +4445,10 @@ snapshots:
'@open-draft/until@2.1.0': {} '@open-draft/until@2.1.0': {}
'@playwright/test@1.58.2':
dependencies:
playwright: 1.58.2
'@radix-ui/number@1.1.1': {} '@radix-ui/number@1.1.1': {}
'@radix-ui/primitive@1.1.3': {} '@radix-ui/primitive@1.1.3': {}
@ -6287,6 +6314,9 @@ snapshots:
jsonfile: 6.2.0 jsonfile: 6.2.0
universalify: 2.0.1 universalify: 2.0.1
fsevents@2.3.2:
optional: true
fsevents@2.3.3: fsevents@2.3.3:
optional: true optional: true
@ -6865,6 +6895,14 @@ snapshots:
pkce-challenge@5.0.1: {} pkce-challenge@5.0.1: {}
playwright-core@1.58.2: {}
playwright@1.58.2:
dependencies:
playwright-core: 1.58.2
optionalDependencies:
fsevents: 2.3.2
postcss-selector-parser@7.1.1: postcss-selector-parser@7.1.1:
dependencies: dependencies:
cssesc: 3.0.0 cssesc: 3.0.0