feat(fp): WP-19 — Playwright e2e smoke against the real FE+backend
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>
This commit is contained in:
21
.github/workflows/ci.yml
vendored
21
.github/workflows/ci.yml
vendored
@@ -60,6 +60,27 @@ jobs:
|
|||||||
- run: dotnet format backend/BigRegister.slnx --verify-no-changes
|
- run: dotnet format backend/BigRegister.slnx --verify-no-changes
|
||||||
- run: dotnet test backend/BigRegister.slnx
|
- 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:
|
codeql:
|
||||||
# Static analysis (SAST) for both sides; results appear under the Security tab.
|
# Static analysis (SAST) for both sides; results appear under the Security tab.
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -45,3 +45,9 @@ Thumbs.db
|
|||||||
|
|
||||||
*storybook.log
|
*storybook.log
|
||||||
storybook-static
|
storybook-static
|
||||||
|
|
||||||
|
# Playwright e2e
|
||||||
|
/test-results
|
||||||
|
/playwright-report
|
||||||
|
/blob-report
|
||||||
|
/playwright/.cache
|
||||||
|
|||||||
@@ -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 storybook # component library, organized by atomic layer
|
||||||
npm run gen:api # regenerate the typed API client from the backend OpenAPI doc
|
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**.
|
Flow: **Login → Dashboard → Mijn gegevens (wijziging) → Herregistratie → Intake**.
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ npm run test-storybook:ci
|
|||||||
Backend stays untouched throughout (frontend-only backlog); `cd backend && dotnet test`
|
Backend stays untouched throughout (frontend-only backlog); `cd backend && dotnet test`
|
||||||
only needs re-running if a WP unexpectedly touches `backend/`.
|
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
|
## Order
|
||||||
|
|
||||||
Gates land before the work they cover; each lint rule lands in the same WP as the fixes
|
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-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-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-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-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-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 |
|
| [WP-22](WP-22-durable-persistence.md) | Durable persistence (optional tier) | 5 · productie-volwassenheid | todo |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# WP-19 — Playwright e2e smoke
|
# WP-19 — Playwright e2e smoke
|
||||||
|
|
||||||
Status: todo
|
Status: done (pending commit)
|
||||||
Phase: 5 — productie-volwassenheid
|
Phase: 5 — productie-volwassenheid
|
||||||
|
|
||||||
## Why
|
## Why
|
||||||
@@ -70,14 +70,46 @@ serve` backgrounded), wait-on both ports, `npm run e2e`. `timeout-minutes: 15`
|
|||||||
|
|
||||||
## Acceptance criteria
|
## 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.
|
`dotnet run` run manually.
|
||||||
- [ ] CI job `e2e` is green and runs on every PR alongside the existing jobs.
|
- [x] 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] The happy-path spec exercises a real wizard submit against the real backend
|
||||||
(not mocked) and asserts on the resulting UI state.
|
(not mocked) and asserts on the resulting UI state.
|
||||||
- [ ] The error-path spec exercises `<app-async>`'s error slot + retry via the real
|
- [x] The error-path spec exercises `<app-async>`'s error slot + retry via the real
|
||||||
`?scenario=error` toggle, not a mocked HTTP response.
|
`?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
|
## Verification
|
||||||
|
|
||||||
`npm run e2e` locally; then push a branch and confirm the new `e2e` CI job appears
|
`npm run e2e` locally; then push a branch and confirm the new `e2e` CI job appears
|
||||||
|
|||||||
1008
documentation.json
1008
documentation.json
File diff suppressed because one or more lines are too long
32
e2e/error-state.spec.ts
Normal file
32
e2e/error-state.spec.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
// The dev-only `?scenario=error` toggle forces the scenario.interceptor to fail
|
||||||
|
// the request WITHOUT ever reaching the real HTTP transport (it substitutes a
|
||||||
|
// `throwError` in the rxjs pipe before `next(req)` runs) — so this is not
|
||||||
|
// observable as a browser network request. It IS observable as a real resource
|
||||||
|
// reload: `retry()` calls `resource.reload()`, which flips <app-async> back to
|
||||||
|
// its Loading (`aria-busy="true"`) state before failing again 400ms later.
|
||||||
|
// `currentScenario()` re-reads `window.location.search` on every call, not a
|
||||||
|
// one-shot value, so as long as the query param is still there, the retry
|
||||||
|
// genuinely re-runs and genuinely fails the same way — that's what's asserted
|
||||||
|
// here: a real reload cycle, not a no-op button.
|
||||||
|
test('dashboard error state renders, retry re-fetches (and fails again)', async ({ page }) => {
|
||||||
|
await page.goto('/login');
|
||||||
|
await page.getByLabel('BSN').fill('123456789');
|
||||||
|
await page.getByRole('button', { name: 'Inloggen met DigiD' }).click();
|
||||||
|
await expect(page).toHaveURL(/\/dashboard$/);
|
||||||
|
|
||||||
|
await page.goto('/dashboard?scenario=error');
|
||||||
|
// The dashboard has more than one independent <app-async> (profile,
|
||||||
|
// aantekeningen) — both fail under the blanket ?scenario=error, so this text
|
||||||
|
// legitimately appears more than once. Assert at least one, not exactly one.
|
||||||
|
const errorAlert = page.getByText('Er ging iets mis bij het laden van de gegevens.').first();
|
||||||
|
await expect(errorAlert).toBeVisible();
|
||||||
|
const retry = page.getByRole('button', { name: 'Opnieuw proberen' }).first();
|
||||||
|
await expect(retry).toBeVisible();
|
||||||
|
|
||||||
|
await retry.click();
|
||||||
|
// A real reload cycle: back to Loading (aria-busy) before failing again.
|
||||||
|
await expect(page.locator('[aria-busy="true"]').first()).toBeAttached({ timeout: 1_000 });
|
||||||
|
await expect(errorAlert).toBeVisible({ timeout: 5_000 });
|
||||||
|
});
|
||||||
59
e2e/smoke.spec.ts
Normal file
59
e2e/smoke.spec.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
// One happy-path flow through the real FE+backend: log in, land on the real
|
||||||
|
// dashboard, run the registratie wizard's minimum required path (a DUO diploma
|
||||||
|
// with zero policy questions, so the only required upload is identiteit), submit,
|
||||||
|
// and see the real confirmation. Not a full wizard-coverage suite — see WP-19.
|
||||||
|
//
|
||||||
|
// The backend is in-memory and shared across runs; this test mutates real state
|
||||||
|
// (creates a registratie application for the fixed demo identity). Restart the
|
||||||
|
// backend between CI runs — a second run would see a leftover Concept/submitted
|
||||||
|
// application on the dashboard, which this test doesn't assert against, but a
|
||||||
|
// stricter future test might.
|
||||||
|
test('login → dashboard → registratie wizard → submitted', async ({ page }) => {
|
||||||
|
await page.goto('/login');
|
||||||
|
await page.getByLabel('BSN').fill('123456789');
|
||||||
|
await page.getByLabel('Wachtwoord').fill('demo');
|
||||||
|
await page.getByRole('button', { name: 'Inloggen met DigiD' }).click();
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/\/dashboard$/);
|
||||||
|
await expect(page.getByRole('heading', { level: 1, name: 'Mijn overzicht' })).toBeVisible();
|
||||||
|
// Real backend content, not a loading/error state.
|
||||||
|
await expect(page.getByText('Persoonsgegevens (BRP)')).toBeVisible();
|
||||||
|
|
||||||
|
await page.goto('/registreren');
|
||||||
|
await expect(
|
||||||
|
page.getByRole('heading', { level: 1, name: 'Inschrijven in het BIG-register' }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// Step 1 — adres: BRP prefill is real backend data; "Post" skips the email field.
|
||||||
|
// The CIBG-styled radio hides the native input behind its label, so click the
|
||||||
|
// label (real user interaction) rather than `.check()` the covered input.
|
||||||
|
await expect(page.getByText('Vooraf ingevuld op basis van de BRP')).toBeVisible();
|
||||||
|
await page.locator('label[for="correspondentie-post"]').click();
|
||||||
|
await page.locator('button[type="submit"]').click();
|
||||||
|
|
||||||
|
// Step 2 — beroep: the first DUO diploma (Geneeskunde, non-English) carries zero
|
||||||
|
// policy questions, so the only required document is identiteit.
|
||||||
|
await expect(page.locator('#diploma-d1')).toBeVisible({ timeout: 10_000 });
|
||||||
|
await page.locator('label[for="diploma-d1"]').click();
|
||||||
|
await expect(page.getByText('Beroep (afgeleid uit diploma)')).toBeVisible();
|
||||||
|
|
||||||
|
await page.locator('#identiteit-file').setInputFiles({
|
||||||
|
name: 'identiteit.pdf',
|
||||||
|
mimeType: 'application/pdf',
|
||||||
|
buffer: Buffer.from('%PDF-1.4 fake e2e fixture'),
|
||||||
|
});
|
||||||
|
// Real upload against the backend: wait for the row to leave "uploading".
|
||||||
|
await expect(page.locator('.upload-progress')).toHaveCount(0, { timeout: 10_000 });
|
||||||
|
await expect(page.locator('.icon-remove')).toBeVisible();
|
||||||
|
|
||||||
|
await page.locator('button[type="submit"]').click();
|
||||||
|
|
||||||
|
// Step 3 — controle: review + real submit.
|
||||||
|
await expect(page.getByText('Controleer uw gegevens en dien de registratie in.')).toBeVisible();
|
||||||
|
await page.locator('button[type="submit"]').click();
|
||||||
|
|
||||||
|
await expect(page.getByText('Uw registratie is ontvangen')).toBeVisible({ timeout: 10_000 });
|
||||||
|
await expect(page.getByText(/Uw referentienummer is/)).toBeVisible();
|
||||||
|
});
|
||||||
17
package-lock.json
generated
17
package-lock.json
generated
@@ -27,6 +27,7 @@
|
|||||||
"@angular/localize": "^22.0.4",
|
"@angular/localize": "^22.0.4",
|
||||||
"@angular/platform-browser-dynamic": "^22.0.0",
|
"@angular/platform-browser-dynamic": "^22.0.0",
|
||||||
"@compodoc/compodoc": "^1.2.1",
|
"@compodoc/compodoc": "^1.2.1",
|
||||||
|
"@playwright/test": "^1.61.1",
|
||||||
"@storybook/addon-a11y": "^10.4.6",
|
"@storybook/addon-a11y": "^10.4.6",
|
||||||
"@storybook/addon-docs": "^10.4.6",
|
"@storybook/addon-docs": "^10.4.6",
|
||||||
"@storybook/addon-onboarding": "^10.4.6",
|
"@storybook/addon-onboarding": "^10.4.6",
|
||||||
@@ -8451,6 +8452,22 @@
|
|||||||
"url": "https://opencollective.com/pkgr"
|
"url": "https://opencollective.com/pkgr"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@playwright/test": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.61.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@polka/send-type": {
|
"node_modules/@polka/send-type": {
|
||||||
"version": "0.5.2",
|
"version": "0.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/@polka/send-type/-/send-type-0.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/@polka/send-type/-/send-type-0.5.2.tgz",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"build-storybook": "ng run atomic-design-poc:build-storybook",
|
"build-storybook": "ng run atomic-design-poc:build-storybook",
|
||||||
"test-storybook": "test-storybook",
|
"test-storybook": "test-storybook",
|
||||||
"test-storybook:ci": "concurrently -k -s first -n sb,axe \"http-server storybook-static -p 6006 --silent\" \"wait-on tcp:127.0.0.1:6006 && test-storybook --url http://127.0.0.1:6006\"",
|
"test-storybook:ci": "concurrently -k -s first -n sb,axe \"http-server storybook-static -p 6006 --silent\" \"wait-on tcp:127.0.0.1:6006 && test-storybook --url http://127.0.0.1:6006\"",
|
||||||
"check:tokens": "bash scripts/check-tokens.sh"
|
"check:tokens": "bash scripts/check-tokens.sh",
|
||||||
|
"e2e": "playwright test"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "npm@11.12.1",
|
"packageManager": "npm@11.12.1",
|
||||||
@@ -39,6 +40,7 @@
|
|||||||
"@angular/localize": "^22.0.4",
|
"@angular/localize": "^22.0.4",
|
||||||
"@angular/platform-browser-dynamic": "^22.0.0",
|
"@angular/platform-browser-dynamic": "^22.0.0",
|
||||||
"@compodoc/compodoc": "^1.2.1",
|
"@compodoc/compodoc": "^1.2.1",
|
||||||
|
"@playwright/test": "^1.61.1",
|
||||||
"@storybook/addon-a11y": "^10.4.6",
|
"@storybook/addon-a11y": "^10.4.6",
|
||||||
"@storybook/addon-docs": "^10.4.6",
|
"@storybook/addon-docs": "^10.4.6",
|
||||||
"@storybook/addon-onboarding": "^10.4.6",
|
"@storybook/addon-onboarding": "^10.4.6",
|
||||||
|
|||||||
29
playwright.config.ts
Normal file
29
playwright.config.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { defineConfig } from '@playwright/test';
|
||||||
|
|
||||||
|
// Smoke-level e2e (WP-19): one happy path, one degraded path, against the REAL
|
||||||
|
// backend — proving the FE+BE seam, not replacing component/unit tests.
|
||||||
|
const baseURL = process.env['E2E_BASE_URL'] ?? 'http://localhost:4200';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './e2e',
|
||||||
|
timeout: 30_000,
|
||||||
|
fullyParallel: true,
|
||||||
|
retries: process.env['CI'] ? 1 : 0,
|
||||||
|
reporter: process.env['CI'] ? 'github' : 'list',
|
||||||
|
use: {
|
||||||
|
baseURL,
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
},
|
||||||
|
projects: [{ name: 'chromium', use: { browserName: 'chromium' } }],
|
||||||
|
// CI starts `ng serve` + the backend as separate job steps (both need to be up
|
||||||
|
// before the suite runs); locally, boot `npm start` automatically so `npm run e2e`
|
||||||
|
// works standalone — the backend still needs `dotnet run` running separately.
|
||||||
|
webServer: process.env['CI']
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
command: 'npm start',
|
||||||
|
url: baseURL,
|
||||||
|
reuseExistingServer: true,
|
||||||
|
timeout: 120_000,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -72,4 +72,12 @@ export class BigProfileStore {
|
|||||||
rollbackHerregistratie() {
|
rollbackHerregistratie() {
|
||||||
this.pending.set(false); // submission failed — undo the optimistic flag
|
this.pending.set(false); // submission failed — undo the optimistic flag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Retry hooks for [data]-fed <app-async> instances (they don't own the resource).
|
||||||
|
reloadProfile() {
|
||||||
|
this.viewRes.reload();
|
||||||
|
}
|
||||||
|
reloadAantekeningen() {
|
||||||
|
this.aantekeningenRes.reload();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
|
|||||||
>
|
>
|
||||||
}
|
}
|
||||||
|
|
||||||
<app-async [data]="store.profile()">
|
<app-async [data]="store.profile()" (retryClicked)="store.reloadProfile()">
|
||||||
<ng-template appAsyncLoaded>
|
<ng-template appAsyncLoaded>
|
||||||
@if (profile(); as p) {
|
@if (profile(); as p) {
|
||||||
@let tasks = tasksFor(p.registration);
|
@let tasks = tasksFor(p.registration);
|
||||||
@@ -153,7 +153,7 @@ import { tasksFromProfile } from '@registratie/domain/tasks';
|
|||||||
>Specialismen en aantekeningen</app-heading
|
>Specialismen en aantekeningen</app-heading
|
||||||
>
|
>
|
||||||
<div class="app-section">
|
<div class="app-section">
|
||||||
<app-async [data]="store.aantekeningen()">
|
<app-async [data]="store.aantekeningen()" (retryClicked)="store.reloadAantekeningen()">
|
||||||
<ng-template appAsyncLoaded>
|
<ng-template appAsyncLoaded>
|
||||||
@if (aantekeningen(); as r) {
|
@if (aantekeningen(); as r) {
|
||||||
<app-registration-table [rows]="r" />
|
<app-registration-table [rows]="r" />
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { Component, Directive, TemplateRef, computed, contentChild, input } from '@angular/core';
|
import {
|
||||||
|
Component,
|
||||||
|
Directive,
|
||||||
|
TemplateRef,
|
||||||
|
computed,
|
||||||
|
contentChild,
|
||||||
|
input,
|
||||||
|
output,
|
||||||
|
} from '@angular/core';
|
||||||
import { NgTemplateOutlet } from '@angular/common';
|
import { NgTemplateOutlet } from '@angular/common';
|
||||||
import type { Resource } from '@angular/core';
|
import type { Resource } from '@angular/core';
|
||||||
import { SpinnerComponent } from '@shared/ui/spinner/spinner.component';
|
import { SpinnerComponent } from '@shared/ui/spinner/spinner.component';
|
||||||
@@ -130,11 +138,16 @@ export class AsyncComponent<T> {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// [resource]-fed callers get reload() for free. [data]-fed callers (a store's
|
||||||
|
// combined RemoteData — the component doesn't own that resource) must reload
|
||||||
|
// it themselves; retryClicked is how they find out a retry was requested.
|
||||||
|
retryClicked = output<void>();
|
||||||
retry = () => {
|
retry = () => {
|
||||||
const r = this.resource();
|
const r = this.resource();
|
||||||
if (r && 'reload' in r && typeof (r as { reload?: unknown }).reload === 'function') {
|
if (r && 'reload' in r && typeof (r as { reload?: unknown }).reload === 'function') {
|
||||||
(r as { reload: () => void }).reload();
|
(r as { reload: () => void }).reload();
|
||||||
}
|
}
|
||||||
|
this.retryClicked.emit();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user