test(timeline): add /zeitstrahl E2E spec

Nav-link smoke + timeline-in-<main> (empty-or-populated), and the 320px
no-overflow guarantee on a timeline seeded with 25+char correspondent names
(REQ-005). Runs against the real stack via the seeded admin session.

Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-13 19:54:07 +02:00
parent 6f32299255
commit 852fb71ee7

View File

@@ -0,0 +1,84 @@
import { test, expect, type APIRequestContext } from '@playwright/test';
/**
* Global /zeitstrahl timeline (#779). Runs against the real stack with the
* seeded admin session (auth.setup). Covers the primary journey (nav → page,
* timeline inside <main>) and the 320px no-overflow guarantee on a populated
* timeline seeded with 25+char correspondent names (REQ-005).
*/
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 with long sender/receiver names so it lands on the timeline. */
async function seedDatedLetter(request: APIRequestContext) {
const senderId = await createPerson(
request,
'Friedrich-Wilhelm',
`Maximilian von Habsburg ${stamp()}`
);
const receiverId = await createPerson(
request,
'Maria-Magdalena',
`Hohenzollern-Sigmaringen ${stamp()}`
);
const createRes = await request.post('/api/documents', {
multipart: { title: `E2E Zeitstrahl Brief ${stamp()}` }
});
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: `E2E Zeitstrahl Brief ${stamp()}`,
documentDate: '1915-06-15',
metaDatePrecision: 'DAY',
senderId,
receiverIds: receiverId
}
});
if (!put.ok()) throw new Error(`update document failed: ${put.status()}`);
}
test.describe('Zeitstrahl — global timeline (#779)', () => {
test('nav link opens /zeitstrahl and the timeline lives in <main>', async ({ page }) => {
await page.goto('/');
await page.getByRole('navigation').getByRole('link', { name: 'Zeitstrahl' }).first().click();
await expect(page).toHaveURL(/\/zeitstrahl$/);
await expect(page.getByRole('heading', { level: 1, name: 'Zeitstrahl' })).toBeVisible();
// The main landmark contains either the populated <ol> or the empty state.
const main = page.getByRole('main');
const ol = main.locator('ol');
const empty = main.getByText('Noch keine Ereignisse.');
await expect(async () => {
const populated = (await ol.count()) > 0;
const isEmpty = await empty.isVisible().catch(() => false);
expect(populated || isEmpty).toBe(true);
}).toPass();
});
test('no horizontal overflow at 320px with long correspondent names (REQ-005)', async ({
page,
request
}) => {
await seedDatedLetter(request);
await page.setViewportSize({ width: 320, height: 900 });
await page.goto('/zeitstrahl');
// Populated: the seeded letter puts the timeline <ol> in the DOM.
await expect(page.getByRole('main').locator('ol')).toHaveCount(1);
const scrollWidth = await page.evaluate(() => document.body.scrollWidth);
expect(scrollWidth).toBe(320);
});
});