Skip to content

Commit da92042

Browse files
[PRME-163] Update feedback page errors & include error box (#758)
* initial implementation of error component in feedback page * refactoring ErrorBox component to be generic * refactor generic errorBoxErrorMessage and groupErrorsByType * move the generic page error types to a separate file * format code * fix and extend e2e tests for feedback page (errorbox) * refactor errorMessageList on feedback page * extend UT to cover for error box component * fix ESlint warnings * address PR comments * fix scroll to errorBox onSubmit * add timeout to the scroll to ensure the ref exists * increase timeout to 50 * remove timeout and fix tests * add happy-dom as vitest env
1 parent 1f2be81 commit da92042

File tree

11 files changed

+324
-91
lines changed

11 files changed

+324
-91
lines changed

app/cypress/e2e/0-ndr-core-tests/feedback_page.cy.js

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,15 @@ describe('Feedback Page', () => {
134134
.should('be.visible')
135135
.and('have.length', 2)
136136
.as('errors');
137-
cy.get('@errors')
138-
.eq(1)
139-
.should('have.text', 'Error: Please enter your feedback');
140-
cy.get('@errors')
141-
.first()
142-
.should('have.text', 'Error: Please select an option');
137+
cy.get('@errors').eq(1).should('have.text', 'Error: Enter your feedback');
138+
cy.get('@errors').first().should('have.text', 'Error: Select an option');
139+
140+
cy.get('.nhsuk-error-summary__list > li > a')
141+
.should('be.visible')
142+
.and('have.length', 2)
143+
.as('errorBox');
144+
cy.get('@errorBox').first().should('have.text', 'Select an option');
145+
cy.get('@errorBox').eq(1).should('have.text', 'Enter your feedback');
143146
},
144147
);
145148

@@ -161,6 +164,59 @@ describe('Feedback Page', () => {
161164
cy.get('.nhsuk-error-message')
162165
.should('be.visible')
163166
.and('have.text', 'Error: Enter a valid email address');
167+
168+
cy.get('.nhsuk-error-summary__list > li > a')
169+
.should('be.visible')
170+
.and('have.length', 1)
171+
.as('errorBox');
172+
cy.get('@errorBox')
173+
.first()
174+
.should('have.text', 'Enter a valid email address');
175+
},
176+
);
177+
178+
it(
179+
'should scroll to corresponding error element when clicking an error link from the error box',
180+
{ tags: 'regression' },
181+
() => {
182+
const mockInputData = {
183+
respondentName: 'Jane Smith',
184+
respondentEmail: 'some_random_string_which_is_not_valid_email',
185+
};
186+
187+
loginAndVisitFeedbackPage(role);
188+
fillInForm(mockInputData);
189+
cy.get('#submit-feedback').click();
190+
191+
cy.get('.nhsuk-error-summary__list > li > a')
192+
.should('be.visible')
193+
.and('have.length', 3)
194+
.as('errorBox');
195+
196+
cy.get('@errorBox')
197+
.eq(0)
198+
.then(($link) => {
199+
const href = $link.attr('href');
200+
expect(href).to.eq('#select-how-satisfied');
201+
cy.wrap($link).click();
202+
cy.get(href).should('be.visible');
203+
});
204+
cy.get('@errorBox')
205+
.eq(1)
206+
.then(($link) => {
207+
const href = $link.attr('href');
208+
expect(href).to.eq('#feedback_textbox');
209+
cy.wrap($link).click();
210+
cy.get(href).should('be.visible');
211+
});
212+
cy.get('@errorBox')
213+
.eq(2)
214+
.then(($link) => {
215+
const href = $link.attr('href');
216+
expect(href).to.eq('#email-text-input');
217+
cy.wrap($link).click();
218+
cy.get(href).should('be.visible');
219+
});
164220
},
165221
);
166222
});

