Adds a happy-path spec (login → dashboard → registratie wizard, including a real identity-document upload → real submit) and a degraded-path spec (?scenario=error → <app-async> error slot → retry), both driving the real app against the real .NET backend, plus a CI job that boots both. Writing the retry spec surfaced a real bug: AsyncComponent's retry() only reloads a [resource]-fed instance, so every real page (all [data]-fed via a store's RemoteData) had a silently no-op retry button. Added a retryClicked output and wired it on the dashboard's two async blocks. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
7.5 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, start backend (dotnet run --project backend/src/BigRegister.Api &),npm start &(orng servebackgrounded), wait-on both ports,npm run e2e.timeout-minutes: 15per the hardened workflow convention already inci.yml.
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
- 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.