Adds missing backend format validations error on submit.

VAN-283
This commit is contained in:
Adeel Khan
2021-01-03 22:49:03 +05:00
parent 632311217b
commit e114fa8663
9 changed files with 419 additions and 107 deletions

View File

@@ -3,6 +3,10 @@ import PropTypes from 'prop-types';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Alert } from '@edx/paragon';
const hasNoErrors = (userErrors) => (
userErrors.every((errorList) => (!errorList[0]))
);
const RegistrationFailureMessage = (props) => {
const errorMessage = props.errors;
const userErrors = [];
@@ -24,20 +28,22 @@ const RegistrationFailureMessage = (props) => {
});
return (
<Alert variant="danger">
<Alert.Heading className="text-danger">
<FormattedMessage
id="logistration.registration.request.failure.header.description.message"
defaultMessage="We couldn't create your account."
description="error message when registration failure."
/>
</Alert.Heading>
<div>
<ul>
{userErrors}
</ul>
</div>
</Alert>
hasNoErrors(userErrors) ? null : (
<Alert variant="danger">
<Alert.Heading className="text-danger">
<FormattedMessage
id="logistration.registration.request.failure.header.description.message"
defaultMessage="We couldn't create your account."
description="error message when registration failure."
/>
</Alert.Heading>
<div>
<ul>
{userErrors}
</ul>
</div>
</Alert>
)
);
};

View File

