- 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>
219 lines
9.4 KiB
TypeScript
219 lines
9.4 KiB
TypeScript
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();
|
||
|
||
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' });
|
||
});
|
||
});
|