Merge branch 'develop' of https://github.com/LLK/scratch-www into load-24-projects-per-page

# Conflicts:
#	src/views/studio/modals/user-projects-modal.jsx
This commit is contained in:
Eric Rosenbaum 2021-05-24 17:18:09 -04:00
commit a390c0655f
33 changed files with 709 additions and 365 deletions

84
package-lock.json generated
View file

@ -1368,23 +1368,6 @@
}
}
},
"@formatjs/ecma402-abstract": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.9.0.tgz",
"integrity": "sha512-hKWk3t4uKmGW1kS6lR8j3vzHhyK3oXb/sgQ6YImsHLen8FFbmPDEEiwz6geNaKtEioCGYFF1B2BYLBH8JjbFxQ==",
"dev": true,
"requires": {
"tslib": "^2.1.0"
},
"dependencies": {
"tslib": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==",
"dev": true
}
}
},
"@formatjs/intl-getcanonicallocales": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@formatjs/intl-getcanonicallocales/-/intl-getcanonicallocales-1.7.0.tgz",
@ -1404,17 +1387,26 @@
}
},
"@formatjs/intl-locale": {
"version": "2.4.27",
"resolved": "https://registry.npmjs.org/@formatjs/intl-locale/-/intl-locale-2.4.27.tgz",
"integrity": "sha512-pUq/0jXqKhFE1j19/TAgHAptrmS2J7CUlR04fov/fF/VV+J1+RMh2Uh9pkp7mZktROQ9t3YfqZIv6oyph+C1Ng==",
"version": "2.4.28",
"resolved": "https://registry.npmjs.org/@formatjs/intl-locale/-/intl-locale-2.4.28.tgz",
"integrity": "sha512-z20qVhgtHFTCGLDCl/pWs3cdnxGT4whsbjxwfrhyF2Qf0TNYWrJ/y88f3vINJ19iGVY3GJ6bxaRI5d+uyQ/7ig==",
"dev": true,
"requires": {
"@formatjs/ecma402-abstract": "1.9.0",
"@formatjs/ecma402-abstract": "1.9.1",
"@formatjs/intl-getcanonicallocales": "1.7.0",
"cldr-core": "38",
"tslib": "^2.1.0"
},
"dependencies": {
"@formatjs/ecma402-abstract": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.9.1.tgz",
"integrity": "sha512-XAJ1ygWKgGEaFuNg3Cf+maJNYEJjl5LjSVZ1iAnSaOKDg/VXa+dDPWhWQP6jimvWv6h9NyDj6Zgh+2qFBeVABw==",
"dev": true,
"requires": {
"tslib": "^2.1.0"
}
},
"tslib": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
@ -1424,15 +1416,24 @@
}
},
"@formatjs/intl-pluralrules": {
"version": "4.0.21",
"resolved": "https://registry.npmjs.org/@formatjs/intl-pluralrules/-/intl-pluralrules-4.0.21.tgz",
"integrity": "sha512-SEouCAFL1QLVfFWCsZFNYtWXU05Q7wIaq9kGQujT25iy7eT53kvt9SVE2DySHooe56y9VBt4jS0YKPzP+BLTug==",
"version": "4.0.22",
"resolved": "https://registry.npmjs.org/@formatjs/intl-pluralrules/-/intl-pluralrules-4.0.22.tgz",
"integrity": "sha512-4pSxb31AsjZXUjQHid9eJuUJrXqLOH3tgfiryvBfgNoS76cqk0cFUAuTGdC07YQZlVuJ4c3K4rqBlRpFJwn4Mg==",
"dev": true,
"requires": {
"@formatjs/ecma402-abstract": "1.9.0",
"@formatjs/ecma402-abstract": "1.9.1",
"tslib": "^2.1.0"
},
"dependencies": {
"@formatjs/ecma402-abstract": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.9.1.tgz",
"integrity": "sha512-XAJ1ygWKgGEaFuNg3Cf+maJNYEJjl5LjSVZ1iAnSaOKDg/VXa+dDPWhWQP6jimvWv6h9NyDj6Zgh+2qFBeVABw==",
"dev": true,
"requires": {
"tslib": "^2.1.0"
}
},
"tslib": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
@ -20865,9 +20866,9 @@
}
},
"scratch-gui": {
"version": "0.1.0-prerelease.20210521035500",
"resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20210521035500.tgz",
"integrity": "sha512-wo7kKITvLVhsOJYag9LjKdeURfwdcGlDaIkkW0+uq5OioDw+7OkH5ejdE+5HKByHVuo2vbSLc3VEL5JAThlVwQ==",
"version": "0.1.0-prerelease.20210522033750",
"resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20210522033750.tgz",
"integrity": "sha512-P32L88nVkWgnlOYZ637+wMxKor2qphP0UmOlFVMwr38UVfvovUjj+k/lp/60SX/coalCByKHdZTOuboKZcggNQ==",
"dev": true,
"requires": {
"arraybuffer-loader": "^1.0.6",
@ -20919,7 +20920,7 @@
"redux-throttle": "0.1.1",
"scratch-audio": "0.1.0-prerelease.20200528195344",
"scratch-blocks": "0.1.0-prerelease.20210521033213",
"scratch-l10n": "3.11.20210521031700",
"scratch-l10n": "3.11.20210522031558",
"scratch-paint": "0.2.0-prerelease.20210407203313",
"scratch-render": "0.1.0-prerelease.20210325231800",
"scratch-render-fonts": "1.0.0-prerelease.20210401210003",
@ -21088,9 +21089,9 @@
"dev": true
},
"electron-to-chromium": {
"version": "1.3.735",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.735.tgz",
"integrity": "sha512-cp7MWzC3NseUJV2FJFgaiesdrS+A8ZUjX5fLAxdRlcaPDkaPGFplX930S5vf84yqDp4LjuLdKouWuVOTwUfqHQ==",
"version": "1.3.736",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.736.tgz",
"integrity": "sha512-DY8dA7gR51MSo66DqitEQoUMQ0Z+A2DSXFi7tK304bdTVqczCAfUuyQw6Wdg8hIoo5zIxkU1L24RQtUce1Ioig==",
"dev": true
},
"has-flag": {
@ -21310,6 +21311,19 @@
"integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=",
"dev": true
},
"scratch-l10n": {
"version": "3.11.20210522031558",
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.11.20210522031558.tgz",
"integrity": "sha512-An6Synzs0MzFnAMDaZpUL7wttZzysq0r/2MWeKO6CdU7+1AVmFDAxtCP/h5xdGzoNhxf8Vy02LvSfn4ofYZDCA==",
"dev": true,
"requires": {
"@babel/cli": "^7.1.2",
"@babel/core": "^7.1.2",
"babel-plugin-react-intl": "^3.0.1",
"react-intl": "^2.8.0",
"transifex": "1.6.6"
}
},
"scratch-storage": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/scratch-storage/-/scratch-storage-1.3.4.tgz",
@ -21387,9 +21401,9 @@
}
},
"scratch-l10n": {
"version": "3.11.20210521031700",
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.11.20210521031700.tgz",
"integrity": "sha512-iY1xA8yZvjn0BpEOtKeiICuyaMlw7jmn5aO6P42DqWfVl9Uye34XBzeZJrMoFsUjhKZXXr7+IE2KUdiet5+7Yw==",
"version": "3.11.20210524031609",
"resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.11.20210524031609.tgz",
"integrity": "sha512-J6fqjT9QAMACI98mk58rn48nKoQNzuP2LiEt5GgkSw+kTt/kKJTYTtT2C2hOnYJZcZrTyPFwAWTyi68sxg23xQ==",
"dev": true,
"requires": {
"@babel/cli": "^7.1.2",

View file

@ -126,7 +126,7 @@
"redux-mock-store": "^1.2.3",
"redux-thunk": "2.0.1",
"sass-loader": "6.0.6",
"scratch-gui": "0.1.0-prerelease.20210521035500",
"scratch-gui": "0.1.0-prerelease.20210522033750",
"scratch-l10n": "latest",
"selenium-webdriver": "3.6.0",
"slick-carousel": "1.6.0",

View file

@ -0,0 +1,41 @@
import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import {FormattedMessage} from 'react-intl';
import Button from '../../components/forms/button.jsx';
import './alert.scss';
const AlertComponent = ({className, icon, id, values, onClear}) => (
<div className="alert-wrapper">
<div
className={classNames('alert', className)}
>
{icon && <img
className="alert-icon"
src={icon}
/>}
<div className="alert-msg">
<FormattedMessage
id={id}
values={values}
/>
</div>
{onClear && <Button
className="alert-close-button"
isCloseType
onClick={onClear}
/>}
</div>
</div>
);
AlertComponent.propTypes = {
className: PropTypes.string,
icon: PropTypes.string,
id: PropTypes.string.isRequired,
values: PropTypes.shape({}),
onClear: PropTypes.func
};
export default AlertComponent;

View file

@ -0,0 +1,18 @@
import {createContext, useContext} from 'react';
import AlertStatus from './alert-status.js';
const AlertContext = createContext({
// Note: defaults here are only used if there is no Provider in the tree
status: AlertStatus.NONE,
data: {},
clearAlert: () => {},
successAlert: () => {},
errorAlert: () => {}
});
const useAlertContext = () => useContext(AlertContext);
export {
AlertContext as default,
useAlertContext
};

View file

@ -0,0 +1,54 @@
import React, {useRef, useState} from 'react';
import PropTypes from 'prop-types';
import AlertStatus from './alert-status.js';
import AlertContext from './alert-context.js';
const AlertProvider = ({children}) => {
const defaultState = {
status: AlertStatus.NONE,
data: {},
showClear: false
};
const [state, setState] = useState(defaultState);
const timeoutRef = useRef(null);
const clearAlert = () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
timeoutRef.current = null;
setState(defaultState);
};
const handleAlert = (status, data, timeoutSeconds = 3) => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
setState({status, data, showClear: !timeoutSeconds});
if (timeoutSeconds) {
timeoutRef.current = setTimeout(() => {
timeoutRef.current = null;
setState(defaultState);
}, timeoutSeconds * 1000);
}
};
return (
<AlertContext.Provider
value={{
status: state.status,
data: state.data,
showClear: state.showClear,
clearAlert: clearAlert,
successAlert: (newData, timeoutSeconds = 3) =>
handleAlert(AlertStatus.SUCCESS, newData, timeoutSeconds),
errorAlert: (newData, timeoutSeconds = 3) =>
handleAlert(AlertStatus.ERROR, newData, timeoutSeconds)
}}
>
{children}
</AlertContext.Provider>
);
};
AlertProvider.propTypes = {
children: PropTypes.node
};
export default AlertProvider;

