Files
atomic-design-poc/docs/project/archive/backlog/WP-19-e2e-smoke.md
T
ehoandClaude Opus 5 12f17d9d73 docs: archive the finished backlogs (RD-30)
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>
2026-09-08 23:00:38 +02:00

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 cover
  • docker-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 (the storybook-a11y job's playwright install --with-deps chromium step — same install pattern for a new e2e job)

Decisions (pre-made, don't relitigate)

  • Playwright, not Cypress. axe-playwright is 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.ts at repo root — baseURL from an env var (default http://localhost:4200), webServer config that can optionally boot ng serve (skip if CI already 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=error path.
  • package.json — add "e2e": "playwright test" script; @playwright/test devDependency.
  • .github/workflows/ci.yml — new job e2e, steps: checkout, setup-node, setup-dotnet, npm ci, npx playwright install --with-deps chromium, npm run e2e. timeout-minutes: 15 per the hardened workflow convention already in ci.yml. Playwright's own webServer config (playwright.config.ts) starts + waits-for-ready + tears down both the backend and ng serve inside that one step — see the "backgrounding servers" deviation below for why this replaced the originally-planned &/wait-on approach.

Steps

  1. Install @playwright/test; scaffold playwright.config.ts with a single chromium project (match test-storybook:ci's browser choice).
  2. 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.
  3. 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).
  4. Wire the CI job; verify it's independent of (doesn't block or get blocked by) the existing jobs — add to concurrency/timeout-minutes conventions already in ci.yml.
  5. Document npm run e2e in README.md's command list.

Acceptance criteria

  • npm run e2e passes locally against docker compose up or npm start + dotnet run run manually.
  • CI job e2e is 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=error toggle, 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 serve with & in one step, then wait-on-ing both ports in a later step. A process backgrounded with & in one GitHub/Gitea Actions run: step is killed when that step's shell exits — so the later wait-on step hung forever on servers that were already gone (the 2-hour e2e hang). Fixed by moving server lifecycle into Playwright itself: playwright.config.ts's webServer array starts both processes, waits for each to be ready, runs the suite, and tears them down — all inside the single npm run e2e process/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-in retry() only calls .reload() on a [resource] input — every real page (dashboard, registration-detail, aanvraag-detail, brief) feeds <app-async> via [data] (a store's combined RemoteData), so clicking "Opnieuw proberen" was a silent no-op everywhere except the showcase teaching page. Added a retryClicked output 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, and brief pages 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() reads window.location.search fresh 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 substitutes throwError in the rxjs pipe before next(req)) to "observes a real Loading→Failure reload cycle via aria-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), watched smoke.spec.ts fail on the now-missing #diploma-d1 selector, reverted.
  • The registratie wizard's minimum path needed an actual file upload (identiteit is always required for registratie regardless of diploma choice, per DocumentRules.CategoriesFor — only diploma/taalvaardigheid are 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 the label[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.