diff --git a/src/_style.scss b/src/_style.scss
index ea43c55c..951e825b 100644
--- a/src/_style.scss
+++ b/src/_style.scss
@@ -624,6 +624,7 @@ select.form-control {
position: absolute;
background-color: #fff;
width: 464px;
+ z-index: 100 !important;
}
.email-error-alert {
@@ -857,7 +858,6 @@ select.form-control {
font-size: 0.75rem;
line-height: 1.25rem;
}
- margin-top: 1rem;
margin-left: 3px;
}
.suggested-username {
diff --git a/src/common-components/data/actions.js b/src/common-components/data/actions.js
index 28a34982..6eb655b5 100644
--- a/src/common-components/data/actions.js
+++ b/src/common-components/data/actions.js
@@ -12,9 +12,9 @@ export const getThirdPartyAuthContextBegin = () => ({
type: THIRD_PARTY_AUTH_CONTEXT.BEGIN,
});
-export const getThirdPartyAuthContextSuccess = (thirdPartyAuthContext) => ({
+export const getThirdPartyAuthContextSuccess = (fieldDescriptions, thirdPartyAuthContext) => ({
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
- payload: { thirdPartyAuthContext },
+ payload: { fieldDescriptions, thirdPartyAuthContext },
});
export const getThirdPartyAuthContextFailure = () => ({
diff --git a/src/common-components/data/reducers.js b/src/common-components/data/reducers.js
index 6a944bd2..9cac42b2 100644
--- a/src/common-components/data/reducers.js
+++ b/src/common-components/data/reducers.js
@@ -3,6 +3,8 @@ import { THIRD_PARTY_AUTH_CONTEXT } from './actions';
import { PENDING_STATE, COMPLETE_STATE } from '../../data/constants';
export const defaultState = {
+ extendedProfile: [],
+ fieldDescriptions: {},
thirdPartyAuthApiStatus: null,
};
@@ -16,6 +18,8 @@ const reducer = (state = defaultState, action) => {
case THIRD_PARTY_AUTH_CONTEXT.SUCCESS:
return {
...state,
+ extendedProfile: action.payload.fieldDescriptions.extendedProfile,
+ fieldDescriptions: action.payload.fieldDescriptions.fields,
thirdPartyAuthContext: action.payload.thirdPartyAuthContext,
thirdPartyAuthApiStatus: COMPLETE_STATE,
};
diff --git a/src/common-components/data/sagas.js b/src/common-components/data/sagas.js
index d3ac7f03..4c56845a 100644
--- a/src/common-components/data/sagas.js
+++ b/src/common-components/data/sagas.js
@@ -18,10 +18,10 @@ import {
export function* fetchThirdPartyAuthContext(action) {
try {
yield put(getThirdPartyAuthContextBegin());
- const { thirdPartyAuthContext } = yield call(getThirdPartyAuthContext, action.payload.urlParams);
+ const { fieldDescriptions, thirdPartyAuthContext } = yield call(getThirdPartyAuthContext, action.payload.urlParams);
yield put(getThirdPartyAuthContextSuccess(
- thirdPartyAuthContext,
+ fieldDescriptions, thirdPartyAuthContext,
));
} catch (e) {
yield put(getThirdPartyAuthContextFailure());
diff --git a/src/common-components/data/selectors.js b/src/common-components/data/selectors.js
index f5bf2173..393385e3 100644
--- a/src/common-components/data/selectors.js
+++ b/src/common-components/data/selectors.js
@@ -8,3 +8,13 @@ export const thirdPartyAuthContextSelector = createSelector(
commonComponentsSelector,
commonComponents => commonComponents.thirdPartyAuthContext,
);
+
+export const fieldDescriptionSelector = createSelector(
+ commonComponentsSelector,
+ commonComponents => commonComponents.fieldDescriptions,
+);
+
+export const extendedProfileSelector = createSelector(
+ commonComponentsSelector,
+ commonComponents => commonComponents.extendedProfile,
+);
diff --git a/src/common-components/data/service.js b/src/common-components/data/service.js
index cd6d97b1..e083c871 100644
--- a/src/common-components/data/service.js
+++ b/src/common-components/data/service.js
@@ -18,6 +18,11 @@ export async function getThirdPartyAuthContext(urlParams) {
throw (e);
});
return {
- thirdPartyAuthContext: camelCaseObject(convertKeyNames(data, { fullname: 'name' })),
+ fieldDescriptions: data.registration_fields || {},
+ // For backward compatibility with the API, once https://github.com/openedx/edx-platform/pull/30198 is merged
+ // and deployed update it to use data.context_data
+ thirdPartyAuthContext: camelCaseObject(
+ convertKeyNames(data.context_data || data, { fullname: 'name' }),
+ ),
};
}
diff --git a/src/common-components/data/tests/sagas.test.js b/src/common-components/data/tests/sagas.test.js
index fe0feb8a..1b40ce90 100644
--- a/src/common-components/data/tests/sagas.test.js
+++ b/src/common-components/data/tests/sagas.test.js
@@ -26,7 +26,7 @@ describe('fetchThirdPartyAuthContext', () => {
it('should call service and dispatch success action', async () => {
const getThirdPartyAuthContext = jest.spyOn(api, 'getThirdPartyAuthContext')
- .mockImplementation(() => Promise.resolve({ thirdPartyAuthContext: data }));
+ .mockImplementation(() => Promise.resolve({ thirdPartyAuthContext: data, fieldDescriptions: {} }));
const dispatched = [];
await runSaga(
@@ -38,7 +38,7 @@ describe('fetchThirdPartyAuthContext', () => {
expect(getThirdPartyAuthContext).toHaveBeenCalledTimes(1);
expect(dispatched).toEqual([
actions.getThirdPartyAuthContextBegin(),
- actions.getThirdPartyAuthContextSuccess(data),
+ actions.getThirdPartyAuthContextSuccess({}, data),
]);
getThirdPartyAuthContext.mockClear();
});
diff --git a/src/field-renderer/FieldRenderer.jsx b/src/field-renderer/FieldRenderer.jsx
index b8d43836..6756a97a 100644
--- a/src/field-renderer/FieldRenderer.jsx
+++ b/src/field-renderer/FieldRenderer.jsx
@@ -6,7 +6,17 @@ import { ExpandMore } from '@edx/paragon/icons';
const FormFieldRenderer = (props) => {
let formField = null;
- const { fieldData, onChangeHandler, value } = props;
+ const {
+ errorMessage, fieldData, onChangeHandler, isRequired, value,
+ } = props;
+
+ const handleFocus = (e) => {
+ if (props.handleFocus) { props.handleFocus(e); }
+ };
+
+ const handleOnBlur = (e) => {
+ if (props.handleBlur) { props.handleBlur(e); }
+ };
switch (fieldData.type) {
case 'select': {
@@ -14,63 +24,95 @@ const FormFieldRenderer = (props) => {
return null;
}
formField = (
-
+
onChangeHandler(e)}
trailingElement={}
floatingLabel={fieldData.label}
+ onBlur={handleOnBlur}
+ onFocus={handleFocus}
>
{fieldData.options.map(option => (
))}
+ {isRequired && errorMessage && (
+
+ {errorMessage}
+
+ )}
);
break;
}
case 'textarea': {
formField = (
-
+
onChangeHandler(e)}
floatingLabel={fieldData.label}
+ onBlur={handleOnBlur}
+ onFocus={handleFocus}
/>
+ {isRequired && errorMessage && (
+
+ {errorMessage}
+
+ )}
);
break;
}
case 'text': {
formField = (
-
+
onChangeHandler(e)}
floatingLabel={fieldData.label}
+ onBlur={handleOnBlur}
+ onFocus={handleFocus}
/>
+ {isRequired && errorMessage && (
+
+ {errorMessage}
+
+ )}
);
break;
}
case 'checkbox': {
formField = (
-
+
onChangeHandler(e)}
+ onBlur={handleOnBlur}
+ onFocus={handleFocus}
>
{fieldData.label}
+ {isRequired && errorMessage && (
+
+ {errorMessage}
+
+ )}
);
break;
@@ -83,6 +125,10 @@ const FormFieldRenderer = (props) => {
};
FormFieldRenderer.defaultProps = {
value: '',
+ handleBlur: null,
+ handleFocus: null,
+ errorMessage: '',
+ isRequired: false,
};
FormFieldRenderer.propTypes = {
@@ -92,6 +138,10 @@ FormFieldRenderer.propTypes = {
name: PropTypes.string,
}).isRequired,
onChangeHandler: PropTypes.func.isRequired,
+ handleBlur: PropTypes.func,
+ handleFocus: PropTypes.func,
+ errorMessage: PropTypes.string,
+ isRequired: PropTypes.bool,
value: PropTypes.string,
};
diff --git a/src/field-renderer/tests/FieldRenderer.test.jsx b/src/field-renderer/tests/FieldRenderer.test.jsx
index 58b358b3..d0fc20f6 100644
--- a/src/field-renderer/tests/FieldRenderer.test.jsx
+++ b/src/field-renderer/tests/FieldRenderer.test.jsx
@@ -102,4 +102,96 @@ describe('FieldRendererTests', () => {
const fieldRenderer = mount( {}} />);
expect(fieldRenderer.html()).toBeNull();
});
+
+ it('should run onBlur and onFocus functions for a field if given', () => {
+ const fieldData = { type: 'text', label: 'Test', name: 'test-field' };
+ let functionValue = '';
+
+ const onBlur = (e) => {
+ functionValue = `${e.target.name} blurred`;
+ };
+
+ const onFocus = (e) => {
+ functionValue = `${e.target.name} focussed`;
+ };
+
+ const fieldRenderer = mount(
+ ,
+ );
+ const field = fieldRenderer.find('#test-field').last();
+
+ field.simulate('focus');
+ expect(functionValue).toEqual('test-field focussed');
+
+ field.simulate('blur');
+ expect(functionValue).toEqual('test-field blurred');
+ });
+
+ it('should render error message for required text fields', () => {
+ const fieldData = { type: 'text', label: 'First Name', name: 'first-name-field' };
+
+ const fieldRenderer = mount(
+ ,
+ );
+
+ expect(fieldRenderer.find('.form-text-size').last().text()).toEqual('Enter your first name');
+ });
+
+ it('should render error message for required select fields', () => {
+ const fieldData = {
+ type: 'select', label: 'Preference', name: 'preference-field', options: [['a', 'Opt 1'], ['b', 'Opt 2']],
+ };
+
+ const fieldRenderer = mount(
+ ,
+ );
+
+ expect(fieldRenderer.find('.form-text-size').last().text()).toEqual('Select your preference');
+ });
+
+ it('should render error message for required textarea fields', () => {
+ const fieldData = { type: 'textarea', label: 'Goals', name: 'goals-field' };
+
+ const fieldRenderer = mount(
+ ,
+ );
+
+ expect(fieldRenderer.find('.form-text-size').last().text()).toEqual('Tell us your goals');
+ });
+
+ it('should render error message for required checkbox fields', () => {
+ const fieldData = { type: 'checkbox', label: 'Honor Code', name: 'honor-code-field' };
+
+ const fieldRenderer = mount(
+ ,
+ );
+
+ expect(fieldRenderer.find('.form-text-size').last().text()).toEqual('You must agree to our Honor Code');
+ });
});
diff --git a/src/index.jsx b/src/index.jsx
index 19c8a2bd..4a810beb 100755
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -41,6 +41,7 @@ initialize({
MARKETING_EMAILS_OPT_IN: process.env.MARKETING_EMAILS_OPT_IN || '',
ENABLE_COPPA_COMPLIANCE: process.env.ENABLE_COPPA_COMPLIANCE || '',
SHOW_DYNAMIC_PROFILING_PAGE: process.env.SHOW_DYNAMIC_PROFILING_PAGE || false,
+ ENABLE_DYNAMIC_REGISTRATION_FIELDS: process.env.ENABLE_DYNAMIC_REGISTRATION_FIELDS || false,
});
},
},
diff --git a/src/register/CountryDropdown.jsx b/src/register/CountryDropdown.jsx
index d6ffc3a5..751e9ef2 100644
--- a/src/register/CountryDropdown.jsx
+++ b/src/register/CountryDropdown.jsx
@@ -176,7 +176,7 @@ class CountryDropdown extends React.Component {
render() {
return (
-
+
{
+ const {
+ intl, errorMessage, onChangeHandler, fieldType, value,
+ } = props;
+
+ if (fieldType === 'tos_and_honor_code') {
+ return (
+
+
+ {intl.formatMessage(messages['terms.of.service.and.honor.code'])}
+
+ ),
+ privacyPolicy: (
+
+ {intl.formatMessage(messages['privacy.policy'])}
+
+ ),
+ }}
+ />
+
+ );
+ }
+
+ return (
+
+
+
+ {intl.formatMessage(messages['honor.code'])}
+
+ ),
+ }}
+ />
+
+ {errorMessage && (
+
+ {errorMessage}
+
+ )}
+
+ );
+};
+
+HonorCode.defaultProps = {
+ errorMessage: '',
+ onChangeHandler: null,
+ fieldType: 'honor_code',
+ value: false,
+};
+
+HonorCode.propTypes = {
+ intl: intlShape.isRequired,
+ errorMessage: PropTypes.string,
+ onChangeHandler: PropTypes.func,
+ fieldType: PropTypes.string,
+ value: PropTypes.bool,
+};
+
+export default injectIntl(HonorCode);
diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx
index 1c83bca7..ee5fd44c 100644
--- a/src/register/RegistrationPage.jsx
+++ b/src/register/RegistrationPage.jsx
@@ -5,35 +5,43 @@ import Skeleton from 'react-loading-skeleton';
import { Helmet } from 'react-helmet';
import PropTypes, { string } from 'prop-types';
-import { getConfig } from '@edx/frontend-platform';
+import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import {
- injectIntl, intlShape, getCountryList, getLocale, FormattedMessage,
+ injectIntl, intlShape, getCountryList, getLocale,
} from '@edx/frontend-platform/i18n';
import {
- Alert, Form, Hyperlink, StatefulButton, Icon,
+ Alert, Form, StatefulButton, Icon,
} from '@edx/paragon';
import { Error, Close } from '@edx/paragon/icons';
-
+import FormFieldRenderer from '../field-renderer';
import {
clearUsernameSuggestions, registerNewUser, resetRegistrationForm, fetchRealtimeValidations,
} from './data/actions';
import {
- FORM_SUBMISSION_ERROR, DEFAULT_SERVICE_PROVIDER_DOMAINS, DEFAULT_TOP_LEVEL_DOMAINS, COMMON_EMAIL_PROVIDERS,
+ FIELDS, FORM_SUBMISSION_ERROR, DEFAULT_SERVICE_PROVIDER_DOMAINS, DEFAULT_TOP_LEVEL_DOMAINS, COMMON_EMAIL_PROVIDERS,
} from './data/constants';
import {
- registrationErrorSelector, registrationRequestSelector, validationsSelector, usernameSuggestionsSelector,
+ registrationErrorSelector,
+ registrationRequestSelector,
+ validationsSelector,
+ usernameSuggestionsSelector,
} from './data/selectors';
import messages from './messages';
import RegistrationFailure from './RegistrationFailure';
import UsernameField from './UsernameField';
+import HonorCode from './HonorCode';
import {
RedirectLogistration, SocialAuthProviders, ThirdPartyAuthAlert, RenderInstitutionButton,
InstitutionLogistration, FormGroup, PasswordField,
} from '../common-components';
import { getThirdPartyAuthContext } from '../common-components/data/actions';
-import { thirdPartyAuthContextSelector } from '../common-components/data/selectors';
+import {
+ extendedProfileSelector,
+ fieldDescriptionSelector,
+ thirdPartyAuthContextSelector,
+} from '../common-components/data/selectors';
import EnterpriseSSO from '../common-components/EnterpriseSSO';
import {
DEFAULT_STATE, PENDING_STATE, REGISTER_PAGE, VALID_EMAIL_REGEX, LETTER_REGEX, NUMBER_REGEX, VALID_NAME_REGEX,
@@ -43,6 +51,7 @@ import {
} from '../data/utils';
import CountryDropdown from './CountryDropdown';
import { getLevenshteinSuggestion, getSuggestionForInvalidEmail } from './utils';
+import TermsOfService from './TermsOfService';
class RegistrationPage extends React.Component {
constructor(props, context) {
@@ -51,6 +60,9 @@ class RegistrationPage extends React.Component {
this.handleOnClose = this.handleOnClose.bind(this);
this.queryParams = getAllPossibleQueryParam();
+ // TODO: Once we have tested it and ready for openedX we can remove this flag and make the code
+ // permanent part of Authn and remove extra code
+ this.showDynamicRegistrationFields = getConfig().ENABLE_DYNAMIC_REGISTRATION_FIELDS;
this.tpaHint = getTpaHint();
this.state = {
country: '',
@@ -75,6 +87,7 @@ class RegistrationPage extends React.Component {
optimizelyExperimentName: '',
readOnly: true,
validatePassword: false,
+ values: {},
};
}
@@ -171,16 +184,28 @@ class RegistrationPage extends React.Component {
}
};
+ onChangeHandler = (e) => {
+ const { name, value, checked } = e.target;
+ const { errors, values } = this.state;
+ if (e.target.type === 'checkbox') {
+ errors[name] = '';
+ values[name] = checked;
+ } else {
+ values[name] = value;
+ }
+ const state = { errors, values };
+ this.setState({ ...state });
+ };
+
handleSubmit = (e) => {
e.preventDefault();
const { startTime } = this.state;
const totalRegistrationTime = (Date.now() - startTime) / 1000;
- const payload = {
+ const dynamicFieldErrorMessages = {};
+ let payload = {
name: this.state.name,
username: this.state.username,
email: this.state.email,
- country: this.state.country,
- honor_code: true,
is_authn_mfe: true,
};
@@ -190,7 +215,28 @@ class RegistrationPage extends React.Component {
payload.password = this.state.password;
}
- if (!this.isFormValid(payload)) {
+ if (this.showDynamicRegistrationFields) {
+ payload.extendedProfile = [];
+ Object.keys(this.props.fieldDescriptions).forEach((fieldName) => {
+ if (this.props.extendedProfile.includes(fieldName)) {
+ payload.extendedProfile.push({ fieldName, fieldValue: this.state.values[fieldName] });
+ } else {
+ payload[fieldName] = this.state.values[fieldName];
+ }
+ dynamicFieldErrorMessages[fieldName] = this.props.fieldDescriptions[fieldName].error_message;
+ });
+ if (
+ this.props.fieldDescriptions[FIELDS.HONOR_CODE]
+ && this.props.fieldDescriptions[FIELDS.HONOR_CODE].type === 'tos_and_honor_code'
+ ) {
+ payload[FIELDS.HONOR_CODE] = true;
+ }
+ } else {
+ payload.country = this.state.country;
+ payload.honor_code = true;
+ }
+
+ if (!this.isFormValid(payload, dynamicFieldErrorMessages)) {
this.setState(prevState => ({
errorCode: FORM_SUBMISSION_ERROR,
failureCount: prevState.failureCount + 1,
@@ -202,6 +248,7 @@ class RegistrationPage extends React.Component {
payload.marketing_emails_opt_in = this.state.marketingOptIn;
}
+ payload = snakeCaseObject(payload);
payload.totalRegistrationTime = totalRegistrationTime;
this.setState({
totalRegistrationTime,
@@ -282,13 +329,22 @@ class RegistrationPage extends React.Component {
this.props.clearUsernameSuggestions();
}
- isFormValid(payload) {
+ validateDynamicFields = (e) => {
+ const { errors } = this.state;
+ const { name, value } = e.target;
+ if (!value) {
+ errors[name] = this.props.fieldDescriptions[name].error_message;
+ }
+ this.setState({ errors });
+ }
+
+ isFormValid(payload, dynamicFieldError) {
const { errors } = this.state;
let isValid = true;
Object.keys(payload).forEach(key => {
if (!payload[key]) {
- errors[key] = this.props.intl.formatMessage(messages[`empty.${key}.field.error`]);
+ errors[key] = (key in dynamicFieldError) ? dynamicFieldError[key] : this.props.intl.formatMessage(messages[`empty.${key}.field.error`]);
}
// Mark form invalid, if there was already a validation error for this key or we added empty field error
if (errors[key]) {
@@ -521,6 +577,73 @@ class RegistrationPage extends React.Component {
});
}
+ const honorCode = [];
+ const formFields = this.showDynamicRegistrationFields ? (
+ Object.keys(this.props.fieldDescriptions).map((fieldName) => {
+ const fieldData = this.props.fieldDescriptions[fieldName];
+ switch (fieldData.name) {
+ case FIELDS.COUNTRY:
+ return (
+
+ this.setState(prevState => ({ values: { ...prevState.values, country: value } }))
+ }
+ errorCode={this.state.errorCode}
+ readOnly={this.state.readOnly}
+ />
+
+ );
+ case FIELDS.HONOR_CODE:
+ honorCode.push(
+
+
+ ,
+ );
+ return null;
+ case FIELDS.TERMS_OF_SERVICE:
+ honorCode.push(
+
+
+ ,
+ );
+ return null;
+ default:
+ return (
+
+
+
+ );
+ }
+ })
+ ) : null;
+
return (
<>
@@ -602,20 +725,24 @@ class RegistrationPage extends React.Component {
floatingLabel={intl.formatMessage(messages['registration.password.label'])}
/>
)}
- this.setState({ country: value })}
- errorCode={this.state.errorCode}
- readOnly={this.state.readOnly}
- />
+ {!(this.showDynamicRegistrationFields)
+ && (
+ this.setState({ country: value })}
+ errorCode={this.state.errorCode}
+ readOnly={this.state.readOnly}
+ />
+ )}
+ {formFields}
{(getConfig().MARKETING_EMAILS_OPT_IN)
&& (
)}
-
-
- {intl.formatMessage(messages['terms.of.service.and.honor.code'])}
-
- ),
- privacyPolicy: (
-
- {intl.formatMessage(messages['privacy.policy'])}
-
- ),
- }}
+ {!(this.showDynamicRegistrationFields) ? (
+
-
+ ) : {honorCode}
}
{
validationDecisions: validationsSelector(state),
statusCode: state.register.statusCode,
usernameSuggestions: usernameSuggestionsSelector(state),
+ fieldDescriptions: fieldDescriptionSelector(state),
+ extendedProfile: extendedProfileSelector(state),
};
};
diff --git a/src/register/TermsOfService.jsx b/src/register/TermsOfService.jsx
new file mode 100644
index 00000000..a4a20dff
--- /dev/null
+++ b/src/register/TermsOfService.jsx
@@ -0,0 +1,60 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { getConfig } from '@edx/frontend-platform';
+import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
+import { Hyperlink, Form } from '@edx/paragon';
+import messages from './messages';
+
+const TermsOfService = (props) => {
+ const {
+ intl, errorMessage, onChangeHandler, value,
+ } = props;
+
+ return (
+
+
+
+ {intl.formatMessage(messages['terms.of.service'])}
+
+ ),
+ }}
+ />
+
+ {errorMessage && (
+
+ {errorMessage}
+
+ )}
+
+ );
+};
+
+TermsOfService.defaultProps = {
+ errorMessage: '',
+ value: false,
+};
+
+TermsOfService.propTypes = {
+ intl: intlShape.isRequired,
+ errorMessage: PropTypes.string,
+ onChangeHandler: PropTypes.func.isRequired,
+ value: PropTypes.bool,
+};
+
+export default injectIntl(TermsOfService);
diff --git a/src/register/data/constants.js b/src/register/data/constants.js
index 2a8e2d8e..6e1c0c30 100644
--- a/src/register/data/constants.js
+++ b/src/register/data/constants.js
@@ -1,3 +1,10 @@
+// Registration Fields
+export const FIELDS = {
+ COUNTRY: 'country',
+ HONOR_CODE: 'honor_code',
+ TERMS_OF_SERVICE: 'terms_of_service',
+};
+
// Registration Error Codes
export const FORBIDDEN_REQUEST = 'forbidden-request';
export const FORM_SUBMISSION_ERROR = 'form-submission-error';
diff --git a/src/register/data/reducers.js b/src/register/data/reducers.js
index 813c6156..b53a63d7 100644
--- a/src/register/data/reducers.js
+++ b/src/register/data/reducers.js
@@ -1,8 +1,14 @@
import {
- REGISTRATION_FORM, REGISTER_NEW_USER, REGISTER_FORM_VALIDATIONS, REGISTER_CLEAR_USERNAME_SUGGESTIONS,
+ REGISTRATION_FORM,
+ REGISTER_NEW_USER,
+ REGISTER_FORM_VALIDATIONS,
+ REGISTER_CLEAR_USERNAME_SUGGESTIONS,
} from './actions';
-import { DEFAULT_STATE, PENDING_STATE } from '../../data/constants';
+import {
+ DEFAULT_STATE,
+ PENDING_STATE,
+} from '../../data/constants';
export const defaultState = {
registrationError: {},
@@ -11,6 +17,9 @@ export const defaultState = {
validations: null,
statusCode: null,
usernameSuggestions: [],
+ extendedProfile: [],
+ fieldDescriptions: {},
+ formRenderState: DEFAULT_STATE,
};
const reducer = (state = defaultState, action) => {
diff --git a/src/register/data/sagas.js b/src/register/data/sagas.js
index 5d338da9..0966317a 100644
--- a/src/register/data/sagas.js
+++ b/src/register/data/sagas.js
@@ -55,7 +55,6 @@ export function* fetchRealtimeValidations(action) {
}
}
}
-
export default function* saga() {
yield takeEvery(REGISTER_NEW_USER.BASE, handleNewUserRegistration);
yield takeEvery(REGISTER_FORM_VALIDATIONS.BASE, fetchRealtimeValidations);
diff --git a/src/register/data/tests/reducers.test.js b/src/register/data/tests/reducers.test.js
index 2fdae6ae..a92acc07 100644
--- a/src/register/data/tests/reducers.test.js
+++ b/src/register/data/tests/reducers.test.js
@@ -14,6 +14,9 @@ describe('register reducer', () => {
validations: null,
statusCode: null,
usernameSuggestions: [],
+ extendedProfile: [],
+ fieldDescriptions: {},
+ formRenderState: DEFAULT_STATE,
},
);
});
diff --git a/src/register/messages.jsx b/src/register/messages.jsx
index 2d86c596..73bba916 100644
--- a/src/register/messages.jsx
+++ b/src/register/messages.jsx
@@ -188,6 +188,16 @@ const messages = defineMessages({
defaultMessage: 'Privacy Policy',
description: 'Text for the hyperlink that redirects user to privacy policy',
},
+ 'honor.code': {
+ id: 'honor.code',
+ defaultMessage: 'Honor Code',
+ description: 'Text for the hyperlink that redirects user to the honor code',
+ },
+ 'terms.of.service': {
+ id: 'terms.of.service',
+ defaultMessage: 'Terms of Service',
+ description: 'Text for the hyperlink that redirects user to the terms of service',
+ },
// Optional fields
'registration.year.of.birth.label': {
id: 'registration.year.of.birth.label',
diff --git a/src/register/tests/HonorCode.test.jsx b/src/register/tests/HonorCode.test.jsx
new file mode 100644
index 00000000..8e590e12
--- /dev/null
+++ b/src/register/tests/HonorCode.test.jsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import { mount } from 'enzyme';
+
+import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
+import { mergeConfig } from '@edx/frontend-platform';
+import HonorCode from '../HonorCode';
+
+const IntlHonorCode = injectIntl(HonorCode);
+
+describe('HonorCodeTest', () => {
+ mergeConfig({
+ PRIVACY_POLICY: 'http://privacy-policy.com',
+ TOS_AND_HONOR_CODE: 'http://tos-and-honot-code.com',
+ });
+ let value = false;
+
+ const changeHandler = (e) => {
+ value = e.target.checked;
+ };
+
+ beforeEach(() => {
+ value = false;
+ });
+
+ it('should render error msg if honor code is not checked', () => {
+ const honorCode = mount(
+
+
+ ,
+ );
+ expect(honorCode.find('.form-text-size').last().text()).toEqual('You must agree to the edx Honor Code');
+ });
+
+ it('should render Honor code field', () => {
+ const expectedMsg = 'I agree to the Your Platform Name Here Honor Codein a new tab';
+ const honorCode = mount(
+
+
+ ,
+ );
+
+ honorCode.find('#honor-code').last().simulate('change', { target: { checked: true, type: 'checkbox' } });
+ expect(honorCode.find('#honor-code').find('label').text()).toEqual(expectedMsg);
+ expect(value).toEqual(true);
+ });
+
+ it('should render Terms of Service and Honor code field', () => {
+ const HonorCodeProps = mount(
+
+
+ ,
+ );
+ const expectedMsg = 'By creating an account, you agree to the Terms of Service and Honor Codein a new tab and you '
+ + 'acknowledge that Your Platform Name Here and each Member process your personal data in '
+ + 'accordance with the Privacy Policyin a new tab.';
+ const field = HonorCodeProps.find('#honor-code');
+ expect(field.text()).toEqual(expectedMsg);
+ });
+});
diff --git a/src/register/tests/RegistrationPage.test.jsx b/src/register/tests/RegistrationPage.test.jsx
index 841a677b..b223f02e 100644
--- a/src/register/tests/RegistrationPage.test.jsx
+++ b/src/register/tests/RegistrationPage.test.jsx
@@ -16,7 +16,9 @@ import {
registerNewUser,
resetRegistrationForm,
} from '../data/actions';
-import { FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_SESSION_EXPIRED } from '../data/constants';
+import {
+ FIELDS, FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_SESSION_EXPIRED,
+} from '../data/constants';
import RegistrationFailureMessage from '../RegistrationFailure';
import RegistrationPage from '../RegistrationPage';
@@ -773,4 +775,87 @@ describe('RegistrationPage', () => {
});
});
});
+
+ describe('TestDynamicFields', () => {
+ it('should render fields returned by backend', () => {
+ mergeConfig({
+ ENABLE_DYNAMIC_REGISTRATION_FIELDS: true,
+ });
+ store = mockStore({
+ ...initialState,
+ commonComponents: {
+ ...initialState.commonComponents,
+ fieldDescriptions: {
+ country: { name: 'country', error_message: true },
+ profession: { name: 'profession', type: 'text', label: 'Profession' },
+ honor_code: { name: FIELDS.HONOR_CODE, error_message: 'You must agree to Honor Code of our site' },
+ terms_of_service: {
+ name: FIELDS.TERMS_OF_SERVICE,
+ error_message: 'You must agree to the Terms and Service agreement of our site',
+ },
+ },
+ },
+ });
+ const registerPage = mount(reduxWrapper());
+ expect(registerPage.find('#country').exists()).toBeTruthy();
+ expect(registerPage.find('#profession').exists()).toBeTruthy();
+ expect(registerPage.find('#honor-code').exists()).toBeTruthy();
+ expect(registerPage.find('#tos').exists()).toBeTruthy();
+ });
+
+ it('should submit form with fields returned by backend in payload', () => {
+ jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
+ store = mockStore({
+ ...initialState,
+ commonComponents: {
+ ...initialState.commonComponents,
+ fieldDescriptions: {
+ country: { name: 'country', error_message: true },
+ profession: { name: 'profession', type: 'text', label: 'Profession' },
+ honor_code: { name: 'honor_code', type: 'tos_and_honor_code' },
+ },
+ extendedProfile: ['profession'],
+ },
+ });
+
+ const payload = {
+ name: 'John Doe',
+ username: 'john_doe',
+ email: 'john.doe@example.com',
+ password: 'password1',
+ country: 'Pakistan',
+ totalRegistrationTime: 0,
+ is_authn_mfe: true,
+ honor_code: true,
+ extended_profile: [{ field_name: 'profession', field_value: 'Engineer' }],
+ };
+
+ store.dispatch = jest.fn(store.dispatch);
+ const registerPage = mount(reduxWrapper());
+
+ populateRequiredFields(registerPage, payload);
+ registerPage.find('input#profession').simulate('change', { target: { value: 'Engineer', name: 'profession' } });
+ registerPage.find('button.btn-brand').simulate('click');
+ expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
+ });
+
+ it('should show error message for fields returned by backend', () => {
+ store = mockStore({
+ ...initialState,
+ commonComponents: {
+ ...initialState.commonComponents,
+ fieldDescriptions: {
+ profession: {
+ name: 'profession', type: 'text', label: 'Profession', error_message: 'Enter profession',
+ },
+ },
+ },
+ });
+
+ const registrationPage = mount(reduxWrapper());
+ registrationPage.find('button.btn-brand').simulate('click');
+
+ expect(registrationPage.find('#profession-error').last().text()).toEqual('Enter profession');
+ });
+ });
});
diff --git a/src/register/tests/TermsOfService.test.jsx b/src/register/tests/TermsOfService.test.jsx
new file mode 100644
index 00000000..3a71b01e
--- /dev/null
+++ b/src/register/tests/TermsOfService.test.jsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import { mount } from 'enzyme';
+
+import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
+import { mergeConfig } from '@edx/frontend-platform';
+import TermsOfService from '../TermsOfService';
+
+const IntlTermsOfService = injectIntl(TermsOfService);
+
+describe('TermsOfServiceTest', () => {
+ mergeConfig({
+ TOS_LINK: 'http://tos-and-honot-code.com',
+ });
+ let value = false;
+
+ const changeHandler = (e) => {
+ value = e.target.checked;
+ };
+
+ beforeEach(() => {
+ value = false;
+ });
+
+ it('should render error msg if Terms of Service checkbox is not checked', () => {
+ const errorMessage = 'You must agree to the edx Terms of Service';
+ const termsOfService = mount(
+
+
+ ,
+ );
+ expect(termsOfService.find('.form-text-size').last().text()).toEqual(errorMessage);
+ });
+
+ it('should render Terms of Service field', () => {
+ const termsOfService = mount(
+
+
+ ,
+ );
+ const expectedMsg = 'I agree to the Your Platform Name Here Terms of Servicein a new tab';
+ expect(termsOfService.find('#terms-of-service').find('label').text()).toEqual(expectedMsg);
+ expect(value).toEqual(false);
+ });
+
+ it('should change value when Terms of Service field is checked', () => {
+ const termsOfService = mount(
+
+
+ ,
+ );
+ const field = termsOfService.find('input#tos');
+ field.simulate('change', { target: { checked: true, type: 'checkbox' } });
+ expect(value).toEqual(true);
+ });
+});