test(timeline): add e2e journey + 375px axe for the layer filter

Playwright spec for /zeitstrahl: the primary journey (hide Letters → letter
cards vanish + trigger reports "1 aktiv" → reset restores) and a 375px axe pass
with the collapsible open in light and dark mode. Not skipped — #779 ships the
route. E2E is not wired into CI, so this runs locally only for now.

Refs #780
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-14 20:38:43 +02:00
committed by marcel
parent 33aff36867
commit 21b1b3b835

View File

@@ -0,0 +1,93 @@
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([]);
});
});