feat: add required field support (#557)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 = () => ({
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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' }),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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 = (
|
||||
<Form.Group controlId={fieldData.name} className="mb-3">
|
||||
<Form.Group controlId={fieldData.name} isInvalid={isRequired && errorMessage}>
|
||||
<Form.Control
|
||||
as="select"
|
||||
name={fieldData.name}
|
||||
value={value}
|
||||
aria-invalid={isRequired && Boolean(errorMessage)}
|
||||
onChange={(e) => onChangeHandler(e)}
|
||||
trailingElement={<Icon src={ExpandMore} />}
|
||||
floatingLabel={fieldData.label}
|
||||
onBlur={handleOnBlur}
|
||||
onFocus={handleFocus}
|
||||
>
|
||||
<option key="default" value="">{fieldData.label}</option>
|
||||
{fieldData.options.map(option => (
|
||||
<option className="data-hj-suppress" key={option[0]} value={option[0]}>{option[1]}</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
{isRequired && errorMessage && (
|
||||
<Form.Control.Feedback id={`${fieldData.name}-error`} type="invalid" className="form-text-size" hasIcon={false}>
|
||||
{errorMessage}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'textarea': {
|
||||
formField = (
|
||||
<Form.Group controlId={fieldData.name} className="mb-3">
|
||||
<Form.Group controlId={fieldData.name} isInvalid={isRequired && errorMessage}>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
name={fieldData.name}
|
||||
value={value}
|
||||
aria-invalid={isRequired && Boolean(errorMessage)}
|
||||
onChange={(e) => onChangeHandler(e)}
|
||||
floatingLabel={fieldData.label}
|
||||
onBlur={handleOnBlur}
|
||||
onFocus={handleFocus}
|
||||
/>
|
||||
{isRequired && errorMessage && (
|
||||
<Form.Control.Feedback id={`${fieldData.name}-error`} type="invalid" className="form-text-size" hasIcon={false}>
|
||||
{errorMessage}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'text': {
|
||||
formField = (
|
||||
<Form.Group controlId={fieldData.name} className="mb-3">
|
||||
<Form.Group controlId={fieldData.name} isInvalid={isRequired && errorMessage}>
|
||||
<Form.Control
|
||||
name={fieldData.name}
|
||||
value={value}
|
||||
aria-invalid={isRequired && Boolean(errorMessage)}
|
||||
onChange={(e) => onChangeHandler(e)}
|
||||
floatingLabel={fieldData.label}
|
||||
onBlur={handleOnBlur}
|
||||
onFocus={handleFocus}
|
||||
/>
|
||||
{isRequired && errorMessage && (
|
||||
<Form.Control.Feedback id={`${fieldData.name}-error`} type="invalid" className="form-text-size" hasIcon={false}>
|
||||
{errorMessage}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'checkbox': {
|
||||
formField = (
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Group isInvalid={isRequired && errorMessage}>
|
||||
<Form.Checkbox
|
||||
id={fieldData.name}
|
||||
checked={!!value}
|
||||
name={fieldData.name}
|
||||
value={value}
|
||||
aria-invalid={isRequired && Boolean(errorMessage)}
|
||||
onChange={(e) => onChangeHandler(e)}
|
||||
onBlur={handleOnBlur}
|
||||
onFocus={handleFocus}
|
||||
>
|
||||
{fieldData.label}
|
||||
</Form.Checkbox>
|
||||
{isRequired && errorMessage && (
|
||||
<Form.Control.Feedback id={`${fieldData.name}-error`} type="invalid" className="form-text-size" hasIcon={false}>
|
||||
{errorMessage}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
);
|
||||
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,
|
||||
};
|
||||
|
||||
|
||||
@@ -102,4 +102,96 @@ describe('FieldRendererTests', () => {
|
||||
const fieldRenderer = mount(<FieldRenderer fieldData={fieldData} onChangeHandler={() => {}} />);
|
||||
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(
|
||||
<FieldRenderer
|
||||
handleFocus={onFocus}
|
||||
handleBlur={onBlur}
|
||||
value={value}
|
||||
fieldData={fieldData}
|
||||
onChangeHandler={changeHandler}
|
||||
/>,
|
||||
);
|
||||
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(
|
||||
<FieldRenderer
|
||||
isRequired
|
||||
fieldData={fieldData}
|
||||
onChangeHandler={changeHandler}
|
||||
errorMessage="Enter your first name"
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<FieldRenderer
|
||||
isRequired
|
||||
fieldData={fieldData}
|
||||
onChangeHandler={changeHandler}
|
||||
errorMessage="Select your preference"
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<FieldRenderer
|
||||
isRequired
|
||||
fieldData={fieldData}
|
||||
onChangeHandler={changeHandler}
|
||||
errorMessage="Tell us your goals"
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<FieldRenderer
|
||||
isRequired
|
||||
fieldData={fieldData}
|
||||
onChangeHandler={changeHandler}
|
||||
errorMessage="You must agree to our Honor Code"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(fieldRenderer.find('.form-text-size').last().text()).toEqual('You must agree to our Honor Code');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -176,7 +176,7 @@ class CountryDropdown extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<FormGroup
|
||||
as="input"
|
||||
name={this.props.name}
|
||||
|
||||
88
src/register/HonorCode.jsx
Normal file
88
src/register/HonorCode.jsx
Normal file
@@ -0,0 +1,88 @@
|
||||
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 HonorCode = (props) => {
|
||||
const {
|
||||
intl, errorMessage, onChangeHandler, fieldType, value,
|
||||
} = props;
|
||||
|
||||
if (fieldType === 'tos_and_honor_code') {
|
||||
return (
|
||||
<div id="honor-code" className="micro text-muted mt-4">
|
||||
<FormattedMessage
|
||||
id="register.page.terms.of.service.and.honor.code"
|
||||
defaultMessage="By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each
|
||||
Member process your personal data in accordance with the {privacyPolicy}."
|
||||
description="Text that appears on registration form stating honor code and privacy policy"
|
||||
values={{
|
||||
platformName: getConfig().SITE_NAME,
|
||||
tosAndHonorCode: (
|
||||
<Hyperlink variant="muted" destination={getConfig().TOS_AND_HONOR_CODE || '#'} target="_blank">
|
||||
{intl.formatMessage(messages['terms.of.service.and.honor.code'])}
|
||||
</Hyperlink>
|
||||
),
|
||||
privacyPolicy: (
|
||||
<Hyperlink variant="muted" destination={getConfig().PRIVACY_POLICY || '#'} target="_blank">
|
||||
{intl.formatMessage(messages['privacy.policy'])}
|
||||
</Hyperlink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="honor-code" className="micro text-muted">
|
||||
<Form.Checkbox
|
||||
className="opt-checkbox mt-1"
|
||||
id="honor-code"
|
||||
checked={value}
|
||||
name="honor_code"
|
||||
value={value}
|
||||
onChange={onChangeHandler}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="register.page.honor.code"
|
||||
defaultMessage="I agree to the {platformName} {tosAndHonorCode}"
|
||||
description="Text that appears on registration form stating honor code"
|
||||
values={{
|
||||
platformName: getConfig().SITE_NAME,
|
||||
tosAndHonorCode: (
|
||||
<Hyperlink variant="muted" destination={getConfig().TOS_AND_HONOR_CODE || '#'} target="_blank">
|
||||
{intl.formatMessage(messages['honor.code'])}
|
||||
</Hyperlink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Form.Checkbox>
|
||||
{errorMessage && (
|
||||
<Form.Control.Feedback type="invalid" className="form-text-size" hasIcon={false}>
|
||||
{errorMessage}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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);
|
||||
@@ -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 (
|
||||
<span key={fieldData.name}>
|
||||
<CountryDropdown
|
||||
name="country"
|
||||
floatingLabel={intl.formatMessage(messages['registration.country.label'])}
|
||||
options={getCountryList(getLocale())}
|
||||
valueKey="code"
|
||||
displayValueKey="name"
|
||||
value={this.state.values[fieldData.name]}
|
||||
handleBlur={this.handleOnBlur}
|
||||
handleFocus={this.handleOnFocus}
|
||||
errorMessage={intl.formatMessage(messages['empty.country.field.error'])}
|
||||
handleChange={
|
||||
(value) => this.setState(prevState => ({ values: { ...prevState.values, country: value } }))
|
||||
}
|
||||
errorCode={this.state.errorCode}
|
||||
readOnly={this.state.readOnly}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
case FIELDS.HONOR_CODE:
|
||||
honorCode.push(
|
||||
<span key={fieldData.name}>
|
||||
<HonorCode
|
||||
fieldType={fieldData.type}
|
||||
value={this.state.values[fieldData.name]}
|
||||
onChangeHandler={this.onChangeHandler}
|
||||
errorMessage={this.state.errors[fieldData.name]}
|
||||
/>
|
||||
</span>,
|
||||
);
|
||||
return null;
|
||||
case FIELDS.TERMS_OF_SERVICE:
|
||||
honorCode.push(
|
||||
<span key={fieldData.name}>
|
||||
<TermsOfService
|
||||
value={this.state.values[fieldData.name]}
|
||||
onChangeHandler={this.onChangeHandler}
|
||||
errorMessage={this.state.errors[fieldData.name]}
|
||||
/>
|
||||
</span>,
|
||||
);
|
||||
return null;
|
||||
default:
|
||||
return (
|
||||
<span key={fieldData.name}>
|
||||
<FormFieldRenderer
|
||||
fieldData={fieldData}
|
||||
value={this.state.values[fieldData.name]}
|
||||
onChangeHandler={this.onChangeHandler}
|
||||
handleBlur={this.validateDynamicFields}
|
||||
handleFocus={this.handleOnFocus}
|
||||
errorMessage={this.state.errors[fieldData.name]}
|
||||
isRequired
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
})
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
@@ -602,20 +725,24 @@ class RegistrationPage extends React.Component {
|
||||
floatingLabel={intl.formatMessage(messages['registration.password.label'])}
|
||||
/>
|
||||
)}
|
||||
<CountryDropdown
|
||||
name="country"
|
||||
floatingLabel={intl.formatMessage(messages['registration.country.label'])}
|
||||
options={getCountryList(getLocale())}
|
||||
valueKey="code"
|
||||
displayValueKey="name"
|
||||
value={this.state.country}
|
||||
handleBlur={this.handleOnBlur}
|
||||
handleFocus={this.handleOnFocus}
|
||||
errorMessage={intl.formatMessage(messages['empty.country.field.error'])}
|
||||
handleChange={(value) => this.setState({ country: value })}
|
||||
errorCode={this.state.errorCode}
|
||||
readOnly={this.state.readOnly}
|
||||
/>
|
||||
{!(this.showDynamicRegistrationFields)
|
||||
&& (
|
||||
<CountryDropdown
|
||||
name="country"
|
||||
floatingLabel={intl.formatMessage(messages['registration.country.label'])}
|
||||
options={getCountryList(getLocale())}
|
||||
valueKey="code"
|
||||
displayValueKey="name"
|
||||
value={this.state.country}
|
||||
handleBlur={this.handleOnBlur}
|
||||
handleFocus={this.handleOnFocus}
|
||||
errorMessage={intl.formatMessage(messages['empty.country.field.error'])}
|
||||
handleChange={(value) => this.setState({ country: value })}
|
||||
errorCode={this.state.errorCode}
|
||||
readOnly={this.state.readOnly}
|
||||
/>
|
||||
)}
|
||||
{formFields}
|
||||
{(getConfig().MARKETING_EMAILS_OPT_IN)
|
||||
&& (
|
||||
<Form.Checkbox
|
||||
@@ -627,27 +754,11 @@ class RegistrationPage extends React.Component {
|
||||
{intl.formatMessage(messages['registration.opt.in.label'], { siteName: getConfig().SITE_NAME })}
|
||||
</Form.Checkbox>
|
||||
)}
|
||||
<div id="honor-code" className="micro text-muted mt-4">
|
||||
<FormattedMessage
|
||||
id="register.page.terms.of.service.and.honor.code"
|
||||
defaultMessage="By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each
|
||||
Member process your personal data in accordance with the {privacyPolicy}."
|
||||
description="Text that appears on registration form stating honor code and privacy policy"
|
||||
values={{
|
||||
platformName: getConfig().SITE_NAME,
|
||||
tosAndHonorCode: (
|
||||
<Hyperlink variant="muted" destination={getConfig().TOS_AND_HONOR_CODE || '#'} target="_blank">
|
||||
{intl.formatMessage(messages['terms.of.service.and.honor.code'])}
|
||||
</Hyperlink>
|
||||
),
|
||||
privacyPolicy: (
|
||||
<Hyperlink variant="muted" destination={getConfig().PRIVACY_POLICY || '#'} target="_blank">
|
||||
{intl.formatMessage(messages['privacy.policy'])}
|
||||
</Hyperlink>
|
||||
),
|
||||
}}
|
||||
{!(this.showDynamicRegistrationFields) ? (
|
||||
<HonorCode
|
||||
fieldType="tos_and_honor_code"
|
||||
/>
|
||||
</div>
|
||||
) : <div className="mt-4">{honorCode}</div>}
|
||||
<StatefulButton
|
||||
name="register-user"
|
||||
id="register-user"
|
||||
@@ -719,6 +830,8 @@ class RegistrationPage extends React.Component {
|
||||
}
|
||||
|
||||
RegistrationPage.defaultProps = {
|
||||
extendedProfile: [],
|
||||
fieldDescriptions: {},
|
||||
registrationResult: null,
|
||||
registerNewUser: null,
|
||||
registrationErrorCode: null,
|
||||
@@ -738,6 +851,8 @@ RegistrationPage.defaultProps = {
|
||||
};
|
||||
|
||||
RegistrationPage.propTypes = {
|
||||
extendedProfile: PropTypes.arrayOf(PropTypes.string),
|
||||
fieldDescriptions: PropTypes.shape({}),
|
||||
intl: intlShape.isRequired,
|
||||
getThirdPartyAuthContext: PropTypes.func.isRequired,
|
||||
registerNewUser: PropTypes.func,
|
||||
@@ -791,6 +906,8 @@ const mapStateToProps = state => {
|
||||
validationDecisions: validationsSelector(state),
|
||||
statusCode: state.register.statusCode,
|
||||
usernameSuggestions: usernameSuggestionsSelector(state),
|
||||
fieldDescriptions: fieldDescriptionSelector(state),
|
||||
extendedProfile: extendedProfileSelector(state),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
60
src/register/TermsOfService.jsx
Normal file
60
src/register/TermsOfService.jsx
Normal file
@@ -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 (
|
||||
<div id="terms-of-service" className="micro text-muted">
|
||||
<Form.Checkbox
|
||||
className="opt-checkbox mt-1"
|
||||
id="tos"
|
||||
checked={value}
|
||||
name="terms_of_service"
|
||||
value={value}
|
||||
onChange={onChangeHandler}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="register.page.terms.of.service"
|
||||
defaultMessage="I agree to the {platformName} {termsOfService}"
|
||||
description="Text that appears on registration form stating terms of service.
|
||||
It is a legal document that users must agree to."
|
||||
values={{
|
||||
platformName: getConfig().SITE_NAME,
|
||||
termsOfService: (
|
||||
<Hyperlink variant="muted" destination={getConfig().TOS_LINK || '#'} target="_blank">
|
||||
{intl.formatMessage(messages['terms.of.service'])}
|
||||
</Hyperlink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Form.Checkbox>
|
||||
{errorMessage && (
|
||||
<Form.Control.Feedback type="invalid" className="form-text-size" hasIcon={false}>
|
||||
{errorMessage}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
TermsOfService.defaultProps = {
|
||||
errorMessage: '',
|
||||
value: false,
|
||||
};
|
||||
|
||||
TermsOfService.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
onChangeHandler: PropTypes.func.isRequired,
|
||||
value: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default injectIntl(TermsOfService);
|
||||
@@ -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';
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -14,6 +14,9 @@ describe('register reducer', () => {
|
||||
validations: null,
|
||||
statusCode: null,
|
||||
usernameSuggestions: [],
|
||||
extendedProfile: [],
|
||||
fieldDescriptions: {},
|
||||
formRenderState: DEFAULT_STATE,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
59
src/register/tests/HonorCode.test.jsx
Normal file
59
src/register/tests/HonorCode.test.jsx
Normal file
@@ -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(
|
||||
<IntlProvider locale="en">
|
||||
<IntlHonorCode errorMessage="You must agree to the edx Honor Code" onChangeHandler={changeHandler} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
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(
|
||||
<IntlProvider locale="en">
|
||||
<IntlHonorCode onChangeHandler={changeHandler} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
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(
|
||||
<IntlProvider locale="en">
|
||||
<IntlHonorCode fieldType="tos_and_honor_code" onChangeHandler={changeHandler} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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(<IntlRegistrationPage {...props} />));
|
||||
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(<IntlRegistrationPage {...props} />));
|
||||
|
||||
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(<IntlRegistrationPage {...props} />));
|
||||
registrationPage.find('button.btn-brand').simulate('click');
|
||||
|
||||
expect(registrationPage.find('#profession-error').last().text()).toEqual('Enter profession');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
55
src/register/tests/TermsOfService.test.jsx
Normal file
55
src/register/tests/TermsOfService.test.jsx
Normal file
@@ -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(
|
||||
<IntlProvider locale="en">
|
||||
<IntlTermsOfService errorMessage={errorMessage} onChangeHandler={changeHandler} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(termsOfService.find('.form-text-size').last().text()).toEqual(errorMessage);
|
||||
});
|
||||
|
||||
it('should render Terms of Service field', () => {
|
||||
const termsOfService = mount(
|
||||
<IntlProvider locale="en">
|
||||
<IntlTermsOfService onChangeHandler={changeHandler} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
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(
|
||||
<IntlProvider locale="en">
|
||||
<IntlTermsOfService onChangeHandler={changeHandler} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
const field = termsOfService.find('input#tos');
|
||||
field.simulate('change', { target: { checked: true, type: 'checkbox' } });
|
||||
expect(value).toEqual(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user