@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.
- package/LICENSE +661 -0
- package/README.rst +204 -0
- package/package.json +79 -0
- package/src/Main.tsx +23 -0
- package/src/__mocks__/file.js +1 -0
- package/src/__mocks__/svg.js +1 -0
- package/src/app.scss +6 -0
- package/src/app.ts +43 -0
- package/src/base-container/components/default-layout/DefaultLayout.test.jsx +48 -0
- package/src/base-container/components/default-layout/LargeLayout.jsx +42 -0
- package/src/base-container/components/default-layout/MediumLayout.jsx +49 -0
- package/src/base-container/components/default-layout/SmallLayout.jsx +37 -0
- package/src/base-container/components/default-layout/index.jsx +3 -0
- package/src/base-container/components/default-layout/messages.js +16 -0
- package/src/base-container/components/image-layout/ExtraSmallLayout.jsx +31 -0
- package/src/base-container/components/image-layout/LargeLayout.jsx +32 -0
- package/src/base-container/components/image-layout/MediumLayout.jsx +32 -0
- package/src/base-container/components/image-layout/SmallLayout.jsx +31 -0
- package/src/base-container/components/image-layout/index.jsx +4 -0
- package/src/base-container/components/image-layout/index.scss +37 -0
- package/src/base-container/components/image-layout/messages.js +16 -0
- package/src/base-container/components/welcome-page-layout/LargeLayout.jsx +46 -0
- package/src/base-container/components/welcome-page-layout/MediumLayout.jsx +49 -0
- package/src/base-container/components/welcome-page-layout/SmallLayout.jsx +38 -0
- package/src/base-container/components/welcome-page-layout/index.jsx +3 -0
- package/src/base-container/components/welcome-page-layout/messages.js +21 -0
- package/src/base-container/index.jsx +70 -0
- package/src/base-container/tests/BaseContainer.test.jsx +44 -0
- package/src/common-components/EmbeddedRegistrationRoute.jsx +26 -0
- package/src/common-components/EnterpriseSSO.jsx +110 -0
- package/src/common-components/FormGroup.jsx +106 -0
- package/src/common-components/InstitutionLogistration.jsx +102 -0
- package/src/common-components/NotFoundPage.jsx +15 -0
- package/src/common-components/PasswordField.jsx +175 -0
- package/src/common-components/RedirectLogistration.jsx +94 -0
- package/src/common-components/RegisterFaIcons.jsx +8 -0
- package/src/common-components/SocialAuthProviders.jsx +75 -0
- package/src/common-components/ThirdPartyAuth.jsx +123 -0
- package/src/common-components/ThirdPartyAuthAlert.jsx +49 -0
- package/src/common-components/UnAuthOnlyRoute.jsx +41 -0
- package/src/common-components/data/actions.js +27 -0
- package/src/common-components/data/reducers.js +63 -0
- package/src/common-components/data/sagas.js +32 -0
- package/src/common-components/data/selectors.js +28 -0
- package/src/common-components/data/service.js +23 -0
- package/src/common-components/data/tests/reducer.test.js +82 -0
- package/src/common-components/data/tests/sagas.test.js +71 -0
- package/src/common-components/index.jsx +14 -0
- package/src/common-components/messages.jsx +127 -0
- package/src/common-components/tests/EmbeddedRegistrationRoute.test.jsx +76 -0
- package/src/common-components/tests/FormField.test.jsx +289 -0
- package/src/common-components/tests/SocialAuthProviders.test.jsx +75 -0
- package/src/common-components/tests/ThirdPartyAuthAlert.test.jsx +39 -0
- package/src/common-components/tests/UnAuthOnlyRoute.test.jsx +73 -0
- package/src/common-components/tests/__snapshots__/SocialAuthProviders.test.jsx.snap +156 -0
- package/src/common-components/tests/__snapshots__/ThirdPartyAuthAlert.test.jsx.snap +53 -0
- package/src/constants.ts +1 -0
- package/src/data/configureStore.js +33 -0
- package/src/data/constants.js +38 -0
- package/src/data/countries.ts +59 -0
- package/src/data/oneTrust.js +3 -0
- package/src/data/reducers.js +36 -0
- package/src/data/sagas.js +19 -0
- package/src/data/tests/cookies.test.js +42 -0
- package/src/data/tests/dataUtils.test.js +19 -0
- package/src/data/tests/reduxUtils.test.js +14 -0
- package/src/data/utils/cookies.js +12 -0
- package/src/data/utils/dataUtils.js +81 -0
- package/src/data/utils/index.js +11 -0
- package/src/data/utils/reduxUtils.js +34 -0
- package/src/data/utils/useMobileResponsive.js +24 -0
- package/src/field-renderer/FieldRenderer.jsx +156 -0
- package/src/field-renderer/index.jsx +2 -0
- package/src/field-renderer/tests/FieldRenderer.test.jsx +201 -0
- package/src/forgot-password/ForgotPasswordAlert.jsx +97 -0
- package/src/forgot-password/ForgotPasswordPage.jsx +193 -0
- package/src/forgot-password/data/actions.js +32 -0
- package/src/forgot-password/data/reducers.js +58 -0
- package/src/forgot-password/data/sagas.js +35 -0
- package/src/forgot-password/data/selectors.js +10 -0
- package/src/forgot-password/data/service.js +21 -0
- package/src/forgot-password/data/tests/reducers.test.js +34 -0
- package/src/forgot-password/data/tests/sagas.test.js +67 -0
- package/src/forgot-password/index.js +5 -0
- package/src/forgot-password/messages.js +128 -0
- package/src/forgot-password/tests/ForgotPasswordPage.test.jsx +269 -0
- package/src/i18n/index.js +25 -0
- package/src/index.ts +3 -0
- package/src/login/AccountActivationMessage.jsx +78 -0
- package/src/login/ChangePasswordPrompt.jsx +91 -0
- package/src/login/LoginFailure.jsx +231 -0
- package/src/login/LoginPage.jsx +368 -0
- package/src/login/data/actions.js +39 -0
- package/src/login/data/constants.js +20 -0
- package/src/login/data/reducers.js +76 -0
- package/src/login/data/sagas.js +45 -0
- package/src/login/data/service.js +24 -0
- package/src/login/data/tests/reducers.test.js +155 -0
- package/src/login/data/tests/sagas.test.js +110 -0
- package/src/login/index.js +5 -0
- package/src/login/messages.jsx +211 -0
- package/src/login/tests/AccountActivationMessage.test.jsx +122 -0
- package/src/login/tests/ChangePasswordPrompt.test.jsx +75 -0
- package/src/login/tests/LoginFailure.test.jsx +323 -0
- package/src/login/tests/LoginPage.test.jsx +839 -0
- package/src/logistration/Logistration.jsx +189 -0
- package/src/logistration/Logistration.test.jsx +275 -0
- package/src/progressive-profiling/ProgressiveProfiling.jsx +313 -0
- package/src/progressive-profiling/ProgressiveProfilingPageModal.jsx +56 -0
- package/src/progressive-profiling/data/actions.js +22 -0
- package/src/progressive-profiling/data/reducers.js +38 -0
- package/src/progressive-profiling/data/sagas.js +24 -0
- package/src/progressive-profiling/data/selectors.js +14 -0
- package/src/progressive-profiling/data/service.js +17 -0
- package/src/progressive-profiling/index.js +5 -0
- package/src/progressive-profiling/messages.jsx +57 -0
- package/src/progressive-profiling/tests/ProgressiveProfiling.test.jsx +322 -0
- package/src/register/RegistrationFields/CountryField/CountryField.jsx +164 -0
- package/src/register/RegistrationFields/CountryField/CountryField.test.jsx +204 -0
- package/src/register/RegistrationFields/CountryField/validator.js +32 -0
- package/src/register/RegistrationFields/EmailField/EmailField.jsx +140 -0
- package/src/register/RegistrationFields/EmailField/EmailField.test.jsx +236 -0
- package/src/register/RegistrationFields/EmailField/constants.js +131 -0
- package/src/register/RegistrationFields/EmailField/validator.js +126 -0
- package/src/register/RegistrationFields/EmailField/validator.test.js +39 -0
- package/src/register/RegistrationFields/HonorCodeField/HonorCode.jsx +104 -0
- package/src/register/RegistrationFields/HonorCodeField/HonorCode.test.jsx +70 -0
- package/src/register/RegistrationFields/NameField/NameField.jsx +68 -0
- package/src/register/RegistrationFields/NameField/NameField.test.jsx +146 -0
- package/src/register/RegistrationFields/NameField/validator.js +22 -0
- package/src/register/RegistrationFields/TermsOfServiceField/TermsOfService.jsx +58 -0
- package/src/register/RegistrationFields/TermsOfServiceField/TermsOfService.test.jsx +55 -0
- package/src/register/RegistrationFields/UsernameField/UsernameField.jsx +162 -0
- package/src/register/RegistrationFields/UsernameField/UsernameField.test.jsx +319 -0
- package/src/register/RegistrationFields/UsernameField/validator.js +16 -0
- package/src/register/RegistrationFields/index.js +6 -0
- package/src/register/RegistrationPage.jsx +428 -0
- package/src/register/RegistrationPage.test.jsx +854 -0
- package/src/register/components/ConfigurableRegistrationForm.jsx +244 -0
- package/src/register/components/RegistrationFailure.jsx +80 -0
- package/src/register/components/tests/ConfigurableRegistrationForm.test.jsx +401 -0
- package/src/register/components/tests/RegistrationFailure.test.jsx +212 -0
- package/src/register/components/tests/ThirdPartyAuth.test.jsx +439 -0
- package/src/register/data/actions.js +85 -0
- package/src/register/data/constants.js +13 -0
- package/src/register/data/reducers.js +140 -0
- package/src/register/data/sagas.js +58 -0
- package/src/register/data/selectors.js +34 -0
- package/src/register/data/service.js +45 -0
- package/src/register/data/tests/reducers.test.js +277 -0
- package/src/register/data/tests/sagas.test.js +239 -0
- package/src/register/data/tests/utils.test.js +98 -0
- package/src/register/data/utils.js +153 -0
- package/src/register/index.js +4 -0
- package/src/register/messages.jsx +206 -0
- package/src/reset-password/ResetPasswordFailure.jsx +55 -0
- package/src/reset-password/ResetPasswordPage.jsx +224 -0
- package/src/reset-password/ResetPasswordSuccess.jsx +19 -0
- package/src/reset-password/data/actions.js +50 -0
- package/src/reset-password/data/constants.js +16 -0
- package/src/reset-password/data/reducers.js +44 -0
- package/src/reset-password/data/sagas.js +67 -0
- package/src/reset-password/data/selectors.js +10 -0
- package/src/reset-password/data/service.js +61 -0
- package/src/reset-password/data/tests/sagas.test.js +185 -0
- package/src/reset-password/index.js +5 -0
- package/src/reset-password/messages.js +88 -0
- package/src/reset-password/tests/ResetPasswordPage.test.jsx +241 -0
- package/src/routes.jsx +79 -0
- package/src/sass/_base_component.scss +202 -0
- package/src/sass/_forgot_password.scss +3 -0
- package/src/sass/_login_page.scss +3 -0
- package/src/sass/_progressive_profiling_page.scss +30 -0
- package/src/sass/_registration.scss +123 -0
- package/src/sass/_reset_password.scss +3 -0
- package/src/sass/_style.scss +465 -0
- package/src/setupTest.js +68 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { Provider } from 'react-redux';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
CurrentAppProvider,
|
|
5
|
+
configureI18n,
|
|
6
|
+
getAuthenticatedUser,
|
|
7
|
+
getSiteConfig,
|
|
8
|
+
identifyAuthenticatedUser,
|
|
9
|
+
injectIntl,
|
|
10
|
+
IntlProvider,
|
|
11
|
+
mergeAppConfig,
|
|
12
|
+
sendTrackEvent
|
|
13
|
+
} from '@openedx/frontend-base';
|
|
14
|
+
import {
|
|
15
|
+
fireEvent, render, screen,
|
|
16
|
+
} from '@testing-library/react';
|
|
17
|
+
import { MemoryRouter, mockNavigate, useLocation } from 'react-router-dom';
|
|
18
|
+
import configureStore from 'redux-mock-store';
|
|
19
|
+
|
|
20
|
+
import { appId } from '../../constants';
|
|
21
|
+
import {
|
|
22
|
+
AUTHN_PROGRESSIVE_PROFILING,
|
|
23
|
+
COMPLETE_STATE, DEFAULT_REDIRECT_URL,
|
|
24
|
+
EMBEDDED,
|
|
25
|
+
FAILURE_STATE,
|
|
26
|
+
PENDING_STATE,
|
|
27
|
+
} from '../../data/constants';
|
|
28
|
+
import { saveUserProfile } from '../data/actions';
|
|
29
|
+
import ProgressiveProfiling from '../ProgressiveProfiling';
|
|
30
|
+
|
|
31
|
+
const IntlProgressiveProfilingPage = injectIntl(ProgressiveProfiling);
|
|
32
|
+
const mockStore = configureStore();
|
|
33
|
+
|
|
34
|
+
jest.mock('@openedx/frontend-base', () => ({
|
|
35
|
+
...jest.requireActual('@openedx/frontend-base'),
|
|
36
|
+
sendPageEvent: jest.fn(),
|
|
37
|
+
sendTrackEvent: jest.fn(),
|
|
38
|
+
identifyAuthenticatedUser: jest.fn(),
|
|
39
|
+
configureAuth: jest.fn(),
|
|
40
|
+
getAuthenticatedUser: jest.fn(),
|
|
41
|
+
getLoggingService: jest.fn(),
|
|
42
|
+
}));
|
|
43
|
+
jest.mock('react-router-dom', () => {
|
|
44
|
+
const mockNavigation = jest.fn();
|
|
45
|
+
|
|
46
|
+
// eslint-disable-next-line react/prop-types
|
|
47
|
+
const Navigate = ({ to }) => {
|
|
48
|
+
mockNavigation(to);
|
|
49
|
+
return <div />;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
...jest.requireActual('react-router-dom'),
|
|
54
|
+
Navigate,
|
|
55
|
+
mockNavigate: mockNavigation,
|
|
56
|
+
useLocation: jest.fn(),
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('ProgressiveProfilingTests', () => {
|
|
61
|
+
let store = {};
|
|
62
|
+
|
|
63
|
+
const DASHBOARD_URL = getSiteConfig().lmsBaseUrl.concat(DEFAULT_REDIRECT_URL);
|
|
64
|
+
const registrationResult = { redirectUrl: getSiteConfig().lmsBaseUrl + DEFAULT_REDIRECT_URL, success: true };
|
|
65
|
+
const fields = {
|
|
66
|
+
company: { name: 'company', type: 'text', label: 'Company' },
|
|
67
|
+
gender: {
|
|
68
|
+
name: 'gender',
|
|
69
|
+
type: 'select',
|
|
70
|
+
label: 'Gender',
|
|
71
|
+
options: [['m', 'Male'], ['f', 'Female'], ['o', 'Other/Prefer Not to Say']],
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
const extendedProfile = ['company'];
|
|
75
|
+
const optionalFields = { fields, extended_profile: extendedProfile };
|
|
76
|
+
const initialState = {
|
|
77
|
+
welcomePage: {},
|
|
78
|
+
commonComponents: {
|
|
79
|
+
thirdPartyAuthApiStatus: null,
|
|
80
|
+
optionalFields: {},
|
|
81
|
+
thirdPartyAuthContext: {
|
|
82
|
+
welcomePageRedirectUrl: null,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const reduxWrapper = children => (
|
|
88
|
+
<IntlProvider locale="en">
|
|
89
|
+
<MemoryRouter>
|
|
90
|
+
<CurrentAppProvider appId={appId}>
|
|
91
|
+
<Provider store={store}>{children}</Provider>
|
|
92
|
+
</CurrentAppProvider>
|
|
93
|
+
</MemoryRouter>
|
|
94
|
+
</IntlProvider>
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
beforeEach(() => {
|
|
98
|
+
store = mockStore(initialState);
|
|
99
|
+
configureI18n({
|
|
100
|
+
messages: { 'es-419': {}, de: {}, 'en-us': {} },
|
|
101
|
+
});
|
|
102
|
+
useLocation.mockReturnValue({
|
|
103
|
+
state: {
|
|
104
|
+
registrationResult,
|
|
105
|
+
optionalFields,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
getAuthenticatedUser.mockReturnValue({ userId: 3, username: 'abc123', name: 'Test User' });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ******** test form links and modal ********
|
|
112
|
+
|
|
113
|
+
it('should not display button "Learn more about how we use this information."', () => {
|
|
114
|
+
mergeAppConfig(appId, {
|
|
115
|
+
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: '',
|
|
116
|
+
});
|
|
117
|
+
const { queryByRole } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
|
118
|
+
const button = queryByRole('button', { name: /learn more about how we use this information/i });
|
|
119
|
+
|
|
120
|
+
expect(button).toBeNull();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should display button "Learn more about how we use this information."', () => {
|
|
124
|
+
mergeAppConfig(appId, {
|
|
125
|
+
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: 'http://localhost:1999/support',
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const { getByText } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
|
129
|
+
|
|
130
|
+
const learnMoreButton = getByText('Learn more about how we use this information.');
|
|
131
|
+
|
|
132
|
+
expect(learnMoreButton).toBeDefined();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should open modal on pressing skip for now button', () => {
|
|
136
|
+
delete window.location;
|
|
137
|
+
window.location = { href: getSiteConfig().baseUrl.concat(AUTHN_PROGRESSIVE_PROFILING) };
|
|
138
|
+
const { getByRole } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
|
139
|
+
|
|
140
|
+
const skipButton = getByRole('button', { name: /skip for now/i });
|
|
141
|
+
fireEvent.click(skipButton);
|
|
142
|
+
|
|
143
|
+
const modalContentContainer = document.getElementsByClassName('.pgn__modal-content-container');
|
|
144
|
+
|
|
145
|
+
expect(modalContentContainer).toBeTruthy();
|
|
146
|
+
|
|
147
|
+
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', { host: '' });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ******** test event functionality ********
|
|
151
|
+
|
|
152
|
+
it('should make identify call to segment on progressive profiling page', () => {
|
|
153
|
+
render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
|
154
|
+
|
|
155
|
+
expect(identifyAuthenticatedUser).toHaveBeenCalledWith(3);
|
|
156
|
+
expect(identifyAuthenticatedUser).toHaveBeenCalled();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should send analytic event for support link click', () => {
|
|
160
|
+
mergeAppConfig(appId, {
|
|
161
|
+
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: 'http://localhost:1999/support',
|
|
162
|
+
});
|
|
163
|
+
render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
|
164
|
+
|
|
165
|
+
const supportLink = screen.getByRole('link', { name: /learn more about how we use this information/i });
|
|
166
|
+
fireEvent.click(supportLink);
|
|
167
|
+
|
|
168
|
+
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.support.link.clicked');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ******** test form submission ********
|
|
172
|
+
|
|
173
|
+
it('should show error message when patch request fails', () => {
|
|
174
|
+
store = mockStore({
|
|
175
|
+
...initialState,
|
|
176
|
+
welcomePage: {
|
|
177
|
+
...initialState.welcomePage,
|
|
178
|
+
showError: true,
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
|
183
|
+
const errorElement = container.querySelector('#pp-page-errors');
|
|
184
|
+
|
|
185
|
+
expect(errorElement).toBeTruthy();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// ******** miscellaneous tests ********
|
|
189
|
+
|
|
190
|
+
it('should redirect to login page if unauthenticated user tries to access welcome page', () => {
|
|
191
|
+
getAuthenticatedUser.mockReturnValue(null);
|
|
192
|
+
delete window.location;
|
|
193
|
+
window.location = {
|
|
194
|
+
assign: jest.fn().mockImplementation((value) => { window.location.href = value; }),
|
|
195
|
+
href: getSiteConfig().baseUrl,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
|
199
|
+
expect(window.location.href).toEqual(DASHBOARD_URL);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('Embedded Form Workflow Test', () => {
|
|
203
|
+
mergeAppConfig(appId, {
|
|
204
|
+
SEARCH_CATALOG_URL: 'http://localhost/search',
|
|
205
|
+
});
|
|
206
|
+
const host = 'http://example.com';
|
|
207
|
+
|
|
208
|
+
beforeEach(() => {
|
|
209
|
+
useLocation.mockReturnValue({
|
|
210
|
+
state: {},
|
|
211
|
+
});
|
|
212
|
+
store = mockStore({
|
|
213
|
+
...initialState,
|
|
214
|
+
commonComponents: {
|
|
215
|
+
...initialState.commonComponents,
|
|
216
|
+
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
|
217
|
+
optionalFields,
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should set host property value embedded host for on ramp experience for skip link event', () => {
|
|
223
|
+
delete window.location;
|
|
224
|
+
window.location = {
|
|
225
|
+
href: getSiteConfig().baseUrl.concat(AUTHN_PROGRESSIVE_PROFILING),
|
|
226
|
+
search: `?host=${host}&variant=${EMBEDDED}`,
|
|
227
|
+
};
|
|
228
|
+
render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
|
229
|
+
|
|
230
|
+
const skipLinkButton = screen.getByText('Skip for now');
|
|
231
|
+
fireEvent.click(skipLinkButton);
|
|
232
|
+
|
|
233
|
+
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', { host });
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should show spinner while fetching the optional fields', () => {
|
|
237
|
+
delete window.location;
|
|
238
|
+
window.location = {
|
|
239
|
+
assign: jest.fn().mockImplementation((value) => { window.location.href = value; }),
|
|
240
|
+
href: getSiteConfig().baseUrl.concat(AUTHN_PROGRESSIVE_PROFILING),
|
|
241
|
+
search: `?host=${host}&variant=${EMBEDDED}`,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
store = mockStore({
|
|
245
|
+
...initialState,
|
|
246
|
+
commonComponents: {
|
|
247
|
+
...initialState.commonComponents,
|
|
248
|
+
thirdPartyAuthApiStatus: PENDING_STATE,
|
|
249
|
+
optionalFields,
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
|
254
|
+
|
|
255
|
+
const tpaSpinnerElement = container.querySelector('#tpa-spinner');
|
|
256
|
+
expect(tpaSpinnerElement).toBeTruthy();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should render fields returned by backend API', () => {
|
|
260
|
+
delete window.location;
|
|
261
|
+
window.location = {
|
|
262
|
+
assign: jest.fn().mockImplementation((value) => { window.location.href = value; }),
|
|
263
|
+
href: getSiteConfig().baseUrl,
|
|
264
|
+
search: `?variant=${EMBEDDED}&host=${host}`,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
|
268
|
+
|
|
269
|
+
const genderField = container.querySelector('#gender');
|
|
270
|
+
expect(genderField).toBeTruthy();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should redirect to dashboard if API call to get form field fails', () => {
|
|
274
|
+
delete window.location;
|
|
275
|
+
window.location = {
|
|
276
|
+
assign: jest.fn().mockImplementation((value) => { window.location.href = value; }),
|
|
277
|
+
href: getSiteConfig().baseUrl,
|
|
278
|
+
search: `?variant=${EMBEDDED}`,
|
|
279
|
+
};
|
|
280
|
+
store = mockStore({
|
|
281
|
+
...initialState,
|
|
282
|
+
commonComponents: {
|
|
283
|
+
...initialState.commonComponents,
|
|
284
|
+
thirdPartyAuthApiStatus: FAILURE_STATE,
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
|
289
|
+
expect(window.location.href).toBe(DASHBOARD_URL);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should redirect to provided redirect url', () => {
|
|
293
|
+
const redirectUrl = 'https://redirect-test.com';
|
|
294
|
+
delete window.location;
|
|
295
|
+
window.location = {
|
|
296
|
+
assign: jest.fn().mockImplementation((value) => { window.location.href = value; }),
|
|
297
|
+
href: getSiteConfig().baseUrl,
|
|
298
|
+
search: `?variant=${EMBEDDED}&host=${host}&next=${redirectUrl}`,
|
|
299
|
+
};
|
|
300
|
+
store = mockStore({
|
|
301
|
+
...initialState,
|
|
302
|
+
commonComponents: {
|
|
303
|
+
...initialState.commonComponents,
|
|
304
|
+
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
|
305
|
+
optionalFields,
|
|
306
|
+
thirdPartyAuthContext: {
|
|
307
|
+
welcomePageRedirectUrl: redirectUrl,
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
welcomePage: {
|
|
311
|
+
...initialState.welcomePage,
|
|
312
|
+
success: true,
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
|
317
|
+
const submitButton = screen.getByText('Submit');
|
|
318
|
+
fireEvent.click(submitButton);
|
|
319
|
+
expect(window.location.href).toBe(redirectUrl);
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
3
|
+
|
|
4
|
+
import { useIntl } from '@openedx/frontend-base';
|
|
5
|
+
import { FormAutosuggest, FormAutosuggestOption, FormControlFeedback } from '@openedx/paragon';
|
|
6
|
+
import classNames from 'classnames';
|
|
7
|
+
import PropTypes from 'prop-types';
|
|
8
|
+
|
|
9
|
+
import { clearRegistrationBackendError } from '../../data/actions';
|
|
10
|
+
import messages from '../../messages';
|
|
11
|
+
import validateCountryField, { COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY } from './validator';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Country field wrapper. It accepts following handlers
|
|
15
|
+
* - handleChange for setting value change and
|
|
16
|
+
* - handleErrorChange for setting error
|
|
17
|
+
*
|
|
18
|
+
* It is responsible for
|
|
19
|
+
* - Auto populating country field if backendCountryCode is available in redux
|
|
20
|
+
* - Performing country field validations
|
|
21
|
+
* - clearing error on focus
|
|
22
|
+
* - setting value on change and selection
|
|
23
|
+
*/
|
|
24
|
+
const CountryField = (props) => {
|
|
25
|
+
const {
|
|
26
|
+
countryList,
|
|
27
|
+
selectedCountry,
|
|
28
|
+
onChangeHandler,
|
|
29
|
+
handleErrorChange,
|
|
30
|
+
onFocusHandler,
|
|
31
|
+
} = props;
|
|
32
|
+
const { formatMessage } = useIntl();
|
|
33
|
+
const dispatch = useDispatch();
|
|
34
|
+
|
|
35
|
+
const countryFieldValue = {
|
|
36
|
+
userProvidedText: selectedCountry.displayValue,
|
|
37
|
+
selectionValue: selectedCountry.displayValue,
|
|
38
|
+
selectionId: selectedCountry.countryCode,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const backendCountryCode = useSelector(state => state.register.backendCountryCode);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (backendCountryCode && backendCountryCode !== selectedCountry?.countryCode) {
|
|
45
|
+
let countryCode = '';
|
|
46
|
+
let countryDisplayValue = '';
|
|
47
|
+
|
|
48
|
+
const countryVal = countryList.find(
|
|
49
|
+
(country) => (country[COUNTRY_CODE_KEY].toLowerCase() === backendCountryCode.toLowerCase()),
|
|
50
|
+
);
|
|
51
|
+
if (countryVal) {
|
|
52
|
+
countryCode = countryVal[COUNTRY_CODE_KEY];
|
|
53
|
+
countryDisplayValue = countryVal[COUNTRY_DISPLAY_KEY];
|
|
54
|
+
}
|
|
55
|
+
onChangeHandler(
|
|
56
|
+
{ target: { name: 'country' } },
|
|
57
|
+
{ countryCode, displayValue: countryDisplayValue },
|
|
58
|
+
);
|
|
59
|
+
} else if (!selectedCountry.displayValue) {
|
|
60
|
+
onChangeHandler(
|
|
61
|
+
{ target: { name: 'country' } },
|
|
62
|
+
{ countryCode: '', displayValue: '' },
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}, [backendCountryCode, countryList]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
66
|
+
|
|
67
|
+
const handleOnBlur = (event) => {
|
|
68
|
+
// Do not run validations when drop-down arrow is clicked
|
|
69
|
+
if (event.relatedTarget && event.relatedTarget.className.includes('pgn__form-autosuggest__icon-button')) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const { value } = event.target;
|
|
74
|
+
|
|
75
|
+
const { error } = validateCountryField(
|
|
76
|
+
value.trim(),
|
|
77
|
+
countryList,
|
|
78
|
+
formatMessage(messages['empty.country.field.error']),
|
|
79
|
+
formatMessage(messages['invalid.country.field.error'])
|
|
80
|
+
);
|
|
81
|
+
handleErrorChange('country', error);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const handleOnFocus = (event) => {
|
|
85
|
+
handleErrorChange('country', '');
|
|
86
|
+
dispatch(clearRegistrationBackendError('country'));
|
|
87
|
+
onFocusHandler(event);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handleOnChange = (value) => {
|
|
91
|
+
onChangeHandler(
|
|
92
|
+
{ target: { name: 'country' } },
|
|
93
|
+
{ countryCode: value.selectionId, displayValue: value.userProvidedText }
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// We have put this check because proviously we also had onSelected event handler and we call
|
|
97
|
+
// the onBlur on that event handler but now there is no such handler and we only have
|
|
98
|
+
// onChange so we check the is there is proper sectionId which only be
|
|
99
|
+
// proper one when we select it from dropdown's item otherwise its null.
|
|
100
|
+
if (value.selectionId !== '') {
|
|
101
|
+
handleOnBlur({ target: { name: 'country', value: value.userProvidedText } });
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const getCountryList = () => countryList.map((country) => (
|
|
106
|
+
<FormAutosuggestOption key={country[COUNTRY_DISPLAY_KEY]} id={country[COUNTRY_CODE_KEY]}>
|
|
107
|
+
{country[COUNTRY_DISPLAY_KEY]}
|
|
108
|
+
</FormAutosuggestOption>
|
|
109
|
+
));
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div className="mb-4">
|
|
113
|
+
<FormAutosuggest
|
|
114
|
+
floatingLabel={formatMessage(messages['registration.country.label'])}
|
|
115
|
+
aria-label="form autosuggest"
|
|
116
|
+
name="country"
|
|
117
|
+
value={countryFieldValue || {}}
|
|
118
|
+
className={classNames({ 'form-field-error': props.errorMessage })}
|
|
119
|
+
onFocus={(e) => handleOnFocus(e)}
|
|
120
|
+
onBlur={(e) => handleOnBlur(e)}
|
|
121
|
+
onChange={(value) => handleOnChange(value)}
|
|
122
|
+
>
|
|
123
|
+
{getCountryList()}
|
|
124
|
+
</FormAutosuggest>
|
|
125
|
+
{props.errorMessage !== '' && (
|
|
126
|
+
<FormControlFeedback
|
|
127
|
+
key="error"
|
|
128
|
+
className="form-text-size"
|
|
129
|
+
hasIcon={false}
|
|
130
|
+
feedback-for="country"
|
|
131
|
+
type="invalid"
|
|
132
|
+
>
|
|
133
|
+
{props.errorMessage}
|
|
134
|
+
</FormControlFeedback>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
CountryField.propTypes = {
|
|
141
|
+
countryList: PropTypes.arrayOf(
|
|
142
|
+
PropTypes.shape({
|
|
143
|
+
code: PropTypes.string,
|
|
144
|
+
name: PropTypes.string,
|
|
145
|
+
}),
|
|
146
|
+
).isRequired,
|
|
147
|
+
errorMessage: PropTypes.string,
|
|
148
|
+
onChangeHandler: PropTypes.func.isRequired,
|
|
149
|
+
handleErrorChange: PropTypes.func.isRequired,
|
|
150
|
+
onFocusHandler: PropTypes.func.isRequired,
|
|
151
|
+
selectedCountry: PropTypes.shape({
|
|
152
|
+
displayValue: PropTypes.string,
|
|
153
|
+
countryCode: PropTypes.string,
|
|
154
|
+
}),
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
CountryField.defaultProps = {
|
|
158
|
+
errorMessage: null,
|
|
159
|
+
selectedCountry: {
|
|
160
|
+
value: '',
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export default CountryField;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { Provider } from 'react-redux';
|
|
2
|
+
|
|
3
|
+
import { injectIntl, IntlProvider } from '@openedx/frontend-base';
|
|
4
|
+
import { fireEvent, render } from '@testing-library/react';
|
|
5
|
+
import { BrowserRouter as Router } from 'react-router-dom';
|
|
6
|
+
import configureStore from 'redux-mock-store';
|
|
7
|
+
|
|
8
|
+
import { CountryField } from '../index';
|
|
9
|
+
import { COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY } from './validator';
|
|
10
|
+
|
|
11
|
+
const IntlCountryField = injectIntl(CountryField);
|
|
12
|
+
const mockStore = configureStore();
|
|
13
|
+
|
|
14
|
+
jest.mock('react-router-dom', () => {
|
|
15
|
+
const mockNavigation = jest.fn();
|
|
16
|
+
|
|
17
|
+
// eslint-disable-next-line react/prop-types
|
|
18
|
+
const Navigate = ({ to }) => {
|
|
19
|
+
mockNavigation(to);
|
|
20
|
+
return <div />;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
...jest.requireActual('react-router-dom'),
|
|
25
|
+
Navigate,
|
|
26
|
+
mockNavigate: mockNavigation,
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('CountryField', () => {
|
|
31
|
+
let props = {};
|
|
32
|
+
let store = {};
|
|
33
|
+
|
|
34
|
+
const reduxWrapper = children => (
|
|
35
|
+
<IntlProvider locale="en">
|
|
36
|
+
<Provider store={store}>{children}</Provider>
|
|
37
|
+
</IntlProvider>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const routerWrapper = children => (
|
|
41
|
+
<Router>
|
|
42
|
+
{children}
|
|
43
|
+
</Router>
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const initialState = {
|
|
47
|
+
register: {},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
store = mockStore(initialState);
|
|
52
|
+
props = {
|
|
53
|
+
countryList: [{
|
|
54
|
+
[COUNTRY_CODE_KEY]: 'PK',
|
|
55
|
+
[COUNTRY_DISPLAY_KEY]: 'Pakistan',
|
|
56
|
+
}],
|
|
57
|
+
selectedCountry: {
|
|
58
|
+
countryCode: '',
|
|
59
|
+
displayValue: '',
|
|
60
|
+
},
|
|
61
|
+
errorMessage: '',
|
|
62
|
+
onChangeHandler: jest.fn(),
|
|
63
|
+
handleErrorChange: jest.fn(),
|
|
64
|
+
onFocusHandler: jest.fn(),
|
|
65
|
+
};
|
|
66
|
+
window.location = { search: '' };
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
afterEach(() => {
|
|
70
|
+
jest.clearAllMocks();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('Test Country Field', () => {
|
|
74
|
+
const emptyFieldValidation = {
|
|
75
|
+
country: 'Select your country or region of residence',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
it('should run country field validation when onBlur is fired', () => {
|
|
79
|
+
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
|
80
|
+
const countryInput = container.querySelector('input[name="country"]');
|
|
81
|
+
|
|
82
|
+
fireEvent.blur(countryInput, {
|
|
83
|
+
target: { value: '', name: 'country' },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
|
|
87
|
+
expect(props.handleErrorChange).toHaveBeenCalledWith(
|
|
88
|
+
'country',
|
|
89
|
+
emptyFieldValidation.country,
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should run country field validation when country name is invalid', () => {
|
|
94
|
+
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
|
95
|
+
const countryInput = container.querySelector('input[name="country"]');
|
|
96
|
+
|
|
97
|
+
fireEvent.blur(countryInput, {
|
|
98
|
+
target: { value: 'Pak', name: 'country' },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
|
|
102
|
+
expect(props.handleErrorChange).toHaveBeenCalledWith(
|
|
103
|
+
'country',
|
|
104
|
+
'Country must match with an option available in the dropdown.',
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should not run country field validation when onBlur is fired by drop-down arrow icon click', () => {
|
|
109
|
+
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
|
110
|
+
const countryInput = container.querySelector('input[name="country"]');
|
|
111
|
+
const dropdownArrowIcon = container.querySelector('.btn-icon.pgn__form-autosuggest__icon-button');
|
|
112
|
+
|
|
113
|
+
fireEvent.blur(countryInput, {
|
|
114
|
+
target: { value: '', name: 'country' },
|
|
115
|
+
relatedTarget: dropdownArrowIcon,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(props.handleErrorChange).toHaveBeenCalledTimes(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should update errors for frontend validations', () => {
|
|
122
|
+
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
|
123
|
+
const countryInput = container.querySelector('input[name="country"]');
|
|
124
|
+
|
|
125
|
+
fireEvent.blur(countryInput, { target: { value: '', name: 'country' } });
|
|
126
|
+
|
|
127
|
+
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
|
|
128
|
+
expect(props.handleErrorChange).toHaveBeenCalledWith('country', emptyFieldValidation.country);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should clear error on focus', () => {
|
|
132
|
+
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
|
133
|
+
const countryInput = container.querySelector('input[name="country"]');
|
|
134
|
+
|
|
135
|
+
fireEvent.focus(countryInput);
|
|
136
|
+
|
|
137
|
+
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
|
|
138
|
+
expect(props.handleErrorChange).toHaveBeenCalledWith('country', '');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should update state from country code present in redux store', () => {
|
|
142
|
+
store = mockStore({
|
|
143
|
+
...initialState,
|
|
144
|
+
register: {
|
|
145
|
+
...initialState.register,
|
|
146
|
+
backendCountryCode: 'PK',
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
|
151
|
+
|
|
152
|
+
container.querySelector('input[name="country"]');
|
|
153
|
+
expect(props.onChangeHandler).toHaveBeenCalledTimes(1);
|
|
154
|
+
expect(props.onChangeHandler).toHaveBeenCalledWith(
|
|
155
|
+
{ target: { name: 'country' } },
|
|
156
|
+
{ countryCode: 'PK', displayValue: 'Pakistan' },
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should set option on dropdown menu item click', () => {
|
|
161
|
+
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
|
162
|
+
|
|
163
|
+
const dropdownButton = container.querySelector('.pgn__form-autosuggest__icon-button');
|
|
164
|
+
fireEvent.click(dropdownButton);
|
|
165
|
+
|
|
166
|
+
const dropdownItem = container.querySelector('.dropdown-item');
|
|
167
|
+
fireEvent.click(dropdownItem);
|
|
168
|
+
|
|
169
|
+
expect(props.onChangeHandler).toHaveBeenCalledTimes(2);
|
|
170
|
+
expect(props.onChangeHandler).toHaveBeenCalledWith(
|
|
171
|
+
{ target: { name: 'country' } },
|
|
172
|
+
{ countryCode: 'PK', displayValue: 'Pakistan' },
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should set value on change', () => {
|
|
177
|
+
const { container } = render(
|
|
178
|
+
routerWrapper(reduxWrapper(<IntlCountryField {...props} />)),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const countryInput = container.querySelector('input[name="country"]');
|
|
182
|
+
fireEvent.change(countryInput, { target: { value: 'pak', name: 'country' } });
|
|
183
|
+
|
|
184
|
+
expect(props.onChangeHandler).toHaveBeenCalledTimes(2);
|
|
185
|
+
expect(props.onChangeHandler).toHaveBeenCalledWith(
|
|
186
|
+
{ target: { name: 'country' } },
|
|
187
|
+
{ countryCode: '', displayValue: 'pak' },
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should display error on invalid country input', () => {
|
|
192
|
+
props = {
|
|
193
|
+
...props,
|
|
194
|
+
errorMessage: 'country error message',
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
|
198
|
+
|
|
199
|
+
const feedbackElement = container.querySelector('div[feedback-for="country"]');
|
|
200
|
+
expect(feedbackElement).toBeTruthy();
|
|
201
|
+
expect(feedbackElement.textContent).toEqual('country error message');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|