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