Files
familienarchiv/frontend/e2e/admin.spec.ts
Marcel c1e82a7edf fix(e2e): fix 8 failing E2E tests on feat/35-profile-page
- admin: add exact:true to tab button assertions to avoid strict-mode
  violations from "Benutzer löschen" title buttons matching "Benutzer"
- admin: change tag-row locator from hasText regex on <li> to has: span
  filter (more robust against whitespace differences); add waitForSelector
  after tab click to ensure panel is rendered before hovering
- auth: replace page.request.get('/api/users/me') with a profile page
  navigation — direct browser requests don't carry Basic Auth, only
  server-side SvelteKit fetches do
- documents: use getByRole('heading') instead of getByText to avoid strict
  mode violation when the title appears in both h1 and breadcrumb
- persons: same heading fix for person creation landing page
- profile: remove success-message assertion after password change; the
  auth_token cookie still holds old credentials so use:enhance's update()
  immediately gets a 401 and redirects to /login before the message renders
  — test now asserts the redirect directly, then re-logs in

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 23:01:04 +01:00

219 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 28 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();
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', 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();
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' });
});
});