diff --git a/package.json b/package.json index aaa0ab8212..a8165e5c21 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-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 79213afe10..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; 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..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 @@ -72,6 +72,106 @@ 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..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 @@ -24,10 +24,9 @@ } 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..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 @@ -15,10 +15,9 @@ } 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..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 @@ -53,10 +53,53 @@ 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-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 e258543580..1409d3b3e9 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 @@ -61,7 +61,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 b912dc2bf2..f71cca2798 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 @@ -98,6 +98,75 @@ 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(); + }); + it('should persist form and navigate to search error page when conflicting criteria are submitted', () => { const router = TestBed.inject(Router); const updateTemporarySpy = vi.spyOn(finesConStore, 'updateSearchAccountFormTemporary'); 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 725fc1614c..bd7c1e57f1 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 @@ -151,10 +151,9 @@

Check and submit

@if (!finesDraftStore.amend()) {
Delete account } @@ -183,10 +182,9 @@

Check and submit

> Contact details @@ -211,10 +209,9 @@

Check and submit

>
Employer details @@ -239,10 +236,9 @@

Check and submit

>
Company details @@ -267,10 +263,9 @@

Check and submit

>
Personal details @@ -296,10 +291,9 @@

Check and submit

>
{{ courtDetailsCopy.taskListLabel }} @@ -325,10 +319,9 @@

Check and submit

>
Parent or guardian details @@ -353,10 +346,9 @@

Check and submit

>
Offence details @@ -395,10 +387,9 @@

Check and submit

@if (canAccessPaymentTerms()) {
Payment terms @@ -429,10 +420,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 2be7c74306..1a2fecbfd7 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 @@ -95,6 +95,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'); diff --git a/src/app/flows/fines/fines-mac/fines-mac-company-details/fines-mac-company-details-form/fines-mac-company-details-form.component.html b/src/app/flows/fines/fines-mac/fines-mac-company-details/fines-mac-company-details-form/fines-mac-company-details-form.component.html index 5b18a15605..bf299517ac 100644 --- a/src/app/flows/fines/fines-mac/fines-mac-company-details/fines-mac-company-details-form/fines-mac-company-details-form.component.html +++ b/src/app/flows/fines/fines-mac/fines-mac-company-details/fines-mac-company-details-form/fines-mac-company-details-form.component.html @@ -64,9 +64,8 @@

Alias {{ 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..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 @@ -55,6 +55,62 @@ 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-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 575b3f9cf9..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 { 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-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-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-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 10a03ccf0b..dddfe417ba 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 @@ -222,9 +222,8 @@

> Add minor creditor details @@ -240,9 +239,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..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 @@ -117,6 +117,40 @@ 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 +292,32 @@ 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 +333,32 @@ 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 34273998db..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 @@ -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..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 @@ -71,6 +71,62 @@ 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..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 @@ -81,6 +81,62 @@ 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 f422a33c50..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 @@ -126,10 +126,9 @@

Check account details

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..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 @@ -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 16df8f7685..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 @@ -65,7 +65,30 @@ describe('FinesMacSubmitConfirmationComponent', () => { expect(component).toBeTruthy(); }); - it('should navigate to create account on createNewAccount', () => { + 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 originator type 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 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 + const routerSpy = vi.spyOn(component['router'], 'navigate'); + + component.createNewAccount(event); + + expect(preventDefaultSpy).toHaveBeenCalled(); + expect(routerSpy).toHaveBeenCalledWith([FINES_MAC_ROUTING_PATHS.children.originatorType], { + 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 56e1130f3a..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,15 +19,17 @@ 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. * @returns {void} */ - public createNewAccount(): void { + public createNewAccount(event?: Event): void { + event?.preventDefault(); this.router.navigate([FINES_MAC_ROUTING_PATHS.children.originatorType], { 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..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 @@ -27,6 +27,33 @@ 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 +77,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..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,7 +136,7 @@

    Search results

    There are no matching results

    - Check your search and try again @@ -145,7 +145,7 @@

    There are no matching results

    There are more than 100 results

    - 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 076c510c5f..16ca62f190 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 @@ -56,6 +56,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 = {}; @@ -283,6 +306,52 @@ 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( + [ + '/', + FINES_ROUTING_PATHS.root, + FINES_DASHBOARD_ROUTING_PATHS.root, + FINES_DASHBOARD_ROUTING_PATHS.children.search, + ], + { + 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 d45864820b..c78bc0cec5 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 @@ -367,8 +367,11 @@ export class FinesSaResultsComponent implements OnInit, OnDestroy { * * This method uses the Angular Router to navigate to the dashboard search route. * It also 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( [ '/', 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 29bb54bc46..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,11 +13,7 @@

    Major creditors

    } @else {

    To search major creditors, - 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..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 @@ -38,6 +38,29 @@ 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 +88,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(), 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"]}} diff --git a/yarn.lock b/yarn.lock index 7a402c6b05..889d5fe22d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3712,15 +3712,15 @@ __metadata: languageName: node linkType: hard -"@hmcts/opal-frontend-common@npm:^0.0.66": - version: 0.0.66 - resolution: "@hmcts/opal-frontend-common@npm:0.0.66" +"@hmcts/opal-frontend-common@npm:^0.0.67": + version: 0.0.67 + resolution: "@hmcts/opal-frontend-common@npm:0.0.67" dependencies: tslib: "npm:^2.3.0" peerDependencies: "@angular/common": ^18.2.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 "@angular/core": ^18.2.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 - checksum: 10/67d2973154c672470ea7a59a3bda2980dd17ff03f2afa0cdd0d19ba2533657b1b1109531147a6f6171ae4b806ffa7528e8729f80266e1152b641478d4f6fd58b + checksum: 10/3efaeb283e0244e9ec148fc6b40325999b517b3e75bb42a4891ea48f158948385e6014b410aa6551cb6eaf658b126424b52fa5d4b5b73dc32462b53b11fcee5e languageName: node linkType: hard @@ -14976,7 +14976,7 @@ __metadata: "@hmcts/info-provider": "npm:^1.1.0" "@hmcts/nodejs-healthcheck": "npm:^1.8.5" "@hmcts/nodejs-logging": "npm:^4.0.4" - "@hmcts/opal-frontend-common": "npm:^0.0.66" + "@hmcts/opal-frontend-common": "npm:^0.0.67" "@hmcts/opal-frontend-common-node": "npm:^0.0.27" "@hmcts/properties-volume": "npm:^1.1.0" "@hmcts/zephyr-automation-nodejs": "npm:0.0.6"