Files
familienarchiv/frontend/e2e/persons.spec.ts
Marcel fb08eb30a4
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Successful in 1m55s
CI / Backend Unit Tests (pull_request) Successful in 2m9s
CI / E2E Tests (pull_request) Successful in 18m8s
feat(persons): add sort toggle to person document list (issue #24)
Extracted sortDocumentsByDate utility with full Vitest coverage (6 tests),
wired it into the person detail page with a DESC/ASC toggle button, and
added an E2E smoke test for the toggle interaction.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 21:24:46 +01:00

118 lines
4.8 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.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('sort toggle changes the button label when person has documents', async ({ page }) => {
await page.goto('/persons');
const firstPerson = page.locator('a[href^="/persons/"]').first();
await firstPerson.click();
await page.waitForSelector('[data-hydrated]');
const sortBtn = page.getByRole('button', { name: /Neueste zuerst|Älteste zuerst/i });
if (await sortBtn.isVisible()) {
await expect(sortBtn).toContainText('Neueste zuerst');
await sortBtn.click();
await expect(sortBtn).toContainText('Älteste zuerst');
await sortBtn.click();
await expect(sortBtn).toContainText('Neueste zuerst');
await page.screenshot({ path: 'test-results/e2e/person-sort-toggle.png' });
}
});
});
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' });
});
});