View file

@ -0,0 +1,5 @@
export default {
NONE: 'NONE',
SUCCESS: 'SUCCESS',
ERROR: 'ERROR'
};

View file

@ -0,0 +1,33 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import AlertComponent from './alert-component.jsx';
import AlertStatus from './alert-status.js';
import {useAlertContext} from './alert-context.js';
import successIcon from './icon-alert-success.svg';
import errorIcon from './icon-alert-error.svg';
const Alert = ({className}) => {
const {status, data, showClear, clearAlert} = useAlertContext();
if (status === AlertStatus.NONE) return null;
return (
<AlertComponent
className={classNames(className, {
'alert-success': status === AlertStatus.SUCCESS,
'alert-error': status === AlertStatus.ERROR
})}
icon={status === AlertStatus.SUCCESS ? successIcon : errorIcon}
id={data.id}
values={data.values}
onClear={showClear && clearAlert}
/>
);
};
Alert.propTypes = {
className: PropTypes.string
};
export default Alert;

View file

@ -0,0 +1,40 @@
.alert-wrapper {
position: absolute;
display: flex;
width: 100%;
justify-content: center;
z-index: 100;
pointer-events: none;
.alert {
display: flex;
box-sizing: border-box;
padding: 10px 20px;
border-radius: 8px;
align-items: center;
margin-top: 1rem;
min-height: 60px;
pointer-events: auto;
&.alert-error {
background: #FFF0DF;
border: 1px solid #FF8C1A;
box-shadow: 0px 0px 0px 2px rgba(255, 140, 26, 0.25)
}
&.alert-success {
background: #CEF2E8;
border: 1px solid #0EBD8C;
box-shadow: 0px 0px 0px rgba(14, 189, 140, 0.25);
}
.alert-msg {
font-size: 14px;
font-weight: bold;
}
.alert-close-button {
position: unset;
margin-left: 1rem;
}
}
}

