Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
569bf58
feat: enhance offence code validation and error handling in forms
iamfrankiemoran Feb 20, 2026
15b7764
Merge branch 'master' into PO-2948
iamfrankiemoran Feb 27, 2026
6f88df8
Updating link class style and adding href to mac account details and …
ChrisGarratt122 Feb 27, 2026
b67a8df
Amending styling on mac account details links to fit design. Adding r…
ChrisGarratt122 Mar 5, 2026
01b62b3
Merge branch 'master' into PO-2948
iamfrankiemoran Mar 10, 2026
47ea9b3
Applying govuk-link changes to the rest of the application
ChrisGarratt122 Mar 11, 2026
9ff2844
Merge branch 'master' into PO-2984
ChrisGarratt122 Mar 12, 2026
e9eb086
Merge branch 'master' into PO-2984
ChrisGarratt122 Mar 18, 2026
364d85b
Merge branch 'master' into PO-2984
ChrisGarratt122 Mar 19, 2026
cc77fcb
Merge branch 'master' into PO-2984
ChrisGarratt122 Mar 30, 2026
91f43b5
Merge branch 'master' into PO-2948
iamfrankiemoran Mar 30, 2026
179994b
Updating to new ui library version. Adding `no-link-visited` where mi…
ChrisGarratt122 Mar 31, 2026
223b521
Merge branch 'PO-2984' of https://github.com/hmcts/opal-frontend into…
ChrisGarratt122 Apr 1, 2026
bc70749
Merge branch 'master' into PO-2984
ChrisGarratt122 Apr 1, 2026
aa62dab
Resolving codex review, removing redundant `govuk-task-list__link` cl…
ChrisGarratt122 Apr 1, 2026
03d00dc
Merge branch 'master' into PO-2984
ChrisGarratt122 Apr 1, 2026
9d4435e
yarn prettier:fix
ChrisGarratt122 Apr 1, 2026
e709028
Undoing offence changes. Incorrectly included on this branch.
ChrisGarratt122 Apr 1, 2026
cbd6ed1
Undoing other offence code changes.
ChrisGarratt122 Apr 1, 2026
be127f5
Merge branch 'master' into PO-2984
ChrisGarratt122 Apr 2, 2026
74a2522
Updating enforcement links to resolve comments.
ChrisGarratt122 Apr 2, 2026
6dd9033
(update): known issues
ChrisGarratt122 Apr 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,22 +244,12 @@ <h3 [class]="style.key">Free text notes</h3>
}
@if (hasAccountMaintenencePermission) {
<p [class]="style.value">
<a
tabindex="0"
class="govuk-link govuk-link--no-visited-state"
(click)="handleAddComments()"
(keyup.enter)="handleAddComments()"
>Change</a
>
<a class="govuk-link govuk-link--no-visited-state" href="" (click)="handleAddComments($event)">Change</a>
</p>
}
} @else if (hasAccountMaintenencePermission) {
<p [class]="style.value">
<a
tabindex="0"
class="govuk-link govuk-link--no-visited-state"
(click)="handleAddComments()"
(keyup.enter)="handleAddComments()"
<a class="govuk-link govuk-link--no-visited-state" href="" (click)="handleAddComments($event)"
>Add comments</a
>
</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<any, any>(component.addComments, 'emit');
component.handleAddComments();
expect(component.addComments.emit).toHaveBeenCalled();
const emitSpy = vi.spyOn<any, any>(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<any, any>(component, 'handleAddComments');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const emitSpy = vi.spyOn<any, any>(component.addComments, 'emit');

link.dispatchEvent(clickEvent);

expect(handleAddCommentsSpy).toHaveBeenCalledWith(clickEvent);
expect(clickEvent.defaultPrevented).toBe(true);
expect(emitSpy).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -337,21 +337,21 @@ <h2 [class]="style.heading">Enforcement status</h2>
<h2 [class]="style.heading">Actions</h2>
@if (hasEnterEnforcementPermission) {
<p>
<a class="govuk-link" tabindex="0">Add enforcement action</a>
<a class="govuk-link govuk-link--no-visited-state" href="">Add enforcement action</a>
</p>
}
@if (
hasAccountMaintenancePermission &&
!tabData.enforcement_override?.enforcement_override_result?.enforcement_override_result_id
) {
<p>
<a href="" class="govuk-link govuk-link--no-visited-state" (click)="handleAddEnforcementOverride($event)"
<a class="govuk-link govuk-link--no-visited-state" href="" (click)="handleAddEnforcementOverride($event)"
>Add enforcement override</a
>
</p>
}
<p>
<a class="govuk-link" tabindex="0">Request an HMRC check</a>
<a class="govuk-link govuk-link--no-visited-state" href="">Request an HMRC check</a>
</p>
</div>
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,10 @@
<div class="govuk-!-margin-bottom-3">
<a
class="govuk-link govuk-link--no-visited-state"
(click)="removeAlias(aliasControls.length - 1, 'facc_party_add_amend_convert_individual_aliases')"
(keyup.enter)="removeAlias(aliasControls.length - 1, 'facc_party_add_amend_convert_individual_aliases')"
tabindex="0"
href=""
(click)="
removeAlias(aliasControls.length - 1, 'facc_party_add_amend_convert_individual_aliases', $event)
"
>
Remove
</a>
Expand Down Expand Up @@ -144,11 +145,10 @@
<div class="govuk-!-margin-bottom-3">
<a
class="govuk-link govuk-link--no-visited-state"
(click)="removeAlias(aliasControls.length - 1, 'facc_party_add_amend_convert_organisation_aliases')"
(keyup.enter)="
removeAlias(aliasControls.length - 1, 'facc_party_add_amend_convert_organisation_aliases')
href=""
(click)="
removeAlias(aliasControls.length - 1, 'facc_party_add_amend_convert_organisation_aliases', $event)
"
tabindex="0"
>
Remove
</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLAnchorElement>,
).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<any, any>(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<HTMLAnchorElement>,
).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<any, any>(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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,9 @@
}

<a
href="#"
href=""
(click)="navigateBackToAccountSummary($event)"
(keydown.enter)="navigateBackToAccountSummary($event)"
class="govuk-link govuk-!-margin-top-4"
class="govuk-link govuk-link--no-visited-state govuk-!-margin-top-4"
>
Go back</a
>
Loading
Loading