@openedx/frontend-app-authn 1.0.0-alpha.0

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.
Files changed (177) hide show
  1. package/LICENSE +661 -0
  2. package/README.rst +204 -0
  3. package/package.json +79 -0
  4. package/src/Main.tsx +23 -0
  5. package/src/__mocks__/file.js +1 -0
  6. package/src/__mocks__/svg.js +1 -0
  7. package/src/app.scss +6 -0
  8. package/src/app.ts +43 -0
  9. package/src/base-container/components/default-layout/DefaultLayout.test.jsx +48 -0
  10. package/src/base-container/components/default-layout/LargeLayout.jsx +42 -0
  11. package/src/base-container/components/default-layout/MediumLayout.jsx +49 -0
  12. package/src/base-container/components/default-layout/SmallLayout.jsx +37 -0
  13. package/src/base-container/components/default-layout/index.jsx +3 -0
  14. package/src/base-container/components/default-layout/messages.js +16 -0
  15. package/src/base-container/components/image-layout/ExtraSmallLayout.jsx +31 -0
  16. package/src/base-container/components/image-layout/LargeLayout.jsx +32 -0
  17. package/src/base-container/components/image-layout/MediumLayout.jsx +32 -0
  18. package/src/base-container/components/image-layout/SmallLayout.jsx +31 -0
  19. package/src/base-container/components/image-layout/index.jsx +4 -0
  20. package/src/base-container/components/image-layout/index.scss +37 -0
  21. package/src/base-container/components/image-layout/messages.js +16 -0
  22. package/src/base-container/components/welcome-page-layout/LargeLayout.jsx +46 -0
  23. package/src/base-container/components/welcome-page-layout/MediumLayout.jsx +49 -0
  24. package/src/base-container/components/welcome-page-layout/SmallLayout.jsx +38 -0
  25. package/src/base-container/components/welcome-page-layout/index.jsx +3 -0
  26. package/src/base-container/components/welcome-page-layout/messages.js +21 -0
  27. package/src/base-container/index.jsx +70 -0
  28. package/src/base-container/tests/BaseContainer.test.jsx +44 -0
  29. package/src/common-components/EmbeddedRegistrationRoute.jsx +26 -0
  30. package/src/common-components/EnterpriseSSO.jsx +110 -0
  31. package/src/common-components/FormGroup.jsx +106 -0
  32. package/src/common-components/InstitutionLogistration.jsx +102 -0
  33. package/src/common-components/NotFoundPage.jsx +15 -0
  34. package/src/common-components/PasswordField.jsx +175 -0
  35. package/src/common-components/RedirectLogistration.jsx +94 -0
  36. package/src/common-components/RegisterFaIcons.jsx +8 -0
  37. package/src/common-components/SocialAuthProviders.jsx +75 -0
  38. package/src/common-components/ThirdPartyAuth.jsx +123 -0
  39. package/src/common-components/ThirdPartyAuthAlert.jsx +49 -0
  40. package/src/common-components/UnAuthOnlyRoute.jsx +41 -0
  41. package/src/common-components/data/actions.js +27 -0
  42. package/src/common-components/data/reducers.js +63 -0
  43. package/src/common-components/data/sagas.js +32 -0
  44. package/src/common-components/data/selectors.js +28 -0
  45. package/src/common-components/data/service.js +23 -0
  46. package/src/common-components/data/tests/reducer.test.js +82 -0
  47. package/src/common-components/data/tests/sagas.test.js +71 -0
  48. package/src/common-components/index.jsx +14 -0
  49. package/src/common-components/messages.jsx +127 -0
  50. package/src/common-components/tests/EmbeddedRegistrationRoute.test.jsx +76 -0
  51. package/src/common-components/tests/FormField.test.jsx +289 -0
  52. package/src/common-components/tests/SocialAuthProviders.test.jsx +75 -0
  53. package/src/common-components/tests/ThirdPartyAuthAlert.test.jsx +39 -0
  54. package/src/common-components/tests/UnAuthOnlyRoute.test.jsx +73 -0
  55. package/src/common-components/tests/__snapshots__/SocialAuthProviders.test.jsx.snap +156 -0
  56. package/src/common-components/tests/__snapshots__/ThirdPartyAuthAlert.test.jsx.snap +53 -0
  57. package/src/constants.ts +1 -0
  58. package/src/data/configureStore.js +33 -0
  59. package/src/data/constants.js +38 -0
  60. package/src/data/countries.ts +59 -0
  61. package/src/data/oneTrust.js +3 -0
  62. package/src/data/reducers.js +36 -0
  63. package/src/data/sagas.js +19 -0
  64. package/src/data/tests/cookies.test.js +42 -0
  65. package/src/data/tests/dataUtils.test.js +19 -0
  66. package/src/data/tests/reduxUtils.test.js +14 -0
  67. package/src/data/utils/cookies.js +12 -0
  68. package/src/data/utils/dataUtils.js +81 -0
  69. package/src/data/utils/index.js +11 -0
  70. package/src/data/utils/reduxUtils.js +34 -0
  71. package/src/data/utils/useMobileResponsive.js +24 -0
  72. package/src/field-renderer/FieldRenderer.jsx +156 -0
  73. package/src/field-renderer/index.jsx +2 -0
  74. package/src/field-renderer/tests/FieldRenderer.test.jsx +201 -0
  75. package/src/forgot-password/ForgotPasswordAlert.jsx +97 -0
  76. package/src/forgot-password/ForgotPasswordPage.jsx +193 -0
  77. package/src/forgot-password/data/actions.js +32 -0
  78. package/src/forgot-password/data/reducers.js +58 -0
  79. package/src/forgot-password/data/sagas.js +35 -0
  80. package/src/forgot-password/data/selectors.js +10 -0
  81. package/src/forgot-password/data/service.js +21 -0
  82. package/src/forgot-password/data/tests/reducers.test.js +34 -0
  83. package/src/forgot-password/data/tests/sagas.test.js +67 -0
  84. package/src/forgot-password/index.js +5 -0
  85. package/src/forgot-password/messages.js +128 -0
  86. package/src/forgot-password/tests/ForgotPasswordPage.test.jsx +269 -0
  87. package/src/i18n/index.js +25 -0
  88. package/src/index.ts +3 -0
  89. package/src/login/AccountActivationMessage.jsx +78 -0
  90. package/src/login/ChangePasswordPrompt.jsx +91 -0
  91. package/src/login/LoginFailure.jsx +231 -0
  92. package/src/login/LoginPage.jsx +368 -0
  93. package/src/login/data/actions.js +39 -0
  94. package/src/login/data/constants.js +20 -0
  95. package/src/login/data/reducers.js +76 -0
  96. package/src/login/data/sagas.js +45 -0
  97. package/src/login/data/service.js +24 -0
  98. package/src/login/data/tests/reducers.test.js +155 -0
  99. package/src/login/data/tests/sagas.test.js +110 -0
  100. package/src/login/index.js +5 -0
  101. package/src/login/messages.jsx +211 -0
  102. package/src/login/tests/AccountActivationMessage.test.jsx +122 -0
  103. package/src/login/tests/ChangePasswordPrompt.test.jsx +75 -0
  104. package/src/login/tests/LoginFailure.test.jsx +323 -0
  105. package/src/login/tests/LoginPage.test.jsx +839 -0
  106. package/src/logistration/Logistration.jsx +189 -0
  107. package/src/logistration/Logistration.test.jsx +275 -0
  108. package/src/progressive-profiling/ProgressiveProfiling.jsx +313 -0
  109. package/src/progressive-profiling/ProgressiveProfilingPageModal.jsx +56 -0
  110. package/src/progressive-profiling/data/actions.js +22 -0
  111. package/src/progressive-profiling/data/reducers.js +38 -0
  112. package/src/progressive-profiling/data/sagas.js +24 -0
  113. package/src/progressive-profiling/data/selectors.js +14 -0
  114. package/src/progressive-profiling/data/service.js +17 -0
  115. package/src/progressive-profiling/index.js +5 -0
  116. package/src/progressive-profiling/messages.jsx +57 -0
  117. package/src/progressive-profiling/tests/ProgressiveProfiling.test.jsx +322 -0
  118. package/src/register/RegistrationFields/CountryField/CountryField.jsx +164 -0
  119. package/src/register/RegistrationFields/CountryField/CountryField.test.jsx +204 -0
  120. package/src/register/RegistrationFields/CountryField/validator.js +32 -0
  121. package/src/register/RegistrationFields/EmailField/EmailField.jsx +140 -0
  122. package/src/register/RegistrationFields/EmailField/EmailField.test.jsx +236 -0
  123. package/src/register/RegistrationFields/EmailField/constants.js +131 -0
  124. package/src/register/RegistrationFields/EmailField/validator.js +126 -0
  125. package/src/register/RegistrationFields/EmailField/validator.test.js +39 -0
  126. package/src/register/RegistrationFields/HonorCodeField/HonorCode.jsx +104 -0
  127. package/src/register/RegistrationFields/HonorCodeField/HonorCode.test.jsx +70 -0
  128. package/src/register/RegistrationFields/NameField/NameField.jsx +68 -0
  129. package/src/register/RegistrationFields/NameField/NameField.test.jsx +146 -0
  130. package/src/register/RegistrationFields/NameField/validator.js +22 -0
  131. package/src/register/RegistrationFields/TermsOfServiceField/TermsOfService.jsx +58 -0
  132. package/src/register/RegistrationFields/TermsOfServiceField/TermsOfService.test.jsx +55 -0
  133. package/src/register/RegistrationFields/UsernameField/UsernameField.jsx +162 -0
  134. package/src/register/RegistrationFields/UsernameField/UsernameField.test.jsx +319 -0
  135. package/src/register/RegistrationFields/UsernameField/validator.js +16 -0
  136. package/src/register/RegistrationFields/index.js +6 -0
  137. package/src/register/RegistrationPage.jsx +428 -0
  138. package/src/register/RegistrationPage.test.jsx +854 -0
  139. package/src/register/components/ConfigurableRegistrationForm.jsx +244 -0
  140. package/src/register/components/RegistrationFailure.jsx +80 -0
  141. package/src/register/components/tests/ConfigurableRegistrationForm.test.jsx +401 -0
  142. package/src/register/components/tests/RegistrationFailure.test.jsx +212 -0
  143. package/src/register/components/tests/ThirdPartyAuth.test.jsx +439 -0
  144. package/src/register/data/actions.js +85 -0
  145. package/src/register/data/constants.js +13 -0
  146. package/src/register/data/reducers.js +140 -0
  147. package/src/register/data/sagas.js +58 -0
  148. package/src/register/data/selectors.js +34 -0
  149. package/src/register/data/service.js +45 -0
  150. package/src/register/data/tests/reducers.test.js +277 -0
  151. package/src/register/data/tests/sagas.test.js +239 -0
  152. package/src/register/data/tests/utils.test.js +98 -0
  153. package/src/register/data/utils.js +153 -0
  154. package/src/register/index.js +4 -0
  155. package/src/register/messages.jsx +206 -0
  156. package/src/reset-password/ResetPasswordFailure.jsx +55 -0
  157. package/src/reset-password/ResetPasswordPage.jsx +224 -0
  158. package/src/reset-password/ResetPasswordSuccess.jsx +19 -0
  159. package/src/reset-password/data/actions.js +50 -0
  160. package/src/reset-password/data/constants.js +16 -0
  161. package/src/reset-password/data/reducers.js +44 -0
  162. package/src/reset-password/data/sagas.js +67 -0
  163. package/src/reset-password/data/selectors.js +10 -0
  164. package/src/reset-password/data/service.js +61 -0
  165. package/src/reset-password/data/tests/sagas.test.js +185 -0
  166. package/src/reset-password/index.js +5 -0
  167. package/src/reset-password/messages.js +88 -0
  168. package/src/reset-password/tests/ResetPasswordPage.test.jsx +241 -0
  169. package/src/routes.jsx +79 -0
  170. package/src/sass/_base_component.scss +202 -0
  171. package/src/sass/_forgot_password.scss +3 -0
  172. package/src/sass/_login_page.scss +3 -0
  173. package/src/sass/_progressive_profiling_page.scss +30 -0
  174. package/src/sass/_registration.scss +123 -0
  175. package/src/sass/_reset_password.scss +3 -0
  176. package/src/sass/_style.scss +465 -0
  177. package/src/setupTest.js +68 -0