app/src/components/blocks/_documentUpload/documentSelectOrderStage/DocumentSelectOrderStage.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Link, useNavigate } from 'react-router';
55
import useTitle from '../../../../helpers/hooks/useTitle';
66
import {
77
fileUploadErrorMessages,
8+
groupUploadErrorsByType,
89
UPLOAD_FILE_ERROR_TYPE,
910
} from '../../../../helpers/utils/fileUploadErrorMessages';
1011
import { routeChildren, routes } from '../../../../types/generic/routes';
@@ -13,12 +14,12 @@ import {
1314
DOCUMENT_TYPE,
1415
SetUploadDocuments,
1516
UploadDocument,
16-
UploadFilesError,
1717
} from '../../../../types/pages/UploadDocumentsPage/types';
1818
import BackButton from '../../../generic/backButton/BackButton';
1919
import PatientSummary, { PatientInfo } from '../../../generic/patientSummary/PatientSummary';
2020
import ErrorBox from '../../../layout/errorBox/ErrorBox';
2121
import DocumentUploadLloydGeorgePreview from '../documentUploadLloydGeorgePreview/DocumentUploadLloydGeorgePreview';
22+
import { ErrorMessageListItem } from '../../../../types/pages/genericPageErrors';
2223
import getMergedPdfBlob from '../../../../helpers/utils/pdfMerger';
2324

