feat(timeline): regroup /zeitstrahl by Ereignis or Thema (#827) #847
123
frontend/e2e/zeitstrahl-grouping.spec.ts
Normal file
123
frontend/e2e/zeitstrahl-grouping.spec.ts
Normal file
@@ -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([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user