@openmrs/esm-login-app 8.0.1-pre.3511 → 8.0.1-pre.3518

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,19 +1,19 @@
1
- import React, { useState, useEffect, useCallback, useMemo } from 'react';
1
+ import React, { useCallback, useEffect, useId, useMemo, useState } from 'react';
2
+ import { Button, Checkbox, InlineLoading } from '@carbon/react';
2
3
  import { useLocation, type Location, useSearchParams } from 'react-router-dom';
3
4
  import { useTranslation } from 'react-i18next';
4
- import { Button, Checkbox, InlineLoading } from '@carbon/react';
5
5
  import {
6
+ getCoreTranslation,
7
+ LocationPicker,
6
8
  navigate,
7
9
  setSessionLocation,
8
10
  useConfig,
9
11
  useConnectivity,
10
12
  useSession,
11
- LocationPicker,
12
- getCoreTranslation,
13
13
  } from '@openmrs/esm-framework';
14
- import type { LoginReferrer } from '../login/login.component';
15
14
  import { useDefaultLocation, useLocationCount } from './location-picker.resource';
16
15
  import type { ConfigSchema } from '../config-schema';
16
+ import type { LoginReferrer } from '../login/login.component';
17
17
  import styles from './location-picker.scss';
18
18
 
19
19
  interface LocationPickerProps {
@@ -27,6 +27,7 @@ const LocationPickerView: React.FC<LocationPickerProps> = ({ hideWelcomeMessage,
27
27
  const { chooseLocation } = config;
28
28
  const isLoginEnabled = useConnectivity();
29
29
  const [searchParams] = useSearchParams();
30
+ const checkboxId = useId();
30
31
  const isUpdateFlow = useMemo(() => searchParams.get('update') === 'true', [searchParams]);
31
32
  const { defaultLocation, updateDefaultLocation, savePreference, setSavePreference } =
32
33
  useDefaultLocation(isUpdateFlow);
@@ -40,7 +41,6 @@ const LocationPickerView: React.FC<LocationPickerProps> = ({ hideWelcomeMessage,
40
41
  const { currentUser, userProperties } = useMemo(
41
42
  () => ({
42
43
  currentUser: user?.display,
43
- userUuid: user?.uuid,
44
44
  userProperties: user?.userProperties,
45
45
  }),
46
46
  [user],
@@ -64,7 +64,7 @@ const LocationPickerView: React.FC<LocationPickerProps> = ({ hideWelcomeMessage,
64
64
  setIsSubmitting(true);
65
65
 
66
66
  const referrer = state?.referrer;
67
- const returnToUrl = new URLSearchParams(location?.search).get('returnToUrl');
67
+ const returnToUrl = searchParams.get('returnToUrl');
68
68
 
69
69
  const sessionDefined = setSessionLocation(locationUuid, new AbortController());
70
70
 
@@ -79,20 +79,23 @@ const LocationPickerView: React.FC<LocationPickerProps> = ({ hideWelcomeMessage,
79
79
  } else {
80
80
  navigate({ to: config.links.loginSuccess });
81
81
  }
82
- return;
83
82
  });
84
83
  },
85
- [state?.referrer, config.links.loginSuccess, updateDefaultLocation],
84
+ [state?.referrer, config.links.loginSuccess, updateDefaultLocation, searchParams],
86
85
  );
87
86
 
88
87
  // Handle cases where the location picker is disabled, there is only one location, or there are no locations.
89
88
  useEffect(() => {
90
89
  if (isLoadingLocationCount) return;
91
90
 
92
- if (locationCount == 0) {
91
+ if (locationCount === 0) {
93
92
  changeLocation();
94
- } else if (locationCount == 1 || !chooseLocation.enabled) {
95
- changeLocation(firstLocation!.resource.id, true);
93
+ } else if (locationCount === 1 || !chooseLocation.enabled) {
94
+ if (firstLocation?.resource?.id) {
95
+ changeLocation(firstLocation.resource.id, true);
96
+ } else {
97
+ console.error('Expected location data is missing', { firstLocation, locationCount });
98
+ }
96
99
  }
97
100
  }, [locationCount, isLoadingLocationCount]);
98
101
 
@@ -111,7 +114,9 @@ const LocationPickerView: React.FC<LocationPickerProps> = ({ hideWelcomeMessage,
111
114
  (evt: React.FormEvent<HTMLFormElement>) => {
112
115
  evt.preventDefault();
113
116
 
114
- if (!activeLocation) return;
117
+ if (!activeLocation) {
118
+ return;
119
+ }
115
120
 
116
121
  changeLocation(activeLocation, savePreference);
117
122
  },
@@ -141,10 +146,10 @@ const LocationPickerView: React.FC<LocationPickerProps> = ({ hideWelcomeMessage,
141
146
  />
142
147
  <div className={styles.footerContainer}>
143
148
  <Checkbox
144
- id="checkbox"
145
149
  className={styles.savePreferenceCheckbox}
146
- labelText={t('rememberLocationForFutureLogins', 'Remember my location for future logins')}
147
150
  checked={savePreference}
151
+ id={checkboxId}
152
+ labelText={t('rememberLocationForFutureLogins', 'Remember my location for future logins')}
148
153
  onChange={(_, { checked }) => setSavePreference(checked)}
149
154
  />
150
155
  <Button
@@ -1,8 +1,8 @@
1
1
  import { useCallback, useEffect, useMemo, useState } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
+ import useSwrImmutable from 'swr/immutable';
3
4
  import { type FetchResponse, openmrsFetch, setUserProperties, showSnackbar, useSession } from '@openmrs/esm-framework';
4
5
  import { useValidateLocationUuid } from '../login.resource';
5
- import useSwrImmutable from 'swr/immutable';
6
6
  import { type LocationResponse } from '../types';
7
7
 
8
8
  export function useDefaultLocation(isUpdateFlow: boolean) {
@@ -1,16 +1,15 @@
1
- /* eslint-disable */
2
- import { act, screen, waitFor } from '@testing-library/react';
1
+ import { screen, waitFor } from '@testing-library/react';
3
2
  import userEvent from '@testing-library/user-event';
4
3
  import {
5
4
  openmrsFetch,
6
- useConfig,
7
- useSession,
8
5
  setSessionLocation,
9
6
  setUserProperties,
10
7
  showSnackbar,
11
- LoggedInUser,
12
- Session,
13
- FetchResponse,
8
+ useConfig,
9
+ useSession,
10
+ type LoggedInUser,
11
+ type Session,
12
+ type FetchResponse,
14
13
  } from '@openmrs/esm-framework';
15
14
  import {
16
15
  mockLoginLocations,
@@ -37,6 +36,9 @@ const userUuid = '90bd24b3-e700-46b0-a5ef-c85afdfededd';
37
36
  const mockOpenmrsFetch = jest.mocked(openmrsFetch);
38
37
  const mockUseConfig = jest.mocked(useConfig);
39
38
  const mockUseSession = jest.mocked(useSession);
39
+ const mockSetSessionLocation = jest.mocked(setSessionLocation);
40
+ const mockSetUserProperties = jest.mocked(setUserProperties);
41
+ const mockShowSnackbar = jest.mocked(showSnackbar);
40
42
 
41
43
  describe('LocationPickerView', () => {
42
44
  beforeEach(() => {
@@ -50,83 +52,110 @@ describe('LocationPickerView', () => {
50
52
  } as LoggedInUser,
51
53
  } as Session);
52
54
 
53
- mockOpenmrsFetch.mockImplementation((url) => {
54
- if (url === `/ws/fhir2/R4/Location?_id=${fistLocation.uuid}`) {
55
- return Promise.resolve(validatingLocationSuccessResponse as FetchResponse<unknown>);
56
- }
57
- if (url === `/ws/fhir2/R4/Location?_id=${invalidLocationUuid}`) {
58
- return Promise.resolve(validatingLocationFailureResponse as FetchResponse<unknown>);
59
- }
60
- return Promise.resolve(mockLoginLocations as FetchResponse<unknown>);
61
- });
55
+ const urlResponseMap: Record<string, FetchResponse<unknown>> = {
56
+ [`/ws/fhir2/R4/Location?_id=${fistLocation.uuid}`]: validatingLocationSuccessResponse as FetchResponse<unknown>,
57
+ [`/ws/fhir2/R4/Location?_id=${invalidLocationUuid}`]: validatingLocationFailureResponse as FetchResponse<unknown>,
58
+ };
59
+
60
+ mockOpenmrsFetch.mockImplementation(
61
+ async (url) => urlResponseMap[url] ?? (mockLoginLocations as FetchResponse<unknown>),
62
+ );
63
+
64
+ mockSetSessionLocation.mockResolvedValue(undefined);
62
65
  });
63
66
 
64
- it('renders the component properly', async () => {
65
- await act(async () => {
66
- renderWithRouter(LocationPickerView, {
67
- currentLocationUuid: 'some-location-uuid',
68
- hideWelcomeMessage: false,
69
- });
67
+ it('renders the welcome message and location selection form', () => {
68
+ renderWithRouter(LocationPickerView, {
69
+ currentLocationUuid: 'some-location-uuid',
70
+ hideWelcomeMessage: false,
70
71
  });
71
72
 
72
- expect(screen.queryByText(/welcome testy mctesterface/i)).toBeInTheDocument();
73
+ expect(screen.getByText(/welcome testy mctesterface/i)).toBeInTheDocument();
73
74
  expect(
74
- screen.queryByText(/select your location from the list below. use the search bar to find your location/i),
75
+ screen.getByText(/select your location from the list below. use the search bar to find your location/i),
75
76
  ).toBeInTheDocument();
76
- expect(screen.queryByRole('button', { name: /confirm/i })).toBeInTheDocument();
77
- expect(screen.getByRole('button', { name: /confirm/i })).toBeDisabled();
78
77
  });
79
78
 
80
- describe('Testing setting user preference workflow', () => {
81
- it('should save user preference if the user checks the checkbox and submit', async () => {
79
+ it('disables the confirm button when no location is selected', () => {
80
+ renderWithRouter(LocationPickerView, {});
81
+
82
+ const confirmButton = screen.getByRole('button', { name: /confirm/i });
83
+ expect(confirmButton).toBeDisabled();
84
+ });
85
+
86
+ it('enables the confirm button when a location is selected', async () => {
87
+ const user = userEvent.setup();
88
+ renderWithRouter(LocationPickerView, {});
89
+
90
+ const confirmButton = screen.getByRole('button', { name: /confirm/i });
91
+ expect(confirmButton).toBeDisabled();
92
+
93
+ const location = await screen.findByRole('radio', { name: fistLocation.name });
94
+ await user.click(location);
95
+
96
+ expect(confirmButton).toBeEnabled();
97
+ });
98
+
99
+ describe('Saving location preference', () => {
100
+ it('allows user to save their preferred location for future logins', async () => {
82
101
  const user = userEvent.setup();
83
102
 
84
- await act(async () => {
85
- renderWithRouter(LocationPickerView, {});
86
- });
103
+ renderWithRouter(LocationPickerView, {});
87
104
 
88
- const checkbox = screen.getByLabelText('Remember my location for future logins');
89
- const location = screen.getByRole('radio', { name: fistLocation.name });
90
- const submitButton = screen.getByText('Confirm');
105
+ const location = await screen.findByRole('radio', { name: fistLocation.name });
106
+ const checkbox = screen.getByLabelText(/remember my location for future logins/i);
107
+ const submitButton = screen.getByRole('button', { name: /confirm/i });
91
108
 
92
109
  await user.click(location);
110
+ expect(submitButton).toBeEnabled();
111
+
93
112
  await user.click(checkbox);
113
+ expect(checkbox).toBeChecked();
114
+
94
115
  await user.click(submitButton);
95
116
 
96
- expect(setSessionLocation).toHaveBeenCalledWith(fistLocation.uuid, expect.anything());
97
- expect(setUserProperties).toHaveBeenCalledWith(userUuid, {
117
+ await waitFor(() => {
118
+ expect(mockSetSessionLocation).toHaveBeenCalledWith(fistLocation.uuid, expect.anything());
119
+ });
120
+
121
+ expect(mockSetUserProperties).toHaveBeenCalledWith(userUuid, {
98
122
  defaultLocation: fistLocation.uuid,
99
123
  });
100
124
 
101
- await waitFor(() =>
102
- expect(showSnackbar).toHaveBeenCalledWith({
103
- isLowContrast: true,
104
- kind: 'success',
105
- subtitle: 'Your preferred location has been saved for future logins',
106
- title: 'Location saved',
107
- }),
108
- );
125
+ await waitFor(() => {
126
+ expect(mockShowSnackbar).toHaveBeenCalledWith(
127
+ expect.objectContaining({
128
+ kind: 'success',
129
+ title: 'Location saved',
130
+ subtitle: 'Your preferred location has been saved for future logins',
131
+ }),
132
+ );
133
+ });
109
134
  });
110
135
 
111
- it("should not save user preference if the user doesn't checks the checkbox and submit", async () => {
136
+ it('does not save preference when user submits without checking the checkbox', async () => {
112
137
  const user = userEvent.setup();
113
138
 
114
- await act(async () => {
115
- renderWithRouter(LocationPickerView, {});
116
- });
139
+ renderWithRouter(LocationPickerView, {});
117
140
 
118
141
  const location = await screen.findByRole('radio', { name: fistLocation.name });
119
- const submitButton = screen.getByText('Confirm');
142
+ const checkbox = screen.getByLabelText(/remember my location for future logins/i);
143
+ const submitButton = screen.getByRole('button', { name: /confirm/i });
120
144
 
121
145
  await user.click(location);
146
+ expect(checkbox).not.toBeChecked();
147
+
122
148
  await user.click(submitButton);
123
149
 
124
- expect(setSessionLocation).toHaveBeenCalledWith(fistLocation.uuid, expect.anything());
125
- expect(setUserProperties).not.toHaveBeenCalled();
126
- expect(showSnackbar).not.toHaveBeenCalled();
150
+ await waitFor(() => {
151
+ expect(mockSetSessionLocation).toHaveBeenCalledWith(fistLocation.uuid, expect.anything());
152
+ });
153
+
154
+ expect(mockSetUserProperties).not.toHaveBeenCalled();
155
+ expect(mockShowSnackbar).not.toHaveBeenCalled();
127
156
  });
128
157
 
129
- it('should redirect to home if user preference in the userProperties is present and the location preference is valid', async () => {
158
+ it('automatically redirects when user has a valid saved location preference', async () => {
130
159
  const validLocationUuid = fistLocation.uuid;
131
160
  mockUseSession.mockReturnValue({
132
161
  user: {
@@ -138,22 +167,16 @@ describe('LocationPickerView', () => {
138
167
  } as LoggedInUser,
139
168
  } as Session);
140
169
 
141
- await act(async () => {
142
- renderWithRouter(LocationPickerView, {});
143
- });
170
+ renderWithRouter(LocationPickerView, {});
144
171
 
145
172
  await waitFor(() => {
146
- expect(setSessionLocation).toHaveBeenCalledWith(validLocationUuid, expect.anything());
173
+ expect(mockSetSessionLocation).toHaveBeenCalledWith(validLocationUuid, expect.anything());
147
174
  });
148
175
 
149
- // Since the user prop and the default login location is the same,
150
- // it shouldn't send a hit to the backend.
151
- expect(setUserProperties).not.toHaveBeenCalledWith(userUuid, {
152
- defaultLocation: validLocationUuid,
153
- });
176
+ expect(mockSetUserProperties).not.toHaveBeenCalled();
154
177
  });
155
178
 
156
- it('should not redirect to home if user preference in the userProperties is present and the location preference is invalid', async () => {
179
+ it('shows location picker when saved location preference is invalid', async () => {
157
180
  mockUseSession.mockReturnValue({
158
181
  user: {
159
182
  display: 'Testy McTesterface',
@@ -164,19 +187,18 @@ describe('LocationPickerView', () => {
164
187
  } as LoggedInUser,
165
188
  } as Session);
166
189
 
167
- await act(async () => {
168
- renderWithRouter(LocationPickerView, {});
169
- });
190
+ renderWithRouter(LocationPickerView, {});
170
191
 
171
- const checkbox = screen.getByLabelText('Remember my location for future logins');
192
+ const checkbox = screen.getByLabelText(/remember my location for future logins/i);
172
193
  expect(checkbox).toBeChecked();
173
194
 
174
- expect(setSessionLocation).not.toHaveBeenCalled();
195
+ expect(screen.getByRole('button', { name: /confirm/i })).toBeInTheDocument();
196
+ expect(mockSetSessionLocation).not.toHaveBeenCalled();
175
197
  });
176
198
  });
177
199
 
178
- describe('Testing updating user preference workflow', () => {
179
- it('should not redirect if the login location page has a searchParam `update`', async () => {
200
+ describe('Updating location preference', () => {
201
+ it('shows location picker when update=true is in URL params', () => {
180
202
  mockUseSession.mockReturnValue({
181
203
  user: {
182
204
  display: 'Testy McTesterface',
@@ -187,17 +209,16 @@ describe('LocationPickerView', () => {
187
209
  } as LoggedInUser,
188
210
  } as Session);
189
211
 
190
- await act(async () => {
191
- renderWithRouter(LocationPickerView, {}, { routes: ['?update=true'] });
192
- });
212
+ renderWithRouter(LocationPickerView, {}, { routes: ['?update=true'] });
193
213
 
194
- const checkbox = screen.getByLabelText('Remember my location for future logins');
214
+ const checkbox = screen.getByLabelText(/remember my location for future logins/i);
195
215
  expect(checkbox).toBeChecked();
196
216
 
197
- expect(setSessionLocation).not.toHaveBeenCalled();
217
+ expect(screen.getByRole('button', { name: /confirm/i })).toBeInTheDocument();
218
+ expect(mockSetSessionLocation).not.toHaveBeenCalled();
198
219
  });
199
220
 
200
- it('should remove the saved preference if the login location page has a searchParam `update=true` and when submitting the user unchecks the checkbox ', async () => {
221
+ it('allows user to remove saved preference by unchecking the checkbox', async () => {
201
222
  const user = userEvent.setup();
202
223
 
203
224
  mockUseSession.mockReturnValue({
@@ -210,38 +231,40 @@ describe('LocationPickerView', () => {
210
231
  } as LoggedInUser,
211
232
  } as Session);
212
233
 
213
- await act(async () => {
214
- renderWithRouter(LocationPickerView, {}, { routes: ['?update=true'] });
215
- });
234
+ renderWithRouter(LocationPickerView, {}, { routes: ['?update=true'] });
216
235
 
217
- const checkbox = screen.getByLabelText('Remember my location for future logins');
236
+ const checkbox = screen.getByLabelText(/remember my location for future logins/i);
218
237
  expect(checkbox).toBeChecked();
219
238
 
220
239
  const location = screen.getByRole('radio', { name: fistLocation.name });
221
240
  await user.click(location);
222
241
 
223
- expect(setSessionLocation).not.toHaveBeenCalled();
242
+ expect(mockSetSessionLocation).not.toHaveBeenCalled();
224
243
 
225
244
  await user.click(checkbox);
226
245
  expect(checkbox).not.toBeChecked();
227
246
 
228
- const submitButton = screen.getByText('Confirm');
247
+ const submitButton = screen.getByRole('button', { name: /confirm/i });
229
248
  await user.click(submitButton);
230
249
 
231
- expect(setSessionLocation).toHaveBeenCalledWith(fistLocation.uuid, expect.anything());
232
- expect(setUserProperties).toHaveBeenCalledWith(userUuid, {});
233
-
234
- await waitFor(() =>
235
- expect(showSnackbar).toHaveBeenCalledWith({
236
- isLowContrast: true,
237
- kind: 'success',
238
- title: 'Location preference removed',
239
- subtitle: 'You will need to select a location on each login',
240
- }),
241
- );
250
+ await waitFor(() => {
251
+ expect(mockSetSessionLocation).toHaveBeenCalledWith(fistLocation.uuid, expect.anything());
252
+ });
253
+
254
+ expect(mockSetUserProperties).toHaveBeenCalledWith(userUuid, {});
255
+
256
+ await waitFor(() => {
257
+ expect(mockShowSnackbar).toHaveBeenCalledWith(
258
+ expect.objectContaining({
259
+ kind: 'success',
260
+ title: 'Location preference removed',
261
+ subtitle: 'You will need to select a location on each login',
262
+ }),
263
+ );
264
+ });
242
265
  });
243
266
 
244
- it('should update the user preference with new selection', async () => {
267
+ it('allows user to update their preferred location', async () => {
245
268
  const user = userEvent.setup();
246
269
 
247
270
  mockUseSession.mockReturnValue({
@@ -254,35 +277,37 @@ describe('LocationPickerView', () => {
254
277
  } as LoggedInUser,
255
278
  } as Session);
256
279
 
257
- await act(async () => {
258
- renderWithRouter(LocationPickerView, {}, { routes: ['?update=true'] });
259
- });
280
+ renderWithRouter(LocationPickerView, {}, { routes: ['?update=true'] });
260
281
 
261
- const checkbox = screen.getByLabelText('Remember my location for future logins');
282
+ const checkbox = screen.getByLabelText(/remember my location for future logins/i);
262
283
  expect(checkbox).toBeChecked();
263
284
 
264
285
  const location = await screen.findByRole('radio', { name: secondLocation.name });
265
- const submitButton = screen.getByText('Confirm');
286
+ const submitButton = screen.getByRole('button', { name: /confirm/i });
266
287
 
267
288
  await user.click(location);
268
289
  await user.click(submitButton);
269
290
 
270
- expect(setSessionLocation).toHaveBeenCalledWith(secondLocation.uuid, expect.anything());
271
- expect(setUserProperties).toHaveBeenCalledWith(userUuid, {
291
+ await waitFor(() => {
292
+ expect(mockSetSessionLocation).toHaveBeenCalledWith(secondLocation.uuid, expect.anything());
293
+ });
294
+
295
+ expect(mockSetUserProperties).toHaveBeenCalledWith(userUuid, {
272
296
  defaultLocation: secondLocation.uuid,
273
297
  });
274
298
 
275
- await waitFor(() =>
276
- expect(showSnackbar).toHaveBeenCalledWith({
277
- isLowContrast: true,
278
- kind: 'success',
279
- title: 'Location updated',
280
- subtitle: 'Your preferred login location has been updated',
281
- }),
282
- );
299
+ await waitFor(() => {
300
+ expect(mockShowSnackbar).toHaveBeenCalledWith(
301
+ expect.objectContaining({
302
+ kind: 'success',
303
+ title: 'Location updated',
304
+ subtitle: 'Your preferred login location has been updated',
305
+ }),
306
+ );
307
+ });
283
308
  });
284
309
 
285
- it('should not update the user preference with same selection', async () => {
310
+ it('does not update preference when user selects the same location', async () => {
286
311
  const user = userEvent.setup();
287
312
 
288
313
  mockUseSession.mockReturnValue({
@@ -295,21 +320,22 @@ describe('LocationPickerView', () => {
295
320
  } as LoggedInUser,
296
321
  } as Session);
297
322
 
298
- await act(async () => {
299
- renderWithRouter(LocationPickerView, {}, { routes: ['?update=true'] });
300
- });
323
+ renderWithRouter(LocationPickerView, {}, { routes: ['?update=true'] });
301
324
 
302
- const checkbox = screen.getByLabelText('Remember my location for future logins');
325
+ const checkbox = screen.getByLabelText(/remember my location for future logins/i);
303
326
  expect(checkbox).toBeChecked();
304
327
 
305
- const communityOutreachLocation = await screen.findByRole('radio', { name: fistLocation.name });
306
- const submitButton = screen.getByText('Confirm');
328
+ const location = await screen.findByRole('radio', { name: fistLocation.name });
329
+ const submitButton = screen.getByRole('button', { name: /confirm/i });
307
330
 
308
- await user.click(communityOutreachLocation);
331
+ await user.click(location);
309
332
  await user.click(submitButton);
310
333
 
311
- expect(setSessionLocation).toHaveBeenCalledWith(fistLocation.uuid, expect.anything());
312
- expect(setUserProperties).not.toHaveBeenCalled();
334
+ await waitFor(() => {
335
+ expect(mockSetSessionLocation).toHaveBeenCalledWith(fistLocation.uuid, expect.anything());
336
+ });
337
+
338
+ expect(mockSetUserProperties).not.toHaveBeenCalled();
313
339
  });
314
340
  });
315
341
  });