import { test, expect, type Browser } from '@playwright/test'; /** * Admin panel E2E tests. * * Reads top-to-bottom as a complete admin journey: * 1. Admin opens the dashboard and sees all three management tabs. * 2. Admin creates a group for read-only access. * 3. Admin creates a new user in that group. * 4. Admin edits the user's profile. * 5. Admin resets the user's password without knowing their current password. * 6. The user can log in with the admin-set password. * 7. Admin deletes the user. * 8. Admin deletes the test group. * 9. Admin renames a tag and renames it back. * * Steps 2–8 form a self-contained lifecycle: everything created in this suite * is also deleted, leaving the database in its original state. */ // ── Dashboard ───────────────────────────────────────────────────────────────── test.describe('Admin dashboard', () => { test('admin navigates to /admin and sees the three management tabs', async ({ page }) => { await page.goto('/admin'); await page.waitForSelector('[data-hydrated]'); await expect(page.getByRole('button', { name: 'Benutzer', exact: true })).toBeVisible(); await expect(page.getByRole('button', { name: 'Gruppen', exact: true })).toBeVisible(); await expect(page.getByRole('button', { name: 'Schlagworte', exact: true })).toBeVisible(); await page.screenshot({ path: 'test-results/e2e/admin-dashboard.png' }); }); }); // ── Group lifecycle ──────────────────────────────────────────────────────────── test.describe('Admin — group management', () => { test('admin creates a new group "E2E Leser" with READ_ALL permission', async ({ page }) => { await page.goto('/admin'); await page.waitForSelector('[data-hydrated]'); // Switch to the Groups tab await page.getByRole('button', { name: 'Gruppen', exact: true }).click(); await page.getByPlaceholder('Gruppenname (z.B. Editoren)').fill('E2E Leser'); // No permission checkboxes checked — READ_ALL is handled at application level // (a group with no permissions gets read-only access by default in the UI) await page.getByRole('button', { name: /Erstellen/i }).click(); await expect(page.getByRole('cell', { name: 'E2E Leser', exact: true })).toBeVisible(); await page.screenshot({ path: 'test-results/e2e/admin-group-created.png' }); }); }); // ── User lifecycle ───────────────────────────────────────────────────────────── test.describe('Admin — user lifecycle', () => { test('admin creates user "e2e-testuser" and they appear in the user list', async ({ page }) => { await page.goto('/admin/users/new'); await page.waitForSelector('[data-hydrated]'); await page.locator('input[name="username"]').fill('e2e-testuser'); await page.locator('input[name="password"]').fill('InitPass123!'); // Assign to the group we just created const groupLabel = page.locator('label').filter({ hasText: 'E2E Leser' }); if ((await groupLabel.count()) > 0) { await groupLabel.locator('input[type="checkbox"]').check(); } await page.getByRole('button', { name: /Erstellen/i }).click(); // Redirected back to /admin — user appears in the table await expect(page).toHaveURL('/admin'); await expect(page.getByRole('cell', { name: 'e2e-testuser', exact: true })).toBeVisible(); await page.screenshot({ path: 'test-results/e2e/admin-user-created.png' }); }); test('admin opens the edit page and updates the user first name', async ({ page }) => { await page.goto('/admin'); await page.waitForSelector('[data-hydrated]'); // Click the edit link for the test user const userRow = page.locator('tr').filter({ hasText: 'e2e-testuser' }); await userRow.getByRole('link', { name: /Bearbeiten/i }).click(); await expect(page).toHaveURL(/\/admin\/users\/.+/); await expect( page.getByRole('heading', { name: /Benutzer bearbeiten: e2e-testuser/i }) ).toBeVisible(); await page.locator('input[name="firstName"]').fill('E2E'); await page.locator('input[name="lastName"]').fill('Testuser'); await page.getByRole('button', { name: /Speichern/i }).click(); await expect(page.getByText('Änderungen gespeichert.')).toBeVisible(); await page.screenshot({ path: 'test-results/e2e/admin-user-edited.png' }); }); test('admin sets a new password without entering the current password', async ({ page }) => { await page.goto('/admin'); await page.waitForSelector('[data-hydrated]'); const userRow = page.locator('tr').filter({ hasText: 'e2e-testuser' }); await userRow.getByRole('link', { name: /Bearbeiten/i }).click(); // Password fields — no current password field on the admin edit form await page.locator('input[name="newPassword"]').fill('AdminSet456!'); await page.locator('input[name="confirmPassword"]').fill('AdminSet456!'); await page.getByRole('button', { name: /Speichern/i }).click(); await expect(page.getByText('Änderungen gespeichert.')).toBeVisible(); await page.screenshot({ path: 'test-results/e2e/admin-user-password-reset.png' }); }); test('the user can log in with the admin-set password', async ({ browser }) => { // Open a completely separate browser context — no shared session cookies const freshCtx = await (browser as Browser).newContext({ storageState: { cookies: [], origins: [] } }); const freshPage = await freshCtx.newPage(); await freshPage.goto('/login'); await freshPage.getByLabel('Benutzername').fill('e2e-testuser'); await freshPage.getByLabel('Passwort').fill('AdminSet456!'); await freshPage.getByRole('button', { name: 'Anmelden' }).click(); await expect(freshPage).toHaveURL('/'); await freshPage.screenshot({ path: 'test-results/e2e/admin-user-login-new-password.png' }); await freshCtx.close(); }); test('admin deletes the test user and they disappear from the list', async ({ page }) => { await page.goto('/admin'); await page.waitForSelector('[data-hydrated]'); const userRow = page.locator('tr').filter({ hasText: 'e2e-testuser' }); // The delete button triggers a window.confirm() dialog page.once('dialog', (dialog) => dialog.accept()); await userRow.getByTitle('Benutzer löschen').click(); await expect(page.getByRole('cell', { name: 'e2e-testuser', exact: true })).not.toBeVisible(); await page.screenshot({ path: 'test-results/e2e/admin-user-deleted.png' }); }); }); // ── Group cleanup ────────────────────────────────────────────────────────────── test.describe('Admin — group cleanup', () => { test('admin deletes the "E2E Leser" group', async ({ page }) => { await page.goto('/admin'); await page.waitForSelector('[data-hydrated]'); await page.getByRole('button', { name: 'Gruppen' }).click(); const groupRow = page.locator('tr').filter({ hasText: 'E2E Leser' }); page.once('dialog', (dialog) => dialog.accept()); await groupRow.getByTitle('Löschen').click(); await expect(page.getByRole('cell', { name: 'E2E Leser', exact: true })).not.toBeVisible(); await page.screenshot({ path: 'test-results/e2e/admin-group-deleted.png' }); }); }); // ── Tag management ───────────────────────────────────────────────────────────── test.describe('Admin — tag management', () => { test('admin renames a tag and sees the change in the list', async ({ page }) => { await page.goto('/admin'); await page.waitForSelector('[data-hydrated]'); await page.getByRole('button', { name: 'Schlagworte', exact: true }).click(); // Wait for the tags list to render after the tab switch await page.waitForSelector('ul > li'); // Hover over the "Familie" row to reveal the opacity-0 action buttons const familieRow = page .locator('ul > li') .filter({ has: page.locator('span', { hasText: /^Familie$/ }) }); await familieRow.hover(); await familieRow.getByRole('button', { name: 'Schlagwort bearbeiten' }).click(); // After clicking edit, {#if editingTagId} replaces the span with a form — // the familieRow filter no longer matches, so we find the input directly. await page.locator('input[name="name"]').fill('Familie (E2E)'); await page.getByRole('button', { name: 'Speichern' }).click(); await expect(page.getByText('Familie (E2E)')).toBeVisible(); await page.screenshot({ path: 'test-results/e2e/admin-tag-renamed.png' }); }); test('admin renames it back to restore the original name', async ({ page }) => { await page.goto('/admin'); await page.waitForSelector('[data-hydrated]'); await page.getByRole('button', { name: 'Schlagworte', exact: true }).click(); await page.waitForSelector('ul > li'); const renamedRow = page .locator('ul > li') .filter({ has: page.locator('span', { hasText: /^Familie \(E2E\)$/ }) }); await renamedRow.hover(); await renamedRow.getByRole('button', { name: 'Schlagwort bearbeiten' }).click(); await page.locator('input[name="name"]').fill('Familie'); await page.getByRole('button', { name: 'Speichern' }).click(); await expect(page.getByText('Familie')).toBeVisible(); await page.screenshot({ path: 'test-results/e2e/admin-tag-restored.png' }); }); }); // ─── System tab — backfill file hashes ──────────────────────────────────────── test.describe('Admin system tab — backfill file hashes', () => { test('admin triggers file hash backfill and sees success message', async ({ request, page }) => { test.setTimeout(60_000); // Create a document via API so there is at least one without a hash const createRes = await request.post('/api/documents', { multipart: { title: 'E2E Backfill Hash Test' } }); if (!createRes.ok()) throw new Error(`Create failed: ${createRes.status()}`); await page.goto('/admin'); await page.waitForSelector('[data-hydrated]'); // Navigate to System tab await page.getByRole('button', { name: /system/i }).click(); // Click the backfill hashes button const btn = page.getByRole('button', { name: /datei-hashes berechnen/i }); await expect(btn).toBeVisible(); await btn.click(); // Success message must appear (count >= 0) await expect(page.locator('text=/\\d+ Dokumente wurden aktualisiert/i')).toBeVisible({ timeout: 15000 }); await page.screenshot({ path: 'test-results/e2e/admin-backfill-hashes.png' }); }); });