Skip to content

Commit 5a268be

Browse files
[PRMP-1482] Implement user restrictions list page
1 parent 326c0e5 commit 5a268be

26 files changed

Lines changed: 1010 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { render, screen } from '@testing-library/react';
2+
import UserPatientRestrictionsAddStage from './UserPatientRestrictionsAddStage';
3+
4+
describe('UserPatientRestrictionsAddStage', () => {
5+
it('renders correctly', () => {
6+
render(<UserPatientRestrictionsAddStage />);
7+
8+
expect(screen.getByText('Add user patient restriction')).toBeInTheDocument();
9+
});
10+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const UserPatientRestrictionsAddStage = (): React.JSX.Element => {
2+
return <div>Add user patient restriction</div>;
3+
};
4+
5+
export default UserPatientRestrictionsAddStage;

app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsIndex/UserPatientRestrictionsIndex.test.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react';
22
import UserPatientRestrictionsIndex from './UserPatientRestrictionsIndex';
33
import { runAxeTest } from '../../../../helpers/test/axeTestHelper';
44
import { beforeEach, describe, expect, it, Mock, vi } from 'vitest';
5-
import { routes } from '../../../../types/generic/routes';
5+
import { routeChildren, routes } from '../../../../types/generic/routes';
66

77
vi.mock('../../../helpers/hooks/useTitle');
88
vi.mock('../../../../styles/right-chevron-circle.svg', () => ({
@@ -59,6 +59,16 @@ describe('UserRestrictionsPage', (): void => {
5959
screen.getByTestId('user-restrictions-back-btn').click();
6060
expect(mockNavigate).toHaveBeenCalledWith(routes.ADMIN_ROUTE);
6161
});
62+
63+
it('navigates to add restriction page when add restriction button is clicked', (): void => {
64+
screen.getByTestId('add-user-restriction-btn').click();
65+
expect(mockNavigate).toHaveBeenCalledWith(routeChildren.USER_PATIENT_RESTRICTIONS_ADD);
66+
});
67+
68+
it('navigates to view restrictions page when view restrictions button is clicked', (): void => {
69+
screen.getByTestId('view-user-restrictions-btn').click();
70+
expect(mockNavigate).toHaveBeenCalledWith(routeChildren.USER_PATIENT_RESTRICTIONS_LIST);
71+
});
6272
});
6373

6474
describe('Accessibility', (): void => {

app/src/components/blocks/_userPatientRestrictions/userPatientRestrictionsIndex/UserPatientRestrictionsIndex.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import { JSX } from 'react';
33
import useTitle from '../../../../helpers/hooks/useTitle';
44
import { ReactComponent as RightCircleIcon } from '../../../../styles/right-chevron-circle.svg';
55
import BackButton from '../../../generic/backButton/BackButton';
6-
import { routes } from '../../../../types/generic/routes';
6+
import { routeChildren, routes } from '../../../../types/generic/routes';
7+
import { useNavigate } from 'react-router-dom';
78

89
const UserPatientRestrictionsIndex = (): JSX.Element => {
10+
const navigate = useNavigate();
911
useTitle({ pageTitle: 'Restrict staff from accessing patient records' });
1012

1113
return (
@@ -17,7 +19,14 @@ const UserPatientRestrictionsIndex = (): JSX.Element => {
1719
<Card clickable cardType="primary">
1820
<Card.Content>
1921
<Card.Heading className="nhsuk-heading-m">
20-
<Card.Link data-testid="add-user-restriction-btn" href="#">
22+
<Card.Link
23+
data-testid="add-user-restriction-btn"
24+
href="#"
25+
onClick={(e): void => {
26+
e.preventDefault();
27+
navigate(routeChildren.USER_PATIENT_RESTRICTIONS_ADD);
28+
}}
29+
>
2130
Add a restriction
2231
</Card.Link>
2332
</Card.Heading>
@@ -32,7 +41,14 @@ const UserPatientRestrictionsIndex = (): JSX.Element => {
3241
<Card clickable cardType="primary">
3342
<Card.Content>
3443
<Card.Heading className="nhsuk-heading-m">
35-
<Card.Link data-testid="view-user-restrictions-btn" href="#">
44+
<Card.Link
45+
data-testid="view-user-restrictions-btn"
46+
href="#"
47+
onClick={(e): void => {
48+
e.preventDefault();
49+
navigate(routeChildren.USER_PATIENT_RESTRICTIONS_LIST);
50+
}}
51+
>
3652
View and remove a restriction
3753
</Card.Link>
3854
</Card.Heading>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.user-patient-restrictions-list-stage {
2+
#search-input {
3+
width: 300px;
4+
}
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
import { render, screen, waitFor } from '@testing-library/react';
2+
import UserPatientRestrictionsListStage from './UserPatientRestrictionsListStage';
3+
import userEvent from '@testing-library/user-event';
4+
import { Mock } from 'vitest';
5+
import { routeChildren } from '../../../../types/generic/routes';
6+
import getUserPatientRestrictions from '../../../../helpers/requests/userPatientRestrictions/getUserPatientRestrictions';
7+
import { buildUserRestrictions } from '../../../../helpers/test/testBuilders';
8+
9+
vi.mock('react-router-dom', async () => {
10+
const actual = await vi.importActual('react-router-dom');
11+
return {
12+
...actual,
13+
useNavigate: (): Mock => mockNavigate,
14+
Link: ({
15+
children,
16+
onClick,
17+
'data-testid': dataTestId,
18+
}: {
19+
children: React.ReactNode;
20+
onClick?: () => void;
21+
'data-testid'?: string;
22+
}): React.JSX.Element => (
23+
<div onClick={onClick} data-testid={dataTestId}>
24+
{children}
25+
</div>
26+
),
27+
};
28+
});
29+
vi.mock('../../../../helpers/requests/userPatientRestrictions/getUserPatientRestrictions');
30+
vi.mock('../../../../helpers/hooks/useBaseAPIHeaders');
31+
vi.mock('../../../../helpers/hooks/useBaseAPIUrl');
32+
33+
const mockNavigate = vi.fn();
34+
const mockGetUserPatientRestrictions = getUserPatientRestrictions as Mock;
35+
36+
describe('UserPatientRestrictionsListStage', () => {
37+
it('renders correctly', () => {
38+
render(<UserPatientRestrictionsListStage />);
39+
40+
expect(
41+
screen.getByText('Manage restrictions on access to patient records'),
42+
).toBeInTheDocument();
43+
});
44+
45+
it('should navigate to add restrictions stage when add restriction button is clicked', async () => {
46+
render(<UserPatientRestrictionsListStage />);
47+
48+
const addRestrictionButton = screen.getByRole('button', { name: 'Add a restriction' });
49+
expect(addRestrictionButton).toBeInTheDocument();
50+
51+
await userEvent.click(addRestrictionButton);
52+
53+
expect(mockNavigate).toHaveBeenCalledWith(routeChildren.USER_PATIENT_RESTRICTIONS_ADD);
54+
});
55+
56+
it('should navigate to view restrictions stage when view restriction button is clicked', async () => {
57+
const restrictions = buildUserRestrictions();
58+
mockGetUserPatientRestrictions.mockResolvedValueOnce({
59+
restrictions,
60+
});
61+
62+
render(<UserPatientRestrictionsListStage />);
63+
64+
await waitFor(async () => {
65+
const viewRestrictionButton = screen.getByTestId(
66+
`view-record-link-${restrictions[0].id}`,
67+
);
68+
expect(viewRestrictionButton).toBeInTheDocument();
69+
70+
await userEvent.click(viewRestrictionButton);
71+
});
72+
73+
expect(mockNavigate).toHaveBeenCalledWith(
74+
routeChildren.USER_PATIENT_RESTRICTIONS_VIEW.replace(
75+
':restrictionId',
76+
restrictions[0].id,
77+
),
78+
);
79+
});
80+
81+
it('should show error message when restrictions fail to load', async () => {
82+
mockGetUserPatientRestrictions.mockRejectedValueOnce(
83+
new Error('Failed to load restrictions'),
84+
);
85+
86+
render(<UserPatientRestrictionsListStage />);
87+
88+
await waitFor(() => {
89+
expect(screen.getByTestId('failed-to-load-error')).toBeInTheDocument();
90+
});
91+
});
92+
93+
it('should show loading state when restrictions are loading', async () => {
94+
mockGetUserPatientRestrictions.mockReturnValue(
95+
new Promise(() => {
96+
// never resolves to simulate loading state
97+
}),
98+
);
99+
100+
render(<UserPatientRestrictionsListStage />);
101+
102+
await waitFor(() => {
103+
expect(screen.getByText('Searching...')).toBeInTheDocument();
104+
expect(screen.getByText('Loading...')).toBeInTheDocument();
105+
});
106+
});
107+
108+
it('should show no restrictions message when there are no restrictions', async () => {
109+
mockGetUserPatientRestrictions.mockResolvedValueOnce({
110+
restrictions: [],
111+
});
112+
113+
render(<UserPatientRestrictionsListStage />);
114+
115+
await waitFor(() => {
116+
expect(screen.getByText('No user patient restrictions found')).toBeInTheDocument();
117+
});
118+
});
119+
120+
it('should show pagination when there are more than 10 restrictions', async () => {
121+
const restrictions = buildUserRestrictions();
122+
mockGetUserPatientRestrictions.mockResolvedValueOnce({
123+
restrictions,
124+
nextPageToken: 'next-page-token',
125+
});
126+
127+
render(<UserPatientRestrictionsListStage />);
128+
129+
await waitFor(() => {
130+
expect(screen.getByText('Next')).toBeInTheDocument();
131+
});
132+
});
133+
134+
it('should load next page of restrictions when next button is clicked', async () => {
135+
const restrictions = buildUserRestrictions();
136+
mockGetUserPatientRestrictions.mockResolvedValueOnce({
137+
restrictions,
138+
nextPageToken: 'next-page-token',
139+
});
140+
141+
render(<UserPatientRestrictionsListStage />);
142+
143+
await waitFor(async () => {
144+
const nextButton = screen.getByText('Next');
145+
expect(nextButton).toBeInTheDocument();
146+
await userEvent.click(nextButton);
147+
});
148+
149+
expect(mockGetUserPatientRestrictions).toHaveBeenCalledWith(
150+
expect.objectContaining({
151+
pageToken: 'next-page-token',
152+
}),
153+
);
154+
});
155+
156+
it('should show previous button when on next page of restrictions', async () => {
157+
const restrictions = buildUserRestrictions();
158+
mockGetUserPatientRestrictions.mockResolvedValueOnce({
159+
restrictions,
160+
nextPageToken: 'next-page-token',
161+
});
162+
163+
render(<UserPatientRestrictionsListStage />);
164+
165+
await waitFor(() => {
166+
expect(screen.getByText('Next')).toBeInTheDocument();
167+
});
168+
169+
mockGetUserPatientRestrictions.mockResolvedValueOnce({
170+
restrictions,
171+
nextPageToken: 'next-page-token-2',
172+
});
173+
174+
await waitFor(async () => {
175+
const nextButton = screen.getByText('Next');
176+
expect(nextButton).toBeInTheDocument();
177+
await userEvent.click(nextButton);
178+
});
179+
180+
await waitFor(() => {
181+
expect(screen.getByText('Previous')).toBeInTheDocument();
182+
});
183+
});
184+
185+
it('should load previous page of restrictions when previous button is clicked', async () => {
186+
const restrictions = buildUserRestrictions();
187+
mockGetUserPatientRestrictions.mockResolvedValueOnce({
188+
restrictions,
189+
nextPageToken: 'next-page-token',
190+
});
191+
192+
render(<UserPatientRestrictionsListStage />);
193+
194+
await waitFor(async () => {
195+
const nextButton = screen.getByText('Next');
196+
expect(nextButton).toBeInTheDocument();
197+
await userEvent.click(nextButton);
198+
});
199+
200+
mockGetUserPatientRestrictions.mockResolvedValueOnce({
201+
restrictions,
202+
nextPageToken: 'next-page-token-2',
203+
});
204+
205+
await waitFor(async () => {
206+
const previousButton = screen.getByText('Previous');
207+
expect(previousButton).toBeInTheDocument();
208+
await userEvent.click(previousButton);
209+
});
210+
211+
expect(mockGetUserPatientRestrictions).toHaveBeenCalledWith(
212+
expect.objectContaining({
213+
pageToken: undefined, // should go back to first page
214+
}),
215+
);
216+
});
217+
218+
it('should load selected page of restrictions when pagination buttons are clicked', async () => {
219+
const restrictions = buildUserRestrictions();
220+
mockGetUserPatientRestrictions.mockResolvedValueOnce({
221+
restrictions,
222+
nextPageToken: 'next-page-token',
223+
});
224+
225+
render(<UserPatientRestrictionsListStage />);
226+
227+
await waitFor(async () => {
228+
const nextButton = screen.getByText('Next');
229+
expect(nextButton).toBeInTheDocument();
230+
await userEvent.click(nextButton);
231+
});
232+
233+
mockGetUserPatientRestrictions.mockResolvedValueOnce({
234+
restrictions,
235+
nextPageToken: 'next-page-token-2',
236+
});
237+
238+
await waitFor(async () => {
239+
const page1Button = screen.getByRole('link', { name: 'Page 1' });
240+
expect(page1Button).toBeInTheDocument();
241+
await userEvent.click(page1Button);
242+
});
243+
244+
expect(mockGetUserPatientRestrictions).toHaveBeenCalledWith(
245+
expect.objectContaining({
246+
pageToken: undefined, // should load first page
247+
}),
248+
);
249+
});
250+
251+
it('should send nhsNumber parameter when searching by valid nhs number', async () => {
252+
render(<UserPatientRestrictionsListStage />);
253+
254+
const nhsNumber = '2222222222';
255+
await userEvent.click(screen.getByTestId('nhs-number-radio-button'));
256+
await userEvent.type(screen.getByTestId('search-input'), nhsNumber);
257+
await userEvent.click(screen.getByRole('button', { name: 'Search' }));
258+
259+
await waitFor(() => {
260+
expect(mockGetUserPatientRestrictions).toHaveBeenCalledWith(
261+
expect.objectContaining({
262+
nhsNumber,
263+
smartcardNumber: undefined,
264+
}),
265+
);
266+
});
267+
});
268+
269+
it('should send smartcardNumber parameter when searching by smartcard number', async () => {
270+
render(<UserPatientRestrictionsListStage />);
271+
272+
const smartcardNumber = '1234567891234567';
273+
await userEvent.click(screen.getByTestId('smartcard-number-radio-button'));
274+
await userEvent.type(screen.getByTestId('search-input'), smartcardNumber);
275+
await userEvent.click(screen.getByRole('button', { name: 'Search' }));
276+
277+
await waitFor(() => {
278+
expect(mockGetUserPatientRestrictions).toHaveBeenCalledWith(
279+
expect.objectContaining({
280+
nhsNumber: undefined,
281+
smartcardNumber,
282+
}),
283+
);
284+
});
285+
});
286+
287+
it.each(['123', 'abc', '123456789', '12345678901', '1234567890'])(
288+
'should show validation error when searching by invalid nhs number: %s',
289+
async (invalidNhsNumber) => {
290+
render(<UserPatientRestrictionsListStage />);
291+
292+
await userEvent.click(screen.getByTestId('nhs-number-radio-button'));
293+
await userEvent.type(screen.getByTestId('search-input'), invalidNhsNumber);
294+
await userEvent.click(screen.getByRole('button', { name: 'Search' }));
295+
296+
await waitFor(() => {
297+
expect(
298+
screen.getByText('Please enter a valid 10-digit NHS number'),
299+
).toBeInTheDocument();
300+
});
301+
},
302+
);
303+
});

0 commit comments

Comments
 (0)