+
{
email={formFields.email}
fieldErrors={errors}
formFields={configurableFormFields}
- setFieldErrors={registrationEmbedded ? setTemporaryErrors : setErrors}
+ setFieldErrors={setErrors}
setFormFields={setConfigurableFormFields}
autoSubmitRegisterForm={autoSubmitRegForm}
fieldDescriptions={fieldDescriptions}
@@ -378,21 +345,19 @@ const RegistrationPage = (props) => {
className="register-button mt-4 mb-4"
state={submitState}
labels={{
- default: buttonLabel,
+ default: formatMessage(messages['create.account.for.free.button']),
pending: '',
}}
onClick={handleSubmit}
onMouseDown={(e) => e.preventDefault()}
/>
- {!registrationEmbedded && (
-
- )}
+
)}
diff --git a/src/register/RegistrationPage.test.jsx b/src/register/RegistrationPage.test.jsx
index 5310f162..c7dc1326 100644
--- a/src/register/RegistrationPage.test.jsx
+++ b/src/register/RegistrationPage.test.jsx
@@ -503,14 +503,6 @@ describe('RegistrationPage', () => {
expect(registrationPage.find('.institutions__heading').text()).toEqual('Register with institution/campus credentials');
});
- 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(
));
- expect(registrationPage.find('button[type="submit"] span').first().text()).toEqual(buttonLabel);
- });
-
it('should not display password field when current provider is present', () => {
store = mockStore({
...initialState,
@@ -892,96 +884,6 @@ describe('RegistrationPage', () => {
expect(registrationPage.find('.email-suggestion-alert-warning').first().text()).toEqual('john.doe@hotmail.com');
});
- // ********* Embedded experience tests *********/
-
- 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: {
- ...initialState.commonComponents,
- optionalFields: {
- extended_profile: {},
- fields: {
- level_of_education: { name: 'level_of_education', error_message: false },
- },
- },
- },
- });
- const progressiveProfilingPage = mount(reduxWrapper(
-
,
- ));
- progressiveProfilingPage.update();
- expect(window.parent.postMessage).toHaveBeenCalledTimes(2);
- });
-
- it('should not display validations error 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(
));
-
- 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 set errors in temporary state when validations are returned by registration api', () => {
- delete window.location;
- window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: '?host=http://localhost/host-website' };
-
- const usernameError = 'It looks like this username is already taken';
- const emailError = 'This email is already associated with an existing or previous account';
- store = mockStore({
- ...initialState,
- register: {
- ...initialState.register,
- registrationError: {
- username: [{ userMessage: usernameError }],
- email: [{ userMessage: emailError }],
- },
- },
- });
- const registrationPage = mount(routerWrapper(reduxWrapper(
-
),
- )).find('RegistrationPage');
-
- expect(registrationPage.find('div[feedback-for="username"]').exists()).toBeFalsy();
- expect(registrationPage.find('div[feedback-for="email"]').exists()).toBeFalsy();
- });
-
- it('should clear error on focus for embedded experience also', () => {
- delete window.location;
- window.location = {
- href: getConfig().BASE_URL.concat(REGISTER_PAGE),
- search: '?host=http://localhost/host-website',
- };
-
- const registrationPage = mount(routerWrapper(reduxWrapper(
)));
- registrationPage.find('button.btn-brand').simulate('click');
-
- 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();
- });
-
it('should show spinner instead of form while registering if autoSubmitRegForm is true', () => {
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
getLocale.mockImplementation(() => ('en-us'));
diff --git a/src/register/components/EmbeddableRegistrationPage.jsx b/src/register/components/EmbeddableRegistrationPage.jsx
new file mode 100644
index 00000000..e4388c59
--- /dev/null
+++ b/src/register/components/EmbeddableRegistrationPage.jsx
@@ -0,0 +1,476 @@
+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 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, REDIRECT,
+} 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) => {
+ switch (fieldName) {
+ case 'name':
+ if (value && !payload.username.trim() && shouldValidateFromBackend) {
+ validateFromBackend(payload);
+ }
+ break;
+ default:
+ break;
+ }
+ };
+
+ 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 '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,
+ );
+ }
+ };
+
+ 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;
+ }
+
+ payload = snakeCaseObject(payload);
+ payload.totalRegistrationTime = totalRegistrationTime;
+
+ // add query params to the payload
+ payload = { ...payload, ...queryParams };
+ props.registerNewUser(payload);
+ };
+
+ return (
+ <>
+
+ {formatMessage(messages['register.page.title'], { siteName: getConfig().SITE_NAME })}
+
+
+
+ >
+ );
+};
+
+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);
diff --git a/src/register/components/ConfigurableRegistrationForm.test.jsx b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
similarity index 97%
rename from src/register/components/ConfigurableRegistrationForm.test.jsx
rename to src/register/components/tests/ConfigurableRegistrationForm.test.jsx
index 6f42eb75..c22e6091 100644
--- a/src/register/components/ConfigurableRegistrationForm.test.jsx
+++ b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx
@@ -9,8 +9,8 @@ import { mount } from 'enzyme';
import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
-import ConfigurableRegistrationForm from './ConfigurableRegistrationForm';
-import { FIELDS } from '../data/constants';
+import ConfigurableRegistrationForm from '../ConfigurableRegistrationForm';
+import { FIELDS } from '../../data/constants';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendPageEvent: jest.fn(),
diff --git a/src/register/components/tests/EmbeddableRegistrationPage.test.jsx b/src/register/components/tests/EmbeddableRegistrationPage.test.jsx
new file mode 100644
index 00000000..1ec36625
--- /dev/null
+++ b/src/register/components/tests/EmbeddableRegistrationPage.test.jsx
@@ -0,0 +1,616 @@
+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 => (
+
+ {children}
+
+ );
+
+ 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(
));
+ 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(
));
+ 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(
));
+ 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(
));
+ 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(
));
+ 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(
)).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(
));
+ registrationPage.find('input#username').simulate('change', { target: { value: ' test-user', name: 'username' } });
+
+ expect(registrationPage.find('input#username').prop('value')).toEqual('test-user');
+ });
+ it('should remove extra character if username is more than 30 character long', () => {
+ const registrationPage = mount(reduxWrapper(
));
+ 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(
));
+ 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(
));
+ 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(
));
+
+ 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(
));
+ 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(
));
+ 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(
));
+ 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(
));
+ 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(
));
+ 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(
));
+ 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(
));
+ 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(
));
+ 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(
));
+ 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(
));
+ 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(
));
+ 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(
));
+ 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(
));
+ 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(
));
+ 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(
));
+ 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(
));
+
+ 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(
));
+ 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(
));
+ 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(
));
+ 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();
+ });
+ });
+});
diff --git a/src/register/index.js b/src/register/index.js
index eb48077a..8b37e689 100644
--- a/src/register/index.js
+++ b/src/register/index.js
@@ -1,4 +1,5 @@
export { default as RegistrationPage } from './RegistrationPage';
+export { default as EmbeddableRegistrationPage } from './components/EmbeddableRegistrationPage';
export { default as reducer } from './data/reducers';
export { default as saga } from './data/sagas';
export { storeName } from './data/reducers';