Compare commits
3 Commits
remove-com
...
sajjad/VAN
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e57994bd85 | ||
|
|
aeec576d8c | ||
|
|
90db7ba1d8 |
@@ -24,7 +24,7 @@ import { ForgotPasswordPage } from './forgot-password';
|
|||||||
import Logistration from './logistration/Logistration';
|
import Logistration from './logistration/Logistration';
|
||||||
import { ProgressiveProfiling } from './progressive-profiling';
|
import { ProgressiveProfiling } from './progressive-profiling';
|
||||||
import { RecommendationsPage } from './recommendations';
|
import { RecommendationsPage } from './recommendations';
|
||||||
import { RegistrationPage } from './register';
|
import { EmbeddableRegistrationPage } from './register';
|
||||||
import { ResetPasswordPage } from './reset-password';
|
import { ResetPasswordPage } from './reset-password';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
@@ -41,7 +41,7 @@ const MainApp = () => (
|
|||||||
<Route path="/" element={<Navigate replace to={updatePathWithQueryParams(REGISTER_PAGE)} />} />
|
<Route path="/" element={<Navigate replace to={updatePathWithQueryParams(REGISTER_PAGE)} />} />
|
||||||
<Route
|
<Route
|
||||||
path={REGISTER_EMBEDDED_PAGE}
|
path={REGISTER_EMBEDDED_PAGE}
|
||||||
element={<EmbeddedRegistrationRoute><RegistrationPage /></EmbeddedRegistrationRoute>}
|
element={<EmbeddedRegistrationRoute><EmbeddableRegistrationPage /></EmbeddedRegistrationRoute>}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={LOGIN_PAGE}
|
path={LOGIN_PAGE}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
import { Navigate } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS, REDIRECT,
|
AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS,
|
||||||
} from '../data/constants';
|
} from '../data/constants';
|
||||||
import { setCookie } from '../data/utils';
|
import { setCookie } from '../data/utils';
|
||||||
|
|
||||||
@@ -17,8 +17,6 @@ const RedirectLogistration = (props) => {
|
|||||||
redirectToRecommendationsPage,
|
redirectToRecommendationsPage,
|
||||||
educationLevel,
|
educationLevel,
|
||||||
userId,
|
userId,
|
||||||
registrationEmbedded,
|
|
||||||
host,
|
|
||||||
} = props;
|
} = props;
|
||||||
let finalRedirectUrl = '';
|
let finalRedirectUrl = '';
|
||||||
|
|
||||||
@@ -38,13 +36,6 @@ const RedirectLogistration = (props) => {
|
|||||||
// TODO: Do we still need this cookie?
|
// TODO: Do we still need this cookie?
|
||||||
setCookie('van-504-returning-user', true);
|
setCookie('van-504-returning-user', true);
|
||||||
|
|
||||||
if (registrationEmbedded) {
|
|
||||||
window.parent.postMessage({
|
|
||||||
action: REDIRECT,
|
|
||||||
redirectUrl: getConfig().POST_REGISTRATION_REDIRECT_URL,
|
|
||||||
}, host);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const registrationResult = { redirectUrl: finalRedirectUrl, success };
|
const registrationResult = { redirectUrl: finalRedirectUrl, success };
|
||||||
return (
|
return (
|
||||||
<Navigate
|
<Navigate
|
||||||
@@ -89,8 +80,6 @@ RedirectLogistration.defaultProps = {
|
|||||||
optionalFields: {},
|
optionalFields: {},
|
||||||
redirectToRecommendationsPage: false,
|
redirectToRecommendationsPage: false,
|
||||||
userId: null,
|
userId: null,
|
||||||
registrationEmbedded: false,
|
|
||||||
host: '',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
RedirectLogistration.propTypes = {
|
RedirectLogistration.propTypes = {
|
||||||
@@ -102,8 +91,6 @@ RedirectLogistration.propTypes = {
|
|||||||
optionalFields: PropTypes.shape({}),
|
optionalFields: PropTypes.shape({}),
|
||||||
redirectToRecommendationsPage: PropTypes.bool,
|
redirectToRecommendationsPage: PropTypes.bool,
|
||||||
userId: PropTypes.number,
|
userId: PropTypes.number,
|
||||||
registrationEmbedded: PropTypes.bool,
|
|
||||||
host: PropTypes.string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RedirectLogistration;
|
export default RedirectLogistration;
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+
|
|||||||
+ ')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\\.)+)(?:[A-Z0-9-]{2,63})'
|
+ ')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\\.)+)(?:[A-Z0-9-]{2,63})'
|
||||||
+ '|\\[(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\]$';
|
+ '|\\[(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\]$';
|
||||||
export const LETTER_REGEX = /[a-zA-Z]/;
|
export const LETTER_REGEX = /[a-zA-Z]/;
|
||||||
|
export const USERNAME_REGEX = /^[a-zA-Z0-9_-]*$/i;
|
||||||
export const NUMBER_REGEX = /\d/;
|
export const NUMBER_REGEX = /\d/;
|
||||||
export const INVALID_NAME_REGEX = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi; // eslint-disable-line no-useless-escape
|
export const INVALID_NAME_REGEX = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi; // eslint-disable-line no-useless-escape
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { FIELDS } from './data/constants';
|
import { COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY, FIELDS } from './data/constants';
|
||||||
import { validateCountryField } from './data/utils';
|
import { validateCountryField } from './data/utils';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { HonorCode, TermsOfService } from './registrationFields';
|
import { HonorCode, TermsOfService } from './registrationFields';
|
||||||
@@ -36,6 +37,7 @@ const ConfigurableRegistrationForm = (props) => {
|
|||||||
setFormFields,
|
setFormFields,
|
||||||
registrationEmbedded,
|
registrationEmbedded,
|
||||||
} = props;
|
} = props;
|
||||||
|
const backendCountryCode = useSelector(state => state.register.backendCountryCode);
|
||||||
|
|
||||||
let showTermsOfServiceAndHonorCode = false;
|
let showTermsOfServiceAndHonorCode = false;
|
||||||
let showCountryField = false;
|
let showCountryField = false;
|
||||||
@@ -54,6 +56,29 @@ const ConfigurableRegistrationForm = (props) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (backendCountryCode && backendCountryCode !== formFields?.country?.countryCode) {
|
||||||
|
let countryCode = '';
|
||||||
|
let countryDisplayValue = '';
|
||||||
|
|
||||||
|
const selectedCountry = countryList.find(
|
||||||
|
(country) => (country[COUNTRY_CODE_KEY].toLowerCase() === backendCountryCode.toLowerCase()),
|
||||||
|
);
|
||||||
|
if (selectedCountry) {
|
||||||
|
countryCode = selectedCountry[COUNTRY_CODE_KEY];
|
||||||
|
countryDisplayValue = selectedCountry[COUNTRY_DISPLAY_KEY];
|
||||||
|
}
|
||||||
|
setFormFields(prevState => (
|
||||||
|
{
|
||||||
|
...prevState,
|
||||||
|
country: {
|
||||||
|
countryCode, displayValue: countryDisplayValue,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}, [backendCountryCode, countryList]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const handleOnChange = (event, countryValue = null) => {
|
const handleOnChange = (event, countryValue = null) => {
|
||||||
const { name } = event.target;
|
const { name } = event.target;
|
||||||
let value;
|
let value;
|
||||||
@@ -218,7 +243,7 @@ ConfigurableRegistrationForm.propTypes = {
|
|||||||
marketingEmailsOptIn: PropTypes.bool,
|
marketingEmailsOptIn: PropTypes.bool,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
setFieldErrors: PropTypes.func.isRequired,
|
setFieldErrors: PropTypes.func.isRequired,
|
||||||
setFocusedField: PropTypes.func.isRequired,
|
setFocusedField: PropTypes.func,
|
||||||
setFormFields: PropTypes.func.isRequired,
|
setFormFields: PropTypes.func.isRequired,
|
||||||
registrationEmbedded: PropTypes.bool,
|
registrationEmbedded: PropTypes.bool,
|
||||||
};
|
};
|
||||||
@@ -226,6 +251,7 @@ ConfigurableRegistrationForm.propTypes = {
|
|||||||
ConfigurableRegistrationForm.defaultProps = {
|
ConfigurableRegistrationForm.defaultProps = {
|
||||||
fieldDescriptions: {},
|
fieldDescriptions: {},
|
||||||
registrationEmbedded: false,
|
registrationEmbedded: false,
|
||||||
|
setFocusedField: () => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ConfigurableRegistrationForm;
|
export default ConfigurableRegistrationForm;
|
||||||
|
|||||||
555
src/register/EmbeddableRegistrationPage.jsx
Normal file
555
src/register/EmbeddableRegistrationPage.jsx
Normal file
@@ -0,0 +1,555 @@
|
|||||||
|
import React, {
|
||||||
|
useEffect, useMemo, useState,
|
||||||
|
} from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
|
||||||
|
import { sendPageEvent } from '@edx/frontend-platform/analytics';
|
||||||
|
import {
|
||||||
|
getCountryList, getLocale, useIntl,
|
||||||
|
} from '@edx/frontend-platform/i18n';
|
||||||
|
import { Form, StatefulButton } from '@edx/paragon';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
|
import ConfigurableRegistrationForm from './ConfigurableRegistrationForm';
|
||||||
|
import {
|
||||||
|
clearRegistertionBackendError,
|
||||||
|
clearUsernameSuggestions,
|
||||||
|
fetchRealtimeValidations,
|
||||||
|
registerNewUser,
|
||||||
|
} from './data/actions';
|
||||||
|
import {
|
||||||
|
COUNTRY_CODE_KEY,
|
||||||
|
COUNTRY_DISPLAY_KEY,
|
||||||
|
FORM_SUBMISSION_ERROR,
|
||||||
|
} from './data/constants';
|
||||||
|
import { registrationErrorSelector, validationsSelector } from './data/selectors';
|
||||||
|
import {
|
||||||
|
emailRegex, getSuggestionForInvalidEmail, urlRegex, validateCountryField, validateEmailAddress,
|
||||||
|
} from './data/utils';
|
||||||
|
import messages from './messages';
|
||||||
|
import RegistrationFailure from './RegistrationFailure';
|
||||||
|
import { EmailField, UsernameField } from './registrationFields';
|
||||||
|
import {
|
||||||
|
FormGroup, PasswordField,
|
||||||
|
} from '../common-components';
|
||||||
|
import { getThirdPartyAuthContext } from '../common-components/data/actions';
|
||||||
|
import {
|
||||||
|
fieldDescriptionSelector,
|
||||||
|
} from '../common-components/data/selectors';
|
||||||
|
import {
|
||||||
|
DEFAULT_STATE, LETTER_REGEX, NUMBER_REGEX, REDIRECT, USERNAME_REGEX,
|
||||||
|
} from '../data/constants';
|
||||||
|
import {
|
||||||
|
getAllPossibleQueryParams, setCookie,
|
||||||
|
} from '../data/utils';
|
||||||
|
|
||||||
|
const EmbeddableRegistrationPage = (props) => {
|
||||||
|
const {
|
||||||
|
backendCountryCode,
|
||||||
|
backendValidations,
|
||||||
|
fieldDescriptions,
|
||||||
|
registrationError,
|
||||||
|
registrationErrorCode,
|
||||||
|
registrationResult,
|
||||||
|
submitState,
|
||||||
|
usernameSuggestions,
|
||||||
|
validationApiRateLimited,
|
||||||
|
// Actions
|
||||||
|
getRegistrationDataFromBackend,
|
||||||
|
validateFromBackend,
|
||||||
|
clearBackendError,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const countryList = useMemo(() => getCountryList(getLocale()), []);
|
||||||
|
const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
|
||||||
|
const { cta, host } = queryParams;
|
||||||
|
const flags = {
|
||||||
|
showConfigurableEdxFields: getConfig().SHOW_CONFIGURABLE_EDX_FIELDS,
|
||||||
|
showConfigurableRegistrationFields: getConfig().ENABLE_DYNAMIC_REGISTRATION_FIELDS,
|
||||||
|
showMarketingEmailOptInCheckbox: getConfig().MARKETING_EMAILS_OPT_IN,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [formFields, setFormFields] = useState({
|
||||||
|
email: '',
|
||||||
|
name: '',
|
||||||
|
password: '',
|
||||||
|
username: '',
|
||||||
|
});
|
||||||
|
const [configurableFormFields, setConfigurableFormFields] = useState({
|
||||||
|
marketingEmailsOptIn: true,
|
||||||
|
});
|
||||||
|
const [errors, setErrors] = useState({
|
||||||
|
email: '',
|
||||||
|
name: '',
|
||||||
|
password: '',
|
||||||
|
username: '',
|
||||||
|
});
|
||||||
|
const [emailSuggestion, setEmailSuggestion] = useState({ suggestion: '', type: '' });
|
||||||
|
const [errorCode, setErrorCode] = useState({ type: '', count: 0 });
|
||||||
|
const [formStartTime, setFormStartTime] = useState(null);
|
||||||
|
const [, setFocusedField] = useState(null);
|
||||||
|
|
||||||
|
const buttonLabel = cta ? formatMessage(messages['create.account.cta.button'], { label: cta }) : formatMessage(messages['create.account.for.free.button']);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!formStartTime) {
|
||||||
|
sendPageEvent('login_and_registration', 'register');
|
||||||
|
const payload = { ...queryParams, is_register_page: true };
|
||||||
|
getRegistrationDataFromBackend(payload);
|
||||||
|
setFormStartTime(Date.now());
|
||||||
|
}
|
||||||
|
}, [formStartTime, getRegistrationDataFromBackend, queryParams]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (backendValidations) {
|
||||||
|
setErrors(prevErrors => ({ ...prevErrors, ...backendValidations }));
|
||||||
|
}
|
||||||
|
}, [backendValidations]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (registrationErrorCode) {
|
||||||
|
setErrorCode(prevState => ({ type: registrationErrorCode, count: prevState.count + 1 }));
|
||||||
|
}
|
||||||
|
}, [registrationErrorCode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (backendCountryCode && backendCountryCode !== configurableFormFields?.country?.countryCode) {
|
||||||
|
let countryCode = '';
|
||||||
|
let countryDisplayValue = '';
|
||||||
|
|
||||||
|
const selectedCountry = countryList.find(
|
||||||
|
(country) => (country[COUNTRY_CODE_KEY].toLowerCase() === backendCountryCode.toLowerCase()),
|
||||||
|
);
|
||||||
|
if (selectedCountry) {
|
||||||
|
countryCode = selectedCountry[COUNTRY_CODE_KEY];
|
||||||
|
countryDisplayValue = selectedCountry[COUNTRY_DISPLAY_KEY];
|
||||||
|
}
|
||||||
|
setConfigurableFormFields(prevState => (
|
||||||
|
{
|
||||||
|
...prevState,
|
||||||
|
country: {
|
||||||
|
countryCode, displayValue: countryDisplayValue,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}, [backendCountryCode, countryList]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We need to remove the placeholder from the field, adding a space will do that.
|
||||||
|
* This is needed because we are placing the username suggestions on top of the field.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (usernameSuggestions.length && !formFields.username) {
|
||||||
|
setFormFields(prevState => ({ ...prevState, username: ' ' }));
|
||||||
|
}
|
||||||
|
}, [usernameSuggestions, formFields]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (registrationResult.success) {
|
||||||
|
// Optimizely registration conversion event
|
||||||
|
window.optimizely = window.optimizely || [];
|
||||||
|
window.optimizely.push({
|
||||||
|
type: 'event',
|
||||||
|
eventName: 'authn-registration-conversion',
|
||||||
|
});
|
||||||
|
|
||||||
|
// We probably don't need this cookie because this fires the same event as
|
||||||
|
// above for optimizely using GTM.
|
||||||
|
setCookie(getConfig().REGISTER_CONVERSION_COOKIE_NAME, true);
|
||||||
|
// This is used by the "User Retention Rate Event" on GTM
|
||||||
|
setCookie('authn-returning-user');
|
||||||
|
|
||||||
|
// Fire GTM event used for integration with impact.com
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
window.dataLayer.push({
|
||||||
|
event: 'ImpactRegistrationEvent',
|
||||||
|
});
|
||||||
|
|
||||||
|
window.parent.postMessage({
|
||||||
|
action: REDIRECT,
|
||||||
|
redirectUrl: encodeURIComponent(getConfig().POST_REGISTRATION_REDIRECT_URL),
|
||||||
|
}, host);
|
||||||
|
}
|
||||||
|
}, [registrationResult, host]);
|
||||||
|
|
||||||
|
const validateInput = (fieldName, value, payload, shouldValidateFromBackend, shouldSetErrors = true) => {
|
||||||
|
let fieldError = '';
|
||||||
|
|
||||||
|
switch (fieldName) {
|
||||||
|
case 'name':
|
||||||
|
if (value && value.match(urlRegex)) {
|
||||||
|
fieldError = formatMessage(messages['name.validation.message']);
|
||||||
|
} else if (value && !payload.username.trim() && shouldValidateFromBackend) {
|
||||||
|
validateFromBackend(payload);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'email':
|
||||||
|
if (value.length <= 2) {
|
||||||
|
fieldError = formatMessage(messages['email.invalid.format.error']);
|
||||||
|
} else {
|
||||||
|
const [username, domainName] = value.split('@');
|
||||||
|
// Check if email address is invalid. If we have a suggestion for invalid email
|
||||||
|
// provide that along with the error message.
|
||||||
|
if (!emailRegex.test(value)) {
|
||||||
|
fieldError = formatMessage(messages['email.invalid.format.error']);
|
||||||
|
setEmailSuggestion({
|
||||||
|
suggestion: getSuggestionForInvalidEmail(domainName, username),
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const response = validateEmailAddress(value, username, domainName);
|
||||||
|
if (response.hasError) {
|
||||||
|
fieldError = formatMessage(messages['email.invalid.format.error']);
|
||||||
|
delete response.hasError;
|
||||||
|
}
|
||||||
|
setEmailSuggestion({ ...response });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'username':
|
||||||
|
if (!value.match(USERNAME_REGEX)) {
|
||||||
|
fieldError = formatMessage(messages['username.format.validation.message']);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'password':
|
||||||
|
if (!value || !LETTER_REGEX.test(value) || !NUMBER_REGEX.test(value) || value.length < 8) {
|
||||||
|
fieldError = formatMessage(messages['password.validation.message']);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'country':
|
||||||
|
if (flags.showConfigurableEdxFields || flags.showConfigurableRegistrationFields) {
|
||||||
|
const {
|
||||||
|
countryCode, displayValue, error,
|
||||||
|
} = validateCountryField(value.trim(), countryList, formatMessage(messages['empty.country.field.error']));
|
||||||
|
fieldError = error;
|
||||||
|
setConfigurableFormFields(prevState => ({ ...prevState, country: { countryCode, displayValue } }));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (flags.showConfigurableRegistrationFields) {
|
||||||
|
if (!value && fieldDescriptions[fieldName]?.error_message) {
|
||||||
|
fieldError = fieldDescriptions[fieldName].error_message;
|
||||||
|
} else if (fieldName === 'confirm_email' && formFields.email && value !== formFields.email) {
|
||||||
|
fieldError = formatMessage(messages['email.do.not.match']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (shouldSetErrors && fieldError) {
|
||||||
|
setErrors(prevErrors => ({
|
||||||
|
...prevErrors,
|
||||||
|
[fieldName]: fieldError,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return fieldError;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isFormValid = (payload) => {
|
||||||
|
const fieldErrors = { ...errors };
|
||||||
|
let isValid = true;
|
||||||
|
Object.keys(payload).forEach(key => {
|
||||||
|
if (!payload[key]) {
|
||||||
|
fieldErrors[key] = formatMessage(messages[`empty.${key}.field.error`]);
|
||||||
|
}
|
||||||
|
if (fieldErrors[key]) {
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (flags.showConfigurableEdxFields) {
|
||||||
|
if (!configurableFormFields.country.displayValue) {
|
||||||
|
fieldErrors.country = formatMessage(messages['empty.country.field.error']);
|
||||||
|
}
|
||||||
|
if (fieldErrors.country) {
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags.showConfigurableRegistrationFields) {
|
||||||
|
Object.keys(fieldDescriptions).forEach(key => {
|
||||||
|
if (key === 'country' && !configurableFormFields.country.displayValue) {
|
||||||
|
fieldErrors[key] = formatMessage(messages['empty.country.field.error']);
|
||||||
|
} else if (!configurableFormFields[key]) {
|
||||||
|
fieldErrors[key] = fieldDescriptions[key].error_message;
|
||||||
|
}
|
||||||
|
if (fieldErrors[key]) {
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setErrors({ ...fieldErrors });
|
||||||
|
return isValid;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSuggestionClick = (event, fieldName, suggestion = '') => {
|
||||||
|
event.preventDefault();
|
||||||
|
setErrors(prevErrors => ({ ...prevErrors, [fieldName]: '' }));
|
||||||
|
switch (fieldName) {
|
||||||
|
case 'email':
|
||||||
|
setFormFields(prevState => ({ ...prevState, email: emailSuggestion.suggestion }));
|
||||||
|
setEmailSuggestion({ suggestion: '', type: '' });
|
||||||
|
break;
|
||||||
|
case 'username':
|
||||||
|
setFormFields(prevState => ({ ...prevState, username: suggestion }));
|
||||||
|
props.resetUsernameSuggestions();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEmailSuggestionClosed = () => setEmailSuggestion({ suggestion: '', type: '' });
|
||||||
|
|
||||||
|
const handleUsernameSuggestionClosed = () => props.resetUsernameSuggestions();
|
||||||
|
|
||||||
|
const handleOnChange = (event) => {
|
||||||
|
const { name } = event.target;
|
||||||
|
let value = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
|
||||||
|
if (registrationError[name]) {
|
||||||
|
clearBackendError(name);
|
||||||
|
setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
|
||||||
|
}
|
||||||
|
if (name === 'username') {
|
||||||
|
if (value.length > 30) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value.startsWith(' ')) {
|
||||||
|
value = value.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setFormFields(prevState => ({ ...prevState, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnBlur = (event) => {
|
||||||
|
const { name, value } = event.target;
|
||||||
|
|
||||||
|
if (name === 'name') {
|
||||||
|
validateInput(
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
{ name: formFields.name, username: formFields.username, form_field_key: name },
|
||||||
|
!validationApiRateLimited,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (name === 'email') {
|
||||||
|
validateInput(name, value, null, !validationApiRateLimited, false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnFocus = (event) => {
|
||||||
|
const { name, value } = event.target;
|
||||||
|
setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
|
||||||
|
clearBackendError(name);
|
||||||
|
// Since we are removing the form errors from the focused field, we will
|
||||||
|
// need to rerun the validation for focused field on form submission.
|
||||||
|
setFocusedField(name);
|
||||||
|
|
||||||
|
if (name === 'username') {
|
||||||
|
props.resetUsernameSuggestions();
|
||||||
|
// If we added a space character to username field to display the suggestion
|
||||||
|
// remove it before user enters the input. This is to ensure user doesn't
|
||||||
|
// have a space prefixed to the username.
|
||||||
|
if (value === ' ') {
|
||||||
|
setFormFields(prevState => ({ ...prevState, [name]: '' }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const totalRegistrationTime = (Date.now() - formStartTime) / 1000;
|
||||||
|
let payload = { ...formFields };
|
||||||
|
if (!isFormValid(payload)) {
|
||||||
|
setErrorCode(prevState => ({ type: FORM_SUBMISSION_ERROR, count: prevState.count + 1 }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(configurableFormFields).forEach((fieldName) => {
|
||||||
|
if (fieldName === 'country') {
|
||||||
|
payload[fieldName] = configurableFormFields[fieldName].countryCode;
|
||||||
|
} else {
|
||||||
|
payload[fieldName] = configurableFormFields[fieldName];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Don't send the marketing email opt-in value if the flag is turned off
|
||||||
|
if (!flags.showMarketingEmailOptInCheckbox) {
|
||||||
|
delete payload.marketingEmailsOptIn;
|
||||||
|
}
|
||||||
|
let isValid = true;
|
||||||
|
Object.entries(payload).forEach(([key, value]) => {
|
||||||
|
if (validateInput(key, value, payload, false, true) !== '') {
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!isValid) {
|
||||||
|
setErrorCode(prevState => ({ type: FORM_SUBMISSION_ERROR, count: prevState.count + 1 }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
payload = snakeCaseObject(payload);
|
||||||
|
payload.totalRegistrationTime = totalRegistrationTime;
|
||||||
|
|
||||||
|
// add query params to the payload
|
||||||
|
payload = { ...payload, ...queryParams };
|
||||||
|
props.registerNewUser(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>{formatMessage(messages['register.page.title'], { siteName: getConfig().SITE_NAME })}</title>
|
||||||
|
</Helmet>
|
||||||
|
<div
|
||||||
|
className="mw-xs mt-3 w-100 m-auto pt-4 main-content"
|
||||||
|
>
|
||||||
|
<RegistrationFailure
|
||||||
|
errorCode={errorCode.type}
|
||||||
|
failureCount={errorCode.count}
|
||||||
|
/>
|
||||||
|
<Form id="registration-form" name="registration-form">
|
||||||
|
<FormGroup
|
||||||
|
name="name"
|
||||||
|
value={formFields.name}
|
||||||
|
handleChange={handleOnChange}
|
||||||
|
handleBlur={handleOnBlur}
|
||||||
|
handleFocus={handleOnFocus}
|
||||||
|
errorMessage={errors.name}
|
||||||
|
helpText={[formatMessage(messages['help.text.name'])]}
|
||||||
|
floatingLabel={formatMessage(messages['registration.fullname.label'])}
|
||||||
|
/>
|
||||||
|
<EmailField
|
||||||
|
name="email"
|
||||||
|
value={formFields.email}
|
||||||
|
handleChange={handleOnChange}
|
||||||
|
handleBlur={handleOnBlur}
|
||||||
|
handleFocus={handleOnFocus}
|
||||||
|
handleSuggestionClick={(e) => handleSuggestionClick(e, 'email')}
|
||||||
|
handleOnClose={handleEmailSuggestionClosed}
|
||||||
|
emailSuggestion={emailSuggestion}
|
||||||
|
errorMessage={errors.email}
|
||||||
|
helpText={[formatMessage(messages['help.text.email'])]}
|
||||||
|
floatingLabel={formatMessage(messages['registration.email.label'])}
|
||||||
|
/>
|
||||||
|
<UsernameField
|
||||||
|
name="username"
|
||||||
|
spellCheck="false"
|
||||||
|
value={formFields.username}
|
||||||
|
handleBlur={handleOnBlur}
|
||||||
|
handleChange={handleOnChange}
|
||||||
|
handleFocus={handleOnFocus}
|
||||||
|
handleSuggestionClick={handleSuggestionClick}
|
||||||
|
handleUsernameSuggestionClose={handleUsernameSuggestionClosed}
|
||||||
|
usernameSuggestions={usernameSuggestions}
|
||||||
|
errorMessage={errors.username}
|
||||||
|
helpText={[formatMessage(messages['help.text.username.1']), formatMessage(messages['help.text.username.2'])]}
|
||||||
|
floatingLabel={formatMessage(messages['registration.username.label'])}
|
||||||
|
/>
|
||||||
|
<PasswordField
|
||||||
|
name="password"
|
||||||
|
value={formFields.password}
|
||||||
|
handleChange={handleOnChange}
|
||||||
|
handleBlur={handleOnBlur}
|
||||||
|
handleFocus={handleOnFocus}
|
||||||
|
errorMessage={errors.password}
|
||||||
|
floatingLabel={formatMessage(messages['registration.password.label'])}
|
||||||
|
/>
|
||||||
|
<ConfigurableRegistrationForm
|
||||||
|
countryList={countryList}
|
||||||
|
email={formFields.email}
|
||||||
|
fieldErrors={errors}
|
||||||
|
registrationEmbedded
|
||||||
|
formFields={configurableFormFields}
|
||||||
|
setFieldErrors={setErrors}
|
||||||
|
setFormFields={setConfigurableFormFields}
|
||||||
|
setFocusedField={setFocusedField}
|
||||||
|
fieldDescriptions={fieldDescriptions}
|
||||||
|
/>
|
||||||
|
<StatefulButton
|
||||||
|
id="register-user"
|
||||||
|
name="register-user"
|
||||||
|
type="submit"
|
||||||
|
variant="brand"
|
||||||
|
className="register-button mt-4 mb-4"
|
||||||
|
state={submitState}
|
||||||
|
labels={{
|
||||||
|
default: buttonLabel,
|
||||||
|
pending: '',
|
||||||
|
}}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
const registerPageState = state.register;
|
||||||
|
return {
|
||||||
|
backendCountryCode: registerPageState.backendCountryCode,
|
||||||
|
backendValidations: validationsSelector(state),
|
||||||
|
fieldDescriptions: fieldDescriptionSelector(state),
|
||||||
|
registrationError: registerPageState.registrationError,
|
||||||
|
registrationErrorCode: registrationErrorSelector(state),
|
||||||
|
registrationResult: registerPageState.registrationResult,
|
||||||
|
submitState: registerPageState.submitState,
|
||||||
|
validationApiRateLimited: registerPageState.validationApiRateLimited,
|
||||||
|
usernameSuggestions: registerPageState.usernameSuggestions,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
EmbeddableRegistrationPage.propTypes = {
|
||||||
|
backendCountryCode: PropTypes.string,
|
||||||
|
backendValidations: PropTypes.shape({
|
||||||
|
name: PropTypes.string,
|
||||||
|
email: PropTypes.string,
|
||||||
|
username: PropTypes.string,
|
||||||
|
password: PropTypes.string,
|
||||||
|
}),
|
||||||
|
fieldDescriptions: PropTypes.shape({}),
|
||||||
|
registrationError: PropTypes.shape({}),
|
||||||
|
registrationErrorCode: PropTypes.string,
|
||||||
|
registrationResult: PropTypes.shape({
|
||||||
|
redirectUrl: PropTypes.string,
|
||||||
|
success: PropTypes.bool,
|
||||||
|
}),
|
||||||
|
submitState: PropTypes.string,
|
||||||
|
usernameSuggestions: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
validationApiRateLimited: PropTypes.bool,
|
||||||
|
// Actions
|
||||||
|
clearBackendError: PropTypes.func.isRequired,
|
||||||
|
getRegistrationDataFromBackend: PropTypes.func.isRequired,
|
||||||
|
registerNewUser: PropTypes.func.isRequired,
|
||||||
|
resetUsernameSuggestions: PropTypes.func.isRequired,
|
||||||
|
validateFromBackend: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
EmbeddableRegistrationPage.defaultProps = {
|
||||||
|
backendCountryCode: '',
|
||||||
|
backendValidations: null,
|
||||||
|
fieldDescriptions: {},
|
||||||
|
registrationError: {},
|
||||||
|
registrationErrorCode: '',
|
||||||
|
registrationResult: null,
|
||||||
|
submitState: DEFAULT_STATE,
|
||||||
|
usernameSuggestions: [],
|
||||||
|
validationApiRateLimited: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
{
|
||||||
|
clearBackendError: clearRegistertionBackendError,
|
||||||
|
getRegistrationDataFromBackend: getThirdPartyAuthContext,
|
||||||
|
resetUsernameSuggestions: clearUsernameSuggestions,
|
||||||
|
validateFromBackend: fetchRealtimeValidations,
|
||||||
|
registerNewUser,
|
||||||
|
},
|
||||||
|
)(EmbeddableRegistrationPage);
|
||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
getCountryList, getLocale, useIntl,
|
getCountryList, getLocale, useIntl,
|
||||||
} from '@edx/frontend-platform/i18n';
|
} from '@edx/frontend-platform/i18n';
|
||||||
import { Form, Spinner, StatefulButton } from '@edx/paragon';
|
import { Form, Spinner, StatefulButton } from '@edx/paragon';
|
||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import Skeleton from 'react-loading-skeleton';
|
import Skeleton from 'react-loading-skeleton';
|
||||||
@@ -24,15 +23,13 @@ import {
|
|||||||
setUserPipelineDataLoaded,
|
setUserPipelineDataLoaded,
|
||||||
} from './data/actions';
|
} from './data/actions';
|
||||||
import {
|
import {
|
||||||
COUNTRY_CODE_KEY,
|
|
||||||
COUNTRY_DISPLAY_KEY,
|
|
||||||
FIELDS,
|
FIELDS,
|
||||||
FORM_SUBMISSION_ERROR,
|
FORM_SUBMISSION_ERROR,
|
||||||
TPA_AUTHENTICATION_FAILURE,
|
TPA_AUTHENTICATION_FAILURE,
|
||||||
} from './data/constants';
|
} from './data/constants';
|
||||||
import { registrationErrorSelector, validationsSelector } from './data/selectors';
|
import { registrationErrorSelector, validationsSelector } from './data/selectors';
|
||||||
import {
|
import {
|
||||||
getSuggestionForInvalidEmail, validateCountryField, validateEmailAddress,
|
emailRegex, getSuggestionForInvalidEmail, urlRegex, validateCountryField, validateEmailAddress,
|
||||||
} from './data/utils';
|
} from './data/utils';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import RegistrationFailure from './RegistrationFailure';
|
import RegistrationFailure from './RegistrationFailure';
|
||||||
@@ -47,20 +44,15 @@ import {
|
|||||||
} from '../common-components/data/selectors';
|
} from '../common-components/data/selectors';
|
||||||
import EnterpriseSSO from '../common-components/EnterpriseSSO';
|
import EnterpriseSSO from '../common-components/EnterpriseSSO';
|
||||||
import {
|
import {
|
||||||
COMPLETE_STATE, DEFAULT_STATE,
|
COMPLETE_STATE, DEFAULT_STATE, LETTER_REGEX, NUMBER_REGEX, PENDING_STATE, REGISTER_PAGE,
|
||||||
INVALID_NAME_REGEX, LETTER_REGEX, NUMBER_REGEX, PENDING_STATE, REGISTER_PAGE, VALID_EMAIL_REGEX,
|
|
||||||
} from '../data/constants';
|
} from '../data/constants';
|
||||||
import {
|
import {
|
||||||
getAllPossibleQueryParams, getTpaHint, getTpaProvider, isHostAvailableInQueryParams, setCookie,
|
getAllPossibleQueryParams, getTpaHint, getTpaProvider, setCookie,
|
||||||
} from '../data/utils';
|
} from '../data/utils';
|
||||||
|
|
||||||
const emailRegex = new RegExp(VALID_EMAIL_REGEX, 'i');
|
|
||||||
const urlRegex = new RegExp(INVALID_NAME_REGEX);
|
|
||||||
|
|
||||||
const RegistrationPage = (props) => {
|
const RegistrationPage = (props) => {
|
||||||
const {
|
const {
|
||||||
backedUpFormData,
|
backedUpFormData,
|
||||||
backendCountryCode,
|
|
||||||
backendValidations,
|
backendValidations,
|
||||||
fieldDescriptions,
|
fieldDescriptions,
|
||||||
handleInstitutionLogin,
|
handleInstitutionLogin,
|
||||||
@@ -73,7 +65,6 @@ const RegistrationPage = (props) => {
|
|||||||
submitState,
|
submitState,
|
||||||
thirdPartyAuthApiStatus,
|
thirdPartyAuthApiStatus,
|
||||||
thirdPartyAuthContext,
|
thirdPartyAuthContext,
|
||||||
usernameSuggestions,
|
|
||||||
validationApiRateLimited,
|
validationApiRateLimited,
|
||||||
// Actions
|
// Actions
|
||||||
backupFormState,
|
backupFormState,
|
||||||
@@ -87,8 +78,6 @@ const RegistrationPage = (props) => {
|
|||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const countryList = useMemo(() => getCountryList(getLocale()), []);
|
const countryList = useMemo(() => getCountryList(getLocale()), []);
|
||||||
const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
|
const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
|
||||||
const registrationEmbedded = isHostAvailableInQueryParams();
|
|
||||||
const { cta, host } = queryParams;
|
|
||||||
const tpaHint = useMemo(() => getTpaHint(), []);
|
const tpaHint = useMemo(() => getTpaHint(), []);
|
||||||
const flags = {
|
const flags = {
|
||||||
showConfigurableEdxFields: getConfig().SHOW_CONFIGURABLE_EDX_FIELDS,
|
showConfigurableEdxFields: getConfig().SHOW_CONFIGURABLE_EDX_FIELDS,
|
||||||
@@ -109,7 +98,6 @@ const RegistrationPage = (props) => {
|
|||||||
providers, currentProvider, secondaryProviders, finishAuthUrl,
|
providers, currentProvider, secondaryProviders, finishAuthUrl,
|
||||||
} = thirdPartyAuthContext;
|
} = thirdPartyAuthContext;
|
||||||
const platformName = getConfig().SITE_NAME;
|
const platformName = getConfig().SITE_NAME;
|
||||||
const buttonLabel = cta ? formatMessage(messages['create.account.cta.button'], { label: cta }) : formatMessage(messages['create.account.for.free.button']);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If auto submitting register form, we will check tos and honor code fields if they exist for feature parity.
|
* If auto submitting register form, we will check tos and honor code fields if they exist for feature parity.
|
||||||
@@ -193,39 +181,6 @@ const RegistrationPage = (props) => {
|
|||||||
}
|
}
|
||||||
}, [registrationErrorCode]);
|
}, [registrationErrorCode]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (backendCountryCode && backendCountryCode !== configurableFormFields?.country?.countryCode) {
|
|
||||||
let countryCode = '';
|
|
||||||
let countryDisplayValue = '';
|
|
||||||
|
|
||||||
const selectedCountry = countryList.find(
|
|
||||||
(country) => (country[COUNTRY_CODE_KEY].toLowerCase() === backendCountryCode.toLowerCase()),
|
|
||||||
);
|
|
||||||
if (selectedCountry) {
|
|
||||||
countryCode = selectedCountry[COUNTRY_CODE_KEY];
|
|
||||||
countryDisplayValue = selectedCountry[COUNTRY_DISPLAY_KEY];
|
|
||||||
}
|
|
||||||
setConfigurableFormFields(prevState => (
|
|
||||||
{
|
|
||||||
...prevState,
|
|
||||||
country: {
|
|
||||||
countryCode, displayValue: countryDisplayValue,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}, [backendCountryCode, countryList]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We need to remove the placeholder from the field, adding a space will do that.
|
|
||||||
* This is needed because we are placing the username suggestions on top of the field.
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (usernameSuggestions.length && !formFields.username) {
|
|
||||||
setFormFields(prevState => ({ ...prevState, username: ' ' }));
|
|
||||||
}
|
|
||||||
}, [usernameSuggestions, formFields]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (registrationResult.success) {
|
if (registrationResult.success) {
|
||||||
// Optimizely registration conversion event
|
// Optimizely registration conversion event
|
||||||
@@ -401,41 +356,21 @@ const RegistrationPage = (props) => {
|
|||||||
|
|
||||||
const handleEmailSuggestionClosed = () => setEmailSuggestion({ suggestion: '', type: '' });
|
const handleEmailSuggestionClosed = () => setEmailSuggestion({ suggestion: '', type: '' });
|
||||||
|
|
||||||
const handleUsernameSuggestionClosed = () => props.resetUsernameSuggestions();
|
|
||||||
|
|
||||||
const handleOnChange = (event) => {
|
const handleOnChange = (event) => {
|
||||||
|
console.log('test handleOnChange', event.target);
|
||||||
const { name } = event.target;
|
const { name } = event.target;
|
||||||
let value = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
|
const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
|
||||||
if (registrationError[name]) {
|
if (registrationError[name]) {
|
||||||
clearBackendError(name);
|
clearBackendError(name);
|
||||||
setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
|
setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
|
||||||
}
|
}
|
||||||
if (name === 'username') {
|
|
||||||
if (value.length > 30) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (value.startsWith(' ')) {
|
|
||||||
value = value.trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setFormFields(prevState => ({ ...prevState, [name]: value }));
|
setFormFields(prevState => ({ ...prevState, [name]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnBlur = (event) => {
|
const handleOnBlur = (event) => {
|
||||||
const { name, value } = event.target;
|
const { name, value } = event.target;
|
||||||
if (registrationEmbedded) {
|
|
||||||
if (name === 'name') {
|
|
||||||
validateInput(
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
{ name: formFields.name, username: formFields.username, form_field_key: name },
|
|
||||||
!validationApiRateLimited,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const payload = {
|
const payload = {
|
||||||
name: formFields.name,
|
name: formFields.name,
|
||||||
email: formFields.email,
|
email: formFields.email,
|
||||||
@@ -449,22 +384,12 @@ const RegistrationPage = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleOnFocus = (event) => {
|
const handleOnFocus = (event) => {
|
||||||
const { name, value } = event.target;
|
const { name } = event.target;
|
||||||
setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
|
setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
|
||||||
clearBackendError(name);
|
clearBackendError(name);
|
||||||
// Since we are removing the form errors from the focused field, we will
|
// Since we are removing the form errors from the focused field, we will
|
||||||
// need to rerun the validation for focused field on form submission.
|
// need to rerun the validation for focused field on form submission.
|
||||||
setFocusedField(name);
|
setFocusedField(name);
|
||||||
|
|
||||||
if (name === 'username') {
|
|
||||||
props.resetUsernameSuggestions();
|
|
||||||
// If we added a space character to username field to display the suggestion
|
|
||||||
// remove it before user enters the input. This is to ensure user doesn't
|
|
||||||
// have a space prefixed to the username.
|
|
||||||
if (value === ' ') {
|
|
||||||
setFormFields(prevState => ({ ...prevState, [name]: '' }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const registerUser = () => {
|
const registerUser = () => {
|
||||||
@@ -540,12 +465,10 @@ const RegistrationPage = (props) => {
|
|||||||
<title>{formatMessage(messages['register.page.title'], { siteName: getConfig().SITE_NAME })}</title>
|
<title>{formatMessage(messages['register.page.title'], { siteName: getConfig().SITE_NAME })}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<RedirectLogistration
|
<RedirectLogistration
|
||||||
host={host}
|
|
||||||
success={registrationResult.success}
|
success={registrationResult.success}
|
||||||
redirectUrl={registrationResult.redirectUrl}
|
redirectUrl={registrationResult.redirectUrl}
|
||||||
finishAuthUrl={finishAuthUrl}
|
finishAuthUrl={finishAuthUrl}
|
||||||
optionalFields={optionalFields}
|
optionalFields={optionalFields}
|
||||||
registrationEmbedded={registrationEmbedded}
|
|
||||||
redirectToProgressiveProfilingPage={
|
redirectToProgressiveProfilingPage={
|
||||||
getConfig().ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN && Object.keys(optionalFields).includes('fields')
|
getConfig().ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN && Object.keys(optionalFields).includes('fields')
|
||||||
}
|
}
|
||||||
@@ -556,10 +479,7 @@ const RegistrationPage = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className="mw-xs mt-3"
|
||||||
'mw-xs mt-3',
|
|
||||||
{ 'w-100 m-auto pt-4 main-content': registrationEmbedded },
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<ThirdPartyAuthAlert
|
<ThirdPartyAuthAlert
|
||||||
currentProvider={currentProvider}
|
currentProvider={currentProvider}
|
||||||
@@ -603,8 +523,6 @@ const RegistrationPage = (props) => {
|
|||||||
handleChange={handleOnChange}
|
handleChange={handleOnChange}
|
||||||
handleFocus={handleOnFocus}
|
handleFocus={handleOnFocus}
|
||||||
handleSuggestionClick={handleSuggestionClick}
|
handleSuggestionClick={handleSuggestionClick}
|
||||||
handleUsernameSuggestionClose={handleUsernameSuggestionClosed}
|
|
||||||
usernameSuggestions={usernameSuggestions}
|
|
||||||
errorMessage={errors.username}
|
errorMessage={errors.username}
|
||||||
helpText={[formatMessage(messages['help.text.username.1']), formatMessage(messages['help.text.username.2'])]}
|
helpText={[formatMessage(messages['help.text.username.1']), formatMessage(messages['help.text.username.2'])]}
|
||||||
floatingLabel={formatMessage(messages['registration.username.label'])}
|
floatingLabel={formatMessage(messages['registration.username.label'])}
|
||||||
@@ -624,7 +542,6 @@ const RegistrationPage = (props) => {
|
|||||||
countryList={countryList}
|
countryList={countryList}
|
||||||
email={formFields.email}
|
email={formFields.email}
|
||||||
fieldErrors={errors}
|
fieldErrors={errors}
|
||||||
registrationEmbedded={registrationEmbedded}
|
|
||||||
formFields={configurableFormFields}
|
formFields={configurableFormFields}
|
||||||
setFieldErrors={setErrors}
|
setFieldErrors={setErrors}
|
||||||
setFormFields={setConfigurableFormFields}
|
setFormFields={setConfigurableFormFields}
|
||||||
@@ -639,21 +556,19 @@ const RegistrationPage = (props) => {
|
|||||||
className="register-button mt-4 mb-4"
|
className="register-button mt-4 mb-4"
|
||||||
state={submitState}
|
state={submitState}
|
||||||
labels={{
|
labels={{
|
||||||
default: buttonLabel,
|
default: formatMessage(messages['create.account.for.free.button']),
|
||||||
pending: '',
|
pending: '',
|
||||||
}}
|
}}
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
/>
|
/>
|
||||||
{!registrationEmbedded && (
|
<ThirdPartyAuth
|
||||||
<ThirdPartyAuth
|
currentProvider={currentProvider}
|
||||||
currentProvider={currentProvider}
|
providers={providers}
|
||||||
providers={providers}
|
secondaryProviders={secondaryProviders}
|
||||||
secondaryProviders={secondaryProviders}
|
handleInstitutionLogin={handleInstitutionLogin}
|
||||||
handleInstitutionLogin={handleInstitutionLogin}
|
thirdPartyAuthApiStatus={thirdPartyAuthApiStatus}
|
||||||
thirdPartyAuthApiStatus={thirdPartyAuthApiStatus}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -706,7 +621,6 @@ RegistrationPage.propTypes = {
|
|||||||
errors: PropTypes.shape({}),
|
errors: PropTypes.shape({}),
|
||||||
emailSuggestion: PropTypes.shape({}),
|
emailSuggestion: PropTypes.shape({}),
|
||||||
}),
|
}),
|
||||||
backendCountryCode: PropTypes.string,
|
|
||||||
backendValidations: PropTypes.shape({
|
backendValidations: PropTypes.shape({
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
email: PropTypes.string,
|
email: PropTypes.string,
|
||||||
@@ -746,7 +660,6 @@ RegistrationPage.propTypes = {
|
|||||||
PropTypes.shape({}),
|
PropTypes.shape({}),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
usernameSuggestions: PropTypes.arrayOf(PropTypes.string),
|
|
||||||
userPipelineDataLoaded: PropTypes.bool,
|
userPipelineDataLoaded: PropTypes.bool,
|
||||||
validationApiRateLimited: PropTypes.bool,
|
validationApiRateLimited: PropTypes.bool,
|
||||||
// Actions
|
// Actions
|
||||||
@@ -755,7 +668,6 @@ RegistrationPage.propTypes = {
|
|||||||
getRegistrationDataFromBackend: PropTypes.func.isRequired,
|
getRegistrationDataFromBackend: PropTypes.func.isRequired,
|
||||||
handleInstitutionLogin: PropTypes.func,
|
handleInstitutionLogin: PropTypes.func,
|
||||||
registerNewUser: PropTypes.func.isRequired,
|
registerNewUser: PropTypes.func.isRequired,
|
||||||
resetUsernameSuggestions: PropTypes.func.isRequired,
|
|
||||||
setUserPipelineDetailsLoaded: PropTypes.func.isRequired,
|
setUserPipelineDetailsLoaded: PropTypes.func.isRequired,
|
||||||
validateFromBackend: PropTypes.func.isRequired,
|
validateFromBackend: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
@@ -775,7 +687,6 @@ RegistrationPage.defaultProps = {
|
|||||||
suggestion: '', type: '',
|
suggestion: '', type: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
backendCountryCode: '',
|
|
||||||
backendValidations: null,
|
backendValidations: null,
|
||||||
fieldDescriptions: {},
|
fieldDescriptions: {},
|
||||||
handleInstitutionLogin: null,
|
handleInstitutionLogin: null,
|
||||||
@@ -797,7 +708,6 @@ RegistrationPage.defaultProps = {
|
|||||||
providers: [],
|
providers: [],
|
||||||
secondaryProviders: [],
|
secondaryProviders: [],
|
||||||
},
|
},
|
||||||
usernameSuggestions: [],
|
|
||||||
userPipelineDataLoaded: false,
|
userPipelineDataLoaded: false,
|
||||||
validationApiRateLimited: false,
|
validationApiRateLimited: false,
|
||||||
};
|
};
|
||||||
@@ -808,7 +718,6 @@ export default connect(
|
|||||||
backupFormState: backupRegistrationFormBegin,
|
backupFormState: backupRegistrationFormBegin,
|
||||||
clearBackendError: clearRegistertionBackendError,
|
clearBackendError: clearRegistertionBackendError,
|
||||||
getRegistrationDataFromBackend: getThirdPartyAuthContext,
|
getRegistrationDataFromBackend: getThirdPartyAuthContext,
|
||||||
resetUsernameSuggestions: clearUsernameSuggestions,
|
|
||||||
validateFromBackend: fetchRealtimeValidations,
|
validateFromBackend: fetchRealtimeValidations,
|
||||||
registerNewUser,
|
registerNewUser,
|
||||||
setUserPipelineDetailsLoaded: setUserPipelineDataLoaded,
|
setUserPipelineDetailsLoaded: setUserPipelineDataLoaded,
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import {
|
|||||||
DEFAULT_SERVICE_PROVIDER_DOMAINS,
|
DEFAULT_SERVICE_PROVIDER_DOMAINS,
|
||||||
DEFAULT_TOP_LEVEL_DOMAINS,
|
DEFAULT_TOP_LEVEL_DOMAINS,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
import {
|
||||||
|
INVALID_NAME_REGEX, VALID_EMAIL_REGEX,
|
||||||
|
} from '../../data/constants';
|
||||||
|
|
||||||
function getLevenshteinSuggestion(word, knownWords, similarityThreshold = 4) {
|
function getLevenshteinSuggestion(word, knownWords, similarityThreshold = 4) {
|
||||||
if (!word) {
|
if (!word) {
|
||||||
@@ -110,3 +113,6 @@ export function validateCountryField(value, countryList, errorMessage) {
|
|||||||
}
|
}
|
||||||
return { error, countryCode, displayValue };
|
return { error, countryCode, displayValue };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const emailRegex = new RegExp(VALID_EMAIL_REGEX, 'i');
|
||||||
|
export const urlRegex = new RegExp(INVALID_NAME_REGEX);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export { default as RegistrationPage } from './RegistrationPage';
|
export { default as RegistrationPage } from './RegistrationPage';
|
||||||
|
export { default as EmbeddableRegistrationPage } from './EmbeddableRegistrationPage';
|
||||||
export { default as reducer } from './data/reducers';
|
export { default as reducer } from './data/reducers';
|
||||||
export { default as saga } from './data/sagas';
|
export { default as saga } from './data/sagas';
|
||||||
export { storeName } from './data/selectors';
|
export { storeName } from './data/selectors';
|
||||||
|
|||||||
@@ -1,21 +1,72 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Button, Icon, IconButton } from '@edx/paragon';
|
import { Button, Icon, IconButton } from '@edx/paragon';
|
||||||
import { Close } from '@edx/paragon/icons';
|
import { Close } from '@edx/paragon/icons';
|
||||||
import PropTypes, { string } from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { FormGroup } from '../../common-components';
|
import { FormGroup } from '../../common-components';
|
||||||
|
import { clearUsernameSuggestions } from '../data/actions';
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
const UsernameField = (props) => {
|
const UsernameField = (props) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleSuggestionClick, handleUsernameSuggestionClose, usernameSuggestions, errorMessage,
|
value,
|
||||||
|
errorMessage,
|
||||||
|
handleChange,
|
||||||
|
handleFocus,
|
||||||
} = props;
|
} = props;
|
||||||
let className = '';
|
let className = '';
|
||||||
let suggestedUsernameDiv = null;
|
let suggestedUsernameDiv = null;
|
||||||
let iconButton = null;
|
let iconButton = null;
|
||||||
|
const usernameSuggestions = useSelector(state => state.register.usernameSuggestions);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We need to remove the placeholder from the field, adding a space will do that.
|
||||||
|
* This is needed because we are placing the username suggestions on top of the field.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (usernameSuggestions.length && !value) {
|
||||||
|
handleChange({ target: { name: 'username', value: ' ' } });
|
||||||
|
}
|
||||||
|
}, [handleChange, usernameSuggestions, value]);
|
||||||
|
|
||||||
|
const handleOnChange = (event) => {
|
||||||
|
let username = event.target.value;
|
||||||
|
if (username.length > 30) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.target.value.startsWith(' ')) {
|
||||||
|
username = username.trim();
|
||||||
|
}
|
||||||
|
handleChange({ target: { name: 'username', value: username } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnFocus = (event) => {
|
||||||
|
const username = event.target.value;
|
||||||
|
dispatch(clearUsernameSuggestions());
|
||||||
|
// If we added a space character to username field to display the suggestion
|
||||||
|
// remove it before user enters the input. This is to ensure user doesn't
|
||||||
|
// have a space prefixed to the username.
|
||||||
|
if (username === ' ') {
|
||||||
|
handleChange({ target: { name: 'username', value: '' } });
|
||||||
|
}
|
||||||
|
handleFocus({ target: { name: 'username', value: username } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSuggestionClick = (event, fieldName, suggestion = '') => {
|
||||||
|
event.preventDefault();
|
||||||
|
handleFocus({ target: { name: 'username', value: suggestion } }); // to clear the error if any
|
||||||
|
handleChange({ target: { name: 'username', value: suggestion } }); // to set suggestion as value
|
||||||
|
dispatch(clearUsernameSuggestions());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUsernameSuggestionClose = () => dispatch(clearUsernameSuggestions());
|
||||||
|
|
||||||
const suggestedUsernames = () => (
|
const suggestedUsernames = () => (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<span className="text-gray username-suggestion--label">{formatMessage(messages['registration.username.suggestion.label'])}</span>
|
<span className="text-gray username-suggestion--label">{formatMessage(messages['registration.username.suggestion.label'])}</span>
|
||||||
@@ -37,11 +88,12 @@ const UsernameField = (props) => {
|
|||||||
{iconButton}
|
{iconButton}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
if (usernameSuggestions.length > 0 && errorMessage && props.value === ' ') {
|
|
||||||
|
if (usernameSuggestions.length > 0 && errorMessage && value === ' ') {
|
||||||
className = 'username-suggestions__error';
|
className = 'username-suggestions__error';
|
||||||
iconButton = <IconButton src={Close} iconAs={Icon} alt="Close" onClick={() => handleUsernameSuggestionClose()} variant="black" size="sm" className="username-suggestions__close__button" />;
|
iconButton = <IconButton src={Close} iconAs={Icon} alt="Close" onClick={() => handleUsernameSuggestionClose()} variant="black" size="sm" className="username-suggestions__close__button" />;
|
||||||
suggestedUsernameDiv = suggestedUsernames();
|
suggestedUsernameDiv = suggestedUsernames();
|
||||||
} else if (usernameSuggestions.length > 0 && props.value === ' ') {
|
} else if (usernameSuggestions.length > 0 && value === ' ') {
|
||||||
className = 'username-suggestions d-flex align-items-center';
|
className = 'username-suggestions d-flex align-items-center';
|
||||||
iconButton = <IconButton src={Close} iconAs={Icon} alt="Close" onClick={() => handleUsernameSuggestionClose()} variant="black" size="sm" className="username-suggestions__close__button" />;
|
iconButton = <IconButton src={Close} iconAs={Icon} alt="Close" onClick={() => handleUsernameSuggestionClose()} variant="black" size="sm" className="username-suggestions__close__button" />;
|
||||||
suggestedUsernameDiv = suggestedUsernames();
|
suggestedUsernameDiv = suggestedUsernames();
|
||||||
@@ -49,22 +101,20 @@ const UsernameField = (props) => {
|
|||||||
suggestedUsernameDiv = suggestedUsernames();
|
suggestedUsernameDiv = suggestedUsernames();
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<FormGroup {...props}>
|
<FormGroup {...props} handleChange={handleOnChange} handleFocus={handleOnFocus}>
|
||||||
{suggestedUsernameDiv}
|
{suggestedUsernameDiv}
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
UsernameField.defaultProps = {
|
UsernameField.defaultProps = {
|
||||||
usernameSuggestions: [],
|
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
autoComplete: null,
|
autoComplete: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
UsernameField.propTypes = {
|
UsernameField.propTypes = {
|
||||||
usernameSuggestions: PropTypes.arrayOf(string),
|
handleChange: PropTypes.func.isRequired,
|
||||||
handleSuggestionClick: PropTypes.func.isRequired,
|
handleFocus: PropTypes.func.isRequired,
|
||||||
handleUsernameSuggestionClose: PropTypes.func.isRequired,
|
|
||||||
errorMessage: PropTypes.string,
|
errorMessage: PropTypes.string,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
value: PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
|
|||||||
707
src/register/tests/EmbeddableRegistrationPage.test.jsx
Normal file
707
src/register/tests/EmbeddableRegistrationPage.test.jsx
Normal file
@@ -0,0 +1,707 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
|
import { getConfig, mergeConfig } from '@edx/frontend-platform';
|
||||||
|
import { sendPageEvent } from '@edx/frontend-platform/analytics';
|
||||||
|
import {
|
||||||
|
configure, getLocale, injectIntl, IntlProvider,
|
||||||
|
} from '@edx/frontend-platform/i18n';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import renderer from 'react-test-renderer';
|
||||||
|
import configureStore from 'redux-mock-store';
|
||||||
|
|
||||||
|
import {
|
||||||
|
PENDING_STATE,
|
||||||
|
} from '../../data/constants';
|
||||||
|
import {
|
||||||
|
clearUsernameSuggestions,
|
||||||
|
registerNewUser,
|
||||||
|
} from '../data/actions';
|
||||||
|
import {
|
||||||
|
FIELDS,
|
||||||
|
} from '../data/constants';
|
||||||
|
import EmbeddableRegistrationPage from '../EmbeddableRegistrationPage';
|
||||||
|
|
||||||
|
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||||
|
sendPageEvent: jest.fn(),
|
||||||
|
sendTrackEvent: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||||
|
...jest.requireActual('@edx/frontend-platform/i18n'),
|
||||||
|
getLocale: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const IntlEmbedableRegistrationForm = injectIntl(EmbeddableRegistrationPage);
|
||||||
|
const mockStore = configureStore();
|
||||||
|
|
||||||
|
describe('RegistrationPage', () => {
|
||||||
|
mergeConfig({
|
||||||
|
PRIVACY_POLICY: 'https://privacy-policy.com',
|
||||||
|
TOS_AND_HONOR_CODE: 'https://tos-and-honot-code.com',
|
||||||
|
REGISTER_CONVERSION_COOKIE_NAME: process.env.REGISTER_CONVERSION_COOKIE_NAME,
|
||||||
|
});
|
||||||
|
|
||||||
|
let props = {};
|
||||||
|
let store = {};
|
||||||
|
const registrationFormData = {
|
||||||
|
configurableFormFields: {
|
||||||
|
marketingEmailsOptIn: true,
|
||||||
|
},
|
||||||
|
formFields: {
|
||||||
|
name: '', email: '', username: '', password: '',
|
||||||
|
},
|
||||||
|
emailSuggestion: {
|
||||||
|
suggestion: '', type: '',
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
name: '', email: '', username: '', password: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const reduxWrapper = children => (
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<Provider store={store}>{children}</Provider>
|
||||||
|
</IntlProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
register: {
|
||||||
|
registrationResult: { success: false, redirectUrl: '' },
|
||||||
|
registrationError: {},
|
||||||
|
registrationFormData,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
store = mockStore(initialState);
|
||||||
|
window.parent.postMessage = jest.fn();
|
||||||
|
configure({
|
||||||
|
loggingService: { logError: jest.fn() },
|
||||||
|
config: {
|
||||||
|
ENVIRONMENT: 'production',
|
||||||
|
LANGUAGE_PREFERENCE_COOKIE_NAME: 'yum',
|
||||||
|
},
|
||||||
|
messages: { 'es-419': {}, de: {}, 'en-us': {} },
|
||||||
|
});
|
||||||
|
props = {
|
||||||
|
registrationResult: jest.fn(),
|
||||||
|
handleInstitutionLogin: jest.fn(),
|
||||||
|
institutionLogin: false,
|
||||||
|
};
|
||||||
|
window.location = { search: '' };
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
const populateRequiredFields = (registrationPage, payload, isThirdPartyAuth = false) => {
|
||||||
|
registrationPage.find('input#name').simulate('change', { target: { value: payload.name, name: 'name' } });
|
||||||
|
registrationPage.find('input#username').simulate('change', { target: { value: payload.username, name: 'username' } });
|
||||||
|
registrationPage.find('input#email').simulate('change', { target: { value: payload.email, name: 'email' } });
|
||||||
|
|
||||||
|
registrationPage.find('input[name="country"]').simulate('change', { target: { value: payload.country, name: 'country' } });
|
||||||
|
registrationPage.find('input[name="country"]').simulate('blur', { target: { value: payload.country, name: 'country' } });
|
||||||
|
|
||||||
|
if (!isThirdPartyAuth) {
|
||||||
|
registrationPage.find('input#password').simulate('change', { target: { value: payload.password, name: 'password' } });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Test Registration Page', () => {
|
||||||
|
mergeConfig({
|
||||||
|
SHOW_CONFIGURABLE_EDX_FIELDS: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emptyFieldValidation = {
|
||||||
|
name: 'Enter your full name',
|
||||||
|
username: 'Username must be between 2 and 30 characters',
|
||||||
|
email: 'Enter your email',
|
||||||
|
password: 'Password criteria has not been met',
|
||||||
|
country: 'Select your country or region of residence',
|
||||||
|
};
|
||||||
|
|
||||||
|
// ******** test registration form submission ********
|
||||||
|
|
||||||
|
it('should submit form for valid input', () => {
|
||||||
|
getLocale.mockImplementation(() => ('en-us'));
|
||||||
|
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||||
|
|
||||||
|
delete window.location;
|
||||||
|
window.location = { href: getConfig().BASE_URL, search: '?next=/course/demo-course-url' };
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
name: 'John Doe',
|
||||||
|
username: 'john_doe',
|
||||||
|
email: 'john.doe@gmail.com',
|
||||||
|
password: 'password1',
|
||||||
|
country: 'Pakistan',
|
||||||
|
honor_code: true,
|
||||||
|
totalRegistrationTime: 0,
|
||||||
|
next: '/course/demo-course-url',
|
||||||
|
};
|
||||||
|
|
||||||
|
store.dispatch = jest.fn(store.dispatch);
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
populateRequiredFields(registrationPage, payload);
|
||||||
|
registrationPage.find('button.btn-brand').simulate('click');
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should submit form with marketing email opt in value', () => {
|
||||||
|
mergeConfig({
|
||||||
|
MARKETING_EMAILS_OPT_IN: 'true',
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
name: 'John Doe',
|
||||||
|
username: 'john_doe',
|
||||||
|
email: 'john.doe@gmail.com',
|
||||||
|
password: 'password1',
|
||||||
|
country: 'Pakistan',
|
||||||
|
honor_code: true,
|
||||||
|
totalRegistrationTime: 0,
|
||||||
|
marketing_emails_opt_in: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
store.dispatch = jest.fn(store.dispatch);
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
populateRequiredFields(registrationPage, payload);
|
||||||
|
registrationPage.find('button.btn-brand').simulate('click');
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||||
|
|
||||||
|
mergeConfig({
|
||||||
|
MARKETING_EMAILS_OPT_IN: '',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not dispatch registerNewUser on empty form Submission', () => {
|
||||||
|
store.dispatch = jest.fn(store.dispatch);
|
||||||
|
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
registrationPage.find('button.btn-brand').simulate('click');
|
||||||
|
expect(store.dispatch).not.toHaveBeenCalledWith(registerNewUser({}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// // ******** test registration form validations ********
|
||||||
|
|
||||||
|
it('should show error messages for required fields on empty form submission', () => {
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
registrationPage.find('button.btn-brand').simulate('click');
|
||||||
|
|
||||||
|
expect(registrationPage.find('div[feedback-for="name"]').text()).toEqual(emptyFieldValidation.name);
|
||||||
|
expect(registrationPage.find('div[feedback-for="username"]').text()).toEqual(emptyFieldValidation.username);
|
||||||
|
expect(registrationPage.find('div[feedback-for="email"]').text()).toEqual(emptyFieldValidation.email);
|
||||||
|
expect(registrationPage.find('div[feedback-for="password"]').text()).toContain(emptyFieldValidation.password);
|
||||||
|
expect(registrationPage.find('div[feedback-for="country"]').text()).toEqual(emptyFieldValidation.country);
|
||||||
|
|
||||||
|
const alertBanner = 'We couldn\'t create your account.Please check your responses and try again.';
|
||||||
|
expect(registrationPage.find('#validation-errors').first().text()).toEqual(alertBanner);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run validations for focused field on form submission', () => {
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
registrationPage.find('input[name="country"]').simulate('focus');
|
||||||
|
registrationPage.find('button.btn-brand').simulate('click');
|
||||||
|
|
||||||
|
expect(registrationPage.find('div[feedback-for="country"]').text()).toEqual(emptyFieldValidation.country);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update props with validations returned by registration api', () => {
|
||||||
|
store = mockStore({
|
||||||
|
...initialState,
|
||||||
|
register: {
|
||||||
|
...initialState.register,
|
||||||
|
registrationError: {
|
||||||
|
username: [{ userMessage: 'It looks like this username is already taken' }],
|
||||||
|
email: [{ userMessage: `This email is already associated with an existing or previous ${ getConfig().SITE_NAME } account` }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />)).find('EmbeddableRegistrationPage');
|
||||||
|
expect(registrationPage.prop('backendValidations')).toEqual({
|
||||||
|
email: `This email is already associated with an existing or previous ${ getConfig().SITE_NAME } account`,
|
||||||
|
username: 'It looks like this username is already taken',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove space from the start of username', () => {
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
registrationPage.find('input#username').simulate('change', { target: { value: ' test-user', name: 'username' } });
|
||||||
|
|
||||||
|
expect(registrationPage.find('input#username').prop('value')).toEqual('test-user');
|
||||||
|
});
|
||||||
|
it('should run username and email frontend validations', () => {
|
||||||
|
const payload = {
|
||||||
|
name: 'John Doe',
|
||||||
|
username: 'test@2u.com',
|
||||||
|
email: 'test@yopmail.test',
|
||||||
|
password: 'password1',
|
||||||
|
country: 'Pakistan',
|
||||||
|
honor_code: true,
|
||||||
|
totalRegistrationTime: 0,
|
||||||
|
marketing_emails_opt_in: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
store.dispatch = jest.fn(store.dispatch);
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
populateRequiredFields(registrationPage, payload);
|
||||||
|
registrationPage.find('input[name="email"]').simulate('focus');
|
||||||
|
registrationPage.find('input[name="email"]').simulate('blur', { target: { value: 'test@yopmail.test', name: 'email' } });
|
||||||
|
expect(registrationPage.find('.email-suggestion__text').exists()).toBeTruthy();
|
||||||
|
|
||||||
|
registrationPage.find('input[name="email"]').simulate('focus');
|
||||||
|
registrationPage.find('input[name="email"]').simulate('blur', { target: { value: 'asasasasas', name: 'email' } });
|
||||||
|
|
||||||
|
registrationPage.find('button.btn-brand').simulate('click');
|
||||||
|
expect(registrationPage.find('div[feedback-for="email"]').exists()).toBeTruthy();
|
||||||
|
expect(registrationPage.find('div[feedback-for="username"]').exists()).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should run email frontend validations when random string is input', () => {
|
||||||
|
const payload = {
|
||||||
|
name: 'John Doe',
|
||||||
|
username: 'testh@2u.com',
|
||||||
|
email: 'as',
|
||||||
|
password: 'password1',
|
||||||
|
country: 'Pakistan',
|
||||||
|
honor_code: true,
|
||||||
|
totalRegistrationTime: 0,
|
||||||
|
marketing_emails_opt_in: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
populateRequiredFields(registrationPage, payload);
|
||||||
|
|
||||||
|
registrationPage.find('button.btn-brand').simulate('click');
|
||||||
|
expect(registrationPage.find('div[feedback-for="email"]').exists()).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should run frontend validations for name field', () => {
|
||||||
|
const payload = {
|
||||||
|
name: 'https://localhost.com',
|
||||||
|
username: 'test@2u.com',
|
||||||
|
email: 'as',
|
||||||
|
password: 'password1',
|
||||||
|
country: 'Pakistan',
|
||||||
|
honor_code: true,
|
||||||
|
totalRegistrationTime: 0,
|
||||||
|
marketing_emails_opt_in: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
populateRequiredFields(registrationPage, payload);
|
||||||
|
|
||||||
|
registrationPage.find('button.btn-brand').simulate('click');
|
||||||
|
expect(registrationPage.find('div[feedback-for="name"]').exists()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run frontend validations for password field', () => {
|
||||||
|
const payload = {
|
||||||
|
name: 'https://localhost.com',
|
||||||
|
username: 'test@2u.com',
|
||||||
|
email: 'as',
|
||||||
|
password: 'as',
|
||||||
|
country: 'Pakistan',
|
||||||
|
honor_code: true,
|
||||||
|
totalRegistrationTime: 0,
|
||||||
|
marketing_emails_opt_in: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
populateRequiredFields(registrationPage, payload);
|
||||||
|
|
||||||
|
registrationPage.find('button.btn-brand').simulate('click');
|
||||||
|
expect(registrationPage.find('div[feedback-for="password"]').exists()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should click on email suggestion in case suggestion is avialable', () => {
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
registrationPage.find('input[name="email"]').simulate('focus');
|
||||||
|
registrationPage.find('input[name="email"]').simulate('blur', { target: { value: 'test@gmail.co', name: 'email' } });
|
||||||
|
|
||||||
|
registrationPage.find('a.email-suggestion-alert-warning').simulate('click');
|
||||||
|
expect(registrationPage.find('input#email').prop('value')).toEqual('test@gmail.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove extra character if username is more than 30 character long', () => {
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
registrationPage.find('input#username').simulate('change', { target: { value: 'why_this_is_not_valid_username_', name: 'username' } });
|
||||||
|
|
||||||
|
expect(registrationPage.find('input#username').prop('value')).toEqual('');
|
||||||
|
});
|
||||||
|
|
||||||
|
// // ******** test field focus in functionality ********
|
||||||
|
|
||||||
|
it('should clear field related error messages on input field Focus', () => {
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
registrationPage.find('button.btn-brand').simulate('click');
|
||||||
|
|
||||||
|
expect(registrationPage.find('div[feedback-for="name"]').text()).toEqual(emptyFieldValidation.name);
|
||||||
|
registrationPage.find('input#name').simulate('focus');
|
||||||
|
expect(registrationPage.find('div[feedback-for="name"]').exists()).toBeFalsy();
|
||||||
|
|
||||||
|
expect(registrationPage.find('div[feedback-for="username"]').text()).toEqual(emptyFieldValidation.username);
|
||||||
|
registrationPage.find('input#username').simulate('focus');
|
||||||
|
expect(registrationPage.find('div[feedback-for="username"]').exists()).toBeFalsy();
|
||||||
|
|
||||||
|
expect(registrationPage.find('div[feedback-for="email"]').text()).toEqual(emptyFieldValidation.email);
|
||||||
|
registrationPage.find('input#email').simulate('focus');
|
||||||
|
expect(registrationPage.find('div[feedback-for="email"]').exists()).toBeFalsy();
|
||||||
|
|
||||||
|
expect(registrationPage.find('div[feedback-for="password"]').text()).toContain(emptyFieldValidation.password);
|
||||||
|
registrationPage.find('input#password').simulate('focus');
|
||||||
|
expect(registrationPage.find('div[feedback-for="password"]').exists()).toBeFalsy();
|
||||||
|
|
||||||
|
expect(registrationPage.find('div[feedback-for="country"]').text()).toEqual(emptyFieldValidation.country);
|
||||||
|
registrationPage.find('input[name="country"]').simulate('focus');
|
||||||
|
expect(registrationPage.find('div[feedback-for="country"]').exists()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear username suggestions when username field is focused in', () => {
|
||||||
|
store.dispatch = jest.fn(store.dispatch);
|
||||||
|
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
registrationPage.find('input#username').simulate('focus');
|
||||||
|
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(clearUsernameSuggestions());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call backend api for username suggestions when input the name field', () => {
|
||||||
|
store.dispatch = jest.fn(store.dispatch);
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
|
||||||
|
registrationPage.find('input#name').simulate('focus');
|
||||||
|
registrationPage.find('input#name').simulate('change', { target: { value: 'ahtesham', name: 'name' } });
|
||||||
|
registrationPage.find('input#name').simulate('blur');
|
||||||
|
|
||||||
|
expect(store.dispatch).toHaveBeenCalledTimes(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
// // ******** test form buttons and fields ********
|
||||||
|
|
||||||
|
it('should match default button state', () => {
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
expect(registrationPage.find('button[type="submit"] span').first().text())
|
||||||
|
.toEqual('Create an account for free');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match pending button state', () => {
|
||||||
|
store = mockStore({
|
||||||
|
...initialState,
|
||||||
|
register: {
|
||||||
|
...initialState.register,
|
||||||
|
submitState: PENDING_STATE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
const button = registrationPage.find('button[type="submit"] span').first();
|
||||||
|
|
||||||
|
expect(button.find('.sr-only').text()).toEqual('pending');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display opt-in/opt-out checkbox', () => {
|
||||||
|
mergeConfig({
|
||||||
|
MARKETING_EMAILS_OPT_IN: 'true',
|
||||||
|
});
|
||||||
|
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
expect(registrationPage.find('div.form-field--checkbox').length).toEqual(1);
|
||||||
|
|
||||||
|
mergeConfig({
|
||||||
|
MARKETING_EMAILS_OPT_IN: '',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show button label based on cta query params value', () => {
|
||||||
|
const buttonLabel = 'Register';
|
||||||
|
delete window.location;
|
||||||
|
window.location = { href: getConfig().BASE_URL, search: `?cta=${buttonLabel}` };
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
expect(registrationPage.find('button[type="submit"] span').first().text()).toEqual(buttonLabel);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check registration conversion cookie', () => {
|
||||||
|
store = mockStore({
|
||||||
|
...initialState,
|
||||||
|
register: {
|
||||||
|
...initialState.register,
|
||||||
|
registrationResult: {
|
||||||
|
success: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
renderer.create(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
expect(document.cookie).toMatch(`${getConfig().REGISTER_CONVERSION_COOKIE_NAME}=true`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show username suggestions in case of conflict with an existing username', () => {
|
||||||
|
store = mockStore({
|
||||||
|
...initialState,
|
||||||
|
register: {
|
||||||
|
...initialState.register,
|
||||||
|
usernameSuggestions: ['test_1', 'test_12', 'test_123'],
|
||||||
|
registrationFormData: {
|
||||||
|
...registrationFormData,
|
||||||
|
errors: {
|
||||||
|
...registrationFormData.errors,
|
||||||
|
username: 'It looks like this username is already taken',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
expect(registrationPage.find('button.username-suggestions--chip').length).toEqual(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show username suggestions when full name is populated', () => {
|
||||||
|
store = mockStore({
|
||||||
|
...initialState,
|
||||||
|
register: {
|
||||||
|
...initialState.register,
|
||||||
|
usernameSuggestions: ['test_1', 'test_12', 'test_123'],
|
||||||
|
registrationFormData: {
|
||||||
|
...registrationFormData,
|
||||||
|
username: ' ',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
registrationPage.find('input#name').simulate('change', { target: { value: 'test name', name: 'name' } });
|
||||||
|
|
||||||
|
expect(registrationPage.find('button.username-suggestions--chip').length).toEqual(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove empty space from username field when it is focused', async () => {
|
||||||
|
store = mockStore({
|
||||||
|
...initialState,
|
||||||
|
register: {
|
||||||
|
...initialState.register,
|
||||||
|
usernameSuggestions: ['test_1', 'test_12', 'test_123'],
|
||||||
|
registrationFormData: {
|
||||||
|
...registrationFormData,
|
||||||
|
username: ' ',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
registrationPage.find('input#name').simulate('change', { target: { value: 'test name', name: 'name' } });
|
||||||
|
registrationPage.find('input#username').simulate('focus');
|
||||||
|
await act(async () => { await expect(registrationPage.find('input#username').text()).toEqual(''); });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should click on username suggestions when full name is populated', () => {
|
||||||
|
store = mockStore({
|
||||||
|
...initialState,
|
||||||
|
register: {
|
||||||
|
...initialState.register,
|
||||||
|
usernameSuggestions: ['test_1', 'test_12', 'test_123'],
|
||||||
|
registrationFormData: {
|
||||||
|
...registrationFormData,
|
||||||
|
username: ' ',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
registrationPage.find('input#name').simulate('change', { target: { value: 'test name', name: 'name' } });
|
||||||
|
registrationPage.find('.username-suggestions--chip').first().simulate('click');
|
||||||
|
expect(registrationPage.find('input#username').props().value).toEqual('test_1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear username suggestions when close icon is clicked', () => {
|
||||||
|
store = mockStore({
|
||||||
|
...initialState,
|
||||||
|
register: {
|
||||||
|
...initialState.register,
|
||||||
|
usernameSuggestions: ['test_1', 'test_12', 'test_123'],
|
||||||
|
registrationFormData: {
|
||||||
|
...registrationFormData,
|
||||||
|
username: ' ',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
store.dispatch = jest.fn(store.dispatch);
|
||||||
|
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
registrationPage.find('input#name').simulate('change', { target: { value: 'test name', name: 'name' } });
|
||||||
|
registrationPage.find('button.username-suggestions__close__button').at(0).simulate('click');
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(clearUsernameSuggestions());
|
||||||
|
});
|
||||||
|
|
||||||
|
// // ******** miscellaneous tests ********
|
||||||
|
|
||||||
|
it('should send page event when register page is rendered', () => {
|
||||||
|
mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update state from country code present in redux store', () => {
|
||||||
|
store = mockStore({
|
||||||
|
...initialState,
|
||||||
|
register: {
|
||||||
|
...initialState.register,
|
||||||
|
backendCountryCode: 'PK',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
expect(registrationPage.find('input[name="country"]').props().value).toEqual('Pakistan');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set country in component state when form is translated used i18n', () => {
|
||||||
|
getLocale.mockImplementation(() => ('ar-ae'));
|
||||||
|
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
registrationPage.find('input[name="country"]').simulate('click');
|
||||||
|
registrationPage.find('button.dropdown-item').at(0).simulate('click', { target: { value: 'أفغانستان ', name: 'countryItem' } });
|
||||||
|
expect(registrationPage.find('div[feedback-for="country"]').exists()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear the registation validation error on change event on field focused', () => {
|
||||||
|
store = mockStore({
|
||||||
|
...initialState,
|
||||||
|
register: {
|
||||||
|
...initialState.register,
|
||||||
|
registrationError: {
|
||||||
|
errorCode: 'duplicate-email',
|
||||||
|
email: [{ userMessage: `This email is already associated with an existing or previous ${ getConfig().SITE_NAME } account` }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
store.dispatch = jest.fn(store.dispatch);
|
||||||
|
const clearBackendError = jest.fn();
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} {...clearBackendError} />));
|
||||||
|
registrationPage.find('input#email').simulate('change', { target: { value: 'a@gmail.com', name: 'email' } });
|
||||||
|
expect(registrationPage.find('div[feedback-for="email"]').exists()).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test Configurable Fields', () => {
|
||||||
|
mergeConfig({
|
||||||
|
ENABLE_DYNAMIC_REGISTRATION_FIELDS: true,
|
||||||
|
SHOW_CONFIGURABLE_EDX_FIELDS: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render fields returned by backend', () => {
|
||||||
|
store = mockStore({
|
||||||
|
...initialState,
|
||||||
|
commonComponents: {
|
||||||
|
...initialState.commonComponents,
|
||||||
|
fieldDescriptions: {
|
||||||
|
profession: { name: 'profession', type: 'text', label: 'Profession' },
|
||||||
|
terms_of_service: {
|
||||||
|
name: FIELDS.TERMS_OF_SERVICE,
|
||||||
|
error_message: 'You must agree to the Terms and Service agreement of our site',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
expect(registrationPage.find('#profession').exists()).toBeTruthy();
|
||||||
|
expect(registrationPage.find('#tos').exists()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should submit form with fields returned by backend in payload', () => {
|
||||||
|
getLocale.mockImplementation(() => ('en-us'));
|
||||||
|
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||||
|
store = mockStore({
|
||||||
|
...initialState,
|
||||||
|
commonComponents: {
|
||||||
|
...initialState.commonComponents,
|
||||||
|
fieldDescriptions: {
|
||||||
|
profession: { name: 'profession', type: 'text', label: 'Profession' },
|
||||||
|
},
|
||||||
|
extendedProfile: ['profession'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
name: 'John Doe',
|
||||||
|
username: 'john_doe',
|
||||||
|
email: 'john.doe@example.com',
|
||||||
|
password: 'password1',
|
||||||
|
country: 'Pakistan',
|
||||||
|
honor_code: true,
|
||||||
|
profession: 'Engineer',
|
||||||
|
totalRegistrationTime: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
store.dispatch = jest.fn(store.dispatch);
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
|
||||||
|
populateRequiredFields(registrationPage, payload);
|
||||||
|
registrationPage.find('input#profession').simulate('change', { target: { value: 'Engineer', name: 'profession' } });
|
||||||
|
registrationPage.find('button.btn-brand').simulate('click');
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show error messages for required fields on empty form submission', () => {
|
||||||
|
const professionError = 'Enter your profession';
|
||||||
|
const countryError = 'Select your country or region of residence';
|
||||||
|
const confirmEmailError = 'Enter your email';
|
||||||
|
|
||||||
|
store = mockStore({
|
||||||
|
...initialState,
|
||||||
|
commonComponents: {
|
||||||
|
...initialState.commonComponents,
|
||||||
|
fieldDescriptions: {
|
||||||
|
profession: {
|
||||||
|
name: 'profession', type: 'text', label: 'Profession', error_message: professionError,
|
||||||
|
},
|
||||||
|
confirm_email: {
|
||||||
|
name: 'confirm_email', type: 'text', label: 'Confirm Email', error_message: confirmEmailError,
|
||||||
|
},
|
||||||
|
country: { name: 'country' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
registrationPage.find('button.btn-brand').simulate('click');
|
||||||
|
|
||||||
|
expect(registrationPage.find('#profession-error').last().text()).toEqual(professionError);
|
||||||
|
expect(registrationPage.find('div[feedback-for="country"]').text()).toEqual(countryError);
|
||||||
|
expect(registrationPage.find('#confirm_email-error').last().text()).toEqual(confirmEmailError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run validations for configurable focused field on form submission', () => {
|
||||||
|
const professionError = 'Enter your profession';
|
||||||
|
store = mockStore({
|
||||||
|
...initialState,
|
||||||
|
commonComponents: {
|
||||||
|
...initialState.commonComponents,
|
||||||
|
fieldDescriptions: {
|
||||||
|
profession: {
|
||||||
|
name: 'profession', type: 'text', label: 'Profession', error_message: professionError,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
registrationPage.find('input#profession').simulate('focus', { target: { value: '', name: 'profession' } });
|
||||||
|
registrationPage.find('button.btn-brand').simulate('click');
|
||||||
|
|
||||||
|
expect(registrationPage.find('#profession-error').last().text()).toEqual(professionError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not run country field validation when onBlur is fired by drop-down arrow icon click', () => {
|
||||||
|
getLocale.mockImplementation(() => ('en-us'));
|
||||||
|
|
||||||
|
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||||
|
registrationPage.find('input[name="country"]').simulate('blur', {
|
||||||
|
target: { value: '', name: 'country' },
|
||||||
|
relatedTarget: { type: 'button', className: 'btn-icon pgn__form-autosuggest__icon-button' },
|
||||||
|
});
|
||||||
|
expect(registrationPage.find('div[feedback-for="country"]').exists()).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -270,18 +270,6 @@ describe('RegistrationPage', () => {
|
|||||||
|
|
||||||
// ******** test registration form validations ********
|
// ******** test registration form validations ********
|
||||||
|
|
||||||
it('should not run validations on blur event when embedded variant is rendered', () => {
|
|
||||||
delete window.location;
|
|
||||||
window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: '?host=http://localhost/host-website' };
|
|
||||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
|
||||||
|
|
||||||
registrationPage.find('input#username').simulate('blur', { target: { value: '', name: 'username' } });
|
|
||||||
expect(registrationPage.find('div[feedback-for="username"]').exists()).toBeFalsy();
|
|
||||||
|
|
||||||
registrationPage.find('input[name="country"]').simulate('blur', { target: { value: '', name: 'country' } });
|
|
||||||
expect(registrationPage.find('div[feedback-for="country"]').exists()).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show error messages for required fields on empty form submission', () => {
|
it('should show error messages for required fields on empty form submission', () => {
|
||||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||||
registrationPage.find('button.btn-brand').simulate('click');
|
registrationPage.find('button.btn-brand').simulate('click');
|
||||||
@@ -639,14 +627,6 @@ describe('RegistrationPage', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show button label based on cta query params value', () => {
|
|
||||||
const buttonLabel = 'Register';
|
|
||||||
delete window.location;
|
|
||||||
window.location = { href: getConfig().BASE_URL, search: `?cta=${buttonLabel}` };
|
|
||||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
|
||||||
expect(registrationPage.find('button[type="submit"] span').first().text()).toEqual(buttonLabel);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should display no password field when current provider is present', () => {
|
it('should display no password field when current provider is present', () => {
|
||||||
store = mockStore({
|
store = mockStore({
|
||||||
...initialState,
|
...initialState,
|
||||||
@@ -902,41 +882,6 @@ describe('RegistrationPage', () => {
|
|||||||
expect(registrationPage.find(`button#${ssoProvider.id}`).hasClass(`btn-tpa btn-${ssoProvider.id}`)).toEqual(true);
|
expect(registrationPage.find(`button#${ssoProvider.id}`).hasClass(`btn-tpa btn-${ssoProvider.id}`)).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call the postMessage API when embedded variant is rendered', () => {
|
|
||||||
getLocale.mockImplementation(() => ('en-us'));
|
|
||||||
mergeConfig({
|
|
||||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
window.parent.postMessage = jest.fn();
|
|
||||||
|
|
||||||
delete window.location;
|
|
||||||
window.location = { href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING), search: '?host=http://localhost/host-website' };
|
|
||||||
|
|
||||||
store = mockStore({
|
|
||||||
...initialState,
|
|
||||||
register: {
|
|
||||||
...initialState.register,
|
|
||||||
registrationResult: {
|
|
||||||
success: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
commonComponents: {
|
|
||||||
optionalFields: {
|
|
||||||
extended_profile: {},
|
|
||||||
fields: {
|
|
||||||
level_of_education: { name: 'level_of_education', error_message: false },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const progressiveProfilingPage = mount(reduxWrapper(
|
|
||||||
<IntlRegistrationPage {...props} />,
|
|
||||||
));
|
|
||||||
progressiveProfilingPage.update();
|
|
||||||
expect(window.parent.postMessage).toHaveBeenCalledTimes(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render icon if icon classes are missing in providers', () => {
|
it('should render icon if icon classes are missing in providers', () => {
|
||||||
ssoProvider.iconClass = null;
|
ssoProvider.iconClass = null;
|
||||||
store = mockStore({
|
store = mockStore({
|
||||||
|
|||||||
Reference in New Issue
Block a user