mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2025-03-14 07:00:25 -04:00
commit
b9a81559d1
50 changed files with 7619 additions and 3459 deletions
|
@ -1,6 +1,6 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- '8'
|
||||
- '12'
|
||||
sudo: required
|
||||
group: deprecated-2017Q4
|
||||
cache:
|
||||
|
@ -48,6 +48,11 @@ env:
|
|||
- PROJECT_HOST_VAR=PROJECT_HOST_$TRAVIS_BRANCH
|
||||
- PROJECT_HOST=${!PROJECT_HOST_VAR}
|
||||
- PROJECT_HOST=${PROJECT_HOST:-$PROJECT_HOST_STAGING}
|
||||
- TEST_PROJECT_ID_master=414835599
|
||||
- TEST_PROJECT_ID_STAGING=1300006196
|
||||
- TEST_PROJECT_ID_VAR=TEST_PROJECT_ID_$TRAVIS_BRANCH
|
||||
- TEST_PROJECT_ID=${!TEST_PROJECT_ID_VAR}
|
||||
- TEST_PROJECT_ID=${TEST_PROJECT_ID:-$TEST_PROJECT_ID_STAGING}
|
||||
- STATIC_HOST_master=https://cdn2.scratch.mit.edu
|
||||
- STATIC_HOST_STAGING=https://cdn.scratch.ly
|
||||
- STATIC_HOST_VAR=STATIC_HOST_$TRAVIS_BRANCH
|
||||
|
@ -141,6 +146,6 @@ stages:
|
|||
- name: test
|
||||
if: type != cron
|
||||
- name: smoke
|
||||
if: type NOT IN (cron, pull_request)
|
||||
if: type NOT IN (cron, pull_request) AND (branch =~ /^(develop|master|release\/|hotfix\/)/)
|
||||
- name: update translations
|
||||
if: branch == develop AND type == cron
|
||||
|
|
|
@ -215,9 +215,13 @@ so for the time being our tests run using both.
|
|||
To run all integration tests from the command-line:
|
||||
|
||||
```bash
|
||||
SMOKE_USERNAME=username SMOKE_PASSWORD=password ROOT_URL=https://scratch.mit.edu npm run test:integration
|
||||
SMOKE_USERNAME=username SMOKE_PASSWORD=password ROOT_URL=https://scratch.mit.edu TEST_PROJECT_ID=1 npm run test:integration
|
||||
```
|
||||
|
||||
Both the TAP and Jest tests use the same username and password. The Jest tests will also use the the username you give with a 1 (soon to be higher numbers as well) appended to the end of it. So if you use the username "test" it will also use the username "test1." Make sure you have created accounts with this pattern and use the same password for all accounts involved.
|
||||
|
||||
The project page tests require a project id included as an environment variable to pass. The project must be shared and must have at least one remix. At this time, the project does not need to be owned by one of the test users, but that is likely to change.
|
||||
|
||||
To run a single file from the command-line using Jest:
|
||||
|
||||
```bash
|
||||
|
|
9045
package-lock.json
generated
9045
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -68,7 +68,7 @@
|
|||
"babel-preset-react": "6.22.0",
|
||||
"bowser": "1.9.4",
|
||||
"cheerio": "1.0.0-rc.2",
|
||||
"chromedriver": "76.0.0",
|
||||
"chromedriver": "86.0.0",
|
||||
"classnames": "2.2.5",
|
||||
"cookie": "0.4.1",
|
||||
"copy-webpack-plugin": "0.2.0",
|
||||
|
@ -108,7 +108,7 @@
|
|||
"lodash.uniqby": "4.7.0",
|
||||
"minilog": "2.0.8",
|
||||
"node-dir": "0.1.16",
|
||||
"node-sass": "4.6.1",
|
||||
"node-sass": "4.14.1",
|
||||
"pako": "0.2.8",
|
||||
"po2icu": "0.0.2",
|
||||
"postcss-loader": "2.0.10",
|
||||
|
@ -128,13 +128,13 @@
|
|||
"redux-mock-store": "^1.2.3",
|
||||
"redux-thunk": "2.0.1",
|
||||
"sass-loader": "6.0.6",
|
||||
"scratch-gui": "0.1.0-prerelease.20200625213046",
|
||||
"scratch-gui": "0.1.0-prerelease.20201021170733",
|
||||
"scratch-l10n": "latest",
|
||||
"selenium-webdriver": "3.6.0",
|
||||
"slick-carousel": "1.6.0",
|
||||
"source-map-support": "0.3.2",
|
||||
"style-loader": "0.12.3",
|
||||
"tap": "14.2.0",
|
||||
"tap": "14.10.8",
|
||||
"url-loader": "2.3.0",
|
||||
"watch": "0.16.0",
|
||||
"webpack": "2.7.0",
|
||||
|
|
|
@ -48,20 +48,20 @@ $mobile: 480px;
|
|||
* ... intermediate-and-smaller |
|
||||
*/
|
||||
|
||||
$small: "only screen and (max-width : #{$mobile}-1)";
|
||||
$medium: "only screen and (min-width : #{$mobile}) and (max-width : #{$tabletPortrait}-1)";
|
||||
$intermediate: "only screen and (min-width : #{$tabletPortrait}) and (max-width : #{$desktop}-1)";
|
||||
$small: "only screen and (max-width : #{$mobile - 1})";
|
||||
$medium: "only screen and (min-width : #{$mobile}) and (max-width : #{$tabletPortrait - 1})";
|
||||
$intermediate: "only screen and (min-width : #{$tabletPortrait}) and (max-width : #{$desktop - 1})";
|
||||
$big: "only screen and (min-width : #{$desktop})";
|
||||
|
||||
$medium-and-smaller: "only screen and (max-width : #{$tabletPortrait}-1)";
|
||||
$intermediate-and-smaller: "only screen and (max-width : #{$desktop}-1)";
|
||||
$medium-and-smaller: "only screen and (max-width : #{$tabletPortrait - 1})";
|
||||
$intermediate-and-smaller: "only screen and (max-width : #{$desktop - 1})";
|
||||
|
||||
$medium-and-intermediate: "only screen and (min-width : #{$mobile}) and (max-width : #{$desktop}-1)";
|
||||
$medium-and-intermediate: "only screen and (min-width : #{$mobile}) and (max-width : #{$desktop - 1})";
|
||||
|
||||
/* Height */
|
||||
|
||||
$small-height: "only screen and (max-height : #{$mobile} - 1)";
|
||||
$medium-height: "only screen and (min-height : #{$mobile}) and (max-height : #{$tabletPortrait} - 1)";
|
||||
$small-height: "only screen and (max-height : #{$mobile - 1})";
|
||||
$medium-height: "only screen and (min-height : #{$mobile}) and (max-height : #{$tabletPortrait - 1})";
|
||||
|
||||
|
||||
//
|
||||
|
|
31
src/components/commenting-status/commenting-status.jsx
Normal file
31
src/components/commenting-status/commenting-status.jsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
const classNames = require('classnames');
|
||||
const PropTypes = require('prop-types');
|
||||
const FlexRow = require('../../components/flex-row/flex-row.jsx');
|
||||
const React = require('react');
|
||||
|
||||
|
||||
require('./commenting-status.scss');
|
||||
|
||||
const CommentingStatus = props => (
|
||||
<div className={classNames('commenting-status', props.className)}>
|
||||
<div className={classNames('commenting-status-inner-content', props.innerClassName)}>
|
||||
<FlexRow className="comment-status-img">
|
||||
<img
|
||||
className="comment-status-icon"
|
||||
src="/svgs/project/comment-status.svg"
|
||||
/>
|
||||
</FlexRow>
|
||||
<FlexRow>
|
||||
{props.children}
|
||||
</FlexRow>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
CommentingStatus.propTypes = {
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
innerClassName: PropTypes.string
|
||||
};
|
||||
|
||||
module.exports = CommentingStatus;
|
23
src/components/commenting-status/commenting-status.scss
Normal file
23
src/components/commenting-status/commenting-status.scss
Normal file
|
@ -0,0 +1,23 @@
|
|||
@import "../../colors";
|
||||
|
||||
.commenting-status {
|
||||
border: 1px solid $ui-blue-10percent;
|
||||
border-radius: 8px;
|
||||
padding: 1.75rem 3rem 2rem;
|
||||
margin: .5rem 0 2.25rem;
|
||||
background-color: $ui-blue-10percent;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
.bottom-text {
|
||||
font-size: .875rem;
|
||||
}
|
||||
.status-icon-class {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
|
@ -2,12 +2,15 @@ const FormattedMessage = require('react-intl').FormattedMessage;
|
|||
const injectIntl = require('react-intl').injectIntl;
|
||||
const intlShape = require('react-intl').intlShape;
|
||||
const MediaQuery = require('react-responsive').default;
|
||||
const connect = require('react-redux').connect;
|
||||
const PropTypes = require('prop-types');
|
||||
const React = require('react');
|
||||
|
||||
const FooterBox = require('../container/footer.jsx');
|
||||
const LanguageChooser = require('../../languagechooser/languagechooser.jsx');
|
||||
|
||||
const frameless = require('../../../lib/frameless');
|
||||
const getScratchWikiLink = require('../../../lib/scratch-wiki');
|
||||
|
||||
require('./footer.scss');
|
||||
|
||||
|
@ -108,7 +111,7 @@ const Footer = props => (
|
|||
</a>
|
||||
</dd>
|
||||
<dd>
|
||||
<a href="https://en.scratch-wiki.info/">
|
||||
<a href={props.scratchWikiLink}>
|
||||
<FormattedMessage id="general.wiki" />
|
||||
</a>
|
||||
</dd>
|
||||
|
@ -213,7 +216,13 @@ const Footer = props => (
|
|||
);
|
||||
|
||||
Footer.propTypes = {
|
||||
intl: intlShape.isRequired
|
||||
intl: intlShape.isRequired,
|
||||
scratchWikiLink: PropTypes.string
|
||||
};
|
||||
|
||||
module.exports = injectIntl(Footer);
|
||||
const mapStateToProps = (state, ownProps) => ({
|
||||
scratchWikiLink: getScratchWikiLink(ownProps.intl.locale)
|
||||
});
|
||||
|
||||
const ConnectedFooter = connect(mapStateToProps)(Footer);
|
||||
module.exports = injectIntl(ConnectedFooter);
|
||||
|
|
|
@ -13,8 +13,8 @@ const Step = require('../../components/steps/step.jsx');
|
|||
require('./install-scratch.scss');
|
||||
|
||||
const downloadUrls = {
|
||||
mac: 'https://downloads.scratch.mit.edu/desktop/Scratch%20Desktop-3.11.1.dmg',
|
||||
win: 'https://downloads.scratch.mit.edu/desktop/Scratch%20Desktop%20Setup%203.11.1.exe',
|
||||
mac: 'https://downloads.scratch.mit.edu/desktop/Scratch.dmg',
|
||||
win: 'https://downloads.scratch.mit.edu/desktop/Scratch%20Setup.exe',
|
||||
googlePlayStore: 'https://play.google.com/store/apps/details?id=org.scratch',
|
||||
microsoftStore: 'https://www.microsoft.com/store/apps/9pfgj25jl6x3?cid=storebadge&ocid=badge',
|
||||
macAppStore: 'https://apps.apple.com/us/app/scratch-desktop/id1446785996?mt=12'
|
||||
|
@ -114,7 +114,7 @@ const InstallScratch = ({
|
|||
<span className="step-description">
|
||||
{currentOS === OS_ENUM.WINDOWS ?
|
||||
<FormattedMessage id="download.winMoveToApplications" /> :
|
||||
<FormattedMessage id="download.macMoveToApplications" />
|
||||
<FormattedMessage id="download.macMoveAppToApplications" />
|
||||
}
|
||||
</span>
|
||||
|
||||
|
|
115
src/components/modal/mute/modal.jsx
Normal file
115
src/components/modal/mute/modal.jsx
Normal file
|
@ -0,0 +1,115 @@
|
|||
const bindAll = require('lodash.bindall');
|
||||
const PropTypes = require('prop-types');
|
||||
const React = require('react');
|
||||
const Modal = require('../base/modal.jsx');
|
||||
const ModalInnerContent = require('../base/modal-inner-content.jsx');
|
||||
const Button = require('../../forms/button.jsx');
|
||||
const Progression = require('../../progression/progression.jsx');
|
||||
const FlexRow = require('../../flex-row/flex-row.jsx');
|
||||
const MuteStep = require('./mute-step.jsx');
|
||||
const classNames = require('classnames');
|
||||
require('./modal.scss');
|
||||
|
||||
class MuteModal extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleNext',
|
||||
'handlePrevious'
|
||||
]);
|
||||
this.state = {
|
||||
step: 0
|
||||
};
|
||||
}
|
||||
handleNext () {
|
||||
this.setState({
|
||||
step: this.state.step + 1
|
||||
});
|
||||
}
|
||||
handlePrevious () {
|
||||
// This shouldn't get called when we're on the first step, but
|
||||
// the Math.max is here as a safeguard so state doesn't go negative.
|
||||
this.setState({
|
||||
step: Math.max(0, this.state.step - 1)
|
||||
});
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<Modal
|
||||
isOpen
|
||||
useStandardSizes
|
||||
className="modal-mute"
|
||||
showCloseButton={false}
|
||||
onRequestClose={this.props.onRequestClose}
|
||||
>
|
||||
<div className="mute-modal-header modal-header" />
|
||||
<ModalInnerContent className="mute-inner-content">
|
||||
<Progression step={this.state.step}>
|
||||
<MuteStep
|
||||
bottomImg="/images/bottom_placeholder.png"
|
||||
bottomImgClass="bottom-img"
|
||||
header="The Scratch comment filter thinks your comment was unconstructive."
|
||||
>
|
||||
<p>
|
||||
If you think something could be better, you can say something you like about the project,
|
||||
and make a suggestion about how to improve it. For example, you could say:
|
||||
</p>
|
||||
</MuteStep>
|
||||
<MuteStep
|
||||
header="For the next X minutes you won't be able to post comments"
|
||||
sideImg="/images/side_placeholder.png"
|
||||
sideImgClass="side-img"
|
||||
>
|
||||
<p>
|
||||
Once X minutes have passed, you will be able to comment again.
|
||||
</p>
|
||||
<p>
|
||||
If you would like more information, you can read the Scratch community guidelines.
|
||||
</p>
|
||||
</MuteStep>
|
||||
</Progression>
|
||||
<FlexRow className={classNames('nav-divider')} />
|
||||
<FlexRow className={classNames('mute-nav')}>
|
||||
{this.state.step > 0 ? (
|
||||
<Button
|
||||
className={classNames(
|
||||
'back-button',
|
||||
)}
|
||||
onClick={this.handlePrevious}
|
||||
>
|
||||
<div className="action-button-text">
|
||||
Back
|
||||
</div>
|
||||
</Button>
|
||||
) : null }
|
||||
{this.state.step >= 1 ? (
|
||||
<Button
|
||||
className={classNames('close-button')}
|
||||
onClick={this.props.onRequestClose}
|
||||
>
|
||||
<div className="action-button-text">
|
||||
Close
|
||||
</div>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className={classNames('next-button')}
|
||||
onClick={this.handleNext}
|
||||
>
|
||||
<div className="action-button-text">
|
||||
Next
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
</FlexRow>
|
||||
</ModalInnerContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MuteModal.propTypes = {
|
||||
onRequestClose: PropTypes.func
|
||||
};
|
||||
|
||||
module.exports = MuteModal;
|
60
src/components/modal/mute/modal.scss
Normal file
60
src/components/modal/mute/modal.scss
Normal file
|
@ -0,0 +1,60 @@
|
|||
@import "../../../colors";
|
||||
@import "../../../frameless";
|
||||
|
||||
.modal-mute {
|
||||
width: 30rem;
|
||||
|
||||
.mute-modal-header {
|
||||
box-shadow: inset 0 -1px 0 0 $ui-mint-green;
|
||||
background-color: $ui-mint-green;
|
||||
border-radius: 1rem 1rem 0 0;
|
||||
}
|
||||
.mute-step {
|
||||
display: flex;
|
||||
padding: 48px 16px;
|
||||
}
|
||||
.mute-content {
|
||||
padding-top: 16px;
|
||||
}
|
||||
.mute-inner-content {
|
||||
padding: 0 32px;
|
||||
}
|
||||
.left-column {
|
||||
padding-right: 32px;
|
||||
}
|
||||
.mute-header {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
line-height: 2rem;
|
||||
}
|
||||
.mute-bottom-row {
|
||||
padding-top: 32px;
|
||||
}
|
||||
.bottom-img {
|
||||
width: 100%;
|
||||
}
|
||||
.mute-side-image {
|
||||
margin-left: -49px;
|
||||
}
|
||||
.side-img {
|
||||
height: 212px;
|
||||
width: 129px;
|
||||
}
|
||||
.nav-divider {
|
||||
border-top: 1px solid $ui-blue-25percent;
|
||||
}
|
||||
.mute-nav {
|
||||
display:flex;
|
||||
justify-content: space-between;
|
||||
padding: 24px 0;
|
||||
}
|
||||
.back-button {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.next-button, .close-button {
|
||||
margin-left: auto;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
55
src/components/modal/mute/mute-step.jsx
Normal file
55
src/components/modal/mute/mute-step.jsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
const PropTypes = require('prop-types');
|
||||
const React = require('react');
|
||||
const classNames = require('classnames');
|
||||
|
||||
const FlexRow = require('../../flex-row/flex-row.jsx');
|
||||
require('./modal.scss');
|
||||
|
||||
const MuteStep = ({
|
||||
bottomImg,
|
||||
bottomImgClass,
|
||||
children,
|
||||
header,
|
||||
sideImg,
|
||||
sideImgClass
|
||||
}) => (
|
||||
<div className="mute-step">
|
||||
{sideImg &&
|
||||
<FlexRow className={classNames('left-column')}>
|
||||
<div className={classNames('mute-side-image')}>
|
||||
<img
|
||||
className={sideImgClass}
|
||||
src={sideImg}
|
||||
/>
|
||||
</div>
|
||||
</FlexRow>
|
||||
}
|
||||
<FlexRow className={classNames('mute-right-column')}>
|
||||
<FlexRow className={classNames('mute-header')}>
|
||||
{header}
|
||||
</FlexRow>
|
||||
<FlexRow className={classNames('mute-content')}>
|
||||
{children}
|
||||
</FlexRow>
|
||||
<FlexRow className={classNames('mute-bottom-row')}>
|
||||
{bottomImg &&
|
||||
<img
|
||||
className={bottomImgClass}
|
||||
src={bottomImg}
|
||||
/>
|
||||
}
|
||||
</FlexRow>
|
||||
</FlexRow>
|
||||
</div>
|
||||
);
|
||||
|
||||
MuteStep.propTypes = {
|
||||
bottomImg: PropTypes.string,
|
||||
bottomImgClass: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
header: PropTypes.string,
|
||||
sideImg: PropTypes.string,
|
||||
sideImgClass: PropTypes.string
|
||||
};
|
||||
|
||||
module.exports = MuteStep;
|
|
@ -27,56 +27,62 @@ class Navigation extends React.Component {
|
|||
super(props);
|
||||
bindAll(this, [
|
||||
'getProfileUrl',
|
||||
'handleSearchSubmit'
|
||||
'handleSearchSubmit',
|
||||
'pollForMessages'
|
||||
]);
|
||||
this.state = {
|
||||
messageCountIntervalId: -1 // javascript method interval id for getting messsage count.
|
||||
};
|
||||
// Keep the timeout id so we can cancel it (e.g. when we unmount)
|
||||
this.messageCountTimeoutId = -1;
|
||||
}
|
||||
componentDidMount () {
|
||||
if (this.props.user) {
|
||||
const intervalId = setInterval(() => {
|
||||
this.props.getMessageCount(this.props.user.username);
|
||||
}, 120000); // check for new messages every 2 mins.
|
||||
this.setState({ // eslint-disable-line react/no-did-mount-set-state
|
||||
messageCountIntervalId: intervalId
|
||||
});
|
||||
// Setup polling for messages to start in 2 minutes.
|
||||
const twoMinInMs = 2 * 60 * 1000;
|
||||
this.messageCountTimeoutId = setTimeout(this.pollForMessages.bind(this, twoMinInMs), twoMinInMs);
|
||||
}
|
||||
}
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.user !== this.props.user) {
|
||||
this.props.handleCloseAccountNav();
|
||||
if (this.props.user) {
|
||||
const intervalId = setInterval(() => {
|
||||
this.props.getMessageCount(this.props.user.username);
|
||||
}, 120000); // check for new messages every 2 mins.
|
||||
this.setState({ // eslint-disable-line react/no-did-update-set-state
|
||||
messageCountIntervalId: intervalId
|
||||
});
|
||||
const twoMinInMs = 2 * 60 * 1000;
|
||||
this.messageCountTimeoutId = setTimeout(this.pollForMessages.bind(this, twoMinInMs), twoMinInMs);
|
||||
} else {
|
||||
// clear message count check, and set to default id.
|
||||
clearInterval(this.state.messageCountIntervalId);
|
||||
// Clear message count check, and set to default id.
|
||||
if (this.messageCountTimeoutId !== -1) {
|
||||
clearTimeout(this.messageCountTimeoutId);
|
||||
}
|
||||
this.props.setMessageCount(0);
|
||||
this.setState({ // eslint-disable-line react/no-did-update-set-state
|
||||
messageCountIntervalId: -1
|
||||
});
|
||||
this.messageCountTimeoutId = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
componentWillUnmount () {
|
||||
// clear message interval if it exists
|
||||
if (this.state.messageCountIntervalId !== -1) {
|
||||
clearInterval(this.state.messageCountIntervalId);
|
||||
if (this.messageCountTimeoutId !== -1) {
|
||||
clearTimeout(this.messageCountTimeoutId);
|
||||
this.props.setMessageCount(0);
|
||||
this.setState({
|
||||
messageCountIntervalId: -1
|
||||
});
|
||||
this.messageCountTimeoutId = -1;
|
||||
}
|
||||
}
|
||||
getProfileUrl () {
|
||||
if (!this.props.user) return;
|
||||
return `/users/${this.props.user.username}/`;
|
||||
}
|
||||
|
||||
pollForMessages (ms) {
|
||||
this.props.getMessageCount(this.props.user.username);
|
||||
// We only poll if it has been less than 32 minutes.
|
||||
// Chances of someone actively using the page for that long without
|
||||
// a navigation is low.
|
||||
if (ms < 32 * 60 * 1000) { // 32 minutes
|
||||
const nextFetch = ms * 2; // exponentially back off next fetch time.
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.pollForMessages(nextFetch);
|
||||
}, nextFetch);
|
||||
this.messageCountTimeoutId = timeoutId;
|
||||
}
|
||||
}
|
||||
|
||||
handleSearchSubmit (formData) {
|
||||
let targetUrl = '/search/projects';
|
||||
if (formData.q) {
|
||||
|
|
46
src/components/people-grid/people-grid.jsx
Normal file
46
src/components/people-grid/people-grid.jsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
const PropTypes = require('prop-types');
|
||||
const React = require('react');
|
||||
const Avatar = require('../../components/avatar/avatar.jsx');
|
||||
|
||||
require('./people-grid.scss');
|
||||
|
||||
const PeopleGrid = props => (
|
||||
<ul className="avatar-grid">
|
||||
{props.people.map((person, index) => (
|
||||
<li
|
||||
className="avatar-item"
|
||||
key={`person-${index}`}
|
||||
>
|
||||
<div>
|
||||
{person.userName ? (
|
||||
<a href={`https://scratch.mit.edu/users/${person.userName}/`}>
|
||||
<Avatar
|
||||
alt=""
|
||||
src={`https://cdn.scratch.mit.edu/get_image/user/${person.userId || 'default'}_80x80.png`}
|
||||
/>
|
||||
</a>
|
||||
) : (
|
||||
/* if userName is not given, there's no chance userId is given */
|
||||
<Avatar
|
||||
alt=""
|
||||
src={`https://cdn.scratch.mit.edu/get_image/user/default_80x80.png`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<span className="avatar-text">
|
||||
{person.name}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
||||
PeopleGrid.propTypes = {
|
||||
people: PropTypes.arrayOf(PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
userId: PropTypes.number,
|
||||
userName: PropTypes.string
|
||||
}))
|
||||
};
|
||||
|
||||
module.exports = PeopleGrid;
|
36
src/components/people-grid/people-grid.scss
Normal file
36
src/components/people-grid/people-grid.scss
Normal file
|
@ -0,0 +1,36 @@
|
|||
@import "../../colors";
|
||||
@import "../../frameless";
|
||||
|
||||
.avatar-grid {
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
max-width: 864px;
|
||||
list-style: none;
|
||||
flex-wrap: wrap;
|
||||
flex-flow: row wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.avatar-item {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding-bottom: 32px;
|
||||
text-align: center;
|
||||
line-height: 1.25rem;
|
||||
|
||||
img {
|
||||
$img-border: rgba(0, 0, 0, .05);
|
||||
border: 2px solid $img-border;
|
||||
border-radius: 8px;
|
||||
background-color: $ui-white;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
display: inline-block;
|
||||
width: 144px;
|
||||
font-size: .875rem;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
|
@ -180,7 +180,7 @@ class UsernameStep extends React.Component {
|
|||
this.props.description
|
||||
) : (
|
||||
<span>
|
||||
<intl.FormattedMessage id="registration.usernameStepDescription" />
|
||||
<intl.FormattedMessage id="registration.usernameStepDescription" />
|
||||
<b>
|
||||
<intl.FormattedMessage id="registration.usernameStepRealName" />
|
||||
</b>
|
||||
|
|
|
@ -14,8 +14,6 @@
|
|||
|
||||
.thumbnail-title,
|
||||
#{$extras} {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.2em;
|
||||
white-space: nowrap;
|
||||
word-wrap: break-word;
|
||||
|
@ -28,10 +26,16 @@
|
|||
|
||||
a {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
#{$extras} {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
color: $type-gray;
|
||||
font-size: .8462em;
|
||||
|
||||
|
|
|
@ -35,6 +35,12 @@
|
|||
content: "";
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
max-width: 133px;
|
||||
max-height: 100px;
|
||||
}
|
||||
|
||||
&.blue {
|
||||
#{$color-bars} {
|
||||
background-color: $ui-blue;
|
||||
|
@ -43,6 +49,10 @@
|
|||
a {
|
||||
color: $ui-blue;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&.green {
|
||||
|
|
|
@ -78,7 +78,8 @@ const Jar = {
|
|||
set: (name, value, opts) => {
|
||||
opts = opts || {};
|
||||
defaults(opts, {
|
||||
expires: new Date(new Date().setYear(new Date().getFullYear() + 1))
|
||||
expires: new Date(new Date().setYear(new Date().getFullYear() + 1)),
|
||||
sameSite: 'Lax' // cookie library requires this capitialization of sameSite
|
||||
});
|
||||
opts.path = '/';
|
||||
const obj = cookie.serialize(name, value, opts);
|
||||
|
|
24
src/lib/scratch-wiki.js
Normal file
24
src/lib/scratch-wiki.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
// This list has to be updated when a new Scratch Wiki is made.
|
||||
// Note that wikis under testwiki are not included.
|
||||
const wwwLocaleToScratchWikiLocale = {
|
||||
en: 'en',
|
||||
ja: 'ja',
|
||||
fr: 'fr',
|
||||
de: 'de',
|
||||
ru: 'ru',
|
||||
hu: 'hu',
|
||||
nl: 'nl',
|
||||
id: 'id'
|
||||
};
|
||||
|
||||
const getScratchWikiLink = locale => {
|
||||
if (!wwwLocaleToScratchWikiLocale.hasOwnProperty(locale)) {
|
||||
locale = locale.split('-')[0];
|
||||
if (!wwwLocaleToScratchWikiLocale.hasOwnProperty(locale)) {
|
||||
locale = 'en';
|
||||
}
|
||||
}
|
||||
return `https://${wwwLocaleToScratchWikiLocale[locale]}.scratch-wiki.info/`;
|
||||
};
|
||||
|
||||
module.exports = getScratchWikiLink;
|
|
@ -434,6 +434,12 @@
|
|||
"routeAlias": "/info/?(\\?.*)?$",
|
||||
"redirect": "/ideas"
|
||||
},
|
||||
{
|
||||
"name": "register-redirect",
|
||||
"pattern": "^/accounts/standalone-registration/?$",
|
||||
"routeAlias": "/accounts/standalone-registration/",
|
||||
"redirect": "/join"
|
||||
},
|
||||
{
|
||||
"name": "research-redirect",
|
||||
"pattern": "^/info/research/?$",
|
||||
|
@ -499,6 +505,16 @@
|
|||
"pattern": "^/pong/?$",
|
||||
"redirect": "/projects/editor/?tutorial=pong"
|
||||
},
|
||||
{
|
||||
"name": "animateacharacter-tutorial-redirect",
|
||||
"pattern": "^/animate-a-character/?$",
|
||||
"redirect": "/projects/editor/?tutorial=animate-a-character"
|
||||
},
|
||||
{
|
||||
"name": "videosensing-tutorial-redirect",
|
||||
"pattern": "^/video-sensing/?$",
|
||||
"redirect": "/projects/editor/?tutorial=video-sensing"
|
||||
},
|
||||
{
|
||||
"name": "clicker-tutorial-redirect",
|
||||
"pattern": "^/clicker/?$",
|
||||
|
|
|
@ -2,9 +2,9 @@ const React = require('react');
|
|||
const render = require('../../lib/render.jsx');
|
||||
const FormattedMessage = require('react-intl').FormattedMessage;
|
||||
const injectIntl = require('react-intl').injectIntl;
|
||||
const Avatar = require('../../components/avatar/avatar.jsx');
|
||||
const Page = require('../../components/page/www/page.jsx');
|
||||
const People = require('./people.json');
|
||||
const PeopleGrid = require('../../components/people-grid/people-grid.jsx');
|
||||
const Supporters = require('./supporters.json');
|
||||
const TitleBanner = require('../../components/title-banner/title-banner.jsx');
|
||||
|
||||
|
@ -25,34 +25,7 @@ const Credits = () => (
|
|||
<FormattedMessage id="credits.developers" />
|
||||
</p>
|
||||
</div>
|
||||
<ul className="avatar-grid">
|
||||
{People.map((person, index) => (
|
||||
<li
|
||||
className="avatar-item"
|
||||
key={`person-${index}`}
|
||||
>
|
||||
<div>
|
||||
{person.userName ? (
|
||||
<a href={`https://scratch.mit.edu/users/${person.userName}/`}>
|
||||
<Avatar
|
||||
alt=""
|
||||
src={`https://cdn.scratch.mit.edu/get_image/user/${person.userId || 'default'}_80x80.png`}
|
||||
/>
|
||||
</a>
|
||||
) : (
|
||||
/* if userName is not given, there's no chance userId is given */
|
||||
<Avatar
|
||||
alt=""
|
||||
src={`https://cdn.scratch.mit.edu/get_image/user/default_80x80.png`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<span className="avatar-text">
|
||||
{person.name}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<PeopleGrid people={People} />
|
||||
</div>
|
||||
<div
|
||||
className="supporters"
|
||||
|
|
|
@ -6,46 +6,10 @@
|
|||
}
|
||||
|
||||
.credits {
|
||||
.avatar-grid {
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
padding: 64px 0;
|
||||
max-width: 864px;
|
||||
list-style: none;
|
||||
flex-wrap: wrap;
|
||||
flex-flow: row wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
.avatar-item {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding-bottom: 32px;
|
||||
text-align: center;
|
||||
line-height: 1.25rem;
|
||||
|
||||
img {
|
||||
$img-border: rgba(0, 0, 0, .05);
|
||||
border: 2px solid $img-border;
|
||||
border-radius: 8px;
|
||||
background-color: $ui-white;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
display: inline-block;
|
||||
width: 144px;
|
||||
font-size: .875rem;
|
||||
word-wrap: break-word;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.acknowledge-content {
|
||||
margin: 0 176px;
|
||||
padding: 2.5rem 0 5.75rem 0;
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
"download.scratch2Desktop" : "Scratch 2.0 Offline Editor",
|
||||
"download.cannotAccessMacStore" : "What if I can't access the Mac App Store?",
|
||||
"download.cannotAccessWindowsStore" : "What if I can't access the Microsoft Store?",
|
||||
"download.macMoveAppToApplications": "Open the .dmg file. Move Scratch 3 into Applications.",
|
||||
"download.macMoveToApplications" : "Open the .dmg file. Move Scratch Desktop into Applications.",
|
||||
"download.winMoveToApplications" : "Run the .exe file.",
|
||||
"download.doIHaveToDownload" : "Do I have to download an app to use Scratch?",
|
||||
|
|
|
@ -110,6 +110,7 @@ $base-bg: $ui-white;
|
|||
padding-top: 16px;
|
||||
padding-bottom: 32px;
|
||||
width: 100%;
|
||||
overflow-anchor: none;
|
||||
|
||||
.button {
|
||||
display: block;
|
||||
|
|
|
@ -173,7 +173,7 @@
|
|||
"faq.chatRoomBody":"While it is technically possible to create chat rooms with cloud variables, they are not allowed on the Scratch website.",
|
||||
"faq.changeCloudVarTitle":"Who can change the information in a cloud variable?",
|
||||
"faq.changeCloudVarBody":"Only you and viewers of your project can store data in your project’s cloud variables. If people \"see inside\" or remix your code, it creates a copy of the variable and does not affect or change the original variable.",
|
||||
"faq.newScratcherCloudTitle":"I am logged in, but I cannot use projects with cloud variables What is going on?",
|
||||
"faq.newScratcherCloudTitle":"I am logged in, but I cannot use projects with cloud variables. What is going on?",
|
||||
"faq.newScratcherCloudBody":"If you are still a \"New Scratcher\" on the website, you will not be able to use projects with cloud variables. You need to become a \"Scratcher\" to have access to cloud variables. See the Accounts section (above) for more information about the transition from “New Scratcher” to \"Scratcher\".",
|
||||
"faq.multiplayerTitle":"Is it possible to make multiplayer games with cloud variables?",
|
||||
"faq.multiplayerBody":"Multiplayer games may be difficult to create, due to network speed and synchronization issues. However, some Scratchers are coming up with creative ways to use the cloud variables for turn-by-turn and other types of games.",
|
||||
|
|
|
@ -33,6 +33,10 @@
|
|||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.messages-social {
|
||||
overflow-anchor: none;
|
||||
}
|
||||
|
||||
.messages-admin-list,
|
||||
.messages-social-list {
|
||||
padding: 0;
|
||||
|
|
|
@ -1,14 +1,23 @@
|
|||
const React = require('react');
|
||||
const Helmet = require('react-helmet').default;
|
||||
const PropTypes = require('prop-types');
|
||||
|
||||
const projectShape = require('./projectshape.jsx').projectShape;
|
||||
|
||||
const Meta = props => {
|
||||
const {id, title, instructions, author} = props.projectInfo;
|
||||
|
||||
// Do not want to render any meta tags unless all the info is loaded
|
||||
// Check only author (object) because it is ok to have empty string instructions
|
||||
if (!author) return null;
|
||||
if (!author) {
|
||||
// Project info is not ready. It's either fetching state, or logged-out users creating project.
|
||||
if (!props.userPresent) {
|
||||
return (
|
||||
<Helmet>
|
||||
<title>Scratch - Imagine, Program, Share</title>
|
||||
</Helmet>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const truncatedInstructions = instructions.split(' ')
|
||||
.slice(0, 50)
|
||||
|
@ -38,7 +47,8 @@ const Meta = props => {
|
|||
};
|
||||
|
||||
Meta.propTypes = {
|
||||
projectInfo: projectShape
|
||||
projectInfo: projectShape,
|
||||
userPresent: PropTypes.bool
|
||||
};
|
||||
|
||||
module.exports = Meta;
|
||||
|
|
|
@ -38,7 +38,6 @@ $stage-width: 480px;
|
|||
|
||||
.inner {
|
||||
margin: 0 auto;
|
||||
overflow: auto;
|
||||
|
||||
@media #{$medium-and-smaller} {
|
||||
// subtract page padding
|
||||
|
@ -231,6 +230,7 @@ $stage-width: 480px;
|
|||
min-width: 65%;
|
||||
max-width: 100%;
|
||||
flex: 1;
|
||||
overflow-anchor: none;
|
||||
|
||||
@media #{$medium-and-smaller} {
|
||||
padding: 0;
|
||||
|
@ -365,7 +365,7 @@ $stage-width: 480px;
|
|||
.guiPlayer {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
max-width: $player-width;
|
||||
width: $player-width;
|
||||
z-index: 1;
|
||||
|
||||
$alert-bg: rgba(255, 255, 255, .85);
|
||||
|
@ -402,6 +402,7 @@ $stage-width: 480px;
|
|||
flex: 1;
|
||||
flex-flow: column;
|
||||
margin-top: .3125rem;
|
||||
overflow: auto;
|
||||
|
||||
@media #{$medium-and-smaller} {
|
||||
margin-top: .5rem;
|
||||
|
@ -444,6 +445,8 @@ $stage-width: 480px;
|
|||
font-size: .875rem;
|
||||
flex-shrink: 1;
|
||||
text-align: left;
|
||||
overflow: auto;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.description-block {
|
||||
|
@ -479,7 +482,6 @@ $stage-width: 480px;
|
|||
line-height: 1.5rem;
|
||||
flex: 1;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.project-description:last-of-type {
|
||||
|
|
|
@ -700,7 +700,10 @@ class Preview extends React.Component {
|
|||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Meta projectInfo={this.props.projectInfo} />
|
||||
<Meta
|
||||
projectInfo={this.props.projectInfo}
|
||||
userPresent={this.props.userPresent}
|
||||
/>
|
||||
{this.props.playerMode ?
|
||||
<Page
|
||||
className={classNames({
|
||||
|
|
|
@ -68,7 +68,7 @@ const OnePointFour = () => (
|
|||
<p><FormattedMessage id="onePointFour.macBody" /></p>
|
||||
<ul className="installation-downloads">
|
||||
<li className="installation-downloads-item">
|
||||
<a href="http://download.scratch.mit.edu/MacScratch1.4.dmg">
|
||||
<a href="https://download.scratch.mit.edu/MacScratch1.4.dmg">
|
||||
MacScratch1.4.dmg
|
||||
</a>
|
||||
</li>
|
||||
|
@ -86,7 +86,7 @@ const OnePointFour = () => (
|
|||
key="installation-downloads"
|
||||
>
|
||||
<li className="installation-downloads-item">
|
||||
<a href="http://download.scratch.mit.edu/ScratchInstaller1.4.exe">
|
||||
<a href="https://download.scratch.mit.edu/ScratchInstaller1.4.exe">
|
||||
ScratchInstaller1.4.exe
|
||||
</a>
|
||||
</li>
|
||||
|
@ -95,7 +95,7 @@ const OnePointFour = () => (
|
|||
id="onePointFour.windowsNetwork"
|
||||
values={{
|
||||
windowsNetworkInstaller: (
|
||||
<a href="http://download.scratch.mit.edu/Scratch1.4.msi.installer.zip">
|
||||
<a href="https://download.scratch.mit.edu/Scratch1.4.msi.installer.zip">
|
||||
<FormattedMessage id="onePointFour.windowsNetworkInstaller" />
|
||||
</a>
|
||||
)
|
||||
|
@ -144,17 +144,17 @@ const OnePointFour = () => (
|
|||
id="onePointFour.resourcesA"
|
||||
values={{
|
||||
gettingStartedGuide: (
|
||||
<a href="http://download.scratch.mit.edu/ScratchGettingStartedv14.pdf">
|
||||
<a href="https://download.scratch.mit.edu/ScratchGettingStartedv14.pdf">
|
||||
<FormattedMessage id="onePointFour.gettingStartedGuide" />
|
||||
</a>
|
||||
),
|
||||
referenceGuide: (
|
||||
<a href="http://download.scratch.mit.edu/ScratchReferenceGuide14.pdf">
|
||||
<a href="https://download.scratch.mit.edu/ScratchReferenceGuide14.pdf">
|
||||
<FormattedMessage id="onePointFour.referenceGuide" />
|
||||
</a>
|
||||
),
|
||||
scratchCards: (
|
||||
<a href="http://download.scratch.mit.edu/ScratchCardsAll-v1.4-PDF.zip">
|
||||
<a href="https://download.scratch.mit.edu/ScratchCardsAll-v1.4-PDF.zip">
|
||||
<FormattedMessage id="onePointFour.scratchCards" />
|
||||
</a>
|
||||
)
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 148 KiB |
Binary file not shown.
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 4.4 KiB |
1
static/svgs/project/comment-status.svg
Normal file
1
static/svgs/project/comment-status.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path d="M9 16c-1 1-3.5 2.5-4 2s0-1 0-2H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H9zm-3-6a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2H6zm0-4a1 1 0 1 0 0 2h8a1 1 0 0 0 0-2H6z" id="a"/></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#575E75" xlink:href="#a"/><g mask="url(#b)" fill="#4D97FF"><path d="M0 0h20v20H0z"/></g></g></svg>
|
After Width: | Height: | Size: 510 B |
|
@ -1,32 +0,0 @@
|
|||
const SeleniumHelper = require('../selenium-helpers.js');
|
||||
const helper = new SeleniumHelper();
|
||||
|
||||
var tap = require('tap');
|
||||
const test = tap.test;
|
||||
|
||||
const driver = helper.buildDriver('www-smoke test-login-failures');
|
||||
|
||||
const {
|
||||
clickText,
|
||||
findByXpath
|
||||
} = helper;
|
||||
|
||||
var rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
|
||||
|
||||
tap.plan(1);
|
||||
|
||||
tap.tearDown(function () {
|
||||
driver.quit();
|
||||
});
|
||||
|
||||
tap.beforeEach(function () {
|
||||
return driver.get(rootUrl);
|
||||
});
|
||||
|
||||
// Skipping this test while launching new join flow.
|
||||
// TODO: Add new smoke tests for the new Join flow!
|
||||
test('Clicking Join Scratch opens scratchr2 iframe', {skip: true}, t => {
|
||||
clickText('Join Scratch')
|
||||
.then(() => findByXpath('//iframe[contains(@class, "mod-registration")]'))
|
||||
.then(() => t.end());
|
||||
});
|
|
@ -1,151 +0,0 @@
|
|||
/*
|
||||
* Tests signing in & My Stuff according to smoke-tests at:
|
||||
*
|
||||
* https://github.com/LLK/scratchr2/wiki/Smoke-Testing-Test-Cases
|
||||
*
|
||||
*/
|
||||
|
||||
const SeleniumHelper = require('../selenium-helpers.js');
|
||||
const helper = new SeleniumHelper();
|
||||
|
||||
var tap = require('tap');
|
||||
const test = tap.skip;
|
||||
|
||||
const driver = helper.buildDriver('www-smoke test-my-stuff');
|
||||
|
||||
const {
|
||||
clickText,
|
||||
findByXpath,
|
||||
clickXpath,
|
||||
clickButton
|
||||
} = helper;
|
||||
|
||||
var username = process.env.SMOKE_USERNAME;
|
||||
var password = process.env.SMOKE_PASSWORD;
|
||||
|
||||
var rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
|
||||
var url = rootUrl + '/users/' + username;
|
||||
|
||||
tap.plan(7);
|
||||
|
||||
tap.tearDown(function () {
|
||||
driver.quit();
|
||||
});
|
||||
|
||||
tap.beforeEach(function () {
|
||||
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'));
|
||||
});
|
||||
|
||||
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'))
|
||||
.then(() => t.end());
|
||||
});
|
||||
|
||||
test('Sign in to Scratch & verify My Stuff structure (tabs, title)', t => {
|
||||
clickXpath('//a[contains(@class, "mystuff-icon")]')
|
||||
.then(() => findByXpath('//div[@class="box-head"]/h2'))
|
||||
.then((element) => element.getText('h2'))
|
||||
.then((text) => t.equal('My Stuff', text, 'title should be My Stuff'))
|
||||
.then(() => findByXpath('//li[@data-tab="projects"]/a'))
|
||||
.then((element) => element.getText('a'))
|
||||
.then((text) => t.match(text, 'All Projects', 'All Projects tab should be present'))
|
||||
.then(() => findByXpath('//li[@data-tab="shared"]/a'))
|
||||
.then((element) => element.getText('a'))
|
||||
.then((text) => t.match(text, 'Shared Projects', 'Shared Projects tab should be present'))
|
||||
.then(() => findByXpath('//li[@data-tab="unshared"]/a'))
|
||||
.then((element) => element.getText('a'))
|
||||
.then((text) => t.match(text, 'Unshared Projects', 'Unshared Projects tab should be present'))
|
||||
.then(() => findByXpath('//li[@data-tab="galleries"]/a'))
|
||||
.then((element) => element.getText('a'))
|
||||
.then((text) => t.match(text, 'My Studios', 'My Studios tab should be present'))
|
||||
.then(() => findByXpath('//li[@data-tab="trash"]/a'))
|
||||
.then((element) => element.getText('a'))
|
||||
.then((text) => t.match(text, 'Trash', 'Trash tab should be present'))
|
||||
.then(() => t.end());
|
||||
});
|
||||
|
||||
test('clicking See Inside should take you to the editor', t => {
|
||||
clickXpath('//a[contains(@class, "mystuff-icon")]')
|
||||
.then(() => findByXpath('//a[@data-control="edit"]'))
|
||||
.then((element) => element.getText('span'))
|
||||
.then((text) => t.equal(text, 'See inside', 'there should be a "See inside" button'))
|
||||
.then(() => clickXpath('//a[@data-control="edit"]'))
|
||||
.then(() => driver.getCurrentUrl())
|
||||
.then(function (u) {
|
||||
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());
|
||||
});
|
||||
|
||||
test('clicking a project title should take you to the project page', t => {
|
||||
clickXpath('//a[contains(@class, "mystuff-icon")]')
|
||||
.then(() => clickXpath('//a[@data-control="edit"]'))
|
||||
.then(() => driver.getCurrentUrl())
|
||||
.then(function (u) {
|
||||
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());
|
||||
});
|
||||
|
||||
test('Add To button should bring up a list of studios', t => {
|
||||
clickXpath('//a[contains(@class, "mystuff-icon")]')
|
||||
.then(() => clickXpath('//div[@id="sidebar"]/ul/li[@data-tab="shared"]'))
|
||||
.then(() => findByXpath('//div[@data-control="add-to"]'))
|
||||
.then((element) => element.getText('span'))
|
||||
.then((text) => t.equal(text, 'Add to', 'there should be an "Add to" button'))
|
||||
.then(() => clickXpath('//div[@data-control="add-to"]'))
|
||||
.then(() => findByXpath('//div[@class="dropdown-menu"]/ul/li'))
|
||||
.then((element) => element.getText('span'))
|
||||
.then(function (text) {
|
||||
var expectedRegExp = new RegExp('.+');
|
||||
t.match(text, expectedRegExp, 'the dropdown menu should have at least 1 text item in it');
|
||||
})
|
||||
.then(() => t.end());
|
||||
});
|
||||
|
||||
test('+ New Studio button should take you to the studio page', {skip: true}, t => {
|
||||
clickXpath('//a[contains(@class, "mystuff-icon")]')
|
||||
.then(() => clickXpath('//form[@id="new_studio"]/button[@type="submit"]'))
|
||||
.then(() => findByXpath('//div[@id="show-add-project"]'))
|
||||
.then((element) => element.getText('span'))
|
||||
.then((text) => t.equal(text, 'Add projects', 'there should be an "Add projects" button'))
|
||||
.then(() => driver.getCurrentUrl())
|
||||
.then(function (u) {
|
||||
var expectedUrlRegExp = new RegExp('/studios/.*[0-9].*/?');
|
||||
t.match(u, expectedUrlRegExp,
|
||||
'after clicking the + New Studio, the URL should end in studios/STUDIO_ID');
|
||||
})
|
||||
.then(() => t.end());
|
||||
});
|
||||
|
||||
test('+ New Project button should open the editor', {skip: true}, t => {
|
||||
clickXpath('//a[contains(@class, "mystuff-icon")]')
|
||||
.then(() => clickText('+ New Project'))
|
||||
.then(() => driver.getCurrentUrl())
|
||||
.then(function (u) {
|
||||
var expectedUrlRegExp = new RegExp('/projects/editor');
|
||||
t.match(u, expectedUrlRegExp,
|
||||
'after clicking, the URL should end in projects/editor');
|
||||
})
|
||||
.then(() => t.end());
|
||||
});
|
|
@ -1,305 +0,0 @@
|
|||
/*
|
||||
* Checks that the links in the footer on the homepage have the right URLs to redirect to
|
||||
*
|
||||
* Test cases: https://github.com/LLK/scratch-www/wiki/Most-Important-Workflows
|
||||
*/
|
||||
|
||||
const SeleniumHelper = require('../selenium-helpers.js');
|
||||
const helper = new SeleniumHelper();
|
||||
|
||||
const tap = require('tap');
|
||||
|
||||
const webdriver = require('selenium-webdriver');
|
||||
const driver = helper.buildDriver('www-smoke test_footer_links');
|
||||
|
||||
const rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
|
||||
|
||||
// timeout for each test; timeout for suite set at command line level
|
||||
const options = {timeout: 30000};
|
||||
|
||||
// number of tests in the plan
|
||||
tap.plan(24);
|
||||
|
||||
tap.tearDown(function () {
|
||||
// quit the instance of the browser
|
||||
driver.quit();
|
||||
});
|
||||
|
||||
tap.beforeEach(function () {
|
||||
// load the page with the driver
|
||||
return driver.get(rootUrl);
|
||||
});
|
||||
|
||||
// Function clicks the link and returns the url of the resulting page
|
||||
|
||||
const clickFooterLinks = function (linkText) {
|
||||
return driver.wait(webdriver.until.elementLocated(webdriver.By.id('footer')))
|
||||
.then(function (element) {
|
||||
return element.findElement(webdriver.By.linkText(linkText));
|
||||
})
|
||||
.then(function (element) {
|
||||
return element.click();
|
||||
})
|
||||
.then(function () {
|
||||
return driver.getCurrentUrl();
|
||||
});
|
||||
};
|
||||
|
||||
// ==== ABOUT SCRATCH column ====
|
||||
|
||||
// ABOUT SCRATCH
|
||||
tap.test('clickAboutScratchLink', options, t => {
|
||||
const linkText = 'About Scratch';
|
||||
const expectedHref = '/about';
|
||||
clickFooterLinks(linkText).then(url => {
|
||||
// the href should be at the end of the URL
|
||||
t.equal(url.substr(-expectedHref.length), expectedHref);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
// FOR PARENTS
|
||||
tap.test('clickForParentsLink', options, t => {
|
||||
const linkText = 'For Parents';
|
||||
const expectedHref = '/parents/';
|
||||
clickFooterLinks(linkText).then(url => {
|
||||
t.equal(url.substr(-expectedHref.length), expectedHref);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
// FOR EDUCATORS
|
||||
tap.test('clickForEducatorsLink', options, t => {
|
||||
const linkText = 'For Educators';
|
||||
const expectedHref = '/educators';
|
||||
clickFooterLinks(linkText).then(url => {
|
||||
t.equal(url.substr(-expectedHref.length), expectedHref);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
// FOR DEVELOPERS
|
||||
tap.test('clickForDevelopersScratchLink', options, t => {
|
||||
const linkText = 'For Developers';
|
||||
const expectedHref = '/developers';
|
||||
clickFooterLinks(linkText).then(url => {
|
||||
t.equal(url.substr(-expectedHref.length), expectedHref);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
// CREDITS
|
||||
tap.test('clickCreditsLink', options, t => {
|
||||
const linkText = 'Credits';
|
||||
const expectedHref = '/credits';
|
||||
clickFooterLinks(linkText).then(url => {
|
||||
t.equal(url.substr(-expectedHref.length), expectedHref);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
// JOBS
|
||||
tap.test('clickJobsLink', options, t => {
|
||||
const linkText = 'Jobs';
|
||||
const expectedUrl = 'https://www.scratchfoundation.org/opportunities/';
|
||||
clickFooterLinks(linkText).then(url => {
|
||||
t.equal(url, expectedUrl);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
// PRESS
|
||||
tap.test('clickPressLink', options, t => {
|
||||
const linkText = 'Press';
|
||||
const expectedUrl = 'https://www.scratchfoundation.org/media-kit/';
|
||||
clickFooterLinks(linkText).then(url => {
|
||||
t.equal(url, expectedUrl);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
// ==== COMMUNITY column ====
|
||||
|
||||
// COMMUNITY GUIDELINES
|
||||
tap.test('clickCommunityGuidelinesLink', options, t => {
|
||||
const linkText = 'Community Guidelines';
|
||||
const expectedHref = '/community_guidelines';
|
||||
clickFooterLinks(linkText).then(url => {
|
||||
t.equal(url.substr(-expectedHref.length), expectedHref);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
// DISCUSSION FORUMS
|
||||
tap.test('clickDiscussionForumsLink', options, t => {
|
||||
const linkText = 'Discussion Forums';
|
||||
const expectedHref = '/discuss/';
|
||||
clickFooterLinks(linkText).then(url => {
|
||||
t.equal(url.substr(-expectedHref.length), expectedHref);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
// SCRATCH WIKI test has been removed.
|
||||
|
||||
// STATISTICS
|
||||
tap.test('clickStatisticsLink', options, t => {
|
||||
const linkText = 'Statistics';
|
||||
const expectedHref = '/statistics/';
|
||||
clickFooterLinks(linkText).then(url => {
|
||||
t.equal(url.substr(-expectedHref.length), expectedHref);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
// ==== SUPPORT column ====
|
||||
|
||||
// IDEAS PAGE
|
||||
tap.test('clickIdeasPageLink', options, t => {
|
||||
const linkText = 'Ideas';
|
||||
const expectedHref = '/ideas';
|
||||
clickFooterLinks(linkText).then(url => {
|
||||
t.equal(url.substr(-expectedHref.length), expectedHref);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
// FAQ
|
||||
tap.test('clickFAQLink', options, t => {
|
||||
const linkText = 'FAQ';
|
||||
const expectedHref = '/info/faq';
|
||||
clickFooterLinks(linkText).then(url => {
|
||||
t.equal(url.substr(-expectedHref.length), expectedHref);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
// OFFLINE EDITOR
|
||||
tap.test('clickOfflineEditorLink', options, t => {
|
||||
const linkText = 'Offline Editor';
|
||||
const expectedHref = '/download';
|
||||
clickFooterLinks(linkText).then(url => {
|
||||
t.equal(url.substr(-expectedHref.length), expectedHref);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
// CONTACT US
|
||||
tap.test('clickContactUsLink', options, t => {
|
||||
const linkText = 'Contact Us';
|
||||
const expectedHref = '/contact-us/';
|
||||
clickFooterLinks(linkText).then(url => {
|
||||
t.equal(url.substr(-expectedHref.length), expectedHref);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
// skip this test since it points to an external site
|
||||
// SCRATCH STORE
|
||||
tap.test('clickScratchStoreLink', {skip: true}, t => {
|
||||
const linkText = 'Scratch Store';
|
||||
const expectedUrl = 'https://scratch-foundation.myshopify.com/';
|
||||
clickFooterLinks(linkText).then(url => {
|
||||
t.equal(url, expectedUrl);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
// skip this test since it points to an external site
|
||||
// DONATE
|
||||
tap.test('clickDonateLink', {skip: true}, t => {
|
||||
const linkText = 'Donate';
|
||||
const expectedUrl = 'https://secure.donationpay.org/scratchfoundation/';
|
||||
clickFooterLinks(linkText).then(url => {
|
||||
t.equal(url, expectedUrl);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
// ==== LEGAL column ====
|
||||
|
||||
// TERMS OF USE
|
||||
tap.test('clickTermsOfUseLink', options, t => {
|
||||
const linkText = 'Terms of Use';
|
||||
const expectedHref = '/terms_of_use';
|
||||
clickFooterLinks(linkText).then(url => {
|
||||
t.equal(url.substr(-expectedHref.length), expectedHref);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
// PRIVACY POLICY
|
||||
tap.test('clickPrivacyPolicyLink', options, t => {
|
||||
const linkText = 'Privacy Policy';
|
||||
const expectedHref = '/privacy_policy';
|
||||
clickFooterLinks(linkText).then(url => {
|
||||
t.equal(url.substr(-expectedHref.length), expectedHref);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
// DMCA
|
||||
tap.test('clickDMCALink', options, t => {
|
||||
const linkText = 'DMCA';
|
||||
const expectedHref = '/DMCA';
|
||||
clickFooterLinks(linkText).then(url => {
|
||||
t.equal(url.substr(-expectedHref.length), expectedHref);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
// ==== SCRATCH FAMILY column ====
|
||||
|
||||
// skip this test since it points to an external site
|
||||
// SCRATCH ED (SCRATCHED)
|
||||
tap.test('clickScratchEdLink', {skip: true}, t => {
|
||||
const linkText = 'ScratchEd';
|
||||
const expectedUrl = 'http://scratched.gse.harvard.edu/';
|
||||
clickFooterLinks(linkText).then(url => {
|
||||
t.equal(url, expectedUrl);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
// skip this test since it points to an external site
|
||||
// SCRATCH JR (SCRATCHJR)
|
||||
tap.test('clickScratchJrLink', {skip: true}, t => {
|
||||
const linkText = 'ScratchJr';
|
||||
const expectedUrl = 'https://www.scratchjr.org/';
|
||||
clickFooterLinks(linkText).then(url => {
|
||||
t.equal(url, expectedUrl);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
// skip this test since it points to an external site
|
||||
// SCRATCH DAY
|
||||
tap.test('clickScratchDayLink', {skip: true}, t => {
|
||||
const linkText = 'Scratch Day';
|
||||
const expectedUrl = 'https://day.scratch.mit.edu/';
|
||||
clickFooterLinks(linkText).then(url => {
|
||||
t.equal(url, expectedUrl);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
// SCRATCH CONFERENCE
|
||||
tap.test('clickScratchConferenceLink', options, t => {
|
||||
const linkText = 'Scratch Conference';
|
||||
const expectedHref = '/conference/20';
|
||||
clickFooterLinks(linkText).then(url => {
|
||||
t.match(url.substr(-(expectedHref.length + 2)), expectedHref);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
// skip this test since it points to an external site
|
||||
// SCRATCH FOUNDATION
|
||||
tap.test('clickScratchFoundationLink', {skip: true}, t => {
|
||||
const linkText = 'Scratch Foundation';
|
||||
const expectedUrl = 'https://www.scratchfoundation.org/';
|
||||
clickFooterLinks(linkText).then(url => {
|
||||
t.equal(url, expectedUrl);
|
||||
t.end();
|
||||
});
|
||||
});
|
|
@ -1,61 +0,0 @@
|
|||
const SeleniumHelper = require('../selenium-helpers.js');
|
||||
const helper = new SeleniumHelper();
|
||||
|
||||
var tap = require('tap');
|
||||
const test = tap.test;
|
||||
|
||||
const driver = helper.buildDriver('www-smoke test_sign_in_out_homepage');
|
||||
|
||||
const {
|
||||
clickText,
|
||||
clickXpath,
|
||||
dragFromXpathToXpath,
|
||||
findByXpath,
|
||||
waitUntilGone
|
||||
} = helper;
|
||||
|
||||
const rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
|
||||
const projectId = 1;
|
||||
const projectUrl = `${rootUrl}/projects/${projectId}`;
|
||||
|
||||
tap.plan(3);
|
||||
|
||||
tap.tearDown(function () {
|
||||
driver.quit();
|
||||
});
|
||||
|
||||
tap.beforeEach(function () {
|
||||
return driver.get(projectUrl);
|
||||
});
|
||||
|
||||
test('Find fullscreen button', {skip: true}, t => {
|
||||
findByXpath('//div[starts-with(@class, "loader_background")]')
|
||||
.then(el => waitUntilGone(el))
|
||||
.then(() => clickXpath('//div[starts-with(@class, "stage_green-flag-overlay")]'))
|
||||
.then(() => clickXpath('//img[contains(@alt, "Enter full screen mode")]'))
|
||||
.then(() => t.end());
|
||||
});
|
||||
|
||||
test('Open and close Copy Link modal', {skip: true}, t => {
|
||||
findByXpath('//div[starts-with(@class, "loader_background")]')
|
||||
.then(el => waitUntilGone(el))
|
||||
.then(() => clickText('Copy Link'))
|
||||
.then(() => clickXpath('//div[contains(@class, "social-label-title")]'))
|
||||
.then(() => clickXpath('//img[contains(@alt, "close-icon")]'))
|
||||
.then(() => clickXpath('//img[contains(@alt, "Enter full screen mode")]'))
|
||||
.then(() => t.end());
|
||||
});
|
||||
|
||||
test('Dragging out of modal should not close modal', {skip: true}, t => {
|
||||
findByXpath('//div[starts-with(@class, "loader_background")]')
|
||||
.then(el => waitUntilGone(el))
|
||||
.then(() => clickXpath('//div[starts-with(@class, "stage_green-flag-overlay")]'))
|
||||
.then(() => clickText('Copy Link'))
|
||||
.then(() => clickXpath('//div[contains(@class, "social-label-title")]'))
|
||||
.then(() => dragFromXpathToXpath(
|
||||
'//div[contains(@class, "social-label-title")]',
|
||||
'//li[contains(@class, "logo")]'
|
||||
))
|
||||
.then(() => clickXpath('//div[contains(@class, "social-label-title")]'))
|
||||
.then(() => t.end());
|
||||
});
|
157
test/integration/footer-links.test.js
Normal file
157
test/integration/footer-links.test.js
Normal file
|
@ -0,0 +1,157 @@
|
|||
const SeleniumHelper = require('./selenium-helpers.js');
|
||||
|
||||
const {
|
||||
clickText,
|
||||
buildDriver
|
||||
} = new SeleniumHelper();
|
||||
|
||||
let remote = process.env.SMOKE_REMOTE || false;
|
||||
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
|
||||
|
||||
if (remote) {
|
||||
jest.setTimeout(60000);
|
||||
} else {
|
||||
jest.setTimeout(10000);
|
||||
}
|
||||
|
||||
let driver;
|
||||
|
||||
describe('www-integration footer links', () => {
|
||||
beforeAll(async () => {
|
||||
driver = await buildDriver('www-integration footer links');
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await driver.get(rootUrl);
|
||||
});
|
||||
|
||||
afterAll(async () => await driver.quit());
|
||||
|
||||
// ==== About Scratch column ====
|
||||
|
||||
test('click About Scratch link', async () => {
|
||||
await clickText('About Scratch');
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/about\/?$/);
|
||||
});
|
||||
|
||||
test('click For Parents link', async () => {
|
||||
await clickText('For Parents');
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/parents\/?$/);
|
||||
});
|
||||
|
||||
test('click For Educators link', async () => {
|
||||
await clickText('For Educators');
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/educators\/?$/);
|
||||
});
|
||||
|
||||
test('click For Developers link', async () => {
|
||||
await clickText('For Developers');
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/developers\/?$/);
|
||||
});
|
||||
|
||||
// ==== COMMUNITY column ====
|
||||
|
||||
test('click Community Guidelines link', async () => {
|
||||
await clickText('Community Guidelines');
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/community_guidelines\/?$/);
|
||||
});
|
||||
|
||||
test('click Discussion Forums link', async () => {
|
||||
await clickText('Discussion Forums');
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/discuss\/?$/);
|
||||
});
|
||||
|
||||
test('click Statistics link', async () => {
|
||||
await clickText('Statistics');
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/statistics\/?$/);
|
||||
});
|
||||
|
||||
// ==== SUPPORT column ====
|
||||
|
||||
test('click Ideas link', async () => {
|
||||
await clickText('Ideas');
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/ideas\/?$/);
|
||||
});
|
||||
|
||||
test('click FAQ link', async () => {
|
||||
await clickText('FAQ');
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/info\/faq\/?$/);
|
||||
});
|
||||
|
||||
test('click Download link', async () => {
|
||||
await clickText('Download');
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/download\/?$/);
|
||||
});
|
||||
|
||||
test('click Contact Us link', async () => {
|
||||
await clickText('Contact Us');
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/contact-us\/?$/);
|
||||
});
|
||||
|
||||
// ==== LEGAL column ====
|
||||
|
||||
test('click Terms of Use link', async () => {
|
||||
await clickText('Terms of Use');
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/terms_of_use\/?$/);
|
||||
});
|
||||
|
||||
test('click Privacy Policy link', async () => {
|
||||
await clickText('Privacy Policy');
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/privacy_policy\/?$/);
|
||||
});
|
||||
|
||||
test('click DMCA link', async () => {
|
||||
await clickText('DMCA');
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/DMCA\/?$/);
|
||||
});
|
||||
|
||||
// ==== SCRATCH FAMILY column ====
|
||||
|
||||
test('click Scratch Conference link', async () => {
|
||||
await clickText('Scratch Conference');
|
||||
let url = await driver.getCurrentUrl();
|
||||
let pathname = (new URL(url)).pathname;
|
||||
expect(pathname).toMatch(/^\/conference\/2020\/?$/);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// The following links in are skipped because they are not on scratch.mit.edu
|
||||
|
||||
// Jobs
|
||||
// Press
|
||||
// SCRATCH STORE
|
||||
// DONATE
|
||||
// SCRATCH WIKI
|
||||
// SCRATCH ED (SCRATCHED)
|
||||
// SCRATCH JR (SCRATCHJR)
|
||||
// SCRATCH DAY
|
||||
// SCRATCH FOUNDATION
|
91
test/integration/join.test.js
Normal file
91
test/integration/join.test.js
Normal file
|
@ -0,0 +1,91 @@
|
|||
const SeleniumHelper = require('./selenium-helpers.js');
|
||||
|
||||
const {
|
||||
findByXpath,
|
||||
clickXpath,
|
||||
buildDriver
|
||||
} = new SeleniumHelper();
|
||||
|
||||
let remote = process.env.SMOKE_REMOTE || false;
|
||||
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
|
||||
let takenUsername = process.env.SMOKE_USERNAME;
|
||||
|
||||
if (remote){
|
||||
jest.setTimeout(60000);
|
||||
} else {
|
||||
jest.setTimeout(10000);
|
||||
}
|
||||
|
||||
let driver;
|
||||
|
||||
describe('www-integration join flow', () => {
|
||||
beforeAll(async () => {
|
||||
driver = await buildDriver('www-integration join flow');
|
||||
await driver.get(rootUrl);
|
||||
});
|
||||
|
||||
afterAll(async () => await driver.quit());
|
||||
|
||||
beforeEach(async () => {
|
||||
driver.get(rootUrl);
|
||||
await clickXpath('//a[@class="registrationLink"]');
|
||||
});
|
||||
|
||||
test('click Join opens join modal', async () => {
|
||||
let joinModal = await findByXpath('//div[@class = "join-flow-outer-content"]');
|
||||
let modalVisible = await joinModal.isDisplayed();
|
||||
await expect(modalVisible).toBe(true);
|
||||
});
|
||||
|
||||
test('username validation message appears', async () => {
|
||||
await clickXpath('//input[contains(@name, "username")]');
|
||||
let message = await findByXpath('//div[contains(@class, "validation-message")]');
|
||||
let messageText = await message.getText();
|
||||
await expect(messageText).toEqual('Don\'t use your real name');
|
||||
|
||||
});
|
||||
|
||||
test('password validation message appears', async () => {
|
||||
await clickXpath('//input[contains(@name, "password")]');
|
||||
let message = await findByXpath('//div[contains(@class, "validation-message")]');
|
||||
let messageText = await message.getText();
|
||||
await expect(messageText).toContain('Write it down so you remember.');
|
||||
});
|
||||
|
||||
test('password validation message appears', async () => {
|
||||
await clickXpath('//input[contains(@name, "passwordConfirm")]');
|
||||
let message = await findByXpath('//div[contains(@class, "validation-message")]');
|
||||
let messageText = await message.getText();
|
||||
await expect(messageText).toEqual('Type password again');
|
||||
});
|
||||
|
||||
test('username validation: too short', async () => {
|
||||
let textInput = await findByXpath('//input[contains(@name, "username")]');
|
||||
await textInput.click();
|
||||
await textInput.sendKeys('ab');
|
||||
await clickXpath('//div[@class = "join-flow-outer-content"]');
|
||||
let message = await findByXpath('//div[contains(@class, "validation-error")]');
|
||||
let messageText = await message.getText();
|
||||
await expect(messageText).toContain('Must be 3 letters or longer');
|
||||
});
|
||||
|
||||
test('username validation: username taken', async () => {
|
||||
let textInput = await findByXpath('//input[contains(@name, "username")]');
|
||||
await textInput.click();
|
||||
await textInput.sendKeys(takenUsername);
|
||||
await clickXpath('//div[@class = "join-flow-outer-content"]');
|
||||
let message = await findByXpath('//div[contains(@class, "validation-error")]');
|
||||
let messageText = await message.getText();
|
||||
await expect(messageText).toContain('Username taken.');
|
||||
});
|
||||
|
||||
test('username validation: bad word', async () => {
|
||||
let textInput = await findByXpath('//input[contains(@name, "username")]');
|
||||
await textInput.click();
|
||||
await textInput.sendKeys('qnb02mclepghwic9');
|
||||
await clickXpath('//div[@class = "join-flow-outer-content"]');
|
||||
let message = await findByXpath('//div[contains(@class, "validation-error")]');
|
||||
let messageText = await message.getText();
|
||||
await expect(messageText).toContain('Username not allowed');
|
||||
});
|
||||
});
|
108
test/integration/my-stuff.test.js
Normal file
108
test/integration/my-stuff.test.js
Normal file
|
@ -0,0 +1,108 @@
|
|||
const SeleniumHelper = require('./selenium-helpers.js');
|
||||
|
||||
const {
|
||||
clickText,
|
||||
findByXpath,
|
||||
clickXpath,
|
||||
buildDriver
|
||||
} = new SeleniumHelper();
|
||||
|
||||
let username = process.env.SMOKE_USERNAME + '1';
|
||||
let password = process.env.SMOKE_PASSWORD;
|
||||
let remote = process.env.SMOKE_REMOTE || false;
|
||||
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
|
||||
let myStuffURL = rootUrl + '/mystuff';
|
||||
|
||||
if (remote){
|
||||
jest.setTimeout(60000);
|
||||
} else {
|
||||
jest.setTimeout(10000);
|
||||
}
|
||||
|
||||
let driver;
|
||||
|
||||
describe('www-integration my_stuff', () => {
|
||||
beforeAll(async () => {
|
||||
driver = await buildDriver('www-integration my_stuff');
|
||||
await driver.get(rootUrl);
|
||||
await driver.sleep(1000);
|
||||
await clickXpath('//li[@class="link right login-item"]/a');
|
||||
let name = await findByXpath('//input[@id="frc-username-1088"]');
|
||||
await name.sendKeys(username);
|
||||
let word = await findByXpath('//input[@id="frc-password-1088"]');
|
||||
await word.sendKeys(password);
|
||||
await driver.sleep(500);
|
||||
await clickXpath('//button[contains(@class, "button") and ' +
|
||||
'contains(@class, "submit-button") and contains(@class, "white")]');
|
||||
await findByXpath('//span[contains(@class, "profile-name")]');
|
||||
});
|
||||
|
||||
afterAll(async () => await driver.quit());
|
||||
|
||||
test('verify My Stuff structure (tabs, title)', async () => {
|
||||
await driver.get(myStuffURL);
|
||||
let header = await findByXpath('//div[@class="box-head"]/h2');
|
||||
let headerVisible = await header.isDisplayed();
|
||||
await expect(headerVisible).toBe(true);
|
||||
let allTab = await findByXpath('//li[@data-tab="projects"]/a');
|
||||
let allTabVisible = await allTab.isDisplayed();
|
||||
await expect(allTabVisible).toBe(true);
|
||||
let sharedTab = await findByXpath('//li[@data-tab="shared"]/a');
|
||||
let sharedTabVisible = await sharedTab.isDisplayed();
|
||||
await expect(sharedTabVisible).toBe(true);
|
||||
let unsharedTab = await findByXpath('//li[@data-tab="unshared"]/a');
|
||||
let unsharedTabVisible = await unsharedTab.isDisplayed();
|
||||
await expect(unsharedTabVisible).toBe(true);
|
||||
let studios = await findByXpath('//li[@data-tab="galleries"]/a');
|
||||
let studiosVisible = await studios.isDisplayed();
|
||||
await expect(studiosVisible).toBe(true);
|
||||
let trash = await findByXpath('//li[@data-tab="trash"]/a');
|
||||
let trashVisible = await trash.isDisplayed();
|
||||
await expect(trashVisible).toBe(true);
|
||||
});
|
||||
|
||||
test('clicking a project title should take you to the project page', async () => {
|
||||
await driver.get(myStuffURL);
|
||||
await clickXpath('//span[@class="media-info-item title"]');
|
||||
await driver.sleep(6000);
|
||||
let gui = await findByXpath('//div[@class="guiPlayer"]');
|
||||
let guiVisible = await gui.isDisplayed();
|
||||
await expect(guiVisible).toBe(true);
|
||||
});
|
||||
|
||||
test('clicking "see inside" should take you to the editor', async () =>{
|
||||
await driver.get(myStuffURL);
|
||||
await clickXpath('//a[@data-control="edit"]');
|
||||
let gf = await findByXpath('//img[@class="green-flag_green-flag_1kiAo"]');
|
||||
let gfVisible = await gf.isDisplayed();
|
||||
await expect(gfVisible).toBe(true);
|
||||
});
|
||||
|
||||
test('Add To button should bring up a list of studios', async () =>{
|
||||
await driver.get(myStuffURL);
|
||||
await clickXpath('//div[@id="sidebar"]/ul/li[@data-tab="shared"]');
|
||||
await clickXpath('//div[@data-control="add-to"]');
|
||||
let dropDown = await findByXpath('//div[@class="dropdown-menu"]/ul/li');
|
||||
let dropDownVisible = await dropDown.isDisplayed();
|
||||
await expect(dropDownVisible).toBe(true);
|
||||
});
|
||||
|
||||
test('+ New Studio button should take you to the studio page', async ()=>{
|
||||
await driver.get(myStuffURL);
|
||||
await clickXpath('//form[@id="new_studio"]/button[@type="submit"]');
|
||||
await driver.sleep(500);
|
||||
// my stuff also has an element with the id tabs
|
||||
let tabs = await findByXpath('//ul[@id="tabs" and @class="tabs-index box-h-tabs h-tabs"]');
|
||||
let tabsVisible = await tabs.isDisplayed();
|
||||
expect(tabsVisible).toBe(true);
|
||||
});
|
||||
|
||||
test('+ New Project button should open the editor', async () =>{
|
||||
await driver.get(myStuffURL);
|
||||
await clickText('+ New Project');
|
||||
let gf = await findByXpath('//img[@class="green-flag_green-flag_1kiAo"]');
|
||||
let gfVisible = await gf.isDisplayed();
|
||||
await expect(gfVisible).toBe(true);
|
||||
});
|
||||
|
||||
});
|
74
test/integration/project-page.test.js
Normal file
74
test/integration/project-page.test.js
Normal file
|
@ -0,0 +1,74 @@
|
|||
const SeleniumHelper = require('./selenium-helpers.js');
|
||||
|
||||
const {
|
||||
findByXpath,
|
||||
clickXpath,
|
||||
buildDriver
|
||||
} = new SeleniumHelper();
|
||||
|
||||
let remote = process.env.SMOKE_REMOTE || false;
|
||||
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
|
||||
let projectId = process.env.TEST_PROJECT_ID || 1300006196;
|
||||
let projectUrl = rootUrl + '/projects/' + projectId;
|
||||
|
||||
if (remote){
|
||||
jest.setTimeout(60000);
|
||||
} else {
|
||||
jest.setTimeout(10000);
|
||||
}
|
||||
|
||||
let driver;
|
||||
|
||||
describe('www-integration project-page signed out', () => {
|
||||
beforeAll(async () => {
|
||||
// expect(projectUrl).toBe(defined);
|
||||
driver = await buildDriver('www-integration project-page signed out');
|
||||
await driver.get(rootUrl);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await driver.get(projectUrl);
|
||||
let gfOverlay = await findByXpath('//div[@class="stage-wrapper_stage-wrapper_2bejr box_box_2jjDp"]');
|
||||
await gfOverlay.isDisplayed();
|
||||
});
|
||||
|
||||
afterAll(async () => await driver.quit());
|
||||
|
||||
// LOGGED OUT TESTS
|
||||
|
||||
test('Find fullscreen button', async () => {
|
||||
await clickXpath('//div[starts-with(@class, "stage_green-flag-overlay")]');
|
||||
await clickXpath('//img[contains(@alt, "Enter full screen mode")]');
|
||||
let fullscreenGui = await findByXpath('//div[@class="guiPlayer fullscreen"]');
|
||||
let guiVisible = await fullscreenGui.isDisplayed();
|
||||
await expect(guiVisible).toBe(true);
|
||||
});
|
||||
|
||||
test.skip('Open Copy Link modal', async () => {
|
||||
await clickXpath('//button[@class="button action-button copy-link-button"]');
|
||||
let projectLink = await findByXpath('//input[@name="link"]');
|
||||
let linkValue = await projectLink.getAttribute('value');
|
||||
await expect(linkValue).toEqual(projectUrl);
|
||||
});
|
||||
|
||||
test('Click Username to go to profile page', async ()=> {
|
||||
await clickXpath('//div[@class="title"]/a');
|
||||
let userContent = await findByXpath('//div[@class="user-content"]');
|
||||
let contentVisible = await userContent.isDisplayed();
|
||||
await expect(contentVisible).toBe(true);
|
||||
});
|
||||
|
||||
test('click See Inside to go to the editor', async ()=> {
|
||||
await clickXpath('//button[@class="button button see-inside-button"]');
|
||||
let infoArea = await findByXpath('//div[@class="sprite-info_sprite-info_3EyZh box_box_2jjDp"]');
|
||||
let areaVisible = await infoArea.isDisplayed();
|
||||
await expect(areaVisible).toBe(true);
|
||||
});
|
||||
|
||||
test('click View All remixes takes you to remix page', async ()=> {
|
||||
await clickXpath('//div[@class="list-header-link"]');
|
||||
let originalLink = await findByXpath('//h2/a');
|
||||
let link = await originalLink.getAttribute('href');
|
||||
await expect(link).toEqual(rootUrl + '/projects/' + projectId + '/');
|
||||
});
|
||||
});
|
|
@ -1,6 +1,7 @@
|
|||
const webdriver = require('selenium-webdriver');
|
||||
const bindAll = require('lodash.bindall');
|
||||
require('chromedriver');
|
||||
const chromedriverVersion = require('chromedriver').version;
|
||||
|
||||
const headless = process.env.SMOKE_HEADLESS || false;
|
||||
const remote = process.env.SMOKE_REMOTE || false;
|
||||
|
@ -62,13 +63,23 @@ class SeleniumHelper {
|
|||
return driver;
|
||||
}
|
||||
|
||||
getChromeVersionNumber () {
|
||||
const versionFinder = /\d+\.\d+/;
|
||||
const versionArray = versionFinder.exec(chromedriverVersion);
|
||||
if (versionArray === null) {
|
||||
throw new Error('couldn\'t find version of chromedriver');
|
||||
}
|
||||
return versionArray[0];
|
||||
}
|
||||
|
||||
getSauceDriver (username, accessKey, name) {
|
||||
const chromeVersion = this.getChromeVersionNumber();
|
||||
// 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: '76.0'
|
||||
version: chromeVersion
|
||||
};
|
||||
var driver = new webdriver.Builder()
|
||||
.withCapabilities({
|
||||
|
|
|
@ -17,6 +17,8 @@ let wwwURL = rootUrl;
|
|||
|
||||
if (remote){
|
||||
jest.setTimeout(60000);
|
||||
} else {
|
||||
jest.setTimeout(10000);
|
||||
}
|
||||
|
||||
let driver;
|
||||
|
@ -41,8 +43,10 @@ describe('www-integration sign-in-and-out', () => {
|
|||
await name.sendKeys(username);
|
||||
let word = await findByXpath('//input[@id="frc-password-1088"]');
|
||||
await word.sendKeys(password);
|
||||
await driver.sleep(500);
|
||||
await clickXpath('//button[contains(@class, "button") and ' +
|
||||
'contains(@class, "submit-button") and contains(@class, "white")]');
|
||||
await driver.sleep(500);
|
||||
let element = await findByXpath('//span[contains(@class, "profile-name")]');
|
||||
let text = await element.getText();
|
||||
await expect(text.toLowerCase()).toEqual(username.toLowerCase());
|
||||
|
@ -70,8 +74,10 @@ describe('www-integration sign-in-and-out', () => {
|
|||
await name.sendKeys(username);
|
||||
let word = await findByXpath('//input[@id="frc-password-1088"]');
|
||||
await word.sendKeys(password);
|
||||
await driver.sleep(500);
|
||||
await clickXpath('//button[contains(@class, "button") and ' +
|
||||
'contains(@class, "submit-button") and contains(@class, "white")]');
|
||||
await driver.sleep(500);
|
||||
});
|
||||
|
||||
test('sign out on www', async () => {
|
||||
|
|
33
test/unit/components/commenting-status.test.jsx
Normal file
33
test/unit/components/commenting-status.test.jsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
const React = require('react');
|
||||
const {shallowWithIntl} = require('../../helpers/intl-helpers.jsx');
|
||||
const CommentingStatus = require('../../../src/components/commenting-status/commenting-status.jsx');
|
||||
|
||||
describe('CommentingStatus', () => {
|
||||
test('Basic render', () => {
|
||||
const component = shallowWithIntl(
|
||||
<CommentingStatus />
|
||||
);
|
||||
expect(component.find('div.commenting-status').exists()).toBe(true);
|
||||
expect(component.find('img.comment-status-icon').exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('ClassNames added', () => {
|
||||
const component = shallowWithIntl(
|
||||
<CommentingStatus
|
||||
className="class1"
|
||||
innerClassName="class2"
|
||||
/>
|
||||
);
|
||||
expect(component.find('div.class1').exists()).toBe(true);
|
||||
expect(component.find('div.class2').exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('Children added', () => {
|
||||
const component = shallowWithIntl(
|
||||
<CommentingStatus>
|
||||
<img className="myChildDiv" />
|
||||
</CommentingStatus>
|
||||
);
|
||||
expect(component.find('img.myChildDiv').exists()).toBe(true);
|
||||
});
|
||||
});
|
93
test/unit/components/mute-modal.test.jsx
Normal file
93
test/unit/components/mute-modal.test.jsx
Normal file
|
@ -0,0 +1,93 @@
|
|||
import React from 'react';
|
||||
import {shallowWithIntl} from '../../helpers/intl-helpers.jsx';
|
||||
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
|
||||
import MuteModal from '../../../src/components/modal/mute/modal';
|
||||
import Modal from '../../../src/components/modal/base/modal';
|
||||
|
||||
|
||||
describe('MuteModalTest', () => {
|
||||
|
||||
test('Mute Modal rendering', () => {
|
||||
const component = shallowWithIntl(
|
||||
<MuteModal />
|
||||
);
|
||||
expect(component.find('div.mute-modal-header').exists()).toEqual(true);
|
||||
|
||||
});
|
||||
|
||||
test('Mute Modal only shows next button on initial step', () => {
|
||||
const component = mountWithIntl(
|
||||
<MuteModal />
|
||||
);
|
||||
expect(component.find('div.mute-nav').exists()).toEqual(true);
|
||||
expect(component.find('button.next-button').exists()).toEqual(true);
|
||||
expect(component.find('button.next-button').getElements()[0].props.onClick)
|
||||
.toEqual(component.instance().handleNext);
|
||||
expect(component.find('button.close-button').exists()).toEqual(false);
|
||||
expect(component.find('button.back-button').exists()).toEqual(false);
|
||||
});
|
||||
|
||||
test('Mute Modal shows back & close button on last step', () => {
|
||||
const component = mountWithIntl(
|
||||
<MuteModal />
|
||||
);
|
||||
// Step 1 is the last step.
|
||||
component.instance().setState({step: 1});
|
||||
component.update();
|
||||
|
||||
expect(component.find('div.mute-nav').exists()).toEqual(true);
|
||||
expect(component.find('button.next-button').exists()).toEqual(false);
|
||||
expect(component.find('button.back-button').exists()).toEqual(true);
|
||||
expect(component.find('button.back-button').getElements()[0].props.onClick)
|
||||
.toEqual(component.instance().handlePrevious);
|
||||
expect(component.find('button.close-button').exists()).toEqual(true);
|
||||
expect(component.find('button.close-button').getElements()[0].props.onClick)
|
||||
.toEqual(component.instance().props.onRequestClose);
|
||||
});
|
||||
|
||||
test('Mute modal sends correct props to Modal', () => {
|
||||
const closeFn = jest.fn();
|
||||
const component = shallowWithIntl(
|
||||
<MuteModal
|
||||
onRequestClose={closeFn}
|
||||
/>
|
||||
);
|
||||
const modal = component.find(Modal);
|
||||
expect(modal).toHaveLength(1);
|
||||
expect(modal.props().showCloseButton).toBe(false);
|
||||
expect(modal.props().isOpen).toBe(true);
|
||||
expect(modal.props().className).toBe('modal-mute');
|
||||
expect(modal.props().onRequestClose).toBe(closeFn);
|
||||
});
|
||||
|
||||
test('Mute modal handle next step', () => {
|
||||
const closeFn = jest.fn();
|
||||
const component = shallowWithIntl(
|
||||
<MuteModal
|
||||
onRequestClose={closeFn}
|
||||
/>
|
||||
);
|
||||
expect(component.instance().state.step).toBe(0);
|
||||
component.instance().handleNext();
|
||||
expect(component.instance().state.step).toBe(1);
|
||||
});
|
||||
|
||||
test('Mute modal handle previous step', () => {
|
||||
const component = shallowWithIntl(
|
||||
<MuteModal />
|
||||
);
|
||||
component.instance().setState({step: 1});
|
||||
|
||||
component.instance().handlePrevious();
|
||||
expect(component.instance().state.step).toBe(0);
|
||||
});
|
||||
|
||||
test('Mute modal handle previous step stops at 0', () => {
|
||||
const component = shallowWithIntl(
|
||||
<MuteModal />
|
||||
);
|
||||
component.instance().setState({step: 0});
|
||||
component.instance().handlePrevious();
|
||||
expect(component.instance().state.step).toBe(0);
|
||||
});
|
||||
});
|
49
test/unit/components/mute-step.test.jsx
Normal file
49
test/unit/components/mute-step.test.jsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
import React from 'react';
|
||||
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
|
||||
import MuteStep from '../../../src/components/modal/mute/mute-step';
|
||||
|
||||
describe('MuteStepTest', () => {
|
||||
test('Mute Step with no images ', () => {
|
||||
const component = mountWithIntl(
|
||||
<MuteStep
|
||||
header="header text"
|
||||
|
||||
/>
|
||||
);
|
||||
expect(component.find('div.mute-step').exists()).toEqual(true);
|
||||
expect(component.find('div.mute-header').exists()).toEqual(true);
|
||||
expect(component.find('div.mute-right-column').exists()).toEqual(true);
|
||||
// No images and no left column.
|
||||
expect(component.find('img').exists()).toEqual(false);
|
||||
expect(component.find('div.left-column').exists()).toEqual(false);
|
||||
|
||||
});
|
||||
|
||||
test('Mute Step with side image ', () => {
|
||||
const component = mountWithIntl(
|
||||
<MuteStep
|
||||
sideImg="/path/to/img.png"
|
||||
sideImgClass="side-img"
|
||||
/>
|
||||
);
|
||||
expect(component.find('div.mute-step').exists()).toEqual(true);
|
||||
expect(component.find('div.mute-header').exists()).toEqual(true);
|
||||
expect(component.find('div.mute-right-column').exists()).toEqual(true);
|
||||
expect(component.find('div.left-column').exists()).toEqual(true);
|
||||
expect(component.find('img.side-img').exists()).toEqual(true);
|
||||
|
||||
});
|
||||
|
||||
test('Mute Step with bottom image ', () => {
|
||||
const component = mountWithIntl(
|
||||
<MuteStep
|
||||
bottomImg="/path/to/img.png"
|
||||
bottomImgClass="bottom-image"
|
||||
/>
|
||||
);
|
||||
expect(component.find('div.mute-step').exists()).toEqual(true);
|
||||
expect(component.find('div.mute-header').exists()).toEqual(true);
|
||||
expect(component.find('div.mute-right-column').exists()).toEqual(true);
|
||||
expect(component.find('img.bottom-image').exists()).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
const React = require('react');
|
||||
const {shallowWithIntl} = require('../../helpers/intl-helpers.jsx');
|
||||
const {shallowWithIntl, mountWithIntl} = require('../../helpers/intl-helpers.jsx');
|
||||
import configureStore from 'redux-mock-store';
|
||||
const Navigation = require('../../../src/components/navigation/www/navigation.jsx');
|
||||
const Registration = require('../../../src/components/registration/registration.jsx');
|
||||
|
@ -11,6 +11,7 @@ describe('Navigation', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
store = null;
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
const getNavigationWrapper = props => {
|
||||
|
@ -103,4 +104,123 @@ describe('Navigation', () => {
|
|||
navWrapper.find('a.registrationLink').simulate('click', {preventDefault () {}});
|
||||
expect(navInstance.props.handleClickRegistration).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Component sets up message polling when it mounts', () => {
|
||||
store = mockStore({
|
||||
navigation: {
|
||||
registrationOpen: false
|
||||
},
|
||||
messageCount: {
|
||||
messageCount: 5
|
||||
}
|
||||
});
|
||||
const props = {
|
||||
user: {
|
||||
thumbnailUrl: 'scratch.mit.edu',
|
||||
username: 'auser'
|
||||
},
|
||||
getMessageCount: jest.fn()
|
||||
};
|
||||
const intlWrapper = mountWithIntl(
|
||||
<Navigation
|
||||
{...props}
|
||||
/>, {context: {store},
|
||||
childContextTypes: {store}
|
||||
});
|
||||
|
||||
const navInstance = intlWrapper.children().find('Navigation')
|
||||
.instance();
|
||||
const twoMin = 2 * 60 * 1000;
|
||||
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), twoMin);
|
||||
expect(navInstance.messageCountTimeoutId).not.toEqual(-1);
|
||||
// Advance timers passed the intial two minutes.
|
||||
jest.advanceTimersByTime(twoMin + 1);
|
||||
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), twoMin * 2);
|
||||
expect(props.getMessageCount).toHaveBeenCalled();
|
||||
expect(navInstance.messageCountTimeoutId).not.toEqual(-1);
|
||||
});
|
||||
test('Component cancels timers when it unmounts', () => {
|
||||
store = mockStore({
|
||||
navigation: {
|
||||
registrationOpen: false
|
||||
},
|
||||
messageCount: {
|
||||
messageCount: 5
|
||||
}
|
||||
});
|
||||
const props = {
|
||||
user: {
|
||||
thumbnailUrl: 'scratch.mit.edu',
|
||||
username: 'auser'
|
||||
},
|
||||
getMessageCount: jest.fn()
|
||||
};
|
||||
const intlWrapper = mountWithIntl(
|
||||
<Navigation
|
||||
{...props}
|
||||
/>, {context: {store},
|
||||
childContextTypes: {store}
|
||||
});
|
||||
|
||||
const navInstance = intlWrapper.children().find('Navigation')
|
||||
.instance();
|
||||
const twoMin = 2 * 60 * 1000;
|
||||
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), twoMin);
|
||||
expect(navInstance.messageCountTimeoutId).not.toEqual(-1);
|
||||
navInstance.componentWillUnmount();
|
||||
expect(clearTimeout).toHaveBeenCalledWith(expect.any(Number));
|
||||
expect(navInstance.messageCountTimeoutId).toEqual(-1);
|
||||
});
|
||||
|
||||
test('pollForMessages polls for messages 5 times', () => {
|
||||
store = mockStore({
|
||||
navigation: {
|
||||
registrationOpen: false
|
||||
},
|
||||
messageCount: {
|
||||
messageCount: 5
|
||||
}
|
||||
});
|
||||
const props = {
|
||||
user: {
|
||||
thumbnailUrl: 'scratch.mit.edu',
|
||||
username: 'auser'
|
||||
},
|
||||
getMessageCount: jest.fn()
|
||||
};
|
||||
const intlWrapper = mountWithIntl(
|
||||
<Navigation
|
||||
{...props}
|
||||
/>, {context: {store},
|
||||
childContextTypes: {store}
|
||||
});
|
||||
|
||||
const navInstance = intlWrapper.children().find('Navigation')
|
||||
.instance();
|
||||
// Clear the timers and mocks because componentDidMount
|
||||
// has already called pollForMessages.
|
||||
jest.clearAllTimers();
|
||||
jest.clearAllMocks();
|
||||
let twoMinInMs = 2 * 60 * 1000; // 2 minutes in ms.
|
||||
navInstance.pollForMessages(twoMinInMs);
|
||||
|
||||
expect(navInstance.messageCountTimeoutId).not.toEqual(-1);
|
||||
// Check that we set the timeout to backoff exponentially
|
||||
for (let count = 1; count < 5; ++count) {
|
||||
jest.advanceTimersByTime(twoMinInMs + 1);
|
||||
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), twoMinInMs * 2);
|
||||
expect(props.getMessageCount).toHaveBeenCalledTimes(count);
|
||||
twoMinInMs = twoMinInMs * 2;
|
||||
}
|
||||
|
||||
// Exhaust all timers (there shouldn't be any left)
|
||||
jest.runAllTimers();
|
||||
// We exponentially back off checking for messages, starting at 2 min
|
||||
// and stop after 32 minutes so it should happen 5 times total.
|
||||
expect(props.getMessageCount).toHaveBeenCalledTimes(5);
|
||||
// setTimeout happens 1 fewer since the original call to
|
||||
// pollForMessages isn't counted here.
|
||||
expect(setTimeout).toHaveBeenCalledTimes(4);
|
||||
expect(navInstance.messageCountTimeoutId).not.toEqual(-1);
|
||||
});
|
||||
});
|
||||
|
|
53
test/unit/lib/jar.test.js
Normal file
53
test/unit/lib/jar.test.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
const jar = require('../../../src/lib/jar');
|
||||
const cookie = require('cookie');
|
||||
|
||||
jest.mock('cookie', () => ({serialize: jest.fn()}));
|
||||
describe('unit test lib/jar.js', () => {
|
||||
|
||||
test('simple set test with no opts', () => {
|
||||
jar.set('name', 'value');
|
||||
expect(cookie.serialize).toHaveBeenCalled();
|
||||
expect(cookie.serialize).toHaveBeenCalledWith('name', 'value',
|
||||
expect.objectContaining({
|
||||
path: '/',
|
||||
sameSite: 'Lax',
|
||||
expires: expect.anything() // not specifically matching the date because it is hard to mock
|
||||
}));
|
||||
});
|
||||
test('test with opts', () => {
|
||||
jar.set('a', 'b', {option: 'one'});
|
||||
expect(cookie.serialize).toHaveBeenCalled();
|
||||
expect(cookie.serialize).toHaveBeenCalledWith('a', 'b',
|
||||
expect.objectContaining({
|
||||
option: 'one',
|
||||
path: '/',
|
||||
sameSite: 'Lax',
|
||||
expires: expect.anything() // not specifically matching the date because it is hard to mock
|
||||
}));
|
||||
});
|
||||
test('expires opts overrides default', () => {
|
||||
jar.set('a', 'b', {
|
||||
option: 'one',
|
||||
expires: 'someday'
|
||||
});
|
||||
expect(cookie.serialize).toHaveBeenCalled();
|
||||
expect(cookie.serialize).toHaveBeenCalledWith('a', 'b',
|
||||
expect.objectContaining({
|
||||
option: 'one',
|
||||
path: '/',
|
||||
expires: 'someday'
|
||||
}));
|
||||
});
|
||||
test('sameSite opts overrides default', () => {
|
||||
jar.set('a', 'b', {
|
||||
option: 'one',
|
||||
sameSite: 'override'
|
||||
});
|
||||
expect(cookie.serialize).toHaveBeenCalled();
|
||||
expect(cookie.serialize).toHaveBeenCalledWith('a', 'b',
|
||||
expect.objectContaining({
|
||||
option: 'one',
|
||||
sameSite: 'override'
|
||||
}));
|
||||
});
|
||||
});
|
19
test/unit/lib/scratch-wiki.test.js
Normal file
19
test/unit/lib/scratch-wiki.test.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
const getScratchWikiLink = require('../../../src/lib/scratch-wiki');
|
||||
|
||||
describe('unit test lib/scratch-wiki.js', () => {
|
||||
test('getScratchWikiLink exists', () => {
|
||||
expect(typeof getScratchWikiLink).toBe('function');
|
||||
});
|
||||
|
||||
test('it returns link to jawiki when ja is given', () => {
|
||||
expect(getScratchWikiLink('ja')).toBe('https://ja.scratch-wiki.info/');
|
||||
});
|
||||
|
||||
test('it returns link to jawiki when ja-Hira is given', () => {
|
||||
expect(getScratchWikiLink('ja-Hira')).toBe('https://ja.scratch-wiki.info/');
|
||||
});
|
||||
|
||||
test('it returns link to enwiki when invalid locale is given', () => {
|
||||
expect(getScratchWikiLink('test')).toBe('https://en.scratch-wiki.info/');
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue