Merge pull request #3258 from LLK/release/2019-08-15

[Master] Release 2019-08-15
This commit is contained in:
Benjamin Wheeler 2019-08-15 10:51:19 -04:00 committed by GitHub
commit 6727cb133c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 791 additions and 105 deletions

91
package-lock.json generated
View file

@ -4035,6 +4035,12 @@
"minimalistic-crypto-utils": "^1.0.0"
}
},
"email-validator": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/email-validator/-/email-validator-2.0.4.tgz",
"integrity": "sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ==",
"dev": true
},
"emoji-regex": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
@ -5474,7 +5480,8 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"aproba": {
"version": "1.2.0",
@ -5495,12 +5502,14 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -5515,17 +5524,20 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
@ -5642,7 +5654,8 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@ -5654,6 +5667,7 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -5668,6 +5682,7 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -5675,12 +5690,14 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -5699,6 +5716,7 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -5779,7 +5797,8 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
@ -5791,6 +5810,7 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -5876,7 +5896,8 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
@ -5912,6 +5933,7 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -5931,6 +5953,7 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -5974,12 +5997,14 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"yallist": {
"version": "3.0.3",
"bundled": true,
"dev": true
"dev": true,
"optional": true
}
}
},
@ -7501,12 +7526,6 @@
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"isemail": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/isemail/-/isemail-1.2.0.tgz",
"integrity": "sha1-vgPfjMPineTSxd9lASY/H6RZXpo=",
"dev": true
},
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@ -9206,6 +9225,14 @@
"isemail": "1.x.x",
"moment": "2.x.x",
"topo": "1.x.x"
},
"dependencies": {
"isemail": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/isemail/-/isemail-1.2.0.tgz",
"integrity": "sha1-vgPfjMPineTSxd9lASY/H6RZXpo=",
"dev": true
}
}
},
"jquery": {
@ -14929,15 +14956,15 @@
}
},
"scratch-gui": {
"version": "0.1.0-prerelease.20190808151251",
"resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20190808151251.tgz",
"integrity": "sha512-KBYxva8dWl/XrWTPePveV8oq2RUkhyLoDFQnz2pJSBJOKvkY1SPVCABpc4ocLJ9nOBAtsWOCSJVFBAu24aZD6A==",
"version": "0.1.0-prerelease.20190814215530",
"resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20190814215530.tgz",
"integrity": "sha512-6xCCphwTYaBXjlQ7CJjmAnrQOoTLke/da2JAdrSWmGM0WxE+kFAcNpr2DBi7wzPIPTbNuy1nw+ECLb3Bst9LEQ==",
"dev": true
},
"scratch-l10n": {
"version": "3.5.20190807144510",
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.5.20190807144510.tgz",
"integrity": "sha512-rq3G4NZvlFvb0bQFwsqK+anb7S74OSutxqabmaP5R4NRbmkpz0PvbT1zMGKEmEScY+8h5VLkNO6im9RoDsSm7g==",
"version": "3.5.20190813223429",
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.5.20190813223429.tgz",
"integrity": "sha512-rSxUSwv0RgZTXUknAWuc7BFZWewiNhrgyPUMos/qAw4GgVMdY1ZRSIHBEIItpCXXYLOzw4ObcNafIim6Taq9NA==",
"dev": true,
"requires": {
"@babel/cli": "^7.1.2",
@ -14998,13 +15025,15 @@
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
"integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
"dev": true
"dev": true,
"optional": true
},
"braces": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
"integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
"dev": true,
"optional": true,
"requires": {
"arr-flatten": "^1.1.0",
"array-unique": "^0.3.2",
@ -15023,6 +15052,7 @@
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
"dev": true,
"optional": true,
"requires": {
"is-extendable": "^0.1.0"
}
@ -15205,6 +15235,7 @@
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
"integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
"dev": true,
"optional": true,
"requires": {
"extend-shallow": "^2.0.1",
"is-number": "^3.0.0",
@ -15217,6 +15248,7 @@
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
"dev": true,
"optional": true,
"requires": {
"is-extendable": "^0.1.0"
}
@ -15296,7 +15328,8 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
"dev": true
"dev": true,
"optional": true
},
"is-glob": {
"version": "4.0.1",
@ -15313,6 +15346,7 @@
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
"integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
"dev": true,
"optional": true,
"requires": {
"kind-of": "^3.0.2"
},
@ -15322,6 +15356,7 @@
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
"dev": true,
"optional": true,
"requires": {
"is-buffer": "^1.1.5"
}
@ -15332,13 +15367,15 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
"dev": true
"dev": true,
"optional": true
},
"kind-of": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
"integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
"dev": true
"dev": true,
"optional": true
},
"micromatch": {
"version": "3.1.10",

View file

@ -16,7 +16,7 @@
"test:unit:jest": "jest ./test/unit/ && jest ./test/localization/*.test.js",
"test:unit:tap": "tap ./test/{unit-legacy,localization-legacy}/*.js --no-coverage -R classic",
"test:coverage": "tap ./test/{unit-legacy,localization-legacy}/*.js --coverage --coverage-report=lcov",
"build": "npm run clean && npm run translate && webpack --bail",
"build": "npm run clean && npm run translate && NODE_OPTIONS=--max_old_space_size=8000 webpack --bail",
"clean": "rm -rf ./build && rm -rf ./intl && mkdir -p build && mkdir -p intl",
"deploy": "npm run deploy:s3 && npm run deploy:fastly",
"deploy:fastly": "node ./bin/configure-fastly.js",
@ -72,6 +72,7 @@
"copy-webpack-plugin": "0.2.0",
"create-react-class": "15.6.2",
"css-loader": "0.23.1",
"email-validator": "2.0.4",
"enzyme": "3.10.0",
"enzyme-adapter-react-16": "1.14.0",
"eslint": "5.16.0",
@ -123,7 +124,7 @@
"redux": "3.5.2",
"redux-thunk": "2.0.1",
"sass-loader": "6.0.6",
"scratch-gui": "0.1.0-prerelease.20190808151251",
"scratch-gui": "0.1.0-prerelease.20190814215530",
"scratch-l10n": "latest",
"selenium-webdriver": "3.6.0",
"slick-carousel": "1.6.0",

View file

@ -0,0 +1,83 @@
const classNames = require('classnames');
const PropTypes = require('prop-types');
const React = require('react');
import {Field} from 'formik';
require('./formik-checkbox.scss');
require('./formik-forms.scss');
require('../forms/row.scss');
const FormikCheckboxSubComponent = ({
className,
field,
id,
label,
...props
}) => (
<div className="checkbox">
<input
checked={field.value}
className={classNames(
'formik-checkbox',
className
)}
id={id}
name={field.name}
type="checkbox"
value={field.value}
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-label',
'formik-checkbox-label'
)}
htmlFor={id}
>
{label}
</label>
)}
</div>
);
FormikCheckboxSubComponent.propTypes = {
className: PropTypes.string,
field: PropTypes.shape({
name: PropTypes.string,
onBlur: PropTypes.function,
onChange: PropTypes.function,
value: PropTypes.bool
}),
id: PropTypes.string,
label: PropTypes.string
};
const FormikCheckbox = ({
className,
id,
label,
name,
...props
}) => (
<Field
className={className}
component={FormikCheckboxSubComponent}
id={id}
label={label}
name={name}
{...props}
/>
);
FormikCheckbox.propTypes = {
className: PropTypes.string,
id: PropTypes.string,
label: PropTypes.string,
name: PropTypes.string
};
module.exports = FormikCheckbox;

View file

@ -0,0 +1,39 @@
@import "../../colors";
.formik-checkbox-label {
font-weight: 300;
}
input[type="checkbox"].formik-checkbox {
display: block;
float: left;
margin-right: .625rem;
border: 1px solid $active-dark-gray;
border-radius: 3px;
width: 1.25rem;
height: 1.25rem;
appearance: none;
&:focus:checked {
transition: all .5s ease;
outline: none;
box-shadow: 0 0 0 .25rem $ui-blue-25percent;
}
&:focus:not(:checked) {
outline: none;
}
&:checked {
background-color: $ui-blue;
text-align: center;
text-indent: .125rem;
line-height: 1.25rem;
font-size: .75rem;
&:after {
color: $type-white;
content: "\2714";
}
}
}

View file

@ -0,0 +1,3 @@
.formik-label {
font-weight: 500;
}

View file

@ -5,8 +5,8 @@ import {Field} from 'formik';
const ValidationMessage = require('../forms/validation-message.jsx');
require('../forms/input.scss');
require('../forms/row.scss');
require('./formik-input.scss');
const FormikInput = ({
className,
@ -26,6 +26,7 @@ const FormikInput = ({
<Field
className={classNames(
'input',
{fail: error},
className
)}
{...props}

View file

@ -0,0 +1,28 @@
@import "../../colors";
.input {
height: 2.75rem;
border-radius: .5rem;
background-color: $ui-white;
margin-bottom: .5rem;
transition: all .5s ease;
border: 1px solid $active-gray;
padding: 0 1rem;
color: $type-gray;
font-size: .875rem;
&:focus {
box-shadow: 0 0 0 .25rem $ui-blue-25percent;
outline: none;
border: 1px solid $ui-blue;
}
&.fail {
border: 1px solid $ui-orange;
&:focus {
box-shadow: 0 0 0 .25rem $ui-orange-25percent;
outline: none;
}
}
}

View file

@ -5,6 +5,7 @@ import {Field} from 'formik';
const FormikInput = require('./formik-input.jsx');
require('./formik-forms.scss');
require('./formik-radio-button.scss');
require('../forms/row.scss');
@ -34,6 +35,7 @@ const FormikRadioButtonSubComponent = ({
{label && (
<label
className={classNames(
'formik-label',
'formik-radio-label',
labelClassName
)}

View file

@ -1,7 +1,6 @@
@import "../../colors";
.formik-radio-label {
font-weight: 300;
margin-left: 1rem;
}

View file

@ -17,6 +17,7 @@ const FormikSelect = ({
}) => {
const optionsList = options.map((item, index) => (
<option
disabled={item.disabled}
key={index}
value={item.value}
>

View file

@ -0,0 +1,7 @@
@import "../../colors";
@import "../../frameless";
.input::placeholder {
font-style: italic;
color: $type-gray-75percent;
}

View file

@ -0,0 +1,46 @@
const bindAll = require('lodash.bindall');
const PropTypes = require('prop-types');
const React = require('react');
require('./info-button.scss');
class InfoButton extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleHideMessage',
'handleShowMessage'
]);
this.state = {
visible: false
};
}
handleHideMessage () {
this.setState({visible: false});
}
handleShowMessage () {
this.setState({visible: true});
}
render () {
return (
<div
className="info-button"
onClick={this.handleShowMessage}
onMouseOut={this.handleHideMessage}
onMouseOver={this.handleShowMessage}
>
{this.state.visible && (
<div className="info-button-message">
{this.props.message}
</div>
)}
</div>
);
}
}
InfoButton.propTypes = {
message: PropTypes.string
};
module.exports = InfoButton;

View file

@ -0,0 +1,78 @@
@import "../../colors";
@import "../../frameless";
.info-button {
position: relative;
display: inline-block;
width: 1rem;
height: 1rem;
margin-left: .375rem;
border-radius: 50%;
background-color: $ui-blue;
&:after {
position: absolute;
content: "?";
color: $ui-white;
font-family: verdana;
font-weight: 400;
top: -.125rem;
left: .325rem;
font-size: .75rem;
}
}
.info-button-message {
$arrow-border-width: 1rem;
display: block;
position: absolute;
top: 0;
left: 0;
transform: translate(1rem, -1rem);
width: 16.5rem;
min-height: 1rem;
margin-left: $arrow-border-width;
border: 1px solid $active-gray;
border-radius: 5px;
padding: .75rem;
overflow: visible;
background-color: $ui-blue;
color: $type-white;
line-height: 1.25rem;
text-align: left;
font-size: .875rem;
z-index: 1;
&:before {
display: block;
position: absolute;
top: 1rem;
left: -$arrow-border-width / 2;
transform: rotate(45deg);
border-bottom: 1px solid $active-gray;
border-left: 1px solid $active-gray;
border-radius: 5px;
background-color: $ui-blue;
width: $arrow-border-width;
height: $arrow-border-width;
content: "";
}
}
@media #{$intermediate-and-smaller} {
.info-button-message {
position: relative;
transform: none;
margin: inherit;
width: 100%;
height: inherit;
&:before {
display: none;
}
}
}

View file

@ -53,7 +53,7 @@ class BirthDateStep extends React.Component {
}
validateSelect (selection) {
if (selection === 'null') {
return this.props.intl.formatMessage({id: 'form.validationRequired'});
return this.props.intl.formatMessage({id: 'general.required'});
}
return null;
}
@ -88,6 +88,7 @@ class BirthDateStep extends React.Component {
<JoinFlowStep
description={this.props.intl.formatMessage({id: 'registration.private'})}
headerImgSrc="/images/hoc/getting-started.jpg"
infoMessage={this.props.intl.formatMessage({id: 'registration.birthDateStepInfo'})}
title={this.props.intl.formatMessage({id: 'registration.birthDateStepTitle'})}
waiting={isSubmitting}
onSubmit={handleSubmit}

View file

@ -0,0 +1,111 @@
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 countryData = require('../../lib/country-data');
const FormikSelect = require('../../components/formik-forms/formik-select.jsx');
const JoinFlowStep = require('./join-flow-step.jsx');
require('./join-flow-steps.scss');
class CountryStep extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleValidSubmit',
'validateForm',
'validateSelect'
]);
this.countryOptions = [];
}
componentDidMount () {
this.setCountryOptions();
}
setCountryOptions () {
if (this.countryOptions.length === 0) {
this.countryOptions = [...countryData.registrationCountryOptions];
this.countryOptions.unshift({
disabled: true,
label: this.props.intl.formatMessage({id: 'registration.selectCountry'}),
value: 'null'
});
}
}
validateSelect (selection) {
if (selection === 'null') {
return this.props.intl.formatMessage({id: 'general.required'});
}
return null;
}
validateForm () {
return {};
}
handleValidSubmit (formData, formikBag) {
formikBag.setSubmitting(false);
this.props.onNextStep(formData);
}
render () {
this.setCountryOptions();
return (
<Formik
initialValues={{
country: 'null'
}}
validate={this.validateForm}
validateOnBlur={false}
validateOnChange={false}
onSubmit={this.handleValidSubmit}
>
{props => {
const {
errors,
handleSubmit,
isSubmitting
} = props;
return (
<JoinFlowStep
description={this.props.intl.formatMessage({id: 'registration.countryStepDescription'})}
headerImgSrc="/images/hoc/getting-started.jpg"
title={this.props.intl.formatMessage({id: 'registration.countryStepTitle'})}
waiting={isSubmitting}
onSubmit={handleSubmit}
>
<div
className={classNames(
'col-sm-9',
'row'
)}
>
<FormikSelect
className={classNames(
'join-flow-select',
'join-flow-select-country',
{fail: errors.country}
)}
error={errors.country}
id="country"
name="country"
options={this.countryOptions}
validate={this.validateSelect}
validationClassName="validation-full-width-input"
/>
</div>
</JoinFlowStep>
);
}}
</Formik>
);
}
}
CountryStep.propTypes = {
intl: intlShape,
onNextStep: PropTypes.func
};
const IntlCountryStep = injectIntl(CountryStep);
module.exports = IntlCountryStep;

View file

@ -1,10 +1,14 @@
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 emailValidator = require('email-validator');
const FormattedMessage = require('react-intl').FormattedMessage;
const JoinFlowStep = require('./join-flow-step.jsx');
const FormikInput = require('../../components/formik-forms/formik-input.jsx');
require('./join-flow-steps.scss');
@ -13,9 +17,18 @@ class EmailStep extends React.Component {
super(props);
bindAll(this, [
'handleValidSubmit',
'validateEmailIfPresent',
'validateForm'
]);
}
validateEmailIfPresent (email) {
if (!email) return null; // skip validation if email is blank; null indicates valid
const isValidLocally = emailValidator.validate(email);
if (isValidLocally) {
return null; // TODO: validate email address remotely
}
return this.props.intl.formatMessage({id: 'registration.validationEmailInvalid'});
}
validateForm () {
return {};
}
@ -35,17 +48,52 @@ class EmailStep extends React.Component {
>
{props => {
const {
errors,
handleSubmit,
isSubmitting
isSubmitting,
validateField
} = props;
return (
<JoinFlowStep
description={this.props.intl.formatMessage({id: 'registration.emailStepDescription'})}
footerContent={(
<FormattedMessage
id="registration.acceptTermsOfUse"
values={{
touLink: (
<a
className="join-flow-link"
href="/terms_of_use"
target="_blank"
>
<FormattedMessage id="general.termsOfUse" />
</a>
)
}}
/>
)}
headerImgSrc="/images/hoc/getting-started.jpg"
innerContentClassName="modal-inner-content-email"
nextButton={this.props.intl.formatMessage({id: 'registration.createAccount'})}
title={this.props.intl.formatMessage({id: 'registration.emailStepTitle'})}
waiting={isSubmitting}
onSubmit={handleSubmit}
>
<FormikInput
className={classNames(
'join-flow-input',
'join-flow-input-tall',
{fail: errors.email}
)}
error={errors.email}
id="email"
name="email"
placeholder={this.props.intl.formatMessage({id: 'general.emailAddress'})}
validate={this.validateEmailIfPresent}
validationClassName="validation-full-width-input"
onBlur={() => validateField('email')} // eslint-disable-line react/jsx-no-bind
/>
</JoinFlowStep>
);
}}
</Formik>
@ -58,4 +106,5 @@ EmailStep.propTypes = {
onNextStep: PropTypes.func
};
module.exports = injectIntl(EmailStep);

View file

@ -82,6 +82,7 @@ class GenderStep extends React.Component {
<JoinFlowStep
className="join-flow-gender-step"
description={this.props.intl.formatMessage({id: 'registration.genderStepDescription'})}
infoMessage={this.props.intl.formatMessage({id: 'registration.genderStepInfo'})}
title={this.props.intl.formatMessage({id: 'registration.genderStepTitle'})}
waiting={isSubmitting}
onSubmit={handleSubmit}

View file

@ -5,6 +5,7 @@ const PropTypes = require('prop-types');
const NextStepButton = require('./next-step-button.jsx');
const ModalTitle = require('../modal/base/modal-title.jsx');
const ModalInnerContent = require('../modal/base/modal-inner-content.jsx');
const InfoButton = require('../info-button/info-button.jsx');
require('./join-flow-step.scss');
@ -12,7 +13,10 @@ const JoinFlowStep = ({
children,
className,
description,
footerContent,
headerImgSrc,
infoMessage,
innerContentClassName,
nextButton,
onSubmit,
title,
@ -28,7 +32,8 @@ const JoinFlowStep = ({
<ModalInnerContent
className={classNames(
'join-flow-inner-content',
className
className,
innerContentClassName
)}
>
{title && (
@ -40,11 +45,19 @@ const JoinFlowStep = ({
{description && (
<div className="join-flow-description">
{description}
{infoMessage && (
<InfoButton message={infoMessage} />
)}
</div>
)}
{children}
</ModalInnerContent>
</div>
{footerContent && (
<div className="join-flow-footer-message">
{footerContent}
</div>
)}
<NextStepButton
content={nextButton}
waiting={waiting}
@ -56,7 +69,10 @@ JoinFlowStep.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
description: PropTypes.string,
footerContent: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
headerImgSrc: PropTypes.string,
infoMessage: PropTypes.string,
innerContentClassName: PropTypes.string,
nextButton: PropTypes.node,
onSubmit: PropTypes.func,
title: PropTypes.string,

View file

@ -33,3 +33,13 @@
border-top-left-radius: 1rem;
border-top-right-radius: 1rem;
}
.join-flow-footer-message {
width: 100%;
padding: 1.125rem 1.5rem 1.125rem;
background-color: $ui-blue-25percent;
font-size: .75rem;
font-weight: 600;
text-align: center;
color: $ui-blue;
}

View file

@ -13,6 +13,14 @@
}
}
.join-flow-password-confirm {
margin-bottom: .6875rem;
}
.join-flow-input-tall {
height: 3rem;
}
.join-flow-input-title {
font-weight: bold;
margin-bottom: .5rem;
@ -47,12 +55,14 @@
}
}
.select .join-flow-select {
.select .join-flow-select-month {
width: 9.125rem;
margin-right: .5rem;
}
.join-flow-select-month {
margin-right: .5rem;
.select .join-flow-select-country {
width: 100%;
margin: 0 auto;
}
.join-flow-password-section {
@ -91,3 +101,11 @@
height: 2rem;
margin-left: .5rem;
}
.modal-inner-content-email {
padding-top: 2.9rem;
}
a.join-flow-link:link, a.join-flow-link:visited, a.join-flow-link:active {
text-decoration: underline;
}

View file

@ -10,6 +10,7 @@ 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 CountryStep = require('./country-step.jsx');
const EmailStep = require('./email-step.jsx');
const WelcomeStep = require('./welcome-step.jsx');
@ -42,6 +43,7 @@ class JoinFlow extends React.Component {
<UsernameStep onNextStep={this.handleAdvanceStep} />
<BirthDateStep onNextStep={this.handleAdvanceStep} />
<GenderStep onNextStep={this.handleAdvanceStep} />
<CountryStep onNextStep={this.handleAdvanceStep} />
<EmailStep onNextStep={this.handleAdvanceStep} />
<WelcomeStep
email={this.state.formData.email}

View file

@ -7,6 +7,7 @@ const {injectIntl, intlShape} = require('react-intl');
const validate = require('../../lib/validate');
const FormikInput = require('../../components/formik-forms/formik-input.jsx');
const FormikCheckbox = require('../../components/formik-forms/formik-checkbox.jsx');
const JoinFlowStep = require('./join-flow-step.jsx');
require('./join-flow-steps.scss');
@ -26,9 +27,6 @@ class UsernameStep extends React.Component {
'validateUsernameIfPresent',
'validateForm'
]);
this.state = {
showPassword: false
};
}
handleChangeShowPassword () {
this.setState({showPassword: !this.state.showPassword});
@ -36,20 +34,26 @@ class UsernameStep extends React.Component {
// we allow username to be empty on blur, since you might not have typed anything yet
validateUsernameIfPresent (username) {
if (!username) return null; // skip validation if username is blank; null indicates valid
// if username is not blank, run both local and remote validations
const localResult = validate.validateUsernameLocally(username);
if (localResult.valid) {
return validate.validateUsernameRemotely(username).then(
remoteResult => {
if (remoteResult.valid) return null;
// there may be multiple validation errors. Prioritize vulgarity, then
// length, then having invalid chars, then all other remote reports
if (remoteResult.valid === false && remoteResult.errMsgId === 'registration.validationUsernameVulgar') {
return this.props.intl.formatMessage({id: remoteResult.errMsgId});
} else if (localResult.valid === false) {
return this.props.intl.formatMessage({id: localResult.errMsgId});
} else if (remoteResult.valid === false) {
return this.props.intl.formatMessage({id: remoteResult.errMsgId});
}
return null;
}
);
}
return this.props.intl.formatMessage({id: localResult.errMsgId});
}
validatePasswordIfPresent (password) {
validatePasswordIfPresent (password, username) {
if (!password) return null; // skip validation if password is blank; null indicates valid
const localResult = validate.validatePassword(password);
const localResult = validate.validatePassword(password, username);
if (localResult.valid) return null;
return this.props.intl.formatMessage({id: localResult.errMsgId});
}
@ -69,13 +73,10 @@ class UsernameStep extends React.Component {
if (!usernameResult.valid) {
errors.username = this.props.intl.formatMessage({id: usernameResult.errMsgId});
}
const passwordResult = validate.validatePassword(values.password);
const passwordResult = validate.validatePassword(values.password, values.username);
if (!passwordResult.valid) {
errors.password = this.props.intl.formatMessage({id: passwordResult.errMsgId});
}
if (values.password === values.username) {
errors.password = this.props.intl.formatMessage({id: 'registration.validationPasswordNotUsername'});
}
const passwordConfirmResult = validate.validatePasswordConfirm(values.password, values.passwordConfirm);
if (!passwordConfirmResult.valid) {
errors.passwordConfirm = this.props.intl.formatMessage({id: passwordConfirmResult.errMsgId});
@ -85,6 +86,7 @@ class UsernameStep extends React.Component {
// called after all validations pass with no errors
handleValidSubmit (formData, formikBag) {
formikBag.setSubmitting(false); // formik makes us do this ourselves
delete formData.showPassword;
this.props.onNextStep(formData);
}
render () {
@ -93,7 +95,8 @@ class UsernameStep extends React.Component {
initialValues={{
username: '',
password: '',
passwordConfirm: ''
passwordConfirm: '',
showPassword: false
}}
validate={this.validateForm}
validateOnBlur={false}
@ -105,6 +108,8 @@ class UsernameStep extends React.Component {
errors,
handleSubmit,
isSubmitting,
setFieldError,
setFieldValue,
validateField,
values
} = props;
@ -123,15 +128,20 @@ class UsernameStep extends React.Component {
</div>
<FormikInput
className={classNames(
'join-flow-input',
{fail: errors.username}
'join-flow-input'
)}
error={errors.username}
id="username"
name="username"
validate={this.validateUsernameIfPresent}
validationClassName="validation-full-width-input"
onBlur={() => validateField('username')} // eslint-disable-line react/jsx-no-bind
/* eslint-disable react/jsx-no-bind */
onBlur={() => validateField('username')}
onChange={e => {
setFieldValue('username', e.target.value);
setFieldError('username', null);
}}
/* eslint-enable react/jsx-no-bind */
/>
<div className="join-flow-password-section">
<div className="join-flow-input-title">
@ -139,28 +149,32 @@ class UsernameStep extends React.Component {
</div>
<FormikInput
className={classNames(
'join-flow-input',
{fail: errors.password}
'join-flow-input'
)}
error={errors.password}
id="password"
name="password"
type={this.state.showPassword ? 'text' : 'password'}
validate={this.validatePasswordIfPresent}
validationClassName="validation-full-width-input"
type={values.showPassword ? 'text' : 'password'}
/* eslint-disable react/jsx-no-bind */
validate={password => this.validatePasswordIfPresent(password, values.username)}
validationClassName="validation-full-width-input"
onBlur={() => validateField('password')}
onChange={e => {
setFieldValue('password', e.target.value);
setFieldError('password', null);
}}
/* eslint-enable react/jsx-no-bind */
/>
<FormikInput
className={classNames(
'join-flow-input',
'join-flow-password-confirm',
{fail: errors.passwordConfirm}
)}
error={errors.passwordConfirm}
id="passwordConfirm"
name="passwordConfirm"
type={this.state.showPassword ? 'text' : 'password'}
type={values.showPassword ? 'text' : 'password'}
/* eslint-disable react/jsx-no-bind */
validate={() =>
this.validatePasswordConfirmIfPresent(values.password,
@ -170,17 +184,18 @@ class UsernameStep extends React.Component {
onBlur={() =>
validateField('passwordConfirm')
}
onChange={e => {
setFieldValue('passwordConfirm', e.target.value);
setFieldError('passwordConfirm', null);
}}
/* eslint-enable react/jsx-no-bind */
/>
<div className="join-flow-input-title">
<div
onClick={this.handleChangeShowPassword}
>
{/* TODO: should localize 'Hide password' if we use that */}
{this.state.showPassword ? 'Hide password' : (
this.props.intl.formatMessage({id: 'registration.showPassword'})
)}
</div>
<FormikCheckbox
id="showPassword"
label={this.props.intl.formatMessage({id: 'registration.showPassword'})}
name="showPassword"
/>
</div>
</div>
</div>

View file

@ -139,7 +139,7 @@ class UsernameStep extends React.Component {
default:
this.form.formsy.updateInputsWithError({
'user.username': this.props.intl.formatMessage({
id: 'registration.validationUsernameInvalid'
id: 'registration.validationUsernameNotAllowed'
})
});
return callback(false);

View file

@ -2,6 +2,7 @@
"general.accountSettings": "Account settings",
"general.about": "About",
"general.aboutScratch": "About Scratch",
"general.apiError": "Whoops, Scratch had an error.",
"general.back": "Back",
"general.birthMonth": "Birth Month",
"general.birthYear": "Birth Year",
@ -20,7 +21,7 @@
"general.create": "Create",
"general.credits": "Credits",
"general.dmca": "DMCA",
"general.emailAddress": "Email Address",
"general.emailAddress": "Email address",
"general.english": "English",
"general.error": "Oops! Something went wrong",
"general.errorIdentifier": "Your error was logged with id {errorId}",
@ -70,6 +71,7 @@
"general.privacyPolicy": "Privacy Policy",
"general.projects": "Projects",
"general.profile": "Profile",
"general.required": "Required",
"general.resourcesTitle": "Educator Resources",
"general.scratchConference": "Scratch Conference",
"general.scratchEd": "ScratchEd",
@ -141,6 +143,7 @@
"parents.FaqResourcesQ": "What resources are available for learning Scratch?",
"parents.introDescription": "Scratch is a programming language and an online community where children can program and share interactive media such as stories, games, and animation with people from all over the world. As children create with Scratch, they learn to think creatively, work collaboratively, and reason systematically. Scratch is designed and maintained by the Lifelong Kindergarten group at the MIT Media Lab.",
"registration.birthDateStepInfo": "This helps us understand the age range of people who use Scratch. We use this to confirm account ownership if you contact our team. This information will not be made public on your account.",
"registration.birthDateStepTitle": "When were you born?",
"registration.checkOutResources": "Get Started with Resources",
"registration.checkOutResourcesDescription": "Explore materials for educators and facilitators written by the Scratch Team, including <a href='/educators#resources'>tips, tutorials, and guides</a>.",
@ -148,14 +151,18 @@
"registration.choosePasswordStepTitle": "Create a password",
"registration.choosePasswordStepTooltip": "Don't use your name or anything that's easy for someone else to guess.",
"registration.classroomApiGeneralError": "Sorry, we could not find the registration information for this class",
"registration.countryStepDescription": "Well display your country on your profile.",
"registration.countryStepTitle": "What country do you live in?",
"registration.generalError": "Sorry, an unexpected error occurred.",
"registration.classroomInviteExistingStudentStepDescription": "you have been invited to join the class:",
"registration.classroomInviteNewStudentStepDescription": "Your teacher has invited you to join a class:",
"registration.confirmYourEmail": "Confirm Your Email",
"registration.confirmYourEmailDescription": "If you haven't already, please click the link in the confirmation email sent to:",
"registration.createAccount": "Create Account",
"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.genderStepInfo": "This helps us understand who uses Scratch, so that we can broaden participation. This information will not be made public on your account.",
"registration.genderOptionAnother": "Another gender:",
"registration.genderOptionPreferNotToSay": "Prefer not to say",
"registration.emailStepTitle": "What's your email?",
@ -184,18 +191,22 @@
"registration.studentUsernameStepHelpText": "Already have a Scratch account?",
"registration.studentUsernameStepTooltip": "You'll need to create a new Scratch account to join this class.",
"registration.studentUsernameFieldHelpText": "For safety, don't use your real name!",
"registration.acceptTermsOfUse": "By creating an account, I accept and agree to the {touLink}.",
"registration.usernameStepTitle": "Request a Teacher Account",
"registration.usernameStepTitleScratcher": "Create a Scratch Account",
"registration.validationMaxLength": "Sorry, you have exceeded the maximum character limit.",
"registration.validationPasswordLength": "Passwords must be at least six characters",
"registration.validationPasswordNotEquals": "Your password may not be \"password\"",
"registration.validationPasswordNotUsername": "Your password may not be your username",
"registration.validationUsernameRegexp": "Your username may only contain letters, numbers, \"-\", and \"_\"",
"registration.validationUsernameMinLength": "Usernames must be at least 3 characters",
"registration.validationUsernameMaxLength": "Usernames must be at most 20 characters",
"registration.validationUsernameExists": "Sorry, that username already exists",
"registration.validationPasswordConfirmNotEquals": "Passwords dont match",
"registration.validationPasswordLength": "Must be 6 letters or longer",
"registration.validationPasswordNotEquals": "Password is too easy to guess. Try something else?",
"registration.validationPasswordNotUsername": "Password cant match your username",
"registration.validationUsernameRegexp": "Usernames can only use letters, numbers, - and _",
"registration.validationUsernameMinLength": "Must be 3 letters or longer",
"registration.validationUsernameMaxLength": "Must be 20 letters or shorter",
"registration.validationUsernameExists": "Username taken. Try another?",
"registration.validationUsernameNotAllowed": "Username not allowed",
"registration.validationUsernameVulgar": "Hmm, that looks inappropriate",
"registration.validationUsernameInvalid": "Invalid username",
"registration.validationEmailInvalid": "Email doesnt look right. Try another?",
"registration.waitForApproval": "Wait for Approval",
"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:",

View file

@ -3,13 +3,13 @@ const api = require('./api');
module.exports.validateUsernameLocally = username => {
if (!username || username === '') {
return {valid: false, errMsgId: 'form.validationRequired'};
return {valid: false, errMsgId: 'general.required'};
} else if (username.length < 3) {
return {valid: false, errMsgId: 'form.validationUsernameMinLength'};
return {valid: false, errMsgId: 'registration.validationUsernameMinLength'};
} else if (username.length > 20) {
return {valid: false, errMsgId: 'form.validationUsernameMaxLength'};
return {valid: false, errMsgId: 'registration.validationUsernameMaxLength'};
} else if (!/^[\w-]+$/i.test(username)) {
return {valid: false, errMsgId: 'form.validationUsernameRegexp'};
return {valid: false, errMsgId: 'registration.validationUsernameRegexp'};
}
return {valid: true};
};
@ -29,34 +29,41 @@ module.exports.validateUsernameRemotely = username => (
case 'username exists':
resolve({valid: false, errMsgId: 'registration.validationUsernameExists'});
break;
case 'bad username':
resolve({valid: false, errMsgId: 'registration.validationUsernameVulgar'});
case 'bad username': // i.e., vulgar
resolve({valid: false, errMsgId: 'registration.validationUsernameNotAllowed'});
break;
case 'invalid username':
default:
resolve({valid: false, errMsgId: 'registration.validationUsernameInvalid'});
resolve({valid: false, errMsgId: 'registration.validationUsernameNotAllowed'});
}
});
})
);
module.exports.validatePassword = password => {
/**
* Validate password value, optionally also considering username value
* @param {string} password password value to validate
* @param {string} username username value to compare
* @return {object} {valid: boolean, errMsgId: string}
*/
module.exports.validatePassword = (password, username) => {
if (!password) {
return {valid: false, errMsgId: 'form.validationRequired'};
return {valid: false, errMsgId: 'general.required'};
} else if (password.length < 6) {
return {valid: false, errMsgId: 'registration.validationPasswordLength'};
} else if (password === 'password') {
return {valid: false, errMsgId: 'registration.validationPasswordNotEquals'};
} else if (username && password === username) {
return {valid: false, errMsgId: 'registration.validationPasswordNotUsername'};
}
return {valid: true};
};
module.exports.validatePasswordConfirm = (password, passwordConfirm) => {
if (!passwordConfirm) {
return {valid: false, errMsgId: 'form.validationRequired'};
return {valid: false, errMsgId: 'general.required'};
} else if (password !== passwordConfirm) {
// TODO: add a new string for this case
return {valid: false, errMsgId: 'general.error'};
return {valid: false, errMsgId: 'registration.validationPasswordConfirmNotEquals'};
}
return {valid: true};
};

View file

@ -54,8 +54,12 @@ class Comment extends React.Component {
}
handleDelete () {
if (this.props.canDeleteWithoutConfirm) {
this.props.onDelete(this.props.id);
} else {
this.setState({deleting: true});
}
}
handleConfirmDelete () {
this.setState({deleting: false});
@ -267,6 +271,7 @@ Comment.propTypes = {
username: PropTypes.string
}),
canDelete: PropTypes.bool,
canDeleteWithoutConfirm: PropTypes.bool,
canReply: PropTypes.bool,
canReport: PropTypes.bool,
canRestore: PropTypes.bool,

View file

@ -83,6 +83,13 @@
width: calc(100% + 1rem);
height: 100%;
content: "";
/*
Because this :before is absolutely positioned, it will eat clicks
from non-absolute elements after it (like the author link).
Prevent this by explicitly disabling pointer events on this background element.
*/
pointer-events: none;
}
.comment-top-row {

View file

@ -74,6 +74,7 @@ class TopLevelComment extends React.Component {
const {
author,
canDelete,
canDeleteWithoutConfirm,
canReply,
canReport,
canRestore,
@ -103,6 +104,7 @@ class TopLevelComment extends React.Component {
content,
datetimeCreated,
canDelete,
canDeleteWithoutConfirm,
canReply,
canReport,
canRestore,
@ -126,6 +128,7 @@ class TopLevelComment extends React.Component {
<Comment
author={reply.author}
canDelete={canDelete}
canDeleteWithoutConfirm={canDeleteWithoutConfirm}
canReply={canReply}
canReport={canReport}
canRestore={canRestore && parentVisible}
@ -168,6 +171,7 @@ TopLevelComment.propTypes = {
username: PropTypes.string
}),
canDelete: PropTypes.bool,
canDeleteWithoutConfirm: PropTypes.bool,
canReply: PropTypes.bool,
canReport: PropTypes.bool,
canRestore: PropTypes.bool,
@ -190,6 +194,7 @@ TopLevelComment.propTypes = {
};
TopLevelComment.defaultProps = {
canDeleteWithoutConfirm: false,
defaultExpanded: false,
moreRepliesToLoad: false
};

View file

@ -599,6 +599,7 @@ const PreviewPresentation = ({
<TopLevelComment
author={comment.author}
canDelete={canDeleteComments}
canDeleteWithoutConfirm={isAdmin}
canReply={isLoggedIn && projectInfo.comments_allowed && isShared}
canReport={isLoggedIn}
canRestore={canRestoreComments}

View file

@ -0,0 +1,34 @@
import React from 'react';
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
import InfoButton from '../../../src/components/info-button/info-button';
describe('InfoButton', () => {
test('Info button defaults to not visible', () => {
const component = mountWithIntl(
<InfoButton
message="Here is some info about something!"
/>
);
expect(component.find('div.info-button-message').exists()).toEqual(false);
});
test('clicking on info button makes info message visible', () => {
const component = mountWithIntl(
<InfoButton
message="Here is some info about something!"
/>
);
component.find('div.info-button').simulate('click');
expect(component.find('div.info-button-message').exists()).toEqual(true);
});
test('after message is visible, mouseOut makes it vanish', () => {
const component = mountWithIntl(
<InfoButton
message="Here is some info about something!"
/>
);
component.find('div.info-button').simulate('click');
expect(component.find('div.info-button-message').exists()).toEqual(true);
component.find('div.info-button').simulate('mouseOut');
expect(component.find('div.info-button-message').exists()).toEqual(false);
});
});

View file

@ -0,0 +1,64 @@
import React from 'react';
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
import JoinFlowStep from '../../../src/components/join-flow/join-flow-step';
describe('JoinFlowStep', () => {
test('components exist when props present', () => {
const props = {
children: null,
className: 'join-flow-step-class',
description: 'description text',
headerImgSrc: '/image.png',
onSubmit: jest.fn(),
nextButton: 'some stuff',
title: 'join flow step title',
waiting: true
};
const component = mountWithIntl(
<JoinFlowStep
{...props}
/>
);
expect(component.find('div.join-flow-header-image').exists()).toEqual(true);
expect(component.find({src: props.headerImgSrc}).exists()).toEqual(true);
expect(component.find('.join-flow-inner-content').exists()).toEqual(true);
expect(component.find('.join-flow-title').exists()).toEqual(true);
expect(component.find('.join-flow-title').first()
.prop('title')).toEqual(props.title);
expect(component.find('div.join-flow-description').exists()).toEqual(true);
expect(component.find('div.join-flow-description').text()).toEqual(props.description);
expect(component.find('NextStepButton').prop('waiting')).toEqual(true);
expect(component.find('NextStepButton').prop('content')).toEqual(props.nextButton);
component.unmount();
});
test('components do not exist when props not present', () => {
const component = mountWithIntl(
<JoinFlowStep />
);
expect(component.find('div.join-flow-header-image').exists()).toEqual(false);
expect(component.find('.join-flow-inner-content').exists()).toEqual(true);
expect(component.find('.join-flow-title').exists()).toEqual(false);
expect(component.find('div.join-flow-description').exists()).toEqual(false);
expect(component.find('NextStepButton').prop('waiting')).toEqual(false);
component.unmount();
});
test('clicking submit calls passed in function', () => {
const props = {
onSubmit: jest.fn()
};
const component = mountWithIntl(
<JoinFlowStep
{...props}
/>
);
component.find('button[type="submit"]').simulate('submit');
expect(props.onSubmit).toHaveBeenCalled();
component.unmount();
});
});

View file

@ -11,17 +11,17 @@ describe('unit test lib/validate.js', () => {
response = validate.validateUsernameLocally('abc-def-ghi');
expect(response).toEqual({valid: true});
response = validate.validateUsernameLocally('');
expect(response).toEqual({valid: false, errMsgId: 'form.validationRequired'});
expect(response).toEqual({valid: false, errMsgId: 'general.required'});
response = validate.validateUsernameLocally('ab');
expect(response).toEqual({valid: false, errMsgId: 'form.validationUsernameMinLength'});
expect(response).toEqual({valid: false, errMsgId: 'registration.validationUsernameMinLength'});
response = validate.validateUsernameLocally('abcdefghijklmnopqrstu');
expect(response).toEqual({valid: false, errMsgId: 'form.validationUsernameMaxLength'});
expect(response).toEqual({valid: false, errMsgId: 'registration.validationUsernameMaxLength'});
response = validate.validateUsernameLocally('abc def');
expect(response).toEqual({valid: false, errMsgId: 'form.validationUsernameRegexp'});
expect(response).toEqual({valid: false, errMsgId: 'registration.validationUsernameRegexp'});
response = validate.validateUsernameLocally('abc!def');
expect(response).toEqual({valid: false, errMsgId: 'form.validationUsernameRegexp'});
expect(response).toEqual({valid: false, errMsgId: 'registration.validationUsernameRegexp'});
response = validate.validateUsernameLocally('abc😄def');
expect(response).toEqual({valid: false, errMsgId: 'form.validationUsernameRegexp'});
expect(response).toEqual({valid: false, errMsgId: 'registration.validationUsernameRegexp'});
});
test('validate password', () => {
@ -34,11 +34,15 @@ describe('unit test lib/validate.js', () => {
response = validate.validatePassword('passwo');
expect(response).toEqual({valid: true});
response = validate.validatePassword('');
expect(response).toEqual({valid: false, errMsgId: 'form.validationRequired'});
expect(response).toEqual({valid: false, errMsgId: 'general.required'});
response = validate.validatePassword('abcde');
expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordLength'});
response = validate.validatePassword('password');
expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordNotEquals'});
response = validate.validatePassword('abcdefg', 'abcdefg');
expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordNotUsername'});
response = validate.validatePassword('abcdefg', 'abcdefG');
expect(response).toEqual({valid: true});
});
test('validate password confirm', () => {
@ -51,12 +55,12 @@ describe('unit test lib/validate.js', () => {
response = validate.validatePasswordConfirm('passwo', 'passwo');
expect(response).toEqual({valid: true});
response = validate.validatePasswordConfirm('', '');
expect(response).toEqual({valid: false, errMsgId: 'form.validationRequired'});
expect(response).toEqual({valid: false, errMsgId: 'general.required'});
response = validate.validatePasswordConfirm('abcdef', 'abcdefg');
expect(response).toEqual({valid: false, errMsgId: 'general.error'});
expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordConfirmNotEquals'});
response = validate.validatePasswordConfirm('abcdef', '123456');
expect(response).toEqual({valid: false, errMsgId: 'general.error'});
expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordConfirmNotEquals'});
response = validate.validatePasswordConfirm('', 'abcdefg');
expect(response).toEqual({valid: false, errMsgId: 'general.error'});
expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordConfirmNotEquals'});
});
});