Compare commits
12 Commits
fb4f8e820c
...
7fbfeb3b39
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fbfeb3b39 | ||
|
|
bbac351f03 | ||
|
|
2411c330a2 | ||
|
|
7d095e159e | ||
|
|
ca73777010 | ||
|
|
0221382c8a | ||
|
|
ea6b727e44 | ||
|
|
2a46136f61 | ||
|
|
c0b9d979ea | ||
|
|
c84bb3ca7b | ||
|
|
cf8425d744 | ||
|
|
1fcd8a6ad6 |
@@ -43,6 +43,42 @@ Repeat for each new behavior.
|
||||
- The Refactor step must not change behavior — if a test breaks, the refactor introduced a bug.
|
||||
- If a bug is reported with no test, write the failing test first, then fix it.
|
||||
|
||||
## User Journeys & E2E Acceptance Criteria
|
||||
|
||||
Every `feature` issue must include two sections before any implementation begins:
|
||||
|
||||
### 1. User Journey
|
||||
|
||||
A plain-prose description of the steps a user takes to get value from the feature. Written from the user's perspective, not the implementation's:
|
||||
|
||||
> User opens a document, clicks "History", sees a chronological list of changes with editor name and timestamp. Clicking a row expands the old vs. new values.
|
||||
|
||||
This makes the scope concrete and prevents scope creep — anything not in the journey is out of scope for the issue.
|
||||
|
||||
### 2. E2E Scenarios
|
||||
|
||||
One or more acceptance criteria written as Playwright-ready scenarios. These become the outermost Red test in the TDD cycle — no feature is considered done until all its E2E scenarios pass:
|
||||
|
||||
```
|
||||
Scenario: View edit history of a document
|
||||
Given I am on a document detail page
|
||||
When I click the "History" tab
|
||||
Then I see at least one revision entry
|
||||
And each entry shows the editor's name and a timestamp
|
||||
```
|
||||
|
||||
Use this format consistently. It maps directly to `test.describe` / `test` blocks in the Playwright spec.
|
||||
|
||||
### Where this fits in the workflow
|
||||
|
||||
```
|
||||
Issue (Journey + Scenarios) → Red E2E test → Implementation → Green
|
||||
```
|
||||
|
||||
The scenarios in the issue are the contract. Write them before planning, treat them as failing tests from day one.
|
||||
|
||||
---
|
||||
|
||||
## Issue Tracking (Gitea)
|
||||
|
||||
All work is tracked in **Gitea** at `http://192.168.178.71:3005` (repo `marcel/familienarchiv`). Never use todo files or CLAUDE.md notes as a substitute.
|
||||
|
||||
@@ -81,7 +81,8 @@ public class DataInitializer {
|
||||
@Profile("e2e")
|
||||
public CommandLineRunner initE2EData(PersonRepository personRepo,
|
||||
DocumentRepository docRepo,
|
||||
TagRepository tagRepo) {
|
||||
TagRepository tagRepo,
|
||||
PasswordEncoder passwordEncoder) {
|
||||
return args -> {
|
||||
if (personRepo.count() > 0) {
|
||||
log.info("E2E seed: Daten bereits vorhanden, überspringe.");
|
||||
@@ -165,8 +166,21 @@ public class DataInitializer {
|
||||
.receivers(Set.of(otto))
|
||||
.build());
|
||||
|
||||
log.info("E2E seed: {} Personen, {} Tags, {} Dokumente erstellt.",
|
||||
personRepo.count(), tagRepo.count(), docRepo.count());
|
||||
// ── Read-only user (for permissions E2E tests) ───────────────────
|
||||
// Username: reader / Password: reader123
|
||||
// Has only READ_ALL — used to assert write controls are absent.
|
||||
UserGroup leserGroup = groupRepository.save(UserGroup.builder()
|
||||
.name("Leser")
|
||||
.permissions(Set.of("READ_ALL"))
|
||||
.build());
|
||||
userRepository.save(AppUser.builder()
|
||||
.username("reader")
|
||||
.password(passwordEncoder.encode("reader123"))
|
||||
.groups(Set.of(leserGroup))
|
||||
.build());
|
||||
|
||||
log.info("E2E seed: {} Personen, {} Tags, {} Dokumente, {} Benutzer erstellt.",
|
||||
personRepo.count(), tagRepo.count(), docRepo.count(), userRepository.count());
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
211
frontend/e2e/admin.spec.ts
Normal file
211
frontend/e2e/admin.spec.ts
Normal file
@@ -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' });
|
||||
});
|
||||
});
|
||||
@@ -48,8 +48,19 @@ test.describe('Authentication', () => {
|
||||
await page.screenshot({ path: 'test-results/e2e/login-success.png' });
|
||||
});
|
||||
|
||||
test('login establishes a session that authenticates API calls', async ({ page }) => {
|
||||
// Guards against regressions where the session cookie is set but broken —
|
||||
// a working URL redirect is not enough evidence that auth works end-to-end.
|
||||
await login(page);
|
||||
const response = await page.request.get('/api/users/me');
|
||||
expect(response.ok()).toBe(true);
|
||||
await page.screenshot({ path: 'test-results/e2e/auth-session-valid.png' });
|
||||
});
|
||||
|
||||
test('logout clears the session and redirects to /login', async ({ page }) => {
|
||||
await login(page);
|
||||
// Logout is inside the user avatar dropdown — open it first
|
||||
await page.locator('button[aria-haspopup="true"]').click();
|
||||
await page.getByRole('button', { name: 'Abmelden' }).click();
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
// Confirm session is gone: navigating to / redirects back
|
||||
|
||||
@@ -80,6 +80,41 @@ test.describe('New document', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Document creation', () => {
|
||||
test('user fills in a title and lands on the new document detail page', async ({ page }) => {
|
||||
await page.goto('/documents/new');
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
|
||||
await page.getByLabel('Titel').fill('E2E Testbrief');
|
||||
await page.getByRole('button', { name: /Speichern/i }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/documents\/[^/]+$/);
|
||||
await expect(page.getByText('E2E Testbrief')).toBeVisible();
|
||||
await page.screenshot({ path: 'test-results/e2e/document-create.png' });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Document editing', () => {
|
||||
test('user opens an existing document, changes the title, and sees the update', async ({
|
||||
page
|
||||
}) => {
|
||||
// Find the document created in the previous describe
|
||||
await page.goto('/?q=E2E+Testbrief');
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
const docLink = page.getByRole('link', { name: 'E2E Testbrief' }).first();
|
||||
const href = await docLink.getAttribute('href');
|
||||
await page.goto(`${href}/edit`);
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
|
||||
await page.getByLabel('Titel').fill('E2E Testbrief (überarbeitet)');
|
||||
await page.getByRole('button', { name: /Speichern/i }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/documents\/[^/]+$/);
|
||||
await expect(page.getByText('E2E Testbrief (überarbeitet)')).toBeVisible();
|
||||
await page.screenshot({ path: 'test-results/e2e/document-edit-save.png' });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Document edit', () => {
|
||||
test('renders the edit form with pre-filled data', async ({ page }) => {
|
||||
// Navigate to home, find first document, go to its edit page
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { login } from './helpers/auth';
|
||||
|
||||
/**
|
||||
* Permission E2E tests.
|
||||
*
|
||||
* Two describe blocks form the full story:
|
||||
* 1. Admin user — can see all write controls.
|
||||
* 2. Read-only user ("reader", seeded in DataInitializer with READ_ALL only) —
|
||||
* can browse content but sees no write controls anywhere.
|
||||
*/
|
||||
|
||||
test.describe('Write permissions — admin user', () => {
|
||||
test('admin user sees Neues Dokument link on home page', async ({ page }) => {
|
||||
@@ -29,3 +39,49 @@ test.describe('Write permissions — admin user', () => {
|
||||
await expect(page.getByRole('button', { name: /Bearbeiten/i })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
// ── Read-only user journey ─────────────────────────────────────────────────────
|
||||
//
|
||||
// The "reader" user is seeded by DataInitializer (e2e profile) with READ_ALL only.
|
||||
// They can browse documents and persons but must not see any mutation controls.
|
||||
|
||||
test.describe('Read-only user — no write controls visible', () => {
|
||||
// Fresh session — no shared admin cookies
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await login(page, 'reader', 'reader123');
|
||||
});
|
||||
|
||||
test('read-only user is redirected to home after login', async ({ page }) => {
|
||||
await expect(page).toHaveURL('/');
|
||||
await page.screenshot({ path: 'test-results/e2e/permissions-reader-home.png' });
|
||||
});
|
||||
|
||||
test('home page does not show the "Neues Dokument" link', async ({ page }) => {
|
||||
await expect(page.getByRole('link', { name: /Neues Dokument/i })).not.toBeVisible();
|
||||
await page.screenshot({ path: 'test-results/e2e/permissions-reader-no-new-doc.png' });
|
||||
});
|
||||
|
||||
test('persons page does not show the "Neue Person" link', async ({ page }) => {
|
||||
await page.goto('/persons');
|
||||
await expect(page.getByRole('link', { name: /Neue Person/i })).not.toBeVisible();
|
||||
await page.screenshot({ path: 'test-results/e2e/permissions-reader-no-new-person.png' });
|
||||
});
|
||||
|
||||
test('person detail page does not show the edit button', async ({ page }) => {
|
||||
await page.goto('/persons');
|
||||
const firstPerson = page.locator('a[href^="/persons/"]:not([href="/persons/new"])').first();
|
||||
await firstPerson.click();
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
await expect(page.getByRole('button', { name: /Bearbeiten/i })).not.toBeVisible();
|
||||
await page.screenshot({ path: 'test-results/e2e/permissions-reader-no-edit.png' });
|
||||
});
|
||||
|
||||
test('navigating directly to /documents/new redirects away', async ({ page }) => {
|
||||
await page.goto('/documents/new');
|
||||
// Read-only user should not be able to access the new document form
|
||||
await expect(page).not.toHaveURL('/documents/new');
|
||||
await page.screenshot({ path: 'test-results/e2e/permissions-reader-no-new-doc-direct.png' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,6 +95,21 @@ test.describe('New person', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Person creation', () => {
|
||||
test('user fills in first and last name and lands on the new person detail page', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.goto('/persons/new');
|
||||
await page.getByLabel('Vorname').fill('E2E');
|
||||
await page.getByLabel('Nachname').fill('Testperson');
|
||||
await page.getByRole('button', { name: /Erstellen/i }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/persons\/[^/]+$/);
|
||||
await expect(page.getByText('E2E Testperson')).toBeVisible();
|
||||
await page.screenshot({ path: 'test-results/e2e/person-create.png' });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Person detail — sort toggle', () => {
|
||||
test('each section has its own sort toggle that works independently', async ({ page }) => {
|
||||
await page.goto('/persons');
|
||||
@@ -259,7 +274,10 @@ test.describe('Conversations — enhancements', () => {
|
||||
const originalReceiverId = url.searchParams.get('receiverId')!;
|
||||
|
||||
await page.getByTestId('conv-swap-btn').click();
|
||||
await page.waitForURL(/senderId=/);
|
||||
// Wait for the URL to reflect the swapped IDs (not just any URL with senderId=)
|
||||
await page.waitForURL(
|
||||
(url) => new URL(url).searchParams.get('senderId') === originalReceiverId
|
||||
);
|
||||
|
||||
const swappedUrl = new URL(page.url());
|
||||
expect(swappedUrl.searchParams.get('senderId')).toBe(originalReceiverId);
|
||||
|
||||
108
frontend/e2e/profile.spec.ts
Normal file
108
frontend/e2e/profile.spec.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Profile page E2E tests.
|
||||
*
|
||||
* Reads top-to-bottom as a single user journey:
|
||||
* the logged-in admin opens their profile, updates their display name,
|
||||
* tries a wrong password (sees an error), then successfully changes their
|
||||
* password and logs back in with the new one.
|
||||
*
|
||||
* The password change test restores the original password at the end so the
|
||||
* shared session remains valid for all subsequent test files.
|
||||
*/
|
||||
|
||||
test.describe('Profile page', () => {
|
||||
test('user opens their profile and sees the personal data and password sections', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.goto('/profile');
|
||||
await expect(page.getByRole('heading', { name: /Mein Profil/i })).toBeVisible();
|
||||
await expect(page.getByText('Persönliche Daten')).toBeVisible();
|
||||
await expect(page.getByText('Passwort ändern')).toBeVisible();
|
||||
await page.screenshot({ path: 'test-results/e2e/profile-view.png' });
|
||||
});
|
||||
|
||||
test('user saves updated first and last name and sees confirmation', async ({ page }) => {
|
||||
await page.goto('/profile');
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
|
||||
await page.locator('input[name="firstName"]').fill('E2E');
|
||||
await page.locator('input[name="lastName"]').fill('Admin');
|
||||
|
||||
// Two "Speichern" buttons exist — the first belongs to the profile form
|
||||
await page
|
||||
.locator('form[action*="updateProfile"]')
|
||||
.getByRole('button', { name: /Speichern/i })
|
||||
.click();
|
||||
|
||||
await expect(page.getByText('Gespeichert.')).toBeVisible();
|
||||
// Nav avatar shows the new initials derived from firstName + lastName
|
||||
await expect(page.locator('button[aria-haspopup="true"]')).toContainText('EA');
|
||||
await page.screenshot({ path: 'test-results/e2e/profile-save.png' });
|
||||
});
|
||||
|
||||
test('shows an error when the current password is wrong', async ({ page }) => {
|
||||
await page.goto('/profile');
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
|
||||
await page.locator('input[name="currentPassword"]').fill('definitely-wrong');
|
||||
await page.locator('input[name="newPassword"]').fill('NewPass123!');
|
||||
await page.locator('input[name="confirmPassword"]').fill('NewPass123!');
|
||||
|
||||
await page
|
||||
.locator('form[action*="changePassword"]')
|
||||
.getByRole('button', { name: /Speichern/i })
|
||||
.click();
|
||||
|
||||
await expect(page.getByText('Das aktuelle Passwort ist falsch.')).toBeVisible();
|
||||
await page.screenshot({ path: 'test-results/e2e/profile-wrong-password.png' });
|
||||
});
|
||||
|
||||
test('user changes their password and can log in with the new one', async ({ page }) => {
|
||||
await page.goto('/profile');
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
|
||||
// ── Step 1: change to a temporary password ─────────────────────────────
|
||||
await page.locator('input[name="currentPassword"]').fill('admin123');
|
||||
await page.locator('input[name="newPassword"]').fill('TempAdmin456!');
|
||||
await page.locator('input[name="confirmPassword"]').fill('TempAdmin456!');
|
||||
await page
|
||||
.locator('form[action*="changePassword"]')
|
||||
.getByRole('button', { name: /Speichern/i })
|
||||
.click();
|
||||
|
||||
await expect(page.getByText('Passwort erfolgreich geändert.')).toBeVisible();
|
||||
|
||||
// ── Step 2: navigate away — server session is invalidated ───────────────
|
||||
await page.goto('/');
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
|
||||
// ── Step 3: log in with the new password ───────────────────────────────
|
||||
await page.getByLabel('Benutzername').fill('admin');
|
||||
await page.getByLabel('Passwort').fill('TempAdmin456!');
|
||||
await page.getByRole('button', { name: 'Anmelden' }).click();
|
||||
await expect(page).toHaveURL('/');
|
||||
await page.screenshot({ path: 'test-results/e2e/profile-password-changed.png' });
|
||||
|
||||
// ── Step 4: restore the original password so subsequent tests still work ─
|
||||
await page.goto('/profile');
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
await page.locator('input[name="currentPassword"]').fill('TempAdmin456!');
|
||||
await page.locator('input[name="newPassword"]').fill('admin123');
|
||||
await page.locator('input[name="confirmPassword"]').fill('admin123');
|
||||
await page
|
||||
.locator('form[action*="changePassword"]')
|
||||
.getByRole('button', { name: /Speichern/i })
|
||||
.click();
|
||||
await expect(page.getByText('Passwort erfolgreich geändert.')).toBeVisible();
|
||||
|
||||
// ── Step 5: log back in with the restored password ─────────────────────
|
||||
await page.goto('/');
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
await page.getByLabel('Benutzername').fill('admin');
|
||||
await page.getByLabel('Passwort').fill('admin123');
|
||||
await page.getByRole('button', { name: 'Anmelden' }).click();
|
||||
await expect(page).toHaveURL('/');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user