@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,162 @@
1
+ import { useEffect } from 'react';
2
+ import { useDispatch, useSelector } from 'react-redux';
3
+
4
+ import { useIntl } from '@openedx/frontend-base';
5
+ import { Button, Icon, IconButton } from '@openedx/paragon';
6
+ import { Close } from '@openedx/paragon/icons';
7
+ import PropTypes from 'prop-types';
8
+
9
+ import { FormGroup } from '../../../common-components';
10
+ import {
11
+ clearRegistrationBackendError,
12
+ clearUsernameSuggestions,
13
+ fetchRealtimeValidations,
14
+ } from '../../data/actions';
15
+ import messages from '../../messages';
16
+ import validateUsername from './validator';
17
+
18
+ /**
19
+ * Username field wrapper. It accepts following handlers
20
+ * - handleChange for setting value change and
21
+ * - handleErrorChange for setting error
22
+ *
23
+ * It is responsible for
24
+ * - Rendering username suggestions
25
+ * - Setting and clearing username suggestions
26
+ * - Performing username field validations
27
+ * - clearing error on focus
28
+ * - setting value on change
29
+ */
30
+ const UsernameField = (props) => {
31
+ const { formatMessage } = useIntl();
32
+ const dispatch = useDispatch();
33
+
34
+ const {
35
+ value,
36
+ errorMessage,
37
+ handleChange,
38
+ handleErrorChange,
39
+ } = props;
40
+
41
+ let className = '';
42
+ let suggestedUsernameDiv = null;
43
+ let iconButton = null;
44
+ const usernameSuggestions = useSelector(state => state.register.usernameSuggestions);
45
+ const validationApiRateLimited = useSelector(state => state.register.validationApiRateLimited);
46
+
47
+ /**
48
+ * We need to remove the placeholder from the field, adding a space will do that.
49
+ * This is needed because we are placing the username suggestions on top of the field.
50
+ */
51
+ useEffect(() => {
52
+ if (usernameSuggestions.length && !value) {
53
+ handleChange({ target: { name: 'username', value: ' ' } });
54
+ }
55
+ }, [handleChange, usernameSuggestions, value]);
56
+
57
+ const handleOnBlur = (event) => {
58
+ const { value: username } = event.target;
59
+ const fieldError = validateUsername(username, formatMessage);
60
+ if (fieldError) {
61
+ handleErrorChange('username', fieldError);
62
+ } else if (!validationApiRateLimited) {
63
+ dispatch(fetchRealtimeValidations({ username }));
64
+ }
65
+ };
66
+
67
+ const handleOnChange = (event) => {
68
+ let username = event.target.value;
69
+ if (username.length > 30) {
70
+ return;
71
+ }
72
+ if (event.target.value.startsWith(' ')) {
73
+ username = username.trim();
74
+ }
75
+ handleChange({ target: { name: 'username', value: username } });
76
+ };
77
+
78
+ const handleOnFocus = (event) => {
79
+ const username = event.target.value;
80
+ dispatch(clearUsernameSuggestions());
81
+ // If we added a space character to username field to display the suggestion
82
+ // remove it before user enters the input. This is to ensure user doesn't
83
+ // have a space prefixed to the username.
84
+ if (username === ' ') {
85
+ handleChange({ target: { name: 'username', value: '' } });
86
+ }
87
+ handleErrorChange('username', '');
88
+ dispatch(clearRegistrationBackendError('username'));
89
+ };
90
+
91
+ const handleSuggestionClick = (event, suggestion = '') => {
92
+ event.preventDefault();
93
+ handleErrorChange('username', ''); // clear error
94
+ handleChange({ target: { name: 'username', value: suggestion } }); // to set suggestion as value
95
+ dispatch(clearUsernameSuggestions());
96
+ };
97
+
98
+ const handleUsernameSuggestionClose = () => {
99
+ handleChange({ target: { name: 'username', value: '' } }); // to remove space in field
100
+ dispatch(clearUsernameSuggestions());
101
+ };
102
+
103
+ const suggestedUsernames = () => (
104
+ <div className={className}>
105
+ <span className="text-gray username-suggestion--label">{formatMessage(messages['registration.username.suggestion.label'])}</span>
106
+ <div className="username-scroll-suggested--form-field">
107
+ {usernameSuggestions.map((username, index) => (
108
+ <Button
109
+ type="button"
110
+ name="username"
111
+ variant="outline-dark"
112
+ className="username-suggestions--chip data-hj-suppress"
113
+ autoComplete={props.autoComplete}
114
+ key={`suggestion-${index.toString()}`}
115
+ onClick={(e) => handleSuggestionClick(e, username)}
116
+ >
117
+ {username}
118
+ </Button>
119
+ ))}
120
+ </div>
121
+ {iconButton}
122
+ </div>
123
+ );
124
+
125
+ if (usernameSuggestions.length > 0 && errorMessage && value === ' ') {
126
+ className = 'username-suggestions__error';
127
+ iconButton = <IconButton src={Close} iconAs={Icon} alt="Close" onClick={() => handleUsernameSuggestionClose()} variant="black" size="sm" className="username-suggestions__close__button" />;
128
+ suggestedUsernameDiv = suggestedUsernames();
129
+ } else if (usernameSuggestions.length > 0 && value === ' ') {
130
+ className = 'username-suggestions d-flex align-items-center';
131
+ iconButton = <IconButton src={Close} iconAs={Icon} alt="Close" onClick={() => handleUsernameSuggestionClose()} variant="black" size="sm" className="username-suggestions__close__button" />;
132
+ suggestedUsernameDiv = suggestedUsernames();
133
+ } else if (usernameSuggestions.length > 0 && errorMessage) {
134
+ suggestedUsernameDiv = suggestedUsernames();
135
+ }
136
+ return (
137
+ <FormGroup
138
+ {...props}
139
+ handleChange={handleOnChange}
140
+ handleFocus={handleOnFocus}
141
+ handleBlur={handleOnBlur}
142
+ >
143
+ {suggestedUsernameDiv}
144
+ </FormGroup>
145
+ );
146
+ };
147
+
148
+ UsernameField.defaultProps = {
149
+ errorMessage: '',
150
+ autoComplete: null,
151
+ };
152
+
153
+ UsernameField.propTypes = {
154
+ handleChange: PropTypes.func.isRequired,
155
+ handleErrorChange: PropTypes.func.isRequired,
156
+ errorMessage: PropTypes.string,
157
+ name: PropTypes.string.isRequired,
158
+ value: PropTypes.string.isRequired,
159
+ autoComplete: PropTypes.string,
160
+ };
161
+
162
+ export default UsernameField;
@@ -0,0 +1,319 @@
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 { clearRegistrationBackendError, clearUsernameSuggestions, fetchRealtimeValidations } from '../../data/actions';
9
+ import { UsernameField } from '../index';
10
+
11
+ const IntlUsernameField = injectIntl(UsernameField);
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('UsernameField', () => {
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
+ usernameSuggestions: [],
49
+ },
50
+ };
51
+
52
+ beforeEach(() => {
53
+ store = mockStore(initialState);
54
+ props = {
55
+ name: 'username',
56
+ value: '',
57
+ errorMessage: '',
58
+ handleChange: jest.fn(),
59
+ handleErrorChange: jest.fn(),
60
+ floatingLabel: '',
61
+ };
62
+ window.location = { search: '' };
63
+ });
64
+
65
+ afterEach(() => {
66
+ jest.clearAllMocks();
67
+ });
68
+
69
+ describe('Test Username Field', () => {
70
+ const fieldValidation = {
71
+ username: 'Username must be between 2 and 30 characters',
72
+ };
73
+
74
+ it('should run username field validation when onBlur is fired', () => {
75
+ const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
76
+
77
+ const usernameField = container.querySelector('input#username');
78
+ fireEvent.blur(usernameField, { target: { value: '', name: 'username' } });
79
+
80
+ expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
81
+ expect(props.handleErrorChange).toHaveBeenCalledWith(
82
+ 'username',
83
+ fieldValidation.username,
84
+ );
85
+ });
86
+
87
+ it('should update errors for frontend validations', () => {
88
+ const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
89
+
90
+ const usernameField = container.querySelector('input#username');
91
+ fireEvent.blur(usernameField, { target: { value: 'user#', name: 'username' } });
92
+
93
+ expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
94
+ expect(props.handleErrorChange).toHaveBeenCalledWith(
95
+ 'username',
96
+ 'Usernames can only contain letters (A-Z, a-z), numerals (0-9), underscores (_), and hyphens (-). Usernames cannot contain spaces',
97
+ );
98
+ });
99
+
100
+ it('should clear error on focus', () => {
101
+ const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
102
+
103
+ const usernameField = container.querySelector('input#username');
104
+ fireEvent.focus(usernameField, { target: { value: '', name: 'username' } });
105
+
106
+ expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
107
+ expect(props.handleErrorChange).toHaveBeenCalledWith(
108
+ 'username',
109
+ '',
110
+ );
111
+ });
112
+
113
+ it('should remove space from field on focus if space exists', () => {
114
+ const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
115
+
116
+ const usernameField = container.querySelector('input#username');
117
+ fireEvent.focus(usernameField, { target: { value: ' ', name: 'username' } });
118
+
119
+ expect(props.handleChange).toHaveBeenCalledTimes(1);
120
+ expect(props.handleChange).toHaveBeenCalledWith(
121
+ { target: { name: 'username', value: '' } },
122
+ );
123
+ });
124
+
125
+ it('should call backend validation api on blur event, if frontend validations have passed', () => {
126
+ store.dispatch = jest.fn(store.dispatch);
127
+ const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
128
+
129
+ const usernameField = container.querySelector('input#username');
130
+ // Enter a valid username so that frontend validations are passed
131
+ fireEvent.blur(usernameField, { target: { value: 'test', name: 'username' } });
132
+
133
+ expect(store.dispatch).toHaveBeenCalledWith(fetchRealtimeValidations({ username: 'test' }));
134
+ });
135
+
136
+ it('should remove space from the start of username on change', () => {
137
+ const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
138
+ const usernameField = container.querySelector('input#username');
139
+ fireEvent.change(usernameField, { target: { value: ' test-user', name: 'username' } });
140
+
141
+ expect(props.handleChange).toHaveBeenCalledTimes(1);
142
+ expect(props.handleChange).toHaveBeenCalledWith(
143
+ { target: { name: 'username', value: 'test-user' } },
144
+ );
145
+ });
146
+
147
+ it('should not set username if it is more than 30 character long', () => {
148
+ const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
149
+
150
+ const usernameField = container.querySelector('input#username');
151
+ fireEvent.change(usernameField, { target: { value: 'why_this_is_not_valid_username_', name: 'username' } });
152
+
153
+ expect(props.handleChange).toHaveBeenCalledTimes(0);
154
+ });
155
+
156
+ it('should clear username suggestions when username field is focused in', () => {
157
+ store.dispatch = jest.fn(store.dispatch);
158
+
159
+ const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
160
+
161
+ const usernameField = container.querySelector('input#username');
162
+ fireEvent.focus(usernameField);
163
+
164
+ expect(store.dispatch).toHaveBeenCalledWith(clearUsernameSuggestions());
165
+ });
166
+
167
+ it('should show username suggestions in case of conflict with an existing username', () => {
168
+ store = mockStore({
169
+ ...initialState,
170
+ register: {
171
+ ...initialState.register,
172
+ usernameSuggestions: ['test_1', 'test_12', 'test_123'],
173
+ },
174
+ });
175
+
176
+ props = {
177
+ ...props,
178
+ errorMessage: 'It looks like this username is already taken',
179
+ };
180
+
181
+ const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
182
+ const usernameSuggestions = container.querySelectorAll('button.username-suggestions--chip');
183
+ expect(usernameSuggestions.length).toEqual(3);
184
+ });
185
+
186
+ it('should show username suggestions when they are populated in redux', () => {
187
+ store = mockStore({
188
+ ...initialState,
189
+ register: {
190
+ ...initialState.register,
191
+ usernameSuggestions: ['test_1', 'test_12', 'test_123'],
192
+ },
193
+ });
194
+
195
+ props = {
196
+ ...props,
197
+ value: ' ',
198
+ };
199
+
200
+ const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
201
+ const usernameSuggestions = container.querySelectorAll('button.username-suggestions--chip');
202
+ expect(usernameSuggestions.length).toEqual(3);
203
+ });
204
+
205
+ it('should show username suggestions even if there is an error in field', () => {
206
+ store = mockStore({
207
+ ...initialState,
208
+ register: {
209
+ ...initialState.register,
210
+ usernameSuggestions: ['test_1', 'test_12', 'test_123'],
211
+ },
212
+ });
213
+
214
+ props = {
215
+ ...props,
216
+ value: ' ',
217
+ errorMessage: 'username error',
218
+ };
219
+
220
+ const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
221
+ const usernameSuggestions = container.querySelectorAll('button.username-suggestions--chip');
222
+ expect(usernameSuggestions.length).toEqual(3);
223
+ });
224
+
225
+ it('should put space in username field if suggestions are populated in redux', () => {
226
+ store = mockStore({
227
+ ...initialState,
228
+ register: {
229
+ ...initialState.register,
230
+ usernameSuggestions: ['test_1', 'test_12', 'test_123'],
231
+ },
232
+ });
233
+
234
+ render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
235
+ expect(props.handleChange).toHaveBeenCalledTimes(1);
236
+ expect(props.handleChange).toHaveBeenCalledWith(
237
+ { target: { name: 'username', value: ' ' } },
238
+ );
239
+ });
240
+
241
+ it('should set suggestion as username by clicking on it', () => {
242
+ store = mockStore({
243
+ ...initialState,
244
+ register: {
245
+ ...initialState.register,
246
+ usernameSuggestions: ['test_1', 'test_12', 'test_123'],
247
+ },
248
+ });
249
+
250
+ props = {
251
+ ...props,
252
+ value: ' ',
253
+ };
254
+
255
+ const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
256
+ const usernameSuggestion = container.querySelector('.username-suggestions--chip');
257
+ fireEvent.click(usernameSuggestion);
258
+ expect(props.handleChange).toHaveBeenCalledTimes(1);
259
+ expect(props.handleChange).toHaveBeenCalledWith(
260
+ { target: { name: 'username', value: 'test_1' } },
261
+ );
262
+ });
263
+
264
+ it('should clear username suggestions when close icon is clicked', () => {
265
+ store = mockStore({
266
+ ...initialState,
267
+ register: {
268
+ ...initialState.register,
269
+ usernameSuggestions: ['test_1', 'test_12', 'test_123'],
270
+ },
271
+ });
272
+ store.dispatch = jest.fn(store.dispatch);
273
+
274
+ props = {
275
+ ...props,
276
+ value: ' ',
277
+ };
278
+
279
+ const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
280
+ let closeButton = container.querySelector('button.username-suggestions__close__button');
281
+ fireEvent.click(closeButton);
282
+ expect(store.dispatch).toHaveBeenCalledWith(clearUsernameSuggestions());
283
+
284
+ props = {
285
+ ...props,
286
+ errorMessage: 'username error',
287
+ };
288
+
289
+ render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
290
+ closeButton = container.querySelector('button.username-suggestions__close__button');
291
+ fireEvent.click(closeButton);
292
+ expect(store.dispatch).toHaveBeenCalledWith(clearUsernameSuggestions());
293
+ });
294
+
295
+ it('should clear the registration validation error on focus on field', () => {
296
+ store.dispatch = jest.fn(store.dispatch);
297
+
298
+ const usernameError = 'It looks like this username is already taken';
299
+ store = mockStore({
300
+ ...initialState,
301
+ register: {
302
+ ...initialState.register,
303
+ registrationError: {
304
+ username: [{ userMessage: usernameError }],
305
+ },
306
+ },
307
+ });
308
+
309
+ store.dispatch = jest.fn(store.dispatch);
310
+
311
+ const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
312
+
313
+ const usernameField = container.querySelector('input#username');
314
+ fireEvent.focus(usernameField, { target: { value: 'test', name: 'username' } });
315
+
316
+ expect(store.dispatch).toHaveBeenCalledWith(clearRegistrationBackendError('username'));
317
+ });
318
+ });
319
+ });
@@ -0,0 +1,16 @@
1
+ import messages from '../../messages';
2
+
3
+ export const VALID_USERNAME_REGEX = /^[a-zA-Z0-9_-]*$/i;
4
+ export const usernameRegex = new RegExp(VALID_USERNAME_REGEX, 'i');
5
+
6
+ const validateUsername = (value, formatMessage) => {
7
+ let fieldError = '';
8
+ if (!value || value.length <= 1 || value.length > 30) {
9
+ fieldError = formatMessage(messages['username.validation.message']);
10
+ } else if (!usernameRegex.test(value)) {
11
+ fieldError = formatMessage(messages['username.format.validation.message']);
12
+ }
13
+ return fieldError;
14
+ };
15
+
16
+ export default validateUsername;
@@ -0,0 +1,6 @@
1
+ export { default as NameField } from './NameField/NameField';
2
+ export { default as EmailField } from './EmailField/EmailField';
3
+ export { default as UsernameField } from './UsernameField/UsernameField';
4
+ export { default as CountryField } from './CountryField/CountryField';
5
+ export { default as HonorCode } from './HonorCodeField/HonorCode';
6
+ export { default as TermsOfService } from './TermsOfServiceField/TermsOfService';