test(briefwechsel): visual spec seeds bilateral pair and asserts row structure

Extends the seeding pattern from the a11y spec: beforeAll creates two
persons + one document so the page renders the row layout. The
structural test now asserts the ConversationThumbnail tile AND the
DistributionBar are present — a regression that drops to the hero
or breaks the row wiring fails here instead of silently passing a
hero-state check.

Snapshot block stays gated on VISUAL=1 (baselines captured during
review against a seeded backend) so the structural coverage ships
immediately and the pixel-diff coverage ships once baselines land.

Refs #305
Fixes @saraholt blocker 2 from PR review

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-23 20:46:25 +02:00
committed by marcel
parent 05c1bf750a
commit 8cb179a8a1

View File

@@ -2,23 +2,69 @@ import { test, expect } from '@playwright/test';
// Visual + structural coverage for the new briefwechsel row layout.
//
// Snapshot assertions are gated on the VISUAL env flag because they require
// pre-captured baselines (see `playwright test --update-snapshots` to regenerate
// after intentional UI changes). CI only runs the structural assertions until
// the baseline set is populated and committed.
// Seeds a bilateral correspondence pair via the API (beforeAll) so the page
// reaches the row state. The structural test asserts that a
// ConversationThumbnail tile AND the DistributionBar render — regressions
// that silently drop to the hero or break the {#each} wiring fail here.
//
// Snapshot assertions are gated on the VISUAL env flag because they need
// pre-captured baselines (see `playwright test --update-snapshots` to
// regenerate after intentional UI changes). CI can opt in via VISUAL=1.
const VISUAL = process.env.VISUAL === '1';
let senderId: string;
let receiverId: string;
test.describe('Briefwechsel — thumbnail-row layout', () => {
test('structural row elements render for a bilateral pair', async ({ page }) => {
await page.goto('/briefwechsel');
// Hero is visible until a person is selected.
await expect(page.getByTestId('conv-hero')).toBeVisible();
test.beforeAll(async ({ request }) => {
const timestamp = Date.now();
const senderRes = await request.post('/api/persons', {
data: { firstName: 'Visual', 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: 'Visual', 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: 'Visual Test Brief',
documentDate: '1950-06-15',
senderId,
receiverIds: receiverId
}
});
if (!docRes.ok()) throw new Error(`Create document failed: ${docRes.status()}`);
});
// Visual regression — one snapshot per (viewport × theme). Snapshot tolerance
// stays generous (maxDiffPixels: 100) so that antialiasing jitter on text
// doesn't flip them on unrelated runs; genuine layout changes are still
// caught because of the thumbnail tile and distribution bar.
async function openBilateral(page: import('@playwright/test').Page) {
await page.goto(
`/briefwechsel?senderId=${encodeURIComponent(senderId)}&receiverId=${encodeURIComponent(receiverId)}`
);
await page.waitForSelector('[data-hydrated]');
}
test('renders a ConversationThumbnail tile and the DistributionBar', async ({ page }) => {
await openBilateral(page);
// Tile appears for the seeded document
await expect(page.locator('[data-testid="conv-thumb-tile"]').first()).toBeVisible();
// DistributionBar is present (role=img with a descriptive aria-label)
const bar = page.locator('[role="img"]');
await expect(bar).toBeVisible();
const label = (await bar.getAttribute('aria-label')) ?? '';
expect(label.length).toBeGreaterThan(0);
});
// Visual regression — one snapshot per (viewport × theme). Tolerance stays
// generous (maxDiffPixels: 100) so antialiasing jitter doesn't flip them on
// unrelated runs; genuine layout changes are still caught because the
// thumbnail tile and distribution bar dominate the frame.
test.describe('snapshots', () => {
test.skip(!VISUAL, 'VISUAL=1 required to compare baselines');
@@ -31,7 +77,7 @@ test.describe('Briefwechsel — thumbnail-row layout', () => {
test(`${viewport.name} / ${theme}`, async ({ page }) => {
await page.setViewportSize({ width: viewport.width, height: viewport.height });
await page.emulateMedia({ colorScheme: theme });
await page.goto('/briefwechsel');
await openBilateral(page);
await expect(page).toHaveScreenshot(`briefwechsel-${viewport.name}-${theme}.png`, {
maxDiffPixels: 100,
fullPage: true