import { signal } from '@angular/core'; import { fireEvent, render, screen } from '@testing-library/angular'; import { of, throwError } from 'rxjs'; import { AuthService } from 'auth'; import { BffApiV1Service } from 'api-client'; import { axe } from 'vitest-axe'; import { RegistrationPage } from './registration-page'; class FakeAuth extends AuthService { readonly isAuthenticated = signal(true); readonly bsn = signal('123456782'); login(): void { /* noop */ } logout(): void { /* noop */ } } function providers( post = vi.fn().mockReturnValue(of({ registrationId: 'reg-9', status: 'Ingediend' })), withdraw = vi.fn().mockReturnValue(of(undefined)), provideDocuments = vi.fn().mockReturnValue(of(undefined)), // Resume lookup (S-26): default to 204/empty — no in-flight registration, so the submit form shows. getCurrent = vi.fn().mockReturnValue(of(undefined)), ) { return { post, withdraw, provideDocuments, getCurrent, providers: [ { provide: AuthService, useClass: FakeAuth }, { provide: BffApiV1Service, useValue: { getSelfServiceRegistrations: getCurrent, postSelfServiceRegistrations: post, postSelfServiceRegistrationsIdWithdraw: withdraw, postSelfServiceRegistrationsIdDocuments: provideDocuments, }, }, ], }; } describe('RegistrationPage', () => { it('shows the signed-in BSN', async () => { await render(RegistrationPage, { providers: providers().providers }); expect(screen.getByText(/123456782/)).toBeTruthy(); }); it('submits the registration and confirms', async () => { const { post, providers: p } = providers(); await render(RegistrationPage, { providers: p }); fireEvent.click(screen.getByRole('button', { name: /indienen/i })); expect(post).toHaveBeenCalledTimes(1); expect(await screen.findByText(/ontvangen/i)).toBeTruthy(); }); it('resumes an existing registration on load, without submitting again (S-26)', async () => { const { post, providers: p } = providers( undefined, undefined, undefined, vi.fn().mockReturnValue(of({ registrationId: 'reg-77', status: 'Ingediend' })), ); await render(RegistrationPage, { providers: p }); // The confirmation view is restored from the in-flight registration — no submit click. expect(await screen.findByText(/ontvangen/i)).toBeTruthy(); expect(screen.getByText(/reg-77/)).toBeTruthy(); expect(post).not.toHaveBeenCalled(); }); it('shows an error and keeps the submit available when the BFF call fails', async () => { const { post, providers: p } = providers(vi.fn().mockReturnValue(throwError(() => new Error('BFF rejected')))); await render(RegistrationPage, { providers: p }); fireEvent.click(screen.getByRole('button', { name: /indienen/i })); expect(post).toHaveBeenCalledTimes(1); // The failure is surfaced (not swallowed), the confirmation is not shown, and the user can retry. expect(await screen.findByRole('alert')).toBeTruthy(); expect(screen.queryByText(/ontvangen/i)).toBeNull(); expect(screen.getByRole('button', { name: /indienen/i })).toBeTruthy(); }); it('offers to withdraw after submitting, and withdrawing confirms', async () => { const { withdraw, providers: p } = providers(); await render(RegistrationPage, { providers: p }); fireEvent.click(screen.getByRole('button', { name: /indienen/i })); await screen.findByText(/ontvangen/i); fireEvent.click(await screen.findByRole('button', { name: /trek aanvraag in/i })); // The withdrawal is keyed by the reference the submit returned, and the page confirms it. expect(withdraw).toHaveBeenCalledWith('reg-9'); expect(await screen.findByText(/ingetrokken/i)).toBeTruthy(); }); // A small PDF file the citizen "uploads"; the component base64-encodes it client-side. const diploma = () => new File([new Uint8Array([1, 2, 3])], 'diploma.pdf', { type: 'application/pdf' }); it('uploads a chosen diploma after submitting, and doing so confirms', async () => { const { provideDocuments, providers: p } = providers(); await render(RegistrationPage, { providers: p }); fireEvent.click(screen.getByRole('button', { name: /indienen/i })); await screen.findByText(/ontvangen/i); // Choose the file, then upload it. fireEvent.change(screen.getByLabelText(/diploma/i), { target: { files: [diploma()] } }); fireEvent.click(await screen.findByRole('button', { name: /documenten aanleveren/i })); // The upload is keyed by the reference and carries the base64 file + its name; the page confirms. expect(await screen.findByText(/documenten.*aangeleverd/i)).toBeTruthy(); expect(provideDocuments).toHaveBeenCalledWith( 'reg-9', expect.objectContaining({ fileName: 'diploma.pdf', contentType: 'application/pdf', contentBase64: expect.any(String) }), ); }); it('surfaces a diploma-upload failure and keeps the action available', async () => { const { providers: p } = providers( vi.fn().mockReturnValue(of({ registrationId: 'reg-9', status: 'Ingediend' })), vi.fn().mockReturnValue(of(undefined)), vi.fn().mockReturnValue(throwError(() => new Error('documents rejected'))), ); await render(RegistrationPage, { providers: p }); fireEvent.click(screen.getByRole('button', { name: /indienen/i })); await screen.findByText(/ontvangen/i); fireEvent.change(screen.getByLabelText(/diploma/i), { target: { files: [diploma()] } }); fireEvent.click(await screen.findByRole('button', { name: /documenten aanleveren/i })); expect(await screen.findByRole('alert')).toBeTruthy(); expect(screen.queryByText(/aangeleverd/i)).toBeNull(); expect(screen.getByRole('button', { name: /documenten aanleveren/i })).toBeTruthy(); }); it('surfaces a withdraw failure and keeps the action available', async () => { const { providers: p } = providers( vi.fn().mockReturnValue(of({ registrationId: 'reg-9', status: 'Ingediend' })), vi.fn().mockReturnValue(throwError(() => new Error('withdraw rejected'))), ); await render(RegistrationPage, { providers: p }); fireEvent.click(screen.getByRole('button', { name: /indienen/i })); await screen.findByText(/ontvangen/i); fireEvent.click(await screen.findByRole('button', { name: /trek aanvraag in/i })); expect(await screen.findByRole('alert')).toBeTruthy(); expect(screen.queryByText(/is ingetrokken/i)).toBeNull(); expect(screen.getByRole('button', { name: /trek aanvraag in/i })).toBeTruthy(); }); it('has no WCAG 2.1 AA violations on the submit page', async () => { // The portal is Dutch; the real index.html sets lang. Set it here so the document-level // html-has-lang rule reflects the app, not the bare jsdom document. document.documentElement.lang = 'nl'; const { container } = await render(RegistrationPage, { providers: providers().providers }); const results = await axe(container, { runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'] }, }); expect(results.violations).toEqual([]); }); });