Two backlog trees are complete: `docs/project/backlog/` (75 files, every WP done) and `docs/project/refactor-backlog-setup/` (the arc before it). Move both under `docs/project/archive/` with `git mv`, so history stays intact through `git log --follow`. `SHOWCASE-ROADMAP.md` moves with them, because it points at the now-archived backlog README. Add `docs/project/archive/README.md`. It states that these trees are historical and names the two directories that are still live. Repoint every inbound reference named in RD-30's Files table: CLAUDE.md, the root README, both backend READMEs, `LetterHtml.cs`, `a11y.mdx`, the `document-feature` and `new-ssp` skills, and the readable-codebase PLAN, README, and RD-19 ticket. Fix two upward-relative links inside the moved WP files (WP-68, WP-69) that gained a directory level and would otherwise break. Repoint `.prettierignore`'s two agent-prompt exclusions to their new path, so prettier keeps leaving those files' exact wording alone. Mark RD-30 done and check off its acceptance criteria; flip its README row to done. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
8.3 KiB
WP-19 — Playwright e2e smoke
Status: done (pending commit) Phase: 5 — productie-volwassenheid
Why
There is no end-to-end test anywhere in the repo — no Playwright/Cypress config, no
e2e/ directory. axe-playwright is already a dependency (used by
test-storybook:ci to run axe against Storybook, .storybook/test-runner.ts), but
nothing drives the actual running app through a real browser. The GREEN gate proves
every unit and component-in-isolation, never a real user flow through the FE+backend
wired together — the thing a demo/reference app should be able to prove first.
Read first
README.md"Run it" + "See every data state (scenario toggle)" — the flows to coverdocker-compose.yml(the two-service dev topology e2e can run against).storybook/test-runner.ts(existing Playwright-adjacent config in the repo, for browser-launch precedent, though it drives Storybook not the app)src/app/shared/infrastructure/scenario.interceptor.ts(the?scenario=toggle — reuse it for the error-path test instead of mocking the network).github/workflows/ci.yml(thestorybook-a11yjob'splaywright install --with-deps chromiumstep — same install pattern for a new e2e job)
Decisions (pre-made, don't relitigate)
- Playwright, not Cypress.
axe-playwrightis already a dependency and the repo already has one Playwright-based CI job (storybook-a11y); adding Cypress would be a second, redundant browser-automation toolchain. - Smoke-level coverage only: one happy-path flow end to end, one degraded-path
flow via
?scenario=. This is not a full e2e suite — it proves the seam works, it doesn't replace component/unit tests. - Run against the real backend, not a mock server — the point is proving FE+BE integration, which is exactly what unit tests (mocked adapters) don't cover.
- Faked auth (
digid.adapter.ts) is used as-is: e2e logs in with any 9-digit BSN, no special e2e auth bypass.
Files
- New
playwright.config.tsat repo root —baseURLfrom an env var (defaulthttp://localhost:4200),webServerconfig that can optionally bootng serve(skip ifCIalready starts the app in a prior step — see Steps). - New
e2e/smoke.spec.ts— the happy path. - New
e2e/error-state.spec.ts— the?scenario=errorpath. package.json— add"e2e": "playwright test"script;@playwright/testdevDependency..github/workflows/ci.yml— new jobe2e, steps: checkout, setup-node, setup-dotnet,npm ci,npx playwright install --with-deps chromium,npm run e2e.timeout-minutes: 15per the hardened workflow convention already inci.yml. Playwright's ownwebServerconfig (playwright.config.ts) starts + waits-for-ready + tears down both the backend andng serveinside that one step — see the "backgrounding servers" deviation below for why this replaced the originally-planned&/wait-onapproach.
Steps
- Install
@playwright/test; scaffoldplaywright.config.tswith a singlechromiumproject (matchtest-storybook:ci's browser choice). e2e/smoke.spec.ts: navigate to/login, submit a BSN, land on/dashboard, assert real dashboard content renders (not a loading/error state), navigate into one wizard (herregistratie or registratie change-request), fill the minimum required fields, submit, assert a success state.e2e/error-state.spec.ts: navigate to/dashboard?scenario=error, assert the error alert + "Opnieuw proberen" button render (<app-async>'s error slot), click retry, assert it re-fetches (scenario is per-request so a retry without the query param would succeed — confirm the interceptor's actual behavior first and assert accordingly).- Wire the CI job; verify it's independent of (doesn't block or get blocked by) the
existing jobs — add to
concurrency/timeout-minutesconventions already inci.yml. - Document
npm run e2einREADME.md's command list.
Acceptance criteria
npm run e2epasses locally againstdocker compose upornpm start+dotnet runrun manually.- CI job
e2eis green and runs on every PR alongside the existing jobs. - The happy-path spec exercises a real wizard submit against the real backend (not mocked) and asserts on the resulting UI state.
- The error-path spec exercises
<app-async>'s error slot + retry via the real?scenario=errortoggle, not a mocked HTTP response.
Deviation from the original plan
- Backgrounding servers as separate CI steps doesn't work. The Files section originally
planned starting the backend and
ng servewith&in one step, thenwait-on-ing both ports in a later step. A process backgrounded with&in one GitHub/Gitea Actionsrun:step is killed when that step's shell exits — so the laterwait-onstep hung forever on servers that were already gone (the 2-hour e2e hang). Fixed by moving server lifecycle into Playwright itself:playwright.config.ts'swebServerarray starts both processes, waits for each to be ready, runs the suite, and tears them down — all inside the singlenpm run e2eprocess/step, so it's self-contained locally and in CI alike. - Found and fixed a real bug while writing the error-path spec:
AsyncComponent's built-inretry()only calls.reload()on a[resource]input — every real page (dashboard,registration-detail,aanvraag-detail,brief) feeds<app-async>via[data](a store's combinedRemoteData), so clicking "Opnieuw proberen" was a silent no-op everywhere except the showcase teaching page. Added aretryClickedoutput that fires regardless of feed mode, and wired the two dashboard instances (BigProfileStore.reloadProfile()/reloadAantekeningen()) since that's what this WP's spec exercises. Not fixed:registration-detail,aanvraag-detail, andbriefpages still have the same latent no-op retry — same "found via testing, fixing the whole surface is beyond this WP" call as WP-17's dashboard CSS finding. Flagging here so it isn't lost. - Confirmed
currentScenario()readswindow.location.searchfresh on every call — the error-path spec's retry assertion had to change from "counts a browser network request" (the scenario interceptor never reaches the real transport; it substitutesthrowErrorin the rxjs pipe beforenext(req)) to "observes a real Loading→Failure reload cycle viaaria-busy". The Steps section's literal suggestion ("assert it re-fetches... via a network tab") didn't hold; adapted per the WP's own Risks note to verify actual interceptor behavior first. - Verified the suite isn't a no-op per the Verification section: temporarily broke
diplomaOptions'value: d.id(appended-x), watchedsmoke.spec.tsfail on the now-missing#diploma-d1selector, reverted. - The registratie wizard's minimum path needed an actual file upload (
identiteitis always required forregistratieregardless of diploma choice, perDocumentRules.CategoriesFor— onlydiploma/taalvaardigheidare answer-gated). Picked the first DUO diploma (non-English,Engelstalig: false) specifically because it carries zero policy questions, keeping the happy path to one upload. - CIBG-styled radios hide the native
<input>behind its<label>— Playwright's.check()on the input times out fighting the label for pointer events; the specs click thelabel[for=...]instead (also more representative of a real click).
Verification
npm run e2e locally; then push a branch and confirm the new e2e CI job appears
and passes. Cross-check that a deliberately broken flow (e.g. temporarily rename a
required form field) fails the e2e spec, proving it isn't a no-op.
Out of scope
Full e2e coverage of every wizard/flow; visual regression testing; cross-browser matrix (chromium only, matching the existing a11y job); load/performance testing.
Risks
The ?scenario= interceptor is dev-only (isDevMode() gated, per
app.config.ts) — confirm the e2e target build runs in dev mode (it does via ng serve/npm start; a production ng build would need the toggle unavailable,
which is correct and should be asserted, not worked around). Backend
in-memory stores mean e2e runs against a fresh seed each restart — don't assert on
data that a previous test run could have mutated; restart the backend per CI run.