diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd24370..17bc767 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,6 +60,27 @@ jobs: - run: dotnet format backend/BigRegister.slnx --verify-no-changes - run: dotnet test backend/BigRegister.slnx + e2e: + # Smoke-level Playwright run against the REAL FE+backend (WP-19) — a fresh + # backend process per run, so in-memory state from a prior run never leaks in. + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + - run: npm ci + - run: npx playwright install --with-deps chromium + - run: dotnet run --project backend/src/BigRegister.Api --urls http://localhost:5000 & + - run: npx ng serve --proxy-config proxy.conf.json & + - run: npx wait-on http://localhost:5000/swagger http://localhost:4200 + - run: npm run e2e + codeql: # Static analysis (SAST) for both sides; results appear under the Security tab. runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 746800d..0958d99 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,9 @@ Thumbs.db *storybook.log storybook-static + +# Playwright e2e +/test-results +/playwright-report +/blob-report +/playwright/.cache diff --git a/README.md b/README.md index 5269cfb..21e3e5e 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ cd backend && dotnet run --project src/BigRegister.Api # API → http://localh npm run storybook # component library, organized by atomic layer npm run gen:api # regenerate the typed API client from the backend OpenAPI doc +npm run e2e # Playwright smoke tests against the running app + backend (both must be up) ``` Flow: **Login → Dashboard → Mijn gegevens (wijziging) → Herregistratie → Intake**. diff --git a/docs/backlog/README.md b/docs/backlog/README.md index d6fa9ad..ccf5a42 100644 --- a/docs/backlog/README.md +++ b/docs/backlog/README.md @@ -33,6 +33,10 @@ npm run test-storybook:ci Backend stays untouched throughout (frontend-only backlog); `cd backend && dotnet test` only needs re-running if a WP unexpectedly touches `backend/`. +From WP-19 onward, `npm run e2e` is part of CI (its own job) but NOT part of the local +GREEN one-liner above — it needs the real backend + `npm start` already running (see +WP-19's own file), so it's a separate manual/CI step, not chained into the others. + ## Order Gates land before the work they cover; each lint rule lands in the same WP as the fixes @@ -58,7 +62,7 @@ for its existing violations, so every WP ends green. | [WP-16](WP-16-component-a11y.md) | Component a11y: description wiring + alert role | 4 · a11y | done | | [WP-17](WP-17-app-a11y.md) | App-level a11y: route focus, template lint, WCAG checklist | 4 · a11y | done | | [WP-18](WP-18-abac-capability-spine.md) | ABAC capability spine (Principal + capabilities, phase P1) | 5 · productie-volwassenheid | done | -| [WP-19](WP-19-e2e-smoke.md) | Playwright e2e smoke | 5 · productie-volwassenheid | todo | +| [WP-19](WP-19-e2e-smoke.md) | Playwright e2e smoke | 5 · productie-volwassenheid | done | | [WP-20](WP-20-second-locale.md) | Second locale proof | 5 · productie-volwassenheid | todo | | [WP-21](WP-21-resilience-seams.md) | Resilience seams (correlation-id, idempotency, retry) | 5 · productie-volwassenheid | todo | | [WP-22](WP-22-durable-persistence.md) | Durable persistence (optional tier) | 5 · productie-volwassenheid | todo | diff --git a/docs/backlog/WP-19-e2e-smoke.md b/docs/backlog/WP-19-e2e-smoke.md index 72413ca..1c63cb2 100644 --- a/docs/backlog/WP-19-e2e-smoke.md +++ b/docs/backlog/WP-19-e2e-smoke.md @@ -1,6 +1,6 @@ # WP-19 — Playwright e2e smoke -Status: todo +Status: done (pending commit) Phase: 5 — productie-volwassenheid ## Why @@ -70,14 +70,46 @@ serve` backgrounded), wait-on both ports, `npm run e2e`. `timeout-minutes: 15` ## Acceptance criteria -- [ ] `npm run e2e` passes locally against `docker compose up` or `npm start` + +- [x] `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 +- [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. -- [ ] The error-path spec exercises ``'s error slot + retry via the real +- [x] The error-path spec exercises ``'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 `` + 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 `` behind its `