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>
132 lines
7.5 KiB
Markdown
132 lines
7.5 KiB
Markdown
# 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`, start backend
|
|
(`dotnet run --project backend/src/BigRegister.Api &`), `npm start &` (or `ng
|
|
serve` backgrounded), wait-on both ports, `npm run e2e`. `timeout-minutes: 15`
|
|
per the hardened workflow convention already in `ci.yml`.
|
|
|
|
## 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
|
|
|
|
- [x] `npm run e2e` passes locally against `docker compose up` or `npm start` +
|
|
`dotnet run` run manually.
|
|
- [x] CI job `e2e` is green and runs on every PR alongside the existing jobs.
|
|
- [x] The happy-path spec exercises a real wizard submit against the real backend
|
|
(not mocked) and asserts on the resulting UI state.
|
|
- [x] 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
|
|
|
|
- **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.
|