Merge pull request #3170 from LLK/develop

Merge develop to release branch
This commit is contained in:
Ray Schamp 2019-07-24 15:52:16 -04:00 committed by GitHub
commit ea3243b8a2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 3738 additions and 487 deletions

View file

@ -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

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

@ -7,5 +7,10 @@ module.exports = {
globals: {
process: true
},
plugins: ['json']
plugins: ['json'],
settings: {
react: {
version: '16.2' // Prevent 16.3 lifecycle method errors
}
}
};

View file

@ -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

View file

@ -5,6 +5,7 @@ const ProjectCard = props => (
<a
className="project-card"
href={props.cardUrl}
rel="noopener noreferrer"
target="_blank"
>
<div className="project-card-image">

View file

@ -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;

View file

@ -24,10 +24,9 @@ const FormikSelect = ({
</option>
));
return (
<div className="col-sm-9 row">
<div className="select">
<Field
className={classNames(
'select',
className
)}
component="select"

View file

@ -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;

View file

@ -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 = {

View 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;

View file

@ -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

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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>
);

View file

@ -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;

View file

@ -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;

View file

@ -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>

View file

@ -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" />

View file

@ -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 = {

View file

@ -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
View file

@ -0,0 +1,5 @@
module.exports = {
env: {
jest: true
}
};

View file

@ -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 |

View 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;

View file

@ -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());
});

View file

@ -1,6 +0,0 @@
{
"dependencies": {
"selenium-webdriver": "3.6.0",
"chromedriver": "2.43.1"
}
}

View file

@ -0,0 +1,5 @@
describe('test jest integration', () => {
test('testing test', () => {
expect('integration').toEqual('integration');
});
});

View file

@ -0,0 +1,5 @@
describe('test jest localization', () => {
test('testing localization test', () => {
expect('localization').toEqual('localization');
});
});

View file

@ -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();
});

View 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'});
});
});