From 6732c9caf9ab0125f2f9f9dca41945f778c9f32f Mon Sep 17 00:00:00 2001 From: Arnab subedi <147511052+Arnabsubedi233@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:25:56 +0000 Subject: [PATCH 1/9] PO-2420 --- .../fines-con-consolidate-acc.component.html | 1 + .../fines-con-search-result.component.html | 18 ++++++++-- .../fines-con-search-result.component.spec.ts | 29 ++++++++++++++-- .../fines-con-search-result.component.ts | 34 ++++++++++++++++--- 4 files changed, 73 insertions(+), 9 deletions(-) diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-consolidate-acc/fines-con-consolidate-acc.component.html b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-consolidate-acc/fines-con-consolidate-acc.component.html index 7cf9cf06fc..27db0ef949 100644 --- a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-consolidate-acc/fines-con-consolidate-acc.component.html +++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-consolidate-acc/fines-con-consolidate-acc.component.html @@ -67,6 +67,7 @@

Consolidate accounts

[defendantType]="getDefendantType()" [searchPayload]="defendantAccountsSearchPayload" [alreadyAddedAccountIds]="finesConStore.selectedAccountIds()" + (navigateToSearch)="handleTabSwitch('search')" > } diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.html b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.html index a0dbdaeb28..fd667425e1 100644 --- a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.html +++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.html @@ -1,4 +1,4 @@ -@if (tableData.length > 0) { +@if (invalidResultsState === 'none') { +} @else if (invalidResultsState === 'tooManyResults') { +

There are more than 100 results.

+

+ Try adding more information + to your search +

} @else { -

There are no matching results

+

There are no matching results.

+

+ Check your search + and try again +

} diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.spec.ts b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.spec.ts index 34c3f655e5..afa5cd0a7d 100644 --- a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.spec.ts +++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.spec.ts @@ -226,8 +226,7 @@ describe('FinesConSearchResultComponent', () => { ]); }); - it('should log and not display results when more than 100 results are provided', () => { - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + it('should set tooManyResults state and not display table when more than 100 results are provided', () => { const defendantAccounts = Array.from({ length: 101 }, (_, index) => ({ defendant_account_id: index + 1, account_number: `ACC-${index + 1}`, @@ -240,7 +239,31 @@ describe('FinesConSearchResultComponent', () => { expect(component.tableData).toHaveLength(0); expect(component.defendantAccountsData).toHaveLength(0); expect(component.checksByAccountId).toEqual({}); - expect(logSpy).toHaveBeenCalledWith('more than 100 results'); + expect(component.invalidResultsState).toBe('tooManyResults'); + }); + + it('should set noResults state when no accounts are provided', () => { + component.defendantAccounts = []; + + expect(component.tableData).toHaveLength(0); + expect(component.defendantAccountsData).toHaveLength(0); + expect(component.checksByAccountId).toEqual({}); + expect(component.invalidResultsState).toBe('noResults'); + }); + + it('should set table state when result set is valid', () => { + component.defendantAccounts = FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_FORMATTING_MOCK; + + expect(component.tableData.length).toBeGreaterThan(0); + expect(component.invalidResultsState).toBe('none'); + }); + + it('should emit navigateToSearch when navigateBackToSearch is called', () => { + const navigateToSearchSpy = vi.spyOn(component.navigateToSearch, 'emit'); + + component.navigateBackToSearch(); + + expect(navigateToSearchSpy).toHaveBeenCalledTimes(1); }); it('should ignore stale in-flight response when a newer search is triggered', () => { diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.ts b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.ts index 78c4cab4db..d316be2697 100644 --- a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.ts +++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.ts @@ -1,4 +1,13 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnDestroy } from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + Output, + inject, + Input, + OnDestroy, +} from '@angular/core'; import { CommonModule } from '@angular/common'; import { Router } from '@angular/router'; import { Subscription } from 'rxjs'; @@ -15,10 +24,11 @@ import { FinesConDefendant } from '../../types/fines-con-defendant.type'; import { FinesConStore } from '../../stores/fines-con.store'; import { IFinesConSearchResultAccountCheck } from './interfaces/fines-con-search-result-account-check.interface'; import { FinesConPayloadService } from '../../services/fines-con-payload.service'; +import { GovukBackLinkComponent } from '@hmcts/opal-frontend-common/components/govuk/govuk-back-link'; @Component({ selector: 'app-fines-con-search-result', - imports: [CommonModule, FinesConSearchResultDefendantTableWrapperComponent], + imports: [CommonModule, GovukBackLinkComponent, FinesConSearchResultDefendantTableWrapperComponent], templateUrl: './fines-con-search-result.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -35,10 +45,12 @@ export class FinesConSearchResultComponent implements OnDestroy { public tableData: IFinesConSearchResultDefendantTableWrapperTableData[] = []; public defendantAccountsData: IFinesConSearchResultDefendantAccount[] = []; public checksByAccountId: Record = {}; + public invalidResultsState: 'none' | 'noResults' | 'tooManyResults' = 'noResults'; @Input({ required: true }) public defendantType: FinesConDefendant = 'individual'; @Input({ required: false }) public alreadyAddedAccountIds: number[] = []; + @Output() public navigateToSearch = new EventEmitter(); @Input({ required: false }) public set searchPayload(searchPayload: IOpalFinesDefendantAccountSearchParams | null) { @@ -96,18 +108,25 @@ export class FinesConSearchResultComponent implements OnDestroy { private applyMappedResults(defendantAccounts: IFinesConSearchResultDefendantAccount[]): void { if (defendantAccounts.length > this.MAX_RESULTS_WARNING_THRESHOLD) { - // eslint-disable-next-line no-console - console.log('more than 100 results'); + this.defendantAccountsData = []; + this.tableData = []; + this.checksByAccountId = {}; + this.invalidResultsState = 'tooManyResults'; + return; + } + if (defendantAccounts.length === 0) { this.defendantAccountsData = []; this.tableData = []; this.checksByAccountId = {}; + this.invalidResultsState = 'noResults'; return; } this.defendantAccountsData = defendantAccounts; this.tableData = this.finesConPayloadService.mapDefendantAccounts(defendantAccounts); this.checksByAccountId = this.finesConPayloadService.buildChecksByAccountId(defendantAccounts); + this.invalidResultsState = 'none'; } /** @@ -148,6 +167,13 @@ export class FinesConSearchResultComponent implements OnDestroy { // Navigation to "For consolidation" tab is implemented in a separate ticket. } + /** + * Navigates user back to Search tab in the consolidation flow. + */ + public navigateBackToSearch(): void { + this.navigateToSearch.emit(); + } + public ngOnDestroy(): void { this.defendantAccountsSearchSubscription?.unsubscribe(); this.defendantAccountsSearchSubscription = null; From fbd57171a947781bacdf45811055ff15c5d3b4da Mon Sep 17 00:00:00 2001 From: Arnab subedi <147511052+Arnabsubedi233@users.noreply.github.com> Date: Thu, 19 Mar 2026 09:22:45 +0000 Subject: [PATCH 2/9] remove unused import --- .../fines-con-search-result.component.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.ts b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.ts index d316be2697..fd2c1831d1 100644 --- a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.ts +++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.ts @@ -24,11 +24,10 @@ import { FinesConDefendant } from '../../types/fines-con-defendant.type'; import { FinesConStore } from '../../stores/fines-con.store'; import { IFinesConSearchResultAccountCheck } from './interfaces/fines-con-search-result-account-check.interface'; import { FinesConPayloadService } from '../../services/fines-con-payload.service'; -import { GovukBackLinkComponent } from '@hmcts/opal-frontend-common/components/govuk/govuk-back-link'; @Component({ selector: 'app-fines-con-search-result', - imports: [CommonModule, GovukBackLinkComponent, FinesConSearchResultDefendantTableWrapperComponent], + imports: [CommonModule, FinesConSearchResultDefendantTableWrapperComponent], templateUrl: './fines-con-search-result.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) From 689bf9b84970d8cbd06a0f75bb7377fde7ae5558 Mon Sep 17 00:00:00 2001 From: Arnab subedi <147511052+Arnabsubedi233@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:40:08 +0000 Subject: [PATCH 3/9] Comment response --- .../fines-con-search-result.component.html | 8 ++------ .../fines-con-search-result.component.ts | 3 ++- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.html b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.html index fd667425e1..0a53f8f72f 100644 --- a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.html +++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.html @@ -10,17 +10,13 @@ } @else if (invalidResultsState === 'tooManyResults') {

There are more than 100 results.

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

} @else {

There are no matching results.

- Check your search + Check your search and try again

} diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.ts b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.ts index fd2c1831d1..22857f52cd 100644 --- a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.ts +++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.ts @@ -169,7 +169,8 @@ export class FinesConSearchResultComponent implements OnDestroy { /** * Navigates user back to Search tab in the consolidation flow. */ - public navigateBackToSearch(): void { + public navigateBackToSearch(event: Event): void { + event.preventDefault(); this.navigateToSearch.emit(); } From 385ab3e84107cccd90d292b67e7dbc3161a4c690 Mon Sep 17 00:00:00 2001 From: hmcts-jenkins-cnp <60659747+hmcts-jenkins-cnp[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:41:33 +0000 Subject: [PATCH 4/9] Bumping chart version/ fixing aliases --- charts/opal-frontend/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/opal-frontend/Chart.yaml b/charts/opal-frontend/Chart.yaml index b259fa7301..f972a695b8 100644 --- a/charts/opal-frontend/Chart.yaml +++ b/charts/opal-frontend/Chart.yaml @@ -3,7 +3,7 @@ appVersion: '1.0' description: A Helm chart for opal-frontend name: opal-frontend home: https://github.com/hmcts/opal-frontend/ -version: 0.0.305 +version: 0.0.306 maintainers: - name: HMCTS Opal team dependencies: From ceaff7ef80caffcde3ee0bf760e350e1cc9453d6 Mon Sep 17 00:00:00 2001 From: Arnab subedi <147511052+Arnabsubedi233@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:56:46 +0000 Subject: [PATCH 5/9] fixing merge issues --- .../fines-con-consolidate-acc.component.html | 12 ++--- .../fines-con-search-result.component.html | 48 ++++++++++--------- .../fines-con-search-result.component.spec.ts | 2 +- 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-consolidate-acc/fines-con-consolidate-acc.component.html b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-consolidate-acc/fines-con-consolidate-acc.component.html index d3f0a1fe75..1c79ed1bf8 100644 --- a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-consolidate-acc/fines-con-consolidate-acc.component.html +++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-consolidate-acc/fines-con-consolidate-acc.component.html @@ -62,12 +62,12 @@

Consolidate accounts

> } @case ('results') { - + } @case ('for-consolidation') {
diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.html b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.html index 7b4ea56e24..11eff08a4e 100644 --- a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.html +++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.html @@ -1,24 +1,26 @@ -@if (searchResults$ | async; as searchResults) { - @if (invalidResultsState === 'none') { - - } @else if (invalidResultsState === 'tooManyResults') { -

There are more than 100 results.

-

- Try adding more information - to your search -

- } @else { -

There are no matching results.

-

- Check your search - and try again -

+
+ @if (searchResults$ | async; as searchResults) { + @if (invalidResultsState === 'none') { + + } @else if (invalidResultsState === 'tooManyResults') { +

There are more than 100 results.

+

+ Try adding more information + to your search +

+ } @else { +

There are no matching results.

+

+ Check your search + and try again +

+ } } -} \ No newline at end of file +
diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.spec.ts b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.spec.ts index 572350e2f7..9d69b53ffb 100644 --- a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.spec.ts +++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.spec.ts @@ -246,7 +246,7 @@ describe('FinesConSearchResultComponent', () => { fixture.detectChanges(); expect(component.tableData).toHaveLength(0); - expect(component.defendantAccountsData).toHaveLength(0); + expect(component.defendantAccountsData).toHaveLength(101); expect(component.checksByAccountId).toEqual({}); expect(component.invalidResultsState).toBe('tooManyResults'); }); From c42a563af23a27448f88f4c4e09eb6dd498b65e6 Mon Sep 17 00:00:00 2001 From: hmcts-jenkins-cnp <60659747+hmcts-jenkins-cnp[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:58:50 +0000 Subject: [PATCH 6/9] Bumping chart version/ fixing aliases --- charts/opal-frontend/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/opal-frontend/Chart.yaml b/charts/opal-frontend/Chart.yaml index ec62809f54..1507657e06 100644 --- a/charts/opal-frontend/Chart.yaml +++ b/charts/opal-frontend/Chart.yaml @@ -3,7 +3,7 @@ appVersion: '1.0' description: A Helm chart for opal-frontend name: opal-frontend home: https://github.com/hmcts/opal-frontend/ -version: 0.0.306 +version: 0.0.307 maintainers: - name: HMCTS Opal team dependencies: From c4c196f6195dc4b1688bb79b1ad02391e51bfbbd Mon Sep 17 00:00:00 2001 From: Arnab subedi <147511052+Arnabsubedi233@users.noreply.github.com> Date: Thu, 26 Mar 2026 14:05:19 +0000 Subject: [PATCH 7/9] fixing accessibility issue --- ...con-search-result-defendant-table-wrapper.component.html | 1 + ...-search-result-defendant-table-wrapper.component.spec.ts | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.html b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.html index 1b5a133618..3ceda3c74a 100644 --- a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.html +++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.html @@ -21,6 +21,7 @@

Select accounts to consolidate

+ Select accounts to consolidate @if (hasSelectableRowsComputed()) {
{ expect(selectAllCheckbox).toBeNull(); }); + it('should render accessible text for the select all header', () => { + const selectAllHeader: HTMLElement | null = fixture.nativeElement.querySelector('#defendants-select-all'); + + expect(selectAllHeader?.textContent).toContain('Select accounts to consolidate'); + }); + it('should remove stale row controls when table data shrinks', () => { component.tableData = GENERATE_FINES_CON_SEARCH_RESULT_DEFENDANT_TABLE_WRAPPER_TABLE_DATA_MOCKS(1); fixture.detectChanges(); From 71b5caf22ea734645e7c076119c1074555aca7da Mon Sep 17 00:00:00 2001 From: Arnab subedi <147511052+Arnabsubedi233@users.noreply.github.com> Date: Mon, 30 Mar 2026 10:30:37 +0100 Subject: [PATCH 8/9] Fixing bold display on tables. --- ...ult-defendant-table-wrapper.component.html | 26 +++++++++++++++++-- ...-defendant-table-wrapper.component.spec.ts | 26 +++++++++++++++++++ ...esult-defendant-table-wrapper.component.ts | 21 +++++++++++++++ .../fines-con-search-result.component.html | 4 +-- 4 files changed, 73 insertions(+), 4 deletions(-) diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.html b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.html index 3ceda3c74a..289afa5c5e 100644 --- a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.html +++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.html @@ -296,11 +296,33 @@

Select accounts to consolidate

@if (severityChecks.length === 1) { - {{ severityChecks[0].message }} + + @for ( + part of getFormattedCheckMessageParts(severityChecks[0].message); + track part.text + '-' + $index + ) { + @if (part.emphasized) { + {{ part.text }} + } @else { + {{ part.text }} + } + } + } @else {
    @for (check of severityChecks; track check.reference + '-' + $index) { -
  • {{ check.message }}
  • +
  • + @for ( + part of getFormattedCheckMessageParts(check.message); + track part.text + '-' + $index + ) { + @if (part.emphasized) { + {{ part.text }} + } @else { + {{ part.text }} + } + } +
  • }
} diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.spec.ts b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.spec.ts index 076f807b51..774c86a95b 100644 --- a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.spec.ts +++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.spec.ts @@ -99,6 +99,25 @@ describe('FinesConSearchResultDefendantTableWrapperComponent', () => { expect(checkMessage).toContain('Account status is Consolidated'); }); + it('should bold delimited text in rendered check messages', () => { + component.tableData = GENERATE_FINES_CON_SEARCH_RESULT_DEFENDANT_TABLE_WRAPPER_TABLE_DATA_MOCKS(1); + component.checksByAccountId = { + 1: [ + { + reference: 'CON.WN.1', + severity: 'warning', + message: 'Last enforcement action on the account is `Application made for Benefit Deductions(ABDC)`', + }, + ], + }; + + fixture.detectChanges(); + + const boldText: HTMLElement | null = fixture.nativeElement.querySelector('.defendant-check-message__text strong'); + expect(boldText?.textContent).toBe('Application made for Benefit Deductions(ABDC)'); + expect(fixture.nativeElement.textContent).toContain('Last enforcement action on the account is'); + }); + it('should only return error checks when both warnings and errors exist', () => { const row = GENERATE_FINES_CON_SEARCH_RESULT_DEFENDANT_TABLE_WRAPPER_TABLE_DATA_MOCKS(1)[0]; component.checksByAccountId = @@ -114,6 +133,13 @@ describe('FinesConSearchResultDefendantTableWrapperComponent', () => { expect(component.getChecksBySeverity(row, 'warning')).toEqual([]); }); + it('should split delimited check messages into emphasised parts', () => { + expect(component.getFormattedCheckMessageParts('Account status is `CS`')).toEqual([ + { text: 'Account status is ', emphasized: false }, + { text: 'CS', emphasized: true }, + ]); + }); + it('should show validation error and not emit when no selectable account is selected', () => { const emitSpy = vi.spyOn(component.addToList, 'emit'); diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.ts b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.ts index a253140699..dc1b4ac1c2 100644 --- a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.ts +++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result-defendant-table-wrapper/fines-con-search-result-defendant-table-wrapper.component.ts @@ -150,6 +150,27 @@ export class FinesConSearchResultDefendantTableWrapperComponent extends Abstract } } + /** + * Splits check messages into plain and emphasised parts when the API wraps values in backticks. + * + * @param message - Raw check message. + * @returns Message parts with emphasis metadata for template rendering. + */ + public getFormattedCheckMessageParts(message: string): { text: string; emphasized: boolean }[] { + const parts = message.split('`'); + + if (parts.length < 3 || parts.length % 2 === 0) { + return [{ text: message, emphasized: false }]; + } + + return parts + .filter((part) => part.length > 0) + .map((text, index) => ({ + text, + emphasized: index % 2 === 1, + })); + } + /** * Emits selected account id for parent-level navigation handling. * diff --git a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.html b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.html index 11eff08a4e..ad87386092 100644 --- a/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.html +++ b/src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/fines-con-search-result.component.html @@ -13,13 +13,13 @@

There are more than 100 results.

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

} @else {

There are no matching results.

Check your search - and try again + and try again.

} } From b29c8bad40f13d766712e9a361eab7906c709afc Mon Sep 17 00:00:00 2001 From: jonathanDuffy Date: Mon, 30 Mar 2026 14:30:58 +0100 Subject: [PATCH 9/9] Add Consolidation Result Test Coverage (#2368) Co-authored-by: hmcts-jenkins-cnp <60659747+hmcts-jenkins-cnp[bot]@users.noreply.github.com> --- .../fines/consolidation/AccountResult.cy.ts | 777 ++++++++++++++++++ .../fines/consolidation/AccountSearch.cy.ts | 311 +++++-- .../fines/consolidation/ErrorPage.cy.ts | 64 ++ .../mocks/account_results_mock.ts | 183 +++++ .../consolidation/setup/SetupComponent.ts | 6 + .../setup/setupComponent.interface.ts | 2 + .../consolidation/consolidation.actions.ts | 336 +++++++- .../FineAccountConsolidation.feature | 179 +++- ...sAccountConsolidationAccessibility.feature | 73 +- .../opal/flows/consolidation.flow.ts | 97 +++ .../consolidation/AccountResults.locators.ts | 43 + .../consolidation/AccountSearch.locators.ts | 1 + .../consolidation/ErrorPage.locators.ts | 8 + .../consolidation/consolidation.steps.ts | 75 ++ 14 files changed, 2065 insertions(+), 90 deletions(-) create mode 100644 cypress/component/fines/consolidation/AccountResult.cy.ts create mode 100644 cypress/component/fines/consolidation/ErrorPage.cy.ts create mode 100644 cypress/component/fines/consolidation/mocks/account_results_mock.ts create mode 100644 cypress/shared/selectors/consolidation/AccountResults.locators.ts create mode 100644 cypress/shared/selectors/consolidation/ErrorPage.locators.ts diff --git a/cypress/component/fines/consolidation/AccountResult.cy.ts b/cypress/component/fines/consolidation/AccountResult.cy.ts new file mode 100644 index 0000000000..ff59bfe565 --- /dev/null +++ b/cypress/component/fines/consolidation/AccountResult.cy.ts @@ -0,0 +1,777 @@ +import { AccountSearchLocators } from '../../../shared/selectors/consolidation/AccountSearch.locators'; +import { AccountResultsLocators } from '../../../shared/selectors/consolidation/AccountResults.locators'; +import { FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_COMPANY_FORMATTING_MOCK } from 'src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/mocks/fines-con-search-result-defendant-accounts-company-formatting.mock'; +import { FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_FORMATTING_MOCK } from 'src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/mocks/fines-con-search-result-defendant-accounts-formatting.mock'; +import { IFinesConSearchResultDefendantAccount } from 'src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/interfaces/fines-con-search-result-defendant-account.interface'; +import { setupConsolidationComponent as mountConsolidationComponent } from './setup/SetupComponent'; +import { IComponentProperties } from './setup/setupComponent.interface'; +import { + createCompanyFalseyResult, + createCompanyMaxResultsMock, + createCompanyTooManyResultsMock, + createCompanyMultipleErrorsAndWarningsResult, + createCompanyMultipleWarningsResult, + createFalseyResult, + createMaxResultsMock, + createTooManyResultsMock, + createMultipleErrorsAndWarningsResult, + createMultipleWarningsResult, +} from './mocks/account_results_mock'; + +const CONSOLIDATION_JIRA_LABEL = '@JIRA-LABEL:consolidation'; +const CONSOLIDATION_EPIC_TAG = '@JIRA-STORY:PO-2294'; +const INDIVIDUAL_STORY_TAG = '@JIRA-STORY:PO-2415'; +const COMPANY_STORY_TAG = '@JIRA-STORY:PO-2421'; +const RESULTS_TAB_FUNCTIONALITY_STORY_TAG = '@JIRA-STORY:PO-2416'; +const INVALID_RESULTS_STORY_TAG = '@JIRA-STORY:PO-2420'; +const EM_DASH = '—'; +const individualResultsTableHeaders = [ + 'Account', + 'Name', + 'Aliases', + 'Date of birth', + 'Address line 1', + 'Postcode', + 'CO', + 'ENF', + 'Balance', + 'P/G', + 'NI number', + 'Ref', +]; +const companyResultsTableHeaders = [ + 'Account', + 'Name', + 'Aliases', + 'Address line 1', + 'Postcode', + 'CO', + 'ENF', + 'Balance', + 'Ref', +]; + +const buildTags = (...tags: string[]): string[] => [...tags, CONSOLIDATION_JIRA_LABEL]; +const buildIndividualTags = (...tags: string[]): string[] => + buildTags(CONSOLIDATION_EPIC_TAG, INDIVIDUAL_STORY_TAG, ...tags); +const buildCompanyTags = (...tags: string[]): string[] => buildTags(CONSOLIDATION_EPIC_TAG, COMPANY_STORY_TAG, ...tags); +const buildResultsTabFunctionalityTags = (...tags: string[]): string[] => + buildTags(CONSOLIDATION_EPIC_TAG, RESULTS_TAB_FUNCTIONALITY_STORY_TAG, ...tags); +const buildInvalidResultsTags = (...tags: string[]): string[] => + buildTags(CONSOLIDATION_EPIC_TAG, INVALID_RESULTS_STORY_TAG, ...tags); +const normaliseText = (value: string): string => value.replace(/\s+/g, ' ').trim(); +type ExpectedResultsOrderRow = { + account: string; + name: string; + dateOfBirth?: string; +}; + +describe('FinesConConsolidateAccComponent - Account Results', () => { + let defendantAccountResults: IFinesConSearchResultDefendantAccount[] = structuredClone( + FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_FORMATTING_MOCK, + ); + + const defaultComponentProperties: IComponentProperties = { + defendantType: 'individual', + fragments: 'results', + }; + + const setupComponent = (componentProperties: IComponentProperties = {}) => { + return mountConsolidationComponent({ + ...defaultComponentProperties, + ...componentProperties, + initialResults: defendantAccountResults, + }); + }; + + const assertResultsTabSummary = (defendantType: 'Individual' | 'Company' = 'Individual') => { + cy.get(AccountSearchLocators.heading).should('contain', 'Consolidate accounts'); + cy.get(AccountSearchLocators.businessUnitKey).should('contain', 'Business unit'); + cy.get(AccountSearchLocators.businessUnitValue).should('contain', 'Historical Debt'); + cy.get(AccountSearchLocators.defendantTypeKey).should('contain', 'Defendant type'); + cy.get(AccountSearchLocators.defendantTypeValue).should('contain', defendantType); + cy.get(AccountSearchLocators.resultsTab).should('have.attr', 'aria-current', 'page'); + }; + + const assertResultsSummary = (defendantType: 'Individual' | 'Company' = 'Individual') => { + assertResultsTabSummary(defendantType); + cy.get(AccountResultsLocators.resultsTable).should('be.visible'); + }; + + const assertNoMatchingResultsState = (defendantType: 'Individual' | 'Company' = 'Individual') => { + assertResultsTabSummary(defendantType); + cy.get(AccountResultsLocators.resultsTable).should('not.exist'); + cy.get(AccountResultsLocators.invalidResultsHeading).should('contain', 'There are no matching results.'); + cy.get(AccountResultsLocators.invalidResultsBody) + .invoke('text') + .then((text) => { + expect(normaliseText(text)).to.equal('Check your search and try again.'); + }); + cy.get(AccountResultsLocators.invalidResultsLink).should('contain', 'Check your search'); + }; + + const assertTooManyResultsState = (defendantType: 'Individual' | 'Company' = 'Individual') => { + assertResultsTabSummary(defendantType); + cy.get(AccountResultsLocators.resultsTable).should('not.exist'); + cy.get(AccountResultsLocators.invalidResultsHeading).should('contain', 'There are more than 100 results.'); + cy.get(AccountResultsLocators.invalidResultsBody) + .invoke('text') + .then((text) => { + expect(normaliseText(text)).to.equal('Try adding more information to your search.'); + }); + cy.get(AccountResultsLocators.invalidResultsLink).should('contain', 'Try adding more information'); + }; + + const assertRowCellText = (accountNumber: string, cellSelector: string, expectedText: string) => { + cy.get(AccountResultsLocators.resultRowWithAccount(accountNumber)) + .find(cellSelector) + .should(($cell) => { + expect(normaliseText($cell.text())).to.equal(expectedText); + }); + }; + + const assertDisplayedResultsOrder = (expectedRows: ExpectedResultsOrderRow[]) => { + cy.get(AccountResultsLocators.resultAccountLink) + .should('have.length', expectedRows.length) + .then(($accountLinks) => { + const actualRows = [...$accountLinks].map((accountLink) => { + const row = Cypress.$(accountLink).closest('tr'); + const actualRow: ExpectedResultsOrderRow = { + account: normaliseText(accountLink.textContent ?? ''), + name: normaliseText(row.find(AccountResultsLocators.resultNameCell).text()), + }; + + if ('dateOfBirth' in expectedRows[0]) { + actualRow.dateOfBirth = normaliseText(row.find(AccountResultsLocators.resultDateOfBirthCell).text()); + } + + return actualRow; + }); + + expect(actualRows).to.deep.equal(expectedRows); + }); + }; + + const buildIndividualResult = ( + overrides: Partial, + ): IFinesConSearchResultDefendantAccount => ({ + ...structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_FORMATTING_MOCK[0]), + aliases: null, + checks: { + errors: [], + warnings: [], + }, + ...overrides, + }); + + const buildCompanyResult = ( + overrides: Partial, + ): IFinesConSearchResultDefendantAccount => ({ + ...structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_COMPANY_FORMATTING_MOCK[0]), + aliases: null, + checks: { + errors: [], + warnings: [], + }, + ...overrides, + }); + + describe('Individual tests', () => { + beforeEach(() => { + defendantAccountResults = structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_FORMATTING_MOCK); + }); + + it( + 'AC1, AC1a, AC1b. should render the individual account results tab with populated mock data', + { tags: buildIndividualTags() }, + () => { + setupComponent(); + + // AC1, AC1a, AC1b. Results tab renders with the selected business unit and defendant type. + cy.get(AccountSearchLocators.heading).should('contain', 'Consolidate accounts'); + cy.get(AccountSearchLocators.businessUnitKey).should('contain', 'Business unit'); + cy.get(AccountSearchLocators.businessUnitValue).should('contain', 'Historical Debt'); + cy.get(AccountSearchLocators.defendantTypeKey).should('contain', 'Defendant type'); + cy.get(AccountSearchLocators.defendantTypeValue).should('contain', 'Individual'); + cy.get(AccountSearchLocators.resultsTab).should('have.attr', 'aria-current', 'page'); + + // AC1. Results tab content renders with the results table and actions. + cy.get(AccountResultsLocators.resultsHeading).should('contain', 'Select accounts to consolidate'); + cy.get(AccountResultsLocators.addToListButton).should('contain', 'Add to list'); + cy.get(AccountResultsLocators.selectedAccountsHint).should('be.visible'); + cy.get(AccountResultsLocators.resultsTable).should('be.visible'); + cy.get(AccountResultsLocators.resultAccountLinkByNumber('ACC001')).should('be.visible'); + cy.get(AccountResultsLocators.resultRowWithAccount('ACC001')) + .find(AccountResultsLocators.resultNameCell) + .should('contain', 'SMITH, John James'); + }, + ); + + it( + 'AC2, AC2a, AC5a, AC5b, AC5c, AC5d, AC5e, AC5f, AC5g, AC5h, AC5i. should display the individual results columns in the AC order and format populated data', + { tags: buildIndividualTags() }, + () => { + defendantAccountResults[0].has_paying_parent_guardian = true; // Set to true to confirm Y is displayed in the relevant cell + defendantAccountResults[0].checks = { errors: [], warnings: [] }; //checks should be empty for check boxes to appear + setupComponent(); + + assertResultsSummary(); + cy.get(AccountResultsLocators.resultSelectAllCheckbox).should('exist'); + // AC2a. Results table displays the named columns in the required order. + cy.get(AccountResultsLocators.resultsTableNamedHeaders).then(($headers) => { + const headers = [...$headers].map((header) => normaliseText(header.textContent ?? '')); + expect(headers).to.deep.equal(individualResultsTableHeaders); + }); + + cy.get(AccountResultsLocators.resultAccountLinkByNumber('ACC001')).should('be.visible'); + // AC5a. Name displays SURNAME, Forename. + assertRowCellText('ACC001', AccountResultsLocators.resultNameCell, 'SMITH, John James'); + // AC5b. Aliases display in ascending alias order when one or more aliases exist. + assertRowCellText('ACC001', AccountResultsLocators.resultAliasesCell, 'ADAMS, Amy BAKER, Ben'); + // AC5c. Date of birth displays as DD Mon YYYY. + assertRowCellText('ACC001', AccountResultsLocators.resultDateOfBirthCell, '03 Jan 1990'); + assertRowCellText('ACC001', AccountResultsLocators.resultAddressLine1Cell, '1 Main Street'); + assertRowCellText('ACC001', AccountResultsLocators.resultPostcodeCell, 'AB1 2CD'); + // AC5d. CO displays Y when collection order is true. + assertRowCellText('ACC001', AccountResultsLocators.resultCollectionOrderCell, 'Y'); + // AC5e. ENF displays the most recent enforcement action code. + assertRowCellText('ACC001', AccountResultsLocators.resultEnforcementCell, 'DISTRESS'); + // AC5f. Balance displays with a pound sign and currency formatting. + assertRowCellText('ACC001', AccountResultsLocators.resultBalanceCell, '£120.50'); + // AC5g. P/G displays Y when a paying parent or guardian exists. + assertRowCellText('ACC001', AccountResultsLocators.resultPayingParentGuardianCell, 'Y'); + // AC5h. NI number displays in the standard formatted layout. + assertRowCellText('ACC001', AccountResultsLocators.resultNationalInsuranceNumberCell, 'QQ 12 34 56 C'); + // AC5i. Ref displays the prosecutor case reference when present. + assertRowCellText('ACC001', AccountResultsLocators.resultRefCell, 'REF-1'); + }, + ); + + it( + 'AC2b, AC2c, AC5b, AC5d, AC5fi, AC5g. should display an em dash for optional or unavailable account data', + { tags: buildIndividualTags() }, + () => { + defendantAccountResults.push(createFalseyResult()); + + setupComponent(); + + assertResultsSummary(); + cy.get(AccountResultsLocators.resultAccountLinkByNumber('ACC002')).should('be.visible'); + cy.get(AccountResultsLocators.resultRowWithAccount('ACC002')) + .find(AccountResultsLocators.resultNameCell) + .should('contain', EM_DASH); + // AC5b. Aliases display only when aliases exist; otherwise the no-data marker is shown. + assertRowCellText('ACC002', AccountResultsLocators.resultAliasesCell, EM_DASH); + assertRowCellText('ACC002', AccountResultsLocators.resultDateOfBirthCell, EM_DASH); + assertRowCellText('ACC002', AccountResultsLocators.resultAddressLine1Cell, EM_DASH); + assertRowCellText('ACC002', AccountResultsLocators.resultPostcodeCell, EM_DASH); + // AC5d. CO displays an '-' when collection order is false. + assertRowCellText('ACC002', AccountResultsLocators.resultCollectionOrderCell, '-'); + assertRowCellText('ACC002', AccountResultsLocators.resultEnforcementCell, EM_DASH); + assertRowCellText('ACC002', AccountResultsLocators.resultBalanceCell, EM_DASH); + // AC5g. P/G displays an '-' when there is no paying parent or guardian. + assertRowCellText('ACC002', AccountResultsLocators.resultPayingParentGuardianCell, '-'); + assertRowCellText('ACC002', AccountResultsLocators.resultNationalInsuranceNumberCell, EM_DASH); + assertRowCellText('ACC002', AccountResultsLocators.resultRefCell, EM_DASH); + }, + ); + + it( + 'AC2d, AC2e. should display a maximum of 100 accounts on a single scrollable page with no pagination', + { tags: buildIndividualTags() }, + () => { + defendantAccountResults = createMaxResultsMock(); + + setupComponent(); + + assertResultsSummary(); + // AC2e. Results are displayed on a single scrollable page. + cy.get(AccountResultsLocators.resultsScrollPane).should('exist'); + // AC2e. No pagination is displayed. + cy.get(AccountResultsLocators.resultsPagination).should('not.exist'); + // AC2d. A maximum of 100 accounts are displayed per search. + cy.get(AccountResultsLocators.resultAccountLink).should('have.length', 100); + cy.get(AccountResultsLocators.resultAccountLinkByNumber('ACC100')).should('be.visible'); + }, + ); + + it( + 'AC3. should display individual results in Name, Date of birth, then Account number ascending order', + { tags: buildIndividualTags() }, + () => { + defendantAccountResults = [ + buildIndividualResult({ + defendant_account_id: 14, + account_number: 'ACC003', + defendant_surname: 'Resultlink', + defendant_firstnames: 'Aaron', + birth_date: '2003-05-15', + }), + buildIndividualResult({ + defendant_account_id: 11, + account_number: 'ACC001', + defendant_surname: 'Resultlink', + defendant_firstnames: 'Consolidation', + birth_date: '2001-05-15', + }), + buildIndividualResult({ + defendant_account_id: 12, + account_number: 'ACC002', + defendant_surname: 'Resultlink', + defendant_firstnames: 'Consolidation', + birth_date: '2001-05-15', + }), + buildIndividualResult({ + defendant_account_id: 13, + account_number: 'ACC004', + defendant_surname: 'Resultlink', + defendant_firstnames: 'Consolidation', + birth_date: '2002-05-15', + }), + ]; + + setupComponent(); + + assertResultsSummary(); + assertDisplayedResultsOrder([ + { account: 'ACC003', name: 'RESULTLINK, Aaron', dateOfBirth: '15 May 2003' }, + { account: 'ACC001', name: 'RESULTLINK, Consolidation', dateOfBirth: '15 May 2001' }, + { account: 'ACC002', name: 'RESULTLINK, Consolidation', dateOfBirth: '15 May 2001' }, + { account: 'ACC004', name: 'RESULTLINK, Consolidation', dateOfBirth: '15 May 2002' }, + ]); + }, + ); + + it( + 'AC1a, AC1b, AC3, AC3a, AC3b, AC3c. should display the individual over-100 results state with the try adding more information link', + { tags: buildInvalidResultsTags() }, + () => { + defendantAccountResults = createTooManyResultsMock(); + + setupComponent(); + // AC1a, AC1b. The Business unit row displays the Business Unit used in the search The Defendant type row displays the defendant type used in the search. + assertResultsTabSummary(); + + assertTooManyResultsState(); + }, + ); + + it( + 'AC1a, AC1b, AC2, AC2a, AC2b, AC2c. should display the individual no-results state with the check your search link', + { tags: buildInvalidResultsTags() }, + () => { + defendantAccountResults = []; + + setupComponent(); + // AC1a, AC1b. The Business unit row displays the Business Unit used in the search The Defendant type row displays the defendant type used in the search. + assertResultsTabSummary(); + + assertNoMatchingResultsState(); + }, + ); + + it( + 'AC7. should display warning and error checks beneath the relevant account row', + { tags: buildIndividualTags() }, + () => { + setupComponent(); + + assertResultsSummary(); + // AC7. Checks are displayed beneath the relevant account row. + cy.get(AccountResultsLocators.resultRowWithAccount('ACC001')) + .next(AccountResultsLocators.resultTableRow) + .find(AccountResultsLocators.resultChecksCellByAccountId(11)) + .should('be.visible') + .and('contain', 'Account has days in default'); + }, + ); + + it( + 'AC7a, AC7b. should show only errors when both errors and warnings exist, listing multiple errors as bullets', + { tags: buildIndividualTags() }, + () => { + defendantAccountResults = [createMultipleErrorsAndWarningsResult()]; + + setupComponent(); + + assertResultsSummary(); + // AC7a. Only errors are displayed when both errors and warnings exist. + cy.get(AccountResultsLocators.resultRowWithAccount('ACC005')) + .next(AccountResultsLocators.resultTableRow) + .find(AccountResultsLocators.resultChecksCellByAccountId(15)) + .should('contain', 'Account status is CS') + .and('contain', 'Account is blocked for consolidation') + .and('not.contain', 'Account has uncleared cheque payments') + .and('not.contain', 'Account has linked cases'); + // AC7b. Multiple errors are displayed as bullet points. + cy.get(AccountResultsLocators.resultChecksCellByAccountId(15)) + .find(AccountResultsLocators.resultChecksBulletItems) + .should('have.length', 2); + }, + ); + + it( + 'AC7c. should display all warnings when multiple warnings apply and no errors exist', + { tags: buildIndividualTags() }, + () => { + defendantAccountResults = [createMultipleWarningsResult()]; + + setupComponent(); + + assertResultsSummary(); + // AC7c. Multiple warnings are displayed when no errors apply. + cy.get(AccountResultsLocators.resultRowWithAccount('ACC006')) + .next(AccountResultsLocators.resultTableRow) + .find(AccountResultsLocators.resultChecksCellByAccountId(16)) + .should('contain', 'Account has uncleared cheque payments') + .and('contain', 'Account has linked cases'); + cy.get(AccountResultsLocators.resultChecksCellByAccountId(16)) + .find(AccountResultsLocators.resultChecksBulletItems) + .should('have.length', 2); + }, + ); + }); + + describe('Company tests', () => { + beforeEach(() => { + defendantAccountResults = structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_COMPANY_FORMATTING_MOCK); + }); + + it( + 'AC1, AC1a, AC1b. should render the company account results tab with populated mock data', + { tags: buildCompanyTags() }, + () => { + setupComponent({ defendantType: 'company' }); + + cy.get(AccountSearchLocators.heading).should('contain', 'Consolidate accounts'); + cy.get(AccountSearchLocators.businessUnitKey).should('contain', 'Business unit'); + cy.get(AccountSearchLocators.businessUnitValue).should('contain', 'Historical Debt'); + cy.get(AccountSearchLocators.defendantTypeKey).should('contain', 'Defendant type'); + cy.get(AccountSearchLocators.defendantTypeValue).should('contain', 'Company'); + cy.get(AccountSearchLocators.resultsTab).should('have.attr', 'aria-current', 'page'); + + cy.get(AccountResultsLocators.resultsHeading).should('contain', 'Select accounts to consolidate'); + cy.get(AccountResultsLocators.addToListButton).should('contain', 'Add to list'); + cy.get(AccountResultsLocators.selectedAccountsHint).should('be.visible'); + cy.get(AccountResultsLocators.resultsTable).should('be.visible'); + cy.get(AccountResultsLocators.resultAccountLinkByNumber('COMP001')).should('be.visible'); + cy.get(AccountResultsLocators.resultRowWithAccount('COMP001')) + .find(AccountResultsLocators.resultNameCell) + .should('contain', 'Acme Corporation'); + }, + ); + + it( + 'AC2, AC2a, AC5a, AC5b, AC5d, AC5e, AC5f, AC5i. should display the company results columns in the AC order and format populated data', + { tags: buildCompanyTags() }, + () => { + setupComponent({ defendantType: 'company' }); + + assertResultsSummary('Company'); + cy.get(AccountResultsLocators.resultSelectAllCheckbox).should('exist'); + cy.get(AccountResultsLocators.resultsTableNamedHeaders).then(($headers) => { + const headers = [...$headers].map((header) => normaliseText(header.textContent ?? '')); + expect(headers).to.deep.equal(companyResultsTableHeaders); + }); + + cy.get(AccountResultsLocators.resultAccountLinkByNumber('COMP001')).should('be.visible'); + assertRowCellText('COMP001', AccountResultsLocators.resultNameCell, 'Acme Corporation'); + assertRowCellText('COMP001', AccountResultsLocators.resultAliasesCell, 'Alpha Ltd Bravo Ltd'); + assertRowCellText('COMP001', AccountResultsLocators.resultAddressLine1Cell, '21 Company Street'); + assertRowCellText('COMP001', AccountResultsLocators.resultPostcodeCell, 'CO1 2MP'); + assertRowCellText('COMP001', AccountResultsLocators.resultCollectionOrderCell, 'Y'); + assertRowCellText('COMP001', AccountResultsLocators.resultEnforcementCell, 'DISTRESS'); + assertRowCellText('COMP001', AccountResultsLocators.resultBalanceCell, '£520.50'); + assertRowCellText('COMP001', AccountResultsLocators.resultRefCell, 'COMP-REF-1'); + cy.get(AccountResultsLocators.resultRowWithAccount('COMP001')) + .find(AccountResultsLocators.resultDateOfBirthCell) + .should('not.exist'); + cy.get(AccountResultsLocators.resultRowWithAccount('COMP001')) + .find(AccountResultsLocators.resultPayingParentGuardianCell) + .should('not.exist'); + cy.get(AccountResultsLocators.resultRowWithAccount('COMP001')) + .find(AccountResultsLocators.resultNationalInsuranceNumberCell) + .should('not.exist'); + }, + ); + + it( + 'AC2b, AC2c, AC5b, AC5d, AC5fi. should display an em dash for unavailable company account data', + { tags: buildCompanyTags() }, + () => { + defendantAccountResults.push(createCompanyFalseyResult()); + + setupComponent({ defendantType: 'company' }); + + assertResultsSummary('Company'); + cy.get(AccountResultsLocators.resultAccountLinkByNumber('COMP002')).should('be.visible'); + cy.get(AccountResultsLocators.resultRowWithAccount('COMP002')) + .find(AccountResultsLocators.resultNameCell) + .should('contain', EM_DASH); + assertRowCellText('COMP002', AccountResultsLocators.resultAliasesCell, EM_DASH); + assertRowCellText('COMP002', AccountResultsLocators.resultAddressLine1Cell, EM_DASH); + assertRowCellText('COMP002', AccountResultsLocators.resultPostcodeCell, EM_DASH); + assertRowCellText('COMP002', AccountResultsLocators.resultCollectionOrderCell, '-'); + assertRowCellText('COMP002', AccountResultsLocators.resultEnforcementCell, EM_DASH); + assertRowCellText('COMP002', AccountResultsLocators.resultBalanceCell, EM_DASH); + assertRowCellText('COMP002', AccountResultsLocators.resultRefCell, EM_DASH); + }, + ); + + it( + 'AC2d, AC2e. should display a maximum of 100 company accounts on a single scrollable page with no pagination', + { tags: buildCompanyTags() }, + () => { + defendantAccountResults = createCompanyMaxResultsMock(); + + setupComponent({ defendantType: 'company' }); + + assertResultsSummary('Company'); + cy.get(AccountResultsLocators.resultsScrollPane).should('exist'); + cy.get(AccountResultsLocators.resultsPagination).should('not.exist'); + cy.get(AccountResultsLocators.resultAccountLink).should('have.length', 100); + cy.get(AccountResultsLocators.resultAccountLinkByNumber('COMP100')).should('be.visible'); + }, + ); + + it( + 'AC3. should display company results in Name, then Account number ascending order', + { tags: buildCompanyTags() }, + () => { + defendantAccountResults = [ + buildCompanyResult({ + defendant_account_id: 24, + account_number: 'COMP003', + organisation_name: 'Alpha Holdings', + }), + buildCompanyResult({ + defendant_account_id: 21, + account_number: 'COMP001', + organisation_name: 'Beta Holdings', + }), + buildCompanyResult({ + defendant_account_id: 22, + account_number: 'COMP002', + organisation_name: 'Beta Holdings', + }), + buildCompanyResult({ + defendant_account_id: 23, + account_number: 'COMP004', + organisation_name: 'Gamma Holdings', + }), + ]; + + setupComponent({ defendantType: 'company' }); + + assertResultsSummary('Company'); + assertDisplayedResultsOrder([ + { account: 'COMP003', name: 'Alpha Holdings' }, + { account: 'COMP001', name: 'Beta Holdings' }, + { account: 'COMP002', name: 'Beta Holdings' }, + { account: 'COMP004', name: 'Gamma Holdings' }, + ]); + }, + ); + + it( + 'AC1a, AC1b, AC3, AC3a, AC3b, AC3c. should display the company over-100 results state with the try adding more information link', + { tags: buildInvalidResultsTags() }, + () => { + defendantAccountResults = createCompanyTooManyResultsMock(); + + setupComponent({ defendantType: 'company' }); + + // AC1a, AC1b. The Business unit row displays the Business Unit used in the search The Defendant type row displays the defendant type used in the search. + assertResultsTabSummary('Company'); + assertTooManyResultsState('Company'); + }, + ); + + it( + 'AC1a, AC1b, AC2, AC2a, AC2b, AC2c. should display the company no-results state with the check your search link', + { tags: buildInvalidResultsTags() }, + () => { + defendantAccountResults = []; + + setupComponent({ defendantType: 'company' }); + + // AC1a, AC1b. The Business unit row displays the Business Unit used in the search The Defendant type row displays the defendant type used in the search. + assertResultsTabSummary('Company'); + assertNoMatchingResultsState('Company'); + }, + ); + + it( + 'AC7. should display warning and error checks beneath the relevant company account row', + { tags: buildCompanyTags() }, + () => { + defendantAccountResults[0].checks = { + errors: [{ reference: 'CON.ER.4', message: 'Account has days in default' }], + warnings: [], + }; + + setupComponent({ defendantType: 'company' }); + + assertResultsSummary('Company'); + cy.get(AccountResultsLocators.resultRowWithAccount('COMP001')) + .next(AccountResultsLocators.resultTableRow) + .find(AccountResultsLocators.resultChecksCellByAccountId(21)) + .should('be.visible') + .and('contain', 'Account has days in default'); + }, + ); + + it( + 'AC7a, AC7b. should show only errors for company results when both errors and warnings exist, listing multiple errors as bullets', + { tags: buildCompanyTags() }, + () => { + defendantAccountResults = [createCompanyMultipleErrorsAndWarningsResult()]; + + setupComponent({ defendantType: 'company' }); + + assertResultsSummary('Company'); + cy.get(AccountResultsLocators.resultRowWithAccount('COMP005')) + .next(AccountResultsLocators.resultTableRow) + .find(AccountResultsLocators.resultChecksCellByAccountId(25)) + .should('contain', 'Account status is CS') + .and('contain', 'Account is blocked for consolidation') + .and('not.contain', 'Account has uncleared cheque payments') + .and('not.contain', 'Account has linked cases'); + cy.get(AccountResultsLocators.resultChecksCellByAccountId(25)) + .find(AccountResultsLocators.resultChecksBulletItems) + .should('have.length', 2); + }, + ); + + it( + 'AC7c. should display all warnings for company results when multiple warnings apply and no errors exist', + { tags: buildCompanyTags() }, + () => { + defendantAccountResults = [createCompanyMultipleWarningsResult()]; + + setupComponent({ defendantType: 'company' }); + + assertResultsSummary('Company'); + cy.get(AccountResultsLocators.resultRowWithAccount('COMP006')) + .next(AccountResultsLocators.resultTableRow) + .find(AccountResultsLocators.resultChecksCellByAccountId(26)) + .should('contain', 'Account has uncleared cheque payments') + .and('contain', 'Account has linked cases'); + cy.get(AccountResultsLocators.resultChecksCellByAccountId(26)) + .find(AccountResultsLocators.resultChecksBulletItems) + .should('have.length', 2); + }, + ); + }); + + describe('Results tab functionality tests', () => { + beforeEach(() => { + defendantAccountResults = structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_FORMATTING_MOCK); + }); + + it( + 'AC3, AC3a, AC3b. should show row checkboxes for selectable accounts, hide them for errors, and keep warning rows enabled', + { tags: buildResultsTabFunctionalityTags() }, + () => { + defendantAccountResults[0].checks = { errors: [], warnings: [] }; + defendantAccountResults.push(createMultipleErrorsAndWarningsResult(), createMultipleWarningsResult()); + + setupComponent(); + + assertResultsSummary(); + + // AC3. Each row includes a checkbox for selecting or unselecting the account. + cy.get(AccountResultsLocators.resultRowCheckboxByAccountId(11)) + .should('exist') + .and('be.enabled') + .and('not.be.checked') + .check({ force: true }) + .should('be.checked'); + cy.get(AccountResultsLocators.resultRowCheckboxByAccountId(11)) + .uncheck({ force: true }) + .should('not.be.checked'); + + // AC3a. Accounts that contain one or more errors have their checkbox hidden. + cy.get(AccountResultsLocators.resultRowWithAccount('ACC005')) + .find(AccountResultsLocators.resultRowCheckboxByAccountId(15)) + .should('not.exist'); + + // AC3b. Accounts that contain warnings keep their checkbox enabled. + cy.get(AccountResultsLocators.resultRowCheckboxByAccountId(16)).should('exist').and('be.enabled'); + }, + ); + + it( + 'AC4, AC4a, AC4b, AC4c, AC5a, AC5b, AC5c. should bulk select and deselect all enabled accounts while excluding accounts with errors', + { tags: buildResultsTabFunctionalityTags() }, + () => { + defendantAccountResults[0].checks = { errors: [], warnings: [] }; + defendantAccountResults.push(createMultipleErrorsAndWarningsResult(), createMultipleWarningsResult()); + + setupComponent(); + + assertResultsSummary(); + + // AC4. A top-level checkbox is displayed above the table to allow bulk selection. + // AC5a, AC5b. The dynamic counter shows selected accounts against the total returned results. + cy.get(AccountResultsLocators.selectedAccountsHint).should('contain', '0 of 3 accounts selected'); + + // AC4a. Selecting the top-level checkbox selects all enabled accounts in the results. + cy.get(AccountResultsLocators.resultSelectAllCheckbox) + .should('exist') + .and('not.be.checked') + .check({ force: true }) + .should('be.checked'); + + // AC4b. Accounts with one or more errors are not selected. + // AC5c. The counter updates automatically as accounts are selected. + cy.get(AccountResultsLocators.selectedAccountsHint).should('contain', '2 of 3 accounts selected'); + cy.get(AccountResultsLocators.resultRowCheckboxByAccountId(11)).should('be.checked'); + cy.get(AccountResultsLocators.resultRowWithAccount('ACC005')) + .find(AccountResultsLocators.resultRowCheckboxByAccountId(15)) + .should('not.exist'); + cy.get(AccountResultsLocators.resultRowCheckboxByAccountId(16)).should('be.checked'); + + // AC4c. Deselecting the top-level checkbox deselects all currently selected accounts. + // AC5c. The counter updates automatically as accounts are deselected. + cy.get(AccountResultsLocators.resultSelectAllCheckbox).uncheck({ force: true }).should('not.be.checked'); + + cy.get(AccountResultsLocators.selectedAccountsHint).should('contain', '0 of 3 accounts selected'); + cy.get(AccountResultsLocators.resultRowCheckboxByAccountId(11)).should('not.be.checked'); + cy.get(AccountResultsLocators.resultRowCheckboxByAccountId(16)).should('not.be.checked'); + }, + ); + + it( + 'AC6, AC6a, AC6b. should display Add to list above the counter and show a validation error when no accounts are selected', + { tags: buildResultsTabFunctionalityTags() }, + () => { + defendantAccountResults[0].checks = { errors: [], warnings: [] }; + + setupComponent(); + + assertResultsSummary(); + + // AC6. An Add to list button is displayed above the counter. + cy.get(AccountResultsLocators.addToListButton) + .should('be.visible') + .and('contain', 'Add to list') + .then(($button) => { + cy.get(AccountResultsLocators.selectedAccountsHint).then(($hint) => { + expect($button[0].compareDocumentPosition($hint[0]) & Node.DOCUMENT_POSITION_FOLLOWING).to.not.equal(0); + }); + }); + + // AC6a, AC6b. Selecting Add to list validates the selected accounts and shows an error when none are selected. + cy.get(AccountResultsLocators.addToListButton).click(); + + cy.get(AccountSearchLocators.errorSummary) + .should('be.visible') + .and('contain', 'Select 1 or more accounts to consolidate.'); + cy.get(AccountResultsLocators.addToListErrorMessage) + .should('be.visible') + .and('contain', 'Select 1 or more accounts to consolidate.'); + }, + ); + }); +}); diff --git a/cypress/component/fines/consolidation/AccountSearch.cy.ts b/cypress/component/fines/consolidation/AccountSearch.cy.ts index 206f21ed3d..76d204c307 100644 --- a/cypress/component/fines/consolidation/AccountSearch.cy.ts +++ b/cypress/component/fines/consolidation/AccountSearch.cy.ts @@ -1,6 +1,7 @@ import { AccountSearchLocators } from '../../../shared/selectors/consolidation/AccountSearch.locators'; import { FINES_CON_SEARCH_ACCOUNT_FORM_EMPTY_MOCK } from 'src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-account/mocks/fines-con-search-account-form-empty.mock'; import { IFinesConSearchAccountState } from 'src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-account/interfaces/fines-con-search-account-state.interface'; +import { FINES_CON_ROUTING_PATHS } from 'src/app/flows/fines/fines-con/routing/constants/fines-con-routing-paths.constant'; import { setupConsolidationComponent as mountConsolidationComponent } from './setup/SetupComponent'; import { ConsolidationTabFragment, IComponentProperties } from './setup/setupComponent.interface'; @@ -35,6 +36,36 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => { cy.get(inlineSelector).should('be.visible').and('contain', message); }; + const assertSearchErrorRedirect = (expectedFormData: Partial) => { + cy.get('@routerNavigate').should('have.been.calledWithMatch', [FINES_CON_ROUTING_PATHS.children.searchError], { + relativeTo: {}, + }); + + cy.get('@finesConStore').then((store: any) => { + const searchAccountForm = store.searchAccountForm(); + expect(searchAccountForm).to.deep.include(expectedFormData); + }); + }; + + const assertNoSearchUpdate = (updateSearchSpy: sinon.SinonSpy) => { + cy.then(() => { + expect(updateSearchSpy).to.not.have.been.called; + }); + }; + + const findSubmittedFormData = ( + updateSearchSpy: sinon.SinonSpy, + predicate: (formData: IFinesConSearchAccountState) => boolean, + ): IFinesConSearchAccountState => { + const matchingCall = updateSearchSpy + .getCalls() + .map((call) => call.args[0] as IFinesConSearchAccountState) + .find(predicate); + + expect(matchingCall, 'matching search payload').to.exist; + return matchingCall!; + }; + const switchToTab = (tab: ConsolidationTabFragment) => { cy.get('@finesConStore').then((store: any) => { store.setActiveTab(tab); @@ -69,18 +100,18 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => { //AC1a. Business unit displays the selected BU and is read-only' - cy.get(AccountSearchLocators.businessUnitKey).should('contain', 'Business Unit'); + cy.get(AccountSearchLocators.businessUnitKey).should('contain', 'Business unit'); cy.get(AccountSearchLocators.businessUnitValue).should('contain', 'Historical Debt'); //AC1b. Defendant type displays 'Individual' - cy.get(AccountSearchLocators.defendantTypeKey).should('contain', 'Defendant Type'); + cy.get(AccountSearchLocators.defendantTypeKey).should('contain', 'Defendant type'); cy.get(AccountSearchLocators.defendantTypeValue).should('contain', 'Individual'); //AC1c. Search screen mirrors expected field types, headings and actions cy.get(AccountSearchLocators.tabsNav).should('be.visible'); cy.get(AccountSearchLocators.searchTab).should('contain', 'Search'); cy.get(AccountSearchLocators.resultsTab).should('contain', 'Results'); - cy.get(AccountSearchLocators.forConsolidationTab).should('contain', 'For Consolidation'); + cy.get(AccountSearchLocators.forConsolidationTab).should('contain', 'For consolidation'); cy.get(AccountSearchLocators.quickSearchHeading).should('contain', 'Quick search'); cy.contains(AccountSearchLocators.advancedSearchHeading, 'Advanced Search').should('be.visible'); @@ -127,14 +158,63 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => { ); it( - 'AC3. Invalid search criteria display the expected errors and no search update occurs', + 'AC3a. Invalid account number format displays the expected error and no search update occurs', + { tags: buildTags('@JIRA-KEY:POT-3869') }, + () => { + const updateSearchSpy = Cypress.sinon.spy(); + finesConSearchAccountFormData.fcon_search_account_number = '1234567'; + + setupConsolidationComponent({ updateSearchSpy }); + cy.get(AccountSearchLocators.searchButton).click(); + + assertValidationError( + 'Enter account number in the correct format such as 12345678 or 12345678A', + AccountSearchLocators.accountNumberError, + ); + assertNoSearchUpdate(updateSearchSpy); + }, + ); + + it( + 'AC3b. Invalid National Insurance number format displays the expected error and no search update occurs', + { tags: buildTags('@JIRA-KEY:POT-3869') }, + () => { + const updateSearchSpy = Cypress.sinon.spy(); + finesConSearchAccountFormData.fcon_search_account_national_insurance_number = 'AB12345$C'; + + setupConsolidationComponent({ updateSearchSpy }); + cy.get(AccountSearchLocators.searchButton).click(); + + assertValidationError( + 'Enter a National Insurance number in the format AANNNNNNA', + AccountSearchLocators.nationalInsuranceNumberError, + ); + assertNoSearchUpdate(updateSearchSpy); + }, + ); + + it( + 'AC4a. Account number max length displays the expected error and no search update occurs', + { tags: buildTags('@JIRA-KEY:POT-3870') }, + () => { + const updateSearchSpy = Cypress.sinon.spy(); + finesConSearchAccountFormData.fcon_search_account_number = '123456789A'; + + setupConsolidationComponent({ updateSearchSpy }); + cy.get(AccountSearchLocators.searchButton).click(); + + assertValidationError('Account number must be 9 characters or fewer', AccountSearchLocators.accountNumberError); + assertNoSearchUpdate(updateSearchSpy); + }, + ); + + it( + 'AC3. Invalid advanced search criteria display the expected errors and no search update occurs', { tags: buildTags('@JIRA-KEY:POT-3869') }, () => { const updateSearchSpy = Cypress.sinon.spy(); finesConSearchAccountFormData = { ...structuredClone(FINES_CON_SEARCH_ACCOUNT_FORM_EMPTY_MOCK.formData), - fcon_search_account_number: '1234567', - fcon_search_account_national_insurance_number: 'AB12345$C', fcon_search_account_individuals_search_criteria: { fcon_search_account_individuals_last_name: 'Smith', fcon_search_account_individuals_last_name_exact_match: false, @@ -150,14 +230,6 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => { cy.get(AccountSearchLocators.searchButton).click(); const expectedValidationErrors = [ - { - message: 'Enter account number in the correct format such as 12345678 or 12345678A', - selector: AccountSearchLocators.accountNumberError, - }, - { - message: 'Enter a National Insurance number in the format AANNNNNNA', - selector: AccountSearchLocators.nationalInsuranceNumberError, - }, { message: 'Date of birth must be in the format DD/MM/YYYY', selector: AccountSearchLocators.dateOfBirthError, @@ -176,21 +248,17 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => { assertValidationError(message, selector); }); - cy.then(() => { - expect(updateSearchSpy).to.not.have.been.called; - }); + assertNoSearchUpdate(updateSearchSpy); }, ); it( - 'AC4. Max length validation errors display expected messages and no search update occurs', + 'AC4. Advanced search max length validation errors display expected messages and no search update occurs', { tags: buildTags('@JIRA-KEY:POT-3870') }, () => { const updateSearchSpy = Cypress.sinon.spy(); finesConSearchAccountFormData = { ...structuredClone(FINES_CON_SEARCH_ACCOUNT_FORM_EMPTY_MOCK.formData), - fcon_search_account_number: '123456789A', - fcon_search_account_national_insurance_number: 'AB1234567C', fcon_search_account_individuals_search_criteria: { fcon_search_account_individuals_last_name: 'A'.repeat(31), fcon_search_account_individuals_last_name_exact_match: false, @@ -206,10 +274,6 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => { cy.get(AccountSearchLocators.searchButton).click(); const expectedValidationErrors = [ - { - message: 'Account number must be 9 characters or fewer', - selector: AccountSearchLocators.accountNumberError, - }, { message: 'Last name must be 30 characters or fewer', selector: AccountSearchLocators.lastNameError, @@ -218,10 +282,6 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => { message: 'First names must be 20 characters or fewer', selector: AccountSearchLocators.firstNamesError, }, - { - message: 'Enter a National Insurance number in the format AANNNNNNA', - selector: AccountSearchLocators.nationalInsuranceNumberError, - }, { message: 'Address line 1 must be 30 characters or fewer', selector: AccountSearchLocators.addressLine1Error, @@ -236,9 +296,7 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => { assertValidationError(message, selector); }); - cy.then(() => { - expect(updateSearchSpy).to.not.have.been.called; - }); + assertNoSearchUpdate(updateSearchSpy); }, ); @@ -254,9 +312,7 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => { cy.get(AccountSearchLocators.searchButton).click(); assertValidationError('Enter last name', AccountSearchLocators.lastNameError); - cy.then(() => { - expect(updateSearchSpy).to.not.have.been.called; - }); + assertNoSearchUpdate(updateSearchSpy); }, ); @@ -271,9 +327,7 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => { cy.get(AccountSearchLocators.searchButton).click(); assertValidationError('Enter last name', AccountSearchLocators.lastNameError); - cy.then(() => { - expect(updateSearchSpy).to.not.have.been.called; - }); + assertNoSearchUpdate(updateSearchSpy); }, ); @@ -287,9 +341,7 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => { cy.get(AccountSearchLocators.searchButton).click(); assertValidationError('Enter last name', AccountSearchLocators.lastNameError); - cy.then(() => { - expect(updateSearchSpy).to.not.have.been.called; - }); + assertNoSearchUpdate(updateSearchSpy); }, ); @@ -304,9 +356,7 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => { cy.get(AccountSearchLocators.searchButton).click(); assertValidationError('Enter last name', AccountSearchLocators.lastNameError); - cy.then(() => { - expect(updateSearchSpy).to.not.have.been.called; - }); + assertNoSearchUpdate(updateSearchSpy); }, ); @@ -321,8 +371,10 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => { cy.get(AccountSearchLocators.searchButton).click(); cy.then(() => { - expect(updateSearchSpy).to.have.been.calledOnce; - const submittedFormData = updateSearchSpy.firstCall.args[0] as IFinesConSearchAccountState; + const submittedFormData = findSubmittedFormData( + updateSearchSpy, + (formData) => formData.fcon_search_account_number === '12345678', + ); expect(submittedFormData.fcon_search_account_number).to.equal('12345678'); expect(submittedFormData.fcon_search_account_national_insurance_number).to.be.null; @@ -351,8 +403,10 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => { cy.get(AccountSearchLocators.searchButton).click(); cy.then(() => { - expect(updateSearchSpy).to.have.been.calledOnce; - const submittedFormData = updateSearchSpy.firstCall.args[0] as IFinesConSearchAccountState; + const submittedFormData = findSubmittedFormData( + updateSearchSpy, + (formData) => formData.fcon_search_account_national_insurance_number === 'AB123456C', + ); expect(submittedFormData.fcon_search_account_national_insurance_number).to.equal('AB123456C'); expect(submittedFormData.fcon_search_account_number).to.be.null; @@ -449,14 +503,14 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => { //AC1a. Business unit displays the selected BU and is read-only' - cy.get(AccountSearchLocators.businessUnitKey).should('contain', 'Business Unit'); + cy.get(AccountSearchLocators.businessUnitKey).should('contain', 'Business unit'); cy.get(AccountSearchLocators.businessUnitValue) .should('contain', 'Historical Debt') .find('input, select, textarea') .should('not.exist'); //AC1b. Defendant type displays 'Company' - cy.get(AccountSearchLocators.defendantTypeKey).should('contain', 'Defendant Type'); + cy.get(AccountSearchLocators.defendantTypeKey).should('contain', 'Defendant type'); cy.get(AccountSearchLocators.defendantTypeValue) .should('contain', 'Company') .find('input, select, textarea') @@ -466,7 +520,7 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => { cy.get(AccountSearchLocators.tabsNav).should('be.visible'); cy.get(AccountSearchLocators.searchTab).should('contain', 'Search'); cy.get(AccountSearchLocators.resultsTab).should('contain', 'Results'); - cy.get(AccountSearchLocators.forConsolidationTab).should('contain', 'For Consolidation'); + cy.get(AccountSearchLocators.forConsolidationTab).should('contain', 'For consolidation'); cy.get(AccountSearchLocators.quickSearchHeading).should('contain', 'Quick search'); cy.contains(AccountSearchLocators.advancedSearchHeading, 'Advanced Search').should('be.visible'); @@ -509,13 +563,45 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => { ); it( - 'AC3. Invalid search criteria display the expected errors and no search update occurs', + 'AC3a. Invalid company account number format displays the expected error and no search update occurs', + { tags: buildTags('@JIRA-KEY:POT-3869') }, + () => { + const updateSearchSpy = Cypress.sinon.spy(); + finesConSearchAccountFormData.fcon_search_account_number = '1234567'; + + setupConsolidationComponent({ updateSearchSpy, defendantType: 'company' }); + cy.get(AccountSearchLocators.searchButton).click(); + + assertValidationError( + 'Enter account number in the correct format such as 12345678 or 12345678A', + AccountSearchLocators.accountNumberError, + ); + assertNoSearchUpdate(updateSearchSpy); + }, + ); + + it( + 'AC4a. Company account number max length displays the expected error and no search update occurs', + { tags: buildTags('@JIRA-KEY:POT-3879') }, + () => { + const updateSearchSpy = Cypress.sinon.spy(); + finesConSearchAccountFormData.fcon_search_account_number = '123456789A'; + + setupConsolidationComponent({ updateSearchSpy, defendantType: 'company' }); + cy.get(AccountSearchLocators.searchButton).click(); + + assertValidationError('Account number must be 9 characters or fewer', AccountSearchLocators.accountNumberError); + assertNoSearchUpdate(updateSearchSpy); + }, + ); + + it( + 'AC3. Invalid advanced search criteria display the expected errors and no search update occurs', { tags: buildTags('@JIRA-KEY:POT-3869') }, () => { const updateSearchSpy = Cypress.sinon.spy(); finesConSearchAccountFormData = { ...structuredClone(FINES_CON_SEARCH_ACCOUNT_FORM_EMPTY_MOCK.formData), - fcon_search_account_number: '1234567', fcon_search_account_companies_search_criteria: { fcon_search_account_companies_company_name: 'Testing!!!', fcon_search_account_companies_company_name_exact_match: false, @@ -528,11 +614,6 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => { cy.get(AccountSearchLocators.searchButton).click(); const expectedValidationErrors = [ - { - //AC3a. User enters value that is not in correct format and the following error is produced - message: 'Enter account number in the correct format such as 12345678 or 12345678A', - selector: AccountSearchLocators.accountNumberError, - }, { //AC3b. User enters non-alphabetical or special characters and the following error is produced message: 'Company name must only include letters a to z, hyphens, spaces and apostrophes', @@ -555,20 +636,17 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => { }); //AC3 Following selecting 'search' the system remains on the same screen - cy.then(() => { - expect(updateSearchSpy).to.not.have.been.called; - }); + assertNoSearchUpdate(updateSearchSpy); }, ); it( - 'AC4. Max length search validation displays the expected errors and no search update occurs', + 'AC4. Advanced search max length validation displays the expected errors and no search update occurs', { tags: buildTags('@JIRA-KEY:POT-3879') }, () => { const updateSearchSpy = Cypress.sinon.spy(); finesConSearchAccountFormData = { ...structuredClone(FINES_CON_SEARCH_ACCOUNT_FORM_EMPTY_MOCK.formData), - fcon_search_account_number: '1234567890', fcon_search_account_companies_search_criteria: { fcon_search_account_companies_company_name: 'QwertyuiopQwertyuiopQwertyuiopQwertyuiopQwertyuiopQ', fcon_search_account_companies_company_name_exact_match: false, @@ -581,11 +659,6 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => { cy.get(AccountSearchLocators.searchButton).click(); const expectedValidationErrors = [ - { - //AC4a. User enters value exceeding the max characters. Error isnt in line with others/conflicts this one first. Confirmed in ..field-errors.constant that it should be 'Account number must be 9 characters or fewer',. Pri 3. How to reach? - message: 'Account number must be 9 characters or fewer', - selector: AccountSearchLocators.accountNumberError, - }, { //AC4b. User enters value exceeding the max characters. message: 'Company name must be 50 characters or fewer', @@ -608,9 +681,7 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => { }); //AC4 Following selecting 'search' the system remains on the same screen - cy.then(() => { - expect(updateSearchSpy).to.not.have.been.called; - }); + assertNoSearchUpdate(updateSearchSpy); }, ); @@ -646,9 +717,7 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => { }); //AC5 Following selecting 'search' the system remains on the same screen - cy.then(() => { - expect(updateSearchSpy).to.not.have.been.called; - }); + assertNoSearchUpdate(updateSearchSpy); }, ); @@ -663,8 +732,10 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => { cy.get(AccountSearchLocators.searchButton).click(); cy.then(() => { - expect(updateSearchSpy).to.have.been.calledOnce; - const submittedFormData = updateSearchSpy.firstCall.args[0] as IFinesConSearchAccountState; + const submittedFormData = findSubmittedFormData( + updateSearchSpy, + (formData) => formData.fcon_search_account_number === '12345678', + ); expect(submittedFormData.fcon_search_account_number).to.equal('12345678'); expect(submittedFormData.fcon_search_account_companies_search_criteria).to.deep.equal({ @@ -724,4 +795,94 @@ describe('FinesConConsolidateAccComponent - Account & Company Search', () => { // cy.get(AccountSearchLocators.forConsolidationTab).should('have.attr', 'aria-current', 'page'); }, ); + + it( + 'AC1a. Individual searches route to Search error when quick search and other account details are combined', + { tags: buildTags('@JIRA-KEY:POT-3882') }, + () => { + const updateSearchSpy = Cypress.sinon.spy(); + finesConSearchAccountFormData = { + ...structuredClone(FINES_CON_SEARCH_ACCOUNT_FORM_EMPTY_MOCK.formData), + fcon_search_account_number: '12345678', + fcon_search_account_individuals_search_criteria: { + fcon_search_account_individuals_last_name: 'Smith', + fcon_search_account_individuals_last_name_exact_match: false, + fcon_search_account_individuals_first_names: null, + fcon_search_account_individuals_first_names_exact_match: false, + fcon_search_account_individuals_include_aliases: false, + fcon_search_account_individuals_date_of_birth: null, + fcon_search_account_individuals_address_line_1: null, + fcon_search_account_individuals_post_code: null, + }, + }; + + setupConsolidationComponent({ updateSearchSpy, setupRouterSpy: true }); + cy.get(AccountSearchLocators.searchButton).click(); + + cy.then(() => { + findSubmittedFormData( + updateSearchSpy, + (formData) => + formData.fcon_search_account_number === '12345678' && + formData.fcon_search_account_individuals_search_criteria?.fcon_search_account_individuals_last_name === + 'Smith', + ); + }); + assertSearchErrorRedirect({ + fcon_search_account_number: '12345678', + fcon_search_account_individuals_search_criteria: { + fcon_search_account_individuals_last_name: 'Smith', + fcon_search_account_individuals_last_name_exact_match: false, + fcon_search_account_individuals_first_names: null, + fcon_search_account_individuals_first_names_exact_match: false, + fcon_search_account_individuals_include_aliases: false, + fcon_search_account_individuals_date_of_birth: null, + fcon_search_account_individuals_address_line_1: null, + fcon_search_account_individuals_post_code: null, + }, + }); + }, + ); + + it( + 'AC1b. Company searches route to Search error when account number and other account details are combined', + { tags: buildTags('@JIRA-KEY:POT-3883') }, + () => { + const updateSearchSpy = Cypress.sinon.spy(); + finesConSearchAccountFormData = { + ...structuredClone(FINES_CON_SEARCH_ACCOUNT_FORM_EMPTY_MOCK.formData), + fcon_search_account_number: '12345678', + fcon_search_account_companies_search_criteria: { + fcon_search_account_companies_company_name: 'Test Company', + fcon_search_account_companies_company_name_exact_match: false, + fcon_search_account_companies_include_aliases: false, + fcon_search_account_companies_address_line_1: null, + fcon_search_account_companies_post_code: null, + }, + }; + + setupConsolidationComponent({ updateSearchSpy, defendantType: 'company', setupRouterSpy: true }); + cy.get(AccountSearchLocators.searchButton).click(); + + cy.then(() => { + findSubmittedFormData( + updateSearchSpy, + (formData) => + formData.fcon_search_account_number === '12345678' && + formData.fcon_search_account_companies_search_criteria?.fcon_search_account_companies_company_name === + 'Test Company', + ); + }); + assertSearchErrorRedirect({ + fcon_search_account_number: '12345678', + fcon_search_account_companies_search_criteria: { + fcon_search_account_companies_company_name: 'Test Company', + fcon_search_account_companies_company_name_exact_match: false, + fcon_search_account_companies_include_aliases: false, + fcon_search_account_companies_address_line_1: null, + fcon_search_account_companies_post_code: null, + }, + }); + }, + ); }); diff --git a/cypress/component/fines/consolidation/ErrorPage.cy.ts b/cypress/component/fines/consolidation/ErrorPage.cy.ts new file mode 100644 index 0000000000..f33b3cd290 --- /dev/null +++ b/cypress/component/fines/consolidation/ErrorPage.cy.ts @@ -0,0 +1,64 @@ +import { mount } from 'cypress/angular'; +import { ActivatedRoute, provideRouter } from '@angular/router'; +import { FinesConSearchErrorComponent } from 'src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-error/fines-con-search-error.component'; +import { FinesConStore } from 'src/app/flows/fines/fines-con/stores/fines-con.store'; +import { ErrorPageLocators } from 'cypress/shared/selectors/consolidation/ErrorPage.locators'; + +const CONSOLIDATION_JIRA_LABEL = '@JIRA-LABEL:consolidation'; +const CONSOLIDATION_STORY_LABEL = '@JIRA-STORY:PO-2417'; + +const buildTags = (...tags: string[]): string[] => [...tags, CONSOLIDATION_JIRA_LABEL, CONSOLIDATION_STORY_LABEL]; + +describe('FinesConSearchErrorComponent', () => { + const setupComponent = (defendantType: 'individual' | 'company') => { + return mount(FinesConSearchErrorComponent, { + providers: [ + provideRouter([]), + FinesConStore, + { + provide: ActivatedRoute, + useValue: { + parent: {}, + }, + }, + ], + }).then(({ fixture }) => { + const store = fixture.componentRef.injector.get(FinesConStore); + cy.stub(store, 'getDefendantType').returns(defendantType); + expect(store.getDefendantType()).to.equal(defendantType); + fixture.detectChanges(); + }); + }; + + const assertHeadingAndIntro = () => { + cy.get(ErrorPageLocators.heading).should('contain', 'There is a problem'); + cy.get(ErrorPageLocators.message) + .invoke('text') + .then((text) => { + const normalisedText = text.replace(/\s+/g, ' ').trim(); + expect(normalisedText).to.equal( + 'Reference data and account information cannot be entered together when searching for an account. Search using either:', + ); + }); + }; + + it('AC2a. displays the individual search error heading and message text', { tags: buildTags() }, () => { + setupComponent('individual'); + + assertHeadingAndIntro(); + cy.get(ErrorPageLocators.bulletItems).then(($items) => { + const items = [...$items].map((item) => item.textContent?.replace(/\s+/g, ' ').trim()); + expect(items).to.deep.equal(['account number, or', 'National Insurance number, or', 'advanced search']); + }); + }); + + it('AC2b. displays the company search error heading and message text', { tags: buildTags() }, () => { + setupComponent('company'); + + assertHeadingAndIntro(); + cy.get(ErrorPageLocators.bulletItems).then(($items) => { + const items = [...$items].map((item) => item.textContent?.replace(/\s+/g, ' ').trim()); + expect(items).to.deep.equal(['account number, or', 'advanced search']); + }); + }); +}); diff --git a/cypress/component/fines/consolidation/mocks/account_results_mock.ts b/cypress/component/fines/consolidation/mocks/account_results_mock.ts new file mode 100644 index 0000000000..d91ebf2ac6 --- /dev/null +++ b/cypress/component/fines/consolidation/mocks/account_results_mock.ts @@ -0,0 +1,183 @@ +import { IFinesConSearchResultDefendantAccount } from '@app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/interfaces/fines-con-search-result-defendant-account.interface'; +import { FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_COMPANY_FORMATTING_MOCK } from '@app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/mocks/fines-con-search-result-defendant-accounts-company-formatting.mock'; +import { FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_FORMATTING_MOCK } from '@app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/mocks/fines-con-search-result-defendant-accounts-formatting.mock'; + +export const createFalseyResult = (): IFinesConSearchResultDefendantAccount => ({ + ...structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_FORMATTING_MOCK[0]), + defendant_account_id: 12, + account_number: 'ACC002', + aliases: null, + address_line_1: null, + postcode: null, + prosecutor_case_reference: null, + last_enforcement_action: null, + account_balance: null, + defendant_firstnames: null, + defendant_surname: null, + birth_date: null, + national_insurance_number: null, + collection_order: false, + last_enforcement: null, + has_paying_parent_guardian: false, + checks: { + errors: [], + warnings: [], + }, +}); + +export const createCompanyFalseyResult = (): IFinesConSearchResultDefendantAccount => ({ + ...structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_COMPANY_FORMATTING_MOCK[0]), + defendant_account_id: 22, + account_number: 'COMP002', + aliases: null, + address_line_1: null, + postcode: null, + prosecutor_case_reference: null, + last_enforcement_action: null, + account_balance: null, + organisation_name: null, + collection_order: false, + last_enforcement: null, + checks: { + errors: [], + warnings: [], + }, +}); + +export const createMaxResultsMock = (): IFinesConSearchResultDefendantAccount[] => + Array.from({ length: 100 }, (_, index) => ({ + ...structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_FORMATTING_MOCK[0]), + defendant_account_id: index + 1, + account_number: `ACC${String(index + 1).padStart(3, '0')}`, + aliases: null, + prosecutor_case_reference: `REF-${index + 1}`, + defendant_firstnames: `First${index + 1}`, + defendant_surname: `Surname${index + 1}`, + birth_date: '1990-01-03', + account_balance: 50 + index, + has_paying_parent_guardian: false, + checks: { + errors: [], + warnings: [], + }, + })); + +export const createTooManyResultsMock = (): IFinesConSearchResultDefendantAccount[] => [ + ...createMaxResultsMock(), + { + ...structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_FORMATTING_MOCK[0]), + defendant_account_id: 101, + account_number: 'ACC101', + aliases: null, + prosecutor_case_reference: 'REF-101', + defendant_firstnames: 'First101', + defendant_surname: 'Surname101', + birth_date: '1990-01-03', + account_balance: 150, + has_paying_parent_guardian: false, + checks: { + errors: [], + warnings: [], + }, + }, +]; + +export const createCompanyMaxResultsMock = (): IFinesConSearchResultDefendantAccount[] => + Array.from({ length: 100 }, (_, index) => ({ + ...structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_COMPANY_FORMATTING_MOCK[0]), + defendant_account_id: index + 1, + account_number: `COMP${String(index + 1).padStart(3, '0')}`, + aliases: null, + prosecutor_case_reference: `COMP-REF-${index + 1}`, + organisation_name: `Company ${index + 1}`, + account_balance: 500 + index, + checks: { + errors: [], + warnings: [], + }, + })); + +export const createCompanyTooManyResultsMock = (): IFinesConSearchResultDefendantAccount[] => [ + ...createCompanyMaxResultsMock(), + { + ...structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_COMPANY_FORMATTING_MOCK[0]), + defendant_account_id: 101, + account_number: 'COMP101', + aliases: null, + prosecutor_case_reference: 'COMP-REF-101', + organisation_name: 'Company 101', + account_balance: 600, + checks: { + errors: [], + warnings: [], + }, + }, +]; + +export const createMultipleErrorsAndWarningsResult = (): IFinesConSearchResultDefendantAccount => ({ + ...structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_FORMATTING_MOCK[0]), + defendant_account_id: 15, + account_number: 'ACC005', + prosecutor_case_reference: 'REF-5', + defendant_firstnames: 'Erin', + defendant_surname: 'Example', + checks: { + errors: [ + { reference: 'CON.ER.1', message: 'Account status is CS' }, + { reference: 'CON.ER.2', message: 'Account is blocked for consolidation' }, + ], + warnings: [ + { reference: 'CON.WN.1', message: 'Account has uncleared cheque payments' }, + { reference: 'CON.WN.2', message: 'Account has linked cases' }, + ], + }, +}); + +export const createMultipleWarningsResult = (): IFinesConSearchResultDefendantAccount => ({ + ...structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_FORMATTING_MOCK[0]), + defendant_account_id: 16, + account_number: 'ACC006', + prosecutor_case_reference: 'REF-6', + defendant_firstnames: 'Wendy', + defendant_surname: 'Warning', + checks: { + errors: [], + warnings: [ + { reference: 'CON.WN.1', message: 'Account has uncleared cheque payments' }, + { reference: 'CON.WN.2', message: 'Account has linked cases' }, + ], + }, +}); + +export const createCompanyMultipleErrorsAndWarningsResult = (): IFinesConSearchResultDefendantAccount => ({ + ...structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_COMPANY_FORMATTING_MOCK[0]), + defendant_account_id: 25, + account_number: 'COMP005', + prosecutor_case_reference: 'COMP-REF-5', + organisation_name: 'Errors & Warnings Ltd', + checks: { + errors: [ + { reference: 'CON.ER.1', message: 'Account status is CS' }, + { reference: 'CON.ER.2', message: 'Account is blocked for consolidation' }, + ], + warnings: [ + { reference: 'CON.WN.1', message: 'Account has uncleared cheque payments' }, + { reference: 'CON.WN.2', message: 'Account has linked cases' }, + ], + }, +}); + +export const createCompanyMultipleWarningsResult = (): IFinesConSearchResultDefendantAccount => ({ + ...structuredClone(FINES_CON_SEARCH_RESULT_DEFENDANT_ACCOUNTS_COMPANY_FORMATTING_MOCK[0]), + defendant_account_id: 26, + account_number: 'COMP006', + prosecutor_case_reference: 'COMP-REF-6', + organisation_name: 'Warnings Only Ltd', + checks: { + errors: [], + warnings: [ + { reference: 'CON.WN.1', message: 'Account has uncleared cheque payments' }, + { reference: 'CON.WN.2', message: 'Account has linked cases' }, + ], + }, +}); diff --git a/cypress/component/fines/consolidation/setup/SetupComponent.ts b/cypress/component/fines/consolidation/setup/SetupComponent.ts index 242a3d23c7..79f3ebe5ad 100644 --- a/cypress/component/fines/consolidation/setup/SetupComponent.ts +++ b/cypress/component/fines/consolidation/setup/SetupComponent.ts @@ -13,6 +13,7 @@ import { IComponentProperties } from './setupComponent.interface'; export const setupConsolidationComponent = (componentProperties: IComponentProperties = {}) => { const fragment = componentProperties.fragments ?? 'search'; const defendantType = componentProperties.defendantType ?? 'individual'; + const initialResults = structuredClone(componentProperties.initialResults ?? []); const finesConSelectBuFormData = defendantType === 'company' @@ -32,6 +33,11 @@ export const setupConsolidationComponent = (componentProperties: IComponentPrope const store = new FinesConStore(); store.updateSelectBuForm(finesConSelectBuFormData); store.updateSearchAccountFormTemporary(searchAccountFormData); + if (defendantType === 'company') { + store.updateDefendantResults([], initialResults); + } else { + store.updateDefendantResults(initialResults, []); + } store.setActiveTab(fragment); if (componentProperties.updateSearchSpy) { diff --git a/cypress/component/fines/consolidation/setup/setupComponent.interface.ts b/cypress/component/fines/consolidation/setup/setupComponent.interface.ts index 0a9fdc87ea..f8a4975ed2 100644 --- a/cypress/component/fines/consolidation/setup/setupComponent.interface.ts +++ b/cypress/component/fines/consolidation/setup/setupComponent.interface.ts @@ -1,4 +1,5 @@ import { IFinesConSearchAccountState } from 'src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-account/interfaces/fines-con-search-account-state.interface'; +import { IFinesConSearchResultDefendantAccount } from 'src/app/flows/fines/fines-con/consolidate-acc/fines-con-search-result/interfaces/fines-con-search-result-defendant-account.interface'; export type ConsolidationTabFragment = 'search' | 'results' | 'for-consolidation'; export type DefendantType = 'individual' | 'company'; @@ -7,6 +8,7 @@ export interface IComponentProperties { defendantType?: DefendantType; fragments?: ConsolidationTabFragment; searchAccountFormData?: IFinesConSearchAccountState; + initialResults?: IFinesConSearchResultDefendantAccount[]; setupRouterSpy?: boolean; updateSearchSpy?: (formData: IFinesConSearchAccountState) => void; } diff --git a/cypress/e2e/functional/opal/actions/consolidation/consolidation.actions.ts b/cypress/e2e/functional/opal/actions/consolidation/consolidation.actions.ts index d916a56863..d8db948e2b 100644 --- a/cypress/e2e/functional/opal/actions/consolidation/consolidation.actions.ts +++ b/cypress/e2e/functional/opal/actions/consolidation/consolidation.actions.ts @@ -5,12 +5,21 @@ import { SelectBusinessUnitLocators } from '../../../../../shared/selectors/consolidation/SelectBusinessUnit.locators'; import { AccountSearchLocators } from '../../../../../shared/selectors/consolidation/AccountSearch.locators'; +import { AccountResultsLocators } from '../../../../../shared/selectors/consolidation/AccountResults.locators'; +import { ErrorPageLocators } from '../../../../../shared/selectors/consolidation/ErrorPage.locators'; import { createScopedLogger } from '../../../../../support/utils/log.helper'; +import { applyUniqPlaceholder } from '../../../../../support/utils/stringUtils'; const log = createScopedLogger('ConsolidationActions'); +const SINGLE_BUSINESS_UNIT_MESSAGE_PREFIX = 'The consolidation will be processed in'; export type ConsolidationDefendantType = 'Individual' | 'Company'; type SearchDetails = Record; +type CreatedAccountAlias = { + accountId?: number | string | null; + accountNumber?: string | null; +}; +const SELECTED_BUSINESS_UNIT_ALIAS = 'selectedConsolidationBusinessUnit'; /** Actions and assertions for the Consolidation flow screens. */ export class ConsolidationActions { @@ -54,15 +63,98 @@ export class ConsolidationActions { throw new Error(`Unsupported checkbox value "${value}". Use true/false (or yes/no).`); } + /** + * Resolves the last created account id/number stored on the shared @etagUpdate alias. + * This alias is set by the draft account creation helpers. + * @returns Chainable yielding the created account id and account number. + */ + private getCreatedAccountAlias(): Cypress.Chainable<{ accountId: number; accountNumber: string }> { + return cy.get('@etagUpdate').then((etagUpdate) => { + const accountId = Number(etagUpdate?.accountId); + const accountNumber = String(etagUpdate?.accountNumber ?? '').trim(); + + if (!Number.isFinite(accountId) || accountId <= 0) { + throw new Error('Expected @etagUpdate to contain a valid accountId for consolidation result assertions.'); + } + + if (!accountNumber) { + throw new Error('Expected @etagUpdate to contain a valid accountNumber for consolidation result assertions.'); + } + + return { accountId, accountNumber }; + }); + } + + /** + * Stores the selected consolidation business unit so later assertions can verify the actual chosen value. + * @param businessUnitName - Business unit label captured from the UI. + */ + private setSelectedBusinessUnitAlias(businessUnitName: string): void { + cy.wrap(String(businessUnitName).trim(), { log: false }).as(SELECTED_BUSINESS_UNIT_ALIAS); + } + + /** + * Resolves the selected consolidation business unit captured during the select BU step. + * @returns Chainable yielding the trimmed business unit name. + */ + private getSelectedBusinessUnitAlias(): Cypress.Chainable { + return cy + .get(`@${SELECTED_BUSINESS_UNIT_ALIAS}`) + .then((businessUnitName) => String(businessUnitName).trim()); + } + + /** + * Waits until the select business unit screen has rendered either the autocomplete + * input or the single-business-unit informational message. + * @returns Chainable yielding the rendered business unit selection mode. + */ + private waitForBusinessUnitSelectionMode(): Cypress.Chainable<'single' | 'multiple'> { + return cy + .get('body', { timeout: 10_000 }) + .should(($body) => { + const hasBusinessUnitInput = $body.find(SelectBusinessUnitLocators.businessUnitInput).length > 0; + const hasSingleBusinessUnitMessage = $body + .find(SelectBusinessUnitLocators.singleBusinessUnitMessage) + .toArray() + .some((element) => Cypress.$(element).text().includes(SINGLE_BUSINESS_UNIT_MESSAGE_PREFIX)); + + expect( + hasBusinessUnitInput || hasSingleBusinessUnitMessage, + 'business unit autocomplete or single business unit message', + ).to.be.true; + }) + .then(($body) => { + const hasBusinessUnitInput = $body.find(SelectBusinessUnitLocators.businessUnitInput).length > 0; + return hasBusinessUnitInput ? 'multiple' : 'single'; + }); + } + /** * Selects a business unit when the selector is present. * If a single business unit is auto-selected, verifies the informational message instead. */ public selectBusinessUnitIfRequired(): void { - cy.get('body').then(($body) => { - if ($body.find(SelectBusinessUnitLocators.businessUnitInput).length === 0) { + // Wait for the select business unit form and its business unit branch to finish rendering + // before deciding whether we are in the single-BU or autocomplete path. + cy.get(SelectBusinessUnitLocators.heading, { timeout: 10_000 }).should('contain.text', 'Consolidate accounts'); + cy.get(SelectBusinessUnitLocators.defendantTypeHeading, { timeout: 10_000 }).should( + 'contain.text', + 'Defendant type', + ); + cy.get(SelectBusinessUnitLocators.continueButton, { timeout: 10_000 }).should('be.visible'); + + this.waitForBusinessUnitSelectionMode().then((mode) => { + if (mode === 'single') { log('info', 'Business unit input not shown; using auto-selected single business unit'); - cy.get(SelectBusinessUnitLocators.singleBusinessUnitMessage, { timeout: 10_000 }).should('be.visible'); + cy.contains(SelectBusinessUnitLocators.singleBusinessUnitMessage, SINGLE_BUSINESS_UNIT_MESSAGE_PREFIX, { + timeout: 10_000, + }) + .should('be.visible') + .invoke('text') + .then((text) => { + const businessUnitName = text.replace(SINGLE_BUSINESS_UNIT_MESSAGE_PREFIX, '').trim(); + this.setSelectedBusinessUnitAlias(businessUnitName); + }); return; } @@ -72,7 +164,11 @@ export class ConsolidationActions { .should('be.visible') .find('li') .first() - .click(); + .then(($item) => { + const businessUnitName = $item.text().trim(); + this.setSelectedBusinessUnitAlias(businessUnitName); + cy.wrap($item).click(); + }); }); } @@ -84,17 +180,53 @@ export class ConsolidationActions { log('select', `Selecting defendant type: ${defendantType}`); if (defendantType === 'Individual') { - cy.get(SelectBusinessUnitLocators.individualInput).check({ force: true }); + cy.get(SelectBusinessUnitLocators.individualInput, { timeout: 10_000 }) + .should('exist') + .and('not.be.disabled') + .check({ force: true }); return; } - cy.get(SelectBusinessUnitLocators.companyInput).check({ force: true }); + cy.get(SelectBusinessUnitLocators.companyInput, { timeout: 10_000 }) + .should('exist') + .and('not.be.disabled') + .check({ force: true }); } /** Clicks Continue on the Select Business Unit screen. */ public continueFromSelectBusinessUnit(): void { log('click', 'Clicking Continue on consolidation select business unit page'); - cy.get(SelectBusinessUnitLocators.continueButton, { timeout: 10_000 }).should('be.visible').click(); + cy.get(SelectBusinessUnitLocators.continueButton, { timeout: 10_000 }) + .should('be.visible') + .and('not.be.disabled') + .click(); + } + + /** + * Waits for the consolidation account search screen to finish rendering after continuing + * from the select business unit page. + * @param defendantType - Expected defendant type shown on the search summary. + */ + public waitForAccountSearchScreen(defendantType: ConsolidationDefendantType): void { + cy.location('pathname', { timeout: 10_000 }).should('include', '/fines/consolidation/consolidate-accounts'); + cy.get(AccountSearchLocators.heading, { timeout: 10_000 }).should('contain.text', 'Consolidate accounts'); + cy.get(AccountSearchLocators.summaryList, { timeout: 10_000 }).should('be.visible'); + cy.get(AccountSearchLocators.searchTabLink, { timeout: 10_000 }).should('have.attr', 'aria-current', 'page'); + cy.get(AccountSearchLocators.defendantTypeValue, { timeout: 10_000 }).should('contain', defendantType); + cy.get(AccountSearchLocators.accountNumberInput, { timeout: 10_000 }).should('be.visible'); + + if (defendantType === 'Company') { + cy.get(AccountSearchLocators.companyNameInput, { timeout: 10_000 }).should('be.visible'); + } + } + + /** Asserts the user is on the consolidation business unit and defendant type selection screen. */ + public assertOnSelectBusinessUnitScreen(): void { + log('assert', 'Verifying user is on consolidation select business unit screen'); + + cy.location('pathname', { timeout: 10_000 }).should('include', '/fines/consolidation/select-business-unit'); + cy.get(SelectBusinessUnitLocators.heading, { timeout: 10_000 }).should('contain.text', 'Consolidate accounts'); + cy.get(SelectBusinessUnitLocators.defendantTypeHeading).should('contain.text', 'Defendant type'); } /** Clicks Search on the consolidation Search tab. */ @@ -103,6 +235,14 @@ export class ConsolidationActions { cy.get(AccountSearchLocators.searchButton, { timeout: 10_000 }).should('be.visible').click(); } + /** Clicks the Clear search link on the consolidation Search tab. */ + public clearSearch(): void { + log('click', 'Clearing consolidation account search form'); + cy.contains(AccountSearchLocators.clearSearchLink, 'Clear search', { timeout: 10_000 }) + .should('be.visible') + .click(); + } + /** Asserts the user is on Consolidation account search for Individuals, Search tab active. */ public assertOnSearchTabForIndividuals(): void { log('assert', 'Verifying user is on consolidation Search tab for Individuals'); @@ -124,6 +264,178 @@ export class ConsolidationActions { cy.get(AccountSearchLocators.companyNameInput).should('be.visible'); } + /** Asserts the page-header back link is displayed on the consolidation shell. */ + public assertBackLinkIsDisplayed(): void { + log('assert', 'Verifying consolidation page-header back link is displayed'); + cy.get(AccountSearchLocators.backLink, { timeout: 10_000 }).should('be.visible').and('contain.text', 'Back'); + } + + /** Clicks the page-header back link on the consolidation shell. */ + public clickBackLink(): void { + log('click', 'Clicking consolidation page-header back link'); + cy.get(AccountSearchLocators.backLink, { timeout: 10_000 }).should('be.visible').click({ force: true }); + } + + /** Clicks the Results tab from the consolidation flow. */ + public openResultsTab(): void { + log('navigate', 'Opening consolidation Results tab'); + cy.get(AccountSearchLocators.resultsTab, { timeout: 10_000 }).should('be.visible').click(); + } + + /** Asserts the user is on the consolidation Results tab. */ + public assertOnResultsTab(): void { + log('assert', 'Verifying user is on consolidation Results tab'); + + cy.location('pathname', { timeout: 10_000 }).should('include', '/fines/consolidation/consolidate-accounts'); + cy.get(AccountSearchLocators.resultsTab, { timeout: 10_000 }).should('have.attr', 'aria-current', 'page'); + cy.get(AccountSearchLocators.searchButton).should('not.exist'); + cy.get(AccountResultsLocators.resultsTable, { timeout: 10_000 }).should('be.visible'); + } + + /** + * Asserts the user is on the consolidation Results tab with the expected summary values. + * Covers the active tab plus the displayed business unit and defendant type rows. + * @param defendantType - Expected defendant type shown in the summary. + */ + public assertOnResultsTabForDefendantType(defendantType: ConsolidationDefendantType): void { + this.assertOnResultsTab(); + this.getSelectedBusinessUnitAlias().then((businessUnitName) => { + cy.get(AccountSearchLocators.businessUnitKey).should('contain', 'Business unit'); + cy.get(AccountSearchLocators.businessUnitValue).should('contain', businessUnitName); + cy.get(AccountSearchLocators.defendantTypeKey).should('contain', 'Defendant type'); + cy.get(AccountSearchLocators.defendantTypeValue).should('contain', defendantType); + }); + } + + /** Asserts the no matching results state is shown with the Check your search hyperlink. */ + public assertNoMatchingResultsState(): void { + log('assert', 'Verifying consolidation no matching results state'); + + cy.get(AccountResultsLocators.resultsTable).should('not.exist'); + cy.get(AccountResultsLocators.invalidResultsHeading, { timeout: 10_000 }).should( + 'contain', + 'There are no matching results.', + ); + cy.get(AccountResultsLocators.invalidResultsBody) + .invoke('text') + .then((text) => { + const normalisedText = text.replace(/\s+/g, ' ').trim(); + expect(normalisedText).to.equal('Check your search and try again.'); + }); + cy.contains(AccountResultsLocators.invalidResultsLink, 'Check your search', { timeout: 10_000 }).should( + 'be.visible', + ); + } + + /** + * Asserts no consolidation result row displays the supplied balance. + * @param balance - Forbidden rendered balance value, e.g. "£0.00" + */ + public assertResultsExcludeBalance(balance: string): void { + const forbiddenBalance = balance.trim(); + + log('assert', 'Verifying consolidation results exclude balance', { forbiddenBalance }); + + cy.get(AccountResultsLocators.resultsRows, { timeout: 10_000 }).its('length').should('be.gte', 1); + cy.get(AccountResultsLocators.resultBalanceCell, { timeout: 10_000 }).each(($cell) => { + const renderedBalance = $cell.text().replace(/\s+/g, ' ').trim(); + expect(renderedBalance).to.not.equal(forbiddenBalance); + }); + } + + /** Clicks the Check your search hyperlink from the no matching results state. */ + public clickCheckYourSearchFromNoMatchingResults(): void { + log('click', 'Clicking Check your search from consolidation no matching results state'); + cy.contains(AccountResultsLocators.invalidResultsLink, 'Check your search', { timeout: 10_000 }) + .should('be.visible') + .click(); + } + + /** Asserts the newly created account number is displayed as a hyperlink in the results table. */ + public assertCreatedAccountLinkIsDisplayed(): void { + this.getCreatedAccountAlias().then(({ accountNumber }) => { + log('assert', 'Verifying created consolidation result account is displayed as a hyperlink', { accountNumber }); + + cy.get(AccountResultsLocators.resultAccountLinkByNumber(accountNumber), { timeout: 10_000 }) + .should('be.visible') + .and('have.class', 'govuk-link') + .then(($link) => { + expect($link.prop('tagName')).to.equal('A'); + }); + }); + } + + /** Opens the created consolidation result account and asserts it is opened in a new tab to the FAE details route. */ + public openCreatedAccountFromResultsInNewTab(): void { + cy.intercept('GET', '**/defendant-accounts/**/header-summary').as('consolidationHeaderSummary'); + + this.getCreatedAccountAlias().then(({ accountNumber }) => { + log('open', 'Opening created consolidation result account from results', { accountNumber }); + + cy.window().then((win) => { + cy.stub(win, 'open') + .callsFake((url?: string | URL, target?: string) => { + expect(target).to.equal('_blank'); + win.location.href = String(url); + }) + .as('consolidationWindowOpen'); + }); + + cy.get(AccountResultsLocators.resultAccountLinkByNumber(accountNumber), { timeout: 10_000 }) + .should('be.visible') + .click(); + + cy.get('@consolidationWindowOpen').then((windowOpenStub) => { + const stub = windowOpenStub as any; + expect(stub.calledOnce).to.equal(true); + + const [openedUrl, target] = stub.getCall(0).args as [string, string]; + expect(target).to.equal('_blank'); + expect(String(openedUrl)).to.match(/\/fines\/account\/defendant\/\d+\/details$/); + }); + + cy.wait('@consolidationHeaderSummary', { timeout: 15_000 }).its('response.statusCode').should('eq', 200); + }); + } + + /** + * Asserts the consolidation search error page content for the given defendant type. + * @param defendantType - "Individual" or "Company" + */ + public assertSearchErrorPage(defendantType: ConsolidationDefendantType): void { + const expectedBulletItems = + defendantType === 'Individual' + ? ['account number, or', 'national insurance number, or', 'advanced search'] + : ['account number, or', 'advanced search']; + + log('assert', 'Verifying consolidation search error page', { defendantType, expectedBulletItems }); + + cy.get(ErrorPageLocators.root, { timeout: 10_000 }).should('be.visible'); + cy.get(`${ErrorPageLocators.root} ${ErrorPageLocators.heading}`).should('have.text', 'There is a problem'); + cy.get(`${ErrorPageLocators.root} ${ErrorPageLocators.message}`) + .should('be.visible') + .invoke('text') + .then((text) => { + const normalisedText = text.replace(/\s+/g, ' ').trim(); + expect(normalisedText).to.equal( + 'Reference data and account information cannot be entered together when searching for an account. Search using either:', + ); + }); + + cy.get(`${ErrorPageLocators.root} ${ErrorPageLocators.bulletItems}`).then(($items) => { + const items = [...$items].map((item) => item.textContent?.replace(/\s+/g, ' ').trim().toLowerCase()); + expect(items).to.deep.equal(expectedBulletItems); + }); + } + + /** Clicks the Go back link on the consolidation search error page. */ + public goBackFromSearchError(): void { + log('click', 'Going back from consolidation search error page'); + cy.contains(`${ErrorPageLocators.root} ${ErrorPageLocators.backLink}`, 'Go back', { timeout: 10_000 }) + .should('be.visible') + .click(); + } + /** * Populates fields on the consolidation Search tab from key/value details. * @param details - Search details keyed by user-facing field labels. @@ -136,16 +448,17 @@ export class ConsolidationActions { Object.entries(details).forEach(([rawKey, value]) => { const key = rawKey.trim().toLowerCase(); + const resolvedValue = applyUniqPlaceholder(value); if (activeTextMap[key]) { const selector = activeTextMap[key]; - cy.get(selector).clear().type(value); + cy.get(selector).clear().type(resolvedValue); return; } if (activeCheckboxMap[key]) { const selector = activeCheckboxMap[key]; - const shouldCheck = this.parseCheckboxValue(value); + const shouldCheck = this.parseCheckboxValue(resolvedValue); if (shouldCheck) { cy.get(selector).check({ force: true }); } else { @@ -200,16 +513,17 @@ export class ConsolidationActions { Object.entries(details).forEach(([rawKey, value]) => { const key = rawKey.trim().toLowerCase(); + const resolvedValue = applyUniqPlaceholder(value); if (activeTextMap[key]) { const selector = activeTextMap[key]; - cy.get(selector).should('have.value', value); + cy.get(selector).should('have.value', resolvedValue); return; } if (activeCheckboxMap[key]) { const selector = activeCheckboxMap[key]; - const shouldCheck = this.parseCheckboxValue(value); + const shouldCheck = this.parseCheckboxValue(resolvedValue); if (shouldCheck) { cy.get(selector).should('be.checked'); } else { diff --git a/cypress/e2e/functional/opal/features/consolidation/FineAccountConsolidation.feature b/cypress/e2e/functional/opal/features/consolidation/FineAccountConsolidation.feature index 136266539f..c6b09ff2ef 100644 --- a/cypress/e2e/functional/opal/features/consolidation/FineAccountConsolidation.feature +++ b/cypress/e2e/functional/opal/features/consolidation/FineAccountConsolidation.feature @@ -39,7 +39,11 @@ Feature: Fines Account Consolidation | first names exact match | true | | include aliases | true | Then I click Search on consolidation account search - Then the consolidation search details are retained: + Then I see the consolidation search error page for "Individual" + # AC3b All data previously entered on the Search page is retained when a user clicks back from the consolidation search error page + When I go back from the consolidation search error page + Then I am on the consolidation Search tab for Individuals + And the consolidation search details are retained: | account number | 12345678 | | national insurance number | AB123456C | | last name | Smith | @@ -50,6 +54,94 @@ Feature: Fines Account Consolidation | last name exact match | true | | first names exact match | true | | include aliases | true | + When I clear the consolidation search + And I enter the following consolidation search details: + | last name | NoMatch | + | last name exact match | true | + And I click Search on consolidation account search + # AC2d - If no matching results are returned, Check your search is a hyperlink that returns the user to Search with all previously entered data retained + Then I see the consolidation no matching results state + When I click Check your search on consolidation no matching results + Then I am on the consolidation Search tab for Individuals + And the consolidation search details are retained: + | last name | NoMatch | + | last name exact match | true | + + And I click Search on consolidation account search + Then I see the consolidation no matching results state + # AC4 - A Back button will be displayed in the page header + Then the consolidation page header back link is displayed + # AC4 - Selecting Back will return the user to the BU and defendant type selection screen + When I click the consolidation page header back link + Then I am on the consolidation business unit and defendant type selection screen + + @JIRA-STORY:PO-2413 @JIRA-KEY:POT-3328 + Scenario: Consolidation Successful account search for Individuals + Given I create a "adultOrYouthOnly" draft account with the following details and set status "Publishing Pending" using user "opal-test-10@dev.platform.hmcts.net": + | Account_status | Submitted | + | account.defendant.forenames | Consolidation | + | account.defendant.surname | ResultLink{uniq} | + | account.defendant.email_address_1 | consolidation.result{uniq}@test.com | + | account.defendant.telephone_number_home | 02078259314 | + | account.account_type | Fine | + | account.prosecutor_case_reference | CONS-RESULT-IND-{uniq} | + | account.collection_order_made | false | + | account.collection_order_made_today | false | + | account.payment_card_request | false | + | account.defendant.dob | 2002-05-15 | + When I open Consolidate accounts + When I continue to the consolidation account search as an "Individual" defendant + Then I am on the consolidation Search tab for Individuals + And I enter the following consolidation search details: + | last name | ResultLink{uniq} | + | last name exact match | true | + And I click Search on consolidation account search + # AC1 - A user is navigated to the Results tab for Individuals when a valid search has been performed from the Search tab within the Consolidate accounts journey + # AC1a - The Business unit row displays the Business Unit used in the search + # AC1b - The Defendant type row displays Individual + Then I am on the consolidation Results tab for Individuals + # AC4 - The Account column displays the account number as a hyperlink. Selecting it will open the relevant FAE – At a glance page in a new tab + And the created consolidation result account number is displayed as a hyperlink + When I open the created consolidation result account in a new tab + Then I should see the account header contains "Mr Consolidation RESULTLINK{uniqUpper}" + + @JIRA-STORY:PO-2413 @JIRA-KEY:POT-3328 + Scenario: Consolidation search excludes zero balance accounts for Individuals + Given I create a "adultOrYouthOnly" draft account with the following details and set status "Publishing Pending" using user "opal-test-10@dev.platform.hmcts.net": + | Account_status | Submitted | + | account.defendant.forenames | Zero | + | account.defendant.surname | ConsolidationZeroBalance{uniq} | + | account.defendant.email_address_1 | consolidation.zero.balance{uniq}@test.com | + | account.defendant.telephone_number_home | 02078259310 | + | account.account_type | Fine | + | account.prosecutor_case_reference | CONS-ZERO-BAL-IND-A-{uniq} | + | account.collection_order_made | false | + | account.collection_order_made_today | false | + | account.payment_card_request | false | + | account.defendant.dob | 2001-05-15 | + | account.offences.0.impositions.0.amount_paid | 125 | + And I create a "adultOrYouthOnly" draft account with the following details and set status "Publishing Pending" using user "opal-test-10@dev.platform.hmcts.net": + | Account_status | Submitted | + | account.defendant.forenames | Visible | + | account.defendant.surname | ConsolidationZeroBalance{uniq} | + | account.defendant.email_address_1 | consolidation.visible.balance{uniq}@test.com | + | account.defendant.telephone_number_home | 02078259311 | + | account.account_type | Fine | + | account.prosecutor_case_reference | CONS-ZERO-BAL-IND-B-{uniq} | + | account.collection_order_made | false | + | account.collection_order_made_today | false | + | account.payment_card_request | false | + | account.defendant.dob | 2002-05-15 | + When I open Consolidate accounts + And I continue to the consolidation account search as an "Individual" defendant + Then I am on the consolidation Search tab for Individuals + And I enter the following consolidation search details: + | last name | ConsolidationZero | + + And I click Search on consolidation account search + Then I am on the consolidation Results tab for Individuals + And the created consolidation result account number is displayed as a hyperlink + And the consolidation results exclude accounts with a balance of "£0.00" @JIRA-STORY:PO-2414 @JIRA-KEY:POT-3329 Scenario: Consolidation account search for Companies @@ -74,10 +166,93 @@ Feature: Fines Account Consolidation | Search exact match | true | | include aliases | true | Then I click Search on consolidation account search - Then the consolidation search details are retained: + Then I see the consolidation search error page for "Company" + # AC3b All data previously entered on the Search page is retained when a user clicks back from the consolidation search error page + When I go back from the consolidation search error page + Then I am on the consolidation Search tab for Companies + And the consolidation search details are retained: | account number | 12345678 | | company name | Company Name | | address line 1 | 1 High St | | postcode | SW1A 1AA | | Search exact match | true | | include aliases | true | + When I clear the consolidation search + And I enter the following consolidation search details: + | company name | No Match Co | + | Search exact match | true | + And I click Search on consolidation account search + # AC2d - If no matching results are returned, Check your search is a hyperlink that returns the user to Search with all previously entered data retained + Then I see the consolidation no matching results state + When I click Check your search on consolidation no matching results + Then I am on the consolidation Search tab for Companies + And the consolidation search details are retained: + | company name | No Match Co | + | Search exact match | true | + And I click Search on consolidation account search + Then I see the consolidation no matching results state + # AC4 - A Back button will be displayed in the page header + Then the consolidation page header back link is displayed + # AC4 - Selecting Back will return the user to the BU and defendant type selection screen + When I click the consolidation page header back link + Then I am on the consolidation business unit and defendant type selection screen + + + @JIRA-STORY:PO-2413 @JIRA-KEY:POT-3328 + Scenario: Consolidation Successful account search for Company + Given I create a "company" draft account with the following details and set status "Publishing Pending" using user "opal-test-10@dev.platform.hmcts.net": + | Account_status | Submitted | + | account.defendant.company_name | Consolidation Result Co {uniq} | + | account.defendant.email_address | consolidation.result.co{uniq}@test.com | + | account.defendant.post_code | AB23 4RN | + | account.prosecutor_case_reference | CONS-RESULT-COMP-{uniq} | + | account.account_type | Fine | + When I open Consolidate accounts + When I continue to the consolidation account search as an "Company" defendant + Then I am on the consolidation Search tab for Companies + And I enter the following consolidation search details: + | company name | Consolidation Result Co {uniq} | + | Search exact match | true | + And I click Search on consolidation account search + # AC1 - A user is navigated to the Results tab for Companies when a valid search has been performed from the Search tab within the Consolidate accounts journey + # AC1a - The Business unit row displays the Business Unit used in the search + # AC1b - The Defendant type row displays Company + Then I am on the consolidation Results tab for Companies + # AC4 - The Account column displays the account number as a hyperlink. Selecting it will open the relevant FAE – At a glance page in a new tab + And the created consolidation result account number is displayed as a hyperlink + When I open the created consolidation result account in a new tab + Then I should see the account header contains "Consolidation Result Co {uniqUpper}" + + @JIRA-STORY:PO-2414 @JIRA-KEY:POT-3329 + Scenario: Consolidation search excludes zero balance accounts for Company + Given I create a "company" draft account with the following details and set status "Publishing Pending" using user "opal-test-10@dev.platform.hmcts.net": + | Account_status | Submitted | + | account.defendant.company_name | Consolidation Zero Balance Co {uniq} | + | account.defendant.email_address_1 | consolidation.zero.balance.co{uniq}@test.com | + | account.defendant.post_code | AB23 4RN | + | account.prosecutor_case_reference | CONS-ZERO-BAL-COMP-A-{uniq} | + | account.account_type | Fine | + | account.collection_order_made | false | + | account.collection_order_made_today | false | + | account.payment_card_request | false | + | account.offences.0.impositions.0.amount_paid | 125 | + And I create a "company" draft account with the following details and set status "Publishing Pending" using user "opal-test-10@dev.platform.hmcts.net": + | Account_status | Submitted | + | account.defendant.company_name | Consolidation Zero Balance Co {uniq} | + | account.defendant.email_address_1 | consolidation.visible.balance.co{uniq}@test.com | + | account.defendant.post_code | AB23 4RN | + | account.prosecutor_case_reference | CONS-ZERO-BAL-COMP-B-{uniq} | + | account.account_type | Fine | + | account.collection_order_made | false | + | account.collection_order_made_today | false | + | account.payment_card_request | false | + When I open Consolidate accounts + And I continue to the consolidation account search as an "Company" defendant + Then I am on the consolidation Search tab for Companies + And I enter the following consolidation search details: + | company name | Consolidation Zero Balance Co {uniq} | + | Search exact match | true | + And I click Search on consolidation account search + Then I am on the consolidation Results tab for Companies + And the created consolidation result account number is displayed as a hyperlink + And the consolidation results exclude accounts with a balance of "£0.00" diff --git a/cypress/e2e/functional/opal/features/consolidation/FinesAccountConsolidationAccessibility.feature b/cypress/e2e/functional/opal/features/consolidation/FinesAccountConsolidationAccessibility.feature index 697966cace..e4beab898d 100644 --- a/cypress/e2e/functional/opal/features/consolidation/FinesAccountConsolidationAccessibility.feature +++ b/cypress/e2e/functional/opal/features/consolidation/FinesAccountConsolidationAccessibility.feature @@ -6,18 +6,87 @@ Feature: Accessibility Tests for Fines Consolidation Given I am logged in with email "opal-test@dev.platform.hmcts.net" Then I should be on the dashboard - @JIRA-STORY:PO-2413 @JIRA-KEY:POT-3210 + @JIRA-STORY:PO-2413 @JIRA-STORY:PO-2415 @JIRA-STORY:PO-2417 @JIRA-KEY:POT-3210 Scenario: Consolidate Accessibility for Individuals + Given I create a "adultOrYouthOnly" draft account with the following details and set status "Publishing Pending" using user "opal-test-10@dev.platform.hmcts.net": + | Account_status | Submitted | + | account.defendant.forenames | Consolidation | + | account.defendant.surname | Accessibility{uniq} | + | account.defendant.email_address_1 | consolidation.accessibility{uniq}@test.com | + | account.defendant.telephone_number_home | 02078259314 | + | account.account_type | Fine | + | account.prosecutor_case_reference | CONS-A11Y-IND-{uniq} | + | account.collection_order_made | false | + | account.collection_order_made_today | false | + | account.payment_card_request | false | + | account.defendant.dob | 2002-05-15 | When I open Consolidate accounts Then I check the page for accessibility And I continue to the consolidation account search as an "Individual" defendant Then I am on the consolidation Search tab for Individuals And I check the page for accessibility + And I enter the following consolidation search details: + | account number | 12345678 | + | last name | Accessibility{uniq} | + When I click Search on consolidation account search + Then I see the consolidation search error page for "Individual" + And I check the page for accessibility + When I go back from the consolidation search error page + Then I am on the consolidation Search tab for Individuals + When I clear the consolidation search + And I enter the following consolidation search details: + | last name | Nobody | + | last name exact match | true | + When I click Search on consolidation account search + Then I see the consolidation no matching results state + Then I check the page for accessibility + When I click Check your search on consolidation no matching results + Then I am on the consolidation Search tab for Individuals + And I enter the following consolidation search details: + | last name | Accessibility{uniq} | + | last name exact match | true | + When I click Search on consolidation account search + Then I am on the consolidation Results tab + And I check the page for accessibility - @JIRA-STORY:PO-2414 @JIRA-KEY:POT-3211 + @JIRA-STORY:PO-2414 @JIRA-STORY:PO-2421 @JIRA-STORY:PO-2417 @JIRA-KEY:POT-3211 Scenario: Consolidate Accessibility for Companies + Given I create a "company" draft account with the following details and set status "Publishing Pending" using user "opal-test-10@dev.platform.hmcts.net": + | Account_status | Submitted | + | account.defendant.company_name | Consolidation Access Comp {uniq} | + | account.defendant.email_address_1 | consolidation.access.comp{uniq}@test.com | + | account.defendant.post_code | AB23 4RN | + | account.account_type | Fine | + | account.prosecutor_case_reference | CONS-A11Y-COMP-{uniq} | + | account.collection_order_made | false | + | account.collection_order_made_today | false | + | account.payment_card_request | false | When I open Consolidate accounts Then I check the page for accessibility And I continue to the consolidation account search as an "Company" defendant Then I am on the consolidation Search tab for Companies And I check the page for accessibility + And I enter the following consolidation search details: + | account number | 12345678 | + | company name | Consolidation Access Comp {uniq} | + When I click Search on consolidation account search + Then I see the consolidation search error page for "Company" + And I check the page for accessibility + When I go back from the consolidation search error page + Then I am on the consolidation Search tab for Companies + When I clear the consolidation search + And I enter the following consolidation search details: + | company name | Nobody | + | Search exact match | true | + When I click Search on consolidation account search + Then I see the consolidation no matching results state + Then I check the page for accessibility + When I click Check your search on consolidation no matching results + Then I am on the consolidation Search tab for Companies + When I clear the consolidation search + And I enter the following consolidation search details: + | company name | Consolidation Access Comp {uniq} | + | Search exact match | true | + When I click Search on consolidation account search + Then I am on the consolidation Results tab + And I check the page for accessibility diff --git a/cypress/e2e/functional/opal/flows/consolidation.flow.ts b/cypress/e2e/functional/opal/flows/consolidation.flow.ts index 4ab89ee09f..6411bec796 100644 --- a/cypress/e2e/functional/opal/flows/consolidation.flow.ts +++ b/cypress/e2e/functional/opal/flows/consolidation.flow.ts @@ -31,6 +31,7 @@ export class ConsolidationFlow { this.consolidation.selectBusinessUnitIfRequired(); this.consolidation.selectDefendantType(defendantType); this.consolidation.continueFromSelectBusinessUnit(); + this.consolidation.waitForAccountSearchScreen(defendantType); } /** @@ -41,18 +42,114 @@ export class ConsolidationFlow { this.consolidation.clickSearch(); } + /** Clears the consolidation account-search form. */ + public clearConsolidationSearch(): void { + log('flow', 'Clearing consolidation account-search form'); + this.consolidation.clearSearch(); + } + /** Asserts consolidation account search lands on Search tab for Individuals. */ public assertSearchTabForIndividuals(): void { log('flow', 'Asserting consolidation account search is on Search tab for Individuals'); this.consolidation.assertOnSearchTabForIndividuals(); } + /** Asserts the user is on the consolidation business unit and defendant type selection screen. */ + public assertSelectBusinessUnitScreen(): void { + log('flow', 'Asserting consolidation select business unit screen is displayed'); + this.consolidation.assertOnSelectBusinessUnitScreen(); + } + /** Asserts consolidation account search lands on Search tab for Companies. */ public assertSearchTabForCompanies(): void { log('flow', 'Asserting consolidation account search is on Search tab for Companies'); this.consolidation.assertOnSearchTabForCompanies(); } + /** Asserts the page-header back link is displayed on the consolidation shell. */ + public assertBackLinkIsDisplayed(): void { + log('flow', 'Asserting consolidation page-header back link is displayed'); + this.consolidation.assertBackLinkIsDisplayed(); + } + + /** Clicks the page-header back link on the consolidation shell. */ + public clickBackLink(): void { + log('flow', 'Clicking consolidation page-header back link'); + this.consolidation.clickBackLink(); + } + + /** Opens the consolidation Results tab. */ + public openResultsTab(): void { + log('flow', 'Opening consolidation Results tab'); + this.consolidation.openResultsTab(); + } + + /** Asserts consolidation account search lands on the Results tab. */ + public assertResultsTab(): void { + log('flow', 'Asserting consolidation account search is on the Results tab'); + this.consolidation.assertOnResultsTab(); + } + + /** Asserts consolidation account search lands on the Results tab for Individuals with the correct summary values. */ + public assertResultsTabForIndividuals(): void { + log('flow', 'Asserting consolidation account search is on the Results tab for Individuals'); + this.consolidation.assertOnResultsTabForDefendantType('Individual'); + } + + /** Asserts consolidation account search lands on the Results tab for Companies with the correct summary values. */ + public assertResultsTabForCompanies(): void { + log('flow', 'Asserting consolidation account search is on the Results tab for Companies'); + this.consolidation.assertOnResultsTabForDefendantType('Company'); + } + + /** Asserts the created account number is rendered as a hyperlink in consolidation results. */ + public assertCreatedAccountLinkIsDisplayed(): void { + log('flow', 'Asserting created consolidation result account is displayed as a hyperlink'); + this.consolidation.assertCreatedAccountLinkIsDisplayed(); + } + + /** Asserts the consolidation no matching results state is displayed. */ + public assertNoMatchingResultsState(): void { + log('flow', 'Asserting consolidation no matching results state'); + this.consolidation.assertNoMatchingResultsState(); + } + + /** + * Asserts the consolidation results do not contain the supplied balance. + * @param balance - Forbidden rendered balance value. + */ + public assertResultsExcludeBalance(balance: string): void { + log('flow', 'Asserting consolidation results exclude balance', { balance }); + this.consolidation.assertResultsExcludeBalance(balance); + } + + /** Clicks the Check your search hyperlink from the consolidation no matching results state. */ + public clickCheckYourSearchFromNoMatchingResults(): void { + log('flow', 'Clicking Check your search from consolidation no matching results state'); + this.consolidation.clickCheckYourSearchFromNoMatchingResults(); + } + + /** Opens the created consolidation result account and verifies the new-tab FAE details navigation. */ + public openCreatedAccountFromResultsInNewTab(): void { + log('flow', 'Opening created consolidation result account in a new tab'); + this.consolidation.openCreatedAccountFromResultsInNewTab(); + } + + /** + * Asserts the consolidation search error page for the given defendant type. + * @param defendantType - "Individual" or "Company" + */ + public assertSearchErrorPage(defendantType: ConsolidationDefendantType): void { + log('flow', 'Asserting consolidation search error page', { defendantType }); + this.consolidation.assertSearchErrorPage(defendantType); + } + + /** Clicks the back link on the consolidation search error page. */ + public goBackFromSearchError(): void { + log('flow', 'Going back from consolidation search error page'); + this.consolidation.goBackFromSearchError(); + } + /** * Enters consolidation account-search details from a two-column data table. * @param table - Data table in key/value form. diff --git a/cypress/shared/selectors/consolidation/AccountResults.locators.ts b/cypress/shared/selectors/consolidation/AccountResults.locators.ts new file mode 100644 index 0000000000..7bef78a735 --- /dev/null +++ b/cypress/shared/selectors/consolidation/AccountResults.locators.ts @@ -0,0 +1,43 @@ +export const AccountResultsLocators = { + // Results headings and actions + resultsHeading: 'h2.govuk-heading-m', + addToListButton: 'button.govuk-button[type="button"]', + selectedAccountsHint: 'p.govuk-hint', + invalidResultsHeading: 'h2.govuk-heading-m', + invalidResultsBody: 'p.govuk-body-m', + invalidResultsLink: 'p.govuk-body-m a.govuk-link', + + // Results table + resultsTable: 'table.govuk-table', + resultsTableHeaders: 'table.govuk-table thead th', + resultsTableNamedHeaders: 'table.govuk-table thead th[opal-lib-moj-sortable-table-header]', + resultsScrollPane: 'opal-lib-custom-horizontal-scroll-pane', + resultsPagination: 'opal-lib-moj-pagination, .govuk-pagination, nav.govuk-pagination', + resultsTableBody: 'table.govuk-table tbody', + resultsRows: 'table.govuk-table tbody > tr.govuk-table__row', + resultSelectionCheckboxes: 'table.govuk-table input[type="checkbox"]', + resultSelectAllCheckbox: '#defendants-select-all-checkbox', + resultAccountLink: 'td#defendantAccountNumber a.govuk-link', + resultNameCell: 'td#defendantName', + resultAliasesCell: 'td#defendantAliases', + resultDateOfBirthCell: 'td#defendantDateOfBirth', + resultAddressLine1Cell: 'td#defendantAddressLine1', + resultPostcodeCell: 'td#defendantPostcode', + resultCollectionOrderCell: 'td#defendantCollectionOrder', + resultEnforcementCell: 'td#defendantEnforcement', + resultBalanceCell: 'td#defendantBalance', + resultPayingParentGuardianCell: 'td#defendantPayingParentGuardian', + resultNationalInsuranceNumberCell: 'td#defendantNationalInsuranceNumber', + resultRefCell: 'td#defendantRef', + resultChecksBulletItems: 'ul.defendant-check-message-list > li', + resultTableRow: 'tr.govuk-table__row', + addToListErrorMessage: '#defendants-select-all-error-message', + + // Results row helpers + resultRowWithAccount: (accountNumber: string) => + `tr.govuk-table__row:has(td#defendantAccountNumber a:contains("${accountNumber}"))`, + resultAccountLinkByNumber: (accountNumber: string) => + `td#defendantAccountNumber a.govuk-link:contains("${accountNumber}")`, + resultChecksCellByAccountId: (accountId: number | string) => `#defendant-checks-${accountId}`, + resultRowCheckboxByAccountId: (accountId: number | string) => `#defendant-select-${accountId}`, +}; diff --git a/cypress/shared/selectors/consolidation/AccountSearch.locators.ts b/cypress/shared/selectors/consolidation/AccountSearch.locators.ts index 344ee2a9dd..05e5d2ea81 100644 --- a/cypress/shared/selectors/consolidation/AccountSearch.locators.ts +++ b/cypress/shared/selectors/consolidation/AccountSearch.locators.ts @@ -1,5 +1,6 @@ export const AccountSearchLocators = { heading: 'h1.govuk-heading-l', + backLink: 'a.govuk-back-link', // Summary rows summaryList: '#consolidateAccSummary', diff --git a/cypress/shared/selectors/consolidation/ErrorPage.locators.ts b/cypress/shared/selectors/consolidation/ErrorPage.locators.ts new file mode 100644 index 0000000000..f525e0442f --- /dev/null +++ b/cypress/shared/selectors/consolidation/ErrorPage.locators.ts @@ -0,0 +1,8 @@ +export const ErrorPageLocators = { + root: 'app-fines-con-search-error', + heading: 'h1.govuk-heading-l', + message: 'p.govuk-body', + bulletList: 'ul.govuk-list.govuk-list--bullet', + bulletItems: 'ul.govuk-list.govuk-list--bullet li', + backLink: 'a.govuk-link', +}; diff --git a/cypress/support/step_definitions/consolidation/consolidation.steps.ts b/cypress/support/step_definitions/consolidation/consolidation.steps.ts index 684548df21..4b3cca9b27 100644 --- a/cypress/support/step_definitions/consolidation/consolidation.steps.ts +++ b/cypress/support/step_definitions/consolidation/consolidation.steps.ts @@ -24,16 +24,91 @@ When('I click Search on consolidation account search', () => { consolidationFlow().clickConsolidationSearch(); }); +When('I clear the consolidation search', () => { + log('step', 'Clearing the consolidation search form'); + consolidationFlow().clearConsolidationSearch(); +}); + Then('I am on the consolidation Search tab for Individuals', () => { log('step', 'Verifying consolidation account search defaults for Individuals'); consolidationFlow().assertSearchTabForIndividuals(); }); +Then('I am on the consolidation business unit and defendant type selection screen', () => { + log('step', 'Verifying consolidation business unit and defendant type selection screen'); + consolidationFlow().assertSelectBusinessUnitScreen(); +}); + Then('I am on the consolidation Search tab for Companies', () => { log('step', 'Verifying consolidation account search defaults for Companies'); consolidationFlow().assertSearchTabForCompanies(); }); +Then('the consolidation page header back link is displayed', () => { + log('step', 'Verifying consolidation page header back link is displayed'); + consolidationFlow().assertBackLinkIsDisplayed(); +}); + +When('I click the consolidation page header back link', () => { + log('step', 'Clicking the consolidation page header back link'); + consolidationFlow().clickBackLink(); +}); + +When('I open the consolidation Results tab', () => { + log('step', 'Opening the consolidation Results tab'); + consolidationFlow().openResultsTab(); +}); + +Then('I am on the consolidation Results tab', () => { + log('step', 'Verifying consolidation Results tab is active'); + consolidationFlow().assertResultsTab(); +}); + +Then('I am on the consolidation Results tab for Individuals', () => { + log('step', 'Verifying consolidation Results tab summary for Individuals'); + consolidationFlow().assertResultsTabForIndividuals(); +}); + +Then('I am on the consolidation Results tab for Companies', () => { + log('step', 'Verifying consolidation Results tab summary for Companies'); + consolidationFlow().assertResultsTabForCompanies(); +}); + +Then('the created consolidation result account number is displayed as a hyperlink', () => { + log('step', 'Verifying created consolidation result account number is displayed as a hyperlink'); + consolidationFlow().assertCreatedAccountLinkIsDisplayed(); +}); + +Then('I see the consolidation no matching results state', () => { + log('step', 'Verifying consolidation no matching results state'); + consolidationFlow().assertNoMatchingResultsState(); +}); + +Then('the consolidation results exclude accounts with a balance of {string}', (balance: string) => { + log('step', 'Verifying consolidation results exclude accounts with balance', { balance }); + consolidationFlow().assertResultsExcludeBalance(balance); +}); + +When('I click Check your search on consolidation no matching results', () => { + log('step', 'Clicking Check your search on consolidation no matching results'); + consolidationFlow().clickCheckYourSearchFromNoMatchingResults(); +}); + +When('I open the created consolidation result account in a new tab', () => { + log('step', 'Opening created consolidation result account in a new tab'); + consolidationFlow().openCreatedAccountFromResultsInNewTab(); +}); + +Then('I see the consolidation search error page for {string}', (defendantType: ConsolidationDefendantType) => { + log('step', 'Verifying consolidation search error page', { defendantType }); + consolidationFlow().assertSearchErrorPage(defendantType); +}); + +When('I go back from the consolidation search error page', () => { + log('step', 'Going back from the consolidation search error page'); + consolidationFlow().goBackFromSearchError(); +}); + When('I enter the following consolidation search details:', (table: DataTable) => { log('step', 'Entering consolidation search details'); consolidationFlow().enterConsolidationSearchDetails(table);