Skip to content

Commit 92dca42

Browse files
[PRMP-1488] Handle restricted patient access when searching
1 parent 72d80e6 commit 92dca42

File tree

10 files changed

+334
-214
lines changed

10 files changed

+334
-214
lines changed

app/src/components/blocks/_reviews/reviewDetailsPatientSearchStage/ReviewDetailsPatientSearchStage.test.tsx

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,13 @@ import { buildPatientDetails } from '../../../../helpers/test/testBuilders';
99
import * as documentTypeModule from '../../../../helpers/utils/documentType';
1010
import { ReviewDetails } from '../../../../types/generic/reviews';
1111
import { routes } from '../../../../types/generic/routes';
12-
import ReviewDetailsPatientSearchStage, {
13-
incorrectFormatMessage,
14-
} from './ReviewDetailsPatientSearchStage';
12+
import ReviewDetailsPatientSearchStage from './ReviewDetailsPatientSearchStage';
1513
import { DOCUMENT_TYPE } from '../../../../helpers/utils/documentType';
1614
import {
1715
ReviewUploadDocument,
1816
DOCUMENT_UPLOAD_STATE,
1917
} from '../../../../types/pages/UploadDocumentsPage/types';
18+
import { incorrectFormatMessage } from '../../../generic/patientSearchForm/PatientSearchForm';
2019

