From 569bf581959982580a0f2ef913cb134ec3772653 Mon Sep 17 00:00:00 2001 From: Frankie Moran Date: Fri, 20 Feb 2026 12:26:25 +0000 Subject: [PATCH 01/11] feat: enhance offence code validation and error handling in forms --- ...xed-penalty-details-form.component.spec.ts | 15 ++ ...-details-offences-field-errors.constant.ts | 10 +- ...ails-add-an-offence-form.component.spec.ts | 110 ++++++++++ ...e-details-add-an-offence-form.component.ts | 37 ++++ .../fines-mac-offence-details.service.spec.ts | 199 +++++++++++++++++- .../fines-mac-offence-details.service.ts | 95 +++++++-- 6 files changed, 450 insertions(+), 16 deletions(-) diff --git a/src/app/flows/fines/fines-mac/fines-mac-fixed-penalty-details/fines-mac-fixed-penalty-details-form/fines-mac-fixed-penalty-details-form.component.spec.ts b/src/app/flows/fines/fines-mac/fines-mac-fixed-penalty-details/fines-mac-fixed-penalty-details-form/fines-mac-fixed-penalty-details-form.component.spec.ts index 5e2f295302..58722ad0c8 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-fixed-penalty-details/fines-mac-fixed-penalty-details-form/fines-mac-fixed-penalty-details-form.component.spec.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-fixed-penalty-details/fines-mac-fixed-penalty-details-form/fines-mac-fixed-penalty-details-form.component.spec.ts @@ -229,6 +229,21 @@ describe('FinesMacFixedPenaltyFormComponent', () => { vi.useRealTimers(); }); + it('should set offenceCodeValidationPending immediately when lookup-length offence code changes', () => { + vi.useFakeTimers(); + component['setupFixedPenaltyDetailsForm'](); + component['setupOffenceCodeListener'](); + component.form.get('fm_fp_offence_details_offence_id')?.setValue(314441); + + component.form.get('fm_fp_offence_details_offence_cjs_code')?.setValue('AK12345'); + + expect(component.form.get('fm_fp_offence_details_offence_id')?.value).toBeNull(); + expect(component.form.get('fm_fp_offence_details_offence_cjs_code')?.errors).toEqual({ + offenceCodeValidationPending: true, + }); + vi.useRealTimers(); + }); + it('should set initial value if dob value already exists', () => { component['setupFixedPenaltyDetailsForm'](); component.form.get('fm_fp_personal_details_dob')?.setValue('01-01-1979'); diff --git a/src/app/flows/fines/fines-mac/fines-mac-offence-details/constants/fines-mac-offence-details-offences-field-errors.constant.ts b/src/app/flows/fines/fines-mac/fines-mac-offence-details/constants/fines-mac-offence-details-offences-field-errors.constant.ts index f6c607a3b7..9ab2423a9d 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-offence-details/constants/fines-mac-offence-details-offences-field-errors.constant.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-offence-details/constants/fines-mac-offence-details-offences-field-errors.constant.ts @@ -36,9 +36,17 @@ export const FINES_MAC_OFFENCE_DETAILS_OFFENCES_FIELD_ERRORS: IAbstractFormBaseF message: 'Offence code must be 7 or 8 characters', priority: 4, }, + offenceCodeLookupFailed: { + message: 'We could not validate the offence code. Try again', + priority: 5, + }, + offenceCodeValidationPending: { + message: 'Wait for offence code validation to complete', + priority: 6, + }, invalidOffenceCode: { message: 'Offence not found', - priority: 5, + priority: 7, }, }, }; diff --git a/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.spec.ts b/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.spec.ts index c5c7be5e88..a80df4f25d 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.spec.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.spec.ts @@ -73,6 +73,7 @@ describe('FinesMacOffenceDetailsAddAnOffenceFormComponent', () => { 'upperCaseFirstLetter', 'scrollToTop', ]); + mockUtilsService.upperCaseAllLetters.mockImplementation((value: string) => value?.toUpperCase?.() ?? value); await TestBed.configureTestingModule({ imports: [FinesMacOffenceDetailsAddAnOffenceFormComponent], @@ -729,6 +730,115 @@ describe('FinesMacOffenceDetailsAddAnOffenceFormComponent', () => { expect(superHandleFormSubmitSpy).toHaveBeenCalledWith(event); }); + it('should set offenceCodeValidationPending on submit when offence code length is valid and offence id is unresolved', () => { + const offenceCodeControl = component.form.controls['fm_offence_details_offence_cjs_code']; + const offenceIdControl = component.form.controls['fm_offence_details_offence_id']; + + offenceCodeControl.setValue('AK12345'); + offenceCodeControl.setErrors(null); + offenceIdControl.setValue(null); + + component.handleFormSubmit(new SubmitEvent('submit')); + + expect(offenceCodeControl.errors).toEqual(expect.objectContaining({ offenceCodeValidationPending: true })); + }); + + it('should preserve existing offence code errors when setting offenceCodeValidationPending', () => { + const offenceCodeControl = component.form.controls['fm_offence_details_offence_cjs_code']; + const offenceIdControl = component.form.controls['fm_offence_details_offence_id']; + + offenceCodeControl.setValue('AK12345'); + offenceCodeControl.setErrors({ customError: true }); + offenceIdControl.setValue(null); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (component as any).enforceOffenceCodeValidationBeforeSubmit(); + + expect(offenceCodeControl.errors).toEqual({ + customError: true, + offenceCodeValidationPending: true, + }); + }); + + it('should handle null offence code values without setting pending validation', () => { + const offenceCodeControl = component.form.controls['fm_offence_details_offence_cjs_code']; + const offenceIdControl = component.form.controls['fm_offence_details_offence_id']; + + offenceCodeControl.setValue(null); + offenceCodeControl.setErrors({ offenceCodeValidationPending: true }); + offenceIdControl.setValue(null); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (component as any).enforceOffenceCodeValidationBeforeSubmit(); + + expect(offenceCodeControl.errors).toBeNull(); + }); + + it('should not set offenceCodeValidationPending on submit when offence code is already invalid', () => { + const offenceCodeControl = component.form.controls['fm_offence_details_offence_cjs_code']; + const offenceIdControl = component.form.controls['fm_offence_details_offence_id']; + + offenceCodeControl.setValue('AK12345'); + offenceCodeControl.setErrors({ invalidOffenceCode: true }); + offenceIdControl.setValue(null); + + component.handleFormSubmit(new SubmitEvent('submit')); + + expect(offenceCodeControl.errors).toEqual({ invalidOffenceCode: true }); + expect(offenceCodeControl.errors?.['offenceCodeValidationPending']).toBeUndefined(); + }); + + it('should not set offenceCodeValidationPending on submit when offence lookup failed', () => { + const offenceCodeControl = component.form.controls['fm_offence_details_offence_cjs_code']; + const offenceIdControl = component.form.controls['fm_offence_details_offence_id']; + + offenceCodeControl.setValue('AK12345'); + offenceCodeControl.setErrors({ offenceCodeLookupFailed: true }); + offenceIdControl.setValue(null); + + component.handleFormSubmit(new SubmitEvent('submit')); + + expect(offenceCodeControl.errors).toEqual({ offenceCodeLookupFailed: true }); + expect(offenceCodeControl.errors?.['offenceCodeValidationPending']).toBeUndefined(); + }); + + it('should remove offenceCodeValidationPending and keep other existing errors', () => { + const offenceCodeControl = component.form.controls['fm_offence_details_offence_cjs_code']; + const offenceIdControl = component.form.controls['fm_offence_details_offence_id']; + + offenceCodeControl.setValue('AK12345'); + offenceCodeControl.setErrors({ + offenceCodeValidationPending: true, + invalidOffenceCode: true, + }); + offenceIdControl.setValue(null); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (component as any).enforceOffenceCodeValidationBeforeSubmit(); + + expect(offenceCodeControl.errors).toEqual({ invalidOffenceCode: true }); + }); + + it('should clear offenceCodeValidationPending on submit when offence id is set', () => { + const offenceCodeControl = component.form.controls['fm_offence_details_offence_cjs_code']; + const offenceIdControl = component.form.controls['fm_offence_details_offence_id']; + + offenceCodeControl.setValue('AK12345'); + offenceCodeControl.setErrors({ offenceCodeValidationPending: true }); + offenceIdControl.setValue(314441); + + component.handleFormSubmit(new SubmitEvent('submit')); + + expect(offenceCodeControl.errors?.['offenceCodeValidationPending']).toBeUndefined(); + }); + + it('should return early when offence code or offence id controls are missing', () => { + component.form.removeControl('fm_offence_details_offence_id'); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(() => (component as any).enforceOffenceCodeValidationBeforeSubmit()).not.toThrow(); + }); + it('should add a new draft offence when index is -1', () => { component.form.controls['fm_offence_details_id'].setValue('test-id'); // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.ts b/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.ts index 22fc1d9708..67c0f6a36f 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.ts @@ -480,6 +480,42 @@ export class FinesMacOffenceDetailsAddAnOffenceFormComponent } } + /** + * Ensures the offence code lookup has completed before allowing submission. + * If a 7 or 8 character code has no resolved offence id yet, a pending-validation + * error is applied so the form remains invalid and the user sees a clear message. + */ + private enforceOffenceCodeValidationBeforeSubmit(): void { + const offenceCodeControl = this.form.get('fm_offence_details_offence_cjs_code') as FormControl | null; + const offenceIdControl = this.form.get('fm_offence_details_offence_id') as FormControl | null; + + if (!offenceCodeControl || !offenceIdControl) { + return; + } + + const offenceCode = offenceCodeControl.value ?? ''; + const isLookupLength = offenceCode.length >= 7 && offenceCode.length <= 8; + const hasOffenceId = offenceIdControl.value !== null && offenceIdControl.value !== undefined; + const hasInvalidOffenceCodeError = Boolean(offenceCodeControl.errors?.['invalidOffenceCode']); + const hasOffenceCodeLookupFailedError = Boolean(offenceCodeControl.errors?.['offenceCodeLookupFailed']); + + if (isLookupLength && !hasOffenceId && !hasInvalidOffenceCodeError && !hasOffenceCodeLookupFailedError) { + const currentErrors = offenceCodeControl.errors; + const updatedErrors = currentErrors + ? { ...currentErrors, offenceCodeValidationPending: true } + : { offenceCodeValidationPending: true }; + offenceCodeControl.setErrors(updatedErrors, { emitEvent: false }); + return; + } + + const currentErrors = offenceCodeControl.errors; + if (currentErrors?.['offenceCodeValidationPending']) { + const remainingErrors = { ...currentErrors }; + delete remainingErrors['offenceCodeValidationPending']; + offenceCodeControl.setErrors(Object.keys(remainingErrors).length ? remainingErrors : null, { emitEvent: false }); + } + } + /** * Navigates to the minor creditor page for the specified row index. * @@ -610,6 +646,7 @@ export class FinesMacOffenceDetailsAddAnOffenceFormComponent */ public override handleFormSubmit(event: SubmitEvent): void { this.checkImpositionMinorCreditors(); + this.enforceOffenceCodeValidationBeforeSubmit(); super.handleFormSubmit(event); } diff --git a/src/app/flows/fines/fines-mac/fines-mac-offence-details/services/fines-mac-offence-details.service.spec.ts b/src/app/flows/fines/fines-mac/fines-mac-offence-details/services/fines-mac-offence-details.service.spec.ts index ce71eb0d95..786c63ef3e 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-offence-details/services/fines-mac-offence-details.service.spec.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-offence-details/services/fines-mac-offence-details.service.spec.ts @@ -3,7 +3,7 @@ import { TestBed } from '@angular/core/testing'; import { FinesMacOffenceDetailsService } from './fines-mac-offence-details.service'; import { FINES_MAC_OFFENCE_DETAILS_FORM_MOCK } from '../mocks/fines-mac-offence-details-form.mock'; import { FormControl, FormGroup } from '@angular/forms'; -import { Observable, of, Subject } from 'rxjs'; +import { Observable, of, Subject, throwError } from 'rxjs'; import { FINES_MAC_OFFENCE_DETAILS_DEFAULT_VALUES } from '../constants/fines-mac-offence-details-default-values.constant'; import { IOpalFinesOffencesRefData } from '@services/fines/opal-fines-service/interfaces/opal-fines-offences-ref-data.interface'; import { provideHttpClient } from '@angular/common/http'; @@ -100,6 +100,31 @@ describe('FinesMacOffenceDetailsService', () => { expect(result[0].formData.fm_offence_details_impositions[0]).toEqual(expected); }); + it('setControlError - should remove one error key and keep remaining errors', () => { + const control = new FormControl('code'); + control.setErrors({ + invalidOffenceCode: true, + customError: true, + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (service as any).setControlError(control, 'invalidOffenceCode', false); + + expect(control.errors).toEqual({ customError: true }); + }); + + it('setControlError - should clear all errors when removing the final error key', () => { + const control = new FormControl('code'); + control.setErrors({ + invalidOffenceCode: true, + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (service as any).setControlError(control, 'invalidOffenceCode', false); + + expect(control.errors).toBeNull(); + }); + describe('initOffenceListener', () => { let form: FormGroup; let destroy$: Subject; @@ -178,6 +203,93 @@ describe('FinesMacOffenceDetailsService', () => { expect(onConfirmChangeSpy).toHaveBeenCalledWith(true); }); + it('should clear offence id and set confirmation to false immediately when code changes', () => { + vi.useFakeTimers(); + form.get('id')?.setValue(314441); + + service.initOffenceCodeListener( + form, + 'code', + 'id', + destroy$, + getOffenceByCjsCode, + onResultSpy, + onConfirmChangeSpy, + ); + + form.get('code')?.setValue('xy98765'); + + expect(form.get('id')?.value).toBeNull(); + expect(form.get('code')?.errors).toEqual({ offenceCodeValidationPending: true }); + expect(onConfirmChangeSpy).toHaveBeenCalledWith(false); + }); + + it('should not set pending validation error immediately for short codes', () => { + vi.useFakeTimers(); + form.get('id')?.setValue(314441); + + service.initOffenceCodeListener( + form, + 'code', + 'id', + destroy$, + getOffenceByCjsCode, + onResultSpy, + onConfirmChangeSpy, + ); + + form.get('code')?.setValue('xy98'); + + expect(form.get('id')?.value).toBeNull(); + expect(form.get('code')?.errors).toBeNull(); + expect(onConfirmChangeSpy).toHaveBeenCalledWith(false); + }); + + it('should ignore stale offence lookup responses when code changes quickly', () => { + vi.useFakeTimers(); + + const firstLookup$ = new Subject(); + const secondLookup$ = new Subject(); + getOffenceByCjsCode = vi.fn((code: string) => { + return code === 'AB12345' ? firstLookup$.asObservable() : secondLookup$.asObservable(); + }); + + const staleResponse: IOpalFinesOffencesRefData = { + ...offenceMockResponse, + refData: [{ ...offenceMockResponse.refData[0], offence_id: 111111, get_cjs_code: 'AB12345' }], + }; + const latestResponse: IOpalFinesOffencesRefData = { + ...offenceMockResponse, + refData: [{ ...offenceMockResponse.refData[0], offence_id: 222222, get_cjs_code: 'CD12345' }], + }; + + service.initOffenceCodeListener( + form, + 'code', + 'id', + destroy$, + getOffenceByCjsCode, + onResultSpy, + onConfirmChangeSpy, + ); + + form.get('code')?.setValue('ab12345'); + vi.advanceTimersByTime(FINES_MAC_OFFENCE_DETAILS_DEFAULT_VALUES.defaultDebounceTime); + + form.get('code')?.setValue('cd12345'); + vi.advanceTimersByTime(FINES_MAC_OFFENCE_DETAILS_DEFAULT_VALUES.defaultDebounceTime); + + firstLookup$.next(staleResponse); + expect(form.get('id')?.value).toBeNull(); + expect(onResultSpy).not.toHaveBeenCalled(); + + secondLookup$.next(latestResponse); + expect(form.get('id')?.value).toBe(222222); + expect(onResultSpy).toHaveBeenCalledTimes(1); + expect(onResultSpy).toHaveBeenCalledWith(latestResponse); + expect(onConfirmChangeSpy).toHaveBeenCalledWith(true); + }); + it('should mark code as invalid when response count is 0', () => { vi.useFakeTimers(); const invalidResponse = { count: 0, refData: [] }; @@ -201,6 +313,91 @@ describe('FinesMacOffenceDetailsService', () => { expect(onConfirmChangeSpy).toHaveBeenCalledWith(true); }); + it('should mark code as invalid when response count is greater than 1', () => { + vi.useFakeTimers(); + const multipleResponse: IOpalFinesOffencesRefData = { + count: 2, + refData: [offenceMockResponse.refData[0], { ...offenceMockResponse.refData[0], offence_id: 123456 }], + }; + getOffenceByCjsCode = () => of(multipleResponse); + + service.initOffenceCodeListener( + form, + 'code', + 'id', + destroy$, + getOffenceByCjsCode, + onResultSpy, + onConfirmChangeSpy, + ); + + form.get('code')?.setValue('zz99999'); + vi.advanceTimersByTime(FINES_MAC_OFFENCE_DETAILS_DEFAULT_VALUES.defaultDebounceTime); + + expect(form.get('code')?.errors).toEqual({ invalidOffenceCode: true }); + expect(form.get('id')?.value).toBeNull(); + expect(onConfirmChangeSpy).toHaveBeenCalledWith(true); + }); + + it('should clear pending error and set lookup failed error when offence lookup request fails', () => { + vi.useFakeTimers(); + getOffenceByCjsCode = () => throwError(() => new Error('request failed')); + + service.initOffenceCodeListener( + form, + 'code', + 'id', + destroy$, + getOffenceByCjsCode, + onResultSpy, + onConfirmChangeSpy, + ); + + form.get('code')?.setValue('zz99999'); + vi.advanceTimersByTime(FINES_MAC_OFFENCE_DETAILS_DEFAULT_VALUES.defaultDebounceTime); + + expect(form.get('code')?.errors).toEqual({ offenceCodeLookupFailed: true }); + expect(form.get('id')?.value).toBeNull(); + expect(onResultSpy).not.toHaveBeenCalled(); + expect(onConfirmChangeSpy).toHaveBeenLastCalledWith(false); + }); + + it('should ignore stale lookup failures from previous offence code values', () => { + vi.useFakeTimers(); + + const firstLookup$ = new Subject(); + const secondLookup$ = new Subject(); + getOffenceByCjsCode = vi.fn((code: string) => { + return code === 'AB12345' ? firstLookup$.asObservable() : secondLookup$.asObservable(); + }); + + service.initOffenceCodeListener( + form, + 'code', + 'id', + destroy$, + getOffenceByCjsCode, + onResultSpy, + onConfirmChangeSpy, + ); + + form.get('code')?.setValue('ab12345'); + vi.advanceTimersByTime(FINES_MAC_OFFENCE_DETAILS_DEFAULT_VALUES.defaultDebounceTime); + + form.get('code')?.setValue('cd12345'); + vi.advanceTimersByTime(FINES_MAC_OFFENCE_DETAILS_DEFAULT_VALUES.defaultDebounceTime); + + firstLookup$.error(new Error('stale failure')); + + expect(form.get('code')?.errors).toEqual({ offenceCodeValidationPending: true }); + expect(onConfirmChangeSpy).toHaveBeenCalledTimes(4); + expect(onResultSpy).not.toHaveBeenCalled(); + + secondLookup$.next(offenceMockResponse); + expect(form.get('id')?.value).toBe(314441); + expect(form.get('code')?.errors).toBeNull(); + }); + it('should not call populateHint for short code', () => { vi.useFakeTimers(); service.initOffenceCodeListener( diff --git a/src/app/flows/fines/fines-mac/fines-mac-offence-details/services/fines-mac-offence-details.service.ts b/src/app/flows/fines/fines-mac/fines-mac-offence-details/services/fines-mac-offence-details.service.ts index 20c41ec6f1..64ea38be01 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-offence-details/services/fines-mac-offence-details.service.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-offence-details/services/fines-mac-offence-details.service.ts @@ -1,16 +1,43 @@ import { inject, Injectable } from '@angular/core'; import { IFinesMacOffenceDetailsForm } from '../interfaces/fines-mac-offence-details-form.interface'; -import { FormGroup } from '@angular/forms'; -import { debounceTime, distinctUntilChanged, Observable, Subject, takeUntil, tap } from 'rxjs'; +import { FormControl, FormGroup } from '@angular/forms'; +import { debounceTime, distinctUntilChanged, Observable, Subject, takeUntil, tap, catchError, EMPTY } from 'rxjs'; import { FINES_MAC_OFFENCE_DETAILS_DEFAULT_VALUES } from '../constants/fines-mac-offence-details-default-values.constant'; import { UtilsService } from '@hmcts/opal-frontend-common/services/utils-service'; import { IOpalFinesOffencesRefData } from '@services/fines/opal-fines-service/interfaces/opal-fines-offences-ref-data.interface'; @Injectable({ - providedIn: 'any', + providedIn: 'root', }) export class FinesMacOffenceDetailsService { public utilsService = inject(UtilsService); + + /** + * Adds or removes a single custom error key while preserving any other control errors. + * + * @param control - The control to update. + * @param errorKey - The custom error key to add or remove. + * @param hasError - True to set the error key, false to clear it. + */ + private setControlError(control: FormControl, errorKey: string, hasError: boolean): void { + const currentErrors = control.errors ?? {}; + + if (hasError) { + if (!currentErrors[errorKey]) { + control.setErrors({ ...currentErrors, [errorKey]: true }, { emitEvent: false }); + } + return; + } + + if (!currentErrors[errorKey]) { + return; + } + + const remainingErrors = { ...currentErrors }; + delete remainingErrors[errorKey]; + control.setErrors(Object.keys(remainingErrors).length ? remainingErrors : null, { emitEvent: false }); + } + /** * Reorders the imposition keys to maintain correct numbering. * @@ -135,48 +162,88 @@ export class FinesMacOffenceDetailsService { onResult: (result: any) => void, onConfirmChange?: (confirmed: boolean) => void, ): void { - const codeControl = form.controls[codeControlName]; - const idControl = form.controls[idControlName]; + const codeControl = form.controls[codeControlName] as FormControl; + const idControl = form.controls[idControlName] as FormControl; + let latestLookupRequest = 0; const populateHint = (code: string) => { - idControl.setValue(null); + const lookupRequest = ++latestLookupRequest; + idControl.setValue(null, { emitEvent: false }); + this.setControlError(codeControl, 'invalidOffenceCode', false); + this.setControlError(codeControl, 'offenceCodeLookupFailed', false); if (code?.length >= 7 && code?.length <= 8) { + this.setControlError(codeControl, 'offenceCodeValidationPending', true); + if (onConfirmChange) onConfirmChange(false); + const result$ = getOffenceByCjsCode(code).pipe( tap((response) => { - codeControl.setErrors(response.count === 0 ? { invalidOffenceCode: true } : null, { emitEvent: false }); + // Ignore stale responses that return after the user has changed the code. + if (lookupRequest !== latestLookupRequest || codeControl.value !== code) { + return; + } + + this.setControlError(codeControl, 'offenceCodeValidationPending', false); + this.setControlError(codeControl, 'offenceCodeLookupFailed', false); + this.setControlError(codeControl, 'invalidOffenceCode', response.count !== 1); idControl.setValue(response.count === 1 ? response.refData[0].offence_id : null, { emitEvent: false }); if (typeof onResult === 'function') { onResult(response); } + + if (onConfirmChange) onConfirmChange(true); + }), + catchError(() => { + // Ignore stale failures for previous lookups. + if (lookupRequest !== latestLookupRequest || codeControl.value !== code) { + return EMPTY; + } + + this.setControlError(codeControl, 'offenceCodeValidationPending', false); + this.setControlError(codeControl, 'invalidOffenceCode', false); + this.setControlError(codeControl, 'offenceCodeLookupFailed', true); + idControl.setValue(null, { emitEvent: false }); + if (onConfirmChange) onConfirmChange(false); + return EMPTY; }), takeUntil(destroy$), ); result$.subscribe(); - if (onConfirmChange) onConfirmChange(true); - } else if (onConfirmChange) { - onConfirmChange(false); + } else { + this.setControlError(codeControl, 'offenceCodeValidationPending', false); + this.setControlError(codeControl, 'offenceCodeLookupFailed', false); + if (onConfirmChange) onConfirmChange(false); } }; if (codeControl.value) { - populateHint(codeControl.value); + const upperCasedCode = this.utilsService.upperCaseAllLetters(codeControl.value); + codeControl.setValue(upperCasedCode, { emitEvent: false }); + populateHint(upperCasedCode); } codeControl.valueChanges .pipe( distinctUntilChanged(), tap((code: string) => { - code = this.utilsService.upperCaseAllLetters(code); - codeControl.setValue(code, { emitEvent: false }); + const upperCasedCode = this.utilsService.upperCaseAllLetters(code); + const isLookupLength = upperCasedCode?.length >= 7 && upperCasedCode?.length <= 8; + codeControl.setValue(upperCasedCode, { emitEvent: false }); + // Invalidate any in-flight lookup as soon as the input changes. + latestLookupRequest++; + idControl.setValue(null, { emitEvent: false }); + this.setControlError(codeControl, 'offenceCodeValidationPending', isLookupLength); + this.setControlError(codeControl, 'invalidOffenceCode', false); + this.setControlError(codeControl, 'offenceCodeLookupFailed', false); + if (onConfirmChange) onConfirmChange(false); }), debounceTime(FINES_MAC_OFFENCE_DETAILS_DEFAULT_VALUES.defaultDebounceTime), takeUntil(destroy$), ) .subscribe((code: string) => { - populateHint(code); + populateHint(this.utilsService.upperCaseAllLetters(code)); }); } } From 6f88df8fe943e2cb1f6dcfc22753692e6d5c712d Mon Sep 17 00:00:00 2001 From: Christopher Garratt Date: Fri, 27 Feb 2026 11:39:36 +0000 Subject: [PATCH 02/11] Updating link class style and adding href to mac account details and search offence. --- .../fines-mac-account-details.component.html | 27 ++++++++++++------- ...-fixed-penalty-details-form.component.html | 2 +- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/app/flows/fines/fines-mac/fines-mac-account-details/fines-mac-account-details.component.html b/src/app/flows/fines/fines-mac/fines-mac-account-details/fines-mac-account-details.component.html index 44deb969dc..78bd0eded0 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-account-details/fines-mac-account-details.component.html +++ b/src/app/flows/fines/fines-mac/fines-mac-account-details/fines-mac-account-details.component.html @@ -168,7 +168,8 @@

Check and submit

> Check and submit > Check and submit > Check and submit > Check and submit > Check and submit > Check and submit > Check and submit @if (canAccessPaymentTerms()) { Check and submit > Offence Details > For example, HY35014. If you don't know the offence code, you can Date: Thu, 5 Mar 2026 17:01:48 +0000 Subject: [PATCH 03/11] Amending styling on mac account details links to fit design. Adding related tests. --- .../fines-mac-account-details.component.html | 57 ++++-------- ...ines-mac-account-details.component.spec.ts | 90 +++++++++++++++++++ 2 files changed, 109 insertions(+), 38 deletions(-) diff --git a/src/app/flows/fines/fines-mac/fines-mac-account-details/fines-mac-account-details.component.html b/src/app/flows/fines/fines-mac/fines-mac-account-details/fines-mac-account-details.component.html index 78bd0eded0..7216d6b6de 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-account-details/fines-mac-account-details.component.html +++ b/src/app/flows/fines/fines-mac/fines-mac-account-details/fines-mac-account-details.component.html @@ -137,9 +137,8 @@

Check and submit


Delete account } @@ -168,11 +167,9 @@

Check and submit

> Contact details @@ -197,11 +194,9 @@

Check and submit

>
Employer details @@ -226,11 +221,9 @@

Check and submit

>
Company details @@ -255,11 +248,9 @@

Check and submit

>
Personal details @@ -285,11 +276,9 @@

Check and submit

>
Court details @@ -315,11 +304,9 @@

Check and submit

>
Parent or guardian details @@ -344,11 +331,9 @@

Check and submit

>
Offence details @@ -387,11 +372,9 @@

Check and submit

@if (canAccessPaymentTerms()) {
Payment terms @@ -422,11 +405,9 @@

Check and submit

>
Account comments and notes diff --git a/src/app/flows/fines/fines-mac/fines-mac-account-details/fines-mac-account-details.component.spec.ts b/src/app/flows/fines/fines-mac/fines-mac-account-details/fines-mac-account-details.component.spec.ts index 57fb2335c6..51c1cbfa8e 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-account-details/fines-mac-account-details.component.spec.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-account-details/fines-mac-account-details.component.spec.ts @@ -93,6 +93,96 @@ describe('FinesMacAccountDetailsComponent', () => { expect(component).toBeTruthy(); }); + it('should enforce template link attributes and classes for action links', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const templateConsts = ((FinesMacAccountDetailsComponent as any).ɵcmp?.consts ?? []).filter((entry: unknown) => + Array.isArray(entry), + ) as unknown[][]; + const templateFunction = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ((FinesMacAccountDetailsComponent as any).ɵcmp?.template?.toString() as string | undefined) ?? ''; + + const findConst = (value: string) => templateConsts.find((entry) => entry.includes(value)); + + const deleteAccountConst = findConst('govuk-error-colour'); + expect(deleteAccountConst).toBeTruthy(); + expect(deleteAccountConst).toContain('href'); + expect(deleteAccountConst).toContain(''); + expect(deleteAccountConst).toContain('click'); + + const actionLinkAriaIds = [ + 'courtDetailsStatus', + 'personalDetailsStatus', + 'contactDetailsStatus', + 'employerDetailsStatus', + 'offenceDetailsStatus', + 'paymentTermsStatus', + 'accountCommentsAndNotesStatus', + ]; + + actionLinkAriaIds.forEach((ariaId) => { + const linkConst = templateConsts.find( + (entry) => entry.includes('aria-describedby') && entry.includes(ariaId) && entry.includes('click'), + ); + + expect(linkConst).toBeTruthy(); + expect(linkConst).toContain('href'); + expect(linkConst).toContain(''); + expect(linkConst).toContain('govuk-task-list__link'); + expect(linkConst).toContain('govuk-link--no-visited-state'); + expect(linkConst).not.toContain('tabindex'); + }); + + expect(templateFunction).not.toContain('keydown.enter'); + expect(templateFunction).not.toContain('keyup.enter'); + }); + + it.each([ + { linkText: 'Delete account', route: 'deleteAccountConfirmation' }, + { linkText: 'Court details', route: 'courtDetails' }, + { linkText: 'Personal details', route: 'personalDetails' }, + { linkText: 'Contact details', route: 'contactDetails' }, + { linkText: 'Employer details', route: 'employerDetails' }, + { linkText: 'Offence details', route: 'offenceDetails' }, + { linkText: 'Payment terms', route: 'paymentTerms' }, + { + linkText: 'Account comments and notes', + route: 'accountCommentsNotes', + }, + ])('should pass $event and preserve navigation logic for %s', ({ linkText, route }) => { + // Force all requested links to render in a single view state. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + vi.spyOn(component, 'canAccessPaymentTerms').mockReturnValue(true); + const finesMacState = structuredClone(FINES_MAC_STATE); + finesMacState.accountDetails.formData = { + ...structuredClone(FINES_MAC_ACCOUNT_DETAILS_STATE), + fm_create_account_defendant_type: FINES_MAC_DEFENDANT_TYPES_KEYS.adultOrYouthOnly, + }; + finesMacStore.setFinesMacStore(finesMacState); + component['setDefendantType'](); + finesDraftStore.setAmend(false); + fixture.detectChanges(); + + const allLinks = Array.from(fixture.nativeElement.querySelectorAll('a.govuk-link')) as HTMLAnchorElement[]; + const link = allLinks.find((candidate) => candidate.textContent?.trim() === linkText) ?? null; + expect(link).toBeTruthy(); + if (!link) throw new Error(`Link not found: ${linkText}`); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const routerNavigateSpy = vi.spyOn(component, 'routerNavigate'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const routerSpy = vi.spyOn(component['router'], 'navigate'); + const event = new MouseEvent('click', { bubbles: true, cancelable: true }); + const routePath = + component['fineMacRoutes'].children[route as keyof (typeof component)['fineMacRoutes']['children']]; + + link.dispatchEvent(event); + + expect(routerNavigateSpy).toHaveBeenCalledWith(routePath, false, event); + expect(event.defaultPrevented).toBe(true); + expect(routerSpy).toHaveBeenCalledWith([routePath], { relativeTo: component['activatedRoute'].parent }); + }); + it('should navigate back on navigateBack', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const routerSpy = vi.spyOn(component['router'], 'navigate'); From 47ea9b3ce8f3f90d4c416d7b50ce8fcb6e8544ff Mon Sep 17 00:00:00 2001 From: Christopher Garratt Date: Wed, 11 Mar 2026 14:37:20 +0000 Subject: [PATCH 04/11] Applying govuk-link changes to the rest of the application --- ...ant-details-at-a-glance-tab.component.html | 14 +-- ...-details-at-a-glance-tab.component.spec.ts | 81 +++++++++++++++- ...ndant-details-at-a-glance-tab.component.ts | 8 +- ...ant-details-enforcement-tab.component.html | 6 +- ...arty-add-amend-convert-form.component.html | 14 +-- ...y-add-amend-convert-form.component.spec.ts | 92 ++++++++++++++++++ ...-payment-terms-amend-denied.component.html | 9 +- ...yment-terms-amend-denied.component.spec.ts | 49 +++++++++- ...cc-payment-terms-amend-denied.component.ts | 4 +- ...-payment-card-access-denied.component.html | 9 +- ...yment-card-access-denied.component.spec.ts | 44 +++++++++ ...raft-create-and-manage-tabs.component.html | 7 +- ...t-create-and-manage-tabs.component.spec.ts | 34 +++++++ ...-draft-create-and-manage-tabs.component.ts | 4 +- .../fines-draft-table-wrapper.component.html | 15 +-- ...ines-draft-table-wrapper.component.spec.ts | 97 +++++++++++++++++++ .../fines-draft-table-wrapper.component.ts | 8 +- .../fines-mac-account-details.component.html | 2 +- ...es-mac-company-details-form.component.html | 5 +- ...mac-company-details-form.component.spec.ts | 54 +++++++++++ ...details-add-an-offence-form.component.html | 10 +- ...ails-add-an-offence-form.component.spec.ts | 77 +++++++++++++++ ...e-details-add-an-offence-form.component.ts | 8 +- ...s-review-offence-imposition.component.html | 5 +- ...eview-offence-imposition.component.spec.ts | 59 +++++++++++ ...ils-review-offence-imposition.component.ts | 9 +- ...ences-results-table-wrapper.component.html | 5 +- ...es-results-table-wrapper.component.spec.ts | 40 ++++++++ ...ffences-results-table-wrapper.component.ts | 4 +- ...arent-guardian-details-form.component.html | 5 +- ...nt-guardian-details-form.component.spec.ts | 54 +++++++++++ ...s-mac-personal-details-form.component.html | 5 +- ...ac-personal-details-form.component.spec.ts | 54 +++++++++++ ...eview-account-decision-form.component.html | 7 +- ...ew-account-decision-form.component.spec.ts | 20 ++++ .../fines-mac-review-account.component.html | 8 +- ...fines-mac-review-account.component.spec.ts | 56 ++++++++++- .../fines-mac-review-account.component.ts | 1 + ...nes-mac-submit-confirmation.component.html | 14 +-- ...-mac-submit-confirmation.component.spec.ts | 80 +++++++++++++++ ...fines-mac-submit-confirmation.component.ts | 8 +- ...lts-defendant-table-wrapper.component.html | 5 +- ...-defendant-table-wrapper.component.spec.ts | 59 +++++++++++ ...sults-defendant-table-wrapper.component.ts | 4 +- ...inor-creditor-table-wrapper.component.html | 10 +- ...r-creditor-table-wrapper.component.spec.ts | 62 ++++++++++++ ...-minor-creditor-table-wrapper.component.ts | 4 +- .../fines-sa-results.component.html | 8 +- .../fines-sa-results.component.spec.ts | 62 ++++++++++++ .../fines-sa-results.component.ts | 5 +- ...ccount-form-major-creditors.component.html | 8 +- ...unt-form-major-creditors.component.spec.ts | 60 ++++++++++++ ...-account-form-major-creditors.component.ts | 10 ++ .../fines-sa-search-problem.component.html | 2 +- .../fines-sa-search-problem.component.spec.ts | 55 +++++++++++ .../fines-sa-search-problem.component.ts | 5 +- 56 files changed, 1303 insertions(+), 141 deletions(-) diff --git a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-at-a-glance-tab/fines-acc-defendant-details-at-a-glance-tab.component.html b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-at-a-glance-tab/fines-acc-defendant-details-at-a-glance-tab.component.html index 6d6bf36d3a..409d2f89ac 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-at-a-glance-tab/fines-acc-defendant-details-at-a-glance-tab.component.html +++ b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-at-a-glance-tab/fines-acc-defendant-details-at-a-glance-tab.component.html @@ -244,22 +244,12 @@

Free text notes

} @if (hasAccountMaintenencePermission) {

- Change + Change

} } @else if (hasAccountMaintenencePermission) {

- Add comments

diff --git a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-at-a-glance-tab/fines-acc-defendant-details-at-a-glance-tab.component.spec.ts b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-at-a-glance-tab/fines-acc-defendant-details-at-a-glance-tab.component.spec.ts index 8a8c71b463..07aa0bbca5 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-at-a-glance-tab/fines-acc-defendant-details-at-a-glance-tab.component.spec.ts +++ b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-at-a-glance-tab/fines-acc-defendant-details-at-a-glance-tab.component.spec.ts @@ -23,10 +23,83 @@ describe('FinesAccDefendantDetailsAtAGlanceTabComponent', () => { expect(component).toBeTruthy(); }); - it('should handle add comments click', () => { + it('should prevent default and emit addComments in handleAddComments', () => { + const event = new Event('click'); + const preventDefaultSpy = vi.spyOn(event, 'preventDefault'); // eslint-disable-next-line @typescript-eslint/no-explicit-any - vi.spyOn(component.addComments, 'emit'); - component.handleAddComments(); - expect(component.addComments.emit).toHaveBeenCalled(); + const emitSpy = vi.spyOn(component.addComments, 'emit'); + + component.handleAddComments(event); + + expect(preventDefaultSpy).toHaveBeenCalled(); + expect(emitSpy).toHaveBeenCalled(); + }); + + it('should enforce action link template metadata', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const templateConsts = ((FinesAccDefendantDetailsAtAGlanceTabComponent as any).ɵcmp?.consts ?? []).filter( + (entry: unknown) => Array.isArray(entry), + ) as unknown[][]; + const templateFunction = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ((FinesAccDefendantDetailsAtAGlanceTabComponent as any).ɵcmp?.template?.toString() as string | undefined) ?? ''; + const actionLinkConsts = templateConsts.filter( + (entry) => + entry.includes('govuk-link') && + entry.includes('govuk-link--no-visited-state') && + entry.includes('href') && + entry.includes('click'), + ); + + expect(actionLinkConsts.length).toBeGreaterThanOrEqual(1); + actionLinkConsts.forEach((entry) => expect(entry).not.toContain('tabindex')); + expect(templateFunction).not.toContain('keydown.enter'); + expect(templateFunction).not.toContain('keyup.enter'); + }); + + it.each([ + { linkText: 'Change', hasComments: true }, + { linkText: 'Add comments', hasComments: false }, + ])('should pass $event and preserve logic for $linkText link', ({ linkText, hasComments }) => { + const tabData = structuredClone(OPAL_FINES_ACCOUNT_DEFENDANT_AT_A_GLANCE_MOCK); + + tabData.comments_and_notes = hasComments + ? { + account_comment: 'has comment', + free_text_note_1: tabData.comments_and_notes?.free_text_note_1 ?? null, + free_text_note_2: tabData.comments_and_notes?.free_text_note_2 ?? null, + free_text_note_3: tabData.comments_and_notes?.free_text_note_3 ?? null, + } + : { + account_comment: null, + free_text_note_1: null, + free_text_note_2: null, + free_text_note_3: null, + }; + + fixture.componentRef.setInput('tabData', tabData); + fixture.componentRef.setInput('hasAccountMaintenencePermission', true); + fixture.detectChanges(); + + const actionLinks = Array.from(fixture.nativeElement.querySelectorAll('a.govuk-link')) as HTMLAnchorElement[]; + const link = actionLinks.find((anchor) => anchor.textContent?.trim() === linkText) ?? null; + expect(link).toBeTruthy(); + if (!link) throw new Error(`Link not found: ${linkText}`); + + expect(link.classList.contains('govuk-link--no-visited-state')).toBe(true); + expect(link.getAttribute('href')).toBe(''); + expect(link.getAttribute('tabindex')).toBeNull(); + + const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handleAddCommentsSpy = vi.spyOn(component, 'handleAddComments'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const emitSpy = vi.spyOn(component.addComments, 'emit'); + + link.dispatchEvent(clickEvent); + + expect(handleAddCommentsSpy).toHaveBeenCalledWith(clickEvent); + expect(clickEvent.defaultPrevented).toBe(true); + expect(emitSpy).toHaveBeenCalled(); }); }); diff --git a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-at-a-glance-tab/fines-acc-defendant-details-at-a-glance-tab.component.ts b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-at-a-glance-tab/fines-acc-defendant-details-at-a-glance-tab.component.ts index 5eceff8df1..85a77935c5 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-at-a-glance-tab/fines-acc-defendant-details-at-a-glance-tab.component.ts +++ b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-at-a-glance-tab/fines-acc-defendant-details-at-a-glance-tab.component.ts @@ -34,7 +34,13 @@ export class FinesAccDefendantDetailsAtAGlanceTabComponent { public readonly languages = FINES_MAC_LANGUAGE_PREFERENCES_OPTIONS; public readonly debtorTypes = FINES_ACC_DEBTOR_TYPES; - public handleAddComments(): void { + /** + * Emits the add comments action for the current tab. + * + * @param event - The optional DOM event that triggered the action. + */ + public handleAddComments(event?: Event): void { + event?.preventDefault(); this.addComments.emit(); } } diff --git a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-enforcement-tab/fines-acc-defendant-details-enforcement-tab.component.html b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-enforcement-tab/fines-acc-defendant-details-enforcement-tab.component.html index ddf74622bb..c6f56e53a9 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-enforcement-tab/fines-acc-defendant-details-enforcement-tab.component.html +++ b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-enforcement-tab/fines-acc-defendant-details-enforcement-tab.component.html @@ -328,7 +328,7 @@

Enforcement status

Actions

@if (hasEnterEnforcementPermission) {

- Add enforcement action + Add enforcement action

} @if ( @@ -336,11 +336,11 @@

Actions

!tabData.enforcement_override.enforcement_override_result.enforcement_override_result_id ) {

- Add enforcement override + Add enforcement override

}

- Request an HMRC check + Request an HMRC check

} diff --git a/src/app/flows/fines/fines-acc/fines-acc-party-add-amend-convert/fines-acc-party-add-amend-convert-form/fines-acc-party-add-amend-convert-form.component.html b/src/app/flows/fines/fines-acc/fines-acc-party-add-amend-convert/fines-acc-party-add-amend-convert-form/fines-acc-party-add-amend-convert-form.component.html index 6bf7655a37..63dbf6fa68 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-party-add-amend-convert/fines-acc-party-add-amend-convert-form/fines-acc-party-add-amend-convert-form.component.html +++ b/src/app/flows/fines/fines-acc/fines-acc-party-add-amend-convert/fines-acc-party-add-amend-convert-form/fines-acc-party-add-amend-convert-form.component.html @@ -85,9 +85,10 @@
Remove @@ -144,11 +145,10 @@
Remove diff --git a/src/app/flows/fines/fines-acc/fines-acc-party-add-amend-convert/fines-acc-party-add-amend-convert-form/fines-acc-party-add-amend-convert-form.component.spec.ts b/src/app/flows/fines/fines-acc/fines-acc-party-add-amend-convert/fines-acc-party-add-amend-convert-form/fines-acc-party-add-amend-convert-form.component.spec.ts index a05a89c30f..5da625d59a 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-party-add-amend-convert/fines-acc-party-add-amend-convert-form/fines-acc-party-add-amend-convert-form.component.spec.ts +++ b/src/app/flows/fines/fines-acc/fines-acc-party-add-amend-convert/fines-acc-party-add-amend-convert-form/fines-acc-party-add-amend-convert-form.component.spec.ts @@ -72,6 +72,98 @@ describe('FinesAccPartyAddAmendConvertFormComponent', () => { expect(component).toBeTruthy(); }); + it('should enforce remove link template semantics', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const templateConsts = ((FinesAccPartyAddAmendConvertFormComponent as any).ɵcmp?.consts ?? []).filter( + (entry: unknown) => Array.isArray(entry), + ) as unknown[][]; + const templateFunction = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ((FinesAccPartyAddAmendConvertFormComponent as any).ɵcmp?.template?.toString() as string | undefined) ?? ''; + const removeLinkConsts = templateConsts.filter( + (entry) => + entry.includes('govuk-link') && + entry.includes('govuk-link--no-visited-state') && + entry.includes('href') && + entry.includes('click'), + ); + + expect(removeLinkConsts.length).toBeGreaterThanOrEqual(1); + removeLinkConsts.forEach((entry) => { + expect(entry).toContain('href'); + expect(entry).toContain(''); + expect(entry).not.toContain('tabindex'); + }); + expect(templateFunction).not.toContain('keydown.enter'); + expect(templateFunction).not.toContain('keyup.enter'); + }); + + it('should render individual remove alias link with href and pass $event into removeAlias', () => { + component.partyType = 'individual'; + fixture.detectChanges(); + + component.form.get('facc_party_add_amend_convert_add_alias')?.setValue(true); + while (component.aliasControls.length < 2) { + component.addAlias(component.aliasControls.length, 'facc_party_add_amend_convert_individual_aliases'); + } + fixture.detectChanges(); + + const link = + (Array.from( + fixture.nativeElement.querySelectorAll('a.govuk-link.govuk-link--no-visited-state') as NodeListOf, + ).find((anchor) => anchor.textContent?.trim().startsWith('Remove')) as HTMLAnchorElement | undefined) ?? null; + expect(link).toBeTruthy(); + if (!link) throw new Error('Individual remove alias link not found'); + + expect(link.getAttribute('href')).toBe(''); + expect(link.getAttribute('tabindex')).toBeNull(); + + const expectedIndex = component.aliasControls.length - 1; + const event = new MouseEvent('click', { bubbles: true, cancelable: true }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const removeAliasSpy = vi.spyOn(component, 'removeAlias'); + + link.dispatchEvent(event); + + expect(removeAliasSpy).toHaveBeenCalledWith(expectedIndex, 'facc_party_add_amend_convert_individual_aliases', event); + expect(event.defaultPrevented).toBe(true); + }); + + it('should render company remove alias link with href and pass $event into removeAlias', () => { + component.partyType = 'company'; + fixture.detectChanges(); + + component.form.get('facc_party_add_amend_convert_add_alias')?.setValue(true); + while (component.aliasControls.length < 2) { + component.addAlias(component.aliasControls.length, 'facc_party_add_amend_convert_organisation_aliases'); + } + fixture.detectChanges(); + + const link = + (Array.from( + fixture.nativeElement.querySelectorAll('a.govuk-link.govuk-link--no-visited-state') as NodeListOf, + ).find((anchor) => anchor.textContent?.trim().startsWith('Remove')) as HTMLAnchorElement | undefined) ?? null; + expect(link).toBeTruthy(); + if (!link) throw new Error('Company remove alias link not found'); + + expect(link.getAttribute('href')).toBe(''); + expect(link.getAttribute('tabindex')).toBeNull(); + + const expectedIndex = component.aliasControls.length - 1; + const event = new MouseEvent('click', { bubbles: true, cancelable: true }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const removeAliasSpy = vi.spyOn(component, 'removeAlias'); + + link.dispatchEvent(event); + + expect(removeAliasSpy).toHaveBeenCalledWith( + expectedIndex, + 'facc_party_add_amend_convert_organisation_aliases', + event, + ); + expect(event.defaultPrevented).toBe(true); + }); + it('should initialize form with empty values when no initial data provided', () => { component.partyType = 'individual'; fixture.detectChanges(); diff --git a/src/app/flows/fines/fines-acc/fines-acc-payment-terms-amend-denied/fines-acc-payment-terms-amend-denied.component.html b/src/app/flows/fines/fines-acc/fines-acc-payment-terms-amend-denied/fines-acc-payment-terms-amend-denied.component.html index c49c8afe51..b7c5060160 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-payment-terms-amend-denied/fines-acc-payment-terms-amend-denied.component.html +++ b/src/app/flows/fines/fines-acc/fines-acc-payment-terms-amend-denied/fines-acc-payment-terms-amend-denied.component.html @@ -23,11 +23,4 @@ } } - - Go back + Go back diff --git a/src/app/flows/fines/fines-acc/fines-acc-payment-terms-amend-denied/fines-acc-payment-terms-amend-denied.component.spec.ts b/src/app/flows/fines/fines-acc/fines-acc-payment-terms-amend-denied/fines-acc-payment-terms-amend-denied.component.spec.ts index 936b12eacd..ddbd4cf040 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-payment-terms-amend-denied/fines-acc-payment-terms-amend-denied.component.spec.ts +++ b/src/app/flows/fines/fines-acc/fines-acc-payment-terms-amend-denied/fines-acc-payment-terms-amend-denied.component.spec.ts @@ -53,10 +53,57 @@ describe('FinesAccPaymentTermsAmendDeniedComponent', () => { expect(component).toBeTruthy(); }); - it('should navigate back to account summary on navigateBackToAccountSummary', () => { + it('should enforce go back link template semantics', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const templateConsts = ((FinesAccPaymentTermsAmendDeniedComponent as any).ɵcmp?.consts ?? []).filter( + (entry: unknown) => Array.isArray(entry), + ) as unknown[][]; + const templateFunction = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ((FinesAccPaymentTermsAmendDeniedComponent as any).ɵcmp?.template?.toString() as string | undefined) ?? ''; + const goBackLinkConst = templateConsts.find( + (entry) => entry.includes('govuk-link') && entry.includes('govuk-!-margin-top-4') && entry.includes('click'), + ); + + expect(goBackLinkConst).toBeTruthy(); + expect(goBackLinkConst).toContain('href'); + expect(goBackLinkConst).toContain(''); + expect(goBackLinkConst).toContain('govuk-link--no-visited-state'); + expect(goBackLinkConst).not.toContain('tabindex'); + expect(templateFunction).not.toContain('keydown.enter'); + expect(templateFunction).not.toContain('keyup.enter'); + }); + + it('should pass $event from go back link click and preserve logic', () => { + const link = fixture.nativeElement.querySelector('a.govuk-link') as HTMLAnchorElement | null; + expect(link).toBeTruthy(); + if (!link) throw new Error('Go back link not found'); + + expect(link.textContent?.trim()).toBe('Go back'); + expect(link.classList.contains('govuk-link--no-visited-state')).toBe(true); + expect(link.getAttribute('href')).toBe(''); + expect(link.getAttribute('tabindex')).toBeNull(); + + const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handlerSpy = vi.spyOn(component, 'navigateBackToAccountSummary'); + const routerSpy = vi.spyOn(component['router'], 'navigate'); + + link.dispatchEvent(clickEvent); + + expect(handlerSpy).toHaveBeenCalledWith(clickEvent); + expect(clickEvent.defaultPrevented).toBe(true); + expect(routerSpy).toHaveBeenCalledWith([`../../../details`], { relativeTo: component['route'] }); + }); + + it('should prevent default and navigate back to account summary on navigateBackToAccountSummary', () => { const routerSpy = vi.spyOn(component['router'], 'navigate'); const event = new Event('click'); + const preventDefaultSpy = vi.spyOn(event, 'preventDefault'); + component.navigateBackToAccountSummary(event); + + expect(preventDefaultSpy).toHaveBeenCalled(); expect(routerSpy).toHaveBeenCalledWith([`../../../details`], { relativeTo: component['route'] }); }); }); diff --git a/src/app/flows/fines/fines-acc/fines-acc-payment-terms-amend-denied/fines-acc-payment-terms-amend-denied.component.ts b/src/app/flows/fines/fines-acc/fines-acc-payment-terms-amend-denied/fines-acc-payment-terms-amend-denied.component.ts index c97a4f86d5..17f7d03349 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-payment-terms-amend-denied/fines-acc-payment-terms-amend-denied.component.ts +++ b/src/app/flows/fines/fines-acc/fines-acc-payment-terms-amend-denied/fines-acc-payment-terms-amend-denied.component.ts @@ -31,8 +31,8 @@ export class FinesAccPaymentTermsAmendDeniedComponent { * Navigates back to the account summary details page. * @param event The event triggered by clicking or pressing enter on the back link. */ - public navigateBackToAccountSummary(event: Event): void { - event.preventDefault(); + public navigateBackToAccountSummary(event?: Event): void { + event?.preventDefault(); this.router.navigate([`../../../details`], { relativeTo: this.route }); } } diff --git a/src/app/flows/fines/fines-acc/fines-acc-request-payment-card-access-denied/fines-acc-request-payment-card-access-denied.component.html b/src/app/flows/fines/fines-acc/fines-acc-request-payment-card-access-denied/fines-acc-request-payment-card-access-denied.component.html index 78a30e3da3..a02c201b34 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-request-payment-card-access-denied/fines-acc-request-payment-card-access-denied.component.html +++ b/src/app/flows/fines/fines-acc/fines-acc-request-payment-card-access-denied/fines-acc-request-payment-card-access-denied.component.html @@ -14,11 +14,4 @@ } } - - Go back + Go back diff --git a/src/app/flows/fines/fines-acc/fines-acc-request-payment-card-access-denied/fines-acc-request-payment-card-access-denied.component.spec.ts b/src/app/flows/fines/fines-acc/fines-acc-request-payment-card-access-denied/fines-acc-request-payment-card-access-denied.component.spec.ts index 25a8625712..88b82460b8 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-request-payment-card-access-denied/fines-acc-request-payment-card-access-denied.component.spec.ts +++ b/src/app/flows/fines/fines-acc/fines-acc-request-payment-card-access-denied/fines-acc-request-payment-card-access-denied.component.spec.ts @@ -53,10 +53,54 @@ describe('FinesAccRequestPaymentCardAccessDeniedComponent', () => { expect(component).toBeTruthy(); }); + it('should enforce current template link semantics', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const templateConsts = ((FinesAccRequestPaymentCardAccessDeniedComponent as any).ɵcmp?.consts ?? []).filter( + (entry: unknown) => Array.isArray(entry), + ) as unknown[][]; + const templateFunction = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ((FinesAccRequestPaymentCardAccessDeniedComponent as any).ɵcmp?.template?.toString() as string | undefined) ?? + ''; + const actionLinkConsts = templateConsts.filter( + (entry) => entry.includes('govuk-link') && entry.includes('href') && entry.includes('click'), + ); + + expect(actionLinkConsts.length).toBeGreaterThanOrEqual(1); + actionLinkConsts.forEach((entry) => { + expect(entry).toContain('govuk-link--no-visited-state'); + expect(entry).toContain('href'); + expect(entry).toContain(''); + expect(entry).not.toContain('tabindex'); + }); + expect(templateFunction).not.toContain('keydown.enter'); + expect(templateFunction).not.toContain('keyup.enter'); + }); + it('should navigate back to account summary on navigateBackToAccountSummary', () => { const routerSpy = vi.spyOn(component['router'], 'navigate'); const event = new Event('click'); + const preventDefaultSpy = vi.spyOn(event, 'preventDefault'); component.navigateBackToAccountSummary(event); + expect(preventDefaultSpy).toHaveBeenCalled(); expect(routerSpy).toHaveBeenCalledWith([`../../../details`], { relativeTo: component['route'] }); }); + + it('should click go back link and prevent default via the passed template event', () => { + const link = fixture.nativeElement.querySelector('a.govuk-link') as HTMLAnchorElement | null; + expect(link).toBeTruthy(); + if (!link) throw new Error('Go back link not found'); + + expect(link.classList.contains('govuk-link--no-visited-state')).toBe(true); + expect(link.getAttribute('href')).toBe(''); + expect(link.getAttribute('tabindex')).toBeNull(); + + const event = new MouseEvent('click', { bubbles: true, cancelable: true }); + const handlerSpy = vi.spyOn(component, 'navigateBackToAccountSummary'); + + link.dispatchEvent(event); + + expect(handlerSpy).toHaveBeenCalledWith(event); + expect(event.defaultPrevented).toBe(true); + }); }); diff --git a/src/app/flows/fines/fines-draft/fines-draft-create-and-manage/fines-draft-create-and-manage-tabs/fines-draft-create-and-manage-tabs.component.html b/src/app/flows/fines/fines-draft/fines-draft-create-and-manage/fines-draft-create-and-manage-tabs/fines-draft-create-and-manage-tabs.component.html index 187875d6da..97ff9fd7e8 100644 --- a/src/app/flows/fines/fines-draft/fines-draft-create-and-manage/fines-draft-create-and-manage-tabs/fines-draft-create-and-manage-tabs.component.html +++ b/src/app/flows/fines/fines-draft/fines-draft-create-and-manage/fines-draft-create-and-manage-tabs/fines-draft-create-and-manage-tabs.component.html @@ -126,10 +126,9 @@

Deleted

To resubmit accounts for other team members, you can view all rejected accounts.

diff --git a/src/app/flows/fines/fines-draft/fines-draft-create-and-manage/fines-draft-create-and-manage-tabs/fines-draft-create-and-manage-tabs.component.spec.ts b/src/app/flows/fines/fines-draft/fines-draft-create-and-manage/fines-draft-create-and-manage-tabs/fines-draft-create-and-manage-tabs.component.spec.ts index de182ccabb..d957a798c7 100644 --- a/src/app/flows/fines/fines-draft/fines-draft-create-and-manage/fines-draft-create-and-manage-tabs/fines-draft-create-and-manage-tabs.component.spec.ts +++ b/src/app/flows/fines/fines-draft/fines-draft-create-and-manage/fines-draft-create-and-manage-tabs/fines-draft-create-and-manage-tabs.component.spec.ts @@ -150,6 +150,40 @@ describe('FinesDraftCreateAndManageTabsComponent', () => { expect(mockRouter.navigate).toHaveBeenCalledWith([route], { relativeTo: component['activatedRoute'].parent }); }); + it('should prevent default and navigate when handleRoute is called with an event', () => { + const route = 'some/route'; + const event = new Event('click'); + const preventDefaultSpy = vi.spyOn(event, 'preventDefault'); + component.activeTab = 'review'; + + component.handleRoute(route, event); + + expect(preventDefaultSpy).toHaveBeenCalled(); + expect(finesDraftStore.fragment()).toEqual('review'); + expect(mockRouter.navigate).toHaveBeenCalledWith([route], { relativeTo: component['activatedRoute'].parent }); + }); + + it('should enforce view all rejected accounts link template semantics', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const templateConsts = ((FinesDraftCreateAndManageTabsComponent as any).ɵcmp?.consts ?? []).filter( + (entry: unknown) => Array.isArray(entry), + ) as unknown[][]; + const templateFunction = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ((FinesDraftCreateAndManageTabsComponent as any).ɵcmp?.template?.toString() as string | undefined) ?? ''; + const rejectedLinkConst = templateConsts.find( + (entry) => entry.includes('govuk-link') && entry.includes('href') && entry.includes('click'), + ); + + expect(rejectedLinkConst).toBeTruthy(); + expect(rejectedLinkConst).toContain('govuk-link--no-visited-state'); + expect(rejectedLinkConst).toContain('href'); + expect(rejectedLinkConst).toContain(''); + expect(rejectedLinkConst).not.toContain('tabindex'); + expect(templateFunction).not.toContain('keydown.enter'); + expect(templateFunction).not.toContain('keyup.enter'); + }); + it('should show "0" when getDraftAccounts returns count 0', async () => { mockOpalFinesService.getDraftAccounts.mockReturnValue(of({ count: 0, summaries: [] })); finesDraftService.populateTableData.mockReturnValue([]); diff --git a/src/app/flows/fines/fines-draft/fines-draft-create-and-manage/fines-draft-create-and-manage-tabs/fines-draft-create-and-manage-tabs.component.ts b/src/app/flows/fines/fines-draft/fines-draft-create-and-manage/fines-draft-create-and-manage-tabs/fines-draft-create-and-manage-tabs.component.ts index 6a1c90cdc3..b97252f3d1 100644 --- a/src/app/flows/fines/fines-draft/fines-draft-create-and-manage/fines-draft-create-and-manage-tabs/fines-draft-create-and-manage-tabs.component.ts +++ b/src/app/flows/fines/fines-draft/fines-draft-create-and-manage/fines-draft-create-and-manage-tabs/fines-draft-create-and-manage-tabs.component.ts @@ -229,8 +229,10 @@ export class FinesDraftCreateAndManageTabsComponent extends AbstractTabData impl * Also sets the current active tab as a fragment in the fines draft store. * * @param route - The route path to navigate to. + * @param event - The event object associated with the navigation action. */ - public handleRoute(route: string): void { + public handleRoute(route: string, event?: Event): void { + event?.preventDefault(); this.finesDraftStore.setFragment(this.activeTab); this['router'].navigate([route], { relativeTo: this['activatedRoute'].parent }); } diff --git a/src/app/flows/fines/fines-draft/fines-draft-table-wrapper/fines-draft-table-wrapper.component.html b/src/app/flows/fines/fines-draft/fines-draft-table-wrapper/fines-draft-table-wrapper.component.html index fa5b5797a1..0cadef6136 100644 --- a/src/app/flows/fines/fines-draft/fines-draft-table-wrapper/fines-draft-table-wrapper.component.html +++ b/src/app/flows/fines/fines-draft/fines-draft-table-wrapper/fines-draft-table-wrapper.component.html @@ -97,9 +97,8 @@ {{ row['Account'] }} @@ -108,13 +107,9 @@ @if (activeTab === 'approved') { {{ row['Defendant'] }} } @else { - {{ row['Defendant'] }} + {{ + row['Defendant'] + }} } diff --git a/src/app/flows/fines/fines-draft/fines-draft-table-wrapper/fines-draft-table-wrapper.component.spec.ts b/src/app/flows/fines/fines-draft/fines-draft-table-wrapper/fines-draft-table-wrapper.component.spec.ts index 629127ad3c..59c917d5fa 100644 --- a/src/app/flows/fines/fines-draft/fines-draft-table-wrapper/fines-draft-table-wrapper.component.spec.ts +++ b/src/app/flows/fines/fines-draft/fines-draft-table-wrapper/fines-draft-table-wrapper.component.spec.ts @@ -63,4 +63,101 @@ describe('FinesDraftTableWrapperComponent', () => { expect(component.accountClicked.emit).toHaveBeenCalledWith(testAccountId); }); + + it('should enforce current template link semantics', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const templateConsts = ((FinesDraftTableWrapperComponent as any).ɵcmp?.consts ?? []).filter((entry: unknown) => + Array.isArray(entry), + ) as unknown[][]; + const templateFunction = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ((FinesDraftTableWrapperComponent as any).ɵcmp?.template?.toString() as string | undefined) ?? ''; + const actionLinkConsts = templateConsts.filter( + (entry) => + entry.includes('govuk-link') && + entry.includes('govuk-link--no-visited-state') && + entry.includes('href') && + entry.includes('click'), + ); + + expect(actionLinkConsts.length).toBeGreaterThanOrEqual(1); + actionLinkConsts.forEach((entry) => { + expect(entry).toContain('href'); + expect(entry).toContain(''); + expect(entry).not.toContain('tabindex'); + }); + expect(templateFunction).not.toContain('keydown.enter'); + expect(templateFunction).not.toContain('keyup.enter'); + }); + + it('should click defendant link and preserve current template click behaviour', () => { + component.activeTab = 'review'; + component.tableData = FINES_DRAFT_TABLE_WRAPPER_TABLE_DATA_MOCK; + fixture.detectChanges(); + + const defendantName = FINES_DRAFT_TABLE_WRAPPER_TABLE_DATA_MOCK[0].Defendant; + const links = Array.from(fixture.nativeElement.querySelectorAll('a.govuk-link')) as HTMLAnchorElement[]; + const link = links.find((anchor) => anchor.textContent?.trim() === defendantName) ?? null; + expect(link).toBeTruthy(); + if (!link) throw new Error('Defendant link not found'); + + expect(link.classList.contains('govuk-link--no-visited-state')).toBe(true); + expect(link.getAttribute('href')).toBe(''); + expect(link.getAttribute('tabindex')).toBeNull(); + + const event = new MouseEvent('click', { bubbles: true, cancelable: true }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handlerSpy = vi.spyOn(component, 'onDefendantClick'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const emitSpy = vi.spyOn(component.linkClicked, 'emit'); + + link.dispatchEvent(event); + + expect(handlerSpy).toHaveBeenCalledWith(FINES_DRAFT_TABLE_WRAPPER_TABLE_DATA_MOCK[0], event); + expect(event.defaultPrevented).toBe(true); + expect(emitSpy).toHaveBeenCalledWith(FINES_DRAFT_TABLE_WRAPPER_TABLE_DATA_MOCK[0]); + }); + + it('should click account link and preserve current template click behaviour', () => { + fixture.componentRef.setInput('activeTab', 'approved'); + fixture.componentRef.setInput('tableData', FINES_DRAFT_TABLE_WRAPPER_TABLE_DATA_MOCK); + fixture.detectChanges(); + + const accountNumber = FINES_DRAFT_TABLE_WRAPPER_TABLE_DATA_MOCK[0].Account; + const links = Array.from(fixture.nativeElement.querySelectorAll('a.govuk-link')) as HTMLAnchorElement[]; + const link = links.find((anchor) => anchor.textContent?.trim() === accountNumber) ?? null; + if (!link) { + const linkTexts = links.map((anchor) => anchor.textContent?.trim()); + throw new Error(`Account link not found. Available links: ${linkTexts.join(', ')}`); + } + + expect(link.classList.contains('govuk-link--no-visited-state')).toBe(true); + expect(link.getAttribute('href')).toBe(''); + expect(link.getAttribute('tabindex')).toBeNull(); + + const event = new MouseEvent('click', { bubbles: true, cancelable: true }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handlerSpy = vi.spyOn(component, 'onAccountClick'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const emitSpy = vi.spyOn(component.accountClicked, 'emit'); + + link.dispatchEvent(event); + + expect(handlerSpy).toHaveBeenCalledWith(FINES_DRAFT_TABLE_WRAPPER_TABLE_DATA_MOCK[0]['Defendant id'], event); + expect(event.defaultPrevented).toBe(true); + expect(emitSpy).toHaveBeenCalledWith(FINES_DRAFT_TABLE_WRAPPER_TABLE_DATA_MOCK[0]['Defendant id']); + }); + + it('should prevent default and emit accountClicked when onAccountClick is called with an event', () => { + const event = new Event('click'); + const preventDefaultSpy = vi.spyOn(event, 'preventDefault'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const emitSpy = vi.spyOn(component.accountClicked, 'emit'); + const accountId = FINES_DRAFT_TABLE_WRAPPER_TABLE_DATA_MOCK[0]['Defendant id']; + + component.onAccountClick(accountId, event); + + expect(preventDefaultSpy).toHaveBeenCalled(); + expect(emitSpy).toHaveBeenCalledWith(accountId); + }); }); diff --git a/src/app/flows/fines/fines-draft/fines-draft-table-wrapper/fines-draft-table-wrapper.component.ts b/src/app/flows/fines/fines-draft/fines-draft-table-wrapper/fines-draft-table-wrapper.component.ts index f7c3c47af5..26a4980b49 100644 --- a/src/app/flows/fines/fines-draft/fines-draft-table-wrapper/fines-draft-table-wrapper.component.ts +++ b/src/app/flows/fines/fines-draft/fines-draft-table-wrapper/fines-draft-table-wrapper.component.ts @@ -56,9 +56,11 @@ export class FinesDraftTableWrapperComponent extends AbstractSortableTablePagina * Emits the clicked row. * * @param {IFinesDraftTableWrapperTableData} row - The row that was clicked. + * @param {Event} event - The optional DOM event that triggered the click. * @returns {void} */ - public onDefendantClick(row: IFinesDraftTableWrapperTableData): void { + public onDefendantClick(row: IFinesDraftTableWrapperTableData, event?: Event): void { + event?.preventDefault(); this.linkClicked.emit(row); } @@ -67,9 +69,11 @@ export class FinesDraftTableWrapperComponent extends AbstractSortableTablePagina * Emits the clicked account id. * * @param {number} account_id - the account id of the clicked account. + * @param {Event} event - The optional DOM event that triggered the click. * @returns {void} */ - public onAccountClick(account_id: number): void { + public onAccountClick(account_id: number, event?: Event): void { + event?.preventDefault(); this.accountClicked.emit(account_id); } } diff --git a/src/app/flows/fines/fines-mac/fines-mac-account-details/fines-mac-account-details.component.html b/src/app/flows/fines/fines-mac/fines-mac-account-details/fines-mac-account-details.component.html index 7216d6b6de..704cdb4be2 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-account-details/fines-mac-account-details.component.html +++ b/src/app/flows/fines/fines-mac/fines-mac-account-details/fines-mac-account-details.component.html @@ -136,7 +136,7 @@

Check and submit

@if (!finesDraftStore.amend()) {
Delete accountAlias {{ rowIndex + 1 }}
Remove alias {{ aliasControls.length }} diff --git a/src/app/flows/fines/fines-mac/fines-mac-company-details/fines-mac-company-details-form/fines-mac-company-details-form.component.spec.ts b/src/app/flows/fines/fines-mac/fines-mac-company-details/fines-mac-company-details-form/fines-mac-company-details-form.component.spec.ts index 728cd40fef..2cc83f3594 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-company-details/fines-mac-company-details-form/fines-mac-company-details-form.component.spec.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-company-details/fines-mac-company-details-form/fines-mac-company-details-form.component.spec.ts @@ -55,6 +55,60 @@ describe('FinesMacCompanyDetailsFormComponent', () => { expect(component).toBeTruthy(); }); + it('should enforce remove alias link template semantics', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const templateConsts = ((FinesMacCompanyDetailsFormComponent as any).ɵcmp?.consts ?? []).filter( + (entry: unknown) => Array.isArray(entry), + ) as unknown[][]; + const templateFunction = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ((FinesMacCompanyDetailsFormComponent as any).ɵcmp?.template?.toString() as string | undefined) ?? ''; + const removeLinkConsts = templateConsts.filter( + (entry) => + entry.includes('govuk-link') && + entry.includes('govuk-link--no-visited-state') && + entry.includes('href') && + entry.includes('click'), + ); + + expect(removeLinkConsts.length).toBeGreaterThanOrEqual(1); + removeLinkConsts.forEach((entry) => { + expect(entry).toContain('href'); + expect(entry).toContain(''); + expect(entry).not.toContain('tabindex'); + }); + expect(templateFunction).not.toContain('keydown.enter'); + expect(templateFunction).not.toContain('keyup.enter'); + }); + + it('should render remove alias link with href and pass $event into removeAlias', () => { + component.form.get('fm_company_details_add_alias')?.setValue(true); + while (component.aliasControls.length < 2) { + component.addAlias(component.aliasControls.length, 'fm_company_details_aliases'); + } + fixture.detectChanges(); + + const link = + (Array.from( + fixture.nativeElement.querySelectorAll('a.govuk-link.govuk-link--no-visited-state') as NodeListOf, + ).find((anchor) => anchor.textContent?.trim().startsWith('Remove')) as HTMLAnchorElement | undefined) ?? null; + expect(link).toBeTruthy(); + if (!link) throw new Error('Company remove alias link not found'); + + expect(link.getAttribute('href')).toBe(''); + expect(link.getAttribute('tabindex')).toBeNull(); + + const expectedIndex = component.aliasControls.length - 1; + const event = new MouseEvent('click', { bubbles: true, cancelable: true }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const removeAliasSpy = vi.spyOn(component, 'removeAlias'); + + link.dispatchEvent(event); + + expect(removeAliasSpy).toHaveBeenCalledWith(expectedIndex, 'fm_company_details_aliases', event); + expect(event.defaultPrevented).toBe(true); + }); + it('should emit form submit event with form value - nestedFlow true', () => { const event = { submitter: { className: 'nested-flow' } } as SubmitEvent; formSubmit.nestedFlow = true; diff --git a/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.html b/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.html index c37299f1b4..1559da8c62 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.html +++ b/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.html @@ -220,9 +220,8 @@

> Add minor creditor details @@ -238,9 +237,8 @@

Remove imposition diff --git a/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.spec.ts b/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.spec.ts index c5c7be5e88..e3235241d5 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.spec.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.spec.ts @@ -117,6 +117,41 @@ describe('FinesMacOffenceDetailsAddAnOffenceFormComponent', () => { expect(component).toBeTruthy(); }); + it('should enforce current template link semantics', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const templateConsts = ((FinesMacOffenceDetailsAddAnOffenceFormComponent as any).ɵcmp?.consts ?? []).filter( + (entry: unknown) => Array.isArray(entry), + ) as unknown[][]; + const templateFunction = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ((FinesMacOffenceDetailsAddAnOffenceFormComponent as any).ɵcmp?.template?.toString() as string | undefined) ?? + ''; + const actionLinkConsts = templateConsts.filter( + (entry) => + entry.includes('govuk-link') && + entry.includes('govuk-link--no-visited-state') && + entry.includes('href') && + entry.includes('click'), + ); + const searchLink = Array.from( + fixture.nativeElement.querySelectorAll('a.govuk-link') as NodeListOf, + ).find((link) => link.textContent?.includes('search the offence list')); + + expect(actionLinkConsts.length).toBeGreaterThanOrEqual(1); + actionLinkConsts.forEach((entry) => { + expect(entry).toContain('href'); + expect(entry).toContain(''); + expect(entry).not.toContain('tabindex'); + }); + + expect(searchLink).toBeTruthy(); + expect(searchLink?.classList.contains('govuk-link--no-visited-state')).toBe(true); + expect(searchLink?.getAttribute('href')).toBe(component.searchOffenceUrl); + expect(searchLink?.getAttribute('tabindex')).toBeNull(); + expect(templateFunction).not.toContain('keydown.enter'); + expect(templateFunction).not.toContain('keyup.enter'); + }); + it('should set needsCreditorControl value to true when result_code is compensation', () => { finesMacOffenceDetailsStore.setOffenceDetailsDraft([]); component.ngOnInit(); @@ -258,6 +293,27 @@ describe('FinesMacOffenceDetailsAddAnOffenceFormComponent', () => { }); }); + it('should prevent default and execute goToMinorCreditor logic when event is provided', () => { + const event = new Event('click'); + const preventDefaultSpy = vi.spyOn(event, 'preventDefault'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const setRemoveMinorCreditorSpy = vi.spyOn(component['finesMacOffenceDetailsStore'], 'setRemoveMinorCreditor'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const setRowIndexSpy = vi.spyOn(component['finesMacOffenceDetailsStore'], 'setRowIndex'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const updateDraftSpy = vi.spyOn(component, 'updateOffenceDetailsDraft'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handleRouteSpy = vi.spyOn(component, 'handleRoute'); + + component.goToMinorCreditor(0, event); + + expect(preventDefaultSpy).toHaveBeenCalled(); + expect(setRemoveMinorCreditorSpy).toHaveBeenCalledWith(null); + expect(setRowIndexSpy).toHaveBeenCalledWith(0); + expect(updateDraftSpy).toHaveBeenCalledWith(component.form.value); + expect(handleRouteSpy).toHaveBeenCalledWith(component['fineMacOffenceDetailsRoutingPaths'].children.addMinorCreditor); + }); + it('should populate offence details draft when navigating to remove imposition when draft is empty', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const routerSpy = vi.spyOn(component['router'], 'navigate'); @@ -273,6 +329,27 @@ describe('FinesMacOffenceDetailsAddAnOffenceFormComponent', () => { }); }); + it('should prevent default and execute removeImpositionConfirmation logic when event is provided', () => { + const event = new Event('click'); + const preventDefaultSpy = vi.spyOn(event, 'preventDefault'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const setRowIndexSpy = vi.spyOn(component['finesMacOffenceDetailsStore'], 'setRowIndex'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const setFormArrayControlsSpy = vi.spyOn(component['finesMacOffenceDetailsStore'], 'setFormArrayControls'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const updateDraftSpy = vi.spyOn(component, 'updateOffenceDetailsDraft'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handleRouteSpy = vi.spyOn(component, 'handleRoute'); + + component.removeImpositionConfirmation(0, event); + + expect(preventDefaultSpy).toHaveBeenCalled(); + expect(setRowIndexSpy).toHaveBeenCalledWith(0); + expect(setFormArrayControlsSpy).toHaveBeenCalledWith(component.formArrayControls); + expect(updateDraftSpy).toHaveBeenCalledWith(component.form.value); + expect(handleRouteSpy).toHaveBeenCalledWith(component['fineMacOffenceDetailsRoutingPaths'].children.removeImposition); + }); + it('should populate offence details draft when navigating to remove imposition when draft is populated', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const routerSpy = vi.spyOn(component['router'], 'navigate'); diff --git a/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.ts b/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.ts index 22fc1d9708..651c29e6a0 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.ts @@ -484,8 +484,10 @@ export class FinesMacOffenceDetailsAddAnOffenceFormComponent * Navigates to the minor creditor page for the specified row index. * * @param rowIndex - The index of the row. + * @param event - The optional DOM event that triggered the navigation. */ - public goToMinorCreditor(rowIndex: number): void { + public goToMinorCreditor(rowIndex: number, event?: Event): void { + event?.preventDefault(); this.finesMacOffenceDetailsStore.setRemoveMinorCreditor(null); this.finesMacOffenceDetailsStore.setRowIndex(rowIndex); @@ -497,9 +499,11 @@ export class FinesMacOffenceDetailsAddAnOffenceFormComponent * Removes the imposition at the specified rowIndex from the form. * * @param rowIndex - The index of the imposition to be removed. + * @param event - The optional DOM event that triggered the removal flow. * @returns void */ - public removeImpositionConfirmation(rowIndex: number): void { + public removeImpositionConfirmation(rowIndex: number, event?: Event): void { + event?.preventDefault(); this.finesMacOffenceDetailsStore.setRowIndex(rowIndex); this.finesMacOffenceDetailsStore.setFormArrayControls(this.formArrayControls); diff --git a/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-review-offence/fines-mac-offence-details-review-offence-imposition/fines-mac-offence-details-review-offence-imposition.component.html b/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-review-offence/fines-mac-offence-details-review-offence-imposition/fines-mac-offence-details-review-offence-imposition.component.html index 214c8e664f..6a7653d51a 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-review-offence/fines-mac-offence-details-review-offence-imposition/fines-mac-offence-details-review-offence-imposition.component.html +++ b/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-review-offence/fines-mac-offence-details-review-offence-imposition/fines-mac-offence-details-review-offence-imposition.component.html @@ -48,9 +48,8 @@ @let linkLabel = show ? 'Hide details' : 'Show details'; {{ linkLabel }} @if (show) { diff --git a/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-review-offence/fines-mac-offence-details-review-offence-imposition/fines-mac-offence-details-review-offence-imposition.component.spec.ts b/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-review-offence/fines-mac-offence-details-review-offence-imposition/fines-mac-offence-details-review-offence-imposition.component.spec.ts index 05bd088d3c..836a52a326 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-review-offence/fines-mac-offence-details-review-offence-imposition/fines-mac-offence-details-review-offence-imposition.component.spec.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-review-offence/fines-mac-offence-details-review-offence-imposition/fines-mac-offence-details-review-offence-imposition.component.spec.ts @@ -92,6 +92,34 @@ describe('FinesMacOffenceDetailsReviewOffenceImpositionComponent', () => { expect(component).toBeTruthy(); }); + it('should enforce current template link semantics', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const templateConsts = ((FinesMacOffenceDetailsReviewOffenceImpositionComponent as any).ɵcmp?.consts ?? []).filter( + (entry: unknown) => Array.isArray(entry), + ) as unknown[][]; + const templateFunction = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ((FinesMacOffenceDetailsReviewOffenceImpositionComponent as any).ɵcmp?.template?.toString() as + | string + | undefined) ?? ''; + const actionLinkConsts = templateConsts.filter( + (entry) => + entry.includes('govuk-link') && + entry.includes('govuk-link--no-visited-state') && + entry.includes('href') && + entry.includes('click'), + ); + + expect(actionLinkConsts.length).toBeGreaterThanOrEqual(1); + actionLinkConsts.forEach((entry) => { + expect(entry).toContain('href'); + expect(entry).toContain(''); + expect(entry).not.toContain('tabindex'); + }); + expect(templateFunction).not.toContain('keydown.enter'); + expect(templateFunction).not.toContain('keyup.enter'); + }); + it('should set impositionsTotalsData with converted monetary strings', () => { const expectedTotal = '£100.00'; mockUtilsService.convertToMonetaryString.mockReturnValue(expectedTotal); @@ -252,6 +280,37 @@ describe('FinesMacOffenceDetailsReviewOffenceImpositionComponent', () => { expect(component.impositionTableData[0].showMinorCreditorData).toBe(false); }); + it('should click show/hide details link and preserve current template click behaviour', () => { + const link = fixture.nativeElement.querySelector('a.govuk-link') as HTMLAnchorElement | null; + expect(link).toBeTruthy(); + if (!link) throw new Error('Show/hide details link not found'); + + expect(link.classList.contains('govuk-link--no-visited-state')).toBe(true); + expect(link.getAttribute('href')).toBe(''); + expect(link.getAttribute('tabindex')).toBeNull(); + + const impositionId = component.impositionTableData[0].impositionId; + const event = new MouseEvent('click', { bubbles: true, cancelable: true }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handlerSpy = vi.spyOn(component, 'invertShowMinorCreditorData'); + + link.dispatchEvent(event); + + expect(handlerSpy).toHaveBeenCalledWith(impositionId, event); + expect(event.defaultPrevented).toBe(true); + }); + + it('should prevent default and still invert minor creditor data when event is provided', () => { + const impositionId = component.impositionTableData[0].impositionId; + const event = new Event('click'); + const preventDefaultSpy = vi.spyOn(event, 'preventDefault'); + + component.invertShowMinorCreditorData(impositionId, event); + + expect(preventDefaultSpy).toHaveBeenCalled(); + expect(component.impositionTableData[0].showMinorCreditorData).toBe(true); + }); + it('should return null for address and payment method for minor creditor', () => { const finesMacState = structuredClone(finesMacStore.getFinesMacStore()); finesMacState.offenceDetails[0].childFormData = [ diff --git a/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-review-offence/fines-mac-offence-details-review-offence-imposition/fines-mac-offence-details-review-offence-imposition.component.ts b/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-review-offence/fines-mac-offence-details-review-offence-imposition/fines-mac-offence-details-review-offence-imposition.component.ts index 86330c559d..a1d29f5e77 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-review-offence/fines-mac-offence-details-review-offence-imposition/fines-mac-offence-details-review-offence-imposition.component.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-review-offence/fines-mac-offence-details-review-offence-imposition/fines-mac-offence-details-review-offence-imposition.component.ts @@ -266,7 +266,14 @@ export class FinesMacOffenceDetailsReviewOffenceImpositionComponent implements O }; } - public invertShowMinorCreditorData(impositionId: number): void { + /** + * Toggles the visibility of minor creditor details for the selected imposition. + * + * @param impositionId - The unique identifier of the imposition to update. + * @param event - The optional DOM event that triggered the toggle. + */ + public invertShowMinorCreditorData(impositionId: number, event?: Event): void { + event?.preventDefault(); const imposition = this.impositionTableData.find((imposition) => imposition.impositionId === impositionId)!; imposition.showMinorCreditorData = !imposition.showMinorCreditorData; } diff --git a/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-search-offences/fines-mac-offence-details-search-offences-results/fines-mac-offence-details-search-offences-results-table-wrapper/fines-mac-offence-details-search-offences-results-table-wrapper.component.html b/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-search-offences/fines-mac-offence-details-search-offences-results/fines-mac-offence-details-search-offences-results-table-wrapper/fines-mac-offence-details-search-offences-results-table-wrapper.component.html index 040f623457..e3a2a9868b 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-search-offences/fines-mac-offence-details-search-offences-results/fines-mac-offence-details-search-offences-results-table-wrapper/fines-mac-offence-details-search-offences-results-table-wrapper.component.html +++ b/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-search-offences/fines-mac-offence-details-search-offences-results/fines-mac-offence-details-search-offences-results-table-wrapper/fines-mac-offence-details-search-offences-results-table-wrapper.component.html @@ -54,9 +54,8 @@ {{ COPY_CODE_TO_CLIPBOARD }} diff --git a/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-search-offences/fines-mac-offence-details-search-offences-results/fines-mac-offence-details-search-offences-results-table-wrapper/fines-mac-offence-details-search-offences-results-table-wrapper.component.spec.ts b/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-search-offences/fines-mac-offence-details-search-offences-results/fines-mac-offence-details-search-offences-results-table-wrapper/fines-mac-offence-details-search-offences-results-table-wrapper.component.spec.ts index 5d9befba3c..370a683951 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-search-offences/fines-mac-offence-details-search-offences-results/fines-mac-offence-details-search-offences-results-table-wrapper/fines-mac-offence-details-search-offences-results-table-wrapper.component.spec.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-search-offences/fines-mac-offence-details-search-offences-results/fines-mac-offence-details-search-offences-results-table-wrapper/fines-mac-offence-details-search-offences-results-table-wrapper.component.spec.ts @@ -35,6 +35,46 @@ describe('FinesMacOffenceDetailsSearchOffencesResultsTableWrapperComponent', () expect(component).toBeTruthy(); }); + it('should enforce current template link semantics', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const templateConsts = ( + (FinesMacOffenceDetailsSearchOffencesResultsTableWrapperComponent as any).ɵcmp?.consts ?? [] + ).filter((entry: unknown) => Array.isArray(entry)) as unknown[][]; + const templateFunction = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ((FinesMacOffenceDetailsSearchOffencesResultsTableWrapperComponent as any).ɵcmp?.template?.toString() as + | string + | undefined) ?? ''; + const actionLinkConsts = templateConsts.filter( + (entry) => + entry.includes('govuk-link') && + entry.includes('govuk-link--no-visited-state') && + entry.includes('href') && + entry.includes('click'), + ); + + expect(actionLinkConsts.length).toBeGreaterThanOrEqual(1); + actionLinkConsts.forEach((entry) => { + expect(entry).toContain('href'); + expect(entry).toContain(''); + expect(entry).not.toContain('tabindex'); + }); + expect(templateFunction).not.toContain('keydown.enter'); + expect(templateFunction).not.toContain('keyup.enter'); + }); + + it('should prevent default and copy to clipboard when copyCodeToClipboard is called with an event', () => { + const event = new Event('click'); + const preventDefaultSpy = vi.spyOn(event, 'preventDefault'); + const linkElement = document.createElement('a'); + const liveRegion = document.createElement('span'); + + component.copyCodeToClipboard(linkElement, liveRegion, '1234', event); + + expect(preventDefaultSpy).toHaveBeenCalled(); + expect(utilsService.copyToClipboard).toHaveBeenCalledWith('1234'); + }); + it('should update link and live region, then revert after timeout', () => { vi.useFakeTimers(); fixture = TestBed.createComponent(FinesMacOffenceDetailsSearchOffencesResultsTableWrapperComponent); diff --git a/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-search-offences/fines-mac-offence-details-search-offences-results/fines-mac-offence-details-search-offences-results-table-wrapper/fines-mac-offence-details-search-offences-results-table-wrapper.component.ts b/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-search-offences/fines-mac-offence-details-search-offences-results/fines-mac-offence-details-search-offences-results-table-wrapper/fines-mac-offence-details-search-offences-results-table-wrapper.component.ts index a032c0e7ef..b3a5397a54 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-search-offences/fines-mac-offence-details-search-offences-results/fines-mac-offence-details-search-offences-results-table-wrapper/fines-mac-offence-details-search-offences-results-table-wrapper.component.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-search-offences/fines-mac-offence-details-search-offences-results/fines-mac-offence-details-search-offences-results-table-wrapper/fines-mac-offence-details-search-offences-results-table-wrapper.component.ts @@ -69,8 +69,10 @@ export class FinesMacOffenceDetailsSearchOffencesResultsTableWrapperComponent * @param linkElement - The HTML element whose label will be temporarily changed to indicate the copy action. * @param liveRegion - The HTML element used as a live region for screen readers to announce the copy action. * @param value - The string value to be copied to the clipboard. + * @param event - The optional DOM event that triggered the copy action. */ - public copyCodeToClipboard(linkElement: HTMLElement, liveRegion: HTMLElement, value: string): void { + public copyCodeToClipboard(linkElement: HTMLElement, liveRegion: HTMLElement, value: string, event?: Event): void { + event?.preventDefault(); this.utilsService.copyToClipboard(value); const originalText = linkElement.innerText; diff --git a/src/app/flows/fines/fines-mac/fines-mac-parent-guardian-details/fines-mac-parent-guardian-details-form/fines-mac-parent-guardian-details-form.component.html b/src/app/flows/fines/fines-mac/fines-mac-parent-guardian-details/fines-mac-parent-guardian-details-form/fines-mac-parent-guardian-details-form.component.html index a633dcc17d..5e7c951c52 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-parent-guardian-details/fines-mac-parent-guardian-details-form/fines-mac-parent-guardian-details-form.component.html +++ b/src/app/flows/fines/fines-mac/fines-mac-parent-guardian-details/fines-mac-parent-guardian-details-form/fines-mac-parent-guardian-details-form.component.html @@ -88,9 +88,8 @@

Alias {{ rowIndex + 1 }}

Remove alias {{ aliasControls.length }} diff --git a/src/app/flows/fines/fines-mac/fines-mac-parent-guardian-details/fines-mac-parent-guardian-details-form/fines-mac-parent-guardian-details-form.component.spec.ts b/src/app/flows/fines/fines-mac/fines-mac-parent-guardian-details/fines-mac-parent-guardian-details-form/fines-mac-parent-guardian-details-form.component.spec.ts index 64489a34fb..a5b115a206 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-parent-guardian-details/fines-mac-parent-guardian-details-form/fines-mac-parent-guardian-details-form.component.spec.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-parent-guardian-details/fines-mac-parent-guardian-details-form/fines-mac-parent-guardian-details-form.component.spec.ts @@ -71,6 +71,60 @@ describe('FinesMacParentGuardianDetailsFormComponent', () => { expect(component).toBeTruthy(); }); + it('should enforce remove alias link template semantics', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const templateConsts = ((FinesMacParentGuardianDetailsFormComponent as any).ɵcmp?.consts ?? []).filter( + (entry: unknown) => Array.isArray(entry), + ) as unknown[][]; + const templateFunction = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ((FinesMacParentGuardianDetailsFormComponent as any).ɵcmp?.template?.toString() as string | undefined) ?? ''; + const removeLinkConsts = templateConsts.filter( + (entry) => + entry.includes('govuk-link') && + entry.includes('govuk-link--no-visited-state') && + entry.includes('href') && + entry.includes('click'), + ); + + expect(removeLinkConsts.length).toBeGreaterThanOrEqual(1); + removeLinkConsts.forEach((entry) => { + expect(entry).toContain('href'); + expect(entry).toContain(''); + expect(entry).not.toContain('tabindex'); + }); + expect(templateFunction).not.toContain('keydown.enter'); + expect(templateFunction).not.toContain('keyup.enter'); + }); + + it('should render remove alias link with href and pass $event into removeAlias', () => { + component.form.get('fm_parent_guardian_details_add_alias')?.setValue(true); + while (component.aliasControls.length < 2) { + component.addAlias(component.aliasControls.length, 'fm_parent_guardian_details_aliases'); + } + fixture.detectChanges(); + + const link = + (Array.from( + fixture.nativeElement.querySelectorAll('a.govuk-link.govuk-link--no-visited-state') as NodeListOf, + ).find((anchor) => anchor.textContent?.trim().startsWith('Remove')) as HTMLAnchorElement | undefined) ?? null; + expect(link).toBeTruthy(); + if (!link) throw new Error('Parent/guardian remove alias link not found'); + + expect(link.getAttribute('href')).toBe(''); + expect(link.getAttribute('tabindex')).toBeNull(); + + const expectedIndex = component.aliasControls.length - 1; + const event = new MouseEvent('click', { bubbles: true, cancelable: true }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const removeAliasSpy = vi.spyOn(component, 'removeAlias'); + + link.dispatchEvent(event); + + expect(removeAliasSpy).toHaveBeenCalledWith(expectedIndex, 'fm_parent_guardian_details_aliases', event); + expect(event.defaultPrevented).toBe(true); + }); + it('should emit form submit event with form value', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any vi.spyOn(component['formSubmit'], 'emit'); diff --git a/src/app/flows/fines/fines-mac/fines-mac-personal-details/fines-mac-personal-details-form/fines-mac-personal-details-form.component.html b/src/app/flows/fines/fines-mac/fines-mac-personal-details/fines-mac-personal-details-form/fines-mac-personal-details-form.component.html index b7e1ea5fe5..66409788ae 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-personal-details/fines-mac-personal-details-form/fines-mac-personal-details-form.component.html +++ b/src/app/flows/fines/fines-mac/fines-mac-personal-details/fines-mac-personal-details-form/fines-mac-personal-details-form.component.html @@ -97,9 +97,8 @@

Alias {{ rowIndex + 1 }}

Remove alias {{ aliasControls.length }} diff --git a/src/app/flows/fines/fines-mac/fines-mac-personal-details/fines-mac-personal-details-form/fines-mac-personal-details-form.component.spec.ts b/src/app/flows/fines/fines-mac/fines-mac-personal-details/fines-mac-personal-details-form/fines-mac-personal-details-form.component.spec.ts index 6caeb76994..ec3b3c4a94 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-personal-details/fines-mac-personal-details-form/fines-mac-personal-details-form.component.spec.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-personal-details/fines-mac-personal-details-form/fines-mac-personal-details-form.component.spec.ts @@ -81,6 +81,60 @@ describe('FinesMacPersonalDetailsFormComponent', () => { expect(component).toBeTruthy(); }); + it('should enforce remove alias link template semantics', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const templateConsts = ((FinesMacPersonalDetailsFormComponent as any).ɵcmp?.consts ?? []).filter( + (entry: unknown) => Array.isArray(entry), + ) as unknown[][]; + const templateFunction = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ((FinesMacPersonalDetailsFormComponent as any).ɵcmp?.template?.toString() as string | undefined) ?? ''; + const removeLinkConsts = templateConsts.filter( + (entry) => + entry.includes('govuk-link') && + entry.includes('govuk-link--no-visited-state') && + entry.includes('href') && + entry.includes('click'), + ); + + expect(removeLinkConsts.length).toBeGreaterThanOrEqual(1); + removeLinkConsts.forEach((entry) => { + expect(entry).toContain('href'); + expect(entry).toContain(''); + expect(entry).not.toContain('tabindex'); + }); + expect(templateFunction).not.toContain('keydown.enter'); + expect(templateFunction).not.toContain('keyup.enter'); + }); + + it('should render remove alias link with href and pass $event into removeAlias', () => { + component.form.get('fm_personal_details_add_alias')?.setValue(true); + while (component.aliasControls.length < 2) { + component.addAlias(component.aliasControls.length, 'fm_personal_details_aliases'); + } + fixture.detectChanges(); + + const link = + (Array.from( + fixture.nativeElement.querySelectorAll('a.govuk-link.govuk-link--no-visited-state') as NodeListOf, + ).find((anchor) => anchor.textContent?.trim().startsWith('Remove')) as HTMLAnchorElement | undefined) ?? null; + expect(link).toBeTruthy(); + if (!link) throw new Error('Personal details remove alias link not found'); + + expect(link.getAttribute('href')).toBe(''); + expect(link.getAttribute('tabindex')).toBeNull(); + + const expectedIndex = component.aliasControls.length - 1; + const event = new MouseEvent('click', { bubbles: true, cancelable: true }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const removeAliasSpy = vi.spyOn(component, 'removeAlias'); + + link.dispatchEvent(event); + + expect(removeAliasSpy).toHaveBeenCalledWith(expectedIndex, 'fm_personal_details_aliases', event); + expect(event.defaultPrevented).toBe(true); + }); + it('should emit form submit event with form value', () => { const event = { submitter: { className: 'nested-flow' } } as SubmitEvent; formSubmit.nestedFlow = true; diff --git a/src/app/flows/fines/fines-mac/fines-mac-review-account/fines-mac-review-account-decision/fines-mac-review-account-decision-form/fines-mac-review-account-decision-form.component.html b/src/app/flows/fines/fines-mac/fines-mac-review-account/fines-mac-review-account-decision/fines-mac-review-account-decision-form/fines-mac-review-account-decision-form.component.html index b5d4cb2921..1a29c679fc 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-review-account/fines-mac-review-account-decision/fines-mac-review-account-decision-form/fines-mac-review-account-decision-form.component.html +++ b/src/app/flows/fines/fines-mac/fines-mac-review-account/fines-mac-review-account-decision/fines-mac-review-account-decision-form/fines-mac-review-account-decision-form.component.html @@ -47,12 +47,9 @@
diff --git a/src/app/flows/fines/fines-mac/fines-mac-review-account/fines-mac-review-account-decision/fines-mac-review-account-decision-form/fines-mac-review-account-decision-form.component.spec.ts b/src/app/flows/fines/fines-mac/fines-mac-review-account/fines-mac-review-account-decision/fines-mac-review-account-decision-form/fines-mac-review-account-decision-form.component.spec.ts index 5bc3ecb210..5ab37c4e8a 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-review-account/fines-mac-review-account-decision/fines-mac-review-account-decision-form/fines-mac-review-account-decision-form.component.spec.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-review-account/fines-mac-review-account-decision/fines-mac-review-account-decision-form/fines-mac-review-account-decision-form.component.spec.ts @@ -49,6 +49,26 @@ describe('FinesMacReviewAccountDecisionFormComponent', () => { expect(component).toBeTruthy(); }); + it('should render delete account link with required classes and pass $event to handleRoute', () => { + const link = fixture.nativeElement.querySelector('a.govuk-link.govuk-error-colour') as HTMLAnchorElement | null; + expect(link).toBeTruthy(); + if (!link) throw new Error('Delete account link not found'); + + expect(link.classList.contains('govuk-link')).toBe(true); + expect(link.classList.contains('govuk-link--no-visited-state')).toBe(true); + expect(link.classList.contains('govuk-error-colour')).toBe(true); + expect(link.getAttribute('href')).toBe(''); + + const event = new MouseEvent('click', { bubbles: true, cancelable: true }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handleRouteSpy = vi.spyOn(component, 'handleRoute'); + const expectedRoute = `${component.finesMacRoutes.children.deleteAccountConfirmation}/${component.accountId}`; + + link.dispatchEvent(event); + + expect(handleRouteSpy).toHaveBeenCalledWith(expectedRoute, { event }); + }); + it('should not emit form submit event with form value', () => { const event = {} as SubmitEvent; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/app/flows/fines/fines-mac/fines-mac-review-account/fines-mac-review-account.component.html b/src/app/flows/fines/fines-mac/fines-mac-review-account/fines-mac-review-account.component.html index 8009bab3b9..e998a129c7 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-review-account/fines-mac-review-account.component.html +++ b/src/app/flows/fines/fines-mac/fines-mac-review-account/fines-mac-review-account.component.html @@ -124,13 +124,7 @@

Check account details

@if (!this.finesDraftStore.draft_account_id()) { } diff --git a/src/app/flows/fines/fines-mac/fines-mac-review-account/fines-mac-review-account.component.spec.ts b/src/app/flows/fines/fines-mac/fines-mac-review-account/fines-mac-review-account.component.spec.ts index 4998790191..77d6bba4f4 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-review-account/fines-mac-review-account.component.spec.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-review-account/fines-mac-review-account.component.spec.ts @@ -1,4 +1,4 @@ -import { TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FinesMacReviewAccountComponent } from './fines-mac-review-account.component'; import { provideRouter, ActivatedRoute } from '@angular/router'; import { of, throwError } from 'rxjs'; @@ -134,6 +134,7 @@ function createTestModule(snapshotData?: any) { describe('FinesMacReviewAccountComponent', () => { describe('when snapshot has localJusticeAreas, courts, results, and major creditors only', () => { let component: FinesMacReviewAccountComponent; + let fixture: ComponentFixture; let mockOpalFinesService: Partial; // eslint-disable-next-line @typescript-eslint/no-explicit-any let mockFinesMacPayloadService: any; @@ -145,6 +146,7 @@ describe('FinesMacReviewAccountComponent', () => { beforeEach(async () => { const setup = createTestModule(); component = setup.component; + fixture = setup.fixture; mockOpalFinesService = setup.mockOpalFinesService; mockFinesMacPayloadService = setup.mockFinesMacPayloadService; mockUtilsService = setup.mockUtilsService; @@ -156,6 +158,33 @@ describe('FinesMacReviewAccountComponent', () => { expect(component).toBeTruthy(); }); + it('should enforce current template link semantics for delete account link', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const templateConsts = ((FinesMacReviewAccountComponent as any).ɵcmp?.consts ?? []).filter( + (entry: unknown) => Array.isArray(entry), + ) as unknown[][]; + const templateFunction = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ((FinesMacReviewAccountComponent as any).ɵcmp?.template?.toString() as string | undefined) ?? ''; + const actionLinkConsts = templateConsts.filter( + (entry) => + entry.includes('govuk-link') && + entry.includes('govuk-error-colour') && + entry.includes('href') && + entry.includes('click'), + ); + + expect(actionLinkConsts.length).toBeGreaterThanOrEqual(1); + actionLinkConsts.forEach((entry) => { + expect(entry).toContain('govuk-link--no-visited-state'); + expect(entry).toContain('href'); + expect(entry).toContain(''); + expect(entry).not.toContain('tabindex'); + }); + expect(templateFunction).not.toContain('keydown.enter'); + expect(templateFunction).not.toContain('keyup.enter'); + }); + it('should test setReviewAccountStatus when draft state is null', () => { finesDraftStore.setFinesDraftState(FINES_DRAFT_STATE); component['setReviewAccountStatus'](); @@ -492,6 +521,7 @@ describe('FinesMacReviewAccountComponent', () => { component.handleDeleteAccount(mockEvent); expect(routerSpy).toHaveBeenCalledWith([route], { relativeTo: component['activatedRoute'].parent }); + expect(mockEvent.preventDefault).toHaveBeenCalled(); expect(component['finesMacStore'].setDeleteFromCheckAccount).toHaveBeenCalledTimes(0); }); @@ -510,9 +540,33 @@ describe('FinesMacReviewAccountComponent', () => { component.handleDeleteAccount(mockEvent); expect(routerSpy).toHaveBeenCalledWith([route], { relativeTo: component['activatedRoute'].parent }); + expect(mockEvent.preventDefault).toHaveBeenCalled(); expect(component['finesMacStore'].setDeleteFromCheckAccount).toHaveBeenCalledTimes(1); }); + it('should click delete account link and prevent default via the passed template event', () => { + component.isReadOnly = false; + finesDraftStore.setDraftAccountId(0); + fixture.detectChanges(); + + const link = fixture.nativeElement.querySelector('a.govuk-link.govuk-error-colour') as HTMLAnchorElement | null; + expect(link).toBeTruthy(); + if (!link) throw new Error('Delete account link not found'); + + expect(link.getAttribute('href')).toBe(''); + expect(link.getAttribute('tabindex')).toBeNull(); + + const event = new MouseEvent('click', { bubbles: true, cancelable: true }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handlerSpy = vi.spyOn(component, 'handleDeleteAccount'); + + link.dispatchEvent(event); + + expect(handlerSpy).toHaveBeenCalled(); + expect(handlerSpy.mock.calls[0][0]).toBe(event); + expect(event.defaultPrevented).toBe(true); + }); + it('should scroll to top and return null on handleRequestError', () => { const result = component['handleRequestError'](); expect(mockUtilsService.scrollToTop).toHaveBeenCalled(); diff --git a/src/app/flows/fines/fines-mac/fines-mac-review-account/fines-mac-review-account.component.ts b/src/app/flows/fines/fines-mac/fines-mac-review-account/fines-mac-review-account.component.ts index 142608cfb7..009b241e77 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-review-account/fines-mac-review-account.component.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-review-account/fines-mac-review-account.component.ts @@ -435,6 +435,7 @@ export class FinesMacReviewAccountComponent extends AbstractFormParentBaseCompon * If true, the route will be treated as an absolute path. */ public handleDeleteAccount(event: Event, nonRelative = false): void { + event.preventDefault(); if (this.accountId > 0) { this.finesMacStore.setDeleteFromCheckAccount(true); this.routerNavigate( diff --git a/src/app/flows/fines/fines-mac/fines-mac-submit-confirmation/fines-mac-submit-confirmation.component.html b/src/app/flows/fines/fines-mac/fines-mac-submit-confirmation/fines-mac-submit-confirmation.component.html index 41481f2c9f..8540434f20 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-submit-confirmation/fines-mac-submit-confirmation.component.html +++ b/src/app/flows/fines/fines-mac/fines-mac-submit-confirmation/fines-mac-submit-confirmation.component.html @@ -5,20 +5,14 @@

Next steps

  • Create a new account
  • - See all accounts in review
  • diff --git a/src/app/flows/fines/fines-mac/fines-mac-submit-confirmation/fines-mac-submit-confirmation.component.spec.ts b/src/app/flows/fines/fines-mac/fines-mac-submit-confirmation/fines-mac-submit-confirmation.component.spec.ts index c21180a628..7fe54ac60a 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-submit-confirmation/fines-mac-submit-confirmation.component.spec.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-submit-confirmation/fines-mac-submit-confirmation.component.spec.ts @@ -65,6 +65,29 @@ describe('FinesMacSubmitConfirmationComponent', () => { expect(component).toBeTruthy(); }); + it('should enforce current template link semantics', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const templateConsts = ((FinesMacSubmitConfirmationComponent as any).ɵcmp?.consts ?? []).filter( + (entry: unknown) => Array.isArray(entry), + ) as unknown[][]; + const templateFunction = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ((FinesMacSubmitConfirmationComponent as any).ɵcmp?.template?.toString() as string | undefined) ?? ''; + const actionLinkConsts = templateConsts.filter( + (entry) => entry.includes('govuk-link') && entry.includes('href') && entry.includes('click'), + ); + + expect(actionLinkConsts.length).toBe(2); + actionLinkConsts.forEach((entry) => { + expect(entry).toContain('govuk-link--no-visited-state'); + expect(entry).toContain('href'); + expect(entry).toContain(''); + expect(entry).not.toContain('tabindex'); + }); + expect(templateFunction).not.toContain('keydown.enter'); + expect(templateFunction).not.toContain('keyup.enter'); + }); + it('should navigate to create account on createNewAccount', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const routerSpy = vi.spyOn(component['router'], 'navigate'); @@ -76,6 +99,40 @@ describe('FinesMacSubmitConfirmationComponent', () => { }); }); + it('should click create new account link and preserve current template click behaviour', () => { + const link = fixture.nativeElement.querySelector('#createNewAccount') as HTMLAnchorElement | null; + expect(link).toBeTruthy(); + if (!link) throw new Error('Create new account link not found'); + + expect(link.classList.contains('govuk-link')).toBe(true); + expect(link.classList.contains('govuk-link--no-visited-state')).toBe(true); + expect(link.getAttribute('href')).toBe(''); + expect(link.getAttribute('tabindex')).toBeNull(); + + const event = new MouseEvent('click', { bubbles: true, cancelable: true }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handlerSpy = vi.spyOn(component, 'createNewAccount'); + + link.dispatchEvent(event); + + expect(handlerSpy).toHaveBeenCalledWith(event); + expect(event.defaultPrevented).toBe(true); + }); + + it('should prevent default and navigate when createNewAccount is called with an event', () => { + const event = new Event('click'); + const preventDefaultSpy = vi.spyOn(event, 'preventDefault'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const routerSpy = vi.spyOn(component['router'], 'navigate'); + + component.createNewAccount(event); + + expect(preventDefaultSpy).toHaveBeenCalled(); + expect(routerSpy).toHaveBeenCalledWith([FINES_MAC_ROUTING_PATHS.children.createAccount], { + relativeTo: component['activatedRoute'].parent, + }); + }); + it('should navigate to create account on seeAllAccounts', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const routerSpy = vi.spyOn(component['router'], 'navigate'); @@ -91,4 +148,27 @@ describe('FinesMacSubmitConfirmationComponent', () => { }, ); }); + + it('should click see all accounts link and preserve current template click behaviour', () => { + const link = fixture.nativeElement.querySelector('#accountsInReview') as HTMLAnchorElement | null; + expect(link).toBeTruthy(); + if (!link) throw new Error('See all accounts link not found'); + + expect(link.classList.contains('govuk-link')).toBe(true); + expect(link.classList.contains('govuk-link--no-visited-state')).toBe(true); + expect(link.getAttribute('href')).toBe(''); + expect(link.getAttribute('tabindex')).toBeNull(); + + const event = new MouseEvent('click', { bubbles: true, cancelable: true }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handlerSpy = vi.spyOn(component, 'seeAllAccounts'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const resetStoreSpy = vi.spyOn(finesMacStore, 'resetStore'); + + link.dispatchEvent(event); + + expect(handlerSpy).toHaveBeenCalledWith(event); + expect(resetStoreSpy).toHaveBeenCalled(); + expect(event.defaultPrevented).toBe(true); + }); }); diff --git a/src/app/flows/fines/fines-mac/fines-mac-submit-confirmation/fines-mac-submit-confirmation.component.ts b/src/app/flows/fines/fines-mac/fines-mac-submit-confirmation/fines-mac-submit-confirmation.component.ts index c3d25e8ff1..0537d023b8 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-submit-confirmation/fines-mac-submit-confirmation.component.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-submit-confirmation/fines-mac-submit-confirmation.component.ts @@ -25,9 +25,11 @@ export class FinesMacSubmitConfirmationComponent { * by `FINES_MAC_ROUTING_PATHS.children.createAccount`. The navigation is * relative to the parent route of the current activated route. * + * @param event - The optional DOM event that triggered the navigation. * @returns {void} */ - public createNewAccount(): void { + public createNewAccount(event?: Event): void { + event?.preventDefault(); this.router.navigate([FINES_MAC_ROUTING_PATHS.children.createAccount], { relativeTo: this.activatedRoute.parent }); } @@ -39,9 +41,11 @@ export class FinesMacSubmitConfirmationComponent { * @remarks * This method is typically used to redirect the user to the "see all accounts" view. * + * @param event - The optional DOM event that triggered the navigation. * @returns {void} */ - public seeAllAccounts(): void { + public seeAllAccounts(event?: Event): void { + event?.preventDefault(); this.finesMacStore.resetStore(); this.router.navigate( diff --git a/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results-defendant-table-wrapper/fines-sa-results-defendant-table-wrapper.component.html b/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results-defendant-table-wrapper/fines-sa-results-defendant-table-wrapper.component.html index 837f19c133..a1acad227b 100644 --- a/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results-defendant-table-wrapper/fines-sa-results-defendant-table-wrapper.component.html +++ b/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results-defendant-table-wrapper/fines-sa-results-defendant-table-wrapper.component.html @@ -124,9 +124,8 @@ @if (row['Account ID']) { {{ row['Account']! }} } @else { diff --git a/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results-defendant-table-wrapper/fines-sa-results-defendant-table-wrapper.component.spec.ts b/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results-defendant-table-wrapper/fines-sa-results-defendant-table-wrapper.component.spec.ts index 83477a1230..f112d4ea11 100644 --- a/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results-defendant-table-wrapper/fines-sa-results-defendant-table-wrapper.component.spec.ts +++ b/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results-defendant-table-wrapper/fines-sa-results-defendant-table-wrapper.component.spec.ts @@ -27,6 +27,32 @@ describe('FinesSaResultsDefendantTableWrapperComponent', () => { expect(component).toBeTruthy(); }); + it('should enforce current template link semantics', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const templateConsts = ((FinesSaResultsDefendantTableWrapperComponent as any).ɵcmp?.consts ?? []).filter( + (entry: unknown) => Array.isArray(entry), + ) as unknown[][]; + const templateFunction = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ((FinesSaResultsDefendantTableWrapperComponent as any).ɵcmp?.template?.toString() as string | undefined) ?? ''; + const actionLinkConsts = templateConsts.filter( + (entry) => + entry.includes('govuk-link') && + entry.includes('govuk-link--no-visited-state') && + entry.includes('href') && + entry.includes('click'), + ); + + expect(actionLinkConsts.length).toBeGreaterThanOrEqual(1); + actionLinkConsts.forEach((entry) => { + expect(entry).toContain('href'); + expect(entry).toContain(''); + expect(entry).not.toContain('tabindex'); + }); + expect(templateFunction).not.toContain('keydown.enter'); + expect(templateFunction).not.toContain('keyup.enter'); + }); + it('should populate table data when tableData input is set', () => { expect(component['sortedTableDataSignal']()).toEqual(GENERATE_FINES_SA_DEFENDANT_TABLE_WRAPPER_TABLE_DATA_MOCKS(1)); }); @@ -48,4 +74,37 @@ describe('FinesSaResultsDefendantTableWrapperComponent', () => { expect(component.accountIdClicked.emit).toHaveBeenCalledWith(77); }); + + it('should prevent default and emit account id when goToAccount is called with an event', () => { + const event = new Event('click'); + const preventDefaultSpy = vi.spyOn(event, 'preventDefault'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const emitSpy = vi.spyOn(component.accountIdClicked, 'emit'); + + component.goToAccount(77, event); + + expect(preventDefaultSpy).toHaveBeenCalled(); + expect(emitSpy).toHaveBeenCalledWith(77); + }); + + it('should click account link and prevent default via the passed template event', () => { + const link = fixture.nativeElement.querySelector('a.govuk-link') as HTMLAnchorElement | null; + expect(link).toBeTruthy(); + if (!link) throw new Error('Account link not found'); + + expect(link.classList.contains('govuk-link--no-visited-state')).toBe(true); + expect(link.getAttribute('href')).toBe(''); + expect(link.getAttribute('tabindex')).toBeNull(); + + const event = new MouseEvent('click', { bubbles: true, cancelable: true }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handlerSpy = vi.spyOn(component, 'goToAccount'); + + link.dispatchEvent(event); + + expect(handlerSpy).toHaveBeenCalled(); + expect(handlerSpy.mock.calls[0][0]).toBe(1); + expect(handlerSpy.mock.calls[0][1]).toBe(event); + expect(event.defaultPrevented).toBe(true); + }); }); diff --git a/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results-defendant-table-wrapper/fines-sa-results-defendant-table-wrapper.component.ts b/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results-defendant-table-wrapper/fines-sa-results-defendant-table-wrapper.component.ts index 53bafdd43a..04a0b0c2ae 100644 --- a/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results-defendant-table-wrapper/fines-sa-results-defendant-table-wrapper.component.ts +++ b/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results-defendant-table-wrapper/fines-sa-results-defendant-table-wrapper.component.ts @@ -62,8 +62,10 @@ export class FinesSaResultsDefendantTableWrapperComponent extends AbstractSortab * Emits an event when an account ID is clicked, passing the selected account ID. * * @param accountID - The account ID that was clicked. + * @param event - The optional DOM event that triggered the click. */ - public goToAccount(accountID: number): void { + public goToAccount(accountID: number, event?: Event): void { + event?.preventDefault(); this.accountIdClicked.emit(accountID); } } diff --git a/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results-minor-creditor-table-wrapper/fines-sa-results-minor-creditor-table-wrapper.component.html b/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results-minor-creditor-table-wrapper/fines-sa-results-minor-creditor-table-wrapper.component.html index bc44661dbb..8543e054d1 100644 --- a/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results-minor-creditor-table-wrapper/fines-sa-results-minor-creditor-table-wrapper.component.html +++ b/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results-minor-creditor-table-wrapper/fines-sa-results-minor-creditor-table-wrapper.component.html @@ -75,9 +75,8 @@ @if (row['Creditor account id'] !== null) { {{ row['Account'] }} } @else { @@ -114,9 +113,8 @@ @if (row['Defendant account id'] !== null) { {{ row['Defendant'] }} } @else { diff --git a/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results-minor-creditor-table-wrapper/fines-sa-results-minor-creditor-table-wrapper.component.spec.ts b/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results-minor-creditor-table-wrapper/fines-sa-results-minor-creditor-table-wrapper.component.spec.ts index e1e6decd6f..7ebbca3745 100644 --- a/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results-minor-creditor-table-wrapper/fines-sa-results-minor-creditor-table-wrapper.component.spec.ts +++ b/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results-minor-creditor-table-wrapper/fines-sa-results-minor-creditor-table-wrapper.component.spec.ts @@ -27,6 +27,34 @@ describe('FinesSaResultsMinorCreditorTableWrapperComponent', () => { expect(component).toBeTruthy(); }); + it('should enforce current template link semantics', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const templateConsts = ((FinesSaResultsMinorCreditorTableWrapperComponent as any).ɵcmp?.consts ?? []).filter( + (entry: unknown) => Array.isArray(entry), + ) as unknown[][]; + const templateFunction = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ((FinesSaResultsMinorCreditorTableWrapperComponent as any).ɵcmp?.template?.toString() as + | string + | undefined) ?? ''; + const actionLinkConsts = templateConsts.filter( + (entry) => + entry.includes('govuk-link') && + entry.includes('govuk-link--no-visited-state') && + entry.includes('href') && + entry.includes('click'), + ); + + expect(actionLinkConsts.length).toBeGreaterThanOrEqual(1); + actionLinkConsts.forEach((entry) => { + expect(entry).toContain('href'); + expect(entry).toContain(''); + expect(entry).not.toContain('tabindex'); + }); + expect(templateFunction).not.toContain('keydown.enter'); + expect(templateFunction).not.toContain('keyup.enter'); + }); + it('should populate table data when tableData input is set', () => { expect(component['sortedTableDataSignal']()).toEqual( GENERATE_FINES_SA_MINOR_CREDITOR_TABLE_WRAPPER_TABLE_DATA_MOCKS(1), @@ -50,4 +78,38 @@ describe('FinesSaResultsMinorCreditorTableWrapperComponent', () => { expect(component.accountIdClicked.emit).toHaveBeenCalledWith(123); }); + + it('should prevent default and emit account id when goToAccount is called with an event', () => { + const event = new Event('click'); + const preventDefaultSpy = vi.spyOn(event, 'preventDefault'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const emitSpy = vi.spyOn(component.accountIdClicked, 'emit'); + + component.goToAccount(123, event); + + expect(preventDefaultSpy).toHaveBeenCalled(); + expect(emitSpy).toHaveBeenCalledWith(123); + }); + + it('should click minor creditor account link and preserve current template click behaviour', () => { + const link = fixture.nativeElement.querySelector('a.govuk-link') as HTMLAnchorElement | null; + expect(link).toBeTruthy(); + if (!link) throw new Error('Minor creditor account link not found'); + + expect(link.classList.contains('govuk-link--no-visited-state')).toBe(true); + expect(link.getAttribute('href')).toBe(''); + expect(link.getAttribute('tabindex')).toBeNull(); + + const event = new MouseEvent('click', { bubbles: true, cancelable: true }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handlerSpy = vi.spyOn(component, 'goToAccount'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const emitSpy = vi.spyOn(component.accountIdClicked, 'emit'); + + link.dispatchEvent(event); + + expect(handlerSpy).toHaveBeenCalledWith(0, event); + expect(event.defaultPrevented).toBe(true); + expect(emitSpy).toHaveBeenCalledWith(0); + }); }); diff --git a/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results-minor-creditor-table-wrapper/fines-sa-results-minor-creditor-table-wrapper.component.ts b/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results-minor-creditor-table-wrapper/fines-sa-results-minor-creditor-table-wrapper.component.ts index 8e5a673cdf..f7e14f4468 100644 --- a/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results-minor-creditor-table-wrapper/fines-sa-results-minor-creditor-table-wrapper.component.ts +++ b/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results-minor-creditor-table-wrapper/fines-sa-results-minor-creditor-table-wrapper.component.ts @@ -58,8 +58,10 @@ export class FinesSaResultsMinorCreditorTableWrapperComponent extends AbstractSo * Emits an event when an account ID is clicked, passing the selected account ID. * * @param accountId - The account ID that was clicked. + * @param event - The DOM event that triggered the click. */ - public goToAccount(accountId: number): void { + public goToAccount(accountId: number, event?: Event): void { + event?.preventDefault(); this.accountIdClicked.emit(accountId); } } diff --git a/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results.component.html b/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results.component.html index f3b0ef0c3a..a39c194d33 100644 --- a/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results.component.html +++ b/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results.component.html @@ -136,18 +136,14 @@

    Search results

    There are no matching results

    - Check your search + Check your search and try again

    There are more than 100 results

    - Try adding more information + Try adding more information to your search

    diff --git a/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results.component.spec.ts b/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results.component.spec.ts index f8ec94179b..3adb48e691 100644 --- a/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results.component.spec.ts +++ b/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results.component.spec.ts @@ -54,6 +54,29 @@ describe('FinesSaResultsComponent', () => { expect(component).toBeTruthy(); }); + it('should enforce current template link semantics', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const templateConsts = ((FinesSaResultsComponent as any).ɵcmp?.consts ?? []).filter((entry: unknown) => + Array.isArray(entry), + ) as unknown[][]; + const templateFunction = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ((FinesSaResultsComponent as any).ɵcmp?.template?.toString() as string | undefined) ?? ''; + const actionLinkConsts = templateConsts.filter( + (entry) => entry.includes('govuk-link') && entry.includes('href') && entry.includes('click'), + ); + + expect(actionLinkConsts.length).toBeGreaterThanOrEqual(1); + actionLinkConsts.forEach((entry) => { + expect(entry).toContain('govuk-link--no-visited-state'); + expect(entry).toContain('href'); + expect(entry).toContain(''); + expect(entry).not.toContain('tabindex'); + }); + expect(templateFunction).not.toContain('keydown.enter'); + expect(templateFunction).not.toContain('keyup.enter'); + }); + it('should initialise resultView, load snapshot data, and set up fragment listener on init', () => { const resultView = 'accountNumber'; const searchAccount = {}; @@ -274,6 +297,45 @@ describe('FinesSaResultsComponent', () => { }); }); + it('should prevent default and navigate when navigateBackToSearch is called with an event', () => { + const event = new Event('click'); + const preventDefaultSpy = vi.spyOn(event, 'preventDefault'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + vi.spyOn(finesSaStore, 'activeTab').mockReturnValue('companies'); + + component.navigateBackToSearch(event); + + expect(preventDefaultSpy).toHaveBeenCalled(); + expect(router.navigate).toHaveBeenCalledWith([component['finesSaSearchRoutingPaths'].root], { + relativeTo: component['activatedRoute'].parent, + fragment: 'companies', + }); + }); + + it('should click "Check your search" link and prevent default via the passed template event', () => { + component.resultView = 'individuals'; + component.individualsData = []; + fixture.detectChanges(); + + const link = Array.from( + fixture.nativeElement.querySelectorAll('a.govuk-link') as NodeListOf, + ).find((anchor) => anchor.textContent?.includes('Check your search')); + expect(link).toBeTruthy(); + if (!link) throw new Error('Check your search link not found'); + + expect(link.getAttribute('href')).toBe(''); + expect(link.getAttribute('tabindex')).toBeNull(); + + const event = new MouseEvent('click', { bubbles: true, cancelable: true }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handlerSpy = vi.spyOn(component, 'navigateBackToSearch'); + + link.dispatchEvent(event); + + expect(handlerSpy).toHaveBeenCalledWith(event); + expect(event.defaultPrevented).toBe(true); + }); + it('should return mapped individual defendant data with aliases', () => { const mockData: IOpalFinesDefendantAccountResponse = { count: 1, diff --git a/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results.component.ts b/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results.component.ts index 226805dab6..5449e81aef 100644 --- a/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results.component.ts +++ b/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results.component.ts @@ -369,8 +369,11 @@ export class FinesSaResultsComponent implements OnInit, OnDestroy { * This method uses the Angular Router to navigate to the root path defined in `finesSaSearchRoutingPaths`. * The navigation is performed relative to the parent of the current activated route. * Additionally, it sets the URL fragment to the currently active tab as determined by `finesSaStore.activeTab()`. + * + * @param event - The optional DOM event that triggered the navigation. */ - public navigateBackToSearch() { + public navigateBackToSearch(event?: Event) { + event?.preventDefault(); this['router'].navigate([this.finesSaSearchRoutingPaths.root], { relativeTo: this['activatedRoute'].parent, fragment: this.finesSaStore.activeTab(), diff --git a/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-account/fines-sa-search-account-form/fines-sa-search-account-form-major-creditors/fines-sa-search-account-form-major-creditors.component.html b/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-account/fines-sa-search-account-form/fines-sa-search-account-form-major-creditors/fines-sa-search-account-form-major-creditors.component.html index 30a325d6c3..39c629c954 100644 --- a/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-account/fines-sa-search-account-form/fines-sa-search-account-form-major-creditors/fines-sa-search-account-form-major-creditors.component.html +++ b/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-account/fines-sa-search-account-form/fines-sa-search-account-form-major-creditors/fines-sa-search-account-form-major-creditors.component.html @@ -13,12 +13,6 @@

    Major creditors

    } @else {

    To search major creditors, - filter by a single business unit + filter by a single business unit

    } diff --git a/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-account/fines-sa-search-account-form/fines-sa-search-account-form-major-creditors/fines-sa-search-account-form-major-creditors.component.spec.ts b/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-account/fines-sa-search-account-form/fines-sa-search-account-form-major-creditors/fines-sa-search-account-form-major-creditors.component.spec.ts index a4425316ae..4b192f19a1 100644 --- a/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-account/fines-sa-search-account-form/fines-sa-search-account-form-major-creditors/fines-sa-search-account-form-major-creditors.component.spec.ts +++ b/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-account/fines-sa-search-account-form/fines-sa-search-account-form-major-creditors/fines-sa-search-account-form-major-creditors.component.spec.ts @@ -38,6 +38,30 @@ describe('FinesSaSearchAccountFormMajorCreditorsComponent', () => { expect(component).toBeTruthy(); }); + it('should enforce filter link template semantics', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const templateConsts = ((FinesSaSearchAccountFormMajorCreditorsComponent as any).ɵcmp?.consts ?? []).filter( + (entry: unknown) => Array.isArray(entry), + ) as unknown[][]; + const templateFunction = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ((FinesSaSearchAccountFormMajorCreditorsComponent as any).ɵcmp?.template?.toString() as string | undefined) ?? + ''; + const filterLinkConsts = templateConsts.filter( + (entry) => entry.includes('govuk-link') && entry.includes('href') && entry.includes('click'), + ); + + expect(filterLinkConsts.length).toBeGreaterThanOrEqual(1); + filterLinkConsts.forEach((entry) => { + expect(entry).toContain('govuk-link--no-visited-state'); + expect(entry).toContain('href'); + expect(entry).toContain(''); + expect(entry).not.toContain('tabindex'); + }); + expect(templateFunction).not.toContain('keydown.enter'); + expect(templateFunction).not.toContain('keyup.enter'); + }); + it('should initialize the form on ngOnInit', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any vi.spyOn(component, 'setupMajorCreditorForm'); @@ -65,4 +89,40 @@ describe('FinesSaSearchAccountFormMajorCreditorsComponent', () => { expect(component['rePopulateForm']).toHaveBeenCalledWith(null); expect(mockFinesSaStore.resetDefendantSearchCriteria).toHaveBeenCalled(); }); + + it('should prevent default and emit when onFilterBusinessUnitClick is called', () => { + const event = new Event('click'); + const preventDefaultSpy = vi.spyOn(event, 'preventDefault'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const emitSpy = vi.spyOn(component.filterBusinessUnitClicked, 'emit'); + + component.onFilterBusinessUnitClick(event); + + expect(preventDefaultSpy).toHaveBeenCalled(); + expect(emitSpy).toHaveBeenCalled(); + }); + + it('should pass $event to onFilterBusinessUnitClick from filter link click', () => { + mockFinesSaStore.setBusinessUnitIds([1, 2]); + fixture.detectChanges(); + + const link = fixture.nativeElement.querySelector('a.govuk-link') as HTMLAnchorElement | null; + expect(link).toBeTruthy(); + if (!link) throw new Error('Filter by business unit link not found'); + + expect(link.classList.contains('govuk-link--no-visited-state')).toBe(true); + expect(link.getAttribute('href')).toBe(''); + expect(link.getAttribute('tabindex')).toBeNull(); + + const event = new MouseEvent('click', { bubbles: true, cancelable: true }); + const handlerSpy = vi.spyOn(component, 'onFilterBusinessUnitClick'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const emitSpy = vi.spyOn(component.filterBusinessUnitClicked, 'emit'); + + link.dispatchEvent(event); + + expect(handlerSpy).toHaveBeenCalledWith(event); + expect(event.defaultPrevented).toBe(true); + expect(emitSpy).toHaveBeenCalled(); + }); }); diff --git a/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-account/fines-sa-search-account-form/fines-sa-search-account-form-major-creditors/fines-sa-search-account-form-major-creditors.component.ts b/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-account/fines-sa-search-account-form/fines-sa-search-account-form-major-creditors/fines-sa-search-account-form-major-creditors.component.ts index 114dc2b458..71eda4f670 100644 --- a/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-account/fines-sa-search-account-form/fines-sa-search-account-form-major-creditors/fines-sa-search-account-form-major-creditors.component.ts +++ b/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-account/fines-sa-search-account-form/fines-sa-search-account-form-major-creditors/fines-sa-search-account-form-major-creditors.component.ts @@ -55,6 +55,16 @@ export class FinesSaSearchAccountFormMajorCreditorsComponent extends AbstractNes this.finesSaStore.resetDefendantSearchCriteria(); } + /** + * Emits the business unit filter action. + * + * @param event - The DOM event that triggered the filter link. + */ + public onFilterBusinessUnitClick(event: Event): void { + event.preventDefault(); + this.filterBusinessUnitClicked.emit(); + } + public override ngOnInit(): void { this.setupMajorCreditorForm(); super.ngOnInit(); diff --git a/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-problem/fines-sa-search-problem.component.html b/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-problem/fines-sa-search-problem.component.html index 668eceb6d3..80ed8c06e7 100644 --- a/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-problem/fines-sa-search-problem.component.html +++ b/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-problem/fines-sa-search-problem.component.html @@ -9,5 +9,5 @@

    There is a problem

  • reference or case number, or
  • selected tab
  • - Go back + Go back
    diff --git a/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-problem/fines-sa-search-problem.component.spec.ts b/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-problem/fines-sa-search-problem.component.spec.ts index 02dc01c107..a8de80a182 100644 --- a/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-problem/fines-sa-search-problem.component.spec.ts +++ b/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-problem/fines-sa-search-problem.component.spec.ts @@ -39,6 +39,29 @@ describe('FinesSaSearchProblemComponent', () => { expect(component).toBeTruthy(); }); + it('should enforce current template link semantics', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const templateConsts = ((FinesSaSearchProblemComponent as any).ɵcmp?.consts ?? []).filter((entry: unknown) => + Array.isArray(entry), + ) as unknown[][]; + const templateFunction = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ((FinesSaSearchProblemComponent as any).ɵcmp?.template?.toString() as string | undefined) ?? ''; + const actionLinkConsts = templateConsts.filter( + (entry) => entry.includes('govuk-link') && entry.includes('href') && entry.includes('click'), + ); + + expect(actionLinkConsts.length).toBeGreaterThanOrEqual(1); + actionLinkConsts.forEach((entry) => { + expect(entry).toContain('govuk-link--no-visited-state'); + expect(entry).toContain('href'); + expect(entry).toContain(''); + expect(entry).not.toContain('tabindex'); + }); + expect(templateFunction).not.toContain('keydown.enter'); + expect(templateFunction).not.toContain('keyup.enter'); + }); + it('should navigate back with fragment when goBack is called', () => { component.goBack(); expect(routerSpy.navigate).toHaveBeenCalledWith([FINES_SA_SEARCH_ROUTING_PATHS.root], { @@ -46,4 +69,36 @@ describe('FinesSaSearchProblemComponent', () => { fragment: 'individuals', }); }); + + it('should prevent default and navigate when goBack is called with an event', () => { + const event = new Event('click'); + const preventDefaultSpy = vi.spyOn(event, 'preventDefault'); + + component.goBack(event); + + expect(preventDefaultSpy).toHaveBeenCalled(); + expect(routerSpy.navigate).toHaveBeenCalledWith([FINES_SA_SEARCH_ROUTING_PATHS.root], { + relativeTo: component['activatedRoute'].parent, + fragment: 'individuals', + }); + }); + + it('should click go back link and prevent default via the passed template event', () => { + const link = fixture.nativeElement.querySelector('a.govuk-link') as HTMLAnchorElement | null; + expect(link).toBeTruthy(); + if (!link) throw new Error('Go back link not found'); + + expect(link.classList.contains('govuk-link--no-visited-state')).toBe(true); + expect(link.getAttribute('href')).toBe(''); + expect(link.getAttribute('tabindex')).toBeNull(); + + const event = new MouseEvent('click', { bubbles: true, cancelable: true }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handlerSpy = vi.spyOn(component, 'goBack'); + + link.dispatchEvent(event); + + expect(handlerSpy).toHaveBeenCalledWith(event); + expect(event.defaultPrevented).toBe(true); + }); }); diff --git a/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-problem/fines-sa-search-problem.component.ts b/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-problem/fines-sa-search-problem.component.ts index 1a31d1727f..adfaff023d 100644 --- a/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-problem/fines-sa-search-problem.component.ts +++ b/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-problem/fines-sa-search-problem.component.ts @@ -18,8 +18,11 @@ export class FinesSaSearchProblemComponent { * Navigates the user back to the root path of the fines search section. * Utilizes Angular's Router to perform navigation relative to the parent route * of the current ActivatedRoute. + * + * @param event - The optional DOM event that triggered the navigation. */ - public goBack(): void { + public goBack(event?: Event): void { + event?.preventDefault(); this.router.navigate([this.finesSaSearchRoutingPaths.root], { relativeTo: this.activatedRoute.parent, fragment: this.finesSaStore.activeTab(), From 179994bb75631b9adf37e5053548b441eb6d2bce Mon Sep 17 00:00:00 2001 From: Christopher Garratt Date: Tue, 31 Mar 2026 09:46:05 +0100 Subject: [PATCH 05/11] Updating to new ui library version. Adding `no-link-visited` where missing and updating tests. --- package.json | 2 +- ...nes-con-search-account-form.component.html | 8 ++- ...-con-search-account-form.component.spec.ts | 69 +++++++++++++++++++ ...-fixed-penalty-details-form.component.html | 2 +- ...xed-penalty-details-form.component.spec.ts | 17 +++++ ...-mac-submit-confirmation.component.spec.ts | 6 +- ...fines-mac-submit-confirmation.component.ts | 4 +- .../pages/dashboard/dashboard.component.html | 8 +-- .../dashboard/dashboard.component.spec.ts | 35 +++++++++- yarn.lock | 10 +-- 10 files changed, 143 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 64750de0e2..f8309c56f9 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "@hmcts/info-provider": "^1.1.0", "@hmcts/nodejs-healthcheck": "^1.8.5", "@hmcts/nodejs-logging": "^4.0.4", - "@hmcts/opal-frontend-common": "^0.0.66", + "@hmcts/opal-frontend-common": "^0.0.67", "@hmcts/opal-frontend-common-node": "^0.0.27", "@hmcts/properties-volume": "^1.1.0", "@hmcts/zephyr-automation-nodejs": "0.0.6", diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-account/fines-con-search-account-form/fines-con-search-account-form.component.html b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-account/fines-con-search-account-form/fines-con-search-account-form.component.html index 4ac47718b3..069357f1d1 100644 --- a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-account/fines-con-search-account-form/fines-con-search-account-form.component.html +++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-account/fines-con-search-account-form/fines-con-search-account-form.component.html @@ -58,7 +58,13 @@

    Quick search

    diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-account/fines-con-search-account-form/fines-con-search-account-form.component.spec.ts b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-account/fines-con-search-account-form/fines-con-search-account-form.component.spec.ts index 0a59855450..24f396edc4 100644 --- a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-account/fines-con-search-account-form/fines-con-search-account-form.component.spec.ts +++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-account/fines-con-search-account-form/fines-con-search-account-form.component.spec.ts @@ -87,4 +87,73 @@ describe('FinesConSearchAccountFormComponent', () => { expect(component.form.get('fcon_search_account_number')?.value).toBeNull(); }); + + it('should enforce current clear search link semantics', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const templateConsts = ((FinesConSearchAccountFormComponent as any).ɵcmp?.consts ?? []).filter((entry: unknown) => + Array.isArray(entry), + ) as unknown[][]; + const templateFunction = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ((FinesConSearchAccountFormComponent as any).ɵcmp?.template?.toString() as string | undefined) ?? ''; + const clearSearchLinkConst = templateConsts.find( + (entry) => + entry.includes('govuk-link') && + entry.includes('govuk-link--no-visited-state') && + entry.includes('href') && + entry.includes('click'), + ); + + expect(clearSearchLinkConst).toBeTruthy(); + expect(clearSearchLinkConst).toContain('href'); + expect(clearSearchLinkConst).toContain(''); + expect(clearSearchLinkConst).not.toContain('tabindex'); + expect(templateFunction).not.toContain('keydown.enter'); + expect(templateFunction).not.toContain('keyup.enter'); + }); + + it('should pass $event from the clear search link click and preserve current behaviour', () => { + const link = fixture.nativeElement.querySelector('a.govuk-link') as HTMLAnchorElement | null; + expect(link).toBeTruthy(); + if (!link) throw new Error('Clear search link not found'); + + component.form.patchValue({ fcon_search_account_number: '12345678' }); + + expect(link.textContent?.trim()).toBe('Clear search'); + expect(link.classList.contains('govuk-link')).toBe(true); + expect(link.classList.contains('govuk-link--no-visited-state')).toBe(true); + expect(link.getAttribute('href')).toBe(''); + expect(link.getAttribute('tabindex')).toBeNull(); + + const event = new MouseEvent('click', { bubbles: true, cancelable: true }); + const handlerSpy = vi.spyOn(component, 'clearSearchForm'); + const resetSpy = vi.spyOn(finesConStore, 'resetSearchAccountForm'); + + link.dispatchEvent(event); + + expect(handlerSpy).toHaveBeenCalledWith(event); + expect(event.defaultPrevented).toBe(true); + expect(resetSpy).toHaveBeenCalled(); + expect(component.form.get('fcon_search_account_number')?.value).toBeNull(); + }); + + it('should prevent default and keep the existing reset logic when clearSearchForm is called', () => { + component.form.patchValue({ fcon_search_account_number: '12345678' }); + + const event = new Event('click'); + const preventDefaultSpy = vi.spyOn(event, 'preventDefault'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const clearAllErrorMessagesSpy = vi.spyOn(component, 'clearAllErrorMessages'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const setInitialErrorMessagesSpy = vi.spyOn(component, 'setInitialErrorMessages'); + const resetSpy = vi.spyOn(finesConStore, 'resetSearchAccountForm'); + + component.clearSearchForm(event); + + expect(preventDefaultSpy).toHaveBeenCalled(); + expect(clearAllErrorMessagesSpy).toHaveBeenCalled(); + expect(setInitialErrorMessagesSpy).toHaveBeenCalled(); + expect(resetSpy).toHaveBeenCalled(); + expect(component.form.get('fcon_search_account_number')?.value).toBeNull(); + }); }); diff --git a/src/app/flows/fines/fines-mac/fines-mac-fixed-penalty-details/fines-mac-fixed-penalty-details-form/fines-mac-fixed-penalty-details-form.component.html b/src/app/flows/fines/fines-mac/fines-mac-fixed-penalty-details/fines-mac-fixed-penalty-details-form/fines-mac-fixed-penalty-details-form.component.html index cd9727a44a..2e346ad01a 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-fixed-penalty-details/fines-mac-fixed-penalty-details-form/fines-mac-fixed-penalty-details-form.component.html +++ b/src/app/flows/fines/fines-mac/fines-mac-fixed-penalty-details/fines-mac-fixed-penalty-details-form/fines-mac-fixed-penalty-details-form.component.html @@ -254,7 +254,7 @@

    Offence Details

    > For example, HY35014. If you don't know the offence code, you can { expect(component).toBeTruthy(); }); + it('should render the search offence list link with the required classes and attributes', () => { + const link = fixture.nativeElement.querySelector( + 'a.govuk-link.govuk-task-list__link.govuk-link--no-visited-state', + ) as HTMLAnchorElement | null; + + expect(link).toBeTruthy(); + if (!link) throw new Error('Search offence list link not found'); + + expect(link.textContent?.trim()).toBe('search the offence list'); + expect(link.classList.contains('govuk-link')).toBe(true); + expect(link.classList.contains('govuk-task-list__link')).toBe(true); + expect(link.classList.contains('govuk-link--no-visited-state')).toBe(true); + expect(link.getAttribute('href')).toBe(component.searchOffenceUrl); + expect(link.getAttribute('target')).toBe('_blank'); + expect(link.getAttribute('rel')).toBe('noopener noreferrer'); + }); + it('should create the form with the correct controls', () => { component['setupFixedPenaltyDetailsForm'](); Object.keys(FINES_MAC_FIXED_PENALTY_DETAILS_FORM_MOCK.formData).forEach((key) => { diff --git a/src/app/flows/fines/fines-mac/fines-mac-submit-confirmation/fines-mac-submit-confirmation.component.spec.ts b/src/app/flows/fines/fines-mac/fines-mac-submit-confirmation/fines-mac-submit-confirmation.component.spec.ts index 639e1efe57..e2395f6e30 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-submit-confirmation/fines-mac-submit-confirmation.component.spec.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-submit-confirmation/fines-mac-submit-confirmation.component.spec.ts @@ -88,7 +88,7 @@ describe('FinesMacSubmitConfirmationComponent', () => { expect(templateFunction).not.toContain('keyup.enter'); }); - it('should navigate to create account on createNewAccount', () => { + it('should navigate to originator type on createNewAccount', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const routerSpy = vi.spyOn(component['router'], 'navigate'); @@ -119,7 +119,7 @@ describe('FinesMacSubmitConfirmationComponent', () => { expect(event.defaultPrevented).toBe(true); }); - it('should prevent default and navigate when createNewAccount is called with an event', () => { + it('should prevent default and navigate to originator type when createNewAccount is called with an event', () => { const event = new Event('click'); const preventDefaultSpy = vi.spyOn(event, 'preventDefault'); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -128,7 +128,7 @@ describe('FinesMacSubmitConfirmationComponent', () => { component.createNewAccount(event); expect(preventDefaultSpy).toHaveBeenCalled(); - expect(routerSpy).toHaveBeenCalledWith([FINES_MAC_ROUTING_PATHS.children.createAccount], { + expect(routerSpy).toHaveBeenCalledWith([FINES_MAC_ROUTING_PATHS.children.originatorType], { relativeTo: component['activatedRoute'].parent, }); }); diff --git a/src/app/flows/fines/fines-mac/fines-mac-submit-confirmation/fines-mac-submit-confirmation.component.ts b/src/app/flows/fines/fines-mac/fines-mac-submit-confirmation/fines-mac-submit-confirmation.component.ts index 2dcbe543f0..d7902b94cb 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-submit-confirmation/fines-mac-submit-confirmation.component.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-submit-confirmation/fines-mac-submit-confirmation.component.ts @@ -19,10 +19,10 @@ export class FinesMacSubmitConfirmationComponent { private readonly finesMacStore = inject(FinesMacStore); /** - * Navigates to the create account page within the fines MAC flow. + * Navigates to the originator type step within the fines MAC flow. * * This method uses the Angular Router to navigate to the route specified - * by `FINES_MAC_ROUTING_PATHS.children.createAccount`. The navigation is + * by `FINES_MAC_ROUTING_PATHS.children.originatorType`. The navigation is * relative to the parent route of the current activated route. * * @param event - The optional DOM event that triggered the navigation. diff --git a/src/app/pages/dashboard/dashboard.component.html b/src/app/pages/dashboard/dashboard.component.html index ffe5b941b8..e463ec7509 100644 --- a/src/app/pages/dashboard/dashboard.component.html +++ b/src/app/pages/dashboard/dashboard.component.html @@ -7,7 +7,7 @@

    Dashboard

    @if (permissionIds.includes(dashboardPermissions['check-and-validate-draft-accounts'])) {
  • Dashboard
  • @if (permissionIds.includes(dashboardPermissions['create-and-manage-draft-accounts'])) {
  • Dashboard @if (permissionIds.includes(dashboardPermissions['search-and-view-accounts'])) {
  • Dashboard @if (permissionIds.includes(dashboardPermissions['consolidate'])) {
  • Date: Wed, 1 Apr 2026 14:34:59 +0100 Subject: [PATCH 06/11] Resolving codex review, removing redundant `govuk-task-list__link` class. Adjusting test. --- .../fines-mac-fixed-penalty-details-form.component.html | 2 +- .../fines-mac-fixed-penalty-details-form.component.spec.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/flows/fines/fines-mac/fines-mac-fixed-penalty-details/fines-mac-fixed-penalty-details-form/fines-mac-fixed-penalty-details-form.component.html b/src/app/flows/fines/fines-mac/fines-mac-fixed-penalty-details/fines-mac-fixed-penalty-details-form/fines-mac-fixed-penalty-details-form.component.html index 2e346ad01a..1e3ddaf2ec 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-fixed-penalty-details/fines-mac-fixed-penalty-details-form/fines-mac-fixed-penalty-details-form.component.html +++ b/src/app/flows/fines/fines-mac/fines-mac-fixed-penalty-details/fines-mac-fixed-penalty-details-form/fines-mac-fixed-penalty-details-form.component.html @@ -254,7 +254,7 @@

    Offence Details

    > For example, HY35014. If you don't know the offence code, you can
    { it('should render the search offence list link with the required classes and attributes', () => { const link = fixture.nativeElement.querySelector( - 'a.govuk-link.govuk-task-list__link.govuk-link--no-visited-state', + 'a.govuk-link.govuk-link--no-visited-state', ) as HTMLAnchorElement | null; expect(link).toBeTruthy(); @@ -121,7 +121,6 @@ describe('FinesMacFixedPenaltyFormComponent', () => { expect(link.textContent?.trim()).toBe('search the offence list'); expect(link.classList.contains('govuk-link')).toBe(true); - expect(link.classList.contains('govuk-task-list__link')).toBe(true); expect(link.classList.contains('govuk-link--no-visited-state')).toBe(true); expect(link.getAttribute('href')).toBe(component.searchOffenceUrl); expect(link.getAttribute('target')).toBe('_blank'); From 9d4435ef136e548a32275ed83cfb6632c000b206 Mon Sep 17 00:00:00 2001 From: Christopher Garratt Date: Wed, 1 Apr 2026 14:51:36 +0100 Subject: [PATCH 07/11] yarn prettier:fix --- ...y-add-amend-convert-form.component.spec.ts | 14 ++++++++++--- ...-payment-terms-amend-denied.component.html | 8 ++++++- ...-payment-card-access-denied.component.html | 8 ++++++- ...yment-card-access-denied.component.spec.ts | 3 +-- ...mac-company-details-form.component.spec.ts | 8 ++++--- ...ails-add-an-offence-form.component.spec.ts | 21 +++++++++++++------ ...nt-guardian-details-form.component.spec.ts | 4 +++- ...ac-personal-details-form.component.spec.ts | 8 ++++--- .../fines-mac-review-account.component.html | 7 ++++++- ...fines-mac-review-account.component.spec.ts | 4 ++-- ...-mac-submit-confirmation.component.spec.ts | 4 ++-- ...r-creditor-table-wrapper.component.spec.ts | 5 ++--- .../fines-sa-results.component.html | 8 +++++-- ...ccount-form-major-creditors.component.html | 4 +++- ...unt-form-major-creditors.component.spec.ts | 3 +-- 15 files changed, 76 insertions(+), 33 deletions(-) diff --git a/src/app/flows/fines/fines-acc/fines-acc-party-add-amend-convert/fines-acc-party-add-amend-convert-form/fines-acc-party-add-amend-convert-form.component.spec.ts b/src/app/flows/fines/fines-acc/fines-acc-party-add-amend-convert/fines-acc-party-add-amend-convert-form/fines-acc-party-add-amend-convert-form.component.spec.ts index 5da625d59a..58f5eb135e 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-party-add-amend-convert/fines-acc-party-add-amend-convert-form/fines-acc-party-add-amend-convert-form.component.spec.ts +++ b/src/app/flows/fines/fines-acc/fines-acc-party-add-amend-convert/fines-acc-party-add-amend-convert-form/fines-acc-party-add-amend-convert-form.component.spec.ts @@ -110,7 +110,9 @@ describe('FinesAccPartyAddAmendConvertFormComponent', () => { const link = (Array.from( - fixture.nativeElement.querySelectorAll('a.govuk-link.govuk-link--no-visited-state') as NodeListOf, + fixture.nativeElement.querySelectorAll( + 'a.govuk-link.govuk-link--no-visited-state', + ) as NodeListOf, ).find((anchor) => anchor.textContent?.trim().startsWith('Remove')) as HTMLAnchorElement | undefined) ?? null; expect(link).toBeTruthy(); if (!link) throw new Error('Individual remove alias link not found'); @@ -125,7 +127,11 @@ describe('FinesAccPartyAddAmendConvertFormComponent', () => { link.dispatchEvent(event); - expect(removeAliasSpy).toHaveBeenCalledWith(expectedIndex, 'facc_party_add_amend_convert_individual_aliases', event); + expect(removeAliasSpy).toHaveBeenCalledWith( + expectedIndex, + 'facc_party_add_amend_convert_individual_aliases', + event, + ); expect(event.defaultPrevented).toBe(true); }); @@ -141,7 +147,9 @@ describe('FinesAccPartyAddAmendConvertFormComponent', () => { const link = (Array.from( - fixture.nativeElement.querySelectorAll('a.govuk-link.govuk-link--no-visited-state') as NodeListOf, + fixture.nativeElement.querySelectorAll( + 'a.govuk-link.govuk-link--no-visited-state', + ) as NodeListOf, ).find((anchor) => anchor.textContent?.trim().startsWith('Remove')) as HTMLAnchorElement | undefined) ?? null; expect(link).toBeTruthy(); if (!link) throw new Error('Company remove alias link not found'); diff --git a/src/app/flows/fines/fines-acc/fines-acc-payment-terms-amend-denied/fines-acc-payment-terms-amend-denied.component.html b/src/app/flows/fines/fines-acc/fines-acc-payment-terms-amend-denied/fines-acc-payment-terms-amend-denied.component.html index b7c5060160..13f7642479 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-payment-terms-amend-denied/fines-acc-payment-terms-amend-denied.component.html +++ b/src/app/flows/fines/fines-acc/fines-acc-payment-terms-amend-denied/fines-acc-payment-terms-amend-denied.component.html @@ -23,4 +23,10 @@ } } - Go back + + Go back diff --git a/src/app/flows/fines/fines-acc/fines-acc-request-payment-card-access-denied/fines-acc-request-payment-card-access-denied.component.html b/src/app/flows/fines/fines-acc/fines-acc-request-payment-card-access-denied/fines-acc-request-payment-card-access-denied.component.html index a02c201b34..404e023482 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-request-payment-card-access-denied/fines-acc-request-payment-card-access-denied.component.html +++ b/src/app/flows/fines/fines-acc/fines-acc-request-payment-card-access-denied/fines-acc-request-payment-card-access-denied.component.html @@ -14,4 +14,10 @@ } } - Go back + + Go back diff --git a/src/app/flows/fines/fines-acc/fines-acc-request-payment-card-access-denied/fines-acc-request-payment-card-access-denied.component.spec.ts b/src/app/flows/fines/fines-acc/fines-acc-request-payment-card-access-denied/fines-acc-request-payment-card-access-denied.component.spec.ts index 88b82460b8..66e472d37e 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-request-payment-card-access-denied/fines-acc-request-payment-card-access-denied.component.spec.ts +++ b/src/app/flows/fines/fines-acc/fines-acc-request-payment-card-access-denied/fines-acc-request-payment-card-access-denied.component.spec.ts @@ -60,8 +60,7 @@ describe('FinesAccRequestPaymentCardAccessDeniedComponent', () => { ) as unknown[][]; const templateFunction = // eslint-disable-next-line @typescript-eslint/no-explicit-any - ((FinesAccRequestPaymentCardAccessDeniedComponent as any).ɵcmp?.template?.toString() as string | undefined) ?? - ''; + ((FinesAccRequestPaymentCardAccessDeniedComponent as any).ɵcmp?.template?.toString() as string | undefined) ?? ''; const actionLinkConsts = templateConsts.filter( (entry) => entry.includes('govuk-link') && entry.includes('href') && entry.includes('click'), ); diff --git a/src/app/flows/fines/fines-mac/fines-mac-company-details/fines-mac-company-details-form/fines-mac-company-details-form.component.spec.ts b/src/app/flows/fines/fines-mac/fines-mac-company-details/fines-mac-company-details-form/fines-mac-company-details-form.component.spec.ts index 2cc83f3594..63cfc392b4 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-company-details/fines-mac-company-details-form/fines-mac-company-details-form.component.spec.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-company-details/fines-mac-company-details-form/fines-mac-company-details-form.component.spec.ts @@ -57,8 +57,8 @@ describe('FinesMacCompanyDetailsFormComponent', () => { it('should enforce remove alias link template semantics', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any - const templateConsts = ((FinesMacCompanyDetailsFormComponent as any).ɵcmp?.consts ?? []).filter( - (entry: unknown) => Array.isArray(entry), + const templateConsts = ((FinesMacCompanyDetailsFormComponent as any).ɵcmp?.consts ?? []).filter((entry: unknown) => + Array.isArray(entry), ) as unknown[][]; const templateFunction = // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -90,7 +90,9 @@ describe('FinesMacCompanyDetailsFormComponent', () => { const link = (Array.from( - fixture.nativeElement.querySelectorAll('a.govuk-link.govuk-link--no-visited-state') as NodeListOf, + fixture.nativeElement.querySelectorAll( + 'a.govuk-link.govuk-link--no-visited-state', + ) as NodeListOf, ).find((anchor) => anchor.textContent?.trim().startsWith('Remove')) as HTMLAnchorElement | undefined) ?? null; expect(link).toBeTruthy(); if (!link) throw new Error('Company remove alias link not found'); diff --git a/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.spec.ts b/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.spec.ts index 8ae25588f8..561cd5ba5c 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.spec.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.spec.ts @@ -125,8 +125,7 @@ describe('FinesMacOffenceDetailsAddAnOffenceFormComponent', () => { ) as unknown[][]; const templateFunction = // eslint-disable-next-line @typescript-eslint/no-explicit-any - ((FinesMacOffenceDetailsAddAnOffenceFormComponent as any).ɵcmp?.template?.toString() as string | undefined) ?? - ''; + ((FinesMacOffenceDetailsAddAnOffenceFormComponent as any).ɵcmp?.template?.toString() as string | undefined) ?? ''; const actionLinkConsts = templateConsts.filter( (entry) => entry.includes('govuk-link') && @@ -298,7 +297,10 @@ describe('FinesMacOffenceDetailsAddAnOffenceFormComponent', () => { const event = new Event('click'); const preventDefaultSpy = vi.spyOn(event, 'preventDefault'); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const setRemoveMinorCreditorSpy = vi.spyOn(component['finesMacOffenceDetailsStore'], 'setRemoveMinorCreditor'); + const setRemoveMinorCreditorSpy = vi.spyOn( + component['finesMacOffenceDetailsStore'], + 'setRemoveMinorCreditor', + ); // eslint-disable-next-line @typescript-eslint/no-explicit-any const setRowIndexSpy = vi.spyOn(component['finesMacOffenceDetailsStore'], 'setRowIndex'); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -312,7 +314,9 @@ describe('FinesMacOffenceDetailsAddAnOffenceFormComponent', () => { expect(setRemoveMinorCreditorSpy).toHaveBeenCalledWith(null); expect(setRowIndexSpy).toHaveBeenCalledWith(0); expect(updateDraftSpy).toHaveBeenCalledWith(component.form.value); - expect(handleRouteSpy).toHaveBeenCalledWith(component['fineMacOffenceDetailsRoutingPaths'].children.addMinorCreditor); + expect(handleRouteSpy).toHaveBeenCalledWith( + component['fineMacOffenceDetailsRoutingPaths'].children.addMinorCreditor, + ); }); it('should populate offence details draft when navigating to remove imposition when draft is empty', () => { @@ -336,7 +340,10 @@ describe('FinesMacOffenceDetailsAddAnOffenceFormComponent', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const setRowIndexSpy = vi.spyOn(component['finesMacOffenceDetailsStore'], 'setRowIndex'); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const setFormArrayControlsSpy = vi.spyOn(component['finesMacOffenceDetailsStore'], 'setFormArrayControls'); + const setFormArrayControlsSpy = vi.spyOn( + component['finesMacOffenceDetailsStore'], + 'setFormArrayControls', + ); // eslint-disable-next-line @typescript-eslint/no-explicit-any const updateDraftSpy = vi.spyOn(component, 'updateOffenceDetailsDraft'); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -348,7 +355,9 @@ describe('FinesMacOffenceDetailsAddAnOffenceFormComponent', () => { expect(setRowIndexSpy).toHaveBeenCalledWith(0); expect(setFormArrayControlsSpy).toHaveBeenCalledWith(component.formArrayControls); expect(updateDraftSpy).toHaveBeenCalledWith(component.form.value); - expect(handleRouteSpy).toHaveBeenCalledWith(component['fineMacOffenceDetailsRoutingPaths'].children.removeImposition); + expect(handleRouteSpy).toHaveBeenCalledWith( + component['fineMacOffenceDetailsRoutingPaths'].children.removeImposition, + ); }); it('should populate offence details draft when navigating to remove imposition when draft is populated', () => { diff --git a/src/app/flows/fines/fines-mac/fines-mac-parent-guardian-details/fines-mac-parent-guardian-details-form/fines-mac-parent-guardian-details-form.component.spec.ts b/src/app/flows/fines/fines-mac/fines-mac-parent-guardian-details/fines-mac-parent-guardian-details-form/fines-mac-parent-guardian-details-form.component.spec.ts index a5b115a206..207001a153 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-parent-guardian-details/fines-mac-parent-guardian-details-form/fines-mac-parent-guardian-details-form.component.spec.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-parent-guardian-details/fines-mac-parent-guardian-details-form/fines-mac-parent-guardian-details-form.component.spec.ts @@ -106,7 +106,9 @@ describe('FinesMacParentGuardianDetailsFormComponent', () => { const link = (Array.from( - fixture.nativeElement.querySelectorAll('a.govuk-link.govuk-link--no-visited-state') as NodeListOf, + fixture.nativeElement.querySelectorAll( + 'a.govuk-link.govuk-link--no-visited-state', + ) as NodeListOf, ).find((anchor) => anchor.textContent?.trim().startsWith('Remove')) as HTMLAnchorElement | undefined) ?? null; expect(link).toBeTruthy(); if (!link) throw new Error('Parent/guardian remove alias link not found'); diff --git a/src/app/flows/fines/fines-mac/fines-mac-personal-details/fines-mac-personal-details-form/fines-mac-personal-details-form.component.spec.ts b/src/app/flows/fines/fines-mac/fines-mac-personal-details/fines-mac-personal-details-form/fines-mac-personal-details-form.component.spec.ts index ec3b3c4a94..195a7bad6e 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-personal-details/fines-mac-personal-details-form/fines-mac-personal-details-form.component.spec.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-personal-details/fines-mac-personal-details-form/fines-mac-personal-details-form.component.spec.ts @@ -83,8 +83,8 @@ describe('FinesMacPersonalDetailsFormComponent', () => { it('should enforce remove alias link template semantics', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any - const templateConsts = ((FinesMacPersonalDetailsFormComponent as any).ɵcmp?.consts ?? []).filter( - (entry: unknown) => Array.isArray(entry), + const templateConsts = ((FinesMacPersonalDetailsFormComponent as any).ɵcmp?.consts ?? []).filter((entry: unknown) => + Array.isArray(entry), ) as unknown[][]; const templateFunction = // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -116,7 +116,9 @@ describe('FinesMacPersonalDetailsFormComponent', () => { const link = (Array.from( - fixture.nativeElement.querySelectorAll('a.govuk-link.govuk-link--no-visited-state') as NodeListOf, + fixture.nativeElement.querySelectorAll( + 'a.govuk-link.govuk-link--no-visited-state', + ) as NodeListOf, ).find((anchor) => anchor.textContent?.trim().startsWith('Remove')) as HTMLAnchorElement | undefined) ?? null; expect(link).toBeTruthy(); if (!link) throw new Error('Personal details remove alias link not found'); diff --git a/src/app/flows/fines/fines-mac/fines-mac-review-account/fines-mac-review-account.component.html b/src/app/flows/fines/fines-mac/fines-mac-review-account/fines-mac-review-account.component.html index 9232f8e338..1db5016f88 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-review-account/fines-mac-review-account.component.html +++ b/src/app/flows/fines/fines-mac/fines-mac-review-account/fines-mac-review-account.component.html @@ -125,7 +125,12 @@

    Check account details

    @if (!this.finesDraftStore.draft_account_id()) { } diff --git a/src/app/flows/fines/fines-mac/fines-mac-review-account/fines-mac-review-account.component.spec.ts b/src/app/flows/fines/fines-mac/fines-mac-review-account/fines-mac-review-account.component.spec.ts index 77d6bba4f4..e3cf7ab74f 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-review-account/fines-mac-review-account.component.spec.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-review-account/fines-mac-review-account.component.spec.ts @@ -160,8 +160,8 @@ describe('FinesMacReviewAccountComponent', () => { it('should enforce current template link semantics for delete account link', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any - const templateConsts = ((FinesMacReviewAccountComponent as any).ɵcmp?.consts ?? []).filter( - (entry: unknown) => Array.isArray(entry), + const templateConsts = ((FinesMacReviewAccountComponent as any).ɵcmp?.consts ?? []).filter((entry: unknown) => + Array.isArray(entry), ) as unknown[][]; const templateFunction = // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/app/flows/fines/fines-mac/fines-mac-submit-confirmation/fines-mac-submit-confirmation.component.spec.ts b/src/app/flows/fines/fines-mac/fines-mac-submit-confirmation/fines-mac-submit-confirmation.component.spec.ts index e2395f6e30..987e7114dd 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-submit-confirmation/fines-mac-submit-confirmation.component.spec.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-submit-confirmation/fines-mac-submit-confirmation.component.spec.ts @@ -67,8 +67,8 @@ describe('FinesMacSubmitConfirmationComponent', () => { it('should enforce current template link semantics', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any - const templateConsts = ((FinesMacSubmitConfirmationComponent as any).ɵcmp?.consts ?? []).filter( - (entry: unknown) => Array.isArray(entry), + const templateConsts = ((FinesMacSubmitConfirmationComponent as any).ɵcmp?.consts ?? []).filter((entry: unknown) => + Array.isArray(entry), ) as unknown[][]; const templateFunction = // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results-minor-creditor-table-wrapper/fines-sa-results-minor-creditor-table-wrapper.component.spec.ts b/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results-minor-creditor-table-wrapper/fines-sa-results-minor-creditor-table-wrapper.component.spec.ts index 7ebbca3745..4b3a8d039c 100644 --- a/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results-minor-creditor-table-wrapper/fines-sa-results-minor-creditor-table-wrapper.component.spec.ts +++ b/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results-minor-creditor-table-wrapper/fines-sa-results-minor-creditor-table-wrapper.component.spec.ts @@ -34,9 +34,8 @@ describe('FinesSaResultsMinorCreditorTableWrapperComponent', () => { ) as unknown[][]; const templateFunction = // eslint-disable-next-line @typescript-eslint/no-explicit-any - ((FinesSaResultsMinorCreditorTableWrapperComponent as any).ɵcmp?.template?.toString() as - | string - | undefined) ?? ''; + ((FinesSaResultsMinorCreditorTableWrapperComponent as any).ɵcmp?.template?.toString() as string | undefined) ?? + ''; const actionLinkConsts = templateConsts.filter( (entry) => entry.includes('govuk-link') && diff --git a/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results.component.html b/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results.component.html index a39c194d33..c11c2f6595 100644 --- a/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results.component.html +++ b/src/app/flows/fines/fines-sa/fines-sa-results/fines-sa-results.component.html @@ -136,14 +136,18 @@

    Search results

    There are no matching results

    - Check your search + Check your search and try again

    There are more than 100 results

    - Try adding more information + Try adding more information to your search

    diff --git a/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-account/fines-sa-search-account-form/fines-sa-search-account-form-major-creditors/fines-sa-search-account-form-major-creditors.component.html b/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-account/fines-sa-search-account-form/fines-sa-search-account-form-major-creditors/fines-sa-search-account-form-major-creditors.component.html index 8e85847f7f..3df9bd6950 100644 --- a/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-account/fines-sa-search-account-form/fines-sa-search-account-form-major-creditors/fines-sa-search-account-form-major-creditors.component.html +++ b/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-account/fines-sa-search-account-form/fines-sa-search-account-form-major-creditors/fines-sa-search-account-form-major-creditors.component.html @@ -13,6 +13,8 @@

    Major creditors

    } @else {

    To search major creditors, - filter by a single business unit + filter by a single business unit

    } diff --git a/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-account/fines-sa-search-account-form/fines-sa-search-account-form-major-creditors/fines-sa-search-account-form-major-creditors.component.spec.ts b/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-account/fines-sa-search-account-form/fines-sa-search-account-form-major-creditors/fines-sa-search-account-form-major-creditors.component.spec.ts index 4b192f19a1..81ff1ed01e 100644 --- a/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-account/fines-sa-search-account-form/fines-sa-search-account-form-major-creditors/fines-sa-search-account-form-major-creditors.component.spec.ts +++ b/src/app/flows/fines/fines-sa/fines-sa-search/fines-sa-search-account/fines-sa-search-account-form/fines-sa-search-account-form-major-creditors/fines-sa-search-account-form-major-creditors.component.spec.ts @@ -45,8 +45,7 @@ describe('FinesSaSearchAccountFormMajorCreditorsComponent', () => { ) as unknown[][]; const templateFunction = // eslint-disable-next-line @typescript-eslint/no-explicit-any - ((FinesSaSearchAccountFormMajorCreditorsComponent as any).ɵcmp?.template?.toString() as string | undefined) ?? - ''; + ((FinesSaSearchAccountFormMajorCreditorsComponent as any).ɵcmp?.template?.toString() as string | undefined) ?? ''; const filterLinkConsts = templateConsts.filter( (entry) => entry.includes('govuk-link') && entry.includes('href') && entry.includes('click'), ); From e709028b1ffcd6b4a53872ef75821f413e68b713 Mon Sep 17 00:00:00 2001 From: Christopher Garratt Date: Wed, 1 Apr 2026 15:55:08 +0100 Subject: [PATCH 08/11] Undoing offence changes. Incorrectly included on this branch. --- ...-details-offences-field-errors.constant.ts | 10 +- .../fines-mac-offence-details.service.spec.ts | 199 +----------------- .../fines-mac-offence-details.service.ts | 93 ++------ 3 files changed, 15 insertions(+), 287 deletions(-) diff --git a/src/app/flows/fines/fines-mac/fines-mac-offence-details/constants/fines-mac-offence-details-offences-field-errors.constant.ts b/src/app/flows/fines/fines-mac/fines-mac-offence-details/constants/fines-mac-offence-details-offences-field-errors.constant.ts index 9ab2423a9d..f6c607a3b7 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-offence-details/constants/fines-mac-offence-details-offences-field-errors.constant.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-offence-details/constants/fines-mac-offence-details-offences-field-errors.constant.ts @@ -36,17 +36,9 @@ export const FINES_MAC_OFFENCE_DETAILS_OFFENCES_FIELD_ERRORS: IAbstractFormBaseF message: 'Offence code must be 7 or 8 characters', priority: 4, }, - offenceCodeLookupFailed: { - message: 'We could not validate the offence code. Try again', - priority: 5, - }, - offenceCodeValidationPending: { - message: 'Wait for offence code validation to complete', - priority: 6, - }, invalidOffenceCode: { message: 'Offence not found', - priority: 7, + priority: 5, }, }, }; diff --git a/src/app/flows/fines/fines-mac/fines-mac-offence-details/services/fines-mac-offence-details.service.spec.ts b/src/app/flows/fines/fines-mac/fines-mac-offence-details/services/fines-mac-offence-details.service.spec.ts index 786c63ef3e..ce71eb0d95 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-offence-details/services/fines-mac-offence-details.service.spec.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-offence-details/services/fines-mac-offence-details.service.spec.ts @@ -3,7 +3,7 @@ import { TestBed } from '@angular/core/testing'; import { FinesMacOffenceDetailsService } from './fines-mac-offence-details.service'; import { FINES_MAC_OFFENCE_DETAILS_FORM_MOCK } from '../mocks/fines-mac-offence-details-form.mock'; import { FormControl, FormGroup } from '@angular/forms'; -import { Observable, of, Subject, throwError } from 'rxjs'; +import { Observable, of, Subject } from 'rxjs'; import { FINES_MAC_OFFENCE_DETAILS_DEFAULT_VALUES } from '../constants/fines-mac-offence-details-default-values.constant'; import { IOpalFinesOffencesRefData } from '@services/fines/opal-fines-service/interfaces/opal-fines-offences-ref-data.interface'; import { provideHttpClient } from '@angular/common/http'; @@ -100,31 +100,6 @@ describe('FinesMacOffenceDetailsService', () => { expect(result[0].formData.fm_offence_details_impositions[0]).toEqual(expected); }); - it('setControlError - should remove one error key and keep remaining errors', () => { - const control = new FormControl('code'); - control.setErrors({ - invalidOffenceCode: true, - customError: true, - }); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (service as any).setControlError(control, 'invalidOffenceCode', false); - - expect(control.errors).toEqual({ customError: true }); - }); - - it('setControlError - should clear all errors when removing the final error key', () => { - const control = new FormControl('code'); - control.setErrors({ - invalidOffenceCode: true, - }); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (service as any).setControlError(control, 'invalidOffenceCode', false); - - expect(control.errors).toBeNull(); - }); - describe('initOffenceListener', () => { let form: FormGroup; let destroy$: Subject; @@ -203,93 +178,6 @@ describe('FinesMacOffenceDetailsService', () => { expect(onConfirmChangeSpy).toHaveBeenCalledWith(true); }); - it('should clear offence id and set confirmation to false immediately when code changes', () => { - vi.useFakeTimers(); - form.get('id')?.setValue(314441); - - service.initOffenceCodeListener( - form, - 'code', - 'id', - destroy$, - getOffenceByCjsCode, - onResultSpy, - onConfirmChangeSpy, - ); - - form.get('code')?.setValue('xy98765'); - - expect(form.get('id')?.value).toBeNull(); - expect(form.get('code')?.errors).toEqual({ offenceCodeValidationPending: true }); - expect(onConfirmChangeSpy).toHaveBeenCalledWith(false); - }); - - it('should not set pending validation error immediately for short codes', () => { - vi.useFakeTimers(); - form.get('id')?.setValue(314441); - - service.initOffenceCodeListener( - form, - 'code', - 'id', - destroy$, - getOffenceByCjsCode, - onResultSpy, - onConfirmChangeSpy, - ); - - form.get('code')?.setValue('xy98'); - - expect(form.get('id')?.value).toBeNull(); - expect(form.get('code')?.errors).toBeNull(); - expect(onConfirmChangeSpy).toHaveBeenCalledWith(false); - }); - - it('should ignore stale offence lookup responses when code changes quickly', () => { - vi.useFakeTimers(); - - const firstLookup$ = new Subject(); - const secondLookup$ = new Subject(); - getOffenceByCjsCode = vi.fn((code: string) => { - return code === 'AB12345' ? firstLookup$.asObservable() : secondLookup$.asObservable(); - }); - - const staleResponse: IOpalFinesOffencesRefData = { - ...offenceMockResponse, - refData: [{ ...offenceMockResponse.refData[0], offence_id: 111111, get_cjs_code: 'AB12345' }], - }; - const latestResponse: IOpalFinesOffencesRefData = { - ...offenceMockResponse, - refData: [{ ...offenceMockResponse.refData[0], offence_id: 222222, get_cjs_code: 'CD12345' }], - }; - - service.initOffenceCodeListener( - form, - 'code', - 'id', - destroy$, - getOffenceByCjsCode, - onResultSpy, - onConfirmChangeSpy, - ); - - form.get('code')?.setValue('ab12345'); - vi.advanceTimersByTime(FINES_MAC_OFFENCE_DETAILS_DEFAULT_VALUES.defaultDebounceTime); - - form.get('code')?.setValue('cd12345'); - vi.advanceTimersByTime(FINES_MAC_OFFENCE_DETAILS_DEFAULT_VALUES.defaultDebounceTime); - - firstLookup$.next(staleResponse); - expect(form.get('id')?.value).toBeNull(); - expect(onResultSpy).not.toHaveBeenCalled(); - - secondLookup$.next(latestResponse); - expect(form.get('id')?.value).toBe(222222); - expect(onResultSpy).toHaveBeenCalledTimes(1); - expect(onResultSpy).toHaveBeenCalledWith(latestResponse); - expect(onConfirmChangeSpy).toHaveBeenCalledWith(true); - }); - it('should mark code as invalid when response count is 0', () => { vi.useFakeTimers(); const invalidResponse = { count: 0, refData: [] }; @@ -313,91 +201,6 @@ describe('FinesMacOffenceDetailsService', () => { expect(onConfirmChangeSpy).toHaveBeenCalledWith(true); }); - it('should mark code as invalid when response count is greater than 1', () => { - vi.useFakeTimers(); - const multipleResponse: IOpalFinesOffencesRefData = { - count: 2, - refData: [offenceMockResponse.refData[0], { ...offenceMockResponse.refData[0], offence_id: 123456 }], - }; - getOffenceByCjsCode = () => of(multipleResponse); - - service.initOffenceCodeListener( - form, - 'code', - 'id', - destroy$, - getOffenceByCjsCode, - onResultSpy, - onConfirmChangeSpy, - ); - - form.get('code')?.setValue('zz99999'); - vi.advanceTimersByTime(FINES_MAC_OFFENCE_DETAILS_DEFAULT_VALUES.defaultDebounceTime); - - expect(form.get('code')?.errors).toEqual({ invalidOffenceCode: true }); - expect(form.get('id')?.value).toBeNull(); - expect(onConfirmChangeSpy).toHaveBeenCalledWith(true); - }); - - it('should clear pending error and set lookup failed error when offence lookup request fails', () => { - vi.useFakeTimers(); - getOffenceByCjsCode = () => throwError(() => new Error('request failed')); - - service.initOffenceCodeListener( - form, - 'code', - 'id', - destroy$, - getOffenceByCjsCode, - onResultSpy, - onConfirmChangeSpy, - ); - - form.get('code')?.setValue('zz99999'); - vi.advanceTimersByTime(FINES_MAC_OFFENCE_DETAILS_DEFAULT_VALUES.defaultDebounceTime); - - expect(form.get('code')?.errors).toEqual({ offenceCodeLookupFailed: true }); - expect(form.get('id')?.value).toBeNull(); - expect(onResultSpy).not.toHaveBeenCalled(); - expect(onConfirmChangeSpy).toHaveBeenLastCalledWith(false); - }); - - it('should ignore stale lookup failures from previous offence code values', () => { - vi.useFakeTimers(); - - const firstLookup$ = new Subject(); - const secondLookup$ = new Subject(); - getOffenceByCjsCode = vi.fn((code: string) => { - return code === 'AB12345' ? firstLookup$.asObservable() : secondLookup$.asObservable(); - }); - - service.initOffenceCodeListener( - form, - 'code', - 'id', - destroy$, - getOffenceByCjsCode, - onResultSpy, - onConfirmChangeSpy, - ); - - form.get('code')?.setValue('ab12345'); - vi.advanceTimersByTime(FINES_MAC_OFFENCE_DETAILS_DEFAULT_VALUES.defaultDebounceTime); - - form.get('code')?.setValue('cd12345'); - vi.advanceTimersByTime(FINES_MAC_OFFENCE_DETAILS_DEFAULT_VALUES.defaultDebounceTime); - - firstLookup$.error(new Error('stale failure')); - - expect(form.get('code')?.errors).toEqual({ offenceCodeValidationPending: true }); - expect(onConfirmChangeSpy).toHaveBeenCalledTimes(4); - expect(onResultSpy).not.toHaveBeenCalled(); - - secondLookup$.next(offenceMockResponse); - expect(form.get('id')?.value).toBe(314441); - expect(form.get('code')?.errors).toBeNull(); - }); - it('should not call populateHint for short code', () => { vi.useFakeTimers(); service.initOffenceCodeListener( diff --git a/src/app/flows/fines/fines-mac/fines-mac-offence-details/services/fines-mac-offence-details.service.ts b/src/app/flows/fines/fines-mac/fines-mac-offence-details/services/fines-mac-offence-details.service.ts index 64ea38be01..3519764552 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-offence-details/services/fines-mac-offence-details.service.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-offence-details/services/fines-mac-offence-details.service.ts @@ -1,7 +1,7 @@ import { inject, Injectable } from '@angular/core'; import { IFinesMacOffenceDetailsForm } from '../interfaces/fines-mac-offence-details-form.interface'; -import { FormControl, FormGroup } from '@angular/forms'; -import { debounceTime, distinctUntilChanged, Observable, Subject, takeUntil, tap, catchError, EMPTY } from 'rxjs'; +import { FormGroup } from '@angular/forms'; +import { debounceTime, distinctUntilChanged, Observable, Subject, takeUntil, tap } from 'rxjs'; import { FINES_MAC_OFFENCE_DETAILS_DEFAULT_VALUES } from '../constants/fines-mac-offence-details-default-values.constant'; import { UtilsService } from '@hmcts/opal-frontend-common/services/utils-service'; import { IOpalFinesOffencesRefData } from '@services/fines/opal-fines-service/interfaces/opal-fines-offences-ref-data.interface'; @@ -11,33 +11,6 @@ import { IOpalFinesOffencesRefData } from '@services/fines/opal-fines-service/in }) export class FinesMacOffenceDetailsService { public utilsService = inject(UtilsService); - - /** - * Adds or removes a single custom error key while preserving any other control errors. - * - * @param control - The control to update. - * @param errorKey - The custom error key to add or remove. - * @param hasError - True to set the error key, false to clear it. - */ - private setControlError(control: FormControl, errorKey: string, hasError: boolean): void { - const currentErrors = control.errors ?? {}; - - if (hasError) { - if (!currentErrors[errorKey]) { - control.setErrors({ ...currentErrors, [errorKey]: true }, { emitEvent: false }); - } - return; - } - - if (!currentErrors[errorKey]) { - return; - } - - const remainingErrors = { ...currentErrors }; - delete remainingErrors[errorKey]; - control.setErrors(Object.keys(remainingErrors).length ? remainingErrors : null, { emitEvent: false }); - } - /** * Reorders the imposition keys to maintain correct numbering. * @@ -162,88 +135,48 @@ export class FinesMacOffenceDetailsService { onResult: (result: any) => void, onConfirmChange?: (confirmed: boolean) => void, ): void { - const codeControl = form.controls[codeControlName] as FormControl; - const idControl = form.controls[idControlName] as FormControl; - let latestLookupRequest = 0; + const codeControl = form.controls[codeControlName]; + const idControl = form.controls[idControlName]; const populateHint = (code: string) => { - const lookupRequest = ++latestLookupRequest; - idControl.setValue(null, { emitEvent: false }); - this.setControlError(codeControl, 'invalidOffenceCode', false); - this.setControlError(codeControl, 'offenceCodeLookupFailed', false); + idControl.setValue(null); if (code?.length >= 7 && code?.length <= 8) { - this.setControlError(codeControl, 'offenceCodeValidationPending', true); - if (onConfirmChange) onConfirmChange(false); - const result$ = getOffenceByCjsCode(code).pipe( tap((response) => { - // Ignore stale responses that return after the user has changed the code. - if (lookupRequest !== latestLookupRequest || codeControl.value !== code) { - return; - } - - this.setControlError(codeControl, 'offenceCodeValidationPending', false); - this.setControlError(codeControl, 'offenceCodeLookupFailed', false); - this.setControlError(codeControl, 'invalidOffenceCode', response.count !== 1); + codeControl.setErrors(response.count === 0 ? { invalidOffenceCode: true } : null, { emitEvent: false }); idControl.setValue(response.count === 1 ? response.refData[0].offence_id : null, { emitEvent: false }); if (typeof onResult === 'function') { onResult(response); } - - if (onConfirmChange) onConfirmChange(true); - }), - catchError(() => { - // Ignore stale failures for previous lookups. - if (lookupRequest !== latestLookupRequest || codeControl.value !== code) { - return EMPTY; - } - - this.setControlError(codeControl, 'offenceCodeValidationPending', false); - this.setControlError(codeControl, 'invalidOffenceCode', false); - this.setControlError(codeControl, 'offenceCodeLookupFailed', true); - idControl.setValue(null, { emitEvent: false }); - if (onConfirmChange) onConfirmChange(false); - return EMPTY; }), takeUntil(destroy$), ); result$.subscribe(); - } else { - this.setControlError(codeControl, 'offenceCodeValidationPending', false); - this.setControlError(codeControl, 'offenceCodeLookupFailed', false); - if (onConfirmChange) onConfirmChange(false); + if (onConfirmChange) onConfirmChange(true); + } else if (onConfirmChange) { + onConfirmChange(false); } }; if (codeControl.value) { - const upperCasedCode = this.utilsService.upperCaseAllLetters(codeControl.value); - codeControl.setValue(upperCasedCode, { emitEvent: false }); - populateHint(upperCasedCode); + populateHint(codeControl.value); } codeControl.valueChanges .pipe( distinctUntilChanged(), tap((code: string) => { - const upperCasedCode = this.utilsService.upperCaseAllLetters(code); - const isLookupLength = upperCasedCode?.length >= 7 && upperCasedCode?.length <= 8; - codeControl.setValue(upperCasedCode, { emitEvent: false }); - // Invalidate any in-flight lookup as soon as the input changes. - latestLookupRequest++; - idControl.setValue(null, { emitEvent: false }); - this.setControlError(codeControl, 'offenceCodeValidationPending', isLookupLength); - this.setControlError(codeControl, 'invalidOffenceCode', false); - this.setControlError(codeControl, 'offenceCodeLookupFailed', false); - if (onConfirmChange) onConfirmChange(false); + code = this.utilsService.upperCaseAllLetters(code); + codeControl.setValue(code, { emitEvent: false }); }), debounceTime(FINES_MAC_OFFENCE_DETAILS_DEFAULT_VALUES.defaultDebounceTime), takeUntil(destroy$), ) .subscribe((code: string) => { - populateHint(this.utilsService.upperCaseAllLetters(code)); + populateHint(code); }); } } From cbd6ed1147ee6e8fb47adbf0bb52d27f78dcdba6 Mon Sep 17 00:00:00 2001 From: Christopher Garratt Date: Wed, 1 Apr 2026 16:12:13 +0100 Subject: [PATCH 09/11] Undoing other offence code changes. --- ...xed-penalty-details-form.component.spec.ts | 15 --- ...ails-add-an-offence-form.component.spec.ts | 110 ------------------ ...e-details-add-an-offence-form.component.ts | 37 ------ 3 files changed, 162 deletions(-) diff --git a/src/app/flows/fines/fines-mac/fines-mac-fixed-penalty-details/fines-mac-fixed-penalty-details-form/fines-mac-fixed-penalty-details-form.component.spec.ts b/src/app/flows/fines/fines-mac/fines-mac-fixed-penalty-details/fines-mac-fixed-penalty-details-form/fines-mac-fixed-penalty-details-form.component.spec.ts index 3f113c551d..35ead5d558 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-fixed-penalty-details/fines-mac-fixed-penalty-details-form/fines-mac-fixed-penalty-details-form.component.spec.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-fixed-penalty-details/fines-mac-fixed-penalty-details-form/fines-mac-fixed-penalty-details-form.component.spec.ts @@ -245,21 +245,6 @@ describe('FinesMacFixedPenaltyFormComponent', () => { vi.useRealTimers(); }); - it('should set offenceCodeValidationPending immediately when lookup-length offence code changes', () => { - vi.useFakeTimers(); - component['setupFixedPenaltyDetailsForm'](); - component['setupOffenceCodeListener'](); - component.form.get('fm_fp_offence_details_offence_id')?.setValue(314441); - - component.form.get('fm_fp_offence_details_offence_cjs_code')?.setValue('AK12345'); - - expect(component.form.get('fm_fp_offence_details_offence_id')?.value).toBeNull(); - expect(component.form.get('fm_fp_offence_details_offence_cjs_code')?.errors).toEqual({ - offenceCodeValidationPending: true, - }); - vi.useRealTimers(); - }); - it('should set initial value if dob value already exists', () => { component['setupFixedPenaltyDetailsForm'](); component.form.get('fm_fp_personal_details_dob')?.setValue('01-01-1979'); diff --git a/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.spec.ts b/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.spec.ts index 561cd5ba5c..c6aa2d65b5 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.spec.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.spec.ts @@ -73,7 +73,6 @@ describe('FinesMacOffenceDetailsAddAnOffenceFormComponent', () => { 'upperCaseFirstLetter', 'scrollToTop', ]); - mockUtilsService.upperCaseAllLetters.mockImplementation((value: string) => value?.toUpperCase?.() ?? value); await TestBed.configureTestingModule({ imports: [FinesMacOffenceDetailsAddAnOffenceFormComponent], @@ -816,115 +815,6 @@ describe('FinesMacOffenceDetailsAddAnOffenceFormComponent', () => { expect(superHandleFormSubmitSpy).toHaveBeenCalledWith(event); }); - it('should set offenceCodeValidationPending on submit when offence code length is valid and offence id is unresolved', () => { - const offenceCodeControl = component.form.controls['fm_offence_details_offence_cjs_code']; - const offenceIdControl = component.form.controls['fm_offence_details_offence_id']; - - offenceCodeControl.setValue('AK12345'); - offenceCodeControl.setErrors(null); - offenceIdControl.setValue(null); - - component.handleFormSubmit(new SubmitEvent('submit')); - - expect(offenceCodeControl.errors).toEqual(expect.objectContaining({ offenceCodeValidationPending: true })); - }); - - it('should preserve existing offence code errors when setting offenceCodeValidationPending', () => { - const offenceCodeControl = component.form.controls['fm_offence_details_offence_cjs_code']; - const offenceIdControl = component.form.controls['fm_offence_details_offence_id']; - - offenceCodeControl.setValue('AK12345'); - offenceCodeControl.setErrors({ customError: true }); - offenceIdControl.setValue(null); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (component as any).enforceOffenceCodeValidationBeforeSubmit(); - - expect(offenceCodeControl.errors).toEqual({ - customError: true, - offenceCodeValidationPending: true, - }); - }); - - it('should handle null offence code values without setting pending validation', () => { - const offenceCodeControl = component.form.controls['fm_offence_details_offence_cjs_code']; - const offenceIdControl = component.form.controls['fm_offence_details_offence_id']; - - offenceCodeControl.setValue(null); - offenceCodeControl.setErrors({ offenceCodeValidationPending: true }); - offenceIdControl.setValue(null); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (component as any).enforceOffenceCodeValidationBeforeSubmit(); - - expect(offenceCodeControl.errors).toBeNull(); - }); - - it('should not set offenceCodeValidationPending on submit when offence code is already invalid', () => { - const offenceCodeControl = component.form.controls['fm_offence_details_offence_cjs_code']; - const offenceIdControl = component.form.controls['fm_offence_details_offence_id']; - - offenceCodeControl.setValue('AK12345'); - offenceCodeControl.setErrors({ invalidOffenceCode: true }); - offenceIdControl.setValue(null); - - component.handleFormSubmit(new SubmitEvent('submit')); - - expect(offenceCodeControl.errors).toEqual({ invalidOffenceCode: true }); - expect(offenceCodeControl.errors?.['offenceCodeValidationPending']).toBeUndefined(); - }); - - it('should not set offenceCodeValidationPending on submit when offence lookup failed', () => { - const offenceCodeControl = component.form.controls['fm_offence_details_offence_cjs_code']; - const offenceIdControl = component.form.controls['fm_offence_details_offence_id']; - - offenceCodeControl.setValue('AK12345'); - offenceCodeControl.setErrors({ offenceCodeLookupFailed: true }); - offenceIdControl.setValue(null); - - component.handleFormSubmit(new SubmitEvent('submit')); - - expect(offenceCodeControl.errors).toEqual({ offenceCodeLookupFailed: true }); - expect(offenceCodeControl.errors?.['offenceCodeValidationPending']).toBeUndefined(); - }); - - it('should remove offenceCodeValidationPending and keep other existing errors', () => { - const offenceCodeControl = component.form.controls['fm_offence_details_offence_cjs_code']; - const offenceIdControl = component.form.controls['fm_offence_details_offence_id']; - - offenceCodeControl.setValue('AK12345'); - offenceCodeControl.setErrors({ - offenceCodeValidationPending: true, - invalidOffenceCode: true, - }); - offenceIdControl.setValue(null); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (component as any).enforceOffenceCodeValidationBeforeSubmit(); - - expect(offenceCodeControl.errors).toEqual({ invalidOffenceCode: true }); - }); - - it('should clear offenceCodeValidationPending on submit when offence id is set', () => { - const offenceCodeControl = component.form.controls['fm_offence_details_offence_cjs_code']; - const offenceIdControl = component.form.controls['fm_offence_details_offence_id']; - - offenceCodeControl.setValue('AK12345'); - offenceCodeControl.setErrors({ offenceCodeValidationPending: true }); - offenceIdControl.setValue(314441); - - component.handleFormSubmit(new SubmitEvent('submit')); - - expect(offenceCodeControl.errors?.['offenceCodeValidationPending']).toBeUndefined(); - }); - - it('should return early when offence code or offence id controls are missing', () => { - component.form.removeControl('fm_offence_details_offence_id'); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect(() => (component as any).enforceOffenceCodeValidationBeforeSubmit()).not.toThrow(); - }); - it('should add a new draft offence when index is -1', () => { component.form.controls['fm_offence_details_id'].setValue('test-id'); // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.ts b/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.ts index b9622ca0cc..252f4df7bf 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.ts +++ b/src/app/flows/fines/fines-mac/fines-mac-offence-details/fines-mac-offence-details-add-an-offence/fines-mac-offence-details-add-an-offence-form/fines-mac-offence-details-add-an-offence-form.component.ts @@ -480,42 +480,6 @@ export class FinesMacOffenceDetailsAddAnOffenceFormComponent } } - /** - * Ensures the offence code lookup has completed before allowing submission. - * If a 7 or 8 character code has no resolved offence id yet, a pending-validation - * error is applied so the form remains invalid and the user sees a clear message. - */ - private enforceOffenceCodeValidationBeforeSubmit(): void { - const offenceCodeControl = this.form.get('fm_offence_details_offence_cjs_code') as FormControl | null; - const offenceIdControl = this.form.get('fm_offence_details_offence_id') as FormControl | null; - - if (!offenceCodeControl || !offenceIdControl) { - return; - } - - const offenceCode = offenceCodeControl.value ?? ''; - const isLookupLength = offenceCode.length >= 7 && offenceCode.length <= 8; - const hasOffenceId = offenceIdControl.value !== null && offenceIdControl.value !== undefined; - const hasInvalidOffenceCodeError = Boolean(offenceCodeControl.errors?.['invalidOffenceCode']); - const hasOffenceCodeLookupFailedError = Boolean(offenceCodeControl.errors?.['offenceCodeLookupFailed']); - - if (isLookupLength && !hasOffenceId && !hasInvalidOffenceCodeError && !hasOffenceCodeLookupFailedError) { - const currentErrors = offenceCodeControl.errors; - const updatedErrors = currentErrors - ? { ...currentErrors, offenceCodeValidationPending: true } - : { offenceCodeValidationPending: true }; - offenceCodeControl.setErrors(updatedErrors, { emitEvent: false }); - return; - } - - const currentErrors = offenceCodeControl.errors; - if (currentErrors?.['offenceCodeValidationPending']) { - const remainingErrors = { ...currentErrors }; - delete remainingErrors['offenceCodeValidationPending']; - offenceCodeControl.setErrors(Object.keys(remainingErrors).length ? remainingErrors : null, { emitEvent: false }); - } - } - /** * Navigates to the minor creditor page for the specified row index. * @@ -650,7 +614,6 @@ export class FinesMacOffenceDetailsAddAnOffenceFormComponent */ public override handleFormSubmit(event: SubmitEvent): void { this.checkImpositionMinorCreditors(); - this.enforceOffenceCodeValidationBeforeSubmit(); super.handleFormSubmit(event); } From 74a252275946ebdcff083b787f9882e4716511ca Mon Sep 17 00:00:00 2001 From: Christopher Garratt Date: Thu, 2 Apr 2026 17:24:37 +0100 Subject: [PATCH 10/11] Updating enforcement links to resolve comments. --- ...ant-details-enforcement-tab.component.html | 6 +-- ...-details-enforcement-tab.component.spec.ts | 50 +++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-enforcement-tab/fines-acc-defendant-details-enforcement-tab.component.html b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-enforcement-tab/fines-acc-defendant-details-enforcement-tab.component.html index ce29601ee9..6256c644d4 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-enforcement-tab/fines-acc-defendant-details-enforcement-tab.component.html +++ b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-enforcement-tab/fines-acc-defendant-details-enforcement-tab.component.html @@ -337,7 +337,7 @@

    Enforcement status

    Actions

    @if (hasEnterEnforcementPermission) {

    - Add enforcement action + Add enforcement action

    } @if ( @@ -345,13 +345,13 @@

    Actions

    !tabData.enforcement_override?.enforcement_override_result?.enforcement_override_result_id ) {

    - Add enforcement override

    }

    - Request an HMRC check + Request an HMRC check

  • } diff --git a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-enforcement-tab/fines-acc-defendant-details-enforcement-tab.component.spec.ts b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-enforcement-tab/fines-acc-defendant-details-enforcement-tab.component.spec.ts index bcace27490..a31790ff9f 100644 --- a/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-enforcement-tab/fines-acc-defendant-details-enforcement-tab.component.spec.ts +++ b/src/app/flows/fines/fines-acc/fines-acc-defendant-details/fines-acc-defendant-details-enforcement-tab/fines-acc-defendant-details-enforcement-tab.component.spec.ts @@ -22,6 +22,56 @@ describe('FinesAccDefendantDetailsEnforcementTab', () => { expect(component).toBeTruthy(); }); + it('should enforce action link template semantics', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const templateConsts = ((FinesAccDefendantDetailsEnforcementTab as any).ɵcmp?.consts ?? []).filter( + (entry: unknown) => Array.isArray(entry), + ) as unknown[][]; + const templateFunction = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ((FinesAccDefendantDetailsEnforcementTab as any).ɵcmp?.template?.toString() as string | undefined) ?? ''; + const actionLinkConsts = templateConsts.filter( + (entry) => + entry.includes('govuk-link') && entry.includes('govuk-link--no-visited-state') && entry.includes('href'), + ); + + expect(actionLinkConsts.length).toBeGreaterThan(0); + actionLinkConsts.forEach((entry) => { + expect(entry).toContain('href'); + expect(entry).toContain(''); + expect(entry).not.toContain('tabindex'); + }); + expect(templateFunction).not.toContain('keydown.enter'); + expect(templateFunction).not.toContain('keyup.enter'); + }); + + it('should render action links with empty href, no visited state, and no tabindex', () => { + const tabData = structuredClone(OPAL_FINES_ACCOUNT_DEFENDANT_DETAILS_ENFORCEMENT_TAB_REF_DATA_MOCK); + tabData.enforcement_override = null; + + fixture.componentRef.setInput('tabData', tabData); + fixture.componentRef.setInput('hasAccountMaintenancePermission', true); + fixture.componentRef.setInput('hasEnterEnforcementPermission', true); + fixture.detectChanges(); + + const actionLinks = Array.from( + fixture.nativeElement.querySelectorAll('div.govuk-grid-column-one-third p > a.govuk-link'), + ) as HTMLAnchorElement[]; + + expect(actionLinks).toHaveLength(3); + expect(actionLinks.map((link) => link.textContent?.trim())).toEqual([ + 'Add enforcement action', + 'Add enforcement override', + 'Request an HMRC check', + ]); + + actionLinks.forEach((link) => { + expect(link.classList.contains('govuk-link--no-visited-state')).toBe(true); + expect(link.getAttribute('href')).toBe(''); + expect(link.getAttribute('tabindex')).toBeNull(); + }); + }); + it('should handleAddEnforcementOverride when add enforcement override button is clicked', () => { const eventEmitterSpy = vi.spyOn(component.addEnforcementOverride, 'emit'); const event = { preventDefault: vi.fn() } as unknown as Event; From 6dd90330c66232f6e7b30e3a610e7f620f4aa003 Mon Sep 17 00:00:00 2001 From: Christopher Garratt Date: Thu, 2 Apr 2026 17:25:27 +0100 Subject: [PATCH 11/11] (update): known issues --- yarn-audit-known-issues | 2 ++ 1 file changed, 2 insertions(+) diff --git a/yarn-audit-known-issues b/yarn-audit-known-issues index 485768801c..95d7e23b3b 100644 --- a/yarn-audit-known-issues +++ b/yarn-audit-known-issues @@ -2,6 +2,8 @@ {"value":"@angular/ssr","children":{"ID":1113513,"Issue":"Angular SSR has an Open Redirect via X-Forwarded-Prefix","URL":"https://github.com/advisories/GHSA-xh43-g2fq-wjrj","Severity":"moderate","Vulnerable Versions":">=21.0.0-next.0 <21.1.5","Tree Versions":["21.1.4"],"Dependents":["opal-frontend@workspace:."]}} {"value":"@angular/ssr","children":{"ID":1115534,"Issue":"Protocol-Relative URL Injection via Single Backslash Bypass in Angular SSR","URL":"https://github.com/advisories/GHSA-vfx2-hv2g-xj5f","Severity":"moderate","Vulnerable Versions":">=21.0.0-next.0 <21.2.3","Tree Versions":["21.1.4"],"Dependents":["opal-frontend@workspace:."]}} {"value":"ajv","children":{"ID":1113715,"Issue":"ajv has ReDoS when using `$data` option","URL":"https://github.com/advisories/GHSA-2g4f-4pwh-qvx6","Severity":"moderate","Vulnerable Versions":">=7.0.0-alpha.0 <8.18.0","Tree Versions":["8.17.1"],"Dependents":["schema-utils@npm:4.3.3"]}} +{"value":"lodash","children":{"ID":1115806,"Issue":"lodash vulnerable to Code Injection via `_.template` imports key names","URL":"https://github.com/advisories/GHSA-r5fr-rjxr-66jc","Severity":"high","Vulnerable Versions":">=4.0.0 <=4.17.23","Tree Versions":["4.17.23"],"Dependents":["@cypress/webpack-preprocessor@virtual:a675e69e845cebadc36cee9a38065a1960f4aab74a9924442784ad5431c94d53a7f1d4962754c45f81da4004ef26e80c7c562b8c146cee281975b943df474817#npm:7.0.2"]}} +{"value":"lodash","children":{"ID":1115810,"Issue":"lodash vulnerable to Prototype Pollution via array path bypass in `_.unset` and `_.omit`","URL":"https://github.com/advisories/GHSA-f23m-r3pf-42rh","Severity":"moderate","Vulnerable Versions":"<=4.17.23","Tree Versions":["4.17.23"],"Dependents":["@cypress/webpack-preprocessor@virtual:a675e69e845cebadc36cee9a38065a1960f4aab74a9924442784ad5431c94d53a7f1d4962754c45f81da4004ef26e80c7c562b8c146cee281975b943df474817#npm:7.0.2"]}} {"value":"minimatch","children":{"ID":1113459,"Issue":"minimatch has a ReDoS via repeated wildcards with non-matching literal in pattern","URL":"https://github.com/advisories/GHSA-3ppc-4f35-3m26","Severity":"high","Vulnerable Versions":"<3.1.3","Tree Versions":["3.1.2"],"Dependents":["find-cypress-specs@npm:1.47.2"]}} {"value":"minimatch","children":{"ID":1113465,"Issue":"minimatch has a ReDoS via repeated wildcards with non-matching literal in pattern","URL":"https://github.com/advisories/GHSA-3ppc-4f35-3m26","Severity":"high","Vulnerable Versions":">=9.0.0 <9.0.6","Tree Versions":["9.0.5"],"Dependents":["mocha@npm:11.7.5"]}} {"value":"minimatch","children":{"ID":1113538,"Issue":"minimatch has ReDoS: matchOne() combinatorial backtracking via multiple non-adjacent GLOBSTAR segments","URL":"https://github.com/advisories/GHSA-7r86-cg39-jmmj","Severity":"high","Vulnerable Versions":"<3.1.3","Tree Versions":["3.1.2"],"Dependents":["find-cypress-specs@npm:1.47.2"]}}