import { signal } from '@angular/core'; import { fireEvent, render, screen } from '@testing-library/angular'; import { of, throwError } from 'rxjs'; import { BffApiV1Service, type WerkbakItem } from 'api-client'; import { AuthService } from 'auth'; import { axe } from 'vitest-axe'; import { WERKBAK_REFRESH_MS, WerkbakPage } from './werkbak-page'; const sample: WerkbakItem[] = [ { registrationId: 'reg-1', bsn: '123456782', status: 'InBehandeling' }, { registrationId: 'reg-2', bsn: '111222333', status: 'InBehandeling' }, ]; class FakeAuth extends AuthService { readonly isAuthenticated = signal(true); readonly bsn = signal(undefined); override readonly roles = signal(['behandelaar']); login(): void { /* not exercised here */ } logout(): void { /* spied in tests */ } } function setup( overrides: { getBehandelWerkbak?: ReturnType; postBehandelRegistrationsIdDecide?: ReturnType; } = {}, ) { const getBehandelWerkbak = overrides.getBehandelWerkbak ?? vi.fn().mockReturnValue(of(sample)); const postBehandelRegistrationsIdDecide = overrides.postBehandelRegistrationsIdDecide ?? vi.fn().mockReturnValue(of(undefined)); return { getBehandelWerkbak, postBehandelRegistrationsIdDecide, providers: [ { provide: BffApiV1Service, useValue: { getBehandelWerkbak, postBehandelRegistrationsIdDecide }, }, { provide: AuthService, useClass: FakeAuth }, ], }; } describe('WerkbakPage', () => { it('lists the registrations awaiting beoordeling on open', async () => { const { getBehandelWerkbak, providers } = setup(); await render(WerkbakPage, { providers }); expect(getBehandelWerkbak).toHaveBeenCalled(); expect(await screen.findByText('reg-1')).toBeTruthy(); expect(screen.getByText('123456782')).toBeTruthy(); expect(screen.getByText('reg-2')).toBeTruthy(); }); it('approves a registration (goedkeuren) and refreshes the werkbak', async () => { const { getBehandelWerkbak, postBehandelRegistrationsIdDecide, providers } = setup(); await render(WerkbakPage, { providers }); fireEvent.click((await screen.findAllByRole('button', { name: /goedkeuren/i }))[0]); expect(postBehandelRegistrationsIdDecide).toHaveBeenCalledWith('reg-1', { besluit: 'goedkeuren', }); // Reloaded after the decision: once on open, once after deciding. expect(getBehandelWerkbak).toHaveBeenCalledTimes(2); }); it('rejects a registration (afwijzen) via the decide endpoint', async () => { const { postBehandelRegistrationsIdDecide, providers } = setup(); await render(WerkbakPage, { providers }); fireEvent.click((await screen.findAllByRole('button', { name: /afwijzen/i }))[0]); expect(postBehandelRegistrationsIdDecide).toHaveBeenCalledWith('reg-1', { besluit: 'afwijzen', }); }); 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('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 }); expect(await screen.findByText(/werkbak is leeg/i)).toBeTruthy(); }); it('surfaces a load failure instead of swallowing it', async () => { const { providers } = setup({ getBehandelWerkbak: vi.fn().mockReturnValue(throwError(() => new Error('403'))), }); await render(WerkbakPage, { providers }); expect(await screen.findByText(/kon de werkbak niet laden/i)).toBeTruthy(); }); it('has no WCAG 2.1 AA violations', async () => { document.documentElement.lang = 'nl'; const { container } = await render(WerkbakPage, { providers: setup().providers }); const results = await axe(container, { runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'] }, }); expect(results.violations).toEqual([]); }); });