From cd3faaf63813e21ad0f29417fad1c5fede37fff7 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Fri, 4 Sep 2026 10:45:17 +0200 Subject: [PATCH 1/6] test(portal-behandel): the werkbak must show a new registration without a reload (refs #162) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A registration only reaches the Beoordelen task after the citizen supplies its documents, so it lands in an already-open werkbak asynchronously — today a behandelaar has to reload to see it. Component spec: one interval passes and the werkbak has re-read itself, the new row is on screen, and the loading state has not flashed over the rows being read. Fails with "expected vi.fn() to be called 2 times, but got 1 times". Two guards go with it — a failing background poll must not replace the list with the load-failure alert, and the refresh must stop when the page is destroyed. E2E: the happy path now waits for the werkbak row WITHOUT reloading the page. The removed `staff.reload()` is the assertion. Refs the accepted mechanism in #163 (ADR-0032, landing with the implementation). Co-Authored-By: Claude Opus 5 (1M context) --- .../src/app/werkbak/werkbak-page.spec.ts | 66 ++++++++++++++++++- apps/behandel/src/app/werkbak/werkbak-page.ts | 6 ++ tests/e2e/registration.spec.ts | 17 ++--- 3 files changed, 78 insertions(+), 11 deletions(-) diff --git a/apps/behandel/src/app/werkbak/werkbak-page.spec.ts b/apps/behandel/src/app/werkbak/werkbak-page.spec.ts index 9799845..202953b 100644 --- a/apps/behandel/src/app/werkbak/werkbak-page.spec.ts +++ b/apps/behandel/src/app/werkbak/werkbak-page.spec.ts @@ -4,7 +4,7 @@ import { of, throwError } from 'rxjs'; import { BffApiV1Service, type WerkbakItem } from 'api-client'; import { AuthService } from 'auth'; import { axe } from 'vitest-axe'; -import { WerkbakPage } from './werkbak-page'; +import { WERKBAK_REFRESH_MS, WerkbakPage } from './werkbak-page'; const sample: WerkbakItem[] = [ { registrationId: 'reg-1', bsn: '123456782', status: 'InBehandeling' }, @@ -81,6 +81,70 @@ describe('WerkbakPage', () => { }); }); + it('picks up a newly submitted registration without a reload', async () => { + // S-26 (#162): a registration reaches Beoordelen asynchronously, after the citizen supplies + // documents — so the werkbak must refresh itself rather than wait for the behandelaar to reload. + vi.useFakeTimers(); + try { + const getBehandelWerkbak = vi + .fn() + .mockReturnValueOnce(of([sample[0]])) + .mockReturnValue(of(sample)); + const { providers } = setup({ getBehandelWerkbak }); + const { detectChanges } = await render(WerkbakPage, { providers }); + + expect(screen.getByText('reg-1')).toBeTruthy(); + expect(screen.queryByText('reg-2')).toBeNull(); + + vi.advanceTimersByTime(WERKBAK_REFRESH_MS); + detectChanges(); + + expect(getBehandelWerkbak).toHaveBeenCalledTimes(2); + expect(screen.getByText('reg-2')).toBeTruthy(); + // A background refresh must not flash the loading state over the rows the behandelaar is reading. + expect(screen.queryByText(/bezig met laden/i)).toBeNull(); + } finally { + vi.useRealTimers(); + } + }); + + it('keeps the rows on screen when a background refresh fails', async () => { + // A blip on a background poll must not replace the list with the load-failure alert; the next + // tick recovers. Only the first load speaks for whether the werkbak is readable at all. + vi.useFakeTimers(); + try { + const getBehandelWerkbak = vi + .fn() + .mockReturnValueOnce(of(sample)) + .mockReturnValue(throwError(() => new Error('503'))); + const { providers } = setup({ getBehandelWerkbak }); + const { detectChanges } = await render(WerkbakPage, { providers }); + + vi.advanceTimersByTime(WERKBAK_REFRESH_MS); + detectChanges(); + + expect(screen.getByText('reg-1')).toBeTruthy(); + expect(screen.queryByText(/kon de werkbak niet laden/i)).toBeNull(); + } finally { + vi.useRealTimers(); + } + }); + + it('stops refreshing once the page is destroyed', async () => { + vi.useFakeTimers(); + try { + const { getBehandelWerkbak, providers } = setup(); + const { fixture } = await render(WerkbakPage, { providers }); + + fixture.destroy(); + vi.advanceTimersByTime(WERKBAK_REFRESH_MS * 3); + + expect(getBehandelWerkbak).toHaveBeenCalledTimes(1); + } finally { + vi.useRealTimers(); + } + }); + it('shows an empty state when the werkbak has no items', async () => { const { providers } = setup({ getBehandelWerkbak: vi.fn().mockReturnValue(of([])) }); await render(WerkbakPage, { providers }); diff --git a/apps/behandel/src/app/werkbak/werkbak-page.ts b/apps/behandel/src/app/werkbak/werkbak-page.ts index cc5b8e4..d7af7d4 100644 --- a/apps/behandel/src/app/werkbak/werkbak-page.ts +++ b/apps/behandel/src/app/werkbak/werkbak-page.ts @@ -2,6 +2,12 @@ import { Component, inject, signal } from '@angular/core'; import { BffApiV1Service, type WerkbakItem } from 'api-client'; import { UtrechtComponentsModule } from 'ui'; +/** + * How often an open werkbak re-reads itself (S-26/#162, ADR-0032). Exported so the spec advances the + * clock by exactly one interval instead of hard-coding the number. + */ +export const WERKBAK_REFRESH_MS = 5_000; + /** The two decisions a behandelaar can make; the BFF validates these exact values (ADR-0013). */ type Besluit = 'goedkeuren' | 'afwijzen'; diff --git a/tests/e2e/registration.spec.ts b/tests/e2e/registration.spec.ts index a7a9c0e..bb88e78 100644 --- a/tests/e2e/registration.spec.ts +++ b/tests/e2e/registration.spec.ts @@ -82,17 +82,14 @@ test('DigiD submit → public INGEDIEND → documenten → behandelaar goedkeurt await expect(staff.getByRole('heading', { name: /Werkbak/i })).toBeVisible(); - // The registration reaches the Beoordelen user task only after its documents are provided (above), so - // it appears in the werkbak asynchronously — reload until this reference's row shows up. Target the - // decide button by reference (not a generic "Goedkeuren"): the shared verify stack holds other open - // tasks, so a positional match could act on someone else's registration. + // The registration reaches the Beoordelen user task only after its documents are provided (above), + // so it appears in the werkbak asynchronously. Since S-26 (#162) the werkbak refreshes itself, so + // this waits on the row WITHOUT reloading the page — the reload here is what the slice removes, and + // its absence is the live-refresh assertion. Target the decide button by reference (not a generic + // "Goedkeuren"): the shared verify stack holds other open tasks, so a positional match could act on + // someone else's registration. const goedkeuren = staff.getByRole('button', { name: `Goedkeuren ${reference}` }); - await expect - .poll(async () => { - await staff.reload(); - return goedkeuren.count(); - }, { timeout: 30_000, intervals: [1_000, 2_000, 3_000, 5_000] }) - .toBeGreaterThan(0); + await expect(goedkeuren).toBeVisible({ timeout: 30_000 }); // Click and wait for the decide POST to finish (204) BEFORE leaving the page. `click()` only // dispatches the request; navigating away immediately cancels it in flight (nginx logs a 499) and -- 2.54.0 From e38bf62b438e7c99943784a6eb004b936bc4e18e Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Fri, 4 Sep 2026 10:46:02 +0200 Subject: [PATCH 2/6] feat(portal-behandel): refresh the werkbak on an interval while it is open (refs #162) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `interval(WERKBAK_REFRESH_MS)` re-reads the existing BFF werkbak endpoint, scoped to the page's lifetime with `takeUntilDestroyed()`. A background read leaves the rows and states on screen alone until it has an answer, so a tick neither flashes the loading state over rows being read nor swaps the list for the failure alert on a blip — only a foreground read (on open, after a decision) speaks for whether the werkbak is readable at all. No new endpoint, dependency or server state: rxjs and the endpoint are both already here (ADR-0032). Co-Authored-By: Claude Opus 5 (1M context) --- apps/behandel/src/app/werkbak/werkbak-page.ts | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/apps/behandel/src/app/werkbak/werkbak-page.ts b/apps/behandel/src/app/werkbak/werkbak-page.ts index d7af7d4..5c0ba4a 100644 --- a/apps/behandel/src/app/werkbak/werkbak-page.ts +++ b/apps/behandel/src/app/werkbak/werkbak-page.ts @@ -1,4 +1,6 @@ import { Component, inject, signal } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { interval } from 'rxjs'; import { BffApiV1Service, type WerkbakItem } from 'api-client'; import { UtrechtComponentsModule } from 'ui'; @@ -16,6 +18,11 @@ type Besluit = 'goedkeuren' | 'afwijzen'; * Flowable `Beoordelen` tasks, read through the domain) and decides each — goedkeuren or afwijzen. A * decision posts to the BFF, which applies the domain transition and completes the workflow task * (ADR-0013; S-12). After a decision the werkbak refreshes so the handled item drops off the list. + * + * The page also re-reads itself every {@link WERKBAK_REFRESH_MS} while it is open, so a registration + * that reaches beoordeling after the behandelaar opened the werkbak shows up on its own — no reload + * (S-26/#162). Polling rather than a pushed stream: nothing notifies the BFF either, so a stream + * would poll the domain in the BFF instead and add connection state for the same freshness (ADR-0032). */ @Component({ selector: 'app-werkbak-page', @@ -33,11 +40,25 @@ export class WerkbakPage { constructor() { this.load(); + // ponytail: a fixed interval, polled while the page lives — it keeps refreshing in a background + // tab. Gate on `document.visibilityState` if the request volume ever matters. + interval(WERKBAK_REFRESH_MS) + .pipe(takeUntilDestroyed()) + .subscribe(() => this.load({ background: true })); } - load(): void { - this.loading.set(true); - this.failed.set(false); + /** + * Read the werkbak. A `background` read is the interval refresh: it leaves the rows and the states + * the behandelaar is looking at alone until it has an answer — no loading flash on every tick, and + * a blip does not swap the list for the failure alert (the next tick recovers). Only a foreground + * read — on open, or after a decision — speaks for whether the werkbak is readable at all. + */ + load(options: { background?: boolean } = {}): void { + const background = options.background ?? false; + if (!background) { + this.loading.set(true); + this.failed.set(false); + } this.bff.getBehandelWerkbak().subscribe({ next: (rows: WerkbakItem[]) => { this.items.set(rows); @@ -46,6 +67,7 @@ export class WerkbakPage { }, // Surface the failure (e.g. 403 for a non-behandelaar) instead of swallowing it. error: () => { + if (background) return; this.items.set([]); this.loading.set(false); this.loaded.set(true); -- 2.54.0 From d906e2c1111799c1cc23ef049f46a4b5bd3ea715 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Fri, 4 Sep 2026 10:46:28 +0200 Subject: [PATCH 3/6] test(portal-behandel): a refresh must clear a stale load failure (refs #162) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If the first read fails the werkbak shows its error until the behandelaar reloads — the very thing this slice removes. Fails with "expected

to be null". Co-Authored-By: Claude Opus 5 (1M context) --- .../src/app/werkbak/werkbak-page.spec.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/apps/behandel/src/app/werkbak/werkbak-page.spec.ts b/apps/behandel/src/app/werkbak/werkbak-page.spec.ts index 202953b..d704561 100644 --- a/apps/behandel/src/app/werkbak/werkbak-page.spec.ts +++ b/apps/behandel/src/app/werkbak/werkbak-page.spec.ts @@ -145,6 +145,30 @@ describe('WerkbakPage', () => { } }); + it('clears a load failure once a refresh succeeds', async () => { + // Without this the werkbak stays stuck on the error until the behandelaar reloads — the very + // thing this slice removes. A recovered read must put the rows back. + vi.useFakeTimers(); + try { + const getBehandelWerkbak = vi + .fn() + .mockReturnValueOnce(throwError(() => new Error('503'))) + .mockReturnValue(of(sample)); + const { providers } = setup({ getBehandelWerkbak }); + const { detectChanges } = await render(WerkbakPage, { providers }); + + expect(screen.getByText(/kon de werkbak niet laden/i)).toBeTruthy(); + + vi.advanceTimersByTime(WERKBAK_REFRESH_MS); + detectChanges(); + + expect(screen.queryByText(/kon de werkbak niet laden/i)).toBeNull(); + expect(screen.getByText('reg-1')).toBeTruthy(); + } finally { + vi.useRealTimers(); + } + }); + it('shows an empty state when the werkbak has no items', async () => { const { providers } = setup({ getBehandelWerkbak: vi.fn().mockReturnValue(of([])) }); await render(WerkbakPage, { providers }); -- 2.54.0 From 3f8b4385002aadb069aeb4a703d06e78920387a3 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Fri, 4 Sep 2026 10:46:41 +0200 Subject: [PATCH 4/6] feat(portal-behandel): let a successful refresh clear the load failure (refs #162) Co-Authored-By: Claude Opus 5 (1M context) --- apps/behandel/src/app/werkbak/werkbak-page.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/behandel/src/app/werkbak/werkbak-page.ts b/apps/behandel/src/app/werkbak/werkbak-page.ts index 5c0ba4a..f73ddb1 100644 --- a/apps/behandel/src/app/werkbak/werkbak-page.ts +++ b/apps/behandel/src/app/werkbak/werkbak-page.ts @@ -64,6 +64,9 @@ export class WerkbakPage { this.items.set(rows); this.loading.set(false); this.loaded.set(true); + // A read that came back is the answer, so a refresh also clears an earlier failure — the + // werkbak recovers on its own instead of showing the error until someone reloads. + this.failed.set(false); }, // Surface the failure (e.g. 403 for a non-behandelaar) instead of swallowing it. error: () => { -- 2.54.0 From fc843a419a260ae5bc1cafda5fd98899bde18a57 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Fri, 4 Sep 2026 11:00:21 +0200 Subject: [PATCH 5/6] docs(portal-behandel): ADR-0032 + demo note for the self-refreshing werkbak (refs #162) Records why the werkbak polls rather than being pushed to (proposal #163): nothing notifies the BFF either, so SSE/WebSockets would poll the domain inside the BFF for the same freshness plus connection state, a dependency and a stateful BFF. Names the ceiling (fixed 5s interval, per open page, keeps polling in a hidden tab) and the upgrade path (publish task events, then swap the interval for a stream). Co-Authored-By: Claude Opus 5 (1M context) --- .../adr-0032-werkbak-live-refresh.md | 79 +++++++++++++++++++ docs/demo-script.md | 34 ++++++++ 2 files changed, 113 insertions(+) create mode 100644 docs/architecture/adr-0032-werkbak-live-refresh.md diff --git a/docs/architecture/adr-0032-werkbak-live-refresh.md b/docs/architecture/adr-0032-werkbak-live-refresh.md new file mode 100644 index 0000000..090c37d --- /dev/null +++ b/docs/architecture/adr-0032-werkbak-live-refresh.md @@ -0,0 +1,79 @@ +# ADR-0032: The werkbak refreshes itself by polling, not by a pushed stream + +- **Status:** Accepted +- **Date:** 2026-09-04 +- **Deciders:** Respellion engineering +- **Slice:** #162 (proposal #163). The issue titles it S-26; that id already belongs to + the self-service resume slice (#111), so #162 is the identifier that counts. + +## Context + +The werkbak (S-12) is a read of the open Flowable `Beoordelen` tasks: portal → BFF +`GET /behandel/werkbak` → domain `Werkbak` query → workflow engine, each task enriched +from its aggregate. A registration reaches `Beoordelen` **asynchronously**, only once the +citizen supplies its documents and the DMN routes it (S-10a) — so it appears in a werkbak +that is already open, and until now a behandelaar had to reload the page to see it. + +Three forces shape the mechanism: + +- **Nothing notifies anyone.** The trigger lives in Flowable. The domain does not publish + task events, and there is no bus between the domain and the BFF. +- **The BFF is stateless** and sits behind each portal's nginx. +- **This is the repo's first live-updating view**, so the choice sets a precedent. + +## Decision + +**The werkbak page re-reads the existing BFF endpoint on a fixed interval +(`WERKBAK_REFRESH_MS`, 5 s) while it is open. No new endpoint, dependency or server-side +state.** + +The refresh is a *background* read: it leaves the rows and the loading/failure states +untouched until it has an answer, so a tick never flashes a spinner over rows a +behandelaar is reading and a single failed poll never swaps the list for the error alert. +A read that comes back also clears an earlier failure, so the view recovers on its own — +the same reload this slice set out to remove would otherwise be needed to escape a +transient error. Only a foreground read (on open, after a decision) speaks for whether the +werkbak is readable at all. + +### Why not SSE or WebSockets + +Neither buys freshness here, because **nothing notifies the BFF either**: + +- **SSE** (`text/event-stream`) would mean a new streaming endpoint whose handler polls the + domain and forwards diffs — the same latency, plus connection lifecycle, nginx + buffering, and auth on a long-lived connection. +- **WebSocket/SignalR** adds a dependency (CLAUDE.md §13) and makes the BFF stateful and + sticky-session-bound. A genuine push path would *also* need the domain to publish task + events. Warranted by high-frequency, bidirectional or fan-out-heavy traffic; the werkbak + is none of those. + +Polling meets the acceptance ("a registration can be seen in the werkbak once it is ready +for review") in a handful of lines inside one component. + +- ponytail ceiling: a fixed 5 s interval, per open page, that keeps polling in a + background tab. Each tick costs one Flowable task query plus a store read per open task. +- Upgrade path: publish task events from the domain, then swap the component's `interval` + for a stream. The endpoint contract and the component's rendering stay as they are; + gate on `document.visibilityState` first if request volume is the concern. + +## Consequences + +**Positive** + +- The outcome is delivered with no new endpoint, dependency, or server-side state, and no + service boundary moves. +- Self-healing: a transient read failure no longer strands the view until a manual reload. +- The e2e got *simpler* — the happy path waits for the werkbak row without reloading the + page, which is itself the live-refresh assertion. + +**Negative / costs** + +- Staleness is bounded by one interval (≤5 s) rather than instant. +- One `GET /behandel/werkbak` per open werkbak per interval, including in hidden tabs. +- The precedent is polling; a future view with genuinely high-frequency updates will have + to revisit this (see the upgrade path above). + +## Coupling rules touched (CLAUDE.md §8) + +None. The poll reuses the existing portal → BFF → domain read path: §8.3 (portals talk +only to the BFF) and §8.2 (only the Workflow Client talks to Flowable) are unchanged. diff --git a/docs/demo-script.md b/docs/demo-script.md index 5172b33..7500cc2 100644 --- a/docs/demo-script.md +++ b/docs/demo-script.md @@ -5,6 +5,40 @@ copy-pasteable walkthrough against a local `make up` stack. --- +## S-26/#162 — the werkbak refreshes itself (ADR-0032) + +**Outcome:** a registration that reaches beoordeling while a behandelaar already has the werkbak open +**appears on its own** — no reload. The page re-reads `GET /behandel/werkbak` every 5 seconds; a +background refresh swaps the rows in without flashing the loading state, and a transient failure no +longer strands the view on its error message until someone reloads. + +```bash +# 1. Two windows. Left: the behandel werkbak, already open and idle. +python3 infra/keycloak/check_realms.py otp # a code, valid right now +open http://localhost:8142 # merel-behandelaar / test123 + that code +# +# 2. Right: submit a registration and supply its documents (this is what routes it to Beoordelen). +open http://localhost:8140 # jan-burger / test123 → indienen → upload a PDF +# +# 3. Watch the left window. Within ~5 seconds the new reference appears in the werkbak — the page was +# never reloaded and never left the werkbak. +# +# 4. Automated, end to end: the happy path now waits for the werkbak row WITHOUT reloading, so the +# absence of the reload IS the assertion. +make verify-e2e # → registration.spec: "… → behandelaar goedkeurt → public INGESCHREVEN" +# +# 5. Component level (background refresh, failure recovery, teardown): +pnpm nx test behandel # → "picks up a newly submitted registration without a reload" (+3 guards) +``` + +**The path:** unchanged — portal → BFF `GET /behandel/werkbak` → domain `Werkbak` → Flowable. Only the +page's cadence is new: `interval(WERKBAK_REFRESH_MS)` scoped to the page with `takeUntilDestroyed()`. + +**Not push:** nothing notifies the BFF either, so SSE/WebSockets would poll the domain inside the BFF +for the same freshness plus connection state — see ADR-0032 for the trade-off and the upgrade path. + +--- + ## S-19a — approval writes the register record to Objecten (#149, ADR-0028) **Outcome:** approving a registration no longer only moves the ZGW zaak to its eindstatus — it also -- 2.54.0 From 3628efc17a79bb575b1713b4d2de3e411a4ef18a Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Fri, 4 Sep 2026 11:12:30 +0200 Subject: [PATCH 6/6] test(e2e): open the werkbak before the registration is ready for beoordeling (refs #162) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The happy path visited the behandel portal only after the documents were supplied, so the row was already in the werkbak at page load — dropping the reload proved nothing. Now the behandelaar logs in first, asserts the row is NOT there yet, and only then does the citizen supply the documents that route it to Beoordelen. The row can therefore only reach that already-open, never-reloaded page by the werkbak refreshing itself. Verified both ways against a live stack: with the interval stubbed out the spec fails at "Goedkeuren … element(s) not found" after 30s; with it, the behandel nginx logs the poll that delivers the row. The werkbak page is foregrounded before the assertion — Chromium throttles timers in a hidden tab. Co-Authored-By: Claude Opus 5 (1M context) --- tests/e2e/registration.spec.ts | 40 ++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/tests/e2e/registration.spec.ts b/tests/e2e/registration.spec.ts index bb88e78..e4182b7 100644 --- a/tests/e2e/registration.spec.ts +++ b/tests/e2e/registration.spec.ts @@ -59,12 +59,32 @@ test('DigiD submit → public INGEDIEND → documenten → behandelaar goedkeurt await expect(staff.getByRole('row', { name: reference }).getByRole('cell', { name: 'INGEDIEND' })) .toBeVisible(); + // A behandelaar opens the behandel-portal werkbak and approves the registration (goedkeuren) — the + // S-12 flow that replaces the temporary admin endpoint. The staff tab switches to the medewerker + // realm (a different Keycloak realm than the citizen's digid session). + // + // The werkbak is opened BEFORE the citizen supplies the documents that route the registration to + // Beoordelen, so its row cannot be there at page load: the only thing that can deliver it to this + // already-open page is the werkbak refreshing itself (S-26/#162, ADR-0032). This spec used to + // `staff.reload()` in a poll loop here; the absence of that reload is the live-refresh assertion. + await staff.goto('http://behandel/'); + // That realm enforces MFA (S-15c), so the behandelaar logs in with password + TOTP. + await loginMedewerker(staff, 'merel-behandelaar'); + + await expect(staff.getByRole('heading', { name: /Werkbak/i })).toBeVisible(); + + // Target the decide button by reference (not a generic "Goedkeuren"): the shared verify stack holds + // other open tasks, so a positional match could act on someone else's registration. + const goedkeuren = staff.getByRole('button', { name: `Goedkeuren ${reference}` }); + await expect(goedkeuren, 'the registration is not awaiting beoordeling yet').toBeHidden(); + // Provide the documents the registration is waiting for (S-10a), on the still-open self-service tab. // The process parks at WachtOpDocumenten only after the zaak is opened; the INGEDIEND row above proves // the zaak exists — so the OpenZaak worker has completed and the process is now at the wait — which is // why we supply the documents here rather than right after submit, when the trigger would race the // wait and no-op. (S-10b turns this into a real file upload; here it is the trigger that unblocks // beoordeling.) + await page.bringToFront(); await page.setInputFiles('#diploma', { name: 'diploma.pdf', mimeType: 'application/pdf', @@ -73,22 +93,10 @@ test('DigiD submit → public INGEDIEND → documenten → behandelaar goedkeurt await page.getByRole('button', { name: /documenten aanleveren/i }).click(); await expect(page.getByText(/documenten zijn aangeleverd/i)).toBeVisible(); - // A behandelaar picks the registration up in the behandel-portal werkbak and approves it (goedkeuren) - // — the S-12 flow that replaces the temporary admin endpoint. The staff tab switches to the - // medewerker realm (a different Keycloak realm than the citizen's digid session). - await staff.goto('http://behandel/'); - // That realm enforces MFA (S-15c), so the behandelaar logs in with password + TOTP. - await loginMedewerker(staff, 'merel-behandelaar'); - - await expect(staff.getByRole('heading', { name: /Werkbak/i })).toBeVisible(); - - // The registration reaches the Beoordelen user task only after its documents are provided (above), - // so it appears in the werkbak asynchronously. Since S-26 (#162) the werkbak refreshes itself, so - // this waits on the row WITHOUT reloading the page — the reload here is what the slice removes, and - // its absence is the live-refresh assertion. Target the decide button by reference (not a generic - // "Goedkeuren"): the shared verify stack holds other open tasks, so a positional match could act on - // someone else's registration. - const goedkeuren = staff.getByRole('button', { name: `Goedkeuren ${reference}` }); + // Back to the werkbak — untouched since login, never reloaded. The row arrives on its own once the + // DMN routes the registration to Beoordelen. (Foregrounded so Chromium doesn't throttle the page's + // refresh timer as a hidden tab.) + await staff.bringToFront(); await expect(goedkeuren).toBeVisible({ timeout: 30_000 }); // Click and wait for the decide POST to finish (204) BEFORE leaving the page. `click()` only -- 2.54.0