From 77805191cf04a566cad334cd69f89cb345a64408 Mon Sep 17 00:00:00 2001 From: CCS Admin Date: Thu, 26 Feb 2026 21:44:25 +0000 Subject: [PATCH] test: add Playwright E2E tests (auth, dashboard, cases, admin) Co-Authored-By: Claude Opus 4.6 --- frontend/e2e/admin.spec.ts | 46 +++++++++++++++++++++++++++++ frontend/e2e/auth.spec.ts | 53 ++++++++++++++++++++++++++++++++++ frontend/e2e/cases.spec.ts | 52 +++++++++++++++++++++++++++++++++ frontend/e2e/dashboard.spec.ts | 45 +++++++++++++++++++++++++++++ frontend/package.json | 4 ++- frontend/playwright.config.ts | 17 +++++++++++ frontend/pnpm-lock.yaml | 38 ++++++++++++++++++++++++ 7 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 frontend/e2e/admin.spec.ts create mode 100644 frontend/e2e/auth.spec.ts create mode 100644 frontend/e2e/cases.spec.ts create mode 100644 frontend/e2e/dashboard.spec.ts create mode 100644 frontend/playwright.config.ts diff --git a/frontend/e2e/admin.spec.ts b/frontend/e2e/admin.spec.ts new file mode 100644 index 0000000..3874169 --- /dev/null +++ b/frontend/e2e/admin.spec.ts @@ -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() + }) +}) diff --git a/frontend/e2e/auth.spec.ts b/frontend/e2e/auth.spec.ts new file mode 100644 index 0000000..b5ff455 --- /dev/null +++ b/frontend/e2e/auth.spec.ts @@ -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/) + }) +}) diff --git a/frontend/e2e/cases.spec.ts b/frontend/e2e/cases.spec.ts new file mode 100644 index 0000000..15b88f8 --- /dev/null +++ b/frontend/e2e/cases.spec.ts @@ -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() + }) +}) diff --git a/frontend/e2e/dashboard.spec.ts b/frontend/e2e/dashboard.spec.ts new file mode 100644 index 0000000..922f2f0 --- /dev/null +++ b/frontend/e2e/dashboard.spec.ts @@ -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() + }) +}) diff --git a/frontend/package.json b/frontend/package.json index 1138971..608df84 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,7 +10,8 @@ "preview": "vite preview", "test": "vitest run", "test:watch": "vitest", - "test:coverage": "vitest run --coverage" + "test:coverage": "vitest run --coverage", + "test:e2e": "playwright test" }, "dependencies": { "@hookform/resolvers": "^5.2.2", @@ -35,6 +36,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", + "@playwright/test": "^1.58.2", "@tailwindcss/vite": "^4.2.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts new file mode 100644 index 0000000..86bf192 --- /dev/null +++ b/frontend/playwright.config.ts @@ -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, + }, +}) diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 13c27be..2678ff0 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -54,6 +54,9 @@ importers: '@eslint/js': specifier: ^9.39.1 version: 9.39.3 + '@playwright/test': + specifier: ^1.58.2 + version: 1.58.2 '@tailwindcss/vite': 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)) @@ -680,6 +683,11 @@ packages: '@open-draft/until@2.1.0': 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': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -2483,6 +2491,11 @@ packages: resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} 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: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -3167,6 +3180,16 @@ packages: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} 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: resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} engines: {node: '>=4'} @@ -4422,6 +4445,10 @@ snapshots: '@open-draft/until@2.1.0': {} + '@playwright/test@1.58.2': + dependencies: + playwright: 1.58.2 + '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.3': {} @@ -6287,6 +6314,9 @@ snapshots: jsonfile: 6.2.0 universalify: 2.0.1 + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -6865,6 +6895,14 @@ snapshots: 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: dependencies: cssesc: 3.0.0