test(briefwechsel): a11y spec seeds bilateral pair and axes the row layout

The previous version navigated to /briefwechsel with no params, which
renders the hero state — axe-core scanned the hero, not the new
ThumbnailRow / ConversationThumbnail / DistributionBar. This commit
seeds two persons + one document via the API in beforeAll, then
drives the URL with ?senderId=X&receiverId=Y so each of the
36 test runs (3 viewports × 2 themes × 2 assertions) actually scans
the intended DOM. Also asserts that conv-person-bar is visible first,
so a regression that drops the page back to hero fails explicitly
rather than silently passing an empty sweep.

Refs #305
Fixes @saraholt blocker 1 from PR review

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-23 20:44:58 +02:00
parent 3b3b551d84
commit d1a69e0b43

View File

@@ -2,9 +2,9 @@ import AxeBuilder from '@axe-core/playwright';
import { test, expect } from '@playwright/test';
// Accessibility coverage specifically for the briefwechsel thumbnail-row
// layout. Runs axe-core (wcag2a + wcag2aa) across three viewports and two
// color schemes so color-contrast regressions on the meta text or the
// distribution bar are caught independently of the site-wide sweep.
// layout. Seeds two persons + a bilateral document via the API so the page
// reaches the results state (not the hero), then runs axe-core
// (wcag2a + wcag2aa) across three viewports and two color schemes.
const VIEWPORTS = [
{ name: 'mobile', width: 375, height: 812 },
@@ -14,15 +14,49 @@ const VIEWPORTS = [
const THEMES = ['light', 'dark'] as const;
let senderId: string;
let receiverId: string;
test.describe('Accessibility — /briefwechsel row layout', () => {
test.beforeAll(async ({ request }) => {
const timestamp = Date.now();
const senderRes = await request.post('/api/persons', {
data: { firstName: 'A11y', lastName: `Sender-${timestamp}` }
});
if (!senderRes.ok()) throw new Error(`Create sender failed: ${senderRes.status()}`);
senderId = (await senderRes.json()).id;
const receiverRes = await request.post('/api/persons', {
data: { firstName: 'A11y', lastName: `Receiver-${timestamp}` }
});
if (!receiverRes.ok()) throw new Error(`Create receiver failed: ${receiverRes.status()}`);
receiverId = (await receiverRes.json()).id;
const docRes = await request.post('/api/documents', {
multipart: {
title: 'A11y Test Brief',
documentDate: '1950-06-15',
senderId,
receiverIds: receiverId
}
});
if (!docRes.ok()) throw new Error(`Create document failed: ${docRes.status()}`);
});
for (const vp of VIEWPORTS) {
for (const theme of THEMES) {
test(`${vp.name} / ${theme} has no wcag2a/wcag2aa violations`, async ({ page }) => {
await page.setViewportSize({ width: vp.width, height: vp.height });
await page.emulateMedia({ colorScheme: theme });
await page.goto('/briefwechsel');
await page.goto(
`/briefwechsel?senderId=${encodeURIComponent(senderId)}&receiverId=${encodeURIComponent(receiverId)}`
);
await page.waitForSelector('[data-hydrated]');
// Assert we actually reached the row layout, not the hero — otherwise
// the axe sweep silently scans the wrong DOM.
await expect(page.getByTestId('conv-person-bar')).toBeVisible();
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.include('main')