- Admin tag test: "Familie" never existed in the database; use "Fest" which is a real seeded tag, with a matching rename-back to restore state - Annotation hash test: the broad `[data-testid^="annotation-"]` locator also matched `annotation-side-panel` (always in DOM, even when off-screen); extend the :not() exclusion list to cover it Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
251 lines
11 KiB
TypeScript
251 lines
11 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 "Fest" row to reveal the opacity-0 action buttons
|
||
const festRow = page
|
||
.locator('ul > li')
|
||
.filter({ has: page.locator('span', { hasText: /^Fest$/ }) });
|
||
await festRow.hover();
|
||
await festRow.getByRole('button', { name: 'Schlagwort bearbeiten' }).click();
|
||
|
||
// After clicking edit, {#if editingTagId} replaces the span with a form —
|
||
// the festRow filter no longer matches, so we find the input directly.
|
||
await page.locator('input[name="name"]').fill('Fest (E2E)');
|
||
await page.getByRole('button', { name: 'Speichern' }).click();
|
||
|
||
await expect(page.getByText('Fest (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: /^Fest \(E2E\)$/ }) });
|
||
await renamedRow.hover();
|
||
await renamedRow.getByRole('button', { name: 'Schlagwort bearbeiten' }).click();
|
||
|
||
await page.locator('input[name="name"]').fill('Fest');
|
||
await page.getByRole('button', { name: 'Speichern' }).click();
|
||
|
||
await expect(page.getByText('Fest')).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' });
|
||
});
|
||
});
|