View file

@ -0,0 +1,3 @@
<svg width="28" height="20" viewBox="-2 -1 15 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.40571 6.50912C1.50472 6.50912 0.775271 7.23857 0.775271 8.13956C0.775271 9.04055 1.50472 9.77 2.40571 9.77C3.3067 9.77 4.03615 9.04055 4.03615 8.13956C4.03615 7.23857 3.3067 6.50912 2.40571 6.50912ZM3.34168 5.02359C2.92699 5.9523 1.88444 5.9523 1.46975 5.02359L0.145744 2.07519C-0.268945 1.15289 0.250665 0 1.08171 0H3.72972C4.56076 0 5.08037 1.15289 4.66568 2.07519L3.34168 5.02359Z" fill="#FF8C1A"/>
</svg>

After

(image error) Size: 559 B

View file

@ -0,0 +1,9 @@
<svg width="28" height="20" viewBox="0 0 28 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.86144 15.403C7.43527 15.403 7.0091 15.2398 6.68447 14.9152L3.48818 11.7189C2.83727 11.068 2.83727 10.0159 3.48818 9.36498C4.13909 8.71407 5.19121 8.71407 5.84212 9.36498L7.86144 11.3843L14.1591 5.08828C14.8084 4.43737 15.8622 4.43737 16.5131 5.08828C17.1623 5.73753 17.1623 6.7913 16.5131 7.44222L9.03841 14.9152C8.71378 15.2398 8.28761 15.403 7.86144 15.403Z" fill="#575E75"/>
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="3" y="4" width="14" height="12">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.86144 15.403C7.43527 15.403 7.0091 15.2398 6.68447 14.9152L3.48818 11.7189C2.83727 11.068 2.83727 10.0159 3.48818 9.36498C4.13909 8.71407 5.19121 8.71407 5.84212 9.36498L7.86144 11.3843L14.1591 5.08828C14.8084 4.43737 15.8622 4.43737 16.5131 5.08828C17.1623 5.73753 17.1623 6.7913 16.5131 7.44222L9.03841 14.9152C8.71378 15.2398 8.28761 15.403 7.86144 15.403Z" fill="white"/>
</mask>
<g mask="url(#mask0)">
<rect width="20" height="20" fill="#0FBD8C"/>
</g>
</svg>

After

(image error) Size: 1.1 KiB

View file

