diff --git a/frontend/e2e/geschichten.spec.ts b/frontend/e2e/geschichten.spec.ts new file mode 100644 index 00000000..14aad908 --- /dev/null +++ b/frontend/e2e/geschichten.spec.ts @@ -0,0 +1,78 @@ +import AxeBuilder from '@axe-core/playwright'; +import { expect, test } from '@playwright/test'; + +/** + * Minimal Geschichten coverage. The deeper a11y / visual-regression suite is + * tracked separately; this file proves the core writer + reader journey works + * end-to-end against the real stack. + * + * Pre-requisite: V59 has granted BLOG_WRITE to the Administrators group, so + * the seeded admin user can author. The auth.setup project handles login. + */ + +const stamp = () => new Date().toISOString().replace(/[^0-9]/g, ''); + +test.describe('Geschichten — writer + reader journey', () => { + test('admin can create a draft, publish it, and see it on the index', async ({ page }) => { + const title = `E2E story ${stamp()}`; + + // Land on the index — empty state or pre-existing demo data is fine + await page.goto('/geschichten'); + await page.waitForSelector('[data-hydrated]'); + await expect(page.getByRole('heading', { name: 'Geschichten', level: 1 })).toBeVisible(); + + // Click "Neue Geschichte" — visible because admin has BLOG_WRITE + await page.getByRole('link', { name: 'Neue Geschichte' }).click(); + await page.waitForURL('/geschichten/new'); + + // Fill in title — the body editor is Tiptap and harder to script reliably + await page.getByPlaceholder('Titel der Geschichte').fill(title); + + // Save as draft and verify we land on the detail page + await page.getByRole('button', { name: 'Entwurf speichern' }).click(); + await page.waitForURL(/\/geschichten\/[^/]+$/); + + // Capture the new id from the URL + const detailUrl = page.url(); + const id = detailUrl.split('/').pop(); + expect(id).toBeTruthy(); + + // Publish from the edit page + await page.getByRole('link', { name: 'Bearbeiten' }).click(); + await page.waitForURL(/\/edit$/); + await page.getByRole('button', { name: 'Veröffentlichen' }).click(); + await page.waitForURL(detailUrl); + + // Index now shows the published story + await page.goto('/geschichten'); + await expect(page.getByRole('link', { name: title })).toBeVisible(); + }); + + test('reader is taken to a story detail when clicking a card', async ({ page }) => { + await page.goto('/geschichten'); + await page.waitForSelector('[data-hydrated]'); + + // Use the first story link in the list (demo data exists; if not, the + // previous test seeded one). The link wraps the whole card. + const firstStory = page.locator('a[href^="/geschichten/"]').filter({ hasText: /.+/ }).first(); + await expect(firstStory).toBeVisible(); + await firstStory.click(); + + await page.waitForURL(/\/geschichten\/[^/]+$/); + await expect(page.locator('article')).toBeVisible(); + }); + + test('AxeBuilder finds no critical violations on the index', async ({ page }) => { + await page.goto('/geschichten'); + await page.waitForSelector('[data-hydrated]'); + const results = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']).analyze(); + + // Filter to non-deferred severity. We don't gate the whole PR on a clean + // AxeBuilder run yet — Sara's review tracks the broader a11y backlog — + // but any "serious" or "critical" finding from this scan would block merge. + const blocking = results.violations.filter( + (v) => v.impact === 'serious' || v.impact === 'critical' + ); + expect(blocking, JSON.stringify(blocking, null, 2)).toEqual([]); + }); +});