import AxeBuilder from '@axe-core/playwright'; import { test, expect, type APIRequestContext } from '@playwright/test'; /** * Global /zeitstrahl layer filter (#780). Runs against the real stack with the * seeded admin session (auth.setup). Covers the primary journey (hide the * Letters layer → letter cards vanish + the trigger reports one active filter → * reset restores everything) and a 375px axe pass with the collapsible open in * both light and dark mode. * * #779 (the /zeitstrahl route) is merged, so this spec is NOT skipped. Per * e2e/CLAUDE.md, E2E is not yet wired into CI — this axe gate runs locally only * for now. */ const stamp = () => new Date().toISOString().replace(/[^0-9]/g, ''); async function createPerson(request: APIRequestContext, firstName: string, lastName: string) { const res = await request.post('/api/persons', { data: { personType: 'PERSON', firstName, lastName } }); if (!res.ok()) throw new Error(`create person failed: ${res.status()}`); return (await res.json()).id as string; } /** Seeds one dated letter so the timeline has content (and a LetterCard to hide). */ async function seedDatedLetter(request: APIRequestContext, isoDate: string, title: string) { const senderId = await createPerson(request, 'Filter-Test', `Absender ${stamp()}`); const receiverId = await createPerson(request, 'Filter-Test', `Empfaenger ${stamp()}`); const createRes = await request.post('/api/documents', { multipart: { title } }); if (!createRes.ok()) throw new Error(`create document failed: ${createRes.status()}`); const docId = (await createRes.json()).id as string; const put = await request.put(`/api/documents/${docId}`, { multipart: { title, documentDate: isoDate, metaDatePrecision: 'DAY', senderId, receiverIds: receiverId } }); if (!put.ok()) throw new Error(`update document failed: ${put.status()}`); } test.describe('Zeitstrahl — layer filter (#780)', () => { test('hiding the Letters layer removes letter cards and reports the active count; reset restores', async ({ page, request }) => { // A sparse year keeps the seeded letter an individual card (not a dense strip). const title = `E2E Filter Brief ${stamp()}`; await seedDatedLetter(request, '1903-03-03', title); await page.goto('/zeitstrahl'); await page.waitForSelector('[data-hydrated]'); await expect(page.getByText(title)).toBeVisible(); await page.getByTestId('timeline-filter-trigger').click(); await page.getByTestId('timeline-filter-letters').click(); await expect(page.getByText(title)).toHaveCount(0); await expect(page.getByTestId('timeline-filter-trigger')).toContainText('1 aktiv'); await page.getByTestId('timeline-filter-reset').click(); await expect(page.getByText(title)).toBeVisible(); }); test('no wcag2a/wcag2aa violations at 375px with the filter bar open (light + dark)', async ({ page, request }) => { await seedDatedLetter(request, '1915-06-15', `E2E Filter A11y ${stamp()}`); await page.setViewportSize({ width: 375, height: 800 }); await page.goto('/zeitstrahl'); await page.waitForSelector('[data-hydrated]'); // Open the collapsible so axe scans the toggles, not just the trigger. await page.getByTestId('timeline-filter-trigger').click(); await expect(page.getByTestId('timeline-filter-personal')).toBeVisible(); const scan = () => new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']).analyze(); const light = await scan(); expect(light.violations, JSON.stringify(light.violations, null, 2)).toEqual([]); await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'dark')); const dark = await scan(); expect(dark.violations, JSON.stringify(dark.violations, null, 2)).toEqual([]); }); });