import AxeBuilder from '@axe-core/playwright'; import { test, expect, type APIRequestContext } from '@playwright/test'; /** * Global /zeitstrahl grouping toggle (#827). Runs against the real stack with the seeded admin * session (auth.setup). Covers REQ-002 (switching modes issues zero extra GET /api/timeline * requests — the regroup is client-side), REQ-011 (the control stays usable and overflow-free at * 320px with full-word aria-labels and ≥44px tap targets), and REQ-010g (a 320px axe pass over * the control in both light and dark mode). * * Per e2e/CLAUDE.md, E2E is not yet wired into CI — this gate runs locally for now, like the * #780 layer-filter spec it mirrors. */ 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 a loose letter and the grouping control is enabled. */ async function seedDatedLetter(request: APIRequestContext, isoDate: string, title: string) { const senderId = await createPerson(request, 'Group-Test', `Absender ${stamp()}`); const receiverId = await createPerson(request, 'Group-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 — grouping toggle (#827)', () => { test('switching grouping modes issues no extra timeline fetch (REQ-002)', async ({ page, request }) => { await seedDatedLetter(request, '1909-05-05', `E2E Group Brief ${stamp()}`); let timelineRequests = 0; page.on('request', (req) => { if (req.url().includes('/api/timeline')) timelineRequests++; }); await page.goto('/zeitstrahl'); await page.waitForSelector('[data-hydrated]'); await expect(page.getByTestId('grouping-control')).toBeVisible(); const afterLoad = timelineRequests; await page.locator('[data-value="event"]').click(); await page.locator('[data-value="thema"]').click(); await page.locator('[data-value="date"]').click(); // the regroup is a pure client-side transform — not one more GET /api/timeline expect(timelineRequests).toBe(afterLoad); }); test('the control stays overflow-free and operable at 320px (REQ-011)', async ({ page, request }) => { await seedDatedLetter(request, '1911-02-02', `E2E Group 320 ${stamp()}`); await page.setViewportSize({ width: 320, height: 800 }); await page.goto('/zeitstrahl'); await page.waitForSelector('[data-hydrated]'); const control = page.getByTestId('grouping-control'); await expect(control).toBeVisible(); // the control fits inside the 320px viewport — no horizontal overflow const box = await control.boundingBox(); expect(box).not.toBeNull(); expect(box!.x + box!.width).toBeLessThanOrEqual(321); for (const [value, fullWord] of [ ['date', 'Datum'], ['event', 'Ereignis'], ['thema', 'Thema'] ]) { const radio = page.locator(`[data-value="${value}"]`); const radioBox = await radio.boundingBox(); expect(radioBox!.height).toBeGreaterThanOrEqual(44); expect(radioBox!.width).toBeGreaterThanOrEqual(44); // the abbreviated segment still announces its full word expect(await radio.getAttribute('aria-label')).toBe(fullWord); } }); test('no wcag2a/wcag2aa violations on the grouping control at 320px (light + dark) (REQ-010g)', async ({ page, request }) => { await seedDatedLetter(request, '1915-06-15', `E2E Group A11y ${stamp()}`); await page.setViewportSize({ width: 320, height: 800 }); await page.goto('/zeitstrahl'); await page.waitForSelector('[data-hydrated]'); await expect(page.getByTestId('grouping-control')).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([]); }); });