2425
type Props = {
@@ -29,6 +30,7 @@ type Props = {
2930
type FormData = {
3031
[key: string]: number | null;
3132
};
33+
type UploadFilesError = ErrorMessageListItem<UPLOAD_FILE_ERROR_TYPE>;
3234

3335
const DocumentSelectOrderStage = ({
3436
documents,
@@ -56,6 +58,10 @@ const DocumentSelectOrderStage = ({
5658
scrollToRef.current?.scrollIntoView();
5759
}, [formState.errors]);
5860

61+
const handleErrors = (_: FieldValues): void => {
62+
scrollToRef.current?.scrollIntoView();
63+
};
64+
5965
useEffect(() => {
6066
documents.forEach((doc) => {
6167
const key = documentPositionKey(doc.id);
@@ -178,10 +184,6 @@ const DocumentSelectOrderStage = ({
178184
navigate(routeChildren.DOCUMENT_UPLOAD_CONFIRMATION);
179185
};
180186

181-
const handleErrors = (_: FieldValues): void => {
182-
scrollToRef.current?.scrollIntoView();
183-
};
184-
185187
const errorMessageList = (formStateErrors: FieldErrors<FormData>): UploadFilesError[] =>
186188
Object.entries(formStateErrors)
187189
.map(([key, error]) => {
@@ -214,6 +216,7 @@ const DocumentSelectOrderStage = ({
214216
errorBoxSummaryId="document-positions"
215217
messageTitle="There is a problem"
216218
errorMessageList={errorMessageList(formState.errors)}
219+
groupErrorsFn={groupUploadErrorsByType}
217220
scrollToRef={scrollToRef}
218221
/>
219222
)}
@@ -267,7 +270,7 @@ const DocumentSelectOrderStage = ({
267270
You have removed all files. Go back to&nbsp;
268271
<button
269272
className="govuk-link"
270-
onClick={(e) => {
273+
onClick={(e): void => {
271274
e.preventDefault();
272275
navigate(routes.DOCUMENT_UPLOAD);
273276
}}
@@ -311,7 +314,7 @@ const DocumentSelectOrderStage = ({
311314
type="button"
312315
aria-label={`Remove ${document.file.name} from selection`}
313316
className="link-button"
314-
onClick={() => {
317+
onClick={(): void => {
315318
onRemove(index);
316319
}}
317320
>

app/src/components/blocks/_documentUpload/documentSelectStage/DocumentSelectStage.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { v4 as uuidv4 } from 'uuid';
66
import useTitle from '../../../../helpers/hooks/useTitle';
77
import {
88
fileUploadErrorMessages,
9+
groupUploadErrorsByType,
910
PDF_PARSING_ERROR_TYPE,
1011
UPLOAD_FILE_ERROR_TYPE,
1112
} from '../../../../helpers/utils/fileUploadErrorMessages';
@@ -17,19 +18,21 @@ import {
1718
FileInputEvent,
1819
SetUploadDocuments,
1920
UploadDocument,
20-
UploadFilesError,
2121
} from '../../../../types/pages/UploadDocumentsPage/types';
2222
import BackButton from '../../../generic/backButton/BackButton';
2323
import LinkButton from '../../../generic/linkButton/LinkButton';
2424
import PatientSummary, { PatientInfo } from '../../../generic/patientSummary/PatientSummary';
2525
import ErrorBox from '../../../layout/errorBox/ErrorBox';
26+
import { ErrorMessageListItem } from '../../../../types/pages/genericPageErrors';
2627

2728
export type Props = {
2829
setDocuments: SetUploadDocuments;
2930
documents: Array<UploadDocument>;
3031
documentType: DOCUMENT_TYPE;
3132
};
3233

34+
type UploadFilesError = ErrorMessageListItem<UPLOAD_FILE_ERROR_TYPE>;
35+
3336
const DocumentSelectStage = ({ documents, setDocuments, documentType }: Props): JSX.Element => {
3437
const fileInputRef = useRef<HTMLInputElement | null>(null);
3538
const [noFilesSelected, setNoFilesSelected] = useState<boolean>(false);
@@ -257,6 +260,7 @@ const DocumentSelectStage = ({ documents, setDocuments, documentType }: Props):
257260
errorBoxSummaryId="failed-document-uploads-summary-title"
258261
messageTitle="There is a problem"
259262
errorMessageList={errorMessageList()}
263+
groupErrorsFn={groupUploadErrorsByType}
260264
scrollToRef={scrollToRef}
261265
></ErrorBox>
262266
)}

app/src/components/layout/errorBox/ErrorBox.tsx

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { ErrorSummary } from 'nhsuk-react-components';
22
import { Ref, MouseEvent, JSX } from 'react';
3-
import { groupUploadErrorsByType } from '../../../helpers/utils/fileUploadErrorMessages';
4-
import { UploadFilesError } from '../../../types/pages/UploadDocumentsPage/types';
3+
import { ErrorMessageListItem, GroupErrors } from '../../../types/pages/genericPageErrors';
54

6-
type Props = {
5+
type ErrorBoxProps<T extends string> = {
76
errorBoxSummaryId: string;
87
messageTitle: string;
98
messageBody?: string;
@@ -12,23 +11,34 @@ type Props = {
1211
errorBody?: string;
1312
dataTestId?: string;
1413
errorOnClick?: () => void;
15-
errorMessageList?: UploadFilesError[];
14+
errorMessageList?: ErrorMessageListItem<T>[];
1615
scrollToRef?: Ref<HTMLDivElement>;
16+
groupErrorsFn?: GroupErrors<T>;
1717
};
1818

19-
type UploadErrorMessagesProps = {
20-
errorMessageList: UploadFilesError[];
19+
type ErrorMessagesProps<T extends string> = {
20+
errorMessageList: ErrorMessageListItem<T>[];
21+
groupErrorsFn?: GroupErrors<T>;
2122
};
2223

23-
function UploadErrorMessages({
24+
function ErrorMessages<T extends string>({
2425
errorMessageList,
25-
}: Readonly<UploadErrorMessagesProps>): JSX.Element {
26-
const uploadErrorsGrouped = groupUploadErrorsByType(errorMessageList);
26+
groupErrorsFn,
27+
}: Readonly<ErrorMessagesProps<T>>): JSX.Element {
28+
if (!groupErrorsFn) return <></>;
29+
30+
const groupedErrors = groupErrorsFn(errorMessageList);
2731

2832
return (
2933
<>
30-
{Object.entries(uploadErrorsGrouped).map(([errorType, { linkIds, errorMessage }]) => {
34+
{Object.entries(groupedErrors).map(([errorType, value]) => {
35+
const { linkIds, errorMessage } = value as {
36+
linkIds: string[];
37+
errorMessage: string;
38+
};
39+
3140
const firstFile = linkIds[0];
41+
3242
return (
3343
<div key={errorType}>
3444
<ErrorSummary.List>
@@ -43,7 +53,7 @@ function UploadErrorMessages({
4353
);
4454
}
4555

46-
const ErrorBox = ({
56+
const ErrorBox = <T extends string>({
4757
errorBoxSummaryId,
4858
messageTitle,
4959
messageBody,
@@ -54,7 +64,8 @@ const ErrorBox = ({
5464
errorOnClick,
5565
errorMessageList,
5666
scrollToRef,
57-
}: Props): JSX.Element => {
67+
groupErrorsFn,
68+
}: ErrorBoxProps<T>): JSX.Element => {
5869
const hasInputLink = errorInputLink && messageLinkBody;
5970
const hasOnClick = errorOnClick && messageLinkBody;
6071

@@ -94,8 +105,12 @@ const ErrorBox = ({
94105
</ErrorSummary.Item>
95106
)}
96107
</ErrorSummary.List>
108+
97109
{errorMessageList && (
98-
<UploadErrorMessages errorMessageList={errorMessageList} />
110+
<ErrorMessages<T>
111+
errorMessageList={errorMessageList}
112+
groupErrorsFn={groupErrorsFn}
113+
/>
99114
)}
100115
</ErrorSummary.Body>
101116
</ErrorSummary>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { ErrorMessageListItem, ErrorMessageMap } from '../../types/pages/genericPageErrors';
2+
3+
export function getMappedErrorMessage<T extends string>(
4+
error: ErrorMessageListItem<T>,
5+
messageMap: ErrorMessageMap<T>,
6+
): string {
7+
return messageMap[error.error].errorBox;
8+
}
9+
10+
export function groupErrorsByType<T extends string>(
11+
errors: ErrorMessageListItem<T>[],
12+
getMessage: (error: ErrorMessageListItem<T>) => string,
13+
): Partial<Record<T, { linkIds: string[]; errorMessage: string }>> {
14+
const result: Partial<Record<T, { linkIds: string[]; errorMessage: string }>> = {};
15+
16+
errors.forEach((errorItem) => {
17+
const { error, linkId = '' } = errorItem;
18+
const errorMessage = getMessage(errorItem);
19+
20+
if (!result[error]) {
21+
result[error] = { linkIds: [linkId], errorMessage };
22+
} else {
23+
result[error]!.linkIds.push(linkId);
24+
}
25+
});
26+
27+
return result;
28+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { ErrorMessageListItem } from '../../types/pages/genericPageErrors';
2+
import { getMappedErrorMessage, groupErrorsByType } from './errorMessages';
3+
4+
type FeedbackError = ErrorMessageListItem<FEEDBACK_ERROR_TYPE>;
5+
6+
export enum FEEDBACK_ERROR_TYPE {
7+
feedbackSatisfaction = 'feedbackSatisfaction',
8+
feedbackTextbox = 'feedbackTextbox',
9+
emailTextInput = 'emailTextInput',
10+
}
11+
12+
export type FeedbackErrorMessageType = {
13+
inline: string;
14+
errorBox: string;
15+
};
16+
17+
export const getFeedbackErrorBoxErrorMessage = (error: FeedbackError): string =>
18+
getMappedErrorMessage(error, feedbackErrorMessages);
19+
20+
export const groupFeedbackErrorsByType = (
21+
errors: FeedbackError[],
22+
): Partial<Record<FEEDBACK_ERROR_TYPE, { linkIds: string[]; errorMessage: string }>> =>
23+
groupErrorsByType(errors, getFeedbackErrorBoxErrorMessage);
24+
25+
type ErrorMessageType = { [errorType in FEEDBACK_ERROR_TYPE]: FeedbackErrorMessageType };
26+
27+
export const feedbackErrorMessages: ErrorMessageType = {
28+
feedbackSatisfaction: {
29+
inline: 'Select an option',
30+
errorBox: 'Select an option',
31+
},
32+
feedbackTextbox: {
33+
inline: 'Enter your feedback',
34+
errorBox: 'Enter your feedback',
35+
},
36+
emailTextInput: {
37+
inline: 'Enter a valid email address',
38+
errorBox: 'Enter a valid email address',
39+
},
40+
};

0 commit comments

Comments
 (0)