feat(briefwechsel): remove the standalone Briefwechsel view and its tests
Delete the /briefwechsel route in full (page, server load, eight components and all co-located unit tests) and its end-to-end coverage (briefwechsel-rows.visual, briefwechsel-a11y, the bilateral-correspondence fixture, and the stale korrespondenz spec which targeted the route's former /korrespondenz path). The card link now deep-links into document search, so this view has no remaining inbound references. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,65 +0,0 @@
|
||||
import AxeBuilder from '@axe-core/playwright';
|
||||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
seedBilateralPair,
|
||||
cleanupBilateralPair,
|
||||
type BilateralPair
|
||||
} from './fixtures/bilateral-correspondence';
|
||||
|
||||
// Accessibility coverage for the briefwechsel thumbnail-row layout. Seeds
|
||||
// two persons + a bilateral document via the shared fixture 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 },
|
||||
{ name: 'tablet', width: 768, height: 1024 },
|
||||
{ name: 'desktop', width: 1280, height: 800 }
|
||||
] as const;
|
||||
|
||||
const THEMES = ['light', 'dark'] as const;
|
||||
|
||||
let pair: BilateralPair;
|
||||
|
||||
test.describe('Accessibility — /briefwechsel row layout', () => {
|
||||
test.beforeAll(async ({ request }) => {
|
||||
pair = await seedBilateralPair(request, 'A11y');
|
||||
});
|
||||
|
||||
test.afterAll(async ({ request }) => {
|
||||
await cleanupBilateralPair(request, pair);
|
||||
});
|
||||
|
||||
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?senderId=${encodeURIComponent(pair.senderId)}&receiverId=${encodeURIComponent(pair.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')
|
||||
.analyze();
|
||||
|
||||
if (results.violations.length > 0) {
|
||||
const summary = results.violations
|
||||
.map((v) => `[${v.impact}] ${v.id}: ${v.description} (${v.nodes.length} node(s))`)
|
||||
.join('\n');
|
||||
console.log(
|
||||
`\nAccessibility violations on briefwechsel (${vp.name}/${theme}):\n${summary}`
|
||||
);
|
||||
}
|
||||
|
||||
expect(results.violations).toEqual([]);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,79 +0,0 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
seedBilateralPair,
|
||||
cleanupBilateralPair,
|
||||
type BilateralPair
|
||||
} from './fixtures/bilateral-correspondence';
|
||||
|
||||
// Visual + structural coverage for the new briefwechsel row layout.
|
||||
//
|
||||
// Seeds a bilateral correspondence pair via the shared fixture 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 pair: BilateralPair;
|
||||
|
||||
test.describe('Briefwechsel — thumbnail-row layout', () => {
|
||||
test.beforeAll(async ({ request }) => {
|
||||
pair = await seedBilateralPair(request, 'Visual');
|
||||
});
|
||||
|
||||
test.afterAll(async ({ request }) => {
|
||||
await cleanupBilateralPair(request, pair);
|
||||
});
|
||||
|
||||
async function openBilateral(page: import('@playwright/test').Page) {
|
||||
await page.goto(
|
||||
`/briefwechsel?senderId=${encodeURIComponent(pair.senderId)}&receiverId=${encodeURIComponent(pair.receiverId)}`
|
||||
);
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
// Parity with the a11y spec: fail loudly if we ever end up on the hero
|
||||
// instead of the row layout.
|
||||
await expect(page.getByTestId('conv-person-bar')).toBeVisible();
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
for (const viewport of [
|
||||
{ name: 'mobile', width: 375, height: 812 },
|
||||
{ name: 'tablet', width: 768, height: 1024 },
|
||||
{ name: 'desktop', width: 1280, height: 800 }
|
||||
] as const) {
|
||||
for (const theme of ['light', 'dark'] as const) {
|
||||
test(`${viewport.name} / ${theme}`, async ({ page }) => {
|
||||
await page.setViewportSize({ width: viewport.width, height: viewport.height });
|
||||
await page.emulateMedia({ colorScheme: theme });
|
||||
await openBilateral(page);
|
||||
await expect(page).toHaveScreenshot(`briefwechsel-${viewport.name}-${theme}.png`, {
|
||||
maxDiffPixels: 100,
|
||||
fullPage: true
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,62 +0,0 @@
|
||||
import type { APIRequestContext } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Test fixture for the briefwechsel row layout.
|
||||
*
|
||||
* Creates two persons and one document with sender/receiver between them so
|
||||
* that `/briefwechsel?senderId=X&receiverId=Y` navigates straight to the row
|
||||
* state (not the hero). Each seed uses a `Date.now()`-suffixed last name so
|
||||
* parallel runs and reruns never collide.
|
||||
*
|
||||
* The backend does not expose a person-delete endpoint, so only the document
|
||||
* is cleaned up in {@link cleanupBilateralPair}. The two timestamped persons
|
||||
* remain in the DB — acceptable for the test environment, and the unique
|
||||
* suffix means they cannot conflict with later runs.
|
||||
*/
|
||||
|
||||
export interface BilateralPair {
|
||||
senderId: string;
|
||||
receiverId: string;
|
||||
documentId: string;
|
||||
}
|
||||
|
||||
export async function seedBilateralPair(
|
||||
request: APIRequestContext,
|
||||
prefix: string
|
||||
): Promise<BilateralPair> {
|
||||
const timestamp = Date.now();
|
||||
|
||||
const senderRes = await request.post('/api/persons', {
|
||||
data: { firstName: prefix, lastName: `Sender-${timestamp}` }
|
||||
});
|
||||
if (!senderRes.ok()) throw new Error(`Create sender failed: ${senderRes.status()}`);
|
||||
const senderId = (await senderRes.json()).id as string;
|
||||
|
||||
const receiverRes = await request.post('/api/persons', {
|
||||
data: { firstName: prefix, lastName: `Receiver-${timestamp}` }
|
||||
});
|
||||
if (!receiverRes.ok()) throw new Error(`Create receiver failed: ${receiverRes.status()}`);
|
||||
const receiverId = (await receiverRes.json()).id as string;
|
||||
|
||||
const docRes = await request.post('/api/documents', {
|
||||
multipart: {
|
||||
title: `${prefix} Brief`,
|
||||
documentDate: '1950-06-15',
|
||||
senderId,
|
||||
receiverIds: receiverId
|
||||
}
|
||||
});
|
||||
if (!docRes.ok()) throw new Error(`Create document failed: ${docRes.status()}`);
|
||||
const documentId = (await docRes.json()).id as string;
|
||||
|
||||
return { senderId, receiverId, documentId };
|
||||
}
|
||||
|
||||
export async function cleanupBilateralPair(
|
||||
request: APIRequestContext,
|
||||
pair: BilateralPair
|
||||
): Promise<void> {
|
||||
// Only the document is purged — the backend has no person-delete endpoint
|
||||
// and the timestamped last names make orphaned person rows safe to leave.
|
||||
await request.delete(`/api/documents/${pair.documentId}`);
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import AxeBuilder from '@axe-core/playwright';
|
||||
|
||||
function buildAxe(page: Parameters<typeof AxeBuilder>[0]['page']) {
|
||||
return new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']);
|
||||
}
|
||||
|
||||
test.describe('Korrespondenz – empty state', () => {
|
||||
test('shows the search heading when no person is selected', async ({ page }) => {
|
||||
await page.goto('/korrespondenz');
|
||||
await expect(page.getByText(/Korrespondenz durchsuchen/i)).toBeVisible();
|
||||
const a11y = await buildAxe(page).analyze();
|
||||
expect(a11y.violations, JSON.stringify(a11y.violations, null, 2)).toHaveLength(0);
|
||||
await page.screenshot({ path: 'test-results/e2e/korrespondenz-empty.png' });
|
||||
});
|
||||
|
||||
test('nav link goes to /korrespondenz', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
// Click the nav link (desktop text or mobile icon)
|
||||
const navLink = page.getByRole('link', { name: /Korrespondenz/i }).first();
|
||||
await navLink.click();
|
||||
await expect(page).toHaveURL(/\/korrespondenz/);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Korrespondenz – single-person mode', () => {
|
||||
test('shows hint bar and documents when navigated with senderId', async ({ page }) => {
|
||||
// Get a real person ID from the persons list
|
||||
await page.goto('/persons');
|
||||
const firstPersonLink = page.locator('a[href^="/persons/"]').first();
|
||||
await firstPersonLink.click();
|
||||
await page.waitForURL(/\/persons\/.+/);
|
||||
|
||||
// Extract the person ID from the URL
|
||||
const personId = page.url().split('/persons/')[1].split('?')[0];
|
||||
|
||||
// Navigate to korrespondenz in single-person mode
|
||||
await page.goto(`/korrespondenz?senderId=${personId}`);
|
||||
|
||||
// Hint bar should be visible
|
||||
await expect(page.getByText(/Alle Briefe von/i)).toBeVisible();
|
||||
|
||||
// Filter controls should be active (not dimmed)
|
||||
const filterStrip = page.locator('[aria-disabled="false"]').first();
|
||||
await expect(filterStrip).toBeAttached();
|
||||
|
||||
const a11y = await buildAxe(page).analyze();
|
||||
expect(a11y.violations, JSON.stringify(a11y.violations, null, 2)).toHaveLength(0);
|
||||
await page.screenshot({ path: 'test-results/e2e/korrespondenz-single-person.png' });
|
||||
});
|
||||
|
||||
test('sort toggle changes URL direction param', async ({ page }) => {
|
||||
await page.goto('/persons');
|
||||
const firstPersonLink = page.locator('a[href^="/persons/"]').first();
|
||||
await firstPersonLink.click();
|
||||
await page.waitForURL(/\/persons\/.+/);
|
||||
const personId = page.url().split('/persons/')[1].split('?')[0];
|
||||
|
||||
await page.goto(`/korrespondenz?senderId=${personId}&dir=DESC`);
|
||||
await page.getByTestId('conv-sort-btn').click();
|
||||
|
||||
await expect(page).toHaveURL(/dir=ASC/);
|
||||
await page.screenshot({ path: 'test-results/e2e/korrespondenz-sort-asc.png' });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Korrespondenz – bilateral mode', () => {
|
||||
test('shows asymmetry bar when both persons have shared documents', async ({ page }) => {
|
||||
// Navigate to a person then follow a co-correspondent suggestion if available
|
||||
await page.goto('/persons');
|
||||
const firstPersonLink = page.locator('a[href^="/persons/"]').first();
|
||||
await firstPersonLink.click();
|
||||
await page.waitForURL(/\/persons\/.+/);
|
||||
const senderId = page.url().split('/persons/')[1].split('?')[0];
|
||||
|
||||
// Try to find a co-correspondent link from the person detail page
|
||||
const corrLink = page
|
||||
.locator('a[href*="/korrespondenz?senderId="][href*="receiverId="]')
|
||||
.first();
|
||||
if (await corrLink.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
await corrLink.click();
|
||||
await page.waitForURL(/\/korrespondenz\?.*receiverId=/);
|
||||
|
||||
// Hint bar should NOT be shown in bilateral mode
|
||||
await expect(page.getByText(/Alle Briefe von/i)).not.toBeVisible();
|
||||
|
||||
const a11y = await buildAxe(page).analyze();
|
||||
expect(a11y.violations, JSON.stringify(a11y.violations, null, 2)).toHaveLength(0);
|
||||
await page.screenshot({ path: 'test-results/e2e/korrespondenz-bilateral.png' });
|
||||
} else {
|
||||
// E2E seed must include bilateral correspondents — a missing link is a test failure.
|
||||
throw new Error(
|
||||
`No bilateral correspondent links found for person ${senderId}. Ensure the E2E seed contains at least one bilateral correspondence pair.`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('swap button swaps sender and receiver in URL', async ({ page }) => {
|
||||
await page.goto('/persons');
|
||||
const firstPersonLink = page.locator('a[href^="/persons/"]').first();
|
||||
await firstPersonLink.click();
|
||||
await page.waitForURL(/\/persons\/.+/);
|
||||
const senderId = page.url().split('/persons/')[1].split('?')[0];
|
||||
|
||||
const corrLink = page
|
||||
.locator('a[href*="/korrespondenz?senderId="][href*="receiverId="]')
|
||||
.first();
|
||||
if (await corrLink.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
const href = await corrLink.getAttribute('href');
|
||||
await corrLink.click();
|
||||
await page.waitForURL(/\/korrespondenz\?.*receiverId=/);
|
||||
|
||||
// Extract original receiverId from the href
|
||||
const url = new URL(href!, 'http://x');
|
||||
const originalReceiverId = url.searchParams.get('receiverId')!;
|
||||
|
||||
// Click swap
|
||||
await page.getByTestId('conv-swap-btn').click();
|
||||
|
||||
// After swap the former receiver is now senderId
|
||||
await expect(page).toHaveURL(new RegExp(`senderId=${originalReceiverId}`));
|
||||
await page.screenshot({ path: 'test-results/e2e/korrespondenz-swapped.png' });
|
||||
} else {
|
||||
test.skip(true, `No bilateral correspondent links found for person ${senderId}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user