2120
const mockNavigate = vi.fn();
2221
const mockUseParams = vi.fn();
@@ -83,6 +82,10 @@ vi.mock('../../../generic/recordLoader/RecordLoader', () => ({
8382
vi.mock('../../../../helpers/requests/getPatientDetails');
8483
const mockGetPatientDetails = getPatientDetails as Mock;
8584

85+
const expectedFormatErrorMessage =
86+
incorrectFormatMessage +
87+
" If you keep getting this message, select 'I don't know the NHS number'.";
88+
8689
describe('ReviewDetailsPatientSearchPage', () => {
8790
const mockReviewId = 'review-123';
8891
const mockBaseUrl = 'https://api.example.com';
@@ -155,7 +158,7 @@ describe('ReviewDetailsPatientSearchPage', () => {
155158
);
156159

157160
expect(screen.getByTestId('nhs-number-input')).toBeInTheDocument();
158-
expect(screen.getByLabelText(/A 10-digit number/)).toBeInTheDocument();
161+
expect(screen.getByText(/A 10-digit number/)).toBeInTheDocument();
159162
});
160163

161164
it('renders continue button', () => {
@@ -226,7 +229,7 @@ describe('ReviewDetailsPatientSearchPage', () => {
226229
await userEvent.click(continueButton);
227230

228231
await waitFor(() => {
229-
const errorMessages = screen.getAllByText(incorrectFormatMessage);
232+
const errorMessages = screen.getAllByText(expectedFormatErrorMessage);
230233
expect(errorMessages.length).toBeGreaterThan(0);
231234
});
232235
});
@@ -249,7 +252,7 @@ describe('ReviewDetailsPatientSearchPage', () => {
249252

250253
await waitFor(() => {
251254
// Check that incorrectFormatMessage appears twice (ErrorBox + TextInput)
252-
const errorMessages = screen.getAllByText(incorrectFormatMessage);
255+
const errorMessages = screen.getAllByText(expectedFormatErrorMessage);
253256
expect(errorMessages).toHaveLength(2);
254257
});
255258
});
@@ -268,7 +271,7 @@ describe('ReviewDetailsPatientSearchPage', () => {
268271
await userEvent.click(screen.getByTestId('continue-button'));
269272

270273
await waitFor(() => {
271-
const errorMessages = screen.getAllByText(incorrectFormatMessage);
274+
const errorMessages = screen.getAllByText(expectedFormatErrorMessage);
272275
expect(errorMessages.length).toBeGreaterThan(0);
273276
});
274277
});
@@ -350,7 +353,7 @@ describe('ReviewDetailsPatientSearchPage', () => {
350353
await userEvent.click(screen.getByTestId('continue-button'));
351354

352355
await waitFor(() => {
353-
const errorMessages = screen.getAllByText(incorrectFormatMessage);
356+
const errorMessages = screen.getAllByText(expectedFormatErrorMessage);
354357
expect(errorMessages.length).toBeGreaterThan(0);
355358
});
356359
});
@@ -369,7 +372,7 @@ describe('ReviewDetailsPatientSearchPage', () => {
369372
await userEvent.click(screen.getByTestId('continue-button'));
370373

371374
await waitFor(() => {
372-
const errorMessages = screen.getAllByText(incorrectFormatMessage);
375+
const errorMessages = screen.getAllByText(expectedFormatErrorMessage);
373376
expect(errorMessages.length).toBeGreaterThan(0);
374377
});
375378
});
@@ -388,7 +391,7 @@ describe('ReviewDetailsPatientSearchPage', () => {
388391
await userEvent.click(screen.getByTestId('continue-button'));
389392

390393
await waitFor(() => {
391-
const errorMessages = screen.getAllByText(incorrectFormatMessage);
394+
const errorMessages = screen.getAllByText(expectedFormatErrorMessage);
392395
expect(errorMessages.length).toBeGreaterThan(0);
393396
});
394397
});
@@ -501,7 +504,7 @@ describe('ReviewDetailsPatientSearchPage', () => {
501504
await userEvent.click(screen.getByTestId('continue-button'));
502505

503506
await waitFor(() => {
504-
const errorMessages = screen.getAllByText(incorrectFormatMessage);
507+
const errorMessages = screen.getAllByText(expectedFormatErrorMessage);
505508
expect(errorMessages.length).toBeGreaterThan(0);
506509
});
507510

app/src/components/blocks/_reviews/reviewDetailsPatientSearchStage/ReviewDetailsPatientSearchStage.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@ import { RecordLayout } from '../../../generic/recordCard/RecordCard';
1919
import { RecordLoader, RecordLoaderProps } from '../../../generic/recordLoader/RecordLoader';
2020
import DocumentUploadLloydGeorgePreview from '../../_documentManagement/documentUploadLloydGeorgePreview/DocumentUploadLloydGeorgePreview';
2121

22-
export const incorrectFormatMessage =
23-
"Enter a valid patient NHS number. If you keep getting this message, select 'I don't know the NHS number'.";
24-
2522
interface ReviewDetailsPatientSearchPageProps {
2623
reviewData: ReviewDetails | null;
2724
uploadDocuments: ReviewUploadDocument[];

app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsSearchPatientStage/UserPatientRestrictionsSearchPatientStage.test.tsx

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -93,15 +93,4 @@ describe('UserPatientRestrictionsSearchPatientStage', () => {
9393
routeChildren.USER_PATIENT_RESTRICTIONS_VERIFY_PATIENT,
9494
);
9595
});
96-
97-
it('navigates to the generic error page if patient access is restricted', async () => {
98-
render(<UserPatientRestrictionsSearchPatientStage />);
99-
100-
const errorButton = screen.getByTestId('error-button');
101-
await userEvent.click(errorButton);
102-
103-
expect(mockNavigate).toHaveBeenCalledWith(
104-
`${routes.GENERIC_ERROR}?errorCode=${UIErrorCode.PATIENT_ACCESS_RESTRICTED}`,
105-
);
106-
});
10796
});

app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsSearchPatientStage/UserPatientRestrictionsSearchPatientStage.tsx

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import { routeChildren, routes } from '../../../../types/generic/routes';
66
import BackButton from '../../../generic/backButton/BackButton';
77
import PatientSearchForm from '../../../generic/patientSearchForm/PatientSearchForm';
88
import { UIErrorCode } from '../../../../types/generic/errors';
9-
import { AxiosError } from 'axios';
10-
import { ErrorResponse } from '../../../../types/generic/errorResponse';
119

1210
const UserPatientRestrictionsSearchPatientStage = (): React.JSX.Element => {
1311
const [, setPatientDetails] = usePatientDetailsContext();
@@ -28,20 +26,12 @@ const UserPatientRestrictionsSearchPatientStage = (): React.JSX.Element => {
2826
navigate(routeChildren.USER_PATIENT_RESTRICTIONS_VERIFY_PATIENT);
2927
};
3028

31-
const onSearchError = (error: AxiosError): void => {
32-
const errorResponse = error.response?.data as ErrorResponse;
33-
if (errorResponse?.err_code === 'SP_4006') {
34-
navigate(routes.GENERIC_ERROR + `?errorCode=${UIErrorCode.PATIENT_ACCESS_RESTRICTED}`);
35-
}
36-
};
37-
3829
return (
3930
<>
4031
<BackButton />
4132

4233
<PatientSearchForm
4334
onSuccess={onSearchSuccess}
44-
onError={onSearchError}
4535
title="Search for a patient"
4636
subtitle="Enter the patient's NHS number to add a restriction"
4737
/>
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import { render, screen } from '@testing-library/react';
2+
import PatientSearchForm from './PatientSearchForm';
3+
import userEvent from '@testing-library/user-event';
4+
import useConfig from '../../../helpers/hooks/useConfig';
5+
import { Mock } from 'vitest';
6+
import usePatient from '../../../helpers/hooks/usePatient';
7+
import { buildPatientDetails } from '../../../helpers/test/testBuilders';
8+
import { getFormattedPatientFullName } from '../../../helpers/utils/formatPatientFullName';
9+
10+
vi.mock('react-router-dom', async () => {
11+
const actual = await vi.importActual('react-router-dom');
12+
return {
13+
...actual,
14+
useNavigate: (): unknown => mockNavigate,
15+
Link: ({
16+
children,
17+
onClick,
18+
}: {
19+
children: React.ReactNode;
20+
onClick?: () => void;
21+
}): React.JSX.Element => <button onClick={onClick}>{children}</button>,
22+
};
23+
});
24+
vi.mock('../../../helpers/utils/handlePatientSearch', async () => ({
25+
...(await vi.importActual('../../../helpers/utils/handlePatientSearch')),
26+
handlePatientSearchError: mockHandlePatientSearchError,
27+
handleSearch: mockHandleSearch,
28+
}));
29+
vi.mock('../../../helpers/hooks/useBaseAPIHeaders');
30+
vi.mock('../../../helpers/hooks/useConfig');
31+
vi.mock('../../../helpers/hooks/useBaseAPIUrl');
32+
vi.mock('../../../helpers/hooks/usePatient');
33+
34+
const mockNavigate = vi.fn();
35+
const mockHandlePatientSearchError = vi.hoisted(() => vi.fn());
36+
const mockHandleSearch = vi.hoisted(() => vi.fn());
37+
const mockUseConfig = useConfig as Mock;
38+
const mockUsePatient = usePatient as Mock;
39+
40+
describe('PatientSearchForm', () => {
41+
beforeEach(() => {
42+
vi.resetAllMocks();
43+
mockUseConfig.mockReturnValue({
44+
mockLocal: true,
45+
});
46+
});
47+
48+
it('calls custom error handler on failure when provided', async () => {
49+
renderComponent();
50+
51+
const error = {
52+
response: {
53+
status: 403,
54+
data: {
55+
err_code: 'SP_4006',
56+
},
57+
},
58+
};
59+
mockHandleSearch.mockResolvedValue([null, 403, error]);
60+
61+
const input = screen.getByTestId('nhs-number-input');
62+
await userEvent.type(input, '1234567890');
63+
const errorButton = screen.getByTestId('continue-button');
64+
await userEvent.click(errorButton);
65+
66+
expect(mockError).toHaveBeenCalledWith(error);
67+
});
68+
69+
it('sets input error to errorCode when error code is set', async () => {
70+
renderComponent();
71+
72+
const errorCode = 'patient not found';
73+
const error = {
74+
response: {
75+
status: 404,
76+
data: {
77+
err_code: errorCode,
78+
},
79+
},
80+
};
81+
mockHandleSearch.mockResolvedValue([errorCode, 404, error]);
82+
83+
const input = screen.getByTestId('nhs-number-input');
84+
await userEvent.type(input, '1234567890');
85+
const errorButton = screen.getByTestId('continue-button');
86+
await userEvent.click(errorButton);
87+
88+
expect(screen.getAllByText(errorCode)).toHaveLength(2);
89+
});
90+
91+
it('calls default error handler when error code is not set', async () => {
92+
renderComponent();
93+
94+
const statusCode = 403;
95+
const error = {
96+
response: {
97+
status: statusCode,
98+
data: {
99+
err_code: null,
100+
},
101+
},
102+
};
103+
mockHandleSearch.mockResolvedValue([null, statusCode, error]);
104+
105+
const input = screen.getByTestId('nhs-number-input');
106+
await userEvent.type(input, '1234567890');
107+
const errorButton = screen.getByTestId('continue-button');
108+
await userEvent.click(errorButton);
109+
110+
expect(mockHandlePatientSearchError).toHaveBeenCalledWith(
111+
statusCode,
112+
mockNavigate,
113+
expect.anything(),
114+
error,
115+
);
116+
});
117+
118+
it('should call onSuccess when search is successful', async () => {
119+
renderComponent();
120+
121+
const patientDetails = {
122+
name: 'John Doe',
123+
nhsNumber: '1234567890',
124+
dateOfBirth: '01/01/1980',
125+
};
126+
mockHandleSearch.mockImplementation(({ handleSuccess }) => {
127+
handleSuccess(patientDetails);
128+
return null;
129+
});
130+
131+
const input = screen.getByTestId('nhs-number-input');
132+
await userEvent.type(input, '1234567890');
133+
const continueButton = screen.getByTestId('continue-button');
134+
await userEvent.click(continueButton);
135+
136+
expect(mockSuccess).toHaveBeenCalledWith(patientDetails);
137+
});
138+
139+
it('should render patient details when displayPatientDetails is true', async () => {
140+
const patient = buildPatientDetails();
141+
mockUsePatient.mockReturnValue(patient);
142+
143+
render(
144+
<PatientSearchForm
145+
onSuccess={mockSuccess}
146+
onError={mockError}
147+
title="Patient search"
148+
displayPatientDetails={true}
149+
/>,
150+
);
151+
152+
expect(screen.getByText(getFormattedPatientFullName(patient))).toBeInTheDocument();
153+
});
154+
155+
it('should not render patient details when displayPatientDetails is false', async () => {
156+
const patient = buildPatientDetails();
157+
mockUsePatient.mockReturnValue(patient);
158+
159+
render(
160+
<PatientSearchForm
161+
onSuccess={mockSuccess}
162+
onError={mockError}
163+
title="Patient search"
164+
displayPatientDetails={false}
165+
/>,
166+
);
167+
168+
expect(screen.queryByText(getFormattedPatientFullName(patient))).not.toBeInTheDocument();
169+
});
170+
171+
it('should call secondaryAction fn when provided with secondaryActionText', async () => {
172+
const mockSecondaryAction = vi.fn();
173+
174+
render(
175+
<PatientSearchForm
176+
onSuccess={mockSuccess}
177+
onError={mockError}
178+
title="Patient search"
179+
secondaryActionText="Can't find NHS number?"
180+
onSecondaryActionClicked={mockSecondaryAction}
181+
/>,
182+
);
183+
184+
const secondaryActionButton = screen.getByText("Can't find NHS number?");
185+
await userEvent.click(secondaryActionButton);
186+
187+
expect(mockSecondaryAction).toHaveBeenCalled();
188+
});
189+
190+
it('displays spinner while search is in progress', async () => {
191+
renderComponent();
192+
193+
mockHandleSearch.mockResolvedValue(new Promise(() => {}));
194+
195+
const input = screen.getByTestId('nhs-number-input');
196+
await userEvent.type(input, '1234567890');
197+
const continueButton = screen.getByTestId('continue-button');
198+
await userEvent.click(continueButton);
199+
200+
expect(screen.getByText('Searching...')).toBeInTheDocument();
201+
});
202+
});
203+
204+
const mockSuccess = vi.fn();
205+
const mockError = vi.fn();
206+
const renderComponent = (): void => {
207+
render(
208+
<PatientSearchForm onSuccess={mockSuccess} onError={mockError} title="Patient search" />,
209+
);
210+
};

0 commit comments

Comments
 (0)