Merge pull request #3225 from LLK/develop

Develop → Merge → Release 2019-08-07
This commit is contained in:
Colby Gutierrez-Kraybill 2019-08-08 00:19:21 -04:00 committed by GitHub
commit 026292ad83
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 5638 additions and 3615 deletions

View file

@ -1,6 +1,7 @@
{
"plugins": [
"transform-object-rest-spread"
"transform-object-rest-spread",
"transform-require-context"
],
"presets": ["es2015", "react"],
}

View file

@ -8,8 +8,6 @@ var routeJson = require('../src/routes.json');
const FASTLY_SERVICE_ID = process.env.FASTLY_SERVICE_ID || '';
const S3_BUCKET_NAME = process.env.S3_BUCKET_NAME || '';
const FASTLY_CONCURRENCY = process.env.FASTLY_CONCURRENCY || 1;
var fastly = require('./lib/fastly-extended')(process.env.FASTLY_API_KEY, FASTLY_SERVICE_ID);
var extraAppRoutes = [
@ -94,7 +92,7 @@ async.auto({
}],
appRouteRequestConditions: ['version', function (results, cb) {
var conditions = {};
async.eachOfLimit(routes, FASTLY_CONCURRENCY, function (route, id, cb2) {
async.forEachOf(routes, function (route, id, cb2) {
var condition = {
name: fastlyConfig.getConditionNameForRoute(route, 'request'),
statement: 'req.url ~ "' + route.pattern + '"',
@ -114,7 +112,7 @@ async.auto({
}],
appRouteHeaders: ['version', 'appRouteRequestConditions', function (results, cb) {
var headers = {};
async.eachOfLimit(routes, FASTLY_CONCURRENCY, function (route, id, cb2) {
async.forEachOf(routes, function (route, id, cb2) {
if (route.redirect) {
async.auto({
responseCondition: function (cb3) {
@ -147,7 +145,7 @@ async.auto({
};
fastly.setFastlyHeader(results.version, header, cb3);
}]
}, FASTLY_CONCURRENCY, function (err, redirectResults) {
}, function (err, redirectResults) {
if (err) return cb2(err);
headers[id] = redirectResults.redirectHeader;
cb2(null, redirectResults);
@ -215,7 +213,7 @@ async.auto({
};
fastly.setFastlyHeader(results.version, header, cb2);
}]
}, FASTLY_CONCURRENCY, function (err, redirectResults) {
}, function (err, redirectResults) {
if (err) return cb(err);
cb(null, redirectResults);
});
@ -261,12 +259,12 @@ async.auto({
};
fastly.setFastlyHeader(results.version, header, cb2);
}]
}, FASTLY_CONCURRENCY, function (err, redirectResults) {
}, function (err, redirectResults) {
if (err) return cb(err);
cb(null, redirectResults);
});
}]
}, FASTLY_CONCURRENCY, function (err, results) {
}, function (err, results) {
if (err) throw new Error(err);
if (process.env.FASTLY_ACTIVATE_CHANGES) {
fastly.activateVersion(results.version, function (e, resp) {

7398
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -61,6 +61,7 @@
"babel-eslint": "10.0.2",
"babel-loader": "7.1.0",
"babel-plugin-transform-object-rest-spread": "6.26.0",
"babel-plugin-transform-require-context": "0.1.1",
"babel-preset-es2015": "6.22.0",
"babel-preset-react": "6.22.0",
"bowser": "1.9.4",
@ -71,6 +72,8 @@
"copy-webpack-plugin": "0.2.0",
"create-react-class": "15.6.2",
"css-loader": "0.23.1",
"enzyme": "3.10.0",
"enzyme-adapter-react-16": "1.14.0",
"eslint": "5.16.0",
"eslint-config-scratch": "5.0.0",
"eslint-plugin-cypress": "^2.0.1",
@ -120,7 +123,7 @@
"redux": "3.5.2",
"redux-thunk": "2.0.1",
"sass-loader": "6.0.6",
"scratch-gui": "0.1.0-prerelease.20190731192641",
"scratch-gui": "0.1.0-prerelease.20190807160203",
"scratch-l10n": "latest",
"selenium-webdriver": "3.6.0",
"slick-carousel": "1.6.0",
@ -133,6 +136,15 @@
"webpack-dev-middleware": "2.0.4",
"xhr": "2.2.0"
},
"jest": {
"setupFiles": [
"<rootDir>/test/helpers/enzyme-setup.js"
],
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/test/__mocks__/fileMock.js",
"\\.(css|less|scss)$": "<rootDir>/test/__mocks__/styleMock.js"
}
},
"nyc": {
"include": [
"bin/**/*.js",

View file

@ -12,9 +12,17 @@ const FormikInput = ({
className,
error,
validationClassName,
wrapperClassName,
...props
}) => (
<div className="col-sm-9 row row-with-tooltip">
<div
className={classNames(
'col-sm-9',
'row',
'row-with-tooltip',
wrapperClassName
)}
>
<Field
className={classNames(
'input',
@ -36,7 +44,8 @@ FormikInput.propTypes = {
className: PropTypes.string,
error: PropTypes.string,
type: PropTypes.string,
validationClassName: PropTypes.string
validationClassName: PropTypes.string,
wrapperClassName: PropTypes.string
};
module.exports = FormikInput;

View file

@ -0,0 +1,107 @@
const classNames = require('classnames');
const PropTypes = require('prop-types');
const React = require('react');
import {Field} from 'formik';
const FormikInput = require('./formik-input.jsx');
require('./formik-radio-button.scss');
require('../forms/row.scss');
const FormikRadioButtonSubComponent = ({
buttonValue,
children,
className,
field,
label,
labelClassName,
...props
}) => (
<React.Fragment>
<input
checked={buttonValue === field.value}
className={classNames(
'formik-radio-button',
className
)}
name={field.name}
type="radio"
value={buttonValue}
onBlur={field.onBlur} /* eslint-disable-line react/jsx-handler-names */
onChange={field.onChange} /* eslint-disable-line react/jsx-handler-names */
{...props}
/>
{label && (
<label
className={classNames(
'formik-radio-label',
labelClassName
)}
htmlFor={buttonValue}
>
{label}
</label>
)}
{children}
</React.Fragment>
);
FormikRadioButtonSubComponent.propTypes = {
buttonValue: PropTypes.string,
children: PropTypes.node,
className: PropTypes.string,
field: PropTypes.shape({
name: PropTypes.string,
onBlur: PropTypes.function,
onChange: PropTypes.function,
value: PropTypes.string
}),
label: PropTypes.string,
labelClassName: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
};
const FormikRadioButton = ({
buttonValue,
className,
isCustomInput,
label,
name,
onSetCustom,
...props
}) => (
<Field
buttonValue={buttonValue}
className={className}
component={FormikRadioButtonSubComponent}
label={label}
labelClassName={isCustomInput ? 'formik-radio-label-other' : ''}
name={name}
{...props}
>
{isCustomInput && (
<FormikInput
className="formik-radio-input"
name="custom"
wrapperClassName="formik-radio-input-wrapper"
/* eslint-disable react/jsx-no-bind */
onChange={event => onSetCustom(event.target.value)}
onFocus={event => onSetCustom(event.target.value)}
/* eslint-enable react/jsx-no-bind */
/>
)}
</Field>
);
FormikRadioButton.propTypes = {
buttonValue: PropTypes.string,
className: PropTypes.string,
isCustomInput: PropTypes.bool,
label: PropTypes.string,
name: PropTypes.string,
onSetCustom: PropTypes.func,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
};
module.exports = FormikRadioButton;

View file

@ -0,0 +1,61 @@
@import "../../colors";
.formik-radio-label {
font-weight: 300;
margin-left: 1rem;
}
input[type="radio"].formik-radio-button {
margin-top: 1px;
border: 1px solid $box-shadow-light-gray;
border-radius: 50%;
width: 1rem;
min-width: 1rem; /* necessary to prevent width from being too small in 'other' case */
height: 1rem;
appearance: none;
background-color: $ui-white;
&:checked,
&:focus {
outline: none;
}
&:checked {
transition: all .25s ease;
box-shadow: 0 0 0 2px $ui-blue-25percent;
border: 1px solid $ui-blue;
background-color: $ui-white;
&:after {
display: block;
transform: translate(.125rem, .125rem);
border-radius: 50%;
background-color: $ui-blue;
width: .625rem;
height: .625rem;
content: "";
}
}
}
input.formik-radio-input, .formik-radio-input input {
height: 2.1875rem;
width: 10.25rem;
margin-bottom: 0;
border-radius: .5rem;
background-color: $ui-white;
&:focus {
box-shadow: 0 0 0 .25rem $ui-blue-25percent;
}
}
.formik-radio-input-wrapper {
margin-left: auto;
margin-right: .25rem;
}
.formik-radio-label-other {
max-width: 7rem;
margin-right: .25rem;
}

View file

@ -30,3 +30,13 @@
margin-bottom: .75rem;
line-height: 1.7rem;
}
.row-inline {
display: flex;
}
/* override margin-bottom so placing a label next to a radio button does not
mess up vertical alignment */
.row-inline label {
margin-bottom: 0;
}

View file

@ -0,0 +1,148 @@
const bindAll = require('lodash.bindall');
const classNames = require('classnames');
const React = require('react');
const PropTypes = require('prop-types');
import {Formik} from 'formik';
const {injectIntl, intlShape} = require('react-intl');
const FormikRadioButton = require('../../components/formik-forms/formik-radio-button.jsx');
const JoinFlowStep = require('./join-flow-step.jsx');
require('./join-flow-steps.scss');
const GenderOption = ({
label,
onSetFieldValue,
selectedValue,
value
}) => (
<div
className={classNames(
'col-sm-9',
'row',
'row-inline',
'gender-radio-row',
{'gender-radio-row-selected': (selectedValue === value)}
)}
/* eslint-disable react/jsx-no-bind */
onClick={() => onSetFieldValue('gender', value, false)}
/* eslint-enable react/jsx-no-bind */
>
<FormikRadioButton
buttonValue={value}
className={classNames(
'join-flow-radio'
)}
label={label}
name="gender"
/>
</div>
);
GenderOption.propTypes = {
label: PropTypes.string,
onSetFieldValue: PropTypes.func,
selectedValue: PropTypes.string,
value: PropTypes.string
};
class GenderStep extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleValidSubmit'
]);
}
handleValidSubmit (formData, formikBag) {
formikBag.setSubmitting(false);
if (!formData.gender || formData.gender === 'null') {
formData.gender = 'Prefer not to say';
}
delete formData.custom;
this.props.onNextStep(formData);
}
render () {
return (
<Formik
initialValues={{
gender: 'null',
custom: ''
}}
onSubmit={this.handleValidSubmit}
>
{props => {
const {
handleSubmit,
isSubmitting,
setFieldValue,
setValues,
values
} = props;
return (
<JoinFlowStep
className="join-flow-gender-step"
description={this.props.intl.formatMessage({id: 'registration.genderStepDescription'})}
title={this.props.intl.formatMessage({id: 'registration.genderStepTitle'})}
waiting={isSubmitting}
onSubmit={handleSubmit}
>
<GenderOption
label={this.props.intl.formatMessage({id: 'general.female'})}
selectedValue={values.gender}
value="Female"
onSetFieldValue={setFieldValue}
/>
<GenderOption
label={this.props.intl.formatMessage({id: 'general.male'})}
selectedValue={values.gender}
value="Male"
onSetFieldValue={setFieldValue}
/>
<div
className={classNames(
'col-sm-9',
'row',
'row-inline',
'gender-radio-row',
{'gender-radio-row-selected': (values.gender === values.custom)}
)}
/* eslint-disable react/jsx-no-bind */
onClick={() => setFieldValue('gender', values.custom, false)}
/* eslint-enable react/jsx-no-bind */
>
<FormikRadioButton
isCustomInput
buttonValue={values.custom}
className={classNames(
'join-flow-radio'
)}
label={this.props.intl.formatMessage({id: 'registration.genderOptionAnother'})}
name="gender"
/* eslint-disable react/jsx-no-bind */
onSetCustom={newCustomVal => setValues({
gender: newCustomVal,
custom: newCustomVal
})}
/* eslint-enable react/jsx-no-bind */
/>
</div>
<GenderOption
label={this.props.intl.formatMessage({id: 'registration.genderOptionPreferNotToSay'})}
selectedValue={values.gender}
value="Prefer not to say"
onSetFieldValue={setFieldValue}
/>
</JoinFlowStep>
);
}}
</Formik>
);
}
}
GenderStep.propTypes = {
intl: intlShape,
onNextStep: PropTypes.func
};
module.exports = injectIntl(GenderStep);

View file

@ -1,3 +1,4 @@
const classNames = require('classnames');
const React = require('react');
const PropTypes = require('prop-types');
@ -9,8 +10,10 @@ require('./join-flow-step.scss');
const JoinFlowStep = ({
children,
className,
description,
headerImgSrc,
nextButton,
onSubmit,
title,
waiting
@ -22,7 +25,12 @@ const JoinFlowStep = ({
</div>
)}
<div>
<ModalInnerContent className="join-flow-inner-content">
<ModalInnerContent
className={classNames(
'join-flow-inner-content',
className
)}
>
{title && (
<ModalTitle
className="join-flow-title"
@ -37,14 +45,19 @@ const JoinFlowStep = ({
{children}
</ModalInnerContent>
</div>
<NextStepButton waiting={waiting} />
<NextStepButton
content={nextButton}
waiting={waiting}
/>
</form>
);
JoinFlowStep.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
description: PropTypes.string,
headerImgSrc: PropTypes.string,
nextButton: PropTypes.node,
onSubmit: PropTypes.func,
title: PropTypes.string,
waiting: PropTypes.bool

View file

@ -18,6 +18,14 @@
margin-bottom: .5rem;
}
.join-flow-instructions {
font-size: .875rem;
font-weight: bold;
line-height: 1.37500rem;
margin-bottom: 1rem;
text-align: center;
}
.validation-full-width-input {
transform: translate(21.5625rem, 0);
}
@ -55,3 +63,31 @@
display: flex;
margin: 0 auto;
}
.join-flow-gender-step {
height: 27.375rem;
padding-top: 3rem;
}
.gender-radio-row {
transition: all .125s ease;
width: 20.875rem;
height: 2.85rem;
background-color: $ui-gray;
border-radius: .5rem;
margin-bottom: 0.375rem;
padding-left: 0.8125rem;
display: flex;
align-items: center;
}
.gender-radio-row-selected {
transition: all .125s ease;
background-color: $ui-blue-25percent;
}
.join-flow-next-button-arrow {
width: 2rem;
height: 2rem;
margin-left: .5rem;
}

View file

@ -9,6 +9,7 @@ const intlShape = require('../../lib/intl.jsx').intlShape;
const Progression = require('../progression/progression.jsx');
const UsernameStep = require('./username-step.jsx');
const BirthDateStep = require('./birthdate-step.jsx');
const GenderStep = require('./gender-step.jsx');
const EmailStep = require('./email-step.jsx');
const WelcomeStep = require('./welcome-step.jsx');
@ -40,8 +41,10 @@ class JoinFlow extends React.Component {
<Progression step={this.state.step}>
<UsernameStep onNextStep={this.handleAdvanceStep} />
<BirthDateStep onNextStep={this.handleAdvanceStep} />
<GenderStep onNextStep={this.handleAdvanceStep} />
<EmailStep onNextStep={this.handleAdvanceStep} />
<WelcomeStep
email={this.state.formData.email}
username={this.state.formData.username}
onNextStep={this.handleAdvanceStep}
/>

View file

@ -19,8 +19,8 @@ const NextStepButton = props => (
{props.waiting ?
<Spinner /> : (
<ModalTitle
className="next-step-title-large"
title={props.text ? props.text : props.intl.formatMessage({id: 'registration.nextStep'})}
className="next-step-title"
title={props.content ? props.content : props.intl.formatMessage({id: 'general.next'})}
/>
)
}
@ -28,8 +28,8 @@ const NextStepButton = props => (
);
NextStepButton.propTypes = {
content: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
intl: intl.intlShape,
text: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
waiting: PropTypes.bool
};

View file

@ -10,6 +10,9 @@
background-color: $ui-orange;
}
.next-step-title-large {
.next-step-title {
font-size: 1.25rem;
display: flex;
justify-content: center;
align-items: center;
}

View file

@ -2,6 +2,7 @@ const bindAll = require('lodash.bindall');
const React = require('react');
const PropTypes = require('prop-types');
import {Formik} from 'formik';
const FormattedMessage = require('react-intl').FormattedMessage;
const {injectIntl, intlShape} = require('react-intl');
const JoinFlowStep = require('./join-flow-step.jsx');
@ -44,13 +45,31 @@ class WelcomeStep extends React.Component {
id: 'registration.welcomeStepDescriptionNonEducator'
})}
headerImgSrc="/images/hoc/getting-started.jpg"
nextButton={
<React.Fragment>
<FormattedMessage id="registration.makeProject" />
<img
className="join-flow-next-button-arrow"
src="/svgs/project/r-arrow.svg"
/>
</React.Fragment>
}
title={`${this.props.intl.formatMessage(
{id: 'registration.welcomeStepTitleNonEducator'},
{username: this.props.username}
)}`}
waiting={isSubmitting}
onSubmit={handleSubmit}
/>
>
<div className="join-flow-instructions">
<FormattedMessage
id="registration.welcomeStepInstructions"
values={{
email: this.props.email
}}
/>
</div>
</JoinFlowStep>
);
}}
</Formik>
@ -59,6 +78,7 @@ class WelcomeStep extends React.Component {
}
WelcomeStep.propTypes = {
email: PropTypes.string,
intl: intlShape,
onNextStep: PropTypes.func,
username: PropTypes.string

View file

@ -20,7 +20,7 @@ const ModalTitle = ({
ModalTitle.propTypes = {
className: PropTypes.string,
title: PropTypes.string
title: PropTypes.oneOfType([PropTypes.string, PropTypes.node])
};
module.exports = ModalTitle;

View file

@ -33,29 +33,20 @@ require('./steps.scss');
const DEFAULT_COUNTRY = 'us';
/**
* Return a list of options to give to frc select
* Return a list of options to give to select
* @param {object} reactIntl react-intl, used to localize strings
* @param {string} defaultCountry optional string of default country to put at top of list
* @return {object} ordered set of county options formatted for frc select
* @return {object} ordered set of county options formatted for select
*/
const getCountryOptions = (reactIntl, defaultCountry) => {
const options = countryData.countryOptions.concat({
label: reactIntl.formatMessage({id: 'registration.selectCountry'}),
disabled: true,
value: ''
});
if (typeof defaultCountry !== 'undefined') {
return options.sort((a, b) => {
if (a.disabled) return -1;
if (b.disabled) return 1;
if (a.value === defaultCountry) return -1;
if (b.value === defaultCountry) return 1;
return 0;
});
}
return options;
};
const getCountryOptions = reactIntl => (
[
{
label: reactIntl.formatMessage({id: 'registration.selectCountry'}),
disabled: true,
value: ''
},
...countryData.registrationCountryOptions
]
);
const NextStepButton = props => (
<Button
@ -417,10 +408,14 @@ class DemographicsStep extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'birthDateValidator',
'countryValidator',
'getCountryName',
'getMonthOptions',
'getYearOptions',
'handleChooseGender',
'handleValidSubmit'
'handleValidSubmit',
'isValidBirthdate'
]);
this.state = {
otherDisabled: true
@ -444,8 +439,23 @@ class DemographicsStep extends React.Component {
handleChooseGender (name, gender) {
this.setState({otherDisabled: gender !== 'other'});
}
// look up country name using user's country code selection
getCountryName (values) {
if (values.countryCode) {
const countryInfo = countryData.lookupCountryInfo(values.countryCode);
if (countryInfo) {
return countryInfo.name;
}
}
return null;
}
handleValidSubmit (formData) {
return this.props.onNextStep(formData);
const countryName = this.getCountryName(formData);
if (countryName && formData.user) {
formData.user.country = countryName;
return this.props.onNextStep(formData);
}
return false;
}
isValidBirthdate (year, month) {
const birthdate = new Date(
@ -459,8 +469,13 @@ class DemographicsStep extends React.Component {
const isValid = this.isValidBirthdate(values['user.birth.year'], values['user.birth.month']);
return isValid ? true : this.props.intl.formatMessage({id: 'teacherRegistration.validationAge'});
}
countryValidator (values) {
const countryName = this.getCountryName(values);
if (countryName) return true;
return this.props.intl.formatMessage({id: 'general.invalidSelection'});
}
render () {
const countryOptions = getCountryOptions(this.props.intl, DEFAULT_COUNTRY);
const countryOptions = getCountryOptions(this.props.intl);
return (
<Slide className="registration-step demographics-step">
<h2>
@ -539,8 +554,11 @@ class DemographicsStep extends React.Component {
<Select
required
label={this.props.intl.formatMessage({id: 'general.country'})}
name="user.country"
name="countryCode"
options={countryOptions}
validations={{
countryVal: values => this.countryValidator(values)
}}
value={countryOptions[0].value}
/>
<Checkbox
@ -948,6 +966,7 @@ class AddressStep extends React.Component {
render () {
let stateOptions = countryData.subdivisionOptions[this.state.countryChoice];
stateOptions = [{}].concat(stateOptions);
const countryOptions = getCountryOptions(this.props.intl);
return (
<Slide className="registration-step address-step">
<h2>
@ -970,7 +989,7 @@ class AddressStep extends React.Component {
this.props.intl.formatMessage({id: 'general.country'})
}
name="address.country"
options={getCountryOptions(this.props.intl)}
options={countryOptions}
value={this.props.defaultCountry}
onChange={this.handleChangeCountry}
/>

View file

@ -33,6 +33,7 @@
"general.getStarted": "Get Started",
"general.gender": "Gender",
"general.guidelines": "Community Guidelines",
"general.invalidSelection": "Invalid selection",
"general.jobs": "Jobs",
"general.joinScratch": "Join Scratch",
"general.legal": "Legal",
@ -56,6 +57,7 @@
"general.myClass": "My Class",
"general.myClasses": "My Classes",
"general.myStuff": "My Stuff",
"general.next": "Next",
"general.noDeletionTitle": "Your Account Will Not Be Deleted",
"general.noDeletionDescription": "Your account was scheduled for deletion but you logged in. Your account has been reactivated. If you didnt request for your account to be deleted, you should {resetLink} to make sure your account is secure.",
"general.noDeletionLink": "change your password",
@ -152,12 +154,17 @@
"registration.confirmYourEmail": "Confirm Your Email",
"registration.confirmYourEmailDescription": "If you haven't already, please click the link in the confirmation email sent to:",
"registration.createUsername": "Create a username",
"registration.genderStepTitle": "What's your gender?",
"registration.genderStepDescription": "Scratch welcomes people of all genders. We will always keep this information private.",
"registration.genderOptionAnother": "Another gender:",
"registration.genderOptionPreferNotToSay": "Prefer not to say",
"registration.emailStepTitle": "What's your email?",
"registration.emailStepDescription": "We need this to finish creating your account. Scratch will always keep this information private.",
"registration.goToClass": "Go to Class",
"registration.invitedBy": "invited by",
"registration.lastStepTitle": "Thank you for requesting a Scratch Teacher Account",
"registration.lastStepDescription": "We are currently processing your application. ",
"registration.makeProject": "Make a Project",
"registration.mustBeNewStudent": "You must be a new student to complete your registration",
"registration.nameStepTooltip": "This information is used for verification and to aggregate usage statistics.",
"registration.newPassword": "New Password",
@ -193,6 +200,7 @@
"registration.waitForApprovalDescription": "You can log into your Scratch Account now, but the features specific to Teachers are not yet available. Your information is being reviewed. Please be patient, the approval process can take up to one day. You will receive an email indicating your account has been upgraded once your account has been approved.",
"registration.welcomeStepDescription": "You have successfully set up a Scratch account! You are now a member of the class:",
"registration.welcomeStepDescriptionNonEducator": "Youre now logged in! You can start exploring and creating projects.",
"registration.welcomeStepInstructions": "Want to share and comment? Click the link on the email we sent to {email}.",
"registration.welcomeStepPrompt": "To get started, click on the button below.",
"registration.welcomeStepTitle": "Hurray! Welcome to Scratch!",
"registration.welcomeStepTitleNonEducator": "Welcome to Scratch, {username}!",

File diff suppressed because it is too large Load diff

View file

@ -115,9 +115,7 @@ class TeacherRegistration extends React.Component {
onNextStep={this.handleAdvanceStep}
/>
<Steps.PhoneNumberStep
defaultCountry={
this.state.formData.user && this.state.formData.user.country
}
defaultCountry={this.state.formData.countryCode}
waiting={this.state.waiting}
onNextStep={this.handleAdvanceStep}
/>
@ -126,9 +124,7 @@ class TeacherRegistration extends React.Component {
onNextStep={this.handleAdvanceStep}
/>
<Steps.AddressStep
defaultCountry={
this.state.formData.user && this.state.formData.user.country
}
defaultCountry={this.state.formData.countryCode}
waiting={this.state.waiting}
onNextStep={this.handleAdvanceStep}
/>

View file

@ -1,4 +1,5 @@
module.exports = {
extends: ['scratch/react'],
env: {
jest: true
}

View file

@ -0,0 +1,3 @@
// __mocks__/fileMock.js
module.exports = 'test-file-stub';

View file

@ -0,0 +1,3 @@
// __mocks__/styleMock.js
module.exports = {};

View file

@ -0,0 +1,4 @@
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({adapter: new Adapter()});

View file

@ -0,0 +1,39 @@
/*
* Helpers for using enzyme and react-test-renderer with react-intl
* Directly from https://github.com/yahoo/react-intl/wiki/Testing-with-React-Intl
*/
import React from 'react';
import renderer from 'react-test-renderer';
import {IntlProvider, intlShape} from 'react-intl';
import {mount, shallow} from 'enzyme';
const intlProvider = new IntlProvider({locale: 'en'}, {});
const {intl} = intlProvider.getChildContext();
const nodeWithIntlProp = node => React.cloneElement(node, {intl});
const shallowWithIntl = (node, {context} = {}) => shallow(
nodeWithIntlProp(node),
{
context: Object.assign({}, context, {intl})
}
);
const mountWithIntl = (node, {context, childContextTypes} = {}) => mount(
nodeWithIntlProp(node),
{
context: Object.assign({}, context, {intl}),
childContextTypes: Object.assign({}, {intl: intlShape}, childContextTypes)
}
);
// react-test-renderer component for use with snapshot testing
const componentWithIntl = (children, props = {locale: 'en'}) => renderer.create(
<IntlProvider {...props}>{children}</IntlProvider>
);
export {
componentWithIntl,
shallowWithIntl,
mountWithIntl
};

View file

@ -0,0 +1,32 @@
import React from 'react';
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
import NextStepButton from '../../../src/components/join-flow/next-step-button';
import Spinner from '../../../src/components/spinner/spinner.jsx';
describe('NextStepButton', () => {
const defaultProps = () => ({
text: 'I am a button',
waiting: false
});
test('testing spinner does not show and button enabled', () => {
const component = mountWithIntl(
<NextStepButton
{...defaultProps()}
/>
);
expect(component.find(Spinner).exists()).toEqual(false);
expect(component.find('button[type="submit"]').prop('disabled')).toBe(false);
});
test('testing spinner does show and button disabled', () => {
const component = mountWithIntl(
<NextStepButton
{...defaultProps()}
/>
);
component.setProps({waiting: true});
expect(component.find(Spinner).exists()).toEqual(true);
expect(component.find('button[type="submit"]').prop('disabled')).toBe(true);
});
});

View file

@ -0,0 +1,111 @@
const {
countryInfo,
countryOptions,
lookupCountryInfo,
dupeCommonCountries,
registrationCountryOptions,
subdivisionOptions
} = require('../../../src/lib/country-data');
describe('unit test lib/country-data.js', () => {
test('countryInfo has the ballpark number of countries we expect', () => {
expect(typeof countryInfo).toBe('object');
expect(countryInfo.length > 200).toEqual(true);
expect(countryInfo.length < 300).toEqual(true);
});
test('countryOptions() maintains number of items', () => {
expect(typeof countryOptions).toBe('function');
const myCountryOptions = countryOptions(countryInfo, 'name');
const numCountries = countryInfo.length;
expect(myCountryOptions.length).toEqual(numCountries);
});
test('countryOptions() called with value=name will use correct display strings and country name', () => {
expect(typeof countryOptions).toBe('function');
const myCountryOptions = countryOptions(countryInfo, 'name');
const eswatiniInfo = myCountryOptions.find(country => country.value === 'Swaziland');
expect(eswatiniInfo).toBeTruthy();
expect(eswatiniInfo.label).toEqual('Eswatini');
const swedenInfo = myCountryOptions.find(country => country.value === 'Sweden');
expect(swedenInfo).toBeTruthy();
expect(swedenInfo.label).toEqual('Sweden');
});
test('countryOptions() called with value==code will use correct display strings and country code', () => {
expect(typeof countryOptions).toBe('function');
const myCountryOptions = countryOptions(countryInfo, 'code');
const szInfo = myCountryOptions
.find(country => country.value === 'sz');
expect(szInfo).toBeTruthy();
expect(szInfo.label).toEqual('Eswatini');
});
test('lookupCountryInfo() will find country info', () => {
expect(typeof lookupCountryInfo).toBe('function');
const eswatiniInfo = lookupCountryInfo('sz');
expect(eswatiniInfo.name).toEqual('Swaziland');
expect(eswatiniInfo.display).toEqual('Eswatini');
expect(eswatiniInfo.code).toEqual('sz');
});
test('calling dupeCommonCountries() will duplicate the requested country info at start of array', () => {
expect(typeof dupeCommonCountries).toBe('function');
const countryInfoWithCommon = dupeCommonCountries(countryInfo, ['ca', 'gb']);
// test that the two entries have been added to the start of the array
const numCountries = countryInfo.length;
expect(countryInfoWithCommon.length).toEqual(numCountries + 2);
expect(countryInfoWithCommon[0]).toEqual({code: 'ca', name: 'Canada'});
expect(countryInfoWithCommon[1]).toEqual({code: 'gb', name: 'United Kingdom'});
// test that there are now two entries for Canada
const canadaItems = countryInfoWithCommon.reduce((acc, thisCountry) => (
thisCountry.code === 'ca' ? [...acc, thisCountry] : acc
), []);
expect(canadaItems.length).toEqual(2);
// test that there are now two entries for UK
const ukItems = countryInfoWithCommon.reduce((acc, thisCountry) => (
thisCountry.code === 'gb' ? [...acc, thisCountry] : acc
), []);
expect(ukItems.length).toEqual(2);
});
test('registrationCountryOptions object places USA and UK at start, with display name versions', () => {
expect(typeof registrationCountryOptions).toBe('object');
const numCountries = countryInfo.length;
// test that the two entries have been added to the start of the array, and that
// the name of the USA includes "America"
expect(registrationCountryOptions.length).toEqual(numCountries + 2);
expect(registrationCountryOptions[0]).toEqual({value: 'us', label: 'United States of America'});
expect(registrationCountryOptions[1]).toEqual({value: 'gb', label: 'United Kingdom'});
// test that there are now two entries for USA
const usaOptions = registrationCountryOptions.reduce((acc, thisCountry) => (
thisCountry.value === 'us' ? [...acc, thisCountry] : acc
), []);
expect(usaOptions.length).toEqual(2);
// test that there are now two entries for UK
const ukOptions = registrationCountryOptions.reduce((acc, thisCountry) => (
thisCountry.value === 'gb' ? [...acc, thisCountry] : acc
), []);
expect(ukOptions.length).toEqual(2);
});
test('subdivisionOptions object should include correct info for sample country', () => {
expect(typeof subdivisionOptions).toBe('object');
// 71 subdivisions in Bangladesh
expect(subdivisionOptions.bd.length > 50).toEqual(true);
expect(subdivisionOptions.bd.length < 100).toEqual(true);
const nilphamari = subdivisionOptions.bd.find(item => item.label === 'Nilphamari');
expect(nilphamari).toEqual({
label: 'Nilphamari',
value: 'bd-46',
type: 'District'
});
});
});