@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,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';
|