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 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) }); }); test.describe('Person detail — conversations link', () => { test('co-correspondent chips link to conversations pre-filled with both persons', async ({ page }) => { await page.goto('/persons'); const firstLink = page.locator('a[href^="/persons/"]:not([href="/persons/new"])').first(); const href = await firstLink.getAttribute('href'); const personId = href!.split('/persons/')[1]; await firstLink.click(); await page.waitForSelector('[data-hydrated]'); // Co-correspondent chips link to /conversations?senderId=X&receiverId=Y const chip = page.locator(`a[href^="/conversations?senderId=${personId}&receiverId="]`).first(); if ((await chip.count()) > 0) { const chipHref = await chip.getAttribute('href'); expect(chipHref).toMatch(/\/conversations\?senderId=.+&receiverId=.+/); } }); }); test.describe('Conversations', () => { test('shows the empty state when no persons are selected', async ({ page }) => { await page.goto('/conversations'); await expect(page.getByText(/Wählen Sie zwei Personen aus/i)).toBeVisible(); await page.screenshot({ path: 'test-results/e2e/conversations-empty.png' }); }); test('nav link is active on the conversations page', async ({ page }) => { await page.goto('/conversations'); const navLink = page.getByRole('link', { name: 'Konversationen' }); await expect(navLink).toHaveClass(/text-brand-navy/); }); test('sort toggle changes the button label', async ({ page }) => { await page.goto('/conversations'); await page.waitForSelector('[data-hydrated]'); const btn = page.getByRole('button', { name: /Sortierung/i }); await expect(btn).toContainText('Neueste zuerst'); await btn.click(); await expect(page).toHaveURL(/dir=ASC/); await expect(btn).toContainText('Älteste zuerst'); await page.screenshot({ path: 'test-results/e2e/conversations-sort.png' }); }); });