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:
93
frontend/e2e/zeitstrahl-filter.spec.ts
Normal file
93
frontend/e2e/zeitstrahl-filter.spec.ts
Normal 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([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user