From be26a2e1b3048e0f567534076a71aca7ae93943a Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 13 Jun 2026 22:53:30 +0200 Subject: [PATCH] test(e2e): thin curator event-editor journey (create, 403, 320px) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One critical create journey (fill form with precision RANGE → HTTP 200 on /zeitstrahl; the card assertion depends on #7), one security counterpart (logged-out → 403), and one 320px no-overflow guarantee. Intentionally thin — ci.yml does not run test:e2e today, so regression coverage lives in the component + server specs that DO run in CI. Written, not executed locally. Refs #781 Co-Authored-By: Claude Opus 4.8 --- frontend/e2e/zeitstrahl-event-editor.spec.ts | 65 ++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 frontend/e2e/zeitstrahl-event-editor.spec.ts diff --git a/frontend/e2e/zeitstrahl-event-editor.spec.ts b/frontend/e2e/zeitstrahl-event-editor.spec.ts new file mode 100644 index 00000000..12b8c2d6 --- /dev/null +++ b/frontend/e2e/zeitstrahl-event-editor.spec.ts @@ -0,0 +1,65 @@ +import { test, expect } from '@playwright/test'; + +/** + * Curator timeline event editor (#781) — intentionally thin. The component + + * server specs carry the real regression coverage (they run in CI's "Unit & + * Component Tests" job); ci.yml does NOT invoke test:e2e today, so this file + * runs only locally/manually against the full Docker Compose stack. + * + * Three checks: one critical create journey (→ HTTP 200 on /zeitstrahl; the full + * "sees the event card" assertion depends on #7), one security counterpart + * (logged-out → 403), and one 320px no-overflow guarantee for the 60+ author + * audience. + */ + +const stamp = () => new Date().toISOString().replace(/[^0-9]/g, ''); + +test.describe('Curator creates a timeline event', () => { + test('fills the create form with precision RANGE and lands on /zeitstrahl (HTTP 200)', async ({ + page + }) => { + await page.goto('/zeitstrahl/events/new'); + + await page.getByLabel(/Titel/i).fill(`E2E Ereignis ${stamp()}`); + await page.getByRole('radio', { name: /Historisch/i }).click(); + + // Date + RANGE end date via the shared German dd.mm.yyyy inputs. + await page.locator('#eventDate').fill('01.04.1925'); + await page.locator('#eventDatePrecision').selectOption('RANGE'); + await expect(page.getByLabel('Enddatum')).toBeVisible(); + await page.locator('#eventDateEnd').fill('01.05.1925'); + + // Submitting redirects to the resolved nav target (/zeitstrahl) — assert the + // route responds 200, not a DOM card (card rendering is #7's concern). + await Promise.all([ + page.waitForURL(/\/zeitstrahl$/), + page.getByRole('button', { name: 'Speichern' }).click() + ]); + const response = await page.goto('/zeitstrahl'); + expect(response?.status()).toBe(200); + }); +}); + +test.describe('Logged-out user is blocked from the curator route', () => { + test.use({ storageState: { cookies: [], origins: [] } }); + + test('navigating to /zeitstrahl/events/new is blocked with 403', async ({ page }) => { + await page.goto('/zeitstrahl/events/new'); + // The load guard throws 403 before any form renders. + await expect(page.getByLabel(/Titel/i)).not.toBeVisible({ timeout: 5000 }); + await expect(page.getByText(/403|Zugriff verweigert|Forbidden/i)).toBeVisible({ + timeout: 5000 + }); + }); +}); + +test.describe('Responsive — 60+ author audience', () => { + test('no horizontal overflow on the create form at 320px', async ({ page }) => { + await page.setViewportSize({ width: 320, height: 900 }); + await page.goto('/zeitstrahl/events/new'); + await expect(page.getByLabel(/Titel/i)).toBeVisible(); + + const scrollWidth = await page.evaluate(() => document.body.scrollWidth); + expect(scrollWidth).toBe(320); + }); +});