Files
familienarchiv/frontend/e2e/persons.spec.ts
Marcel 731cdc75ab refactor(frontend): delete dead conversations/ route (#193)
Remove the old conversations page that was superseded by briefwechsel/.
No navigation link pointed to /conversations; it was unreachable through
the UI. Deletes 5 files, removes 14 orphaned i18n keys from de/en/es
message bundles, and removes E2E tests that navigated to /conversations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 12:43:40 +02:00

184 lines
7.4 KiB
TypeScript

import { test, expect } from '@playwright/test';
test.describe('Person list', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/persons');
});
test('renders the persons list page', async ({ page }) => {
await expect(page.getByRole('heading', { name: /Personen/i })).toBeVisible();
await expect(page.getByRole('link', { name: /Neue Person/i })).toBeVisible();
await page.screenshot({ path: 'test-results/e2e/persons-list.png' });
});
test('search filters the persons list', async ({ page }) => {
// Navigate directly with the query param — tests that search results are filtered
// correctly without depending on the debounced oninput → goto chain in CI.
await page.goto('/persons?q=zzz_unlikely_match');
await expect(page.getByText(/Keine Personen gefunden/i)).toBeVisible();
await page.screenshot({ path: 'test-results/e2e/persons-search-empty.png' });
});
test('clicking a person opens the detail page', async ({ page }) => {
const firstPerson = page.locator('a[href^="/persons/"]').first();
await firstPerson.click();
await expect(page).toHaveURL(/\/persons\/.+/);
await page.screenshot({ path: 'test-results/e2e/person-detail.png' });
});
});
test.describe('Person detail', () => {
test('shows the person name and their documents', async ({ page }) => {
await page.goto('/persons');
const firstPerson = page.locator('a[href^="/persons/"]').first();
await firstPerson.click();
// The detail page shows the person's name as the top-level heading
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
await page.screenshot({ path: 'test-results/e2e/person-detail-documents.png' });
});
test('can enter and cancel edit mode', async ({ page }) => {
await page.goto('/persons');
const firstPerson = page.locator('a[href^="/persons/"]').first();
await firstPerson.click();
// Click the edit button
const editBtn = page.getByRole('button', { name: /Bearbeiten/i });
if (await editBtn.isVisible()) {
await editBtn.click();
await expect(page.getByLabel('Vorname')).toBeVisible();
await page.screenshot({ path: 'test-results/e2e/person-edit-form.png' });
// Cancel
await page.getByRole('button', { name: /Abbrechen/i }).click();
await expect(page.getByLabel('Vorname')).not.toBeVisible();
}
});
test('birth and death year fields appear in edit mode and save correctly', 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]');
const editBtn = page.getByRole('button', { name: /Bearbeiten/i });
await editBtn.click();
await expect(page.getByLabel(/Geburtsjahr/i)).toBeVisible();
await expect(page.getByLabel(/Todesjahr/i)).toBeVisible();
await page.getByLabel(/Geburtsjahr/i).fill('1890');
await page.getByLabel(/Todesjahr/i).fill('1965');
await page.getByRole('button', { name: /Speichern/i }).click();
// After saving, the years should be shown in view mode
await expect(page.getByText('* 1890')).toBeVisible();
await expect(page.getByText('† 1965')).toBeVisible();
await page.screenshot({ path: 'test-results/e2e/person-birth-death-years.png' });
});
});
test.describe('New person', () => {
test('renders the new person form', async ({ page }) => {
await page.goto('/persons/new');
await expect(page.getByLabel('Vorname')).toBeVisible();
await expect(page.getByLabel('Nachname')).toBeVisible();
await expect(page.getByRole('button', { name: /Erstellen/i })).toBeVisible();
await page.screenshot({ path: 'test-results/e2e/person-new.png' });
});
test('shows a validation error when submitting with empty fields', async ({ page }) => {
await page.goto('/persons/new');
// HTML required attribute prevents submission without filling required fields
await page.getByRole('button', { name: /Erstellen/i }).click();
// The form should not have navigated away
await expect(page).toHaveURL('/persons/new');
});
});
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.getByRole('heading', { name: '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');
const firstPerson = page.locator('a[href^="/persons/"]').first();
await firstPerson.click();
await page.waitForSelector('[data-hydrated]');
// Find sort buttons — there may be 0, 1 or 2 depending on whether sections have >1 doc
const sortBtns = page.getByRole('button', { name: /Neueste zuerst|Älteste zuerst/i });
const btnCount = await sortBtns.count();
if (btnCount >= 1) {
const firstBtn = sortBtns.first();
await expect(firstBtn).toContainText('Neueste zuerst');
await firstBtn.click();
await expect(firstBtn).toContainText('Älteste zuerst');
await firstBtn.click();
await expect(firstBtn).toContainText('Neueste zuerst');
}
if (btnCount >= 2) {
// Second sort button toggles independently
const secondBtn = sortBtns.nth(1);
await expect(secondBtn).toContainText('Neueste zuerst');
await secondBtn.click();
await expect(secondBtn).toContainText('Älteste zuerst');
// First button should be unaffected
await expect(sortBtns.first()).toContainText('Neueste zuerst');
}
await page.screenshot({ path: 'test-results/e2e/person-sort-toggle.png' });
});
});
test.describe('Person detail — sent and received documents', () => {
test('shows both sent and received document sections', 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('heading', { name: /Gesendete Dokumente/i })).toBeVisible();
await expect(page.getByRole('heading', { name: /Empfangene Dokumente/i })).toBeVisible();
await page.screenshot({ path: 'test-results/e2e/person-sent-received.png' });
});
test('shows year range next to document count when documents have dates', async ({ page }) => {
// Navigate to the first person who has documents with dates
await page.goto('/persons');
const personLinks = page.locator('a[href^="/persons/"]:not([href="/persons/new"])');
const count = await personLinks.count();
for (let i = 0; i < count; i++) {
await page.goto('/persons');
await personLinks.nth(i).click();
await page.waitForSelector('[data-hydrated]');
// Check if either section heading has a year range (4 digits)
const sentHeading = page.getByRole('heading', { name: /Gesendete Dokumente/i }).locator('..');
const hasYearRange = await sentHeading.locator('span').filter({ hasText: /\d{4}/ }).count();
if (hasYearRange > 0) {
await expect(
sentHeading.locator('span').filter({ hasText: /\d{4}/ }).first()
).toBeVisible();
await page.screenshot({ path: 'test-results/e2e/person-year-range.png' });
return;
}
}
// If no person has dated documents, the test is a no-op (year range is optional)
});
});