diff --git a/frontend/e2e/zeitstrahl-grouping.spec.ts b/frontend/e2e/zeitstrahl-grouping.spec.ts new file mode 100644 index 00000000..d49de9cf --- /dev/null +++ b/frontend/e2e/zeitstrahl-grouping.spec.ts @@ -0,0 +1,123 @@ +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([]); + }); +});