import { expect, test } from '@playwright/test'; import { Actors, loginAs } from './support/actors'; // One flow through both Brief v2 axes on the real FE+backend (WP-19 conventions): // content (drafter composes via the besluit panel → approver approves → sends) and // appearance (admin edits + publishes the org template, drafter's canvas reflects it). // Preview assertions are content-type/body-level (text/html + watermark marker), not // pixel, per WP-28's decision. // // The backend persists to SQLite (WP-22) and is shared across runs: `/brief/reset` // covers the letter, but org templates have no reset endpoint, so this test restores // the org-template draft it edits (step 8) and never asserts an absolute version // number — only that it increased by exactly one. test('drafter composes → approver sends; admin republishes appearance', async ({ page }) => { await loginAs(page, Actors.zorgverlener); await expect(page).toHaveURL(/\/dashboard$/); // --- Compose (drafter) --- await page.goto('/brief?role=drafter'); await page.getByRole('button', { name: 'Opnieuw beginnen (demo)' }).click(); await expect(page.getByRole('heading', { name: 'Aanvraag herregistratie' })).toBeVisible(); const submitButton = page.getByRole('button', { name: 'Indienen ter beoordeling' }); await expect(submitButton).toBeDisabled(); await page.locator('label[for="besluit-positief"]').click(); await expect(page.getByText(/standaardtekst\(en\) toegevoegd/)).toBeVisible(); await expect(submitButton).toBeEnabled(); // Wait for the debounced draft save before navigating away. await expect(page.getByText('Concept opgeslagen')).toBeVisible({ timeout: 10_000 }); // --- Preview: draft is watermarked --- await page.getByRole('button', { name: 'Voorbeeld', exact: true }).click(); const [draftPreview] = await Promise.all([ page.waitForResponse((r) => r.url().includes('/api/v1/brief/preview'), { timeout: 10_000 }), page.getByRole('button', { name: 'Openen als document (PDF)' }).click(), ]); expect(draftPreview.headers()['content-type']).toContain('text/html'); // The `.preview-watermark` CSS rule ships in every preview (draft or sent) — only the // "VOORBEELD" marker div (LetterHtml.Render's `watermark` param) is actually conditional. expect(await draftPreview.text()).toContain('>VOORBEELD<'); await page.getByRole('button', { name: 'Sluiten' }).click(); // --- Submit → approve → send (role change = full navigation, per WP-33 stickiness) --- await submitButton.click(); await expect(page.getByText('De brief wacht op beoordeling door een collega.')).toBeVisible(); await page.goto('/brief?role=approver'); await page.getByRole('button', { name: 'Goedkeuren' }).click({ timeout: 10_000 }); await page.getByRole('button', { name: 'Versturen' }).click({ timeout: 10_000 }); await expect(page.getByText('De brief is verzonden.')).toBeVisible(); // --- Preview: sent letter serves its frozen, unwatermarked archive --- // Sent = !canEdit → app-letter-composer, whose "Voorbeeld" click goes straight to // store.previewLetter() (fetch + window.open), unlike the drafter's behandel-scherm // dialog above where "Voorbeeld" only opens a local modal and a second click inside it // triggers the fetch. const [sentPreview] = await Promise.all([ page.waitForResponse((r) => r.url().includes('/api/v1/brief/preview'), { timeout: 10_000 }), page.getByRole('button', { name: 'Voorbeeld', exact: true }).click(), ]); expect(sentPreview.headers()['content-type']).toContain('text/html'); expect(await sentPreview.text()).not.toContain('>VOORBEELD<'); // --- Admin republishes the appearance --- await page.goto('/brief/huisstijl?role=admin'); const orgNameInput = page.getByLabel('Organisatienaam'); const before = await page.getByText(/Gepubliceerde versie:/).textContent(); const beforeVersion = Number(before?.match(/\d+/)?.[0]); const unique = `BIG-register E2E ${Date.now()}`; await orgNameInput.fill(unique); await expect(page.getByText('Concept opgeslagen')).toBeVisible({ timeout: 10_000 }); await page.getByRole('button', { name: 'Publiceren' }).click(); await expect(page.getByText(/Dit raakt \d+ nog niet verzonden brieven/)).toBeVisible(); await page.getByRole('button', { name: 'Bevestigen' }).click(); await expect(page.getByText(`Gepubliceerde versie: ${beforeVersion + 1}`)).toBeVisible({ timeout: 10_000, }); // --- Drafter's canvas reflects the new appearance on a fresh letter --- await page.goto('/brief?role=drafter'); await page.getByRole('button', { name: 'Opnieuw beginnen (demo)' }).click(); await page.getByRole('button', { name: 'Voorbeeld', exact: true }).click(); await expect(page.locator('dialog')).toContainText(unique); await page.getByRole('button', { name: 'Sluiten' }).click(); // --- Restore: put the org template's appearance back the way this test found it --- await page.goto('/brief/huisstijl?role=admin'); await page .locator('.history-row', { // beforeVersion is a number this test itself captured earlier, never external/attacker input // (detect-non-literal-regexp false positive — the reported check_id doesn't match what // `nosemgrep` compares against for this rule, confirmed by trial; bare form suppresses it). hasText: new RegExp(`Versie ${beforeVersion} ·`), // nosemgrep }) .getByRole('button', { name: 'Terugzetten in concept' }) .click(); await expect(orgNameInput).not.toHaveValue(unique); await page.getByRole('button', { name: 'Publiceren' }).click(); await page.getByRole('button', { name: 'Bevestigen' }).click(); await expect(page.getByText(`Gepubliceerde versie: ${beforeVersion + 2}`)).toBeVisible({ timeout: 10_000, }); });