Mirrors the #780 layer-filter e2e for #827: switching Datum/Ereignis/Thema issues zero extra GET /api/timeline (REQ-002), the control stays overflow-free and ≥44px with full-word aria-labels at 320px (REQ-011), and a 320px axe pass holds in light and dark mode (REQ-010g). Local-only like the filter e2e (E2E is not yet in CI). Refs #827 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
124 lines
4.6 KiB
TypeScript
124 lines
4.6 KiB
TypeScript
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([]);
|
|
});
|
|
});
|