From 2411c330a247cc9667d11c2aa92ea6a4683093e5 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 22 Mar 2026 20:00:41 +0100 Subject: [PATCH] test(e2e): add admin management journey (users, groups, tags) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full lifecycle: create group → create user → edit user → reset password → verify login → delete user → delete group → rename tag. Self-contained: everything created is also deleted. Refs #48 Co-Authored-By: Claude Sonnet 4.6 --- frontend/e2e/admin.spec.ts | 211 +++++++++++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 frontend/e2e/admin.spec.ts diff --git a/frontend/e2e/admin.spec.ts b/frontend/e2e/admin.spec.ts new file mode 100644 index 00000000..209cec3d --- /dev/null +++ b/frontend/e2e/admin.spec.ts @@ -0,0 +1,211 @@ +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' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Gruppen' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Schlagworte' })).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' }).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' }).click(); + + // Hover over the "Familie" row to reveal the opacity-0 action buttons + const familieRow = page.locator('li').filter({ hasText: /^Familie$/ }); + await familieRow.hover(); + await familieRow.getByRole('button', { name: 'Schlagwort bearbeiten' }).click(); + + const nameInput = familieRow.locator('input[name="name"]'); + await nameInput.fill('Familie (E2E)'); + await familieRow.getByRole('button', { name: /Speichern/i }).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' }).click(); + + const renamedRow = page.locator('li').filter({ hasText: /^Familie \(E2E\)$/ }); + await renamedRow.hover(); + await renamedRow.getByRole('button', { name: 'Schlagwort bearbeiten' }).click(); + + const nameInput = renamedRow.locator('input[name="name"]'); + await nameInput.fill('Familie'); + await renamedRow.getByRole('button', { name: /Speichern/i }).click(); + + await expect(page.getByText('Familie')).toBeVisible(); + await page.screenshot({ path: 'test-results/e2e/admin-tag-restored.png' }); + }); +});