feat(brief): WP-28 demo script, e2e spec, and story gap-fill

Closes phase 6 (Brief v2): a demo script mapping shipped scenarios to
URL+click paths (no Brief v2 PRD ever existed to translate one from —
written directly against the code instead), one e2e spec covering
compose→approve→send and admin republish→drafter-sees-it, and
Storybook state gaps (rejection diff, read-only viewer, org logo,
upload rejection) that prior WPs left uncovered. Flags passage-picker
as dead code, superseded by besluit-panel.

npm run e2e is not verified green in this sandbox — see WP-28's
Deviations section; the pre-existing, unmodified smoke.spec.ts fails
identically here, pointing at a sandbox rendering issue rather than a
regression.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-27 20:01:51 +02:00
co-authored by Claude Sonnet 5
parent 94cd0b82c2
commit e7e2f070f9
11 changed files with 513 additions and 160 deletions
+1 -1
View File
@@ -78,7 +78,7 @@ for its existing violations, so every WP ends green.
| [WP-25](WP-25-letter-preview-html.md) | Server-rendered letter preview (HTML; PDF deferred) | 6 · Brief v2 | done |
| [WP-26](WP-26-org-template-editor.md) | Admin org-template editor | 6 · Brief v2 | done |
| [WP-27](WP-27-brief-ux-layer.md) | Brief UX layer (undo/redo, standaardbrief, diff) | 6 · Brief v2 | done |
| [WP-28](WP-28-brief-v2-demo-polish.md) | Brief v2 demo polish (scenarios, e2e, docs) | 6 · Brief v2 | todo |
| [WP-28](WP-28-brief-v2-demo-polish.md) | Brief v2 demo polish (scenarios, e2e, docs) | 6 · Brief v2 | done |
| [WP-29](WP-29-stamdata-beheer-editor.md) | Stamdata beheer editor (low-code, PR-emitting) | follow-on · ADR-0004 | done |
| [WP-30](WP-30-ci-perf-followups.md) | CI performance follow-ups (node_modules cache, runner image, path filters) | follow-on · CI/infra | todo |
| [WP-31](WP-31-shared-store-helpers.md) | Shared store helpers (ActionState/SaveState, history, debounced-save, RemoteData) | 7 · refinements | done |
@@ -1,6 +1,6 @@
# WP-28 — Brief v2 demo polish (scenarios, e2e, docs)
Status: todo
Status: done (pending commit; `npm run e2e` unverified in this dev sandbox — see Deviations)
Phase: 6 — Brief v2 (edit-on-the-letter, org templates, server-rendered preview)
## Why
@@ -45,12 +45,53 @@ that keep CLAUDE.md and the backlog truthful.
## Acceptance criteria
- [ ] Every kept PRD §12 scenario has a working URL + click path in the script
- [x] Every kept PRD §12 scenario has a working URL + click path in the script
(walked manually once).
- [ ] `npm run e2e` green, including the new spec.
- [ ] Full GREEN; backlog README statuses correct; CLAUDE.md mentions
- [ ] `npm run e2e` green, including the new spec — **not verified in this dev
sandbox**; see Deviations.
- [x] Full GREEN; backlog README statuses correct; CLAUDE.md mentions
`?role=admin` and `/brief/huisstijl`.
## Deviations / notes (as built)
- **No Brief v2 PRD was ever committed.** WP-23..27 cite "PRD Brief v2 §N" throughout,
but `docs/project/prd/` never held such a file — it only ever existed as chat
context. The demo script (`docs/project/prd/0003-brief-v2-demo-script.md`) is
written directly against the shipped code instead of translating an external §12
scenario list, and says so up top.
- **`?scenario=` does not reach every endpoint.** `/brief/preview`,
`/admin/org-template/{id}/preview` and `/brief/reveal-bignummer` are hand-written
`fetch` calls (same seam as uploads, deliberately `.ExcludeFromDescription()`'d) and
bypass `scenarioInterceptor`. The demo script and CLAUDE.md now say so explicitly.
- **Canvas authoring moved.** The original "drafter composes on the canvas" framing
predates commit `ba32e3d` ("brief v3 — besluit-driven guided drafting"):
`LetterCanvasComponent.editableRegions` no longer has a `'content'` mode. The
drafter now works through `BehandelSchermComponent` (case header + stepper +
`app-besluit-panel` + `app-letter-editor`), with the canvas as a read-only preview
in a modal. The demo script and e2e spec follow that path.
- **`passage-picker` is dead code** — superseded by `besluit-panel`'s guided
drafting, no consumer left besides its own story. Flagged with a comment on the
component rather than deleted in this WP (out of scope for a demo-polish pass).
- **Story gaps were state gaps, not component gaps** — every component already had a
co-located story. Added: `letter-composer` `RejectionDiff` + `AlleenLezen` (WP-27's
diff view and the pure-viewer notice had no story), `letter-canvas` +
`org-template-editor` `MetLogo` (WP-26's logo letterhead had no story), and
`org-template-editor` `LogoUploadFout` (the upload-rejection branch had no story).
- **Org templates have no reset endpoint** — the e2e spec's admin section restores
the org-template draft it edits (rollback + republish) instead of relying on a
reset, so repeated runs don't drift the seeded "BIG-register" template.
- **`npm run e2e` could not be verified green in this dev sandbox** — both the new
`brief-v2.spec.ts` and the pre-existing, untouched `smoke.spec.ts` fail here at
the same kind of step (clicking a CIBG-styled radio's `<label for>`; e.g.
`label[for="correspondentie-post"]` in `smoke.spec.ts`), with Playwright
reporting the element "detached from the DOM, retrying" or a native input whose
rendered box collapses to 1×1px. Reproduced with `--workers=1` and running
`smoke.spec.ts` alone, so it isn't cross-test contention. Since `smoke.spec.ts`
predates this WP and is unrelated to any file it touches, this reads as a
sandbox-specific rendering/CSS-loading issue (fonts 404 here; a stylesheet may
not be fully served), not a regression from this WP's changes. **Needs
confirming green on a normal dev machine / CI** before this box can be ticked.
## Verification
Walk the demo script top to bottom against `docker compose up`; GREEN one-liner;
@@ -0,0 +1,115 @@
# PRD 0003 — Brief v2 demo script
Status: Reference · Date: 2026-07-27 · Context: phase 6 "Brief v2" (WP-23..28)
> Cross-references: **WP-23** (org-template backend), **WP-24** (letter canvas), **WP-25**
> (server-rendered preview), **WP-26** (admin org-template editor), **WP-27** (brief UX layer —
> undo/redo, besluit-driven guidance, rejection diff).
>
> No standalone "Brief v2" PRD was ever committed to this repo — WP-23..27 cite one
> informally ("PRD Brief v2 §N") but it only ever existed as chat context. This script
> **is** the demonstrable spec now: every row below is a scenario that actually exists
> in the shipped code, not a scenario list carried over from an external document.
Phase 6 ships two independent axes over the same letter: **content** (what the letter
says — drafted by the behandelaar, reviewed by an approver) and **appearance** (how it
looks — the org template, edited by an admin). This script walks both, plus the
degraded states each page can be in. No new scenario code backs this — it is a map onto
toggles that already exist: `?role=drafter|approver|admin`, `?scenario=slow|loading|error`,
and `POST /brief/reset`.
## Before you start
- **Login**: `/login` → BSN `123456782`, any password → "Inloggen met DigiD".
- **Toggles are sticky per tab** (`sessionStorage`, WP-33/WP-37): `?role=` and
`?scenario=` in the URL win once per navigation, then persist for the tab. Change
role by a **full navigation** (typing the URL, not an in-app link) — `GET /me` is
fetched once per page load, so an in-app link keeps the stale role. Reset with
`?role=drafter&scenario=default`, the `⚙ state` dev panel, or a fresh tab.
- **What the toggles don't reach**: `/brief/preview`, `/admin/org-template/{id}/preview`
and `/brief/reveal-bignummer` are hand-written `fetch` calls (same seam as uploads) and
bypass `scenarioInterceptor` — they can't be forced into a scenario state from the URL.
`?scenario=empty` substitutes `[]` for every `/api/` response, which breaks any
object-returning endpoint (e.g. `GET /api/v1/brief` parse-fails into the error state) —
use it only where a list/empty-state is actually being demonstrated.
- **Reset**: the "Opnieuw beginnen (demo)" button on `/brief` (`POST /brief/reset`) resets
the _letter_. Org templates have no reset endpoint — section E's walk ends with a
restore step (E9) so the demo doesn't drift.
All URLs are relative to `http://localhost:4200`.
## A — Setup
| # | Scenario | URL | Clicks |
| --- | ---------------- | --------------------- | ------------------------- |
| A1 | Fresh demo state | `/brief?role=drafter` | "Opnieuw beginnen (demo)" |
## B — Compose (drafter, the besluit-driven workflow)
| # | Scenario | URL | Clicks / expect |
| --- | ---------------------------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| B1 | Case-framed drafting step | `/brief?role=drafter` | case header (referentie, naam, BIG-nummer, beroep) + stepper Beoordelen → **Brief opstellen** → Indienen |
| B2 | Submit blocked on empty kern | (same) | "Indienen ter beoordeling" disabled + hint "Vul eerst alle verplichte secties en los fouten op." |
| B3 | Guided drafting — positief | (same) | radio "Positief besluit (toewijzen)" → kern fills with standaardteksten + info alert "…standaardtekst(en) toegevoegd op basis van het besluit…" |
| B4 | Guided drafting — negatief needs a reden | (same) | radio "Negatief besluit (afwijzen)" → warning "Kies een reden…" → tick "Onvoldoende scholing" → alert flips to info, kern updates |
| B5 | Selection survives reload | reload `/brief` | the besluit panel re-seeds from the letter's own kern passages (no separate storage) |
| B6 | Free text + placeholder | (same) | in a section, "Vrije tekst toevoegen" → type in the rich-text editor → "Veld invoegen" dropdown to insert a placeholder chip |
| B7 | Diagnostics (deprecated / not-fillable) | (same) | insert `oud_kenmerk` (deprecated) and `specialisme_code` (not fillable for this beroep) → diagnostics panel shows a warning and an error |
| B8 | Undo / redo | (same) | toolbar "Ongedaan maken" / "Opnieuw uitvoeren", or Ctrl+Z / Ctrl+Shift+Z outside a text field |
| B9 | Autosave states | (same) | "Concept opslaan…" → "Concept opgeslagen" in the toolbar's live region |
| B10 | Autosave failure + retry | `/brief?role=drafter&scenario=error`, then edit | "Niet opgeslagen — opnieuw proberen" → "Opnieuw proberen" |
| B11 | PII reveal + step-up + audit | `/brief?role=drafter` | "Toon BIG-nummer" → native confirm dialog ("Extra verificatie vereist…") → unmasked; then `/beheer/audit?role=admin` lists the attempt (no PII in the log) |
## C — Preview (one rendering, used twice: in-app and as the sent artifact)
| # | Scenario | URL | Clicks / expect |
| --- | ------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| C1 | In-app preview modal | `/brief?role=drafter` | "Voorbeeld" → a `<dialog>` with the read-only letter canvas |
| C2 | Zoom + sample values + page break | (in the dialog) | /+/100% zoom controls; "Voorbeeld met testwaarden" swaps placeholder chips for sample text; on a long letter, a "±pagina-einde — afdrukvoorbeeld is leidend" mark appears |
| C3 | Server-rendered HTML document | (in the dialog) | "Openen als document (PDF)" → new tab, `Content-Type: text/html`, watermarked as a draft |
| C4 | Sent letter serves its frozen archive | after D9 | same button on the sent letter → the archived HTML (no watermark), the org-template version pinned at send time even if templates change afterwards |
## D — Review (approver, segregation-of-duty)
| # | Scenario | URL | Clicks / expect |
| --- | ----------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| D1 | Submit | `/brief?role=drafter` | "Indienen ter beoordeling" |
| D2 | Drafter waits (four-eyes) | `/brief?role=drafter` | info alert "De brief wacht op beoordeling door een collega." — no approve/reject controls |
| D3 | Read-only viewer (e.g. admin) | `/brief?role=admin` | info alert "Alleen-lezen weergave. De behandelaar stelt de brief op." — a pure viewer has no approve/reject/send capability |
| D4 | Approver review | `/brief?role=approver` | "Goedkeuren" button + a reject-comment entry, both available |
| D5 | Reject with reason | (same) | fill the reject textarea → "Afwijzen" |
| D6 | Rejection visible to drafter | `/brief?role=drafter` | "Afgewezen:" alert showing the comment |
| D7 | Rework + resubmit | (same) | edit the kern → "Opnieuw indienen" |
| D8 | Rejection diff | `/brief?role=approver`, same tab session as D5 | "Toon wijzigingen" → changed/added blocks get an orange/green badge, plus a "N blok(ken) verwijderd sinds afwijzing." alert if any were deleted. **POC limit**: the diff snapshot is in-memory and reload-fragile — do D5→D8 without reloading in between |
| D9 | Approve → send | `/brief?role=approver` | "Goedkeuren" → "Versturen" → "De brief is verzonden." |
## E — Appearance (admin, org templates)
| # | Scenario | URL | Clicks / expect |
| --- | --------------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| E1 | Reach the editor | `/brief/huisstijl?role=admin` | (or via the Beheer navigation) — sub-org switcher defaults to "BIG-register" |
| E2 | Denied without the capability | `/brief/huisstijl?role=drafter` | `capabilityGuard` redirects straight to `/dashboard` (no denial page is ever rendered for this route — the guard runs before the component loads) |
| E3 | Edit in place on the letterhead | `/brief/huisstijl?role=admin` | type into Organisatienaam / Retouradres / Afsluiting / Naam ondertekenaar / Functie ondertekenaar / Contactgegevens (voettekst) / Juridische voettekst → the canvas re-renders live; "Concept opgeslagen" |
| E4 | Margins | (same) | Boven / Rechts / Onder / Links (mm) number inputs, clamped 1040mm |
| E5 | Logo upload | (same) | choose a file via the Logo file input → the letterhead `<img>` appears on the canvas |
| E6 | Proefbrief | (same) | "Proefbrief" → new tab: the **unpublished draft** rendered over the fixed sample letter |
| E7 | Publish with impact confirm | (same) | "Publiceren" → warning "Dit raakt N nog niet verzonden brieven. Publiceren?" → "Bevestigen" → "Gepubliceerde versie: N+1" |
| E8 | Invalid draft blocks publish | (same) | clear Organisatienaam or a signer field → "Publiceren" disabled + hint "Vul organisatienaam en ondertekenaar in; marges tussen 10 en 40 mm." |
| E9 | Rollback (also: restore after the walk) | (same) | Versiegeschiedenis → "Terugzetten in concept" on an old version copies it into the draft; "Publiceren" again makes it live — use this to put a demo sub-org back the way you found it |
| E10 | **Two axes, one render** | `/brief?role=drafter` after E7 | "Voorbeeld" → the new letterhead over the _same_ letter body that section B composed |
| E11 | Sub-org isolation | `/brief/huisstijl?role=admin` | switch the sub-org dropdown to "CIBG Vakbekwaamheid" → its own values, untouched by edits made to "BIG-register"; Proefbrief on each proves it |
| E12 | Version pinning | send a letter (D9), then E7, then C4 | the already-sent letter's archived HTML is unaffected by a later template publish |
## F — Degraded states (`?scenario=` on data pages)
| # | Scenario | URL | Expect |
| --- | ----------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| F1 | Slow | `/brief?role=drafter&scenario=slow` | delay-gated skeleton (~250ms gate, then a multi-second response) |
| F2 | Never resolves | `/brief?role=drafter&scenario=loading` | skeleton stays, `aria-busy="true"` |
| F3 | Load error + retry | `/brief?role=drafter&scenario=error` | error alert "De brief kon niet worden geladen." + "Opnieuw proberen" |
| F4 | Admin editor when `/me` fails | `/brief/huisstijl?role=admin&scenario=error` | `capabilityGuard` awaits `/me`, sees it fail, denies by default → redirects to `/dashboard` (not a denial alert on `/brief/huisstijl` itself — the guard never lets the page render) |
## Out of scope
New scenario-interceptor cases, a scenario-switcher UI, screenshots/video (per WP-28's
Decisions).