diff --git a/frontend/e2e/zeitstrahl.spec.ts b/frontend/e2e/zeitstrahl.spec.ts new file mode 100644 index 00000000..18c633e0 --- /dev/null +++ b/frontend/e2e/zeitstrahl.spec.ts @@ -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
) 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
', 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
    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
      in the DOM. + await expect(page.getByRole('main').locator('ol')).toHaveCount(1); + + const scrollWidth = await page.evaluate(() => document.body.scrollWidth); + expect(scrollWidth).toBe(320); + }); +});