@ -135,6 +135,7 @@ class Navigation extends React.Component {
/>
<Input
aria-label={this.props.intl.formatMessage({id: 'general.search'})}
className="search-wrapper"
name="q"
placeholder={this.props.intl.formatMessage({id: 'general.search'})}
type="text"

View file

@ -12,7 +12,7 @@
flex-wrap: wrap;
li {
li, button {
display: inline-block;
margin: 5px;
border: 1px solid $active-gray;

View file

@ -128,6 +128,7 @@ module.exports.selectToken = state => get(state, ['session', 'session', 'user',
module.exports.selectIsAdmin = state => get(state, ['session', 'session', 'permissions', 'admin'], false);
module.exports.selectIsSocial = state => get(state, ['session', 'session', 'permissions', 'social'], false);
module.exports.selectIsEducator = state => get(state, ['session', 'session', 'permissions', 'educator'], false);
module.exports.selectHasFetchedSession = state => state.session.status === module.exports.Status.FETCHED;
// NB logged out user id as NaN so that it can never be used in equality testing since NaN !== NaN
module.exports.selectUserId = state => get(state, ['session', 'session', 'user', 'id'], NaN);

View file

@ -25,7 +25,7 @@
"name": "conference-index",
"pattern": "^/conference/?(\\?.*)?$",
"routeAlias": "/conference(?!/201[4-9])",
"redirect": "/conference/2020"
"redirect": "/conference/2021"
},
{
"name": "conference-index-2017",
@ -83,6 +83,12 @@
"name": "conference-index-2020",
"pattern": "^/conference/2020/?$",
"routeAlias": "/conference(?!/201[4-9])",
"redirect": "/conference/2021"
},
{
"name": "conference-index-2021",
"pattern": "^/conference/2021/?$",
"routeAlias": "/conference(?!/201[4-9])",
"view": "conference/2020/index/index",
"title": "Scratch Conferences",
"viewportWidth": "device-width"

View file

@ -14,12 +14,43 @@ const SubNavigation = require('../../components/subnavigation/subnavigation.jsx'
const Select = require('../../components/forms/select.jsx');
const OverflowMenu = require('../../components/overflow-menu/overflow-menu.jsx').default;
const exampleIcon = require('./example-icon.svg');
const AlertProvider = require('../../components/alert/alert-provider.jsx').default;
const {useAlertContext} = require('../../components/alert/alert-context.js');
const Alert = require('../../components/alert/alert.jsx').default;
require('./components.scss');
/* eslint-disable react/prop-types, react/jsx-no-bind */
/* Demo of how to use the useAlertContext hook */
const AlertButton = ({type, timeoutSeconds}) => {
const {errorAlert, successAlert} = useAlertContext();
const onClick = type === 'success' ?
() => successAlert({id: 'success-alert.string.id'}, timeoutSeconds) :
() => errorAlert({id: 'error-alert.string.id'}, timeoutSeconds);
return (
<Button onClick={onClick}>
{type}, {timeoutSeconds || 'no '} timeout
</Button>
);
};
const Components = () => (
<div className="components">
<div className="inner">
<h1>Alert Provider, Display and Hooks</h1>
<AlertProvider>
<div style={{position: 'relative', minHeight: '200px', border: '1px solid red'}}>
<Alert />
<div><AlertButton
type="success"
timeoutSeconds={3}
/></div>
<div><AlertButton
type="error"
timeoutSeconds={null}
/></div>
</div>
</AlertProvider>
<h1>Overflow Menu</h1>
<div className="example-tile">
<OverflowMenu>

View file

@ -26,7 +26,11 @@
"studio.projectsEmpty1": "This studio has no projects yet.",
"studio.projectsEmpty2": "Suggest projects you want to add in the comments!",
"studio.browseProjects": "Browse Projects",
"studio.projectErrors.checkUrl": "Could not add project. Check the URL and try again.",
"studio.projectErrors.checkUrl": "Could not find that project. Check the URL and try again.",
"studio.projectErrors.generic": "Could not add project.",
"studio.projectErrors.tooFast": "You are adding projects too quickly.",
"studio.projectErrors.permission": "You do not have permission to add that project.",
"studio.projectErrors.duplicate": "That project is already in this studio.",
"studio.creatorRole": "Studio Creator",
@ -73,5 +77,15 @@
"studio.reportPleaseExplain": "Please select which part of the studio you find to be disrespectful or inappropriate, or otherwise breaks the Scratch Community Guidelines.",
"studio.reportAreThereComments": "Are there inappropriate comments in the studio? Please report them by clicking the \"report\" button on the individual comments.",
"studio.reportThanksForLettingUsKnow": "Thanks for letting us know!",
"studio.reportYourFeedback": "Your feedback will help us make Scratch better."
"studio.reportYourFeedback": "Your feedback will help us make Scratch better.",
"studio.alertProjectAdded": "\"{title}\" added to studio",
"studio.alertProjectAlreadyAdded": "That project is already in this studio",
"studio.alertProjectRemoveError": "Something went wrong removing the project",
"studio.alertProjectAddError": "Something went wrong adding the project",
"studio.alertCuratorAlreadyInvited": "\"{name}\" has already been invited",
"studio.alertCuratorInvited": "Curator invite sent to \"{name}\"",
"studio.alertManagerPromote": "\"{name}\" is now a manager",
"studio.alertManagerPromoteError": "Something went wrong promoting \"{name}\"",
"studio.alertMemberRemoveError": "Something went wrong removing \"{name}\""
}

View file

@ -121,8 +121,6 @@ const inviteCurator = username => ((dispatch, getState) => new Promise((resolve,
}, (err, body, res) => {
const error = normalizeError(err, body, res);
if (error) return reject(error);
// eslint-disable-next-line no-alert
alert(`successfully invited ${username}`);
return resolve(username);
});
}));

View file

@ -18,6 +18,8 @@ import UserProjectsTile from './user-projects-tile.jsx';
import './user-projects-modal.scss';
import {selectIsEducator} from '../../../redux/session';
import AlertProvider from '../../../components/alert/alert-provider.jsx';
import Alert from '../../../components/alert/alert.jsx';
const UserProjectsModal = ({
items, error, loading, moreToLoad, showStudentsFilter,
@ -44,60 +46,64 @@ const UserProjectsModal = ({
align="left"
className="user-projects-modal-nav"
>
<li
<button
className={classNames({active: filter === Filters.SHARED})}
onClick={() => setFilter(Filters.SHARED)}
>
<FormattedMessage id="studio.sharedFilter" />
</li>
<li
</button>
<button
className={classNames({active: filter === Filters.FAVORITED})}
onClick={() => setFilter(Filters.FAVORITED)}
>
<FormattedMessage id="studio.favoritedFilter" />
</li>
<li
</button>
<button
className={classNames({active: filter === Filters.RECENT})}
onClick={() => setFilter(Filters.RECENT)}
>
<FormattedMessage id="studio.recentFilter" />
</li>
</button>
{showStudentsFilter &&
<li
<button
className={classNames({active: filter === Filters.STUDENTS})}
onClick={() => setFilter(Filters.STUDENTS)}
>
<FormattedMessage id="studio.studentsFilter" />
</li>
</button>
}
</SubNavigation>
<ModalInnerContent className="user-projects-modal-content">
{error && <div>Error loading {filter}: {error}</div>}
<div className="user-projects-modal-grid">
{items.map(project => (
<UserProjectsTile
key={project.id}
id={project.id}
title={project.title}
image={project.image}
inStudio={project.inStudio}
onAdd={onAdd}
onRemove={onRemove}
/>
))}
</div>
{moreToLoad &&
<div className="studio-projects-load-more">
<button
className={classNames('button', {
'mod-mutating': loading
})}
onClick={() => onLoadMore(filter)}
>
<FormattedMessage id="general.loadMore" />
</button>
</div>
}
<AlertProvider>
{error && <div>Error loading {filter}: {error}</div>}
<Alert className="studio-alert" />
<div className="user-projects-modal-grid">
{items.map(project => (
<UserProjectsTile
key={project.id}
id={project.id}
title={project.title}
image={project.image}
inStudio={project.inStudio}
onAdd={onAdd}
onRemove={onRemove}
/>
))}
</div>
{moreToLoad &&
<div className="studio-projects-load-more">
<button
className={classNames('button', {
'mod-mutating': loading
})}
onClick={() => onLoadMore(filter)}
>
<FormattedMessage id="general.loadMore" />
</button>
</div>
}
</AlertProvider>
</ModalInnerContent>
</Modal>
);

View file

@ -13,7 +13,7 @@
}
.user-projects-modal-nav {
padding: 6px 12px;
li {
button {
cursor: pointer;
background: rgba(0, 0, 0, 0.15);
&.active { background: $ui-blue; }

View file

@ -1,23 +1,26 @@
/* eslint-disable react/jsx-no-bind */
import React, {useState} from 'react';
import React, {useContext, useState} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import AlertContext from '../../../components/alert/alert-context.js';
const UserProjectsTile = ({id, title, image, inStudio, onAdd, onRemove}) => {
const [submitting, setSubmitting] = useState(false);
const [added, setAdded] = useState(inStudio);
const [error, setError] = useState(null);
const {errorAlert} = useContext(AlertContext);
const toggle = () => {
setSubmitting(true);
setError(null);
(added ? onRemove(id) : onAdd(id))
.then(() => {
setAdded(!added);
setSubmitting(false);
})
.catch(e => {
setError(e);
.catch(() => {
setSubmitting(false);
errorAlert({
id: added ? 'studio.alertProjectRemoveError' :
'studio.alertProjectAddError'
}, null);
});
};
return (
@ -52,7 +55,6 @@ const UserProjectsTile = ({id, title, image, inStudio, onAdd, onRemove}) => {
}
/>
</div>
{error && <div>{error}</div>}
</div>
</div>
);

View file

@ -9,7 +9,7 @@ import TopLevelComment from '../preview/comment/top-level-comment.jsx';
import studioCommentActions from '../../redux/studio-comment-actions.js';
import StudioCommentsAllowed from './studio-comments-allowed.jsx';
import {selectIsAdmin} from '../../redux/session';
import {selectIsAdmin, selectHasFetchedSession} from '../../redux/session';
import {
selectShowCommentComposer,
selectCanDeleteComment,
@ -24,6 +24,7 @@ const StudioComments = ({
comments,
commentsAllowed,
isAdmin,
hasFetchedSession,
handleLoadMoreComments,
handleNewComment,
moreCommentsToLoad,
@ -42,8 +43,8 @@ const StudioComments = ({
handleLoadMoreReplies
}) => {
useEffect(() => {
if (comments.length === 0) handleLoadMoreComments();
}, [comments.length === 0]);
if (comments.length === 0 && hasFetchedSession) handleLoadMoreComments();
}, [comments.length === 0, hasFetchedSession]);
// The comments you see depend on your admin status
// so reset them if isAdmin changes.
@ -108,6 +109,7 @@ StudioComments.propTypes = {
comments: PropTypes.arrayOf(PropTypes.shape({})),
commentsAllowed: PropTypes.bool,
isAdmin: PropTypes.bool,
hasFetchedSession: PropTypes.bool,
handleLoadMoreComments: PropTypes.func,
handleNewComment: PropTypes.func,
moreCommentsToLoad: PropTypes.bool,
@ -133,6 +135,7 @@ export {
export default connect(
state => ({
comments: state.comments.comments,
hasFetchedSession: selectHasFetchedSession(state),
isAdmin: selectIsAdmin(state),
moreCommentsToLoad: state.comments.moreCommentsToLoad,
replies: state.comments.replies,

View file

@ -5,6 +5,7 @@ import {connect} from 'react-redux';
import classNames from 'classnames';
import {FormattedMessage, intlShape, injectIntl} from 'react-intl';
import {useAlertContext} from '../../components/alert/alert-context';
import {Errors, inviteCurator} from './lib/studio-member-actions';
import ValidationMessage from '../../components/forms/validation-message.jsx';
@ -24,12 +25,30 @@ const StudioCuratorInviter = ({intl, onSubmit}) => {
const [value, setValue] = useState('');
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState(null);
const {successAlert} = useAlertContext();
const submit = () => {
setSubmitting(true);
setError(null);
onSubmit(value)
.then(() => setValue(''))
.catch(e => setError(e))
.then(() => {
successAlert({
id: 'studio.alertCuratorInvited',
values: {name: value}
});
setValue('');
})
.catch(e => {
if (e === Errors.DUPLICATE) {
successAlert({
id: 'studio.alertCuratorAlreadyInvited',
values: {name: value}
});
setValue('');
} else {
setError(e);
}
})
.then(() => setSubmitting(false));
};
return (

View file

@ -11,6 +11,8 @@ import CuratorInviter from './studio-curator-inviter.jsx';
import CuratorInvite from './studio-curator-invite.jsx';
import {loadCurators} from './lib/studio-member-actions';
import {selectCanInviteCurators, selectShowCuratorInvite} from '../../redux/studio-permissions';
import AlertProvider from '../../components/alert/alert-provider.jsx';
import Alert from '../../components/alert/alert.jsx';
const StudioCurators = ({
canInviteCurators, showCuratorInvite, items, error, loading, moreToLoad, onLoadMore
@ -19,61 +21,65 @@ const StudioCurators = ({
if (items.length === 0) onLoadMore();
}, []);
return (<div className="studio-members">
<div className="studio-header-container">
<h2><FormattedMessage id="studio.curatorsHeader" /></h2>
</div>
{canInviteCurators && <CuratorInviter />}
{showCuratorInvite && <CuratorInvite />}
{error && <Debug
label="Error"
data={error}
/>}
<div className="studio-members-grid">
{items.length === 0 && !loading ? (
<div className="studio-empty">
<img
width="179"
height="111"
className="studio-empty-img"
src="/images/studios/curators-empty.png"
/>
{canInviteCurators ? (
<div className="studio-empty-msg">
<div><FormattedMessage id="studio.curatorsEmptyCanAdd1" /></div>
<div><FormattedMessage id="studio.curatorsEmptyCanAdd2" /></div>
return (
<AlertProvider>
<div className="studio-members">
<Alert className="studio-alert" />
<div className="studio-header-container">
<h2><FormattedMessage id="studio.curatorsHeader" /></h2>
</div>
{canInviteCurators && <CuratorInviter />}
{showCuratorInvite && <CuratorInvite />}
{error && <Debug
label="Error"
data={error}
/>}
<div className="studio-members-grid">
{items.length === 0 && !loading ? (
<div className="studio-empty">
<img
width="179"
height="111"
className="studio-empty-img"
src="/images/studios/curators-empty.png"
/>
{canInviteCurators ? (
<div className="studio-empty-msg">
<div><FormattedMessage id="studio.curatorsEmptyCanAdd1" /></div>
<div><FormattedMessage id="studio.curatorsEmptyCanAdd2" /></div>
</div>
) : (
<div className="studio-empty-msg">
<div><FormattedMessage id="studio.curatorsEmpty1" /></div>
</div>
)}
</div>
) : (
<div className="studio-empty-msg">
<div><FormattedMessage id="studio.curatorsEmpty1" /></div>
</div>
<React.Fragment>
{items.map(item =>
(<CuratorTile
key={item.username}
username={item.username}
image={item.profile.images['90x90']}
/>)
)}
{moreToLoad &&
<div className="studio-members-load-more">
<button
className={classNames('button', {
'mod-mutating': loading
})}
onClick={onLoadMore}
>
<FormattedMessage id="general.loadMore" />
</button>
</div>
}
</React.Fragment>
)}
</div>
) : (
<React.Fragment>
{items.map(item =>
(<CuratorTile
key={item.username}
username={item.username}
image={item.profile.images['90x90']}
/>)
)}
{moreToLoad &&
<div className="studio-members-load-more">
<button
className={classNames('button', {
'mod-mutating': loading
})}
onClick={onLoadMore}
>
<FormattedMessage id="general.loadMore" />
</button>
</div>
}
</React.Fragment>
)}
</div>
</div>);
</div>
</AlertProvider>);
};
StudioCurators.propTypes = {

View file

@ -8,6 +8,8 @@ import {managers} from './lib/redux-modules';
import {loadManagers} from './lib/studio-member-actions';
import Debug from './debug.jsx';
import {ManagerTile} from './studio-member-tile.jsx';
import AlertProvider from '../../components/alert/alert-provider.jsx';
import Alert from '../../components/alert/alert.jsx';
const StudioManagers = ({items, error, loading, moreToLoad, onLoadMore}) => {
@ -16,24 +18,26 @@ const StudioManagers = ({items, error, loading, moreToLoad, onLoadMore}) => {
}, []);
return (
<div className="studio-members">
<div className="studio-header-container">
<h2><FormattedMessage id="studio.managersHeader" /></h2>
</div>
{error && <Debug
label="Error"
data={error}
/>}
<div className="studio-members-grid">
{items.map(item =>
(<ManagerTile
key={item.username}
id={item.id}
username={item.username}
image={item.profile.images['90x90']}
/>)
)}
{moreToLoad &&
<AlertProvider>
<div className="studio-members">
<Alert className="studio-alert" />
<div className="studio-header-container">
<h2><FormattedMessage id="studio.managersHeader" /></h2>
</div>
{error && <Debug
label="Error"
data={error}
/>}
<div className="studio-members-grid">
{items.map(item =>
(<ManagerTile
key={item.username}
id={item.id}
username={item.username}
image={item.profile.images['90x90']}
/>)
)}
{moreToLoad &&
<div className="studio-members-load-more">
<button
className={classNames('button', {
@ -44,9 +48,10 @@ const StudioManagers = ({items, error, loading, moreToLoad, onLoadMore}) => {
<FormattedMessage id="general.loadMore" />
</button>
</div>
}
}
</div>
</div>
</div>
</AlertProvider>
);
};

View file

@ -13,6 +13,7 @@ import {
removeCurator,
removeManager
} from './lib/studio-member-actions';
import {useAlertContext} from '../../components/alert/alert-context';
import OverflowMenu from '../../components/overflow-menu/overflow-menu.jsx';
import removeIcon from './icons/remove-icon.svg';
@ -23,7 +24,7 @@ const StudioMemberTile = ({
username, image // own props
}) => {
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState(null);
const {errorAlert, successAlert} = useAlertContext();
const userUrl = `/users/${username}`;
return (
<div className="studio-member-tile">
@ -50,11 +51,20 @@ const StudioMemberTile = ({
disabled={submitting}
onClick={() => {
setSubmitting(true);
setError(null);
onPromote(username).catch(e => {
setError(e);
setSubmitting(false);
});
onPromote(username)
.then(() => {
successAlert({
id: 'studio.alertManagerPromote',
values: {name: username}
});
})
.catch(() => {
errorAlert({
id: 'studio.alertManagerPromoteError',
values: {name: username}
});
setSubmitting(false);
});
}}
>
<img src={promoteIcon} />
@ -69,9 +79,11 @@ const StudioMemberTile = ({
disabled={submitting}
onClick={() => {
setSubmitting(true);
setError(null);
onRemove(username).catch(e => {
setError(e);
onRemove(username).catch(() => {
errorAlert({
id: 'studio.alertMemberRemoveError',
values: {name: username}
}, null);
setSubmitting(false);
});
}}
@ -82,7 +94,6 @@ const StudioMemberTile = ({
</li>}
</OverflowMenu>
}
{error && <div>{error}</div>}
</div>
);
};

View file

@ -5,21 +5,50 @@ import {connect} from 'react-redux';
import classNames from 'classnames';
import {FormattedMessage, intlShape, injectIntl} from 'react-intl';
import {addProject} from './lib/studio-project-actions';
import {Errors, addProject} from './lib/studio-project-actions';
import UserProjectsModal from './modals/user-projects-modal.jsx';
import ValidationMessage from '../../components/forms/validation-message.jsx';
import {useAlertContext} from '../../components/alert/alert-context';
const errorToMessageId = error => {
switch (error) {
case Errors.NETWORK: return 'studio.projectErrors.generic';
case Errors.SERVER: return 'studio.projectErrors.generic';
case Errors.PERMISSION: return 'studio.projectErrors.permission';
case Errors.DUPLICATE: return 'studio.projectErrors.duplicate';
case Errors.RATE_LIMIT: return 'studio.projectErrors.tooFast';
case Errors.UNKNOWN_PROJECT: return 'studio.projectErrors.checkUrl';
default: return 'studio.projectErrors.generic';
}
};
const StudioProjectAdder = ({intl, onSubmit}) => {
const [value, setValue] = useState('');
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState(null);
const [modalOpen, setModalOpen] = useState(false);
const {successAlert} = useAlertContext();
const submit = () => {
setSubmitting(true);
setError(null);
onSubmit(value)
.then(() => setValue(''))
.catch(e => setError(e))
.then(() => {
successAlert({
id: 'studio.alertProjectAdded',
values: {title: value}
});
setValue('');
})
.catch(e => {
// Duplicate project will show success alert
if (e === Errors.DUPLICATE) {
successAlert({id: 'studio.alertProjectAlreadyAdded'});
setValue('');
} else {
// Other errors are displayed by this component
setError(e);
}
})
.then(() => setSubmitting(false));
};
return (
@ -30,7 +59,7 @@ const StudioProjectAdder = ({intl, onSubmit}) => {
<ValidationMessage
mode="error"
className="validation-left"
message={<FormattedMessage id="studio.projectErrors.checkUrl" />}
message={<FormattedMessage id={errorToMessageId(error)} />}
/>
</div>}
<input

View file

@ -1,10 +1,11 @@
/* eslint-disable react/jsx-no-bind */
import React, {useState} from 'react';
import React, {useContext, useState} from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import classNames from 'classnames';
import {FormattedMessage} from 'react-intl';
import AlertContext from '../../components/alert/alert-context.js';
import {selectCanRemoveProject} from '../../redux/studio-permissions';
import {removeProject} from './lib/studio-project-actions';
@ -16,9 +17,9 @@ const StudioProjectTile = ({
id, title, image, avatar, username // own props
}) => {
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState(null);
const projectUrl = `/projects/${id}`;
const userUrl = `/users/${username}`;
const {errorAlert} = useContext(AlertContext);
return (
<div className="studio-project-tile">
<a href={projectUrl}>
@ -54,11 +55,10 @@ const StudioProjectTile = ({
disabled={submitting}
onClick={() => {
setSubmitting(true);
setError(null);
onRemove(id)
.catch(e => {
setError(e);
.catch(() => {
setSubmitting(false);
errorAlert({id: 'studio.alertProjectRemoveError'}, null);
});
}}
>
@ -67,7 +67,6 @@ const StudioProjectTile = ({
</button></li>
</OverflowMenu>
}
{error && <div>{error}</div>} {/* TODO where do these errors go? */}
</div>
</div>
);

View file

@ -11,6 +11,8 @@ import StudioProjectAdder from './studio-project-adder.jsx';
import StudioProjectTile from './studio-project-tile.jsx';
import {loadProjects} from './lib/studio-project-actions.js';
import classNames from 'classnames';
import AlertProvider from '../../components/alert/alert-provider.jsx';
import Alert from '../../components/alert/alert.jsx';
const StudioProjects = ({
canAddProjects, canEditOpenToAll, items, error, loading, moreToLoad, onLoadMore
@ -20,62 +22,64 @@ const StudioProjects = ({
}, []);
return (
<div className="studio-projects">
<div className="studio-header-container">
<h2><FormattedMessage id="studio.projectsHeader" /></h2>
{canEditOpenToAll && <StudioOpenToAll />}
</div>
{canAddProjects && <StudioProjectAdder />}
{error && <Debug
label="Error"
data={error}
/>}
<div className="studio-projects-grid">
{items.length === 0 && !loading ? (
<div className="studio-empty">
{canAddProjects ? (
<React.Fragment>
<img
width="388"
height="265"
className="studio-empty-img"
src="/images/studios/projects-empty-can-add.png"
/>
<div className="studio-empty-msg">
<div><FormattedMessage id="studio.projectsEmptyCanAdd1" /></div>
<div><FormattedMessage id="studio.projectsEmptyCanAdd2" /></div>
</div>
</React.Fragment>
) : (
<React.Fragment>
<img
width="186"
height="138"
className="studio-empty-img"
src="/images/studios/projects-empty.png"
/>
<div className="studio-empty-msg">
<div><FormattedMessage id="studio.projectsEmpty1" /></div>
<div><FormattedMessage id="studio.projectsEmpty2" /></div>
</div>
</React.Fragment>
)}
</div>
) : (
<React.Fragment>
{items.map(item =>
(<StudioProjectTile
fetching={loading}
key={item.id}
id={item.id}
title={item.title}
image={item.image}
avatar={item.avatar['90x90']}
username={item.username}
addedBy={item.actor_id}
/>)
)}
{moreToLoad &&
<AlertProvider>
<div className="studio-projects">
<Alert className="studio-alert" />
<div className="studio-header-container">
<h2><FormattedMessage id="studio.projectsHeader" /></h2>
{canEditOpenToAll && <StudioOpenToAll />}
</div>
{canAddProjects && <StudioProjectAdder />}
{error && <Debug
label="Error"
data={error}
/>}
<div className="studio-projects-grid">
{items.length === 0 && !loading ? (
<div className="studio-empty">
{canAddProjects ? (
<React.Fragment>
<img
width="388"
height="265"
className="studio-empty-img"
src="/images/studios/projects-empty-can-add.png"
/>
<div className="studio-empty-msg">
<div><FormattedMessage id="studio.projectsEmptyCanAdd1" /></div>
<div><FormattedMessage id="studio.projectsEmptyCanAdd2" /></div>
</div>
</React.Fragment>
) : (
<React.Fragment>
<img
width="186"
height="138"
className="studio-empty-img"
src="/images/studios/projects-empty.png"
/>
<div className="studio-empty-msg">
<div><FormattedMessage id="studio.projectsEmpty1" /></div>
<div><FormattedMessage id="studio.projectsEmpty2" /></div>
</div>
</React.Fragment>
)}
</div>
) : (
<React.Fragment>
{items.map(item =>
(<StudioProjectTile
fetching={loading}
key={item.id}
id={item.id}
title={item.title}
image={item.image}
avatar={item.avatar['90x90']}
username={item.username}
addedBy={item.actor_id}
/>)
)}
{moreToLoad &&
<div className="studio-projects-load-more">
<button
className={classNames('button', {
@ -86,11 +90,12 @@ const StudioProjects = ({
<FormattedMessage id="general.loadMore" />
</button>
</div>
}
</React.Fragment>
)}
}
</React.Fragment>
)}
</div>
</div>
</div>
</AlertProvider>
);
};

View file

@ -113,7 +113,10 @@ $radius: 8px;
.active > li { background: $ui-blue; }
}
.studio-projects {}
.studio-projects, .studio-members {
position: relative;
}
.studio-projects-grid {
margin-top: 20px;
display: grid;
@ -193,7 +196,6 @@ $radius: 8px;
}
}
.studio-members {}
.studio-members-grid {
margin-top: 20px;
display: grid;

View file

@ -1,123 +0,0 @@
/*
* Checks that the links in the navbar 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();
var tap = require('tap');
const webdriver = require('selenium-webdriver');
const driver = helper.buildDriver('www-smoke test_navbar_links');
// Set test url through environment variable
var rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
// number of tests in the plan
tap.plan(7);
tap.tearDown(function () {
// quit the instance of the browser
driver.quit();
});
tap.beforeEach(function () {
// load the page with the driver
return driver.get(rootUrl);
});
// ==== Links in navbar ====
// the create link changes depending on whether the user is signed in or not (tips window opens)
tap.test('checkCreateLinkWhenSignedOut', function (t) {
var xPathLink = '//li[contains(@class, "link") and contains(@class, "create")]/a';
var expectedHref = '/projects/editor/?tutorial=getStarted';
driver.findElement(webdriver.By.xpath(xPathLink))
.then(function (element) {
return element.getAttribute('href');
})
.then(function (url) {
t.equal(url.substr(-expectedHref.length), expectedHref);
t.end();
});
});
tap.test('checkExploreLinkWhenSignedOut', function (t) {
var xPathLink = '//li[contains(@class, "link") and contains(@class, "explore")]/a';
var expectedHref = '/explore/projects/all';
driver.findElement(webdriver.By.xpath(xPathLink))
.then(function (element) {
return element.getAttribute('href');
})
.then(function (url) {
t.equal(url.substr(-expectedHref.length), expectedHref);
t.end();
});
});
tap.test('checkIdeasLinkWhenSignedOut', function (t) {
var xPathLink = '//li[contains(@class, "link") and contains(@class, "ideas")]/a';
var expectedHref = '/ideas';
driver.findElement(webdriver.By.xpath(xPathLink))
.then(function (element) {
return element.getAttribute('href');
})
.then(function (url) {
t.equal(url.substr(-expectedHref.length), expectedHref);
t.end();
});
});
tap.test('checkAboutLinkWhenSignedOut', function (t) {
var xPathLink = '//li[contains(@class, "link") and contains(@class, "about")]/a';
var expectedHref = '/about';
driver.findElement(webdriver.By.xpath(xPathLink))
.then(function (element) {
return element.getAttribute('href');
})
.then(function (url) {
t.equal(url.substr(-expectedHref.length), expectedHref);
t.end();
});
});
// ==== Search bar ====
tap.test('checkSearchBar', function (t) {
var xPathLink = '//input[@id="frc-q-1088"]';
// search bar should exist
driver.findElement(webdriver.By.xpath(xPathLink)).then(function (element) {
t.ok(element);
t.end();
});
});
// ==== Join Scratch & Sign In ====
tap.test('checkJoinScratchLinkWhenSignedOut', function (t) {
var xPathLink = '//li[contains(@class, "link") and contains(@class, "right") and contains(@class, "join")]/a';
var expectedText = 'Join Scratch';
driver.findElement(webdriver.By.xpath(xPathLink))
.then(function (element) {
return element.getText('a');
})
.then(function (text) {
t.equal(text, expectedText);
t.end();
});
});
tap.test('checkSignInLinkWhenSignedOut', function (t) {
var xPathLink = '//li[contains(@class, "link") and contains(@class, "right") and contains(@class, "login-item")]/a';
var expectedText = 'Sign in';
driver.findElement(webdriver.By.xpath(xPathLink))
.then(function (element) {
return element.getText('a');
})
.then(function (text) {
t.equal(text, expectedText);
t.end();
});
});

View file

@ -139,7 +139,7 @@ describe('www-integration footer links', () => {
await clickText('Scratch Conference');
let url = await driver.getCurrentUrl();
let pathname = (new URL(url)).pathname;
expect(pathname).toMatch(/^\/conference\/2020\/?$/);
expect(pathname).toMatch(/^\/conference\/2021\/?$/);
});
});

View file

@ -0,0 +1,106 @@
const SeleniumHelper = require('./selenium-helpers.js');
const {
clickXpath,
findByXpath,
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 navbar links', () => {
beforeAll(async () => {
driver = await buildDriver('www-integration navbar links');
});
beforeEach(async () => {
await driver.get(rootUrl);
});
afterAll(async () => await driver.quit());
test('Check text of navbar items', async () => {
let create = await findByXpath('//li[@class="link create"]');
let createText = await create.getText();
await expect(createText).toEqual('Create');
let explore = await findByXpath('//li[@class="link explore"]');
let exploreText = await explore.getText();
await expect(exploreText).toEqual('Explore');
let ideas = await findByXpath('//li[@class="link ideas"]');
let ideasText = await ideas.getText();
await expect(ideasText).toEqual('Ideas');
let about = await findByXpath('//li[@class="link about"]');
let aboutText = await about.getText();
await expect(aboutText).toEqual('About');
let join = await findByXpath('//a[@class="registrationLink"]');
let joinText = await join.getText();
await expect(joinText).toEqual('Join Scratch');
let signIn = await findByXpath('//li[@class="link right login-item"]/a');
let signInText = await signIn.getText();
await expect(signInText).toEqual('Sign in');
});
test('create when signed out', async () => {
await clickXpath('//li[@class="link create"]');
let gui = await findByXpath('//div[contains(@class, "gui")]');
let guiVisible = await gui.isDisplayed();
await expect(guiVisible).toBe(true);
});
test('Explore link when signed out', async () => {
await clickXpath('//li[@class="link explore"]');
let banner = await findByXpath('//h1[@class="title-banner-h1"]');
let bannerText = await banner.getText();
await expect(bannerText).toEqual('Explore');
});
test('Ideas link when signed out', async () => {
await clickXpath('//li[@class="link ideas"]');
let banner = await findByXpath('//div[contains(@class, "ideas-banner")]');
let bannerVisible = await banner.isDisplayed();
await expect(bannerVisible).toBe(true);
});
test('About link when signed out', async () => {
await clickXpath('//li[@class="link about"]');
let aboutPage = await findByXpath('//div[@class="inner about"]');
let aboutPageVisible = await aboutPage.isDisplayed();
await expect(aboutPageVisible).toBe(true);
});
test('Search Bar', async () => {
let searchBar = await findByXpath('//div[contains(@class, "search-wrapper")]/div/input');
await searchBar.sendKeys('cat');
await driver.sleep(500); // without it sends an empty string on submit
await searchBar.submit();
let banner = await findByXpath('//h1[@class="title-banner-h1"]');
let bannerText = await banner.getText();
await expect(bannerText).toEqual('Search');
});
test('Scratch Logo', async () => {
await clickXpath('//li[@class="link explore"]');
await findByXpath('//h1[@class="title-banner-h1"]');
await clickXpath('//li[@class="logo"]');
let splash = await findByXpath('//div[@class="splash"]');
let splashVisible = await splash.isDisplayed();
expect(splashVisible).toBe(true);
});
// Sign In is tested in sign-in-and-out tests
// Create Account is tested in Join tests
});

View file

@ -7,10 +7,14 @@ describe('Studio comments', () => {
const loadComments = jest.fn();
const component = mountWithIntl(
<StudioComments
hasFetchedSession={false}
comments={[]}
handleLoadMoreComments={loadComments}
/>
);
expect(loadComments).not.toHaveBeenCalled();
component.setProps({hasFetchedSession: true});
component.update();
expect(loadComments).toHaveBeenCalled();
// When updated to have comments, load is not called again
@ -30,6 +34,7 @@ describe('Studio comments', () => {
const resetComments = jest.fn();
const component = mountWithIntl(
<StudioComments
hasFetchedSession
isAdmin={false}
comments={[{id: 123, author: {}}]}
handleResetComments={resetComments}
@ -57,6 +62,7 @@ describe('Studio comments', () => {
mountWithIntl(
<StudioComments
isAdmin
hasFetchedSession
comments={[{id: 123, author: {}}]}
handleResetComments={resetComments}
/>