Files
strangler-fig-demo/portal-frontend/src/app/case-detail/edit-applicant-details/edit-applicant-details.spec.ts
T
eho 9e031da724 feat(portal-frontend): implement edit-applicant-details form with write-through disclaimer
Shared form for writeThrough and owned modes. Shows the ADR-002
disclaimer only in writeThrough mode (no client-side validation
stricter than legacy provides). Maps 400 ErrorsResponse to per-field
inline errors, 422/409 to a banner with the server's own text -
never a client-side switch over invariant names. Embedded in
case-detail with reload() on save so the actions block refreshes.
2026-07-31 08:59:55 +02:00

119 lines
4.8 KiB
TypeScript

import { HttpErrorResponse } from '@angular/common/http';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { of, throwError } from 'rxjs';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { CaseDetailService } from '../case-detail.service';
import { ActionLink, ApplicantDetailsRequest } from '../case-detail.types';
import { EditApplicantDetails } from './edit-applicant-details';
const initial: ApplicantDetailsRequest = {
surname: 'de Vries',
initials: 'A.',
address: { street: 'Kerkweg', number: '12', postalCode: '3512JK', city: 'Utrecht' },
email: 'anna@example.nl',
phone: null,
preferredChannel: 'Post',
};
describe('EditApplicantDetails', () => {
let fixture: ComponentFixture<EditApplicantDetails>;
let updateDetails: ReturnType<typeof vi.fn>;
function render(actionLink: ActionLink): HTMLElement {
fixture = TestBed.createComponent(EditApplicantDetails);
fixture.componentRef.setInput('actionLink', actionLink);
fixture.componentRef.setInput('initial', initial);
fixture.detectChanges();
return fixture.nativeElement as HTMLElement;
}
function submitForm(el: HTMLElement): void {
el.querySelector('form')!.dispatchEvent(new Event('submit', { cancelable: true }));
fixture.detectChanges();
}
beforeEach(async () => {
updateDetails = vi.fn();
await TestBed.configureTestingModule({
imports: [EditApplicantDetails],
providers: [{ provide: CaseDetailService, useValue: { updateDetails } }],
}).compileComponents();
});
it('shows the write-through disclaimer only in writeThrough mode', () => {
const writeThroughEl = render({ mode: 'writeThrough', href: '/api/worklist/legacy/1001/details' });
expect(writeThroughEl.querySelector('[data-testid="write-through-disclaimer"]')).toBeTruthy();
const ownedEl = render({ mode: 'owned', href: '/api/worklist/owned/abc/details' });
expect(ownedEl.querySelector('[data-testid="write-through-disclaimer"]')).toBeFalsy();
});
it('submits the form body to the given href and emits saved on success', () => {
updateDetails.mockReturnValue(of(undefined));
const el = render({ mode: 'owned', href: '/api/worklist/owned/abc/details' });
const savedSpy = vi.fn();
fixture.componentInstance.saved.subscribe(savedSpy);
submitForm(el);
expect(updateDetails).toHaveBeenCalledWith('/api/worklist/owned/abc/details', {
surname: 'de Vries',
initials: 'A.',
address: { street: 'Kerkweg', number: '12', postalCode: '3512JK', city: 'Utrecht' },
email: 'anna@example.nl',
phone: null,
preferredChannel: 'Post',
});
expect(savedSpy).toHaveBeenCalled();
});
it('maps a 400 ErrorsResponse to per-field inline errors', () => {
updateDetails.mockReturnValue(
throwError(
() =>
new HttpErrorResponse({
status: 400,
error: {
errors: [
{ field: 'surname', message: 'Achternaam is verplicht.' },
{ field: 'address.number', message: 'Huisnummer is verplicht.' },
{ field: 'address.postalCode', message: 'Ongeldige postcode.' },
],
},
}),
),
);
const el = render({ mode: 'writeThrough', href: '/api/worklist/legacy/1001/details' });
submitForm(el);
expect(el.querySelector('[data-testid="field-error-surname"]')?.textContent).toContain('Achternaam is verplicht.');
expect(el.querySelector('[data-testid="field-error-address.number"]')?.textContent).toContain('Huisnummer is verplicht.');
expect(el.querySelector('[data-testid="field-error-address.postalCode"]')?.textContent).toContain('Ongeldige postcode.');
});
it('shows a 422 InvariantViolationResponse as an invariant + message banner', () => {
updateDetails.mockReturnValue(
throwError(() => new HttpErrorResponse({ status: 422, error: { invariant: 'Address.AllPartsRequired', message: 'All address parts are required.' } })),
);
const el = render({ mode: 'owned', href: '/api/worklist/owned/abc/details' });
submitForm(el);
expect(el.querySelector('[data-testid="edit-details-banner"]')?.textContent).toContain('Address.AllPartsRequired');
expect(el.querySelector('[data-testid="edit-details-banner"]')?.textContent).toContain('All address parts are required.');
});
it('shows a 409 MessageResponse as a banner', () => {
updateDetails.mockReturnValue(
throwError(() => new HttpErrorResponse({ status: 409, error: { message: 'This aanvraag has already been migrated.' } })),
);
const el = render({ mode: 'writeThrough', href: '/api/worklist/legacy/1002/details' });
submitForm(el);
expect(el.querySelector('[data-testid="edit-details-banner"]')?.textContent).toContain('This aanvraag has already been migrated.');
});
});