@@ -61,7 +61,15 @@ class RegistrationPage extends React.Component {
confirmEmail: '',
enableOptionalField: false,
validationFieldName: '',
emptyFields: {},
validationErrorsAlertMessages: {
name: [{ user_message: '' }],
username: [{ user_message: '' }],
email: [{ user_message: '' }],
emailFormat: [{ user_message: '' }],
password: [{ user_message: '' }],
country: [{ user_message: '' }],
},
currentValidations: null,
errors: {
email: '',
name: '',
@@ -78,8 +86,8 @@ class RegistrationPage extends React.Component {
countryValid: false,
honorCodeValid: true,
termsOfServiceValid: false,
formValid: false,
institutionLogin: false,
formValid: false,
};
}
@@ -93,44 +101,34 @@ class RegistrationPage extends React.Component {
}
shouldComponentUpdate(nextProps) {
if (this.props.validations !== nextProps.validations) {
if (nextProps.statusCode !== 403 && this.props.validations !== nextProps.validations) {
const { errors } = this.state;
const errorMsg = nextProps.validations.validation_decisions[this.state.validationFieldName];
errors[this.state.validationFieldName] = errorMsg;
const stateValidKey = `${camelCase(this.state.validationFieldName)}Valid`;
const stateValidValue = !errorMsg;
const currentValidations = nextProps.validations.validation_decisions;
this.setState(({ [stateValidKey]: stateValidValue }), () => {
const {
emailValid,
nameValid,
usernameValid,
passwordValid,
} = this.state;
const validityMap = REGISTRATION_VALIDITY_MAP;
let extraFieldsValid = true;
Object.entries(validityMap).forEach(([key, value]) => {
if (value) {
const stateValid = `${camelCase(key)}Valid`;
extraFieldsValid = extraFieldsValid && this.state[stateValid];
}
});
const formValid = emailValid && nameValid && usernameValid && passwordValid && extraFieldsValid;
this.setState({
errors,
formValid,
});
this.setState({
[stateValidKey]: !errorMsg,
errors,
currentValidations,
});
return false;
}
if (this.props.thirdPartyAuthContext.pipelineUserDetails !== nextProps.thirdPartyAuthContext.pipelineUserDetails) {
this.setState({
...nextProps.thirdPartyAuthContext.pipelineUserDetails,
});
return false;
}
if (this.props.registrationError !== nextProps.registrationError) {
this.setState({
formValid: false,
});
return false;
}
return true;
}
@@ -141,19 +139,23 @@ class RegistrationPage extends React.Component {
handleSubmit = (e) => {
e.preventDefault();
const params = (new URL(document.location)).searchParams;
const payload = {
name: this.state.name,
username: this.state.username,
email: this.state.email,
password: this.state.password,
};
const payload = {};
const payloadMap = new Map();
payloadMap.set('name', this.state.name);
payloadMap.set('username', this.state.username);
payloadMap.set('email', this.state.email);
if (!this.props.thirdPartyAuthContext.currentProvider) {
payloadMap.set('password', this.state.password);
}
const fieldMap = { ...REGISTRATION_VALIDITY_MAP, ...REGISTRATION_OPTIONAL_MAP };
Object.entries(fieldMap).forEach(([key, value]) => {
if (value) {
payload[key] = this.state[camelCase(key)];
payloadMap.set(key, this.state[camelCase(key)]);
}
});
payloadMap.forEach((value, key) => { payload[key] = value; });
const next = params.get('next');
const courseId = params.get('course_id');
@@ -163,16 +165,35 @@ class RegistrationPage extends React.Component {
if (courseId) {
payload.course_id = params.course_id;
}
if (!this.state.formValid) {
let finalValidation = this.isFormValid();
if (!this.isFormValid()) {
// Special case where honor code and tos is a single field, true by default. We don't need
// to validate this field
Object.entries(payload).filter(([key]) => (key !== 'honor_code' || 'terms_of_service' in REGISTRATION_VALIDITY_MAP))
.forEach(([key, value]) => {
this.validateInput(key, value);
});
return;
payloadMap.forEach((value, key) => {
if (key !== 'honor_code' || 'terms_of_service' in REGISTRATION_VALIDITY_MAP) {
finalValidation = this.validateInput(key, value);
}
});
}
this.props.registerNewUser(payload);
if (finalValidation) {
this.props.registerNewUser(payload);
} else {
this.props.fetchRealtimeValidations(payload);
}
}
checkNoValidationsErrors(validations) {
let keyValidList = null;
keyValidList = Object.entries(validations).map(([key]) => {
const validation = validations[key][0];
return !validation.user_message;
});
return keyValidList.every((current) => current === true);
}
isFormValid() {
return this.state.formValid;
}
handleOnBlur(e) {
@@ -206,39 +227,85 @@ class RegistrationPage extends React.Component {
});
}
validateInput(inputName, value) {
handleOnClick(e) {
const { errors } = this.state;
const { emptyFields } = this.state;
let {
emailValid,
nameValid,
usernameValid,
passwordValid,
countryValid,
honorCodeValid,
termsOfServiceValid,
if (this.state.currentValidations) {
const fieldName = e.target.name;
errors[fieldName] = this.state.currentValidations[fieldName];
const stateValidKey = `${camelCase(fieldName)}Valid`;
this.setState(prevState => ({
[stateValidKey]: !prevState.currentValidations[fieldName],
errors,
}));
}
}
validateInput(inputName, value) {
const {
errors,
validationErrorsAlertMessages,
} = this.state;
let {
honorCodeValid,
termsOfServiceValid,
formValid,
} = this.state;
const validations = this.state.currentValidations;
switch (inputName) {
case 'email':
emailValid = value.length >= 1;
emptyFields.email = this.generateUserMessage(emailValid, 'logistration.email.validation.message');
if (this.props.statusCode !== 403 && validations && validations.email) {
validationErrorsAlertMessages.email = [{ user_message: validations.email }];
} else if (value.length < 1) {
const errorEmpty = this.generateUserMessage(value.length < 1, 'logistration.email.validation.message');
validationErrorsAlertMessages.email = errorEmpty;
} else {
const errorCharlength = this.generateUserMessage(value.length <= 2, 'logistration.email.ratelimit.less.chars.validation.message');
const formatError = this.generateUserMessage(!value.match(/^([\w.%+-]+)@([\w-]+\.)+([\w]{2,})$/i), 'logistration.email.ratelimit.incorrect.format.validation.message');
validationErrorsAlertMessages.email = errorCharlength;
validationErrorsAlertMessages.emailFormat = formatError;
}
break;
case 'name':
nameValid = value.length >= 1;
emptyFields.name = this.generateUserMessage(nameValid, 'logistration.fullname.validation.message');
if (this.props.statusCode !== 403 && validations && validations.name) {
validationErrorsAlertMessages.name = [{ user_message: validations.name }];
} else if (value.length < 1) {
const errorEmpty = this.generateUserMessage(value.length < 1, 'logistration.fullname.validation.message');
validationErrorsAlertMessages.name = errorEmpty;
} else {
validationErrorsAlertMessages.name = [{ user_message: '' }];
}
break;
case 'username':
usernameValid = value.length >= 1;
emptyFields.username = this.generateUserMessage(usernameValid, 'logistration.username.validation.message');
if (this.props.statusCode !== 403 && validations && validations.username) {
validationErrorsAlertMessages.username = [{ user_message: validations.username }];
} else if (value.length < 1) {
const errorEmpty = this.generateUserMessage(value.length < 1, 'logistration.username.validation.message');
validationErrorsAlertMessages.username = errorEmpty;
} else {
const errorCharLength = this.generateUserMessage(value.length <= 1, 'logistration.username.ratelimit.less.chars.message');
validationErrorsAlertMessages.username = errorCharLength;
}
break;
case 'password':
passwordValid = value.length >= 1;
emptyFields.password = this.generateUserMessage(passwordValid, 'logistration.register.page.password.validation.message');
if (this.props.statusCode !== 403 && validations && validations.password) {
validationErrorsAlertMessages.password = [{ user_message: validations.password }];
} else if (value.length < 1) {
const errorEmpty = this.generateUserMessage(value.length < 1, 'logistration.register.page.password.validation.message');
validationErrorsAlertMessages.password = errorEmpty;
} else {
const errorCharLength = this.generateUserMessage(value.length < 8, 'logistration.email.ratelimit.password.validation.message');
validationErrorsAlertMessages.password = errorCharLength;
}
break;
case 'country':
countryValid = value !== '';
emptyFields.country = this.generateUserMessage(countryValid, 'logistration.country.validation.message');
if (this.props.statusCode !== 403 && validations && validations.country) {
validationErrorsAlertMessages.country = [{ user_message: validations.country }];
} else {
const emptyError = this.generateUserMessage(value === '', 'logistration.country.validation.message');
validationErrorsAlertMessages.country = emptyError;
}
break;
case 'honor_code':
honorCodeValid = value !== false;
@@ -252,16 +319,14 @@ class RegistrationPage extends React.Component {
break;
}
formValid = this.checkNoValidationsErrors(validationErrorsAlertMessages);
this.setState({
emptyFields,
emailValid,
nameValid,
usernameValid,
passwordValid,
countryValid,
formValid,
validationErrorsAlertMessages,
honorCodeValid,
termsOfServiceValid,
});
return formValid;
}
addExtraRequiredFields() {
@@ -341,6 +406,7 @@ class RegistrationPage extends React.Component {
}));
props.options = options;
props.onBlur = e => this.handleOnBlur(e);
props.onClick = e => this.handleOnClick(e);
}
return (
<ValidationFormGroup
@@ -409,14 +475,14 @@ class RegistrationPage extends React.Component {
return fields;
}
generateUserMessage(isFieldValid, messageID) {
return [{ user_message: isFieldValid ? '' : this.intl.formatMessage(messages[messageID]) }];
generateUserMessage(isFieldInValid, messageID) {
return [{ user_message: isFieldInValid ? this.intl.formatMessage(messages[messageID]) : '' }];
}
renderErrors() {
let errorsObject = null;
if (Object.keys(this.state.emptyFields).length > 0) {
errorsObject = this.state.emptyFields;
if (!this.checkNoValidationsErrors(this.state.validationErrorsAlertMessages)) {
errorsObject = this.state.validationErrorsAlertMessages;
} else if (this.props.registrationError) {
errorsObject = this.props.registrationError;
} else {
@@ -492,6 +558,7 @@ class RegistrationPage extends React.Component {
invalid={this.state.errors.name !== ''}
invalidMessage={this.state.errors.name}
className="mb-0"
helpText="This name will be used by any certificates that you earn."
>
<label htmlFor="name" className="h6 pt-10">
{intl.formatMessage(messages['logistration.fullname.label'])}
@@ -504,6 +571,7 @@ class RegistrationPage extends React.Component {
value={this.state.name}
onChange={e => this.handleOnChange(e)}
onBlur={e => this.handleOnBlur(e)}
onClick={e => this.handleOnClick(e)}
required
/>
</ValidationFormGroup>
@@ -512,6 +580,7 @@ class RegistrationPage extends React.Component {
invalid={this.state.errors.username !== ''}
invalidMessage={this.state.errors.username}
className="mb-0"
helpText="The name that will identify you in your courses. It cannot be changed later."
>
<label htmlFor="username" className="h6 pt-10">
{intl.formatMessage(messages['logistration.username.label'])}
@@ -522,8 +591,10 @@ class RegistrationPage extends React.Component {
type="text"
placeholder=""
value={this.state.username}
maxLength="30"
onChange={e => this.handleOnChange(e)}
onBlur={e => this.handleOnBlur(e)}
onClick={e => this.handleOnClick(e)}
required
/>
</ValidationFormGroup>
@@ -532,6 +603,7 @@ class RegistrationPage extends React.Component {
invalid={this.state.errors.email !== ''}
invalidMessage={this.state.errors.email}
className="mb-0"
helpText="This is what you will use to login."
>
<label htmlFor="email" className="h6 pt-10">
{intl.formatMessage(messages['logistration.register.page.email.label'])}
@@ -544,6 +616,7 @@ class RegistrationPage extends React.Component {
value={this.state.email}
onChange={e => this.handleOnChange(e)}
onBlur={e => this.handleOnBlur(e)}
onClick={e => this.handleOnClick(e)}
required
/>
</ValidationFormGroup>
@@ -553,6 +626,7 @@ class RegistrationPage extends React.Component {
invalid={this.state.errors.password !== ''}
invalidMessage={this.state.errors.password}
className="mb-0"
helpText="Your password must contain at least 8 characters, including 1 letter & 1 number."
>
<label htmlFor="password" className="h6 pt-10">
{intl.formatMessage(messages['logistration.password.label'])}
@@ -565,6 +639,7 @@ class RegistrationPage extends React.Component {
value={this.state.password}
onChange={e => this.handleOnChange(e)}
onBlur={e => this.handleOnBlur(e)}
onClick={e => this.handleOnClick(e)}
required
/>
</ValidationFormGroup>
@@ -620,6 +695,7 @@ RegistrationPage.defaultProps = {
},
formData: null,
validations: null,
statusCode: null,
};
RegistrationPage.propTypes = {
@@ -664,6 +740,7 @@ RegistrationPage.propTypes = {
username: PropTypes.string,
}),
}),
statusCode: PropTypes.number,
};
const mapStateToProps = state => {
@@ -676,6 +753,7 @@ const mapStateToProps = state => {
thirdPartyAuthContext,
formData: state.logistration.formData,
validations: state.logistration.validations,
statusCode: state.logistration.statusCode,
};
};

View File

@@ -100,6 +100,7 @@ export const fetchRealtimeValidationsSuccess = (validations) => ({
payload: { validations },
});
export const fetchRealtimeValidationsFailure = () => ({
export const fetchRealtimeValidationsFailure = (error, statusCode) => ({
type: REGISTER_FORM_VALIDATIONS.FAILURE,
payload: { error, statusCode },
});

View File

@@ -15,6 +15,7 @@ export const defaultState = {
registrationResult: {},
formData: null,
validations: null,
statusCode: null,
};
const reducer = (state = defaultState, action) => {
@@ -89,6 +90,8 @@ const reducer = (state = defaultState, action) => {
case REGISTER_FORM_VALIDATIONS.FAILURE:
return {
...state,
validations: action.payload.error,
statusCode: action.payload.statusCode,
};
default:
return state;

View File

@@ -116,7 +116,10 @@ export function* fetchRealtimeValidations(action) {
fieldValidations,
));
} catch (e) {
yield put(fetchRealtimeValidationsFailure());
const statusCodes = [403];
if (e.response && statusCodes.includes(e.response.status)) {
yield put(fetchRealtimeValidationsFailure(e.response.data, e.response.status));
}
logError(e);
}
}

View File

@@ -180,8 +180,16 @@ describe('fetchRealtimeValidations', () => {
});
it('should call service and dispatch error action', async () => {
const validationRatelimitResponse = {
response: {
status: 403,
data: {
detail: 'You do not have permission to perform this action.',
},
},
};
const getFieldsValidations = jest.spyOn(api, 'getFieldsValidations')
.mockImplementation(() => Promise.reject(new Error('something went wrong')));
.mockImplementation(() => Promise.reject(validationRatelimitResponse));
const dispatched = [];
await runSaga(
@@ -194,7 +202,10 @@ describe('fetchRealtimeValidations', () => {
expect(loggingService.logError).toHaveBeenCalled();
expect(dispatched).toEqual([
actions.fetchRealtimeValidationsBegin(),
actions.fetchRealtimeValidationsFailure(),
actions.fetchRealtimeValidationsFailure(
validationRatelimitResponse.response.data,
validationRatelimitResponse.response.status,
),
]);
getFieldsValidations.mockClear();
});

View File

@@ -131,6 +131,21 @@ const messages = defineMessages({
defaultMessage: 'Please enter your Email.',
description: 'Validation message that appears when email address is empty',
},
'logistration.email.ratelimit.less.chars.validation.message': {
id: 'logistration.email.ratelimit.less.chars.validation.message',
defaultMessage: 'Email must have 3 characters.',
description: 'Validation message that appears when email address is less than 3 characters',
},
'logistration.email.ratelimit.incorrect.format.validation.message': {
id: 'logistration.email.ratelimit.incorrect.format.validation.message',
defaultMessage: 'The email address you provided isn\'t formatted correctly.',
description: 'Validation message that appears when email address is not formatted correctly with no backend validations available.',
},
'logistration.email.ratelimit.password.validation.message': {
id: 'logistration.email.ratelimit.password.validation.message',
defaultMessage: 'Your password must contain at least 8 characters',
description: 'Validation message that appears when password is not formatted correctly with no backend validations available.',
},
'logistration.email.help.message': {
id: 'logistration.email.help.message',
defaultMessage: 'The email address you used to register with edX.',
@@ -176,6 +191,11 @@ const messages = defineMessages({
defaultMessage: 'Please enter your Public Username.',
description: 'Validation message that appears when username is invalid',
},
'logistration.username.ratelimit.less.chars.message': {
id: 'logistration.username.ratelimit.less.chars.message',
defaultMessage: 'Public Username must have atleast 2 characters.',
description: 'Validation message that appears when username is less than 2 characters and with no backend validations available.',
},
'logistration.country.validation.message': {
id: 'logistration.country.validation.message',
defaultMessage: 'Select your country or region of residence.',

View File

@@ -223,6 +223,58 @@ describe('./RegistrationPage.js', () => {
expect(store.dispatch).toHaveBeenCalledWith(fetchRealtimeValidations(formPayload));
});
it('tests shouldComponentUpdate change validations and formValid state', () => {
const nextProps = {
validations: {
validation_decisions: {
username: 'Username must be between 2 and 30 characters long.',
},
},
thirdPartyAuthContext: {
pipelineUserDetails: {
name: 'test',
email: 'test@example.com',
username: 'test-username',
},
},
registrationError: {
username: [{ username: 'Username must be between 2 and 30 characters long.' }],
},
};
const root = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
const shouldUpdate = root.find('RegistrationPage').instance().shouldComponentUpdate(nextProps);
expect(root.find('RegistrationPage').state('currentValidations')).not.toEqual(null);
expect(root.find('RegistrationPage').state('formValid')).not.toEqual(true);
expect(shouldUpdate).toBe(false);
});
it('tests onClick should change errors state via realtime validation', () => {
const nextProps = {
validations: {
validation_decisions: {
username: 'Username must be between 2 and 30 characters long.',
},
},
};
const errors = {
email: '',
name: '',
username: 'Username must be between 2 and 30 characters long.',
password: '',
country: '',
honorCode: '',
termsOfService: '',
};
const root = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
root.find('RegistrationPage').instance().shouldComponentUpdate(nextProps);
root.find('input#username').simulate('click', { target: { value: '', name: 'username' } });
expect(root.find('RegistrationPage').state('errors')).toEqual(errors);
});
it('should not dispatch registerNewUser on Submit', () => {
const formPayload = {
email: '',

View File

@@ -44,17 +44,24 @@ exports[`./RegistrationPage.js should display no password field when current pro
Full Name (required)
</label>
<input
aria-describedby=""
aria-describedby="name-help-text"
className="form-control"
id="name"
name="name"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
placeholder=""
required={true}
type="text"
value=""
/>
<small
className="form-text text-muted"
id="name-help-text"
>
This name will be used by any certificates that you earn.
</small>
</div>
<div
className="form-group mb-0"
@@ -66,17 +73,25 @@ exports[`./RegistrationPage.js should display no password field when current pro
Public Username (required)
</label>
<input
aria-describedby=""
aria-describedby="username-help-text"
className="form-control"
id="username"
maxLength="30"
name="username"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
placeholder=""
required={true}
type="text"
value=""
/>
<small
className="form-text text-muted"
id="username-help-text"
>
The name that will identify you in your courses. It cannot be changed later.
</small>
</div>
<div
className="form-group mb-0"
@@ -88,17 +103,24 @@ exports[`./RegistrationPage.js should display no password field when current pro
Email (required)
</label>
<input
aria-describedby=""
aria-describedby="email-help-text"
className="form-control"
id="email"
name="email"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
placeholder="username@domain.com"
required={true}
type="email"
value=""
/>
<small
className="form-text text-muted"
id="email-help-text"
>
This is what you will use to login.
</small>
</div>
<div
className="form-group custom-control mb-0"
@@ -248,17 +270,24 @@ exports[`./RegistrationPage.js should match TPA provider snapshot 1`] = `
Full Name (required)
</label>
<input
aria-describedby=""
aria-describedby="name-help-text"
className="form-control"
id="name"
name="name"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
placeholder=""
required={true}
type="text"
value=""
/>
<small
className="form-text text-muted"
id="name-help-text"
>
This name will be used by any certificates that you earn.
</small>
</div>
<div
className="form-group mb-0"
@@ -270,17 +299,25 @@ exports[`./RegistrationPage.js should match TPA provider snapshot 1`] = `
Public Username (required)
</label>
<input
aria-describedby=""
aria-describedby="username-help-text"
className="form-control"
id="username"
maxLength="30"
name="username"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
placeholder=""
required={true}
type="text"
value=""
/>
<small
className="form-text text-muted"
id="username-help-text"
>
The name that will identify you in your courses. It cannot be changed later.
</small>
</div>
<div
className="form-group mb-0"
@@ -292,17 +329,24 @@ exports[`./RegistrationPage.js should match TPA provider snapshot 1`] = `
Email (required)
</label>
<input
aria-describedby=""
aria-describedby="email-help-text"
className="form-control"
id="email"
name="email"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
placeholder="username@domain.com"
required={true}
type="email"
value=""
/>
<small
className="form-text text-muted"
id="email-help-text"
>
This is what you will use to login.
</small>
</div>
<div
className="form-group mb-0"
@@ -314,17 +358,24 @@ exports[`./RegistrationPage.js should match TPA provider snapshot 1`] = `
Password (required)
</label>
<input
aria-describedby=""
aria-describedby="password-help-text"
className="form-control"
id="password"
name="password"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
placeholder=""
required={true}
type="password"
value=""
/>
<small
className="form-text text-muted"
id="password-help-text"
>
Your password must contain at least 8 characters, including 1 letter & 1 number.
</small>
</div>
<div
className="form-group custom-control mb-0"
@@ -434,17 +485,24 @@ exports[`./RegistrationPage.js should match default section snapshot 1`] = `
Full Name (required)
</label>
<input
aria-describedby=""
aria-describedby="name-help-text"
className="form-control"
id="name"
name="name"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
placeholder=""
required={true}
type="text"
value=""
/>
<small
className="form-text text-muted"
id="name-help-text"
>
This name will be used by any certificates that you earn.
</small>
</div>
<div
className="form-group mb-0"
@@ -456,17 +514,25 @@ exports[`./RegistrationPage.js should match default section snapshot 1`] = `
Public Username (required)
</label>
<input
aria-describedby=""
aria-describedby="username-help-text"
className="form-control"
id="username"
maxLength="30"
name="username"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
placeholder=""
required={true}
type="text"
value=""
/>
<small
className="form-text text-muted"
id="username-help-text"
>
The name that will identify you in your courses. It cannot be changed later.
</small>
</div>
<div
className="form-group mb-0"
@@ -478,17 +544,24 @@ exports[`./RegistrationPage.js should match default section snapshot 1`] = `
Email (required)
</label>
<input
aria-describedby=""
aria-describedby="email-help-text"
className="form-control"
id="email"
name="email"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
placeholder="username@domain.com"
required={true}
type="email"
value=""
/>
<small
className="form-text text-muted"
id="email-help-text"
>
This is what you will use to login.
</small>
</div>
<div
className="form-group mb-0"
@@ -500,17 +573,24 @@ exports[`./RegistrationPage.js should match default section snapshot 1`] = `
Password (required)
</label>
<input
aria-describedby=""
aria-describedby="password-help-text"
className="form-control"
id="password"
name="password"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
placeholder=""
required={true}
type="password"
value=""
/>
<small
className="form-text text-muted"
id="password-help-text"
>
Your password must contain at least 8 characters, including 1 letter & 1 number.
</small>
</div>
<div
className="form-group custom-control mb-0"
@@ -620,17 +700,24 @@ exports[`./RegistrationPage.js should match pending button state snapshot 1`] =
Full Name (required)
</label>
<input
aria-describedby=""
aria-describedby="name-help-text"
className="form-control"
id="name"
name="name"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
placeholder=""
required={true}
type="text"
value=""
/>
<small
className="form-text text-muted"
id="name-help-text"
>
This name will be used by any certificates that you earn.
</small>
</div>
<div
className="form-group mb-0"
@@ -642,17 +729,25 @@ exports[`./RegistrationPage.js should match pending button state snapshot 1`] =
Public Username (required)
</label>
<input
aria-describedby=""
aria-describedby="username-help-text"
className="form-control"
id="username"
maxLength="30"
name="username"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
placeholder=""
required={true}
type="text"
value=""
/>
<small
className="form-text text-muted"
id="username-help-text"
>
The name that will identify you in your courses. It cannot be changed later.
</small>
</div>
<div
className="form-group mb-0"
@@ -664,17 +759,24 @@ exports[`./RegistrationPage.js should match pending button state snapshot 1`] =
Email (required)
</label>
<input
aria-describedby=""
aria-describedby="email-help-text"
className="form-control"
id="email"
name="email"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
placeholder="username@domain.com"
required={true}
type="email"
value=""
/>
<small
className="form-text text-muted"
id="email-help-text"
>
This is what you will use to login.
</small>
</div>
<div
className="form-group mb-0"
@@ -686,17 +788,24 @@ exports[`./RegistrationPage.js should match pending button state snapshot 1`] =
Password (required)
</label>
<input
aria-describedby=""
aria-describedby="password-help-text"
className="form-control"
id="password"
name="password"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
placeholder=""
required={true}
type="password"
value=""
/>
<small
className="form-text text-muted"
id="password-help-text"
>
Your password must contain at least 8 characters, including 1 letter & 1 number.
</small>
</div>
<div
className="form-group custom-control mb-0"
@@ -841,17 +950,24 @@ exports[`./RegistrationPage.js should show error message on 409 1`] = `
Full Name (required)
</label>
<input
aria-describedby=""
aria-describedby="name-help-text"
className="form-control"
id="name"
name="name"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
placeholder=""
required={true}
type="text"
value=""
/>
<small
className="form-text text-muted"
id="name-help-text"
>
This name will be used by any certificates that you earn.
</small>
</div>
<div
className="form-group mb-0"
@@ -863,17 +979,25 @@ exports[`./RegistrationPage.js should show error message on 409 1`] = `
Public Username (required)
</label>
<input
aria-describedby=""
aria-describedby="username-help-text"
className="form-control"
id="username"
maxLength="30"
name="username"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
placeholder=""
required={true}
type="text"
value=""
/>
<small
className="form-text text-muted"
id="username-help-text"
>
The name that will identify you in your courses. It cannot be changed later.
</small>
</div>
<div
className="form-group mb-0"
@@ -885,17 +1009,24 @@ exports[`./RegistrationPage.js should show error message on 409 1`] = `
Email (required)
</label>
<input
aria-describedby=""
aria-describedby="email-help-text"
className="form-control"
id="email"
name="email"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
placeholder="username@domain.com"
required={true}
type="email"
value=""
/>
<small
className="form-text text-muted"
id="email-help-text"
>
This is what you will use to login.
</small>
</div>
<div
className="form-group mb-0"
@@ -907,17 +1038,24 @@ exports[`./RegistrationPage.js should show error message on 409 1`] = `
Password (required)
</label>
<input
aria-describedby=""
aria-describedby="password-help-text"
className="form-control"
id="password"
name="password"
onBlur={[Function]}
onChange={[Function]}
onClick={[Function]}
placeholder=""
required={true}
type="password"
value=""
/>
<small
className="form-text text-muted"
id="password-help-text"
>
Your password must contain at least 8 characters, including 1 letter & 1 number.
</small>
</div>
<div
className="form-group custom-control mb-0"