@@ -0,0 +1,401 @@
1
+ import { Provider } from 'react-redux';
2
+
3
+ import {
4
+ CurrentAppProvider, getLocale, injectIntl, IntlProvider, mergeAppConfig
5
+ } from '@openedx/frontend-base';
6
+ import { fireEvent, render } from '@testing-library/react';
7
+ import { BrowserRouter as Router } from 'react-router-dom';
8
+ import configureStore from 'redux-mock-store';
9
+
10
+ import { appId } from '../../../constants';
11
+ import { registerNewUser } from '../../data/actions';
12
+ import { FIELDS } from '../../data/constants';
13
+ import RegistrationPage from '../../RegistrationPage';
14
+ import ConfigurableRegistrationForm from '../ConfigurableRegistrationForm';
15
+
16
+ jest.mock('@openedx/frontend-base', () => ({
17
+ ...jest.requireActual('@openedx/frontend-base'),
18
+ sendPageEvent: jest.fn(),
19
+ sendTrackEvent: jest.fn(),
20
+ getLocale: jest.fn(),
21
+ }));
22
+
23
+ jest.mock('react-router-dom', () => ({
24
+ ...jest.requireActual('react-router-dom'),
25
+ useNavigate: jest.fn(),
26
+ }));
27
+
28
+ const IntlConfigurableRegistrationForm = injectIntl(ConfigurableRegistrationForm);
29
+ const IntlRegistrationPage = injectIntl(RegistrationPage);
30
+ const mockStore = configureStore();
31
+
32
+ describe('ConfigurableRegistrationForm', () => {
33
+ mergeAppConfig(appId, {
34
+ PRIVACY_POLICY: 'https://privacy-policy.com',
35
+ TOS_AND_HONOR_CODE: 'https://tos-and-honot-code.com',
36
+ });
37
+
38
+ let props = {};
39
+ let store = {};
40
+ const registrationFormData = {
41
+ configurableFormFields: {
42
+ marketingEmailsOptIn: true,
43
+ },
44
+ formFields: {
45
+ name: '', email: '', username: '', password: '',
46
+ },
47
+ emailSuggestion: {
48
+ suggestion: '', type: '',
49
+ },
50
+ errors: {
51
+ name: '', email: '', username: '', password: '',
52
+ },
53
+ };
54
+
55
+ const reduxWrapper = children => (
56
+ <IntlProvider locale="en">
57
+ <CurrentAppProvider appId={appId}>
58
+ <Provider store={store}>{children}</Provider>
59
+ </CurrentAppProvider>
60
+ </IntlProvider>
61
+ );
62
+
63
+ const routerWrapper = children => (
64
+ <Router>
65
+ {children}
66
+ </Router>
67
+ );
68
+
69
+ const thirdPartyAuthContext = {
70
+ currentProvider: null,
71
+ finishAuthUrl: null,
72
+ providers: [],
73
+ secondaryProviders: [],
74
+ pipelineUserDetails: null,
75
+ countryCode: null,
76
+ };
77
+
78
+ const initialState = {
79
+ register: {
80
+ registrationResult: { success: false, redirectUrl: '' },
81
+ registrationError: {},
82
+ registrationFormData,
83
+ usernameSuggestions: [],
84
+ },
85
+ commonComponents: {
86
+ thirdPartyAuthApiStatus: null,
87
+ thirdPartyAuthContext,
88
+ fieldDescriptions: {},
89
+ optionalFields: {
90
+ fields: {},
91
+ extended_profile: [],
92
+ },
93
+ },
94
+ };
95
+
96
+ beforeEach(() => {
97
+ store = mockStore(initialState);
98
+ props = {
99
+ email: '',
100
+ fieldDescriptions: {},
101
+ fieldErrors: {},
102
+ formFields: {},
103
+ setFieldErrors: jest.fn(),
104
+ setFormFields: jest.fn(),
105
+ registrationEmbedded: false,
106
+ autoSubmitRegistrationForm: false,
107
+ handleInstitutionLogin: jest.fn(),
108
+ institutionLogin: false,
109
+ };
110
+ window.location = { search: '' };
111
+ getLocale.mockImplementationOnce(() => ('en-us'));
112
+ });
113
+
114
+ afterEach(() => {
115
+ jest.clearAllMocks();
116
+ });
117
+
118
+ const populateRequiredFields = (getByLabelText, payload, isThirdPartyAuth = false) => {
119
+ fireEvent.change(getByLabelText('Full name'), { target: { value: payload.name, name: 'name' } });
120
+ fireEvent.change(getByLabelText('Public username'), { target: { value: payload.username, name: 'username' } });
121
+ fireEvent.change(getByLabelText('Email'), { target: { value: payload.email, name: 'email' } });
122
+
123
+ if (!isThirdPartyAuth) {
124
+ fireEvent.change(getByLabelText('Password'), { target: { value: payload.password, name: 'password' } });
125
+ }
126
+ };
127
+
128
+ describe('Test Configurable Fields', () => {
129
+ mergeAppConfig(appId, {
130
+ ENABLE_DYNAMIC_REGISTRATION_FIELDS: true,
131
+ });
132
+
133
+ it('should render fields returned by backend as field descriptions', () => {
134
+ props = {
135
+ ...props,
136
+ fieldDescriptions: {
137
+ profession: { name: 'profession', type: 'text', label: 'Profession' },
138
+ terms_of_service: {
139
+ name: FIELDS.TERMS_OF_SERVICE,
140
+ error_message: 'You must agree to the Terms and Service agreement of our site',
141
+ },
142
+ },
143
+ };
144
+
145
+ render(routerWrapper(reduxWrapper(
146
+ <IntlConfigurableRegistrationForm {...props} />,
147
+ )));
148
+
149
+ expect(document.querySelector('#profession')).toBeTruthy();
150
+ expect(document.querySelector('#tos')).toBeTruthy();
151
+ });
152
+
153
+ it('should check TOS and honor code fields if they exist when auto submitting register form', () => {
154
+ props = {
155
+ ...props,
156
+ formFields: {},
157
+ fieldDescriptions: {
158
+ terms_of_service: {
159
+ name: FIELDS.TERMS_OF_SERVICE,
160
+ error_message: 'You must agree to the Terms and Service agreement of our site',
161
+ },
162
+ honor_code: {
163
+ name: FIELDS.HONOR_CODE,
164
+ error_message: 'You must agree to the Honor Code agreement of our site',
165
+ },
166
+ },
167
+ autoSubmitRegistrationForm: true,
168
+ };
169
+
170
+ render(routerWrapper(reduxWrapper(
171
+ <IntlConfigurableRegistrationForm {...props} />,
172
+ )));
173
+
174
+ expect(props.setFormFields).toHaveBeenCalledTimes(2);
175
+ expect(props.setFormFields.mock.calls[0][0]()).toEqual({
176
+ [FIELDS.HONOR_CODE]: true,
177
+ });
178
+
179
+ expect(props.setFormFields.mock.calls[1][0]()).toEqual({
180
+ [FIELDS.TERMS_OF_SERVICE]: true,
181
+ });
182
+ });
183
+
184
+ it('should render fields returned by backend', () => {
185
+ store = mockStore({
186
+ ...initialState,
187
+ commonComponents: {
188
+ ...initialState.commonComponents,
189
+ fieldDescriptions: {
190
+ profession: { name: 'profession', type: 'text', label: 'Profession' },
191
+ terms_of_service: {
192
+ name: FIELDS.TERMS_OF_SERVICE,
193
+ error_message: 'You must agree to the Terms and Service agreement of our site',
194
+ },
195
+ },
196
+ },
197
+ });
198
+ render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
199
+ expect(document.querySelector('#profession')).toBeTruthy();
200
+ expect(document.querySelector('#tos')).toBeTruthy();
201
+ });
202
+
203
+ it('should submit form with fields returned by backend in payload', () => {
204
+ mergeAppConfig(appId, {
205
+ ENABLE_DYNAMIC_REGISTRATION_FIELDS: true,
206
+ });
207
+ getLocale.mockImplementation(() => ('en-us'));
208
+ jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
209
+ store = mockStore({
210
+ ...initialState,
211
+ commonComponents: {
212
+ ...initialState.commonComponents,
213
+ fieldDescriptions: {
214
+ profession: { name: 'profession', type: 'text', label: 'Profession' },
215
+ },
216
+ extendedProfile: ['profession'],
217
+ },
218
+ });
219
+
220
+ const payload = {
221
+ name: 'John Doe',
222
+ username: 'john_doe',
223
+ email: 'john.doe@example.com',
224
+ password: 'password1',
225
+ profession: 'Engineer',
226
+ total_registration_time: 0,
227
+ };
228
+
229
+ store.dispatch = jest.fn(store.dispatch);
230
+ const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
231
+
232
+ populateRequiredFields(getByLabelText, payload);
233
+
234
+ const professionInput = getByLabelText('Profession');
235
+ fireEvent.change(professionInput, { target: { value: 'Engineer', name: 'profession' } });
236
+
237
+ const submitButton = container.querySelector('button.btn-brand');
238
+
239
+ fireEvent.click(submitButton);
240
+
241
+ expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload }));
242
+ });
243
+
244
+ it('should show error messages for required fields on empty form submission', () => {
245
+ const professionError = 'Enter your profession';
246
+ const countryError = 'Select your country or region of residence';
247
+ const confirmEmailError = 'Enter your email';
248
+
249
+ store = mockStore({
250
+ ...initialState,
251
+ commonComponents: {
252
+ ...initialState.commonComponents,
253
+ fieldDescriptions: {
254
+ profession: {
255
+ name: 'profession', type: 'text', label: 'Profession', error_message: professionError,
256
+ },
257
+ confirm_email: {
258
+ name: 'confirm_email', type: 'text', label: 'Confirm Email', error_message: confirmEmailError,
259
+ },
260
+ country: { name: 'country' },
261
+ },
262
+ },
263
+ });
264
+
265
+ const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
266
+ const submitButton = container.querySelector('button.btn-brand');
267
+
268
+ fireEvent.click(submitButton);
269
+
270
+ const professionErrorElement = container.querySelector('#profession-error');
271
+ const countryErrorElement = container.querySelector('div[feedback-for="country"]');
272
+ const confirmEmailErrorElement = container.querySelector('#confirm_email-error');
273
+
274
+ expect(professionErrorElement.textContent).toEqual(professionError);
275
+ expect(countryErrorElement.textContent).toEqual(countryError);
276
+ expect(confirmEmailErrorElement.textContent).toEqual(confirmEmailError);
277
+ });
278
+
279
+ it('should show country field validation when country name is invalid', () => {
280
+ const invalidCountryError = 'Country must match with an option available in the dropdown.';
281
+
282
+ store = mockStore({
283
+ ...initialState,
284
+ commonComponents: {
285
+ ...initialState.commonComponents,
286
+ fieldDescriptions: {
287
+ country: { name: 'country' },
288
+ },
289
+ },
290
+ });
291
+ const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
292
+ const countryInput = container.querySelector('input[name="country"]');
293
+ fireEvent.change(countryInput, { target: { value: 'Pak', name: 'country' } });
294
+ fireEvent.blur(countryInput, { target: { value: 'Pak', name: 'country' } });
295
+
296
+ const submitButton = container.querySelector('button.btn-brand');
297
+ fireEvent.click(submitButton);
298
+
299
+ const countryErrorElement = container.querySelector('div[feedback-for="country"]');
300
+
301
+ expect(countryErrorElement.textContent).toEqual(invalidCountryError);
302
+ });
303
+
304
+ it('should show error if email and confirm email fields do not match', () => {
305
+ store = mockStore({
306
+ ...initialState,
307
+ commonComponents: {
308
+ ...initialState.commonComponents,
309
+ fieldDescriptions: {
310
+ confirm_email: {
311
+ name: 'confirm_email', type: 'text', label: 'Confirm Email',
312
+ },
313
+ },
314
+ },
315
+ });
316
+ const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
317
+
318
+ const emailInput = getByLabelText('Email');
319
+ const confirmEmailInput = getByLabelText('Confirm Email');
320
+
321
+ fireEvent.change(emailInput, { target: { value: 'test1@gmail.com', name: 'email' } });
322
+ fireEvent.blur(confirmEmailInput, { target: { value: 'test2@gmail.com', name: 'confirm_email' } });
323
+
324
+ const confirmEmailErrorElement = container.querySelector('div#confirm_email-error');
325
+
326
+ expect(confirmEmailErrorElement.textContent).toEqual('The email addresses do not match.');
327
+ });
328
+
329
+ it('should show error if email and confirm email fields do not match on submit click', () => {
330
+ const formPayload = {
331
+ name: 'Petro',
332
+ username: 'petro_qa',
333
+ email: 'petro@example.com',
334
+ password: 'password1',
335
+ country: 'Ukraine',
336
+ honor_code: true,
337
+ total_registration_time: 0,
338
+ };
339
+
340
+ store = mockStore({
341
+ ...initialState,
342
+ commonComponents: {
343
+ ...initialState.commonComponents,
344
+ fieldDescriptions: {
345
+ confirm_email: {
346
+ name: 'confirm_email', type: 'text', label: 'Confirm Email',
347
+ },
348
+ country: { name: 'country' },
349
+ },
350
+ },
351
+ });
352
+ const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
353
+
354
+ populateRequiredFields(getByLabelText, formPayload, true);
355
+ fireEvent.change(
356
+ getByLabelText('Confirm Email'),
357
+ { target: { value: 'test2@gmail.com', name: 'confirm_email' } },
358
+ );
359
+
360
+ const button = container.querySelector('button.btn-brand');
361
+ fireEvent.click(button);
362
+
363
+ const confirmEmailErrorElement = container.querySelector('div#confirm_email-error');
364
+ expect(confirmEmailErrorElement.textContent).toEqual('The email addresses do not match.');
365
+
366
+ const validationErrors = container.querySelector('#validation-errors');
367
+ expect(validationErrors.textContent).toContain(
368
+ "We couldn't create your account.Please check your responses and try again.",
369
+ );
370
+ });
371
+
372
+ it('should run validations for configurable focused field on form submission', () => {
373
+ const professionError = 'Enter your profession';
374
+ store = mockStore({
375
+ ...initialState,
376
+ commonComponents: {
377
+ ...initialState.commonComponents,
378
+ fieldDescriptions: {
379
+ profession: {
380
+ name: 'profession', type: 'text', label: 'Profession', error_message: professionError,
381
+ },
382
+ },
383
+ },
384
+ });
385
+
386
+ const { getByLabelText, container } = render(
387
+ routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
388
+ );
389
+
390
+ const professionInput = getByLabelText('Profession');
391
+ fireEvent.focus(professionInput);
392
+
393
+ const submitButton = container.querySelector('button.btn-brand');
394
+ fireEvent.click(submitButton);
395
+
396
+ const professionErrorElement = container.querySelector('#profession-error');
397
+
398
+ expect(professionErrorElement.textContent).toEqual(professionError);
399
+ });
400
+ });
401
+ });
@@ -0,0 +1,212 @@
1
+ import { Provider } from 'react-redux';
2
+
3
+ import {
4
+ configureI18n, getLocale, injectIntl, IntlProvider, mergeAppConfig
5
+ } from '@openedx/frontend-base';
6
+ import { render, screen } from '@testing-library/react';
7
+ import { BrowserRouter as Router } from 'react-router-dom';
8
+ import configureStore from 'redux-mock-store';
9
+
10
+ import {
11
+ FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_AUTHENTICATION_FAILURE, TPA_SESSION_EXPIRED,
12
+ } from '../../data/constants';
13
+ import { appId } from '../../../constants';
14
+ import RegistrationPage from '../../RegistrationPage';
15
+ import RegistrationFailureMessage from '../RegistrationFailure';
16
+
17
+ jest.mock('@openedx/frontend-base', () => ({
18
+ ...jest.requireActual('@openedx/frontend-base'),
19
+ sendPageEvent: jest.fn(),
20
+ sendTrackEvent: jest.fn(),
21
+ getLocale: jest.fn(),
22
+ }));
23
+
24
+ const IntlRegistrationPage = injectIntl(RegistrationPage);
25
+ const IntlRegistrationFailure = injectIntl(RegistrationFailureMessage);
26
+ const mockStore = configureStore();
27
+
28
+ jest.mock('react-router-dom', () => {
29
+ const mockNavigation = jest.fn();
30
+
31
+ // eslint-disable-next-line react/prop-types
32
+ const Navigate = ({ to }) => {
33
+ mockNavigation(to);
34
+ return <div />;
35
+ };
36
+
37
+ return {
38
+ ...jest.requireActual('react-router-dom'),
39
+ Navigate,
40
+ };
41
+ });
42
+
43
+ describe('RegistrationFailure', () => {
44
+ mergeAppConfig(appId, {
45
+ PRIVACY_POLICY: 'https://privacy-policy.com',
46
+ TOS_AND_HONOR_CODE: 'https://tos-and-honot-code.com',
47
+ USER_RETENTION_COOKIE_NAME: 'authn-returning-user',
48
+ });
49
+
50
+ let props = {};
51
+ let store = {};
52
+ const registrationFormData = {
53
+ configurableFormFields: {
54
+ marketingEmailsOptIn: true,
55
+ },
56
+ formFields: {
57
+ name: '', email: '', username: '', password: '',
58
+ },
59
+ emailSuggestion: {
60
+ suggestion: '', type: '',
61
+ },
62
+ errors: {
63
+ name: '', email: '', username: '', password: '',
64
+ },
65
+ };
66
+
67
+ const reduxWrapper = children => (
68
+ <IntlProvider locale="en">
69
+ <Provider store={store}>{children}</Provider>
70
+ </IntlProvider>
71
+ );
72
+
73
+ const routerWrapper = children => (
74
+ <Router>
75
+ {children}
76
+ </Router>
77
+ );
78
+
79
+ const thirdPartyAuthContext = {
80
+ currentProvider: null,
81
+ finishAuthUrl: null,
82
+ providers: [],
83
+ secondaryProviders: [],
84
+ pipelineUserDetails: null,
85
+ countryCode: null,
86
+ };
87
+
88
+ const initialState = {
89
+ register: {
90
+ registrationResult: { success: false, redirectUrl: '' },
91
+ registrationError: {},
92
+ registrationFormData,
93
+ usernameSuggestions: [],
94
+ },
95
+ commonComponents: {
96
+ thirdPartyAuthApiStatus: null,
97
+ thirdPartyAuthContext,
98
+ fieldDescriptions: {},
99
+ optionalFields: {
100
+ fields: {},
101
+ extended_profile: [],
102
+ },
103
+ },
104
+ };
105
+
106
+ beforeEach(() => {
107
+ store = mockStore(initialState);
108
+ configureI18n({
109
+ messages: { 'es-419': {}, de: {}, 'en-us': {} },
110
+ });
111
+ props = {
112
+ handleInstitutionLogin: jest.fn(),
113
+ institutionLogin: false,
114
+ };
115
+ window.location = { search: '' };
116
+ });
117
+
118
+ afterEach(() => {
119
+ jest.clearAllMocks();
120
+ });
121
+
122
+ describe('Test Registration Failure', () => {
123
+ getLocale.mockImplementation(() => ('en-us'));
124
+
125
+ it('should match internal server error message', () => {
126
+ const expectedMessage = 'We couldn\'t create your account.An error has occurred. Try refreshing the page, or check your internet connection.';
127
+ props = {
128
+ errorCode: INTERNAL_SERVER_ERROR,
129
+ failureCount: 0,
130
+ };
131
+
132
+ const { container } = render(reduxWrapper(<IntlRegistrationFailure {...props} />));
133
+
134
+ const alertHeading = container.querySelectorAll('div.alert-heading');
135
+ expect(alertHeading.length).toEqual(1);
136
+
137
+ const alert = container.querySelector('div.alert');
138
+ expect(alert.textContent).toContain(expectedMessage);
139
+ });
140
+
141
+ it('should match registration api rate limit error message', () => {
142
+ const expectedMessage = 'We couldn\'t create your account.Too many failed registration attempts. Try again later.';
143
+ props = {
144
+ errorCode: FORBIDDEN_REQUEST,
145
+ failureCount: 0,
146
+ };
147
+
148
+ const { container } = render(reduxWrapper(<IntlRegistrationFailure {...props} />));
149
+
150
+ const alertHeading = container.querySelectorAll('div.alert-heading');
151
+ expect(alertHeading.length).toEqual(1);
152
+
153
+ const alert = container.querySelector('div.alert');
154
+ expect(alert.textContent).toContain(expectedMessage);
155
+ });
156
+
157
+ it('should match tpa session expired error message', () => {
158
+ const expectedMessage = 'We couldn\'t create your account.Registration using Google has timed out.';
159
+ props = {
160
+ context: {
161
+ provider: 'Google',
162
+ },
163
+ errorCode: TPA_SESSION_EXPIRED,
164
+ failureCount: 0,
165
+ };
166
+
167
+ const { container } = render(reduxWrapper(<IntlRegistrationFailure {...props} />));
168
+
169
+ const alertHeading = container.querySelectorAll('div.alert-heading');
170
+ expect(alertHeading.length).toEqual(1);
171
+
172
+ const alert = container.querySelector('div.alert');
173
+ expect(alert.textContent).toContain(expectedMessage);
174
+ });
175
+
176
+ it('should match tpa authentication failed error message', () => {
177
+ const expectedMessageSubstring = 'We are sorry, you are not authorized to access';
178
+ props = {
179
+ context: {
180
+ provider: 'Google',
181
+ },
182
+ errorCode: TPA_AUTHENTICATION_FAILURE,
183
+ failureCount: 0,
184
+ };
185
+
186
+ const { container } = render(reduxWrapper(<IntlRegistrationFailure {...props} />));
187
+
188
+ const alertHeading = container.querySelectorAll('div.alert-heading');
189
+ expect(alertHeading.length).toEqual(1);
190
+
191
+ const alert = container.querySelector('div.alert');
192
+ expect(alert.textContent).toContain(expectedMessageSubstring);
193
+ });
194
+
195
+ it('should display error message based on the error code returned by API', () => {
196
+ store = mockStore({
197
+ ...initialState,
198
+ register: {
199
+ ...initialState.register,
200
+ registrationError: {
201
+ errorCode: INTERNAL_SERVER_ERROR,
202
+ },
203
+ },
204
+ });
205
+
206
+ render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
207
+ const validationError = screen.queryByText('An error has occurred. Try refreshing the page, or check your internet connection.');
208
+
209
+ expect(validationError).not.toBeNull();
210
+ });
211
+ });
212
+ });