mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-26 17:16:11 -05:00
Merge pull request #3170 from LLK/develop
Merge develop to release branch
This commit is contained in:
commit
ea3243b8a2
55 changed files with 3738 additions and 487 deletions
|
@ -129,11 +129,7 @@ jobs:
|
|||
branch:
|
||||
- master
|
||||
- stage: smoke
|
||||
install:
|
||||
- cd test/integration
|
||||
- npm install
|
||||
- cd -
|
||||
script: npm run test:smoke:sauce
|
||||
script: npm run test:integration:remote
|
||||
stages:
|
||||
- test
|
||||
- name: smoke
|
||||
|
|
35
README.md
35
README.md
|
@ -95,10 +95,43 @@ To stop the process that is making the site available to your web browser (creat
|
|||
|
||||
**NOTE:** Because by default `API_HOST=https://api.scratch.mit.edu`, please be aware that, by default, you will be seeing and interacting with real data on the Scratch website.
|
||||
|
||||
### To Test
|
||||
### Unit Tests
|
||||
To run:
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
This will build the application and run the unit and localization tests. Some of the tests are run using the TAP framework and others run using Jest.
|
||||
|
||||
### Integration tests
|
||||
We are transitioning from using TAP to using Jest as our testing framework so for the time being our tests run using both.
|
||||
|
||||
#### Running the tests
|
||||
|
||||
* By default, tests run against our Staging instance, but you can pass in a different location with the ROOT_URL environment variable (see below) if you want to run the tests against e.g. your local build
|
||||
|
||||
#### Running the tests
|
||||
* Run all tests from the command-line: `$ SMOKE_USERNAME=username SMOKE_PASSWORD=password ROOT_URL=https://scratch.mit.edu npm run test:integration`
|
||||
* To run a single file from the command-line using TAP: `$ SMOKE_USERNAME=username SMOKE_PASSWORD=password ROOT_URL=https://scratch.mit.edu node_modules/.bin/tap ./test/integration-legacy/smoke-testing/filename.js --timeout=3600`
|
||||
* The timeout var is for the length of the entire tap test-suite; if you are getting a timeout error, you may need to adjust this value (some of the Selenium tests take a while to run)
|
||||
* To run a single file from the command-line using Jest: `$ SMOKE_USERNAME=username SMOKE_PASSWORD=password ROOT_URL=https://scratch.mit.edu node_modules/.bin/jest ./test/integration/filename.test.js`
|
||||
|
||||
#### Running Remote tests
|
||||
* TAP tests can be run using Saucelabs, an online service that can test browser/os combinations remotely. Currently all tests are written for use for chrome on mac.
|
||||
* You will need a Saucelabs account in order to use it for testing. To find the Access Key, click your username and select User Settings from the dropdown menu. Near the bottom of the page is your access key that you can copy and use in the command line.
|
||||
* Currently Jest tests will not run with Saucelabs.
|
||||
* To run tests using saucelabs run this command `$ SMOKE_USERNAME=username SMOKE_PASSWORD=password SAUCE_USERNAME=saucelabsUsername SAUCE_ACCESS_KEY=saucelabsAccessKey ROOT_URL=https://scratch.mit.edu npm run test:integration:remote`
|
||||
|
||||
#### Configuration
|
||||
|
||||
| Variable | Default | Description |
|
||||
| --------------------- | --------------------- | --------------------------------------------------------- |
|
||||
| `ROOT_URL` | `scratch.ly` | Location you want to run the tests against |
|
||||
| `SMOKE_USERNAME` | `None` | Username for Scratch user you're signing in with to test |
|
||||
| `SMOKE_PASSWORD` | `None` | Password for Scratch user you're signing in with to test |
|
||||
| `SMOKE_REMOTE` | `false` | Tests with Sauce Labs or not. True if running test:smoke:sauce |
|
||||
| `SMOKE_HEADLESS` | `false` | Run browser in headless mode. Flaky at the moment |
|
||||
| `SAUCE_USERNAME` | `None` | Username for your Sauce Labs account |
|
||||
| `SAUCE_ACCESS_KEY` | `None` | Access Key for Sauce Labs found under User Settings |
|
||||
|
||||
### To Deploy
|
||||
|
||||
|
|
3555
package-lock.json
generated
3555
package-lock.json
generated
File diff suppressed because it is too large
Load diff
30
package.json
30
package.json
|
@ -4,13 +4,18 @@
|
|||
"description": "Standalone WWW client for Scratch",
|
||||
"scripts": {
|
||||
"start": "node ./dev-server/index.js",
|
||||
"test": "npm run test:lint && npm run build && npm run test:tap",
|
||||
"test": "npm run test:lint && npm run build && npm run test:unit",
|
||||
"test:lint": "eslint . --ext .js,.jsx,.json",
|
||||
"test:smoke": "tap ./test/integration/smoke-testing/*.js --timeout=3600 --no-coverage -R classic",
|
||||
"test:smoke:verbose": "tap ./test/integration/smoke-testing/*.js --timeout=3600 --no-coverage -R spec",
|
||||
"test:smoke:sauce": "SMOKE_REMOTE=true tap ./test/integration/smoke-testing/*.js --timeout=60000 --no-coverage -R classic",
|
||||
"test:tap": "tap ./test/{unit,localization}/*.js --no-coverage -R classic",
|
||||
"test:coverage": "tap ./test/{unit,localization}/*.js --coverage --coverage-report=lcov",
|
||||
"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: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",
|
||||
"test:unit": "npm run test:unit:jest && npm run test:unit:tap",
|
||||
"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",
|
||||
"clean": "rm -rf ./build && rm -rf ./intl && mkdir -p build && mkdir -p intl",
|
||||
"deploy": "npm run deploy:s3 && npm run deploy:fastly",
|
||||
|
@ -53,23 +58,24 @@
|
|||
"autoprefixer": "6.3.6",
|
||||
"babel-cli": "6.26.0",
|
||||
"babel-core": "6.23.1",
|
||||
"babel-eslint": "8.0.2",
|
||||
"babel-eslint": "10.0.2",
|
||||
"babel-loader": "7.1.0",
|
||||
"babel-plugin-transform-object-rest-spread": "6.26.0",
|
||||
"babel-preset-es2015": "6.22.0",
|
||||
"babel-preset-react": "6.22.0",
|
||||
"bowser": "1.9.4",
|
||||
"cheerio": "1.0.0-rc.2",
|
||||
"chromedriver": "75.1.0",
|
||||
"classnames": "2.2.5",
|
||||
"cookie": "0.2.2",
|
||||
"copy-webpack-plugin": "0.2.0",
|
||||
"create-react-class": "15.6.2",
|
||||
"css-loader": "0.23.1",
|
||||
"eslint": "4.7.1",
|
||||
"eslint": "5.16.0",
|
||||
"eslint-config-scratch": "5.0.0",
|
||||
"eslint-plugin-cypress": "^2.0.1",
|
||||
"eslint-plugin-json": "1.2.0",
|
||||
"eslint-plugin-react": "7.4.0",
|
||||
"eslint-plugin-json": "1.4.0",
|
||||
"eslint-plugin-react": "7.14.2",
|
||||
"exenv": "1.2.0",
|
||||
"fastly": "1.2.1",
|
||||
"file-loader": "4.0.0",
|
||||
|
@ -81,6 +87,7 @@
|
|||
"google-libphonenumber": "3.2.3",
|
||||
"html-webpack-plugin": "2.22.0",
|
||||
"iso-3166-2": "0.4.0",
|
||||
"jest": "^23.6.0",
|
||||
"json-loader": "0.5.2",
|
||||
"json2po-stream": "1.0.3",
|
||||
"keymirror": "0.1.1",
|
||||
|
@ -113,8 +120,9 @@
|
|||
"redux": "3.5.2",
|
||||
"redux-thunk": "2.0.1",
|
||||
"sass-loader": "6.0.6",
|
||||
"scratch-gui": "0.1.0-prerelease.20190722181739",
|
||||
"scratch-gui": "0.1.0-prerelease.20190724175453",
|
||||
"scratch-l10n": "latest",
|
||||
"selenium-webdriver": "3.6.0",
|
||||
"slick-carousel": "1.6.0",
|
||||
"source-map-support": "0.3.2",
|
||||
"style-loader": "0.12.3",
|
||||
|
|
|
@ -7,5 +7,10 @@ module.exports = {
|
|||
globals: {
|
||||
process: true
|
||||
},
|
||||
plugins: ['json']
|
||||
plugins: ['json'],
|
||||
settings: {
|
||||
react: {
|
||||
version: '16.2' // Prevent 16.3 lifecycle method errors
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -31,6 +31,7 @@ const InstallScratchLink = ({
|
|||
'https://www.microsoft.com/store/productId/9N48XLLCZH0X' :
|
||||
'https://itunes.apple.com/us/app/scratch-link/id1408863490'
|
||||
}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
|
|
|
@ -5,6 +5,7 @@ const ProjectCard = props => (
|
|||
<a
|
||||
className="project-card"
|
||||
href={props.cardUrl}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<div className="project-card-image">
|
||||
|
|
|
@ -11,9 +11,10 @@ require('../forms/row.scss');
|
|||
const FormikInput = ({
|
||||
className,
|
||||
error,
|
||||
validationClassName,
|
||||
...props
|
||||
}) => (
|
||||
<div className="col-sm-9 row">
|
||||
<div className="col-sm-9 row row-with-tooltip">
|
||||
<Field
|
||||
className={classNames(
|
||||
'input',
|
||||
|
@ -22,7 +23,10 @@ const FormikInput = ({
|
|||
{...props}
|
||||
/>
|
||||
{error && (
|
||||
<ValidationMessage message={error} />
|
||||
<ValidationMessage
|
||||
className={validationClassName}
|
||||
message={error}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -31,7 +35,8 @@ const FormikInput = ({
|
|||
FormikInput.propTypes = {
|
||||
className: PropTypes.string,
|
||||
error: PropTypes.string,
|
||||
type: PropTypes.string
|
||||
type: PropTypes.string,
|
||||
validationClassName: PropTypes.string
|
||||
};
|
||||
|
||||
module.exports = FormikInput;
|
||||
|
|
|
@ -24,10 +24,9 @@ const FormikSelect = ({
|
|||
</option>
|
||||
));
|
||||
return (
|
||||
<div className="col-sm-9 row">
|
||||
<div className="select">
|
||||
<Field
|
||||
className={classNames(
|
||||
'select',
|
||||
className
|
||||
)}
|
||||
component="select"
|
||||
|
|
|
@ -21,6 +21,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* allow elements such as validation errors to position relative to this row */
|
||||
.row-with-tooltip {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.row-label {
|
||||
margin-bottom: .75rem;
|
||||
line-height: 1.7rem;
|
||||
|
|
|
@ -53,7 +53,12 @@ const Grid = props => (
|
|||
Grid.propTypes = {
|
||||
className: PropTypes.string,
|
||||
itemType: PropTypes.string,
|
||||
items: PropTypes.arrayOf(PropTypes.object)
|
||||
items: PropTypes.arrayOf(PropTypes.object),
|
||||
showAvatar: PropTypes.bool,
|
||||
showFavorites: PropTypes.bool,
|
||||
showLoves: PropTypes.bool,
|
||||
showRemixes: PropTypes.bool,
|
||||
showViews: PropTypes.bool
|
||||
};
|
||||
|
||||
Grid.defaultProps = {
|
||||
|
|
143
src/components/join-flow/birthdate-step.jsx
Normal file
143
src/components/join-flow/birthdate-step.jsx
Normal file
|
@ -0,0 +1,143 @@
|
|||
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 FormikSelect = require('../../components/formik-forms/formik-select.jsx');
|
||||
const JoinFlowStep = require('./join-flow-step.jsx');
|
||||
|
||||
require('./join-flow-steps.scss');
|
||||
|
||||
const getBirthMonthOptions = intl => ([
|
||||
{value: 'null', label: intl.formatMessage({id: 'general.month'})},
|
||||
{value: '1', label: intl.formatMessage({id: 'general.monthJanuary'})},
|
||||
{value: '2', label: intl.formatMessage({id: 'general.monthFebruary'})},
|
||||
{value: '3', label: intl.formatMessage({id: 'general.monthMarch'})},
|
||||
{value: '4', label: intl.formatMessage({id: 'general.monthApril'})},
|
||||
{value: '5', label: intl.formatMessage({id: 'general.monthMay'})},
|
||||
{value: '6', label: intl.formatMessage({id: 'general.monthJune'})},
|
||||
{value: '7', label: intl.formatMessage({id: 'general.monthJuly'})},
|
||||
{value: '8', label: intl.formatMessage({id: 'general.monthAugust'})},
|
||||
{value: '9', label: intl.formatMessage({id: 'general.monthSeptember'})},
|
||||
{value: '10', label: intl.formatMessage({id: 'general.monthOctober'})},
|
||||
{value: '11', label: intl.formatMessage({id: 'general.monthNovember'})},
|
||||
{value: '12', label: intl.formatMessage({id: 'general.monthDecember'})}
|
||||
]);
|
||||
|
||||
const getBirthYearOptions = intl => {
|
||||
const curYearRaw = (new Date()).getYear();
|
||||
const curYear = curYearRaw + 1900;
|
||||
// including both 1900 and current year, there are (curYearRaw + 1) options.
|
||||
const numYearOptions = curYearRaw + 1;
|
||||
const birthYearOptions = Array(numYearOptions).fill()
|
||||
.map((defaultVal, i) => (
|
||||
{value: String(curYear - i), label: String(curYear - i)}
|
||||
));
|
||||
birthYearOptions.unshift({
|
||||
value: 'null',
|
||||
label: intl.formatMessage({id: 'general.year'})
|
||||
});
|
||||
return birthYearOptions;
|
||||
};
|
||||
|
||||
class BirthDateStep extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleValidSubmit',
|
||||
'validateForm',
|
||||
'validateSelect'
|
||||
]);
|
||||
}
|
||||
validateSelect (selection) {
|
||||
if (selection === 'null') {
|
||||
return this.props.intl.formatMessage({id: 'form.validationRequired'});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
validateForm () {
|
||||
return {};
|
||||
}
|
||||
handleValidSubmit (formData, formikBag) {
|
||||
formikBag.setSubmitting(false);
|
||||
this.props.onNextStep(formData);
|
||||
}
|
||||
render () {
|
||||
const birthMonthOptions = getBirthMonthOptions(this.props.intl);
|
||||
const birthYearOptions = getBirthYearOptions(this.props.intl);
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
birth_month: 'null',
|
||||
birth_year: '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.birthDateStepDescription'})}
|
||||
headerImgSrc="/images/hoc/getting-started.jpg"
|
||||
title={this.props.intl.formatMessage({id: 'general.joinScratch'})}
|
||||
waiting={isSubmitting}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'col-sm-9',
|
||||
'row',
|
||||
'birthdate-select-row'
|
||||
)}
|
||||
>
|
||||
<FormikSelect
|
||||
className={classNames(
|
||||
'join-flow-select',
|
||||
'join-flow-select-month',
|
||||
{fail: errors.birth_month}
|
||||
)}
|
||||
error={errors.birth_month}
|
||||
id="birth_month"
|
||||
name="birth_month"
|
||||
options={birthMonthOptions}
|
||||
validate={this.validateSelect}
|
||||
validationClassName="validation-full-width-input"
|
||||
/>
|
||||
<FormikSelect
|
||||
className={classNames(
|
||||
'join-flow-select',
|
||||
{fail: errors.birth_year}
|
||||
)}
|
||||
error={errors.birth_year}
|
||||
id="birth_year"
|
||||
name="birth_year"
|
||||
options={birthYearOptions}
|
||||
validate={this.validateSelect}
|
||||
validationClassName="validation-full-width-input"
|
||||
/>
|
||||
</div>
|
||||
</JoinFlowStep>
|
||||
);
|
||||
}}
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BirthDateStep.propTypes = {
|
||||
intl: intlShape,
|
||||
onNextStep: PropTypes.func
|
||||
};
|
||||
|
||||
const IntlBirthDateStep = injectIntl(BirthDateStep);
|
||||
|
||||
module.exports = IntlBirthDateStep;
|
|
@ -10,11 +10,17 @@ require('./join-flow-step.scss');
|
|||
const JoinFlowStep = ({
|
||||
children,
|
||||
description,
|
||||
headerImgSrc,
|
||||
onSubmit,
|
||||
title,
|
||||
waiting
|
||||
}) => (
|
||||
<form onSubmit={onSubmit}>
|
||||
{headerImgSrc && (
|
||||
<div className="join-flow-header-image">
|
||||
<img src={headerImgSrc} />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<ModalInnerContent className="join-flow-inner-content">
|
||||
{title && (
|
||||
|
@ -38,6 +44,7 @@ const JoinFlowStep = ({
|
|||
JoinFlowStep.propTypes = {
|
||||
children: PropTypes.node,
|
||||
description: PropTypes.string,
|
||||
headerImgSrc: PropTypes.string,
|
||||
onSubmit: PropTypes.func,
|
||||
title: PropTypes.string,
|
||||
waiting: PropTypes.bool
|
||||
|
|
|
@ -21,3 +21,13 @@
|
|||
padding: 2.3125rem 0 2.5rem;
|
||||
font-size: .875rem;
|
||||
}
|
||||
|
||||
/* overflow will only work if this class is set on parent of img, not img itself */
|
||||
.join-flow-header-image {
|
||||
width: 100%;
|
||||
height: 7.5rem;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
border-top-left-radius: 1rem;
|
||||
border-top-right-radius: 1rem;
|
||||
}
|
||||
|
|
|
@ -22,6 +22,19 @@
|
|||
transform: translate(21.5625rem, 0);
|
||||
}
|
||||
|
||||
select.join-flow-select {
|
||||
width: 9.125rem;
|
||||
}
|
||||
|
||||
.join-flow-select-month {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.join-flow-password-section {
|
||||
margin-top: 1.125rem;
|
||||
}
|
||||
|
||||
.birthdate-select-row {
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,8 @@ const injectIntl = require('../../lib/intl.jsx').injectIntl;
|
|||
const intlShape = require('../../lib/intl.jsx').intlShape;
|
||||
|
||||
const Progression = require('../progression/progression.jsx');
|
||||
const JoinFlowSteps = require('./join-flow-steps.jsx');
|
||||
const UsernameStep = require('./username-step.jsx');
|
||||
const BirthDateStep = require('./birthdate-step.jsx');
|
||||
|
||||
/*
|
||||
eslint-disable react/prefer-stateless-function, react/no-unused-prop-types, no-useless-constructor
|
||||
|
@ -35,9 +36,8 @@ class JoinFlow extends React.Component {
|
|||
return (
|
||||
<React.Fragment>
|
||||
<Progression step={this.state.step}>
|
||||
<JoinFlowSteps.UsernameStep
|
||||
onNextStep={this.handleAdvanceStep}
|
||||
/>
|
||||
<UsernameStep onNextStep={this.handleAdvanceStep} />
|
||||
<BirthDateStep onNextStep={this.handleAdvanceStep} />
|
||||
</Progression>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable react/no-multi-comp */
|
||||
const bindAll = require('lodash.bindall');
|
||||
const classNames = require('classnames');
|
||||
const React = require('react');
|
||||
|
@ -10,6 +9,8 @@ const validate = require('../../lib/validate');
|
|||
const FormikInput = require('../../components/formik-forms/formik-input.jsx');
|
||||
const JoinFlowStep = require('./join-flow-step.jsx');
|
||||
|
||||
require('./join-flow-steps.scss');
|
||||
|
||||
/*
|
||||
* Username step
|
||||
*/
|
||||
|
@ -127,6 +128,7 @@ class UsernameStep extends React.Component {
|
|||
id="username"
|
||||
name="username"
|
||||
validate={this.validateUsernameIfPresent}
|
||||
validationClassName="validation-full-width-input"
|
||||
onBlur={() => validateField('username')} // eslint-disable-line react/jsx-no-bind
|
||||
/>
|
||||
<div className="join-flow-password-section">
|
||||
|
@ -187,11 +189,12 @@ class UsernameStep extends React.Component {
|
|||
);
|
||||
}
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
UsernameStep.propTypes = {
|
||||
intl: intlShape,
|
||||
onNextStep: PropTypes.func
|
||||
};
|
||||
|
||||
module.exports.UsernameStep = injectIntl(UsernameStep);
|
||||
const IntlUsernameStep = injectIntl(UsernameStep);
|
||||
|
||||
module.exports = IntlUsernameStep;
|
|
@ -29,7 +29,7 @@ class Microworld extends React.Component {
|
|||
};
|
||||
}
|
||||
markVideoOpen (key) {
|
||||
/*
|
||||
/*
|
||||
When a video is clicked, mark it as an open video, so the video Modal will open.
|
||||
Key is the number of the video, so distinguish between different videos on the page
|
||||
*/
|
||||
|
@ -88,7 +88,7 @@ class Microworld extends React.Component {
|
|||
}
|
||||
renderEditorWindow () {
|
||||
const projectId = this.props.microworldData.microworld_project_id;
|
||||
|
||||
|
||||
if (!projectId) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -3,15 +3,17 @@
|
|||
|
||||
.mod-addToStudio {
|
||||
min-height: 15rem;
|
||||
max-height: calc(100% - 8rem);
|
||||
max-height: calc(100% - 5rem);
|
||||
|
||||
/* Some value for height must be set for scrolling to work */
|
||||
height: 100%;
|
||||
height: 28rem;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
@media #{$small}, #{$small-height} {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,6 +73,12 @@
|
|||
min-height: 30rem;
|
||||
*/
|
||||
|
||||
/* provides buffer at bottom of list, so that bottommost items
|
||||
are not obscured by gradient overlay */
|
||||
.studio-list-footer-spacer {
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.studio-list-bottom-gradient {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
|
|
|
@ -56,6 +56,7 @@ const AddToStudioModalPresentation = ({
|
|||
<div className="studio-list-container">
|
||||
{studioButtons}
|
||||
</div>
|
||||
<div className="studio-list-footer-spacer" />
|
||||
<div className="studio-list-bottom-gradient" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -56,6 +56,7 @@ const TTTModal = props => (
|
|||
<div className="ttt-item">
|
||||
<a
|
||||
href={props.cardsUrl}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage id="ideas.downloadPDF" />
|
||||
|
@ -72,6 +73,7 @@ const TTTModal = props => (
|
|||
<div className="ttt-item">
|
||||
<a
|
||||
href={props.guideUrl}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage id="ideas.downloadPDF" />
|
||||
|
|
|
@ -51,7 +51,12 @@ const ThumbnailColumn = props => (
|
|||
ThumbnailColumn.propTypes = {
|
||||
className: PropTypes.string,
|
||||
itemType: PropTypes.string,
|
||||
items: PropTypes.arrayOf(PropTypes.object)
|
||||
items: PropTypes.arrayOf(PropTypes.object),
|
||||
showAvatar: PropTypes.bool,
|
||||
showFavorites: PropTypes.bool,
|
||||
showLoves: PropTypes.bool,
|
||||
showRemixes: PropTypes.bool,
|
||||
showViews: PropTypes.bool
|
||||
};
|
||||
|
||||
ThumbnailColumn.defaultProps = {
|
||||
|
|
|
@ -158,6 +158,7 @@ class Ideas extends React.Component {
|
|||
href={this.props.intl.formatMessage({
|
||||
id: 'cards.scratch-cards-allLink'
|
||||
})}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<Button className="ideas-button">
|
||||
|
|
5
test/.eslintrc.js
Normal file
5
test/.eslintrc.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
env: {
|
||||
jest: true
|
||||
}
|
||||
};
|
|
@ -12,10 +12,10 @@
|
|||
* Tests can be run using Saucelabs, an online service that can test browser/os combinations remotely. Currently all tests are written for use for chrome on mac.
|
||||
|
||||
## Using tap
|
||||
* Run all tests in the smoke-testing directory from the command-line: `$ SMOKE_USERNAME=username SMOKE_PASSWORD=password ROOT_URL=https://scratch.mit.edu npm run test:smoke`
|
||||
* Run all tests in the smoke-testing directory from the command-line: `$ SMOKE_USERNAME=username SMOKE_PASSWORD=password ROOT_URL=https://scratch.mit.edu npm run smoke`
|
||||
* To run a single file from the command-line: `$ SMOKE_USERNAME=username SMOKE_PASSWORD=password ROOT_URL=https://scratch.mit.edu node_modules/.bin/tap ./test/integration/smoke-testing/filename.js --timeout=3600`
|
||||
* The timeout var is for the length of the entire tap test-suite; if you are getting a timeout error, you may need to adjust this value (some of the Selenium tests take a while to run)
|
||||
* To run tests using saucelabs run this command `$ SMOKE_USERNAME=username SMOKE_PASSWORD=password SAUCE_USERNAME=saucelabsUsername SAUCE_ACCESS_KEY=saucelabsAccessKey ROOT_URL=https://scratch.mit.edu npm run test:smoke:sauce`
|
||||
* To run tests using saucelabs run this command `$ SMOKE_USERNAME=username SMOKE_PASSWORD=password SAUCE_USERNAME=saucelabsUsername SAUCE_ACCESS_KEY=saucelabsAccessKey ROOT_URL=https://scratch.mit.edu npm run smoke-sauce`
|
||||
|
||||
|
||||
### Configuration
|
||||
|
@ -25,7 +25,7 @@
|
|||
| `ROOT_URL` | `scratch.ly` | Location you want to run the tests against |
|
||||
| `SMOKE_USERNAME` | `None` | Username for Scratch user you're signing in with to test |
|
||||
| `SMOKE_PASSWORD` | `None` | Password for Scratch user you're signing in with to test |
|
||||
| `SMOKE_REMOTE` | `false` | Tests with Sauce Labs or not. True if running test:smoke:sauce |
|
||||
| `SMOKE_REMOTE` | `false` | Tests with Sauce Labs or not. True if running smoke-sauce |
|
||||
| `SMOKE_HEADLESS` | `false` | Run browser in headless mode. Flaky at the moment |
|
||||
| `SAUCE_USERNAME` | `None` | Username for your Sauce Labs account |
|
||||
| `SAUCE_ACCESS_KEY` | `None` | Access Key for Sauce Labs found under User Settings |
|
168
test/integration-legacy/selenium-helpers.js
Normal file
168
test/integration-legacy/selenium-helpers.js
Normal file
|
@ -0,0 +1,168 @@
|
|||
const webdriver = require('selenium-webdriver');
|
||||
const bindAll = require('lodash.bindall');
|
||||
require('chromedriver');
|
||||
|
||||
const headless = process.env.SMOKE_HEADLESS || false;
|
||||
const remote = process.env.SMOKE_REMOTE || false;
|
||||
const ci = process.env.CI || false;
|
||||
const buildID = process.env.TRAVIS_BUILD_NUMBER;
|
||||
const {SAUCE_USERNAME, SAUCE_ACCESS_KEY} = process.env;
|
||||
const {By, Key, until} = webdriver;
|
||||
|
||||
const DEFAULT_TIMEOUT_MILLISECONDS = 20 * 1000;
|
||||
|
||||
class SeleniumHelper {
|
||||
constructor () {
|
||||
bindAll(this, [
|
||||
'buildDriver',
|
||||
'clickButton',
|
||||
'clickCss',
|
||||
'clickText',
|
||||
'clickXpath',
|
||||
'dragFromXpathToXpath',
|
||||
'findByCss',
|
||||
'findByXpath',
|
||||
'findText',
|
||||
'getKey',
|
||||
'getDriver',
|
||||
'getLogs',
|
||||
'getSauceDriver',
|
||||
'urlMatches',
|
||||
'waitUntilGone'
|
||||
]);
|
||||
}
|
||||
buildDriver (name) {
|
||||
if (remote === 'true'){
|
||||
let nameToUse;
|
||||
if (ci === 'true'){
|
||||
nameToUse = 'travis ' + buildID + ' : ' + name;
|
||||
} else {
|
||||
nameToUse = name;
|
||||
}
|
||||
this.driver = this.getSauceDriver(SAUCE_USERNAME, SAUCE_ACCESS_KEY, nameToUse);
|
||||
} else {
|
||||
this.driver = this.getDriver();
|
||||
}
|
||||
return this.driver;
|
||||
}
|
||||
|
||||
getDriver () {
|
||||
const chromeCapabilities = webdriver.Capabilities.chrome();
|
||||
let args = [];
|
||||
if (headless) {
|
||||
args.push('--headless');
|
||||
args.push('window-size=1024,1680');
|
||||
args.push('--no-sandbox');
|
||||
}
|
||||
chromeCapabilities.set('chromeOptions', {args});
|
||||
let driver = new webdriver.Builder()
|
||||
.forBrowser('chrome')
|
||||
.withCapabilities(chromeCapabilities)
|
||||
.build();
|
||||
return driver;
|
||||
}
|
||||
|
||||
getSauceDriver (username, accessKey, name) {
|
||||
// Driver configs can be generated with the Sauce Platform Configurator
|
||||
// https://wiki.saucelabs.com/display/DOCS/Platform+Configurator
|
||||
let driverConfig = {
|
||||
browserName: 'chrome',
|
||||
platform: 'macOS 10.14',
|
||||
version: '75.0'
|
||||
};
|
||||
var driver = new webdriver.Builder()
|
||||
.withCapabilities({
|
||||
browserName: driverConfig.browserName,
|
||||
platform: driverConfig.platform,
|
||||
version: driverConfig.version,
|
||||
username: username,
|
||||
accessKey: accessKey,
|
||||
name: name
|
||||
})
|
||||
.usingServer(`http://${username}:${accessKey
|
||||
}@ondemand.saucelabs.com:80/wd/hub`)
|
||||
.build();
|
||||
return driver;
|
||||
}
|
||||
|
||||
getKey (keyName) {
|
||||
return Key[keyName];
|
||||
}
|
||||
|
||||
findByXpath (xpath, timeoutMessage = `findByXpath timed out for path: ${xpath}`) {
|
||||
return this.driver.wait(until.elementLocated(By.xpath(xpath)), DEFAULT_TIMEOUT_MILLISECONDS, timeoutMessage)
|
||||
.then(el => (
|
||||
this.driver.wait(el.isDisplayed(), DEFAULT_TIMEOUT_MILLISECONDS, `${xpath} is not visible`)
|
||||
.then(() => el)
|
||||
));
|
||||
}
|
||||
|
||||
waitUntilGone (element) {
|
||||
return this.driver.wait(until.stalenessOf(element));
|
||||
}
|
||||
|
||||
clickXpath (xpath) {
|
||||
return this.findByXpath(xpath).then(el => el.click());
|
||||
}
|
||||
|
||||
clickText (text) {
|
||||
return this.clickXpath(`//*[contains(text(), '${text}')]`);
|
||||
}
|
||||
|
||||
findText (text) {
|
||||
return this.driver.wait(until.elementLocated(By.xpath(`//*[contains(text(), '${text}')]`), 5 * 1000));
|
||||
}
|
||||
|
||||
clickButton (text) {
|
||||
return this.clickXpath(`//button[contains(text(), '${text}')]`);
|
||||
}
|
||||
|
||||
findByCss (css) {
|
||||
return this.driver.wait(until.elementLocated(By.css(css), 1000 * 5));
|
||||
}
|
||||
|
||||
clickCss (css) {
|
||||
return this.findByCss(css).then(el => el.click());
|
||||
}
|
||||
|
||||
dragFromXpathToXpath (startXpath, endXpath) {
|
||||
return this.findByXpath(startXpath).then(startEl => {
|
||||
return this.findByXpath(endXpath).then(endEl => {
|
||||
return this.driver.actions()
|
||||
.dragAndDrop(startEl, endEl)
|
||||
.perform();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
urlMatches (regex) {
|
||||
return this.driver.wait(until.urlMatches(regex), 1000 * 5);
|
||||
}
|
||||
|
||||
getLogs (whitelist) {
|
||||
return this.driver.manage()
|
||||
.logs()
|
||||
.get('browser')
|
||||
.then((entries) => {
|
||||
return entries.filter((entry) => {
|
||||
const message = entry.message;
|
||||
for (let i = 0; i < whitelist.length; i++) {
|
||||
if (message.indexOf(whitelist[i]) !== -1) {
|
||||
// eslint-disable-next-line no-console
|
||||
// console.warn('Ignoring whitelisted error: ' + whitelist[i]);
|
||||
return false;
|
||||
} else if (entry.level !== 'SEVERE') {
|
||||
// eslint-disable-next-line no-console
|
||||
// console.warn('Ignoring non-SEVERE entry: ' + message);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = SeleniumHelper;
|
|
@ -33,18 +33,24 @@ tap.tearDown(function () {
|
|||
});
|
||||
|
||||
tap.beforeEach(function () {
|
||||
return driver.get(url);
|
||||
});
|
||||
|
||||
test('Sign in to Scratch using scratchr2 navbar', t => {
|
||||
clickText('Sign in')
|
||||
return driver.get(url)
|
||||
.then(() => 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(() => clickButton('Sign in'));
|
||||
});
|
||||
|
||||
tap.afterEach(function () {
|
||||
return clickXpath('//span[@class="user-name dropdown-toggle"]')
|
||||
.then(() => clickXpath('//li[@id="logout"] '))
|
||||
.then(() => findByXpath('//div[@class="title-banner intro-banner"]'));
|
||||
});
|
||||
|
||||
test('Sign in to Scratch using scratchr2 navbar', t => {
|
||||
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'))
|
||||
|
@ -85,6 +91,7 @@ test('clicking See Inside should take you to the editor', t => {
|
|||
var expectedUrl = '/editor';
|
||||
t.equal(u.substr(-expectedUrl.length), expectedUrl, 'after clicking, the URL should end in #editor');
|
||||
})
|
||||
.then(() => driver.get(url))
|
||||
.then(() => t.end());
|
||||
});
|
||||
|
||||
|
@ -96,6 +103,7 @@ test('clicking a project title should take you to the project page', t => {
|
|||
var expectedUrlRegExp = new RegExp('/projects/.*[0-9].*/?');
|
||||
t.match(u, expectedUrlRegExp, 'after clicking, the URL should end in projects/PROJECT_ID/');
|
||||
})
|
||||
.then(() => driver.get(url))
|
||||
.then(() => t.end());
|
||||
});
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"selenium-webdriver": "3.6.0",
|
||||
"chromedriver": "2.43.1"
|
||||
}
|
||||
}
|
5
test/integration/test-integration.test.js
Normal file
5
test/integration/test-integration.test.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
describe('test jest integration', () => {
|
||||
test('testing test', () => {
|
||||
expect('integration').toEqual('integration');
|
||||
});
|
||||
});
|
5
test/localization/test-localization.test.js
Normal file
5
test/localization/test-localization.test.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
describe('test jest localization', () => {
|
||||
test('testing localization test', () => {
|
||||
expect('localization').toEqual('localization');
|
||||
});
|
||||
});
|
|
@ -1,66 +0,0 @@
|
|||
const tap = require('tap');
|
||||
const validate = require('../../../src/lib/validate');
|
||||
|
||||
tap.tearDown(() => process.nextTick(process.exit));
|
||||
|
||||
tap.test('validate username locally', t => {
|
||||
let response;
|
||||
t.type(validate.validateUsernameLocally, 'function');
|
||||
response = validate.validateUsernameLocally('abc');
|
||||
t.deepEqual(response, {valid: true});
|
||||
response = validate.validateUsernameLocally('abcdefghijklmnopqrst');
|
||||
t.deepEqual(response, {valid: true});
|
||||
response = validate.validateUsernameLocally('abc-def-ghi');
|
||||
t.deepEqual(response, {valid: true});
|
||||
response = validate.validateUsernameLocally('');
|
||||
t.deepEqual(response, {valid: false, errMsgId: 'form.validationRequired'});
|
||||
response = validate.validateUsernameLocally('ab');
|
||||
t.deepEqual(response, {valid: false, errMsgId: 'form.validationUsernameMinLength'});
|
||||
response = validate.validateUsernameLocally('abcdefghijklmnopqrstu');
|
||||
t.deepEqual(response, {valid: false, errMsgId: 'form.validationUsernameMaxLength'});
|
||||
response = validate.validateUsernameLocally('abc def');
|
||||
t.deepEqual(response, {valid: false, errMsgId: 'form.validationUsernameRegexp'});
|
||||
response = validate.validateUsernameLocally('abc!def');
|
||||
t.deepEqual(response, {valid: false, errMsgId: 'form.validationUsernameRegexp'});
|
||||
response = validate.validateUsernameLocally('abc😄def');
|
||||
t.deepEqual(response, {valid: false, errMsgId: 'form.validationUsernameRegexp'});
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('validate password', t => {
|
||||
let response;
|
||||
t.type(validate.validatePassword, 'function');
|
||||
response = validate.validatePassword('abcdef');
|
||||
t.deepEqual(response, {valid: true});
|
||||
response = validate.validatePassword('abcdefghijklmnopqrst');
|
||||
t.deepEqual(response, {valid: true});
|
||||
response = validate.validatePassword('passwo');
|
||||
t.deepEqual(response, {valid: true});
|
||||
response = validate.validatePassword('');
|
||||
t.deepEqual(response, {valid: false, errMsgId: 'form.validationRequired'});
|
||||
response = validate.validatePassword('abcde');
|
||||
t.deepEqual(response, {valid: false, errMsgId: 'registration.validationPasswordLength'});
|
||||
response = validate.validatePassword('password');
|
||||
t.deepEqual(response, {valid: false, errMsgId: 'registration.validationPasswordNotEquals'});
|
||||
t.end();
|
||||
});
|
||||
|
||||
tap.test('validate password confirm', t => {
|
||||
let response;
|
||||
t.type(validate.validatePasswordConfirm, 'function');
|
||||
response = validate.validatePasswordConfirm('abcdef', 'abcdef');
|
||||
t.deepEqual(response, {valid: true});
|
||||
response = validate.validatePasswordConfirm('abcdefghijklmnopqrst', 'abcdefghijklmnopqrst');
|
||||
t.deepEqual(response, {valid: true});
|
||||
response = validate.validatePasswordConfirm('passwo', 'passwo');
|
||||
t.deepEqual(response, {valid: true});
|
||||
response = validate.validatePasswordConfirm('', '');
|
||||
t.deepEqual(response, {valid: false, errMsgId: 'form.validationRequired'});
|
||||
response = validate.validatePasswordConfirm('abcdef', 'abcdefg');
|
||||
t.deepEqual(response, {valid: false, errMsgId: 'general.error'});
|
||||
response = validate.validatePasswordConfirm('abcdef', '123456');
|
||||
t.deepEqual(response, {valid: false, errMsgId: 'general.error'});
|
||||
response = validate.validatePasswordConfirm('', 'abcdefg');
|
||||
t.deepEqual(response, {valid: false, errMsgId: 'general.error'});
|
||||
t.end();
|
||||
});
|
62
test/unit/lib/validate.test.js
Normal file
62
test/unit/lib/validate.test.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
const validate = require('../../../src/lib/validate');
|
||||
|
||||
describe('unit test lib/validate.js', () => {
|
||||
test('validate username locally', () => {
|
||||
let response;
|
||||
expect(typeof validate.validateUsernameLocally).toBe('function');
|
||||
response = validate.validateUsernameLocally('abc');
|
||||
expect(response).toEqual({valid: true});
|
||||
response = validate.validateUsernameLocally('abcdefghijklmnopqrst');
|
||||
expect(response).toEqual({valid: true});
|
||||
response = validate.validateUsernameLocally('abc-def-ghi');
|
||||
expect(response).toEqual({valid: true});
|
||||
response = validate.validateUsernameLocally('');
|
||||
expect(response).toEqual({valid: false, errMsgId: 'form.validationRequired'});
|
||||
response = validate.validateUsernameLocally('ab');
|
||||
expect(response).toEqual({valid: false, errMsgId: 'form.validationUsernameMinLength'});
|
||||
response = validate.validateUsernameLocally('abcdefghijklmnopqrstu');
|
||||
expect(response).toEqual({valid: false, errMsgId: 'form.validationUsernameMaxLength'});
|
||||
response = validate.validateUsernameLocally('abc def');
|
||||
expect(response).toEqual({valid: false, errMsgId: 'form.validationUsernameRegexp'});
|
||||
response = validate.validateUsernameLocally('abc!def');
|
||||
expect(response).toEqual({valid: false, errMsgId: 'form.validationUsernameRegexp'});
|
||||
response = validate.validateUsernameLocally('abc😄def');
|
||||
expect(response).toEqual({valid: false, errMsgId: 'form.validationUsernameRegexp'});
|
||||
});
|
||||
|
||||
test('validate password', () => {
|
||||
let response;
|
||||
expect(typeof validate.validatePassword).toBe('function');
|
||||
response = validate.validatePassword('abcdef');
|
||||
expect(response).toEqual({valid: true});
|
||||
response = validate.validatePassword('abcdefghijklmnopqrst');
|
||||
expect(response).toEqual({valid: true});
|
||||
response = validate.validatePassword('passwo');
|
||||
expect(response).toEqual({valid: true});
|
||||
response = validate.validatePassword('');
|
||||
expect(response).toEqual({valid: false, errMsgId: 'form.validationRequired'});
|
||||
response = validate.validatePassword('abcde');
|
||||
expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordLength'});
|
||||
response = validate.validatePassword('password');
|
||||
expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordNotEquals'});
|
||||
});
|
||||
|
||||
test('validate password confirm', () => {
|
||||
let response;
|
||||
expect(typeof validate.validatePasswordConfirm).toBe('function');
|
||||
response = validate.validatePasswordConfirm('abcdef', 'abcdef');
|
||||
expect(response).toEqual({valid: true});
|
||||
response = validate.validatePasswordConfirm('abcdefghijklmnopqrst', 'abcdefghijklmnopqrst');
|
||||
expect(response).toEqual({valid: true});
|
||||
response = validate.validatePasswordConfirm('passwo', 'passwo');
|
||||
expect(response).toEqual({valid: true});
|
||||
response = validate.validatePasswordConfirm('', '');
|
||||
expect(response).toEqual({valid: false, errMsgId: 'form.validationRequired'});
|
||||
response = validate.validatePasswordConfirm('abcdef', 'abcdefg');
|
||||
expect(response).toEqual({valid: false, errMsgId: 'general.error'});
|
||||
response = validate.validatePasswordConfirm('abcdef', '123456');
|
||||
expect(response).toEqual({valid: false, errMsgId: 'general.error'});
|
||||
response = validate.validatePasswordConfirm('', 'abcdefg');
|
||||
expect(response).toEqual({valid: false, errMsgId: 'general.error'});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue