diff --git a/src/components/join-flow/email-step.jsx b/src/components/join-flow/email-step.jsx
index 6d7b8bff7..c70093c74 100644
--- a/src/components/join-flow/email-step.jsx
+++ b/src/components/join-flow/email-step.jsx
@@ -25,8 +25,7 @@ class EmailStep extends React.Component {
'validateForm',
'setCaptchaRef',
'captchaSolved',
- 'onCaptchaLoad',
- 'onCaptchaError'
+ 'onCaptchaLoad'
]);
this.state = {
captchaIsLoading: true
@@ -49,7 +48,7 @@ class EmailStep extends React.Component {
// Load Google ReCaptcha script.
const script = document.createElement('script');
script.async = true;
- script.onerror = this.onCaptchaError;
+ script.onerror = this.props.onCaptchaError;
script.src = `https://www.recaptcha.net/recaptcha/api.js?onload=grecaptchaOnLoad&render=explicit&hl=${window._locale}`;
document.body.appendChild(script);
}
@@ -60,20 +59,13 @@ class EmailStep extends React.Component {
handleSetEmailRef (emailInputRef) {
this.emailInput = emailInputRef;
}
- onCaptchaError () {
- this.props.onRegistrationError(
- this.props.intl.formatMessage({
- id: 'registration.troubleReload'
- })
- );
- }
onCaptchaLoad () {
this.setState({captchaIsLoading: false});
this.grecaptcha = window.grecaptcha;
if (!this.grecaptcha) {
// According to the reCaptcha documentation, this callback shouldn't get
// called unless window.grecaptcha exists. This is just here to be extra defensive.
- this.onCaptchaError();
+ this.props.onCaptchaError();
return;
}
this.widgetId = this.grecaptcha.render(this.captchaRef,
@@ -234,8 +226,8 @@ class EmailStep extends React.Component {
EmailStep.propTypes = {
intl: intlShape,
+ onCaptchaError: PropTypes.func,
onNextStep: PropTypes.func,
- onRegistrationError: PropTypes.func,
waiting: PropTypes.bool
};
diff --git a/src/components/join-flow/join-flow-steps.scss b/src/components/join-flow/join-flow-steps.scss
index 78df6a2bc..3d7e1df30 100644
--- a/src/components/join-flow/join-flow-steps.scss
+++ b/src/components/join-flow/join-flow-steps.scss
@@ -33,8 +33,9 @@
.join-flow-instructions {
font-size: .875rem;
font-weight: bold;
- line-height: 1.37500rem;
- margin-bottom: 1rem;
+ line-height: 1.375rem;
+ margin-top: 1.25rem;
+ margin-bottom: .5rem;
text-align: center;
}
@@ -161,6 +162,15 @@
padding-bottom: 1rem;
}
+.join-flow-inner-error-step {
+ user-select: text; /* make text selectable, so users can copy errors */
+ padding-top: 5.5rem;
+}
+
+.join-flow-error-title {
+ margin-bottom: 2rem;
+}
+
.join-flow-birthdate-title {
margin-bottom: 2.875rem;
}
@@ -177,11 +187,6 @@
background-color: $dd-medium-blue;
}
-.join-flow-registration-error {
- user-select: text; /* make text selectable, so users can copy errors */
- padding-top: 5.5rem;
-}
-
.join-flow-gender-description {
margin-top: .625rem;
margin-bottom: 1.25rem;
@@ -197,11 +202,7 @@
}
.join-flow-welcome-title {
- margin-bottom: .25rem;
-}
-
-.join-flow-welcome-description {
- margin-bottom: 1.25rem;
+ margin-bottom: 1rem;
}
.welcome-step-image {
diff --git a/src/components/join-flow/join-flow.jsx b/src/components/join-flow/join-flow.jsx
index b3139677c..876bab9b6 100644
--- a/src/components/join-flow/join-flow.jsx
+++ b/src/components/join-flow/join-flow.jsx
@@ -8,6 +8,7 @@ const api = require('../../lib/api');
const injectIntl = require('../../lib/intl.jsx').injectIntl;
const intlShape = require('../../lib/intl.jsx').intlShape;
const sessionActions = require('../../redux/session.js');
+const validate = require('../../lib/validate');
const Progression = require('../progression/progression.jsx');
const UsernameStep = require('./username-step.jsx');
@@ -23,8 +24,8 @@ class JoinFlow extends React.Component {
super(props);
bindAll(this, [
'handleAdvanceStep',
+ 'handleCaptchaError',
'handleErrorNext',
- 'handleRegistrationError',
'handlePrepareToRegister',
'handleRegistrationResponse',
'handleSubmitRegistration'
@@ -42,15 +43,17 @@ class JoinFlow extends React.Component {
this.state = this.initialState;
}
canTryAgain () {
- return (this.state.numAttempts <= 1);
+ return (this.state.registrationError.errorAllowsTryAgain && this.state.numAttempts <= 1);
}
- handleRegistrationError (message) {
- if (!message) {
- message = this.props.intl.formatMessage({
- id: 'registration.generalError'
- });
- }
- this.setState({registrationError: message});
+ handleCaptchaError () {
+ this.setState({
+ registrationError: {
+ errorAllowsTryAgain: false,
+ errorMsg: this.props.intl.formatMessage({
+ id: 'registration.errorCaptcha'
+ })
+ }
+ });
}
handlePrepareToRegister (newFormData) {
newFormData = newFormData || {};
@@ -61,59 +64,113 @@ class JoinFlow extends React.Component {
this.handleSubmitRegistration(this.state.formData);
});
}
+ getErrorsFromResponse (err, body, res) {
+ const errorsFromResponse = [];
+ if (!err && res.statusCode === 200 && body && body[0]) {
+ const responseBodyErrors = body[0].errors;
+ if (responseBodyErrors) {
+ Object.keys(responseBodyErrors).forEach(fieldName => {
+ const errorStrs = responseBodyErrors[fieldName];
+ errorStrs.forEach(errorStr => {
+ errorsFromResponse.push({fieldName: fieldName, errorStr: errorStr});
+ });
+ });
+ }
+ }
+ return errorsFromResponse;
+ }
+ getCustomErrMsg (errorsFromResponse) {
+ if (!errorsFromResponse || errorsFromResponse.length === 0) return null;
+ let customErrMsg = '';
+ // body can include zero or more error objects. Here we assemble
+ // all of them into a single string, customErrMsg.
+ errorsFromResponse.forEach(errorFromResponse => {
+ if (customErrMsg.length) customErrMsg += '; ';
+ customErrMsg += `${errorFromResponse.fieldName}: ${errorFromResponse.errorStr}`;
+ });
+ const problemsStr = this.props.intl.formatMessage({id: 'registration.problemsAre'});
+ return `${problemsStr}: "${customErrMsg}"`;
+ }
+ registrationIsSuccessful (err, body, res) {
+ return !!(!err && res.statusCode === 200 && body && body[0] && body[0].success);
+ }
+ // example of failing response:
+ // [
+ // {
+ // "msg": "This field is required.",
+ // "errors": {
+ // "username": ["This field is required."],
+ // "recaptcha": ["Incorrect, please try again."]
+ // },
+ // "success": false
+ // }
+ // ]
+ //
+ // username messages:
+ // * "username": ["username exists"]
+ // * "username": ["invalid username"] (length, charset)
+ // * "username": ["bad username"] (cleanspeak)
+ // password messages:
+ // * "password": ["Ensure this value has at least 6 characters (it has LENGTH_NUM_HERE)."]
+ // recaptcha messages:
+ // * "recaptcha": ["This field is required."]
+ // * "recaptcha": ["Incorrect, please try again."]
+ // * "recaptcha": [some timeout message?]
+ // other messages:
+ // * "birth_month": ["Ensure this value is less than or equal to 12."]
+ // * "birth_month": ["Ensure this value is greater than or equal to 1."]
handleRegistrationResponse (err, body, res) {
- // example of failing response:
- // [
- // {
- // "msg": "This field is required.",
- // "errors": {
- // "username": ["This field is required."],
- // "recaptcha": ["Incorrect, please try again."]
- // },
- // "success": false
- // }
- // ]
- // username: 'username exists'
this.setState({
numAttempts: this.state.numAttempts + 1,
waiting: false
}, () => {
- let errStr = '';
- if (!err && res.statusCode === 200) {
- if (body && body[0]) {
- if (body[0].success) {
- this.props.refreshSession();
- this.setState({
- step: this.state.step + 1
- });
- return;
- }
- if (body[0].errors) {
- // body can include zero or more error objects, each
- // with its own key and description. Here we assemble
- // all of them into a single string, errStr.
- const errorKeys = Object.keys(body[0].errors);
- errorKeys.forEach(key => {
- const val = body[0].errors[key];
- if (val && val[0]) {
- if (errStr.length) errStr += '; ';
- errStr += `${key}: ${val[0]}`;
- }
- });
- }
- if (!errStr.length && body[0].msg) errStr = body[0].msg;
+ const success = this.registrationIsSuccessful(err, body, res);
+ if (success) {
+ this.props.refreshSession();
+ this.setState({step: this.state.step + 1});
+ return;
+ }
+ // now we know something went wrong -- either an actual error (client-side
+ // or server-side), or just a problem with the registration content.
+
+ // if an actual error, prompt user to try again.
+ if (err || res.statusCode !== 200) {
+ this.setState({registrationError: {errorAllowsTryAgain: true}});
+ return;
+ }
+
+ // now we know there was a problem with the registration content.
+ // If the server provided us info on why registration failed,
+ // build a summary explanation string
+ let errorMsg = null;
+ const errorsFromResponse = this.getErrorsFromResponse(err, body, res);
+ // if there was exactly one error, check if we have a pre-written message
+ // about that precise error
+ if (errorsFromResponse.length === 1) {
+ const singleErrMsgId = validate.responseErrorMsg(
+ errorsFromResponse[0].fieldName,
+ errorsFromResponse[0].errorStr
+ );
+ if (singleErrMsgId) { // one error that we have a predefined explanation string for
+ errorMsg = this.props.intl.formatMessage({id: singleErrMsgId});
}
}
+ // if we have more than one error, build a custom message with all of the
+ // server-provided error messages
+ if (!errorMsg && errorsFromResponse.length > 0) {
+ errorMsg = this.getCustomErrMsg(errorsFromResponse);
+ }
this.setState({
- registrationError: errStr ||
- `${this.props.intl.formatMessage({
- id: 'registration.generalError'
- })} (${res.statusCode})`
+ registrationError: {
+ errorAllowsTryAgain: false,
+ errorMsg: errorMsg
+ }
});
});
}
handleSubmitRegistration (formData) {
this.setState({
+ registrationError: null, // clear any existing error
waiting: true
}, () => {
api({
@@ -164,7 +221,7 @@ class JoinFlow extends React.Component {
{this.state.registrationError ? (
+ >
+
+
+
+
tips, tutorials, and guides.",
"registration.choosePasswordStepDescription": "Type in a new password for your account. You will use this password the next time you log into Scratch.",
@@ -172,6 +173,10 @@
"registration.confirmYourEmailDescription": "If you haven't already, please click the link in the confirmation email sent to:",
"registration.createAccount": "Create Your Account",
"registration.createUsername": "Create a username",
+ "registration.errorBadUsername": "The username you chose is not allowed. Try again with a different username.",
+ "registration.errorCaptcha": "There was a problem with the CAPTCHA test.",
+ "registration.errorPasswordTooShort": "Your password is too short. It needs to be at least 6 letters long.",
+ "registration.errorUsernameExists": "The username you chose already exists. Try again with a different username.",
"registration.genderStepTitle": "What's your gender?",
"registration.genderStepDescription": "Scratch welcomes people of all genders.",
"registration.genderStepInfo": "This helps us understand who uses Scratch, so that we can broaden participation. This information will not be made public on your account.",
@@ -194,11 +199,14 @@
"registration.personalStepTitle": "Personal Information",
"registration.personalStepDescription": "Your individual responses will not be displayed publicly, and will be kept confidential and secure",
"registration.private": "We will keep this information private.",
+ "registration.problemsAre": "The problems are:",
"registration.receiveEmails": "I'd like to receive emails from the Scratch Team about project ideas, events, and more.",
"registration.selectCountry": "Select country",
+ "registration.startOverInstruction": "Click \"Start over.\"",
"registration.studentPersonalStepDescription": "This information will not appear on the Scratch website.",
"registration.showPassword": "Show password",
"registration.troubleReload": "Scratch is having trouble finishing registration. Try reloading the page or try again in another browser.",
+ "registration.tryAgainInstruction": "Click \"Try again\".",
"registration.usernameStepDescription": "Fill in the following forms to request an account. The approval process may take up to one day.",
"registration.usernameStepDescriptionNonEducator": "Create projects, share ideas, make friends. It’s free!",
"registration.usernameStepRealName": "Please do not use any portion of your real name in your username.",
diff --git a/src/lib/validate.js b/src/lib/validate.js
index 5df20b0e4..65efcc03e 100644
--- a/src/lib/validate.js
+++ b/src/lib/validate.js
@@ -101,3 +101,30 @@ module.exports.validateEmailRemotely = email => (
});
})
);
+
+const responseErrorMsgs = module.exports.responseErrorMsgs = {
+ username: {
+ 'username exists': {errMsgId: 'registration.errorUsernameExists'},
+ 'bad username': {errMsgId: 'registration.errorBadUsername'}
+ },
+ password: {
+ 'Ensure this value has at least 6 characters \\(it has \\d\\).': {
+ errMsgId: 'registration.errorPasswordTooShort'
+ }
+ },
+ recaptcha: {
+ 'Incorrect, please try again.': {errMsgId: 'registration.errorCaptcha'}
+ }
+};
+
+module.exports.responseErrorMsg = (fieldName, serverRawErr) => {
+ if (fieldName && responseErrorMsgs[fieldName]) {
+ const serverErrPatterns = responseErrorMsgs[fieldName];
+ // use regex compare to find matching error string in responseErrorMsgs
+ const matchingKey = Object.keys(serverErrPatterns).find(errPattern => (
+ RegExp(errPattern).test(serverRawErr)
+ ));
+ if (matchingKey) return responseErrorMsgs[fieldName][matchingKey].errMsgId;
+ }
+ return null;
+};
diff --git a/test/unit/components/email-step.test.jsx b/test/unit/components/email-step.test.jsx
index 9896d84f9..f74eebc9c 100644
--- a/test/unit/components/email-step.test.jsx
+++ b/test/unit/components/email-step.test.jsx
@@ -26,23 +26,23 @@ describe('EmailStep test', () => {
});
test('send correct props to formik', () => {
- const wrapper = shallowWithIntl();
+ const intlWrapper = shallowWithIntl();
- const formikWrapper = wrapper.dive();
- expect(formikWrapper.props().initialValues.subscribe).toBe(false);
- expect(formikWrapper.props().initialValues.email).toBe('');
- expect(formikWrapper.props().validateOnBlur).toBe(false);
- expect(formikWrapper.props().validateOnChange).toBe(false);
- expect(formikWrapper.props().validate).toBe(formikWrapper.instance().validateForm);
- expect(formikWrapper.props().onSubmit).toBe(formikWrapper.instance().handleValidSubmit);
+ const emailStepWrapper = intlWrapper.dive();
+ expect(emailStepWrapper.props().initialValues.subscribe).toBe(false);
+ expect(emailStepWrapper.props().initialValues.email).toBe('');
+ expect(emailStepWrapper.props().validateOnBlur).toBe(false);
+ expect(emailStepWrapper.props().validateOnChange).toBe(false);
+ expect(emailStepWrapper.props().validate).toBe(emailStepWrapper.instance().validateForm);
+ expect(emailStepWrapper.props().onSubmit).toBe(emailStepWrapper.instance().handleValidSubmit);
});
test('props sent to JoinFlowStep', () => {
- const wrapper = shallowWithIntl();
+ const intlWrapper = shallowWithIntl();
// Dive to get past the intl wrapper
- const formikWrapper = wrapper.dive();
+ const emailStepWrapper = intlWrapper.dive();
// Dive to get past the anonymous component.
- const joinFlowWrapper = formikWrapper.dive().find(JoinFlowStep);
+ const joinFlowWrapper = emailStepWrapper.dive().find(JoinFlowStep);
expect(joinFlowWrapper).toHaveLength(1);
expect(joinFlowWrapper.props().footerContent.props.id).toBe('registration.acceptTermsOfUse');
expect(joinFlowWrapper.props().headerImgSrc).toBe('/images/join-flow/email-header.png');
@@ -54,11 +54,11 @@ describe('EmailStep test', () => {
});
test('props sent to FormikInput for email', () => {
- const wrapper = shallowWithIntl();
+ const intlWrapper = shallowWithIntl();
// Dive to get past the intl wrapper
- const formikWrapper = wrapper.dive();
+ const emailStepWrapper = intlWrapper.dive();
// Dive to get past the anonymous component.
- const joinFlowWrapper = formikWrapper.dive().find(JoinFlowStep);
+ const joinFlowWrapper = emailStepWrapper.dive().find(JoinFlowStep);
expect(joinFlowWrapper).toHaveLength(1);
const emailInputWrapper = joinFlowWrapper.find(FormikInput).first();
expect(emailInputWrapper.props().id).toEqual('email');
@@ -66,16 +66,16 @@ describe('EmailStep test', () => {
expect(emailInputWrapper.props().name).toEqual('email');
expect(emailInputWrapper.props().placeholder).toEqual('general.emailAddress');
expect(emailInputWrapper.props().validationClassName).toEqual('validation-full-width-input');
- expect(emailInputWrapper.props().onSetRef).toEqual(formikWrapper.instance().handleSetEmailRef);
- expect(emailInputWrapper.props().validate).toEqual(formikWrapper.instance().validateEmail);
+ expect(emailInputWrapper.props().onSetRef).toEqual(emailStepWrapper.instance().handleSetEmailRef);
+ expect(emailInputWrapper.props().validate).toEqual(emailStepWrapper.instance().validateEmail);
});
test('props sent to FormikCheckbox for subscribe', () => {
- const wrapper = shallowWithIntl();
+ const intlWrapper = shallowWithIntl();
// Dive to get past the intl wrapper
- const formikWrapper = wrapper.dive();
+ const emailStepWrapper = intlWrapper.dive();
// Dive to get past the anonymous component.
- const joinFlowWrapper = formikWrapper.dive().find(JoinFlowStep);
+ const joinFlowWrapper = emailStepWrapper.dive().find(JoinFlowStep);
expect(joinFlowWrapper).toHaveLength(1);
const checkboxWrapper = joinFlowWrapper.find(FormikCheckbox).first();
expect(checkboxWrapper).toHaveLength(1);
@@ -93,12 +93,12 @@ describe('EmailStep test', () => {
render: jest.fn()
};
const formData = {item1: 'thing', item2: 'otherthing'};
- const wrapper = shallowWithIntl(
+ const intlWrapper = shallowWithIntl(
);
- const formikWrapper = wrapper.dive();
- formikWrapper.instance().onCaptchaLoad(); // to setup catpcha state
- formikWrapper.instance().handleValidSubmit(formData, formikBag);
+ const emailStepWrapper = intlWrapper.dive();
+ emailStepWrapper.instance().onCaptchaLoad(); // to setup catpcha state
+ emailStepWrapper.instance().handleValidSubmit(formData, formikBag);
expect(formikBag.setSubmitting).toHaveBeenCalledWith(false);
expect(global.grecaptcha.execute).toHaveBeenCalled();
@@ -116,18 +116,18 @@ describe('EmailStep test', () => {
render: jest.fn()
};
const formData = {item1: 'thing', item2: 'otherthing'};
- const wrapper = shallowWithIntl(
+ const intlWrapper = shallowWithIntl(
);
- const formikWrapper = wrapper.dive();
+ const emailStepWrapper = intlWrapper.dive();
// Call these to setup captcha.
- formikWrapper.instance().onCaptchaLoad(); // to setup catpcha state
- formikWrapper.instance().handleValidSubmit(formData, formikBag);
+ emailStepWrapper.instance().onCaptchaLoad(); // to setup catpcha state
+ emailStepWrapper.instance().handleValidSubmit(formData, formikBag);
const captchaToken = 'abcd';
- formikWrapper.instance().captchaSolved(captchaToken);
+ emailStepWrapper.instance().captchaSolved(captchaToken);
// Make sure captchaSolved calls onNextStep with formData that has
// a captcha token and left everything else in the object in place.
expect(props.onNextStep).toHaveBeenCalledWith(
@@ -139,65 +139,51 @@ describe('EmailStep test', () => {
expect(formikBag.setSubmitting).toHaveBeenCalledWith(true);
});
- test('onCaptchaError calls error function with correct message', () => {
- const props = {
- onRegistrationError: jest.fn()
- };
-
- const wrapper = shallowWithIntl(
- );
-
- const formikWrapper = wrapper.dive();
- formikWrapper.instance().onCaptchaError();
- expect(props.onRegistrationError).toHaveBeenCalledWith('registration.troubleReload');
- });
-
test('Captcha load error calls error function', () => {
const props = {
- onRegistrationError: jest.fn()
+ onCaptchaError: jest.fn()
};
// Set this to null to force an error.
global.grecaptcha = null;
- const wrapper = shallowWithIntl(
+ const intlWrapper = shallowWithIntl(
);
+ />
+ );
- const formikWrapper = wrapper.dive();
- formikWrapper.instance().onCaptchaLoad();
- expect(props.onRegistrationError).toHaveBeenCalledWith('registration.troubleReload');
+ const emailStepWrapper = intlWrapper.dive();
+ emailStepWrapper.instance().onCaptchaLoad();
+ expect(props.onCaptchaError).toHaveBeenCalled();
});
test('validateEmail test email empty', () => {
- const wrapper = shallowWithIntl(
+ const intlWrapper = shallowWithIntl(
);
- const formikWrapper = wrapper.dive();
- const val = formikWrapper.instance().validateEmail('');
+ const emailStepWrapper = intlWrapper.dive();
+ const val = emailStepWrapper.instance().validateEmail('');
expect(val).toBe('general.required');
});
test('validateEmail test email null', () => {
- const wrapper = shallowWithIntl(
+ const intlWrapper = shallowWithIntl(
);
- const formikWrapper = wrapper.dive();
- const val = formikWrapper.instance().validateEmail(null);
+ const emailStepWrapper = intlWrapper.dive();
+ const val = emailStepWrapper.instance().validateEmail(null);
expect(val).toBe('general.required');
});
test('validateEmail test email undefined', () => {
- const wrapper = shallowWithIntl(
+ const intlWrapper = shallowWithIntl(
);
- const formikWrapper = wrapper.dive();
- const val = formikWrapper.instance().validateEmail();
+ const emailStepWrapper = intlWrapper.dive();
+ const val = emailStepWrapper.instance().validateEmail();
expect(val).toBe('general.required');
});
test('validateEmailRemotelyWithCache calls validate.validateEmailRemotely', done => {
- const wrapper = shallowWithIntl(
+ const intlWrapper = shallowWithIntl(
);
- const instance = wrapper.dive().instance();
+ const instance = intlWrapper.dive().instance();
instance.validateEmailRemotelyWithCache('some-email@some-domain.com')
.then(response => {
@@ -209,10 +195,10 @@ describe('EmailStep test', () => {
});
test('validateEmailRemotelyWithCache, called twice with different data, makes two remote requests', done => {
- const wrapper = shallowWithIntl(
+ const intlWrapper = shallowWithIntl(
);
- const instance = wrapper.dive().instance();
+ const instance = intlWrapper.dive().instance();
instance.validateEmailRemotelyWithCache('some-email@some-domain.com')
.then(response => {
@@ -233,10 +219,10 @@ describe('EmailStep test', () => {
});
test('validateEmailRemotelyWithCache, called twice with same data, only makes one remote request', done => {
- const wrapper = shallowWithIntl(
+ const intlWrapper = shallowWithIntl(
);
- const instance = wrapper.dive().instance();
+ const instance = intlWrapper.dive().instance();
instance.validateEmailRemotelyWithCache('some-email@some-domain.com')
.then(response => {
diff --git a/test/unit/components/join-flow.test.jsx b/test/unit/components/join-flow.test.jsx
index 8724c8ec4..66505d1b8 100644
--- a/test/unit/components/join-flow.test.jsx
+++ b/test/unit/components/join-flow.test.jsx
@@ -9,6 +9,35 @@ import RegistrationErrorStep from '../../../src/components/join-flow/registratio
describe('JoinFlow', () => {
const mockStore = configureStore();
let store;
+ const responseBodyMultipleErrs = [
+ {
+ msg: 'This field is required.',
+ errors: {
+ username: ['This field is required.'],
+ recaptcha: ['Incorrect, please try again.']
+ },
+ success: false
+ }
+ ];
+ const responseBodySingleErr = [
+ {
+ msg: 'This field is required.',
+ errors: {
+ recaptcha: ['Incorrect, please try again.']
+ },
+ success: false
+ }
+ ];
+ const responseBodySuccess = [
+ {
+ msg: 'This field is required.',
+ errors: {
+ recaptcha: ['Incorrect, please try again.']
+ },
+ success: true
+ }
+ ];
+
beforeEach(() => {
store = mockStore({sessionActions: {
@@ -28,118 +57,14 @@ describe('JoinFlow', () => {
.dive(); // unwrap injectIntl(JoinFlow)
};
- test('handleRegistrationResponse with successful response', () => {
- const props = {
- refreshSession: jest.fn()
- };
- const joinFlowInstance = getJoinFlowWrapper(props).instance();
- const responseErr = null;
- const responseBody = [
- {
- success: true
- }
- ];
- const responseObj = {
- statusCode: 200
- };
- joinFlowInstance.handleRegistrationResponse(responseErr, responseBody, responseObj);
- expect(joinFlowInstance.props.refreshSession).toHaveBeenCalled();
- expect(joinFlowInstance.state.registrationError).toBe(null);
- });
-
- test('handleRegistrationResponse with healthy response, indicating failure', () => {
- const props = {
- refreshSession: jest.fn()
- };
- const joinFlowInstance = getJoinFlowWrapper(props).instance();
- const responseErr = null;
- const responseBody = [
- {
- msg: 'This field is required.',
- errors: {
- username: ['This field is required.']
- },
- success: false
- }
- ];
- const responseObj = {
- statusCode: 200
- };
- joinFlowInstance.handleRegistrationResponse(responseErr, responseBody, responseObj);
- expect(joinFlowInstance.props.refreshSession).not.toHaveBeenCalled();
- expect(joinFlowInstance.state.registrationError).toBe('username: This field is required.');
- });
-
- test('handleRegistrationResponse with failure response, with error fields missing', () => {
- const props = {
- refreshSession: jest.fn()
- };
- const joinFlowInstance = getJoinFlowWrapper(props).instance();
- const responseErr = null;
- const responseBody = [
- {
- msg: 'This field is required.',
- success: false
- }
- ];
- const responseObj = {
- statusCode: 200
- };
- joinFlowInstance.handleRegistrationResponse(responseErr, responseBody, responseObj);
- expect(joinFlowInstance.props.refreshSession).not.toHaveBeenCalled();
- expect(joinFlowInstance.state.registrationError).toBe('This field is required.');
- });
-
- test('handleRegistrationResponse with failure response, with no text explanation', () => {
- const props = {
- refreshSession: jest.fn()
- };
- const joinFlowInstance = getJoinFlowWrapper(props).instance();
- const responseErr = null;
- const responseBody = [
- {
- success: false
- }
- ];
- const responseObj = {
- statusCode: 200
- };
- joinFlowInstance.handleRegistrationResponse(responseErr, responseBody, responseObj);
- expect(joinFlowInstance.props.refreshSession).not.toHaveBeenCalled();
- expect(joinFlowInstance.state.registrationError).toBe('registration.generalError (200)');
- });
-
- test('handleRegistrationResponse with failure status code', () => {
- const props = {
- refreshSession: jest.fn()
- };
- const joinFlowInstance = getJoinFlowWrapper(props).instance();
- const responseErr = null;
- const responseBody = [
- {
- success: false
- }
- ];
- const responseObj = {
- statusCode: 400
- };
- joinFlowInstance.handleRegistrationResponse(responseErr, responseBody, responseObj);
- expect(joinFlowInstance.props.refreshSession).not.toHaveBeenCalled();
- expect(joinFlowInstance.state.registrationError).toBe('registration.generalError (400)');
- });
-
- test('handleRegistrationError with no message ', () => {
+ test('handleCaptchaError gives state with captcha message', () => {
const joinFlowInstance = getJoinFlowWrapper().instance();
joinFlowInstance.setState({});
- joinFlowInstance.handleRegistrationError();
- expect(joinFlowInstance.state.registrationError).toBe('registration.generalError');
- });
-
- test('handleRegistrationError with message ', () => {
- const joinFlowInstance = getJoinFlowWrapper().instance();
- joinFlowInstance.setState({});
- joinFlowInstance.handleRegistrationError('my message');
- expect(joinFlowInstance.state.registrationError).toBe('my message');
+ joinFlowInstance.handleCaptchaError();
+ expect(joinFlowInstance.state.registrationError).toEqual({
+ errorAllowsTryAgain: false,
+ errorMsg: 'registration.errorCaptcha'
+ });
});
test('handleAdvanceStep', () => {
@@ -178,39 +103,64 @@ describe('JoinFlow', () => {
expect(progressionWrapper).toHaveLength(1);
});
- test('when numAttempts is 0, RegistrationErrorStep receives canTryAgain prop with value true', () => {
+ test('when numAttempts is 0 and registrationError errorAllowsTryAgain is true, ' +
+ 'RegistrationErrorStep receives errorAllowsTryAgain prop with value true', () => {
const joinFlowWrapper = getJoinFlowWrapper();
joinFlowWrapper.instance().setState({
numAttempts: 0,
- registrationError: 'halp there is a errors!!'
+ registrationError: {
+ errorAllowsTryAgain: true,
+ errorMsg: 'halp there is a errors!!'
+ }
});
const registrationErrorWrapper = joinFlowWrapper.find(RegistrationErrorStep);
expect(registrationErrorWrapper.first().props().canTryAgain).toEqual(true);
});
- test('when numAttempts is 1, RegistrationErrorStep receives canTryAgain prop with value true', () => {
+ test('when numAttempts is 1 and registrationError errorAllowsTryAgain is true, ' +
+ 'RegistrationErrorStep receives errorAllowsTryAgain prop with value true', () => {
const joinFlowWrapper = getJoinFlowWrapper();
joinFlowWrapper.instance().setState({
numAttempts: 1,
- registrationError: 'halp there is a errors!!'
+ registrationError: {
+ errorAllowsTryAgain: true,
+ errorMsg: 'halp there is a errors!!'
+ }
});
const registrationErrorWrapper = joinFlowWrapper.find(RegistrationErrorStep);
expect(registrationErrorWrapper.first().props().canTryAgain).toEqual(true);
});
- test('when numAttempts is 2, RegistrationErrorStep receives canTryAgain prop with value false', () => {
+ test('when numAttempts is 2 and registrationError errorAllowsTryAgain is true, ' +
+ 'RegistrationErrorStep receives errorAllowsTryAgain prop with value false', () => {
const joinFlowWrapper = getJoinFlowWrapper();
joinFlowWrapper.instance().setState({
numAttempts: 2,
- registrationError: 'halp there is a errors!!'
+ registrationError: {
+ errorAllowsTryAgain: true,
+ errorMsg: 'halp there is a errors!!'
+ }
+ });
+ const registrationErrorWrapper = joinFlowWrapper.find(RegistrationErrorStep);
+ expect(registrationErrorWrapper.first().props().canTryAgain).toEqual(false);
+ });
+
+ test('when numAttempts is 0 and registrationError errorAllowsTryAgain is false, ' +
+ 'RegistrationErrorStep receives errorAllowsTryAgain prop with value false', () => {
+ const joinFlowWrapper = getJoinFlowWrapper();
+ joinFlowWrapper.instance().setState({
+ numAttempts: 0,
+ registrationError: {
+ errorAllowsTryAgain: false,
+ errorMsg: 'halp there is a errors!!'
+ }
});
const registrationErrorWrapper = joinFlowWrapper.find(RegistrationErrorStep);
expect(registrationErrorWrapper.first().props().canTryAgain).toEqual(false);
});
test('resetState resets entire state, does not leave any state keys out', () => {
- const joinFlowWrapper = getJoinFlowWrapper();
- const joinFlowInstance = joinFlowWrapper.instance();
+ const joinFlowInstance = getJoinFlowWrapper().instance();
Object.keys(joinFlowInstance.state).forEach(key => {
joinFlowInstance.setState({[key]: 'Different than the initial value'});
});
@@ -221,8 +171,7 @@ describe('JoinFlow', () => {
});
test('resetState makes each state field match initial state', () => {
- const joinFlowWrapper = getJoinFlowWrapper();
- const joinFlowInstance = joinFlowWrapper.instance();
+ const joinFlowInstance = getJoinFlowWrapper().instance();
const stateSnapshot = {};
Object.keys(joinFlowInstance.state).forEach(key => {
stateSnapshot[key] = joinFlowInstance.state[key];
@@ -234,8 +183,7 @@ describe('JoinFlow', () => {
});
test('calling resetState results in state.formData which is not same reference as before', () => {
- const joinFlowWrapper = getJoinFlowWrapper();
- const joinFlowInstance = joinFlowWrapper.instance();
+ const joinFlowInstance = getJoinFlowWrapper().instance();
joinFlowInstance.setState({
formData: defaults({}, {username: 'abcdef'})
});
@@ -244,4 +192,185 @@ describe('JoinFlow', () => {
expect(formDataReference).not.toBe(joinFlowInstance.state.formData);
expect(formDataReference).not.toEqual(joinFlowInstance.state.formData);
});
+
+ test('getErrorsFromResponse returns object of errors', () => {
+ const joinFlowInstance = getJoinFlowWrapper().instance();
+ const errorsFromResponse =
+ joinFlowInstance.getErrorsFromResponse(null, responseBodyMultipleErrs, {statusCode: 200});
+ expect(errorsFromResponse).toEqual([
+ {
+ fieldName: 'username',
+ errorStr: 'This field is required.'
+ }, {
+ fieldName: 'recaptcha',
+ errorStr: 'Incorrect, please try again.'
+ }
+ ]);
+ });
+
+ test('getErrorsFromResponse called with non-null err returns empty array', () => {
+ const joinFlowInstance = getJoinFlowWrapper().instance();
+ const errorsFromResponse =
+ joinFlowInstance.getErrorsFromResponse({}, responseBodyMultipleErrs, {statusCode: 200});
+ expect(errorsFromResponse).toEqual([]);
+ });
+
+ test('getErrorsFromResponse called with non-200 status code returns empty array', () => {
+ const joinFlowInstance = getJoinFlowWrapper().instance();
+ const errorsFromResponse =
+ joinFlowInstance.getErrorsFromResponse({}, responseBodyMultipleErrs, {statusCode: 400});
+ expect(errorsFromResponse).toEqual([]);
+ });
+
+ test('getErrorsFromResponse gets single error, when given response body with only one error', () => {
+ const joinFlowInstance = getJoinFlowWrapper().instance();
+ const errorsFromResponse =
+ joinFlowInstance.getErrorsFromResponse(null, responseBodySingleErr, {statusCode: 200});
+ expect(errorsFromResponse.length).toEqual(1);
+ });
+
+ test('getCustomErrMsg string when given response body with multiple errors', () => {
+ const joinFlowInstance = getJoinFlowWrapper().instance();
+ const errorsFromResponse =
+ joinFlowInstance.getErrorsFromResponse(null, responseBodyMultipleErrs, {statusCode: 200});
+ const customErrMsg = joinFlowInstance.getCustomErrMsg(errorsFromResponse);
+ expect(customErrMsg).toEqual('registration.problemsAre: "username: This field is required.; ' +
+ 'recaptcha: Incorrect, please try again."');
+ });
+
+ test('getCustomErrMsg string when given response body with single error', () => {
+ const joinFlowInstance = getJoinFlowWrapper().instance();
+ const errorsFromResponse =
+ joinFlowInstance.getErrorsFromResponse(null, responseBodySingleErr, {statusCode: 200});
+ const customErrMsg = joinFlowInstance.getCustomErrMsg(errorsFromResponse);
+ expect(customErrMsg).toEqual('registration.problemsAre: "recaptcha: Incorrect, please try again."');
+ });
+
+ test('registrationIsSuccessful returns true when given response body with single error', () => {
+ const joinFlowInstance = getJoinFlowWrapper().instance();
+ const success = joinFlowInstance.registrationIsSuccessful(null, responseBodySuccess, {statusCode: 200});
+ expect(success).toEqual(true);
+ });
+
+ test('registrationIsSuccessful returns false when given status code not 200', () => {
+ const joinFlowInstance = getJoinFlowWrapper().instance();
+ const success = joinFlowInstance.registrationIsSuccessful(null, responseBodySuccess, {statusCode: 500});
+ expect(success).toEqual(false);
+ });
+
+ test('registrationIsSuccessful returns false when given body with success field false', () => {
+ const joinFlowInstance = getJoinFlowWrapper().instance();
+ const success = joinFlowInstance.registrationIsSuccessful(null, responseBodySingleErr, {statusCode: 200});
+ expect(success).toEqual(false);
+ });
+
+ test('registrationIsSuccessful returns false when given non null err', () => {
+ const joinFlowInstance = getJoinFlowWrapper().instance();
+ const success = joinFlowInstance.registrationIsSuccessful({}, responseBodySuccess, {statusCode: 200});
+ expect(success).toEqual(false);
+ });
+
+ test('handleRegistrationResponse when passed body with success', () => {
+ const props = {
+ refreshSession: jest.fn()
+ };
+ const joinFlowInstance = getJoinFlowWrapper(props).instance();
+ joinFlowInstance.handleRegistrationResponse(null, responseBodySuccess, {statusCode: 200});
+ expect(joinFlowInstance.state.registrationError).toEqual(null);
+ expect(joinFlowInstance.props.refreshSession).toHaveBeenCalled();
+ expect(joinFlowInstance.state.step).toEqual(1);
+ expect(joinFlowInstance.state.waiting).toBeFalsy();
+ });
+
+ test('handleRegistrationResponse when passed body with preset server error', () => {
+ const joinFlowInstance = getJoinFlowWrapper().instance();
+ joinFlowInstance.handleRegistrationResponse(null, responseBodySingleErr, {statusCode: 200});
+ expect(joinFlowInstance.state.registrationError).toEqual({
+ errorAllowsTryAgain: false,
+ errorMsg: 'registration.errorCaptcha'
+ });
+ });
+
+ test('handleRegistrationResponse with failure response, with error fields missing', () => {
+ const props = {
+ refreshSession: jest.fn()
+ };
+ const joinFlowInstance = getJoinFlowWrapper(props).instance();
+ const responseErr = null;
+ const responseBody = [
+ {
+ msg: 'This field is required.',
+ success: false
+ }
+ ];
+ const responseObj = {
+ statusCode: 200
+ };
+ joinFlowInstance.handleRegistrationResponse(responseErr, responseBody, responseObj);
+ expect(joinFlowInstance.props.refreshSession).not.toHaveBeenCalled();
+ expect(joinFlowInstance.state.registrationError).toEqual({
+ errorAllowsTryAgain: false,
+ errorMsg: null
+ });
+ });
+
+ test('handleRegistrationResponse when passed body with unfamiliar server error', () => {
+ const joinFlowInstance = getJoinFlowWrapper().instance();
+ joinFlowInstance.handleRegistrationResponse(null, responseBodyMultipleErrs, {statusCode: 200});
+ expect(joinFlowInstance.state.registrationError).toEqual({
+ errorAllowsTryAgain: false,
+ errorMsg: 'registration.problemsAre: "username: This field is required.; ' +
+ 'recaptcha: Incorrect, please try again."'
+ });
+ });
+
+ test('handleRegistrationResponse with failure response, with no text explanation', () => {
+ const props = {
+ refreshSession: jest.fn()
+ };
+ const joinFlowInstance = getJoinFlowWrapper(props).instance();
+ const responseErr = null;
+ const responseBody = [
+ {
+ success: false
+ }
+ ];
+ const responseObj = {
+ statusCode: 200
+ };
+ joinFlowInstance.handleRegistrationResponse(responseErr, responseBody, responseObj);
+ expect(joinFlowInstance.props.refreshSession).not.toHaveBeenCalled();
+ expect(joinFlowInstance.state.registrationError).toEqual({
+ errorAllowsTryAgain: false,
+ errorMsg: null
+ });
+ });
+
+ test('handleRegistrationResponse when passed non null outgoing request error', () => {
+ const joinFlowInstance = getJoinFlowWrapper().instance();
+ joinFlowInstance.handleRegistrationResponse({}, responseBodyMultipleErrs, {statusCode: 200});
+ expect(joinFlowInstance.state.registrationError).toEqual({
+ errorAllowsTryAgain: true
+ });
+ });
+
+ test('handleRegistrationResponse when passed status 400', () => {
+ const props = {
+ refreshSession: jest.fn()
+ };
+ const joinFlowInstance = getJoinFlowWrapper(props).instance();
+ joinFlowInstance.handleRegistrationResponse({}, responseBodyMultipleErrs, {statusCode: 400});
+ expect(joinFlowInstance.props.refreshSession).not.toHaveBeenCalled();
+ expect(joinFlowInstance.state.registrationError).toEqual({
+ errorAllowsTryAgain: true
+ });
+ });
+
+ test('handleRegistrationResponse when passed status 500', () => {
+ const joinFlowInstance = getJoinFlowWrapper().instance();
+ joinFlowInstance.handleRegistrationResponse(null, responseBodyMultipleErrs, {statusCode: 500});
+ expect(joinFlowInstance.state.registrationError).toEqual({
+ errorAllowsTryAgain: true
+ });
+ });
});
diff --git a/test/unit/components/registration-error-step.test.jsx b/test/unit/components/registration-error-step.test.jsx
index afad5e63a..97876db95 100644
--- a/test/unit/components/registration-error-step.test.jsx
+++ b/test/unit/components/registration-error-step.test.jsx
@@ -9,8 +9,6 @@ describe('RegistrationErrorStep', () => {
const getRegistrationErrorStepWrapper = props => {
const wrapper = shallowWithIntl(
);
@@ -18,31 +16,90 @@ describe('RegistrationErrorStep', () => {
.dive(); // unwrap injectIntl()
};
- test('when canTryAgain is true, show tryAgain message', () => {
- const props = {canTryAgain: true};
+ test('registrationError has JoinFlowStep', () => {
+ const props = {
+ canTryAgain: true,
+ onSubmit: onSubmit
+ };
+ const joinFlowStepWrapper = getRegistrationErrorStepWrapper(props).find(JoinFlowStep);
+ expect(joinFlowStepWrapper).toHaveLength(1);
+ });
+
+ test('when errorMsg provided, registrationError shows it', () => {
+ const props = {
+ canTryAgain: true,
+ errorMsg: 'halp there is a errors!!',
+ onSubmit: onSubmit
+ };
+ const joinFlowStepWrapper = getRegistrationErrorStepWrapper(props).find(JoinFlowStep);
+ const joinFlowStepInstance = joinFlowStepWrapper.dive();
+ const errMsgElement = joinFlowStepInstance.find('.registration-error-msg');
+ expect(errMsgElement).toHaveLength(1);
+ expect(errMsgElement.text()).toEqual('halp there is a errors!!');
+ });
+
+ test('when errorMsg is null, registrationError does not show it', () => {
+ const props = {
+ canTryAgain: true,
+ errorMsg: null,
+ onSubmit: onSubmit
+ };
+ const joinFlowStepWrapper = getRegistrationErrorStepWrapper(props).find(JoinFlowStep);
+ const joinFlowStepInstance = joinFlowStepWrapper.dive();
+ const errMsgElement = joinFlowStepInstance.find('.registration-error-msg');
+ expect(errMsgElement).toHaveLength(0);
+ });
+
+ test('when no errorMsg provided, registrationError does not show it', () => {
+ const props = {
+ canTryAgain: true,
+ onSubmit: onSubmit
+ };
+ const joinFlowStepWrapper = getRegistrationErrorStepWrapper(props).find(JoinFlowStep);
+ const joinFlowStepInstance = joinFlowStepWrapper.dive();
+ const errMsgElement = joinFlowStepInstance.find('.registration-error-msg');
+ expect(errMsgElement).toHaveLength(0);
+ });
+
+ test('when canTryAgain is true, show tryAgain message', () => {
+ const props = {
+ canTryAgain: true,
+ errorMsg: 'halp there is a errors!!',
+ onSubmit: onSubmit
+ };
const joinFlowStepWrapper = getRegistrationErrorStepWrapper(props).find(JoinFlowStep);
expect(joinFlowStepWrapper).toHaveLength(1);
- expect(joinFlowStepWrapper.props().description).toBe('error message');
expect(joinFlowStepWrapper.props().nextButton).toBe('general.tryAgain');
});
test('when canTryAgain is false, show startOver message', () => {
- const props = {canTryAgain: false};
+ const props = {
+ canTryAgain: false,
+ errorMsg: 'halp there is a errors!!',
+ onSubmit: onSubmit
+ };
const joinFlowStepWrapper = getRegistrationErrorStepWrapper(props).find(JoinFlowStep);
expect(joinFlowStepWrapper).toHaveLength(1);
- expect(joinFlowStepWrapper.props().description).toBe('error message');
expect(joinFlowStepWrapper.props().nextButton).toBe('general.startOver');
});
test('when canTryAgain is missing, show startOver message', () => {
- const joinFlowStepWrapper = getRegistrationErrorStepWrapper().find(JoinFlowStep);
+ const props = {
+ errorMsg: 'halp there is a errors!!',
+ onSubmit: onSubmit
+ };
+ const joinFlowStepWrapper = getRegistrationErrorStepWrapper(props).find(JoinFlowStep);
expect(joinFlowStepWrapper).toHaveLength(1);
- expect(joinFlowStepWrapper.props().description).toBe('error message');
expect(joinFlowStepWrapper.props().nextButton).toBe('general.startOver');
});
test('when submitted, onSubmit is called', () => {
- const joinFlowStepWrapper = getRegistrationErrorStepWrapper().find(JoinFlowStep);
+ const props = {
+ canTryAgain: true,
+ errorMsg: 'halp there is a errors!!',
+ onSubmit: onSubmit
+ };
+ const joinFlowStepWrapper = getRegistrationErrorStepWrapper(props).find(JoinFlowStep);
joinFlowStepWrapper.props().onSubmit(new Event('event')); // eslint-disable-line no-undef
expect(onSubmit).toHaveBeenCalled();
});
diff --git a/test/unit/lib/validate.test.js b/test/unit/lib/validate.test.js
index 6074736d5..d34eef808 100644
--- a/test/unit/lib/validate.test.js
+++ b/test/unit/lib/validate.test.js
@@ -139,4 +139,16 @@ describe('unit test lib/validate.js', () => {
response = validate.validateEmailLocally('much."more unusual"@example.com');
expect(response).toEqual({valid: false, errMsgId: 'registration.validationEmailInvalid'});
});
+
+ test('get responseErrorMsg in cases where there is a dedicated string for that case', () => {
+ let response = validate.responseErrorMsg('username', 'bad username');
+ expect(response).toEqual('registration.errorBadUsername');
+ response = validate.responseErrorMsg('password', 'Ensure this value has at least 6 characters (it has 3).');
+ expect(response).toEqual('registration.errorPasswordTooShort');
+ });
+
+ test('responseErrorMsg is null in case where there is no dedicated string for that case', () => {
+ let response = validate.responseErrorMsg('username', 'some error that is not covered');
+ expect(response).toEqual(null);
+ });
});