mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-27 01:25:52 -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=${!CLOUDDATA_HOST_VAR}
|
||||
- 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_STAGING=https://scratch.ly
|
||||
- ROOT_URL_VAR=ROOT_URL_$TRAVIS_BRANCH
|
||||
|
|
1162
package-lock.json
generated
1162
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:integration": "npm run test:integration:jest && npm run test:smoke",
|
||||
"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: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",
|
||||
|
@ -66,7 +67,7 @@
|
|||
"babel-preset-react": "6.22.0",
|
||||
"bowser": "1.9.4",
|
||||
"cheerio": "1.0.0-rc.2",
|
||||
"chromedriver": "75.1.0",
|
||||
"chromedriver": "76.0.0",
|
||||
"classnames": "2.2.5",
|
||||
"cookie": "0.2.2",
|
||||
"copy-webpack-plugin": "0.2.0",
|
||||
|
@ -122,9 +123,10 @@
|
|||
"react-string-replace": "0.4.1",
|
||||
"react-telephone-input": "4.3.4",
|
||||
"redux": "3.5.2",
|
||||
"redux-mock-store": "^1.2.3",
|
||||
"redux-thunk": "2.0.1",
|
||||
"sass-loader": "6.0.6",
|
||||
"scratch-gui": "0.1.0-prerelease.20190828224521",
|
||||
"scratch-gui": "0.1.0-prerelease.20190912180550",
|
||||
"scratch-l10n": "latest",
|
||||
"selenium-webdriver": "3.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-10percent: hsla(35, 90, 55, .1);
|
||||
$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
|
||||
|
||||
|
|
|
@ -12,9 +12,10 @@ const FormikCheckboxSubComponent = ({
|
|||
id,
|
||||
label,
|
||||
labelClassName,
|
||||
outerClassName,
|
||||
...props
|
||||
}) => (
|
||||
<div className="checkbox">
|
||||
<div className={classNames('checkbox', outerClassName)}>
|
||||
<input
|
||||
checked={field.value}
|
||||
className="formik-checkbox"
|
||||
|
@ -50,7 +51,8 @@ FormikCheckboxSubComponent.propTypes = {
|
|||
}),
|
||||
id: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
labelClassName: PropTypes.string
|
||||
labelClassName: PropTypes.string,
|
||||
outerClassName: PropTypes.string
|
||||
};
|
||||
|
||||
|
||||
|
@ -59,6 +61,7 @@ const FormikCheckbox = ({
|
|||
label,
|
||||
labelClassName,
|
||||
name,
|
||||
outerClassName,
|
||||
...props
|
||||
}) => (
|
||||
<Field
|
||||
|
@ -67,6 +70,7 @@ const FormikCheckbox = ({
|
|||
label={label}
|
||||
labelClassName={labelClassName}
|
||||
name={name}
|
||||
outerClassName={outerClassName}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
@ -75,7 +79,8 @@ FormikCheckbox.propTypes = {
|
|||
id: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
labelClassName: PropTypes.string,
|
||||
name: PropTypes.string
|
||||
name: PropTypes.string,
|
||||
outerClassName: PropTypes.string
|
||||
};
|
||||
|
||||
module.exports = FormikCheckbox;
|
||||
|
|
|
@ -8,6 +8,7 @@ const {injectIntl, intlShape} = require('react-intl');
|
|||
const countryData = require('../../lib/country-data');
|
||||
const FormikSelect = require('../../components/formik-forms/formik-select.jsx');
|
||||
const JoinFlowStep = require('./join-flow-step.jsx');
|
||||
const FormikCheckbox = require('../../components/formik-forms/formik-checkbox.jsx');
|
||||
|
||||
require('./join-flow-steps.scss');
|
||||
|
||||
|
@ -94,6 +95,13 @@ class CountryStep extends React.Component {
|
|||
validate={this.validateSelect}
|
||||
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>
|
||||
</JoinFlowStep>
|
||||
);
|
||||
|
|
|
@ -4,9 +4,9 @@ const React = require('react');
|
|||
const PropTypes = require('prop-types');
|
||||
import {Formik} from 'formik';
|
||||
const {injectIntl, intlShape} = require('react-intl');
|
||||
const emailValidator = require('email-validator');
|
||||
const FormattedMessage = require('react-intl').FormattedMessage;
|
||||
|
||||
const validate = require('../../lib/validate');
|
||||
const JoinFlowStep = require('./join-flow-step.jsx');
|
||||
const FormikInput = require('../../components/formik-forms/formik-input.jsx');
|
||||
const FormikCheckbox = require('../../components/formik-forms/formik-checkbox.jsx');
|
||||
|
@ -20,30 +20,91 @@ class EmailStep extends React.Component {
|
|||
'handleSetEmailRef',
|
||||
'handleValidSubmit',
|
||||
'validateEmail',
|
||||
'validateForm'
|
||||
'validateForm',
|
||||
'setCaptchaRef',
|
||||
'captchaSolved',
|
||||
'onCaptchaLoad',
|
||||
'onCaptchaError'
|
||||
]);
|
||||
this.state = {
|
||||
captchaIsLoading: true
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
// automatically start with focus on username field
|
||||
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) {
|
||||
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) {
|
||||
if (!email) return this.props.intl.formatMessage({id: 'general.required'});
|
||||
const isValidLocally = emailValidator.validate(email);
|
||||
if (isValidLocally) {
|
||||
return null; // TODO: validate email address remotely
|
||||
const localResult = validate.validateEmailLocally(email);
|
||||
if (!localResult.valid) return this.props.intl.formatMessage({id: localResult.errMsgId});
|
||||
return validate.validateEmailRemotely(email).then(
|
||||
remoteResult => {
|
||||
if (remoteResult.valid === true) {
|
||||
return null;
|
||||
}
|
||||
return this.props.intl.formatMessage({id: 'registration.validationEmailInvalid'});
|
||||
return this.props.intl.formatMessage({id: remoteResult.errMsgId});
|
||||
}
|
||||
);
|
||||
}
|
||||
validateForm () {
|
||||
return {};
|
||||
}
|
||||
handleValidSubmit (formData, formikBag) {
|
||||
formikBag.setSubmitting(false);
|
||||
this.props.onNextStep(formData);
|
||||
this.formData = 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 () {
|
||||
return (
|
||||
|
@ -63,6 +124,8 @@ class EmailStep extends React.Component {
|
|||
handleSubmit,
|
||||
isSubmitting,
|
||||
setFieldError,
|
||||
setFieldTouched,
|
||||
setFieldValue,
|
||||
validateField
|
||||
} = props;
|
||||
return (
|
||||
|
@ -88,7 +151,7 @@ class EmailStep extends React.Component {
|
|||
innerClassName="join-flow-inner-email-step"
|
||||
nextButton={this.props.intl.formatMessage({id: 'registration.createAccount'})}
|
||||
title={this.props.intl.formatMessage({id: 'registration.emailStepTitle'})}
|
||||
waiting={isSubmitting}
|
||||
waiting={this.props.waiting || isSubmitting || this.state.captchaIsLoading}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<FormikInput
|
||||
|
@ -105,7 +168,11 @@ class EmailStep extends React.Component {
|
|||
validationClassName="validation-full-width-input"
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
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 */
|
||||
onSetRef={this.handleSetEmailRef}
|
||||
/>
|
||||
|
@ -116,6 +183,13 @@ class EmailStep extends React.Component {
|
|||
name="subscribe"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="g-recaptcha"
|
||||
data-badge="bottomright"
|
||||
data-sitekey={process.env.RECAPTCHA_SITE_KEY}
|
||||
data-size="invisible"
|
||||
ref={this.setCaptchaRef}
|
||||
/>
|
||||
</JoinFlowStep>
|
||||
);
|
||||
}}
|
||||
|
@ -126,7 +200,8 @@ class EmailStep extends React.Component {
|
|||
|
||||
EmailStep.propTypes = {
|
||||
intl: intlShape,
|
||||
onNextStep: PropTypes.func
|
||||
onNextStep: PropTypes.func,
|
||||
waiting: PropTypes.bool
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -132,6 +132,10 @@
|
|||
margin-left: -.5rem;
|
||||
}
|
||||
|
||||
.join-flow-registration-error {
|
||||
padding-top: 5.5rem;
|
||||
}
|
||||
|
||||
.join-flow-gender-description {
|
||||
margin-top: .625rem;
|
||||
margin-bottom: 1.25rem;
|
||||
|
@ -180,3 +184,7 @@
|
|||
a.join-flow-link:link, a.join-flow-link:visited, a.join-flow-link:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.yesNoCheckbox {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
const bindAll = require('lodash.bindall');
|
||||
const connect = require('react-redux').connect;
|
||||
const defaults = require('lodash.defaultsdeep');
|
||||
const PropTypes = require('prop-types');
|
||||
const React = require('react');
|
||||
|
||||
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 Progression = require('../progression/progression.jsx');
|
||||
const UsernameStep = require('./username-step.jsx');
|
||||
|
@ -13,44 +16,141 @@ const GenderStep = require('./gender-step.jsx');
|
|||
const CountryStep = require('./country-step.jsx');
|
||||
const EmailStep = require('./email-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 {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleAdvanceStep'
|
||||
'handleAdvanceStep',
|
||||
'handlePrepareToRegister',
|
||||
'handleRegistrationResponse',
|
||||
'handleSubmitRegistration'
|
||||
]);
|
||||
this.state = {
|
||||
formData: {},
|
||||
registrationError: null,
|
||||
step: 0
|
||||
step: 0,
|
||||
waiting: false
|
||||
};
|
||||
}
|
||||
handleAdvanceStep (formData) {
|
||||
formData = formData || {};
|
||||
handlePrepareToRegister (newFormData) {
|
||||
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,
|
||||
formData: defaults({}, formData, this.state.formData)
|
||||
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({
|
||||
formData: defaults({}, newFormData, this.state.formData),
|
||||
step: this.state.step + 1
|
||||
});
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.state.registrationError ? (
|
||||
<RegistrationErrorStep
|
||||
errorMsg={this.state.registrationError}
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
onTryAgain={() => this.handleSubmitRegistration(this.state.formData)}
|
||||
/* eslint-enable react/jsx-no-bind */
|
||||
/>
|
||||
) : (
|
||||
<Progression step={this.state.step}>
|
||||
<UsernameStep onNextStep={this.handleAdvanceStep} />
|
||||
<BirthDateStep onNextStep={this.handleAdvanceStep} />
|
||||
<GenderStep onNextStep={this.handleAdvanceStep} />
|
||||
<CountryStep onNextStep={this.handleAdvanceStep} />
|
||||
<EmailStep onNextStep={this.handleAdvanceStep} />
|
||||
<EmailStep
|
||||
waiting={this.state.waiting}
|
||||
onNextStep={this.handlePrepareToRegister}
|
||||
/>
|
||||
<WelcomeStep
|
||||
email={this.state.formData.email}
|
||||
username={this.state.formData.username}
|
||||
onNextStep={this.handleAdvanceStep}
|
||||
onNextStep={this.props.onCompleteRegistration}
|
||||
/>
|
||||
</Progression>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
@ -58,11 +158,27 @@ class JoinFlow extends React.Component {
|
|||
|
||||
JoinFlow.propTypes = {
|
||||
intl: intlShape,
|
||||
onCompleteRegistration: PropTypes.func
|
||||
onCompleteRegistration: PropTypes.func,
|
||||
refreshSession: PropTypes.func
|
||||
};
|
||||
|
||||
module.exports = injectIntl(JoinFlow);
|
||||
const IntlJoinFlow = injectIntl(JoinFlow);
|
||||
|
||||
/*
|
||||
eslint-enable
|
||||
*/
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
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 {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-bottom-left-radius: 1rem;
|
||||
border-bottom-right-radius: 1rem;
|
||||
height: 5.1875rem;
|
||||
background-color: $ui-orange;
|
||||
|
||||
&:hover {
|
||||
transition: background-color .25s ease;
|
||||
background-color: $ui-orange-90percent;
|
||||
}
|
||||
}
|
||||
|
||||
.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.useScratch3Registration ? (
|
||||
<Scratch3Registration
|
||||
createProjectOnComplete
|
||||
isOpen
|
||||
key="scratch3registration"
|
||||
/>
|
||||
|
|
|
@ -23,17 +23,19 @@ const Registration = ({
|
|||
);
|
||||
|
||||
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,
|
||||
handleCompleteRegistration: PropTypes.func,
|
||||
isOpen: PropTypes.bool
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
const mapDispatchToProps = (dispatch, ownProps) => ({
|
||||
handleCloseRegistration: () => {
|
||||
dispatch(navigationActions.setRegistrationOpen(false));
|
||||
},
|
||||
handleCompleteRegistration: () => {
|
||||
dispatch(navigationActions.handleCompleteRegistration());
|
||||
dispatch(navigationActions.handleCompleteRegistration(ownProps.createProjectOnComplete));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -89,6 +89,7 @@
|
|||
"general.ideas": "Ideas",
|
||||
"general.tipsWindow": "Tips Window",
|
||||
"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.username": "Username",
|
||||
"general.validationEmail": "Please enter a valid email address",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
module.exports = {};
|
||||
const api = require('./api');
|
||||
const emailValidator = require('email-validator');
|
||||
|
||||
module.exports.validateUsernameLocally = username => {
|
||||
if (!username || username === '') {
|
||||
|
@ -67,3 +68,36 @@ module.exports.validatePasswordConfirm = (password, passwordConfirm) => {
|
|||
}
|
||||
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
|
||||
});
|
||||
|
||||
module.exports.handleCompleteRegistration = () => (dispatch => {
|
||||
module.exports.handleCompleteRegistration = createProject => (dispatch => {
|
||||
if (createProject) {
|
||||
window.location = '/projects/editor/?tutorial=getStarted';
|
||||
} else {
|
||||
dispatch(sessionActions.refreshSession());
|
||||
dispatch(module.exports.setRegistrationOpen(false));
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.handleLogIn = (formData, callback) => (dispatch => {
|
||||
|
|
|
@ -161,6 +161,13 @@
|
|||
"view": "jobs/moderator/moderator",
|
||||
"title": "Community Moderator"
|
||||
},
|
||||
{
|
||||
"name": "join",
|
||||
"pattern": "^/join/?$",
|
||||
"routeAlias": "/join/?$",
|
||||
"view": "join/join",
|
||||
"title": "Join Scratch"
|
||||
},
|
||||
{
|
||||
"name": "messages",
|
||||
"pattern": "^/messages/?$",
|
||||
|
|
|
@ -114,8 +114,8 @@ class Download extends React.Component {
|
|||
className="download-button"
|
||||
href={
|
||||
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-3.5.0.dmg'
|
||||
'https://downloads.scratch.mit.edu/desktop/Scratch%20Desktop%20Setup%203.6.0.exe' :
|
||||
'https://downloads.scratch.mit.edu/desktop/Scratch%20Desktop-3.6.0.dmg'
|
||||
}
|
||||
>
|
||||
<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 = {
|
||||
browserName: 'chrome',
|
||||
platform: 'macOS 10.14',
|
||||
version: '75.0'
|
||||
version: '76.0'
|
||||
};
|
||||
var driver = new webdriver.Builder()
|
||||
.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
|
||||
let driverConfig = {
|
||||
browserName: 'chrome',
|
||||
platform: 'macOS 10.13',
|
||||
version: '70.0'
|
||||
platform: 'macOS 10.14',
|
||||
version: '76.0'
|
||||
};
|
||||
var driver = new webdriver.Builder()
|
||||
.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');
|
||||
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({
|
||||
'process.env.NODE_ENV': '"' + (process.env.NODE_ENV || 'development') + '"',
|
||||
'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.BACKPACK_HOST': '"' + (process.env.BACKPACK_HOST || 'https://backpack.scratch.mit.edu') + '"',
|
||||
'process.env.CLOUDDATA_HOST': '"' + (process.env.CLOUDDATA_HOST || 'clouddata.scratch.mit.edu') + '"',
|
||||
|
|
Loading…
Reference in a new issue