test(portal-behandel): the werkbak must show a new registration without a reload (refs #162)
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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 });
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user