mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-23 07:38:07 -05:00
Merge pull request #3339 from LLK/release/2019-09-11
[Master] Release for 2019-09-11
This commit is contained in:
commit
665b9566ee
30 changed files with 1474 additions and 721 deletions
|
@ -33,6 +33,11 @@ env:
|
||||||
- CLOUDDATA_HOST_VAR=CLOUDDATA_HOST_$TRAVIS_BRANCH
|
- CLOUDDATA_HOST_VAR=CLOUDDATA_HOST_$TRAVIS_BRANCH
|
||||||
- CLOUDDATA_HOST=${!CLOUDDATA_HOST_VAR}
|
- CLOUDDATA_HOST=${!CLOUDDATA_HOST_VAR}
|
||||||
- CLOUDDATA_HOST=${CLOUDDATA_HOST:-$CLOUDDATA_HOST_STAGING}
|
- CLOUDDATA_HOST=${CLOUDDATA_HOST:-$CLOUDDATA_HOST_STAGING}
|
||||||
|
- RECAPTCHA_SITE_KEY_master=6LeRbUwUAAAAAFYhKgk3G9OKWqE_OJ7Z-7VTUCbl
|
||||||
|
- RECAPTCHA_SITE_KEY_STAGING=6LfukK4UAAAAAFR44yoZMhv8fj6xh-PMiIxwryG3
|
||||||
|
- RECAPTCHA_SITE=RECAPTCHA_SITE_KEY_$TRAVIS_BRANCH
|
||||||
|
- RECAPTCHA_SITE_KEY=${!RECAPTCHA_SITE_KEY_VAR}
|
||||||
|
- RECAPTCHA_SITE_KEY=${RECAPTCHA_SITE_KEY:-$RECAPTCHA_SITE_KEY_STAGING}
|
||||||
- ROOT_URL_master=https://scratch.mit.edu
|
- ROOT_URL_master=https://scratch.mit.edu
|
||||||
- ROOT_URL_STAGING=https://scratch.ly
|
- ROOT_URL_STAGING=https://scratch.ly
|
||||||
- ROOT_URL_VAR=ROOT_URL_$TRAVIS_BRANCH
|
- ROOT_URL_VAR=ROOT_URL_$TRAVIS_BRANCH
|
||||||
|
|
1138
package-lock.json
generated
1138
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -8,7 +8,8 @@
|
||||||
"test:lint": "eslint . --ext .js,.jsx,.json",
|
"test:lint": "eslint . --ext .js,.jsx,.json",
|
||||||
"test:integration": "npm run test:integration:jest && npm run test:smoke",
|
"test:integration": "npm run test:integration:jest && npm run test:smoke",
|
||||||
"test:integration:jest": "jest ./test/integration/*.test.js",
|
"test:integration:jest": "jest ./test/integration/*.test.js",
|
||||||
"test:integration:remote": "npm run test:integration:jest && npm run test:smoke:sauce",
|
"test:integration:remote": "npm run test:integration:jest:remote && npm run test:smoke:sauce",
|
||||||
|
"test:integration:jest:remote": "SMOKE_REMOTE=true jest ./test/integration/*.test.js",
|
||||||
"test:smoke": "tap ./test/integration-legacy/smoke-testing/*.js --timeout=3600 --no-coverage -R classic",
|
"test:smoke": "tap ./test/integration-legacy/smoke-testing/*.js --timeout=3600 --no-coverage -R classic",
|
||||||
"test:smoke:verbose": "tap ./test/integration-legacy/smoke-testing/*.js --timeout=3600 --no-coverage -R spec",
|
"test:smoke:verbose": "tap ./test/integration-legacy/smoke-testing/*.js --timeout=3600 --no-coverage -R spec",
|
||||||
"test:smoke:sauce": "SMOKE_REMOTE=true tap ./test/integration-legacy/smoke-testing/*.js --timeout=60000 --no-coverage -R classic",
|
"test:smoke:sauce": "SMOKE_REMOTE=true tap ./test/integration-legacy/smoke-testing/*.js --timeout=60000 --no-coverage -R classic",
|
||||||
|
@ -66,7 +67,7 @@
|
||||||
"babel-preset-react": "6.22.0",
|
"babel-preset-react": "6.22.0",
|
||||||
"bowser": "1.9.4",
|
"bowser": "1.9.4",
|
||||||
"cheerio": "1.0.0-rc.2",
|
"cheerio": "1.0.0-rc.2",
|
||||||
"chromedriver": "75.1.0",
|
"chromedriver": "76.0.0",
|
||||||
"classnames": "2.2.5",
|
"classnames": "2.2.5",
|
||||||
"cookie": "0.2.2",
|
"cookie": "0.2.2",
|
||||||
"copy-webpack-plugin": "0.2.0",
|
"copy-webpack-plugin": "0.2.0",
|
||||||
|
@ -122,9 +123,10 @@
|
||||||
"react-string-replace": "0.4.1",
|
"react-string-replace": "0.4.1",
|
||||||
"react-telephone-input": "4.3.4",
|
"react-telephone-input": "4.3.4",
|
||||||
"redux": "3.5.2",
|
"redux": "3.5.2",
|
||||||
|
"redux-mock-store": "^1.2.3",
|
||||||
"redux-thunk": "2.0.1",
|
"redux-thunk": "2.0.1",
|
||||||
"sass-loader": "6.0.6",
|
"sass-loader": "6.0.6",
|
||||||
"scratch-gui": "0.1.0-prerelease.20190828224521",
|
"scratch-gui": "0.1.0-prerelease.20190912180550",
|
||||||
"scratch-l10n": "latest",
|
"scratch-l10n": "latest",
|
||||||
"selenium-webdriver": "3.6.0",
|
"selenium-webdriver": "3.6.0",
|
||||||
"slick-carousel": "1.6.0",
|
"slick-carousel": "1.6.0",
|
||||||
|
|
|
@ -8,6 +8,7 @@ $ui-orange: hsla(38, 100, 55, 1); // #FFAB19 Control Primary
|
||||||
$ui-orange-high-contrast: hsla(30, 100, 55, 1); // #FFAB19 Control Primary
|
$ui-orange-high-contrast: hsla(30, 100, 55, 1); // #FFAB19 Control Primary
|
||||||
$ui-orange-10percent: hsla(35, 90, 55, .1);
|
$ui-orange-10percent: hsla(35, 90, 55, .1);
|
||||||
$ui-orange-25percent: hsla(35, 90, 55, .25);
|
$ui-orange-25percent: hsla(35, 90, 55, .25);
|
||||||
|
$ui-orange-90percent: hsla(38, 100, 55, .9);
|
||||||
|
|
||||||
$ui-dark-orange: hsla(30, 100, 55, 1); // ##FF8C1A Variables Primary
|
$ui-dark-orange: hsla(30, 100, 55, 1); // ##FF8C1A Variables Primary
|
||||||
|
|
||||||
|
|
|
@ -12,9 +12,10 @@ const FormikCheckboxSubComponent = ({
|
||||||
id,
|
id,
|
||||||
label,
|
label,
|
||||||
labelClassName,
|
labelClassName,
|
||||||
|
outerClassName,
|
||||||
...props
|
...props
|
||||||
}) => (
|
}) => (
|
||||||
<div className="checkbox">
|
<div className={classNames('checkbox', outerClassName)}>
|
||||||
<input
|
<input
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
className="formik-checkbox"
|
className="formik-checkbox"
|
||||||
|
@ -50,7 +51,8 @@ FormikCheckboxSubComponent.propTypes = {
|
||||||
}),
|
}),
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
labelClassName: PropTypes.string
|
labelClassName: PropTypes.string,
|
||||||
|
outerClassName: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -59,6 +61,7 @@ const FormikCheckbox = ({
|
||||||
label,
|
label,
|
||||||
labelClassName,
|
labelClassName,
|
||||||
name,
|
name,
|
||||||
|
outerClassName,
|
||||||
...props
|
...props
|
||||||
}) => (
|
}) => (
|
||||||
<Field
|
<Field
|
||||||
|
@ -67,6 +70,7 @@ const FormikCheckbox = ({
|
||||||
label={label}
|
label={label}
|
||||||
labelClassName={labelClassName}
|
labelClassName={labelClassName}
|
||||||
name={name}
|
name={name}
|
||||||
|
outerClassName={outerClassName}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -75,7 +79,8 @@ FormikCheckbox.propTypes = {
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
labelClassName: PropTypes.string,
|
labelClassName: PropTypes.string,
|
||||||
name: PropTypes.string
|
name: PropTypes.string,
|
||||||
|
outerClassName: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = FormikCheckbox;
|
module.exports = FormikCheckbox;
|
||||||
|
|
|
@ -8,6 +8,7 @@ const {injectIntl, intlShape} = require('react-intl');
|
||||||
const countryData = require('../../lib/country-data');
|
const countryData = require('../../lib/country-data');
|
||||||
const FormikSelect = require('../../components/formik-forms/formik-select.jsx');
|
const FormikSelect = require('../../components/formik-forms/formik-select.jsx');
|
||||||
const JoinFlowStep = require('./join-flow-step.jsx');
|
const JoinFlowStep = require('./join-flow-step.jsx');
|
||||||
|
const FormikCheckbox = require('../../components/formik-forms/formik-checkbox.jsx');
|
||||||
|
|
||||||
require('./join-flow-steps.scss');
|
require('./join-flow-steps.scss');
|
||||||
|
|
||||||
|
@ -94,6 +95,13 @@ class CountryStep extends React.Component {
|
||||||
validate={this.validateSelect}
|
validate={this.validateSelect}
|
||||||
validationClassName="validation-full-width-input"
|
validationClassName="validation-full-width-input"
|
||||||
/>
|
/>
|
||||||
|
{/* note that this is a hidden checkbox the user will never see */}
|
||||||
|
<FormikCheckbox
|
||||||
|
id="yesno"
|
||||||
|
label={this.props.intl.formatMessage({id: 'registration.receiveEmails'})}
|
||||||
|
name="yesno"
|
||||||
|
outerClassName="yesNoCheckbox"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</JoinFlowStep>
|
</JoinFlowStep>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,9 +4,9 @@ const React = require('react');
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
import {Formik} from 'formik';
|
import {Formik} from 'formik';
|
||||||
const {injectIntl, intlShape} = require('react-intl');
|
const {injectIntl, intlShape} = require('react-intl');
|
||||||
const emailValidator = require('email-validator');
|
|
||||||
const FormattedMessage = require('react-intl').FormattedMessage;
|
const FormattedMessage = require('react-intl').FormattedMessage;
|
||||||
|
|
||||||
|
const validate = require('../../lib/validate');
|
||||||
const JoinFlowStep = require('./join-flow-step.jsx');
|
const JoinFlowStep = require('./join-flow-step.jsx');
|
||||||
const FormikInput = require('../../components/formik-forms/formik-input.jsx');
|
const FormikInput = require('../../components/formik-forms/formik-input.jsx');
|
||||||
const FormikCheckbox = require('../../components/formik-forms/formik-checkbox.jsx');
|
const FormikCheckbox = require('../../components/formik-forms/formik-checkbox.jsx');
|
||||||
|
@ -20,30 +20,91 @@ class EmailStep extends React.Component {
|
||||||
'handleSetEmailRef',
|
'handleSetEmailRef',
|
||||||
'handleValidSubmit',
|
'handleValidSubmit',
|
||||||
'validateEmail',
|
'validateEmail',
|
||||||
'validateForm'
|
'validateForm',
|
||||||
|
'setCaptchaRef',
|
||||||
|
'captchaSolved',
|
||||||
|
'onCaptchaLoad',
|
||||||
|
'onCaptchaError'
|
||||||
]);
|
]);
|
||||||
|
this.state = {
|
||||||
|
captchaIsLoading: true
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
// automatically start with focus on username field
|
// automatically start with focus on username field
|
||||||
if (this.emailInput) this.emailInput.focus();
|
if (this.emailInput) this.emailInput.focus();
|
||||||
|
|
||||||
|
// If grecaptcha doesn't exist on window, we havent loaded the captcha js yet. Load it.
|
||||||
|
if (!window.grecaptcha) {
|
||||||
|
// ReCaptcha calls a callback when the grecatpcha object is usable. That callback
|
||||||
|
// needs to be global so set it on the window.
|
||||||
|
window.grecaptchaOnLoad = this.onCaptchaLoad;
|
||||||
|
// Load Google ReCaptcha script.
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.async = true;
|
||||||
|
script.onerror = this.onCaptchaError;
|
||||||
|
script.src = `https://www.recaptcha.net/recaptcha/api.js?onload=grecaptchaOnLoad&render=explicit&hl=${window._locale}`;
|
||||||
|
document.body.appendChild(script);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
componentWillUnmount () {
|
||||||
|
window.grecaptchaOnLoad = null;
|
||||||
}
|
}
|
||||||
handleSetEmailRef (emailInputRef) {
|
handleSetEmailRef (emailInputRef) {
|
||||||
this.emailInput = emailInputRef;
|
this.emailInput = emailInputRef;
|
||||||
}
|
}
|
||||||
|
onCaptchaError () {
|
||||||
|
// TODO send user to error step once we have one.
|
||||||
|
}
|
||||||
|
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.
|
||||||
|
// TODO: Put up the error screen when we have one.
|
||||||
|
}
|
||||||
|
// TODO: Add in error callback for render once we have an error screen.
|
||||||
|
this.widgetId = this.grecaptcha.render(this.captchaRef,
|
||||||
|
{
|
||||||
|
callback: this.captchaSolved,
|
||||||
|
sitekey: process.env.RECAPTCHA_SITE_KEY
|
||||||
|
},
|
||||||
|
true);
|
||||||
|
}
|
||||||
validateEmail (email) {
|
validateEmail (email) {
|
||||||
if (!email) return this.props.intl.formatMessage({id: 'general.required'});
|
if (!email) return this.props.intl.formatMessage({id: 'general.required'});
|
||||||
const isValidLocally = emailValidator.validate(email);
|
const localResult = validate.validateEmailLocally(email);
|
||||||
if (isValidLocally) {
|
if (!localResult.valid) return this.props.intl.formatMessage({id: localResult.errMsgId});
|
||||||
return null; // TODO: validate email address remotely
|
return validate.validateEmailRemotely(email).then(
|
||||||
}
|
remoteResult => {
|
||||||
return this.props.intl.formatMessage({id: 'registration.validationEmailInvalid'});
|
if (remoteResult.valid === true) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.props.intl.formatMessage({id: remoteResult.errMsgId});
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
validateForm () {
|
validateForm () {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
handleValidSubmit (formData, formikBag) {
|
handleValidSubmit (formData, formikBag) {
|
||||||
formikBag.setSubmitting(false);
|
this.formData = formData;
|
||||||
this.props.onNextStep(formData);
|
this.formikBag = formikBag;
|
||||||
|
// Change set submitting to false so that if the user clicks out of
|
||||||
|
// the captcha, the button is clickable again (instead of a disabled button with a spinner).
|
||||||
|
this.formikBag.setSubmitting(false);
|
||||||
|
this.grecaptcha.execute(this.widgetId);
|
||||||
|
}
|
||||||
|
captchaSolved (token) {
|
||||||
|
// Now thatcaptcha is done, we can tell Formik we're submitting.
|
||||||
|
this.formikBag.setSubmitting(true);
|
||||||
|
this.formData['g-recaptcha-response'] = token;
|
||||||
|
this.props.onNextStep(this.formData);
|
||||||
|
}
|
||||||
|
setCaptchaRef (ref) {
|
||||||
|
this.captchaRef = ref;
|
||||||
}
|
}
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
|
@ -63,6 +124,8 @@ class EmailStep extends React.Component {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
setFieldError,
|
setFieldError,
|
||||||
|
setFieldTouched,
|
||||||
|
setFieldValue,
|
||||||
validateField
|
validateField
|
||||||
} = props;
|
} = props;
|
||||||
return (
|
return (
|
||||||
|
@ -88,7 +151,7 @@ class EmailStep extends React.Component {
|
||||||
innerClassName="join-flow-inner-email-step"
|
innerClassName="join-flow-inner-email-step"
|
||||||
nextButton={this.props.intl.formatMessage({id: 'registration.createAccount'})}
|
nextButton={this.props.intl.formatMessage({id: 'registration.createAccount'})}
|
||||||
title={this.props.intl.formatMessage({id: 'registration.emailStepTitle'})}
|
title={this.props.intl.formatMessage({id: 'registration.emailStepTitle'})}
|
||||||
waiting={isSubmitting}
|
waiting={this.props.waiting || isSubmitting || this.state.captchaIsLoading}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
<FormikInput
|
<FormikInput
|
||||||
|
@ -105,7 +168,11 @@ class EmailStep extends React.Component {
|
||||||
validationClassName="validation-full-width-input"
|
validationClassName="validation-full-width-input"
|
||||||
/* eslint-disable react/jsx-no-bind */
|
/* eslint-disable react/jsx-no-bind */
|
||||||
onBlur={() => validateField('email')}
|
onBlur={() => validateField('email')}
|
||||||
onFocus={() => setFieldError('email', null)}
|
onChange={e => {
|
||||||
|
setFieldValue('email', e.target.value);
|
||||||
|
setFieldTouched('email');
|
||||||
|
setFieldError('email', null);
|
||||||
|
}}
|
||||||
/* eslint-enable react/jsx-no-bind */
|
/* eslint-enable react/jsx-no-bind */
|
||||||
onSetRef={this.handleSetEmailRef}
|
onSetRef={this.handleSetEmailRef}
|
||||||
/>
|
/>
|
||||||
|
@ -116,6 +183,13 @@ class EmailStep extends React.Component {
|
||||||
name="subscribe"
|
name="subscribe"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className="g-recaptcha"
|
||||||
|
data-badge="bottomright"
|
||||||
|
data-sitekey={process.env.RECAPTCHA_SITE_KEY}
|
||||||
|
data-size="invisible"
|
||||||
|
ref={this.setCaptchaRef}
|
||||||
|
/>
|
||||||
</JoinFlowStep>
|
</JoinFlowStep>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
@ -126,7 +200,8 @@ class EmailStep extends React.Component {
|
||||||
|
|
||||||
EmailStep.propTypes = {
|
EmailStep.propTypes = {
|
||||||
intl: intlShape,
|
intl: intlShape,
|
||||||
onNextStep: PropTypes.func
|
onNextStep: PropTypes.func,
|
||||||
|
waiting: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -132,6 +132,10 @@
|
||||||
margin-left: -.5rem;
|
margin-left: -.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.join-flow-registration-error {
|
||||||
|
padding-top: 5.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.join-flow-gender-description {
|
.join-flow-gender-description {
|
||||||
margin-top: .625rem;
|
margin-top: .625rem;
|
||||||
margin-bottom: 1.25rem;
|
margin-bottom: 1.25rem;
|
||||||
|
@ -180,3 +184,7 @@
|
||||||
a.join-flow-link:link, a.join-flow-link:visited, a.join-flow-link:active {
|
a.join-flow-link:link, a.join-flow-link:visited, a.join-flow-link:active {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.yesNoCheckbox {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
const bindAll = require('lodash.bindall');
|
const bindAll = require('lodash.bindall');
|
||||||
|
const connect = require('react-redux').connect;
|
||||||
const defaults = require('lodash.defaultsdeep');
|
const defaults = require('lodash.defaultsdeep');
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
|
||||||
|
const api = require('../../lib/api');
|
||||||
const injectIntl = require('../../lib/intl.jsx').injectIntl;
|
const injectIntl = require('../../lib/intl.jsx').injectIntl;
|
||||||
const intlShape = require('../../lib/intl.jsx').intlShape;
|
const intlShape = require('../../lib/intl.jsx').intlShape;
|
||||||
|
const sessionActions = require('../../redux/session.js');
|
||||||
|
|
||||||
const Progression = require('../progression/progression.jsx');
|
const Progression = require('../progression/progression.jsx');
|
||||||
const UsernameStep = require('./username-step.jsx');
|
const UsernameStep = require('./username-step.jsx');
|
||||||
|
@ -13,44 +16,141 @@ const GenderStep = require('./gender-step.jsx');
|
||||||
const CountryStep = require('./country-step.jsx');
|
const CountryStep = require('./country-step.jsx');
|
||||||
const EmailStep = require('./email-step.jsx');
|
const EmailStep = require('./email-step.jsx');
|
||||||
const WelcomeStep = require('./welcome-step.jsx');
|
const WelcomeStep = require('./welcome-step.jsx');
|
||||||
|
const RegistrationErrorStep = require('./registration-error-step.jsx');
|
||||||
|
|
||||||
/*
|
|
||||||
eslint-disable react/prefer-stateless-function, react/no-unused-prop-types, no-useless-constructor
|
|
||||||
*/
|
|
||||||
class JoinFlow extends React.Component {
|
class JoinFlow extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
bindAll(this, [
|
bindAll(this, [
|
||||||
'handleAdvanceStep'
|
'handleAdvanceStep',
|
||||||
|
'handlePrepareToRegister',
|
||||||
|
'handleRegistrationResponse',
|
||||||
|
'handleSubmitRegistration'
|
||||||
]);
|
]);
|
||||||
this.state = {
|
this.state = {
|
||||||
formData: {},
|
formData: {},
|
||||||
registrationError: null,
|
registrationError: null,
|
||||||
step: 0
|
step: 0,
|
||||||
|
waiting: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
handleAdvanceStep (formData) {
|
handlePrepareToRegister (newFormData) {
|
||||||
formData = formData || {};
|
newFormData = newFormData || {};
|
||||||
|
const newState = {
|
||||||
|
formData: defaults({}, newFormData, this.state.formData)
|
||||||
|
};
|
||||||
|
this.setState(newState, () => {
|
||||||
|
this.handleSubmitRegistration(this.state.formData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
this.setState({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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
registrationError: errStr ||
|
||||||
|
`${this.props.intl.formatMessage({
|
||||||
|
id: 'registration.generalError'
|
||||||
|
})} (${res.statusCode})`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
handleSubmitRegistration (formData) {
|
||||||
|
this.setState({waiting: true}, () => {
|
||||||
|
api({
|
||||||
|
host: '',
|
||||||
|
uri: '/accounts/register_new_user/',
|
||||||
|
method: 'post',
|
||||||
|
useCsrf: true,
|
||||||
|
formData: {
|
||||||
|
'username': formData.username,
|
||||||
|
'email': formData.email,
|
||||||
|
'password': formData.password,
|
||||||
|
'birth_month': formData.birth_month,
|
||||||
|
'birth_year': formData.birth_year,
|
||||||
|
'g-recaptcha-response': formData['g-recaptcha-response'],
|
||||||
|
'gender': formData.gender,
|
||||||
|
'country': formData.country,
|
||||||
|
'subscribe': true,
|
||||||
|
'is_robot': formData.yesno
|
||||||
|
// no need to include csrfmiddlewaretoken; will be provided in
|
||||||
|
// X-CSRFToken header, which scratchr2 looks for in
|
||||||
|
// scratchr2/middleware/csrf.py line 237.
|
||||||
|
}
|
||||||
|
}, (err, body, res) => {
|
||||||
|
this.handleRegistrationResponse(err, body, res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
handleAdvanceStep (newFormData) {
|
||||||
|
newFormData = newFormData || {};
|
||||||
this.setState({
|
this.setState({
|
||||||
step: this.state.step + 1,
|
formData: defaults({}, newFormData, this.state.formData),
|
||||||
formData: defaults({}, formData, this.state.formData)
|
step: this.state.step + 1
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Progression step={this.state.step}>
|
{this.state.registrationError ? (
|
||||||
<UsernameStep onNextStep={this.handleAdvanceStep} />
|
<RegistrationErrorStep
|
||||||
<BirthDateStep onNextStep={this.handleAdvanceStep} />
|
errorMsg={this.state.registrationError}
|
||||||
<GenderStep onNextStep={this.handleAdvanceStep} />
|
/* eslint-disable react/jsx-no-bind */
|
||||||
<CountryStep onNextStep={this.handleAdvanceStep} />
|
onTryAgain={() => this.handleSubmitRegistration(this.state.formData)}
|
||||||
<EmailStep onNextStep={this.handleAdvanceStep} />
|
/* eslint-enable react/jsx-no-bind */
|
||||||
<WelcomeStep
|
|
||||||
email={this.state.formData.email}
|
|
||||||
username={this.state.formData.username}
|
|
||||||
onNextStep={this.handleAdvanceStep}
|
|
||||||
/>
|
/>
|
||||||
</Progression>
|
) : (
|
||||||
|
<Progression step={this.state.step}>
|
||||||
|
<UsernameStep onNextStep={this.handleAdvanceStep} />
|
||||||
|
<BirthDateStep onNextStep={this.handleAdvanceStep} />
|
||||||
|
<GenderStep onNextStep={this.handleAdvanceStep} />
|
||||||
|
<CountryStep onNextStep={this.handleAdvanceStep} />
|
||||||
|
<EmailStep
|
||||||
|
waiting={this.state.waiting}
|
||||||
|
onNextStep={this.handlePrepareToRegister}
|
||||||
|
/>
|
||||||
|
<WelcomeStep
|
||||||
|
email={this.state.formData.email}
|
||||||
|
username={this.state.formData.username}
|
||||||
|
onNextStep={this.props.onCompleteRegistration}
|
||||||
|
/>
|
||||||
|
</Progression>
|
||||||
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -58,11 +158,27 @@ class JoinFlow extends React.Component {
|
||||||
|
|
||||||
JoinFlow.propTypes = {
|
JoinFlow.propTypes = {
|
||||||
intl: intlShape,
|
intl: intlShape,
|
||||||
onCompleteRegistration: PropTypes.func
|
onCompleteRegistration: PropTypes.func,
|
||||||
|
refreshSession: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = injectIntl(JoinFlow);
|
const IntlJoinFlow = injectIntl(JoinFlow);
|
||||||
|
|
||||||
/*
|
const mapDispatchToProps = dispatch => ({
|
||||||
eslint-enable
|
refreshSession: () => {
|
||||||
*/
|
dispatch(sessionActions.refreshSession());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Allow incoming props to override redux-provided props. Used to mock in tests.
|
||||||
|
const mergeProps = (stateProps, dispatchProps, ownProps) => Object.assign(
|
||||||
|
{}, stateProps, dispatchProps, ownProps
|
||||||
|
);
|
||||||
|
|
||||||
|
const ConnectedJoinFlow = connect(
|
||||||
|
() => ({}),
|
||||||
|
mapDispatchToProps,
|
||||||
|
mergeProps
|
||||||
|
)(IntlJoinFlow);
|
||||||
|
|
||||||
|
module.exports = ConnectedJoinFlow;
|
||||||
|
|
|
@ -4,10 +4,16 @@
|
||||||
.modal-flush-bottom-button {
|
.modal-flush-bottom-button {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
border: none;
|
||||||
border-bottom-left-radius: 1rem;
|
border-bottom-left-radius: 1rem;
|
||||||
border-bottom-right-radius: 1rem;
|
border-bottom-right-radius: 1rem;
|
||||||
height: 5.1875rem;
|
height: 5.1875rem;
|
||||||
background-color: $ui-orange;
|
background-color: $ui-orange;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transition: background-color .25s ease;
|
||||||
|
background-color: $ui-orange-90percent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.next-step-title {
|
.next-step-title {
|
||||||
|
|
45
src/components/join-flow/registration-error-step.jsx
Normal file
45
src/components/join-flow/registration-error-step.jsx
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
const bindAll = require('lodash.bindall');
|
||||||
|
const React = require('react');
|
||||||
|
const PropTypes = require('prop-types');
|
||||||
|
const {injectIntl, intlShape} = require('react-intl');
|
||||||
|
|
||||||
|
const JoinFlowStep = require('./join-flow-step.jsx');
|
||||||
|
|
||||||
|
require('./join-flow-steps.scss');
|
||||||
|
|
||||||
|
class RegistrationErrorStep extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
bindAll(this, [
|
||||||
|
'handleSubmit'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
handleSubmit (e) {
|
||||||
|
// JoinFlowStep includes a <form> that handles a submit action.
|
||||||
|
// But here, we're not really submitting, so we need to prevent
|
||||||
|
// the form from navigating away from the current page.
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.onTryAgain();
|
||||||
|
}
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<JoinFlowStep
|
||||||
|
description={this.props.errorMsg}
|
||||||
|
innerClassName="join-flow-registration-error"
|
||||||
|
nextButton={this.props.intl.formatMessage({id: 'general.tryAgain'})}
|
||||||
|
title={this.props.intl.formatMessage({id: 'registration.generalError'})}
|
||||||
|
onSubmit={this.handleSubmit}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RegistrationErrorStep.propTypes = {
|
||||||
|
errorMsg: PropTypes.string,
|
||||||
|
intl: intlShape,
|
||||||
|
onTryAgain: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
const IntlRegistrationErrorStep = injectIntl(RegistrationErrorStep);
|
||||||
|
|
||||||
|
module.exports = IntlRegistrationErrorStep;
|
|
@ -217,6 +217,7 @@ class Navigation extends React.Component {
|
||||||
{this.props.registrationOpen && (
|
{this.props.registrationOpen && (
|
||||||
this.props.useScratch3Registration ? (
|
this.props.useScratch3Registration ? (
|
||||||
<Scratch3Registration
|
<Scratch3Registration
|
||||||
|
createProjectOnComplete
|
||||||
isOpen
|
isOpen
|
||||||
key="scratch3registration"
|
key="scratch3registration"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -23,17 +23,19 @@ const Registration = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
Registration.propTypes = {
|
Registration.propTypes = {
|
||||||
|
// used in mapDispatchToProps; eslint doesn't understand that this prop is used
|
||||||
|
createProjectOnComplete: PropTypes.bool, // eslint-disable-line react/no-unused-prop-types
|
||||||
handleCloseRegistration: PropTypes.func,
|
handleCloseRegistration: PropTypes.func,
|
||||||
handleCompleteRegistration: PropTypes.func,
|
handleCompleteRegistration: PropTypes.func,
|
||||||
isOpen: PropTypes.bool
|
isOpen: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = (dispatch, ownProps) => ({
|
||||||
handleCloseRegistration: () => {
|
handleCloseRegistration: () => {
|
||||||
dispatch(navigationActions.setRegistrationOpen(false));
|
dispatch(navigationActions.setRegistrationOpen(false));
|
||||||
},
|
},
|
||||||
handleCompleteRegistration: () => {
|
handleCompleteRegistration: () => {
|
||||||
dispatch(navigationActions.handleCompleteRegistration());
|
dispatch(navigationActions.handleCompleteRegistration(ownProps.createProjectOnComplete));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -89,6 +89,7 @@
|
||||||
"general.ideas": "Ideas",
|
"general.ideas": "Ideas",
|
||||||
"general.tipsWindow": "Tips Window",
|
"general.tipsWindow": "Tips Window",
|
||||||
"general.termsOfUse": "Terms of Use",
|
"general.termsOfUse": "Terms of Use",
|
||||||
|
"general.tryAgain": "Try again",
|
||||||
"general.unhandledError": "We are so sorry, but it looks like Scratch has crashed. This bug has been automatically reported to the Scratch Team.",
|
"general.unhandledError": "We are so sorry, but it looks like Scratch has crashed. This bug has been automatically reported to the Scratch Team.",
|
||||||
"general.username": "Username",
|
"general.username": "Username",
|
||||||
"general.validationEmail": "Please enter a valid email address",
|
"general.validationEmail": "Please enter a valid email address",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
module.exports = {};
|
module.exports = {};
|
||||||
const api = require('./api');
|
const api = require('./api');
|
||||||
|
const emailValidator = require('email-validator');
|
||||||
|
|
||||||
module.exports.validateUsernameLocally = username => {
|
module.exports.validateUsernameLocally = username => {
|
||||||
if (!username || username === '') {
|
if (!username || username === '') {
|
||||||
|
@ -67,3 +68,36 @@ module.exports.validatePasswordConfirm = (password, passwordConfirm) => {
|
||||||
}
|
}
|
||||||
return {valid: true};
|
return {valid: true};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports.validateEmailLocally = email => {
|
||||||
|
if (!email || email === '') {
|
||||||
|
return {valid: false, errMsgId: 'general.required'};
|
||||||
|
} else if (emailValidator.validate(email)) {
|
||||||
|
return {valid: true};
|
||||||
|
}
|
||||||
|
return ({valid: false, errMsgId: 'registration.validationEmailInvalid'});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.validateEmailRemotely = email => (
|
||||||
|
new Promise(resolve => {
|
||||||
|
api({
|
||||||
|
host: '', // not handled by API; use existing infrastructure
|
||||||
|
params: {email: email},
|
||||||
|
uri: '/accounts/check_email/'
|
||||||
|
}, (err, body, res) => {
|
||||||
|
if (err || res.statusCode !== 200 || !body || body.length < 1 || !body[0].msg) {
|
||||||
|
resolve({valid: false, errMsgId: 'general.apiError'});
|
||||||
|
}
|
||||||
|
switch (body[0].msg) {
|
||||||
|
case 'valid email':
|
||||||
|
resolve({valid: true});
|
||||||
|
break;
|
||||||
|
case 'Scratch is not allowed to send email to this address.': // e.g., bad TLD or block-listed
|
||||||
|
case 'Enter a valid email address.':
|
||||||
|
default:
|
||||||
|
resolve({valid: false, errMsgId: 'registration.validationEmailInvalid'});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
|
@ -92,9 +92,13 @@ module.exports.setSearchTerm = searchTerm => ({
|
||||||
searchTerm: searchTerm
|
searchTerm: searchTerm
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports.handleCompleteRegistration = () => (dispatch => {
|
module.exports.handleCompleteRegistration = createProject => (dispatch => {
|
||||||
dispatch(sessionActions.refreshSession());
|
if (createProject) {
|
||||||
dispatch(module.exports.setRegistrationOpen(false));
|
window.location = '/projects/editor/?tutorial=getStarted';
|
||||||
|
} else {
|
||||||
|
dispatch(sessionActions.refreshSession());
|
||||||
|
dispatch(module.exports.setRegistrationOpen(false));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports.handleLogIn = (formData, callback) => (dispatch => {
|
module.exports.handleLogIn = (formData, callback) => (dispatch => {
|
||||||
|
|
|
@ -161,6 +161,13 @@
|
||||||
"view": "jobs/moderator/moderator",
|
"view": "jobs/moderator/moderator",
|
||||||
"title": "Community Moderator"
|
"title": "Community Moderator"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "join",
|
||||||
|
"pattern": "^/join/?$",
|
||||||
|
"routeAlias": "/join/?$",
|
||||||
|
"view": "join/join",
|
||||||
|
"title": "Join Scratch"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "messages",
|
"name": "messages",
|
||||||
"pattern": "^/messages/?$",
|
"pattern": "^/messages/?$",
|
||||||
|
|
|
@ -114,8 +114,8 @@ class Download extends React.Component {
|
||||||
className="download-button"
|
className="download-button"
|
||||||
href={
|
href={
|
||||||
this.state.OS === OS_ENUM.WINDOWS ?
|
this.state.OS === OS_ENUM.WINDOWS ?
|
||||||
'https://downloads.scratch.mit.edu/desktop/Scratch%20Desktop%20Setup%203.5.0.exe' :
|
'https://downloads.scratch.mit.edu/desktop/Scratch%20Desktop%20Setup%203.6.0.exe' :
|
||||||
'https://downloads.scratch.mit.edu/desktop/Scratch%20Desktop-3.5.0.dmg'
|
'https://downloads.scratch.mit.edu/desktop/Scratch%20Desktop-3.6.0.dmg'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FormattedMessage id="download.downloadButton" />
|
<FormattedMessage id="download.downloadButton" />
|
||||||
|
|
17
src/views/join/join.jsx
Normal file
17
src/views/join/join.jsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
const React = require('react');
|
||||||
|
const render = require('../../lib/render.jsx');
|
||||||
|
const JoinModal = require('../../components/modal/join/modal.jsx');
|
||||||
|
const ErrorBoundary = require('../../components/errorboundary/errorboundary.jsx');
|
||||||
|
// Require this even though we don't use it because, without it, webpack runs out of memory...
|
||||||
|
const Page = require('../../components/page/www/page.jsx'); // eslint-disable-line no-unused-vars
|
||||||
|
|
||||||
|
const openModal = true;
|
||||||
|
const Register = () => (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<JoinModal
|
||||||
|
isOpen={openModal}
|
||||||
|
key="scratch3registration"
|
||||||
|
/>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
render(<Register />, document.getElementById('app'));
|
|
@ -68,7 +68,7 @@ class SeleniumHelper {
|
||||||
let driverConfig = {
|
let driverConfig = {
|
||||||
browserName: 'chrome',
|
browserName: 'chrome',
|
||||||
platform: 'macOS 10.14',
|
platform: 'macOS 10.14',
|
||||||
version: '75.0'
|
version: '76.0'
|
||||||
};
|
};
|
||||||
var driver = new webdriver.Builder()
|
var driver = new webdriver.Builder()
|
||||||
.withCapabilities({
|
.withCapabilities({
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
/*
|
|
||||||
* Tests from:
|
|
||||||
*
|
|
||||||
* https://github.com/LLK/scratchr2/wiki/Smoke-Testing-Test-Cases
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
const SeleniumHelper = require('../selenium-helpers.js');
|
|
||||||
const helper = new SeleniumHelper();
|
|
||||||
|
|
||||||
var tap = require('tap');
|
|
||||||
const test = tap.test;
|
|
||||||
|
|
||||||
const driver = helper.buildDriver('www-smoke test_sign_in_out_discuss');
|
|
||||||
|
|
||||||
const {
|
|
||||||
clickText,
|
|
||||||
findByXpath,
|
|
||||||
findText,
|
|
||||||
clickXpath,
|
|
||||||
clickButton
|
|
||||||
} = helper;
|
|
||||||
|
|
||||||
var username = process.env.SMOKE_USERNAME;
|
|
||||||
var password = process.env.SMOKE_PASSWORD;
|
|
||||||
|
|
||||||
var rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
|
|
||||||
var url = rootUrl + '/discuss';
|
|
||||||
|
|
||||||
tap.plan(2);
|
|
||||||
|
|
||||||
tap.tearDown(function () {
|
|
||||||
driver.quit();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.beforeEach(function () {
|
|
||||||
return driver.get(url);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Sign in to Scratch using scratchr2 navbar', t => {
|
|
||||||
clickText('Sign in')
|
|
||||||
.then(() => findByXpath('//input[@id="login_dropdown_username"]'))
|
|
||||||
.then((element) => element.sendKeys(username))
|
|
||||||
.then(() => findByXpath('//input[@name="password"]'))
|
|
||||||
.then((element) => element.sendKeys(password))
|
|
||||||
.then(() => clickButton('Sign in'))
|
|
||||||
.then(() => findByXpath('//li[contains(@class, "logged-in-user")' +
|
|
||||||
'and contains(@class, "dropdown")]/span'))
|
|
||||||
.then((element) => element.getText('span'))
|
|
||||||
.then((text) => t.match(text.toLowerCase(), username.substring(0, 10).toLowerCase(),
|
|
||||||
'first part of username should be displayed in navbar'))
|
|
||||||
.then(() => t.end());
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Sign out of Scratch using scratchr2 navbar', t => {
|
|
||||||
clickXpath('//span[contains(@class, "user-name")' +
|
|
||||||
' and contains(@class, "dropdown-toggle")]/img[contains(@class, "user-icon")]')
|
|
||||||
.then(() => clickXpath('//input[@value="Sign out"]'))
|
|
||||||
.then(() => findText('Sign in'))
|
|
||||||
.then((element) => t.ok(element, 'Sign in reappeared on the page after signing out'))
|
|
||||||
.then(() => t.end());
|
|
||||||
});
|
|
|
@ -1,59 +0,0 @@
|
||||||
/*
|
|
||||||
* Tests from:
|
|
||||||
*
|
|
||||||
* https://github.com/LLK/scratchr2/wiki/Smoke-Testing-Test-Cases
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
const SeleniumHelper = require('../selenium-helpers.js');
|
|
||||||
const helper = new SeleniumHelper();
|
|
||||||
|
|
||||||
var tap = require('tap');
|
|
||||||
const test = tap.test;
|
|
||||||
|
|
||||||
const driver = helper.buildDriver('www-smoke test_sign_in_out_homepage');
|
|
||||||
|
|
||||||
const {
|
|
||||||
clickText,
|
|
||||||
findText,
|
|
||||||
findByXpath,
|
|
||||||
clickXpath
|
|
||||||
} = helper;
|
|
||||||
|
|
||||||
var username = process.env.SMOKE_USERNAME;
|
|
||||||
var password = process.env.SMOKE_PASSWORD;
|
|
||||||
|
|
||||||
var rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
|
|
||||||
|
|
||||||
tap.plan(2);
|
|
||||||
|
|
||||||
tap.tearDown(function () {
|
|
||||||
driver.quit();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.beforeEach(function () {
|
|
||||||
return driver.get(rootUrl);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Sign in to Scratch using scratch-www navbar', {skip: true}, t => {
|
|
||||||
clickText('Sign in')
|
|
||||||
.then(() => findByXpath('//input[@id="frc-username-1088"]'))
|
|
||||||
.then((element) => element.sendKeys(username))
|
|
||||||
.then(() => findByXpath('//input[@id="frc-password-1088"]'))
|
|
||||||
.then((element) => element.sendKeys(password))
|
|
||||||
.then(() => clickXpath('//button[contains(@class, "button") and ' +
|
|
||||||
'contains(@class, "submit-button") and contains(@class, "white")]'))
|
|
||||||
.then(() => findByXpath('//span[contains(@class, "profile-name")]'))
|
|
||||||
.then((element) => element.getText())
|
|
||||||
.then((text) => t.match(text.toLowerCase(), username.substring(0, 10).toLowerCase(),
|
|
||||||
'first part of username should be displayed in navbar'))
|
|
||||||
.then(() => t.end());
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Sign out of Scratch using scratch-www navbar', {skip: true}, t => {
|
|
||||||
clickXpath('//a[contains(@class, "user-info")]')
|
|
||||||
.then(() => clickText('Sign out'))
|
|
||||||
.then(() => findText('Sign in'))
|
|
||||||
.then((element) => t.ok(element, 'Sign in reappeared on the page after signing out'))
|
|
||||||
.then(() => t.end());
|
|
||||||
});
|
|
|
@ -67,8 +67,8 @@ class SeleniumHelper {
|
||||||
// https://wiki.saucelabs.com/display/DOCS/Platform+Configurator
|
// https://wiki.saucelabs.com/display/DOCS/Platform+Configurator
|
||||||
let driverConfig = {
|
let driverConfig = {
|
||||||
browserName: 'chrome',
|
browserName: 'chrome',
|
||||||
platform: 'macOS 10.13',
|
platform: 'macOS 10.14',
|
||||||
version: '70.0'
|
version: '76.0'
|
||||||
};
|
};
|
||||||
var driver = new webdriver.Builder()
|
var driver = new webdriver.Builder()
|
||||||
.withCapabilities({
|
.withCapabilities({
|
||||||
|
|
100
test/integration/sign-in-and-out.test.js
Normal file
100
test/integration/sign-in-and-out.test.js
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
const SeleniumHelper = require('./selenium-helpers.js');
|
||||||
|
|
||||||
|
const {
|
||||||
|
clickText,
|
||||||
|
findByXpath,
|
||||||
|
clickXpath,
|
||||||
|
clickButton,
|
||||||
|
buildDriver
|
||||||
|
} = new SeleniumHelper();
|
||||||
|
|
||||||
|
let username = process.env.SMOKE_USERNAME;
|
||||||
|
let password = process.env.SMOKE_PASSWORD;
|
||||||
|
let remote = process.env.SMOKE_REMOTE || false;
|
||||||
|
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
|
||||||
|
let scratchr2url = rootUrl + '/users/' + username;
|
||||||
|
let wwwURL = rootUrl;
|
||||||
|
|
||||||
|
if (remote){
|
||||||
|
jest.setTimeout(60000);
|
||||||
|
}
|
||||||
|
|
||||||
|
let driver;
|
||||||
|
|
||||||
|
describe('www-integration sign-in-and-out', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
driver = await buildDriver('www-integration sign-in-out');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sign in', () => {
|
||||||
|
afterEach(async () => {
|
||||||
|
await driver.get(wwwURL);
|
||||||
|
await clickXpath('//div[@class="account-nav"]');
|
||||||
|
await clickText('Sign out');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sign in on www', async () => {
|
||||||
|
await driver.get(wwwURL);
|
||||||
|
await driver.sleep(1000);
|
||||||
|
await clickXpath('//li[@class="link right login-item"]/a');
|
||||||
|
let name = await findByXpath('//input[@id="frc-username-1088"]');
|
||||||
|
await name.sendKeys(username);
|
||||||
|
let word = await findByXpath('//input[@id="frc-password-1088"]');
|
||||||
|
await word.sendKeys(password);
|
||||||
|
await clickXpath('//button[contains(@class, "button") and ' +
|
||||||
|
'contains(@class, "submit-button") and contains(@class, "white")]');
|
||||||
|
let element = await findByXpath('//span[contains(@class, "profile-name")]');
|
||||||
|
let text = await element.getText();
|
||||||
|
await expect(text.toLowerCase()).toEqual(username.toLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sign in on scratchr2', async () => {
|
||||||
|
await driver.get(scratchr2url);
|
||||||
|
await clickXpath('//li[@class="sign-in dropdown"]/span');
|
||||||
|
let name = await findByXpath('//input[@id="login_dropdown_username"]');
|
||||||
|
await name.sendKeys(username);
|
||||||
|
let word = await findByXpath('//input[@name="password"]');
|
||||||
|
await word.sendKeys(password);
|
||||||
|
await clickButton('Sign in');
|
||||||
|
let element = await findByXpath('//span[@class="user-name dropdown-toggle"]');
|
||||||
|
let text = await element.getText();
|
||||||
|
await expect(text.toLowerCase()).toEqual(username.toLowerCase());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sign out', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await driver.get(wwwURL);
|
||||||
|
await clickXpath('//li[@class="link right login-item"]');
|
||||||
|
let name = await findByXpath('//input[@id="frc-username-1088"]');
|
||||||
|
await name.sendKeys(username);
|
||||||
|
let word = await findByXpath('//input[@id="frc-password-1088"]');
|
||||||
|
await word.sendKeys(password);
|
||||||
|
await clickXpath('//button[contains(@class, "button") and ' +
|
||||||
|
'contains(@class, "submit-button") and contains(@class, "white")]');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sign out on www', async () => {
|
||||||
|
await clickXpath('//a[contains(@class, "user-info")]');
|
||||||
|
await clickText('Sign out');
|
||||||
|
let element = await findByXpath('//li[@class="link right login-item"]/a/span');
|
||||||
|
let text = await element.getText();
|
||||||
|
await expect(text.toLowerCase()).toEqual('Sign In'.toLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sign out on scratchr2', async () => {
|
||||||
|
await driver.get(scratchr2url);
|
||||||
|
await clickXpath('//span[@class="user-name dropdown-toggle"]');
|
||||||
|
await clickXpath('//li[@id="logout"]');
|
||||||
|
let element = await findByXpath('//li[@class="link right login-item"]/a/span');
|
||||||
|
let text = await element.getText();
|
||||||
|
await expect(text.toLowerCase()).toEqual('Sign In'.toLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await driver.quit();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -1,5 +0,0 @@
|
||||||
describe('test jest integration', () => {
|
|
||||||
test('testing test', () => {
|
|
||||||
expect('integration').toEqual('integration');
|
|
||||||
});
|
|
||||||
});
|
|
140
test/unit/components/email-step.test.jsx
Normal file
140
test/unit/components/email-step.test.jsx
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
const React = require('react');
|
||||||
|
const {shallowWithIntl} = require('../../helpers/intl-helpers.jsx');
|
||||||
|
const EmailStep = require('../../../src/components/join-flow/email-step.jsx');
|
||||||
|
const JoinFlowStep = require('../../../src/components/join-flow/join-flow-step.jsx');
|
||||||
|
const FormikInput = require('../../../src/components/formik-forms/formik-input.jsx');
|
||||||
|
const FormikCheckbox = require('../../../src/components/formik-forms/formik-checkbox.jsx');
|
||||||
|
|
||||||
|
describe('EmailStep test', () => {
|
||||||
|
test('send correct props to formik', () => {
|
||||||
|
const wrapper = shallowWithIntl(<EmailStep />);
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('props sent to JoinFlowStep', () => {
|
||||||
|
const wrapper = shallowWithIntl(<EmailStep />);
|
||||||
|
// Dive to get past the intl wrapper
|
||||||
|
const formikWrapper = wrapper.dive();
|
||||||
|
// Dive to get past the anonymous component.
|
||||||
|
const joinFlowWrapper = formikWrapper.dive().find(JoinFlowStep);
|
||||||
|
expect(joinFlowWrapper).toHaveLength(1);
|
||||||
|
expect(joinFlowWrapper.props().description).toBe('registration.emailStepDescription');
|
||||||
|
expect(joinFlowWrapper.props().footerContent.props.id).toBe('registration.acceptTermsOfUse');
|
||||||
|
expect(joinFlowWrapper.props().headerImgSrc).toBe('/images/join-flow/email-header.png');
|
||||||
|
expect(joinFlowWrapper.props().innerClassName).toBe('join-flow-inner-email-step');
|
||||||
|
expect(joinFlowWrapper.props().nextButton).toBe('registration.createAccount');
|
||||||
|
expect(joinFlowWrapper.props().title).toBe('registration.emailStepTitle');
|
||||||
|
expect(joinFlowWrapper.props().waiting).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('props sent to FormikInput for email', () => {
|
||||||
|
const wrapper = shallowWithIntl(<EmailStep />);
|
||||||
|
// Dive to get past the intl wrapper
|
||||||
|
const formikWrapper = wrapper.dive();
|
||||||
|
// Dive to get past the anonymous component.
|
||||||
|
const joinFlowWrapper = formikWrapper.dive().find(JoinFlowStep);
|
||||||
|
expect(joinFlowWrapper).toHaveLength(1);
|
||||||
|
const emailInputWrapper = joinFlowWrapper.find(FormikInput).first();
|
||||||
|
expect(emailInputWrapper.props().id).toEqual('email');
|
||||||
|
expect(emailInputWrapper.props().error).toBeUndefined();
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
test('props sent to FormikCheckbox for subscribe', () => {
|
||||||
|
const wrapper = shallowWithIntl(<EmailStep />);
|
||||||
|
// Dive to get past the intl wrapper
|
||||||
|
const formikWrapper = wrapper.dive();
|
||||||
|
// Dive to get past the anonymous component.
|
||||||
|
const joinFlowWrapper = formikWrapper.dive().find(JoinFlowStep);
|
||||||
|
expect(joinFlowWrapper).toHaveLength(1);
|
||||||
|
const checkboxWrapper = joinFlowWrapper.find(FormikCheckbox).first();
|
||||||
|
expect(checkboxWrapper).toHaveLength(1);
|
||||||
|
expect(checkboxWrapper.first().props().id).toEqual('subscribeCheckbox');
|
||||||
|
expect(checkboxWrapper.first().props().label).toEqual('registration.receiveEmails');
|
||||||
|
expect(checkboxWrapper.first().props().name).toEqual('subscribe');
|
||||||
|
});
|
||||||
|
test('handleValidSubmit passes formData to next step', () => {
|
||||||
|
const formikBag = {
|
||||||
|
setSubmitting: jest.fn()
|
||||||
|
};
|
||||||
|
global.grecaptcha = {
|
||||||
|
execute: jest.fn(),
|
||||||
|
render: jest.fn()
|
||||||
|
};
|
||||||
|
const formData = {item1: 'thing', item2: 'otherthing'};
|
||||||
|
const wrapper = shallowWithIntl(
|
||||||
|
<EmailStep />);
|
||||||
|
|
||||||
|
const formikWrapper = wrapper.dive();
|
||||||
|
formikWrapper.instance().onCaptchaLoad(); // to setup catpcha state
|
||||||
|
formikWrapper.instance().handleValidSubmit(formData, formikBag);
|
||||||
|
|
||||||
|
expect(formikBag.setSubmitting).toHaveBeenCalledWith(false);
|
||||||
|
expect(global.grecaptcha.execute).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
test('captchaSolved sets token and goes to next step', () => {
|
||||||
|
const props = {
|
||||||
|
onNextStep: jest.fn()
|
||||||
|
};
|
||||||
|
const formikBag = {
|
||||||
|
setSubmitting: jest.fn()
|
||||||
|
};
|
||||||
|
global.grecaptcha = {
|
||||||
|
execute: jest.fn(),
|
||||||
|
render: jest.fn()
|
||||||
|
};
|
||||||
|
const formData = {item1: 'thing', item2: 'otherthing'};
|
||||||
|
const wrapper = shallowWithIntl(
|
||||||
|
<EmailStep
|
||||||
|
{...props}
|
||||||
|
/>);
|
||||||
|
|
||||||
|
const formikWrapper = wrapper.dive();
|
||||||
|
// Call these to setup captcha.
|
||||||
|
formikWrapper.instance().onCaptchaLoad(); // to setup catpcha state
|
||||||
|
formikWrapper.instance().handleValidSubmit(formData, formikBag);
|
||||||
|
|
||||||
|
const captchaToken = 'abcd';
|
||||||
|
formikWrapper.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(
|
||||||
|
expect.objectContaining({
|
||||||
|
'item1': formData.item1,
|
||||||
|
'item2': formData.item2,
|
||||||
|
'g-recaptcha-response': captchaToken
|
||||||
|
}));
|
||||||
|
expect(formikBag.setSubmitting).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
test('validateEmail test email empty', () => {
|
||||||
|
const wrapper = shallowWithIntl(
|
||||||
|
<EmailStep />);
|
||||||
|
const formikWrapper = wrapper.dive();
|
||||||
|
const val = formikWrapper.instance().validateEmail('');
|
||||||
|
expect(val).toBe('general.required');
|
||||||
|
});
|
||||||
|
test('validateEmail test email null', () => {
|
||||||
|
const wrapper = shallowWithIntl(
|
||||||
|
<EmailStep />);
|
||||||
|
const formikWrapper = wrapper.dive();
|
||||||
|
const val = formikWrapper.instance().validateEmail(null);
|
||||||
|
expect(val).toBe('general.required');
|
||||||
|
});
|
||||||
|
test('validateEmail test email undefined', () => {
|
||||||
|
const wrapper = shallowWithIntl(
|
||||||
|
<EmailStep />);
|
||||||
|
const formikWrapper = wrapper.dive();
|
||||||
|
const val = formikWrapper.instance().validateEmail();
|
||||||
|
expect(val).toBe('general.required');
|
||||||
|
});
|
||||||
|
});
|
165
test/unit/components/join-flow.test.jsx
Normal file
165
test/unit/components/join-flow.test.jsx
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
import React from 'react';
|
||||||
|
const {shallowWithIntl} = require('../../helpers/intl-helpers.jsx');
|
||||||
|
import configureStore from 'redux-mock-store';
|
||||||
|
import JoinFlow from '../../../src/components/join-flow/join-flow';
|
||||||
|
import Progression from '../../../src/components/progression/progression.jsx';
|
||||||
|
import RegistrationErrorStep from '../../../src/components/join-flow/registration-error-step';
|
||||||
|
|
||||||
|
describe('JoinFlow', () => {
|
||||||
|
const mockStore = configureStore();
|
||||||
|
let store;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
store = mockStore({sessionActions: {
|
||||||
|
refreshSession: jest.fn()
|
||||||
|
}});
|
||||||
|
});
|
||||||
|
|
||||||
|
const getJoinFlowWrapper = props => {
|
||||||
|
const wrapper = shallowWithIntl(
|
||||||
|
<JoinFlow
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
, {context: {store}}
|
||||||
|
);
|
||||||
|
return wrapper
|
||||||
|
.dive() // unwrap redux connect(injectIntl(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('handleAdvanceStep', () => {
|
||||||
|
const joinFlowInstance = getJoinFlowWrapper().instance();
|
||||||
|
joinFlowInstance.setState({formData: {username: 'ScratchCat123'}, step: 2});
|
||||||
|
joinFlowInstance.handleAdvanceStep({email: 'scratchcat123@scratch.mit.edu'});
|
||||||
|
expect(joinFlowInstance.state.formData.username).toBe('ScratchCat123');
|
||||||
|
expect(joinFlowInstance.state.formData.email).toBe('scratchcat123@scratch.mit.edu');
|
||||||
|
expect(joinFlowInstance.state.step).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('when state.registrationError has error message, we show RegistrationErrorStep', () => {
|
||||||
|
const joinFlowWrapper = getJoinFlowWrapper();
|
||||||
|
joinFlowWrapper.instance().setState({registrationError: 'halp there is a errors!!'});
|
||||||
|
const registrationErrorWrapper = joinFlowWrapper.find(RegistrationErrorStep);
|
||||||
|
const progressionWrapper = joinFlowWrapper.find(Progression);
|
||||||
|
expect(registrationErrorWrapper).toHaveLength(1);
|
||||||
|
expect(progressionWrapper).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('when state.registrationError has null error message, we show Progression', () => {
|
||||||
|
const joinFlowWrapper = getJoinFlowWrapper();
|
||||||
|
joinFlowWrapper.instance().setState({registrationError: null});
|
||||||
|
const registrationErrorWrapper = joinFlowWrapper.find(RegistrationErrorStep);
|
||||||
|
const progressionWrapper = joinFlowWrapper.find(Progression);
|
||||||
|
expect(registrationErrorWrapper).toHaveLength(0);
|
||||||
|
expect(progressionWrapper).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('when state.registrationError has empty error message, we show Progression', () => {
|
||||||
|
const joinFlowWrapper = getJoinFlowWrapper();
|
||||||
|
joinFlowWrapper.instance().setState({registrationError: ''});
|
||||||
|
const registrationErrorWrapper = joinFlowWrapper.find(RegistrationErrorStep);
|
||||||
|
const progressionWrapper = joinFlowWrapper.find(Progression);
|
||||||
|
expect(registrationErrorWrapper).toHaveLength(0);
|
||||||
|
expect(progressionWrapper).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
33
test/unit/components/registration-error-step.test.jsx
Normal file
33
test/unit/components/registration-error-step.test.jsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {shallowWithIntl} from '../../helpers/intl-helpers.jsx';
|
||||||
|
import JoinFlowStep from '../../../src/components/join-flow/join-flow-step';
|
||||||
|
import RegistrationErrorStep from '../../../src/components/join-flow/registration-error-step';
|
||||||
|
|
||||||
|
describe('RegistrationErrorStep', () => {
|
||||||
|
const onTryAgain = jest.fn();
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = shallowWithIntl(
|
||||||
|
<RegistrationErrorStep
|
||||||
|
errorMsg={'error message'}
|
||||||
|
onTryAgain={onTryAgain}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows JoinFlowStep with props', () => {
|
||||||
|
// Dive to get past the anonymous component.
|
||||||
|
const joinFlowStepWrapper = wrapper.dive().find(JoinFlowStep);
|
||||||
|
expect(joinFlowStepWrapper).toHaveLength(1);
|
||||||
|
expect(joinFlowStepWrapper.props().description).toBe('error message');
|
||||||
|
expect(joinFlowStepWrapper.props().nextButton).toBe('general.tryAgain');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('when submitted, onTryAgain is called', () => {
|
||||||
|
// Dive to get past the anonymous component.
|
||||||
|
const joinFlowStepWrapper = wrapper.dive().find(JoinFlowStep);
|
||||||
|
joinFlowStepWrapper.props().onSubmit(new Event('event')); // eslint-disable-line no-undef
|
||||||
|
expect(onTryAgain).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
|
@ -63,4 +63,52 @@ describe('unit test lib/validate.js', () => {
|
||||||
response = validate.validatePasswordConfirm('', 'abcdefg');
|
response = validate.validatePasswordConfirm('', 'abcdefg');
|
||||||
expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordConfirmNotEquals'});
|
expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordConfirmNotEquals'});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('validate email address locally', () => {
|
||||||
|
let response;
|
||||||
|
expect(typeof validate.validateEmailLocally).toBe('function');
|
||||||
|
|
||||||
|
// permitted addresses:
|
||||||
|
response = validate.validateEmailLocally('abc@def.com');
|
||||||
|
expect(response).toEqual({valid: true});
|
||||||
|
response = validate.validateEmailLocally('abcdefghijklmnopqrst@abcdefghijklmnopqrst.info');
|
||||||
|
expect(response).toEqual({valid: true});
|
||||||
|
response = validate.validateEmailLocally('abc-def-ghi@jkl-mno.org');
|
||||||
|
expect(response).toEqual({valid: true});
|
||||||
|
response = validate.validateEmailLocally('_______@example.com');
|
||||||
|
expect(response).toEqual({valid: true});
|
||||||
|
response = validate.validateEmailLocally('email@example.museum');
|
||||||
|
expect(response).toEqual({valid: true});
|
||||||
|
response = validate.validateEmailLocally('email@example.co.jp');
|
||||||
|
expect(response).toEqual({valid: true});
|
||||||
|
|
||||||
|
// non-permitted addresses:
|
||||||
|
response = validate.validateEmailLocally('');
|
||||||
|
expect(response).toEqual({valid: false, errMsgId: 'general.required'});
|
||||||
|
response = validate.validateEmailLocally('a');
|
||||||
|
expect(response).toEqual({valid: false, errMsgId: 'registration.validationEmailInvalid'});
|
||||||
|
response = validate.validateEmailLocally('abc@def');
|
||||||
|
expect(response).toEqual({valid: false, errMsgId: 'registration.validationEmailInvalid'});
|
||||||
|
response = validate.validateEmailLocally('abc@def.c');
|
||||||
|
expect(response).toEqual({valid: false, errMsgId: 'registration.validationEmailInvalid'});
|
||||||
|
response = validate.validateEmailLocally('abc😄def@emoji.pizza');
|
||||||
|
expect(response).toEqual({valid: false, errMsgId: 'registration.validationEmailInvalid'});
|
||||||
|
response = validate.validateEmailLocally('あいうえお@example.com');
|
||||||
|
expect(response).toEqual({valid: false, errMsgId: 'registration.validationEmailInvalid'});
|
||||||
|
response = validate.validateEmailLocally('Abc..123@example.com');
|
||||||
|
expect(response).toEqual({valid: false, errMsgId: 'registration.validationEmailInvalid'});
|
||||||
|
response = validate.validateEmailLocally('Joe Smith <email@example.com>');
|
||||||
|
expect(response).toEqual({valid: false, errMsgId: 'registration.validationEmailInvalid'});
|
||||||
|
response = validate.validateEmailLocally('email@example@example.com');
|
||||||
|
expect(response).toEqual({valid: false, errMsgId: 'registration.validationEmailInvalid'});
|
||||||
|
response = validate.validateEmailLocally('email@example..com');
|
||||||
|
expect(response).toEqual({valid: false, errMsgId: 'registration.validationEmailInvalid'});
|
||||||
|
|
||||||
|
// edge cases:
|
||||||
|
// these are strictly legal according to email addres spec, but rejected by library we use:
|
||||||
|
response = validate.validateEmailLocally('email@123.123.123.123');
|
||||||
|
expect(response).toEqual({valid: false, errMsgId: 'registration.validationEmailInvalid'});
|
||||||
|
response = validate.validateEmailLocally('much."more unusual"@example.com');
|
||||||
|
expect(response).toEqual({valid: false, errMsgId: 'registration.validationEmailInvalid'});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -173,6 +173,8 @@ module.exports = {
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
'process.env.NODE_ENV': '"' + (process.env.NODE_ENV || 'development') + '"',
|
'process.env.NODE_ENV': '"' + (process.env.NODE_ENV || 'development') + '"',
|
||||||
'process.env.API_HOST': '"' + (process.env.API_HOST || 'https://api.scratch.mit.edu') + '"',
|
'process.env.API_HOST': '"' + (process.env.API_HOST || 'https://api.scratch.mit.edu') + '"',
|
||||||
|
'process.env.RECAPTCHA_SITE_KEY': '"' +
|
||||||
|
(process.env.RECAPTCHA_SITE_KEY || '6Lf6kK4UAAAAABKTyvdSqgcSVASEnMrCquiAkjVW') + '"',
|
||||||
'process.env.ASSET_HOST': '"' + (process.env.ASSET_HOST || 'https://assets.scratch.mit.edu') + '"',
|
'process.env.ASSET_HOST': '"' + (process.env.ASSET_HOST || 'https://assets.scratch.mit.edu') + '"',
|
||||||
'process.env.BACKPACK_HOST': '"' + (process.env.BACKPACK_HOST || 'https://backpack.scratch.mit.edu') + '"',
|
'process.env.BACKPACK_HOST': '"' + (process.env.BACKPACK_HOST || 'https://backpack.scratch.mit.edu') + '"',
|
||||||
'process.env.CLOUDDATA_HOST': '"' + (process.env.CLOUDDATA_HOST || 'clouddata.scratch.mit.edu') + '"',
|
'process.env.CLOUDDATA_HOST': '"' + (process.env.CLOUDDATA_HOST || 'clouddata.scratch.mit.edu') + '"',
|
||||||
|
|
Loading…
Reference in a new issue