diff --git a/src/components/green-flag/green-flag.css b/src/components/green-flag/green-flag.css index 07e57621bbbe2b115812827f22497a6398fb0e49..3fda3a2072ac38f93a346d3515d6c22744d0d4d6 100644 --- a/src/components/green-flag/green-flag.css +++ b/src/components/green-flag/green-flag.css @@ -2,10 +2,9 @@ width: 1.1rem; height: 1.1rem; opacity: 0.5; - margin: 0.25rem 0.6rem; user-select: none; cursor: pointer; - transition: opacity 0.2s ease-out; /* @todo: standardize with var */ + transition: opacity 0.2s ease-out; /* @todo: standardize with var */ } .green-flag.is-active, diff --git a/src/components/green-flag/green-flag.jsx b/src/components/green-flag/green-flag.jsx index 692a347cd1eac1b8c33a56973c50bf0ca256d7c6..6b8733c1db116fd53c4a91970206e8596244cd6c 100644 --- a/src/components/green-flag/green-flag.jsx +++ b/src/components/green-flag/green-flag.jsx @@ -8,16 +8,20 @@ import styles from './green-flag.css'; const GreenFlagComponent = function (props) { const { active, + className, onClick, title, ...componentProps } = props; return ( <img - className={classNames({ - [styles.greenFlag]: true, - [styles.isActive]: active - })} + className={classNames( + className, + styles.greenFlag, + { + [styles.isActive]: active + } + )} src={greenFlagIcon} title={title} onClick={onClick} @@ -27,6 +31,7 @@ const GreenFlagComponent = function (props) { }; GreenFlagComponent.propTypes = { active: PropTypes.bool, + className: PropTypes.string, onClick: PropTypes.func.isRequired, title: PropTypes.string }; diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css index 6fce08e8c89cc4c06b05761c25d13a591e869700..6a063a166edd3eedd74aeff3efc67874fda5f5e7 100644 --- a/src/components/gui/gui.css +++ b/src/components/gui/gui.css @@ -101,6 +101,10 @@ position: relative; } +.green-flag { + margin: 0.25rem 0.6rem; +} + .stage-and-target-wrapper { /* Makes rows for children: diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index 1e7fe027e54d69c1caead489e3bb5f9f6dde6fdf..8c8c9f96d8d00a3be9c9dc7a5b9136fb182a2405 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -90,7 +90,10 @@ const GUIComponent = props => { <Box className={styles.stageAndTargetWrapper}> <Box className={styles.stageMenuWrapper}> - <GreenFlag vm={vm} /> + <GreenFlag + className={styles.greenFlag} + vm={vm} + /> <StopAll vm={vm} /> </Box> <Box className={styles.stageWrapper}> diff --git a/src/components/stop-all/stop-all.jsx b/src/components/stop-all/stop-all.jsx index aba3ed42415c51456610ea5a892ddaccd0927921..f2916f605010441b3f7c5985ff5045dae1d5861f 100644 --- a/src/components/stop-all/stop-all.jsx +++ b/src/components/stop-all/stop-all.jsx @@ -8,16 +8,20 @@ import styles from './stop-all.css'; const StopAllComponent = function (props) { const { active, + className, onClick, title, ...componentProps } = props; return ( <img - className={classNames({ - [styles.stopAll]: true, - [styles.isActive]: active - })} + className={classNames( + className, + styles.stopAll, + { + [styles.isActive]: active + } + )} src={stopAllIcon} title={title} onClick={onClick} @@ -28,6 +32,7 @@ const StopAllComponent = function (props) { StopAllComponent.propTypes = { active: PropTypes.bool, + className: PropTypes.string, onClick: PropTypes.func.isRequired, title: PropTypes.string }; diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 86d9108973e67e3f294166e0c86a4b39badae8a1..63a76276585a197941462999f26c911e31526ebe 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -208,7 +208,7 @@ class Blocks extends React.Component { } Blocks.propTypes = { - isVisible: PropTypes.bool.isRequired, + isVisible: PropTypes.bool, options: PropTypes.shape({ media: PropTypes.string, zoom: PropTypes.shape({ @@ -260,6 +260,7 @@ Blocks.defaultOptions = { }; Blocks.defaultProps = { + isVisible: true, options: Blocks.defaultOptions }; diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx index 87f195aab83d5e1a70b7d33258babfeee7dd14a3..c41f7a9a97a8026ae96742354fcd6458a0135e08 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -1,3 +1,4 @@ +import AudioEngine from 'scratch-audio'; import PropTypes from 'prop-types'; import React from 'react'; import VM from 'scratch-vm'; @@ -16,6 +17,8 @@ class GUI extends React.Component { this.state = {tabIndex: 0}; } componentDidMount () { + this.audioEngine = new AudioEngine(); + this.props.vm.attachAudioEngine(this.audioEngine); this.props.vm.loadProject(this.props.projectData); this.props.vm.setCompatibilityMode(true); this.props.vm.start(); @@ -33,6 +36,7 @@ class GUI extends React.Component { } render () { const { + children, projectData, // eslint-disable-line no-unused-vars vm, ...componentProps @@ -43,7 +47,9 @@ class GUI extends React.Component { vm={vm} onTabSelect={this.handleTabSelect} {...componentProps} - /> + > + {children} + </GUIComponent> ); } } diff --git a/src/containers/stage.jsx b/src/containers/stage.jsx index d4e3de5d3e4286c95d7f3a070e32201885f53da0..1b5b2ec9cc4782fdb10a06e9099db31d7fcec64e 100644 --- a/src/containers/stage.jsx +++ b/src/containers/stage.jsx @@ -2,7 +2,6 @@ import bindAll from 'lodash.bindall'; import PropTypes from 'prop-types'; import React from 'react'; import Renderer from 'scratch-render'; -import AudioEngine from 'scratch-audio'; import VM from 'scratch-vm'; import {getEventXY} from '../lib/touch-utils'; @@ -38,8 +37,6 @@ class Stage extends React.Component { this.updateRect(); this.renderer = new Renderer(this.canvas); this.props.vm.attachRenderer(this.renderer); - this.audioEngine = new AudioEngine(); - this.props.vm.attachAudioEngine(this.audioEngine); } shouldComponentUpdate (nextProps) { return this.props.width !== nextProps.width || this.props.height !== nextProps.height; diff --git a/src/examples/blocks-only.css b/src/examples/blocks-only.css new file mode 100644 index 0000000000000000000000000000000000000000..9713613886c9caa437135a932fb4375be6db5b96 --- /dev/null +++ b/src/examples/blocks-only.css @@ -0,0 +1,13 @@ +.green-flag { + position: absolute; + z-index: 2; + top: 10px; + right: 50px; +} + +.stop-all { + position: absolute; + z-index: 2; + top: 10px; + right: 15px; +} diff --git a/src/examples/blocks-only.jsx b/src/examples/blocks-only.jsx new file mode 100644 index 0000000000000000000000000000000000000000..c97444d9ec1ffdf318d66089da35b79470399dc9 --- /dev/null +++ b/src/examples/blocks-only.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import {connect} from 'react-redux'; + +import AppStateHOC from '../lib/app-state-hoc.jsx'; +import GreenFlag from '../containers/green-flag.jsx'; +import StopAll from '../containers/stop-all.jsx'; +import Blocks from '../containers/blocks.jsx'; +import GUI from '../containers/gui.jsx'; +import ProjectLoaderHOC from '../lib/project-loader-hoc.jsx'; + +import styles from './blocks-only.css'; + +const mapStateToProps = state => ({vm: state.vm}); + +const VMBlocks = connect(mapStateToProps)(Blocks); +const VMGreenFlag = connect(mapStateToProps)(GreenFlag); +const VMStopAll = connect(mapStateToProps)(StopAll); + +const BlocksOnly = props => ( + <GUI {...props}> + <VMBlocks + grow={1} + options={{ + media: `static/blocks-media/` + }} + /> + <VMGreenFlag className={styles.greenFlag} /> + <VMStopAll className={styles.stopAll} /> + </GUI> +); + +const App = AppStateHOC(ProjectLoaderHOC(BlocksOnly)); + +const appTarget = document.createElement('div'); +document.body.appendChild(appTarget); + +ReactDOM.render(<App />, appTarget); diff --git a/src/examples/player.css b/src/examples/player.css new file mode 100644 index 0000000000000000000000000000000000000000..f5a3096c44d594f5bae77d04b8b9b5d2aaefa517 --- /dev/null +++ b/src/examples/player.css @@ -0,0 +1,4 @@ +body { + padding: 0; + margin: 0; +} diff --git a/src/examples/player.jsx b/src/examples/player.jsx new file mode 100644 index 0000000000000000000000000000000000000000..56c7a73607256c69bfebc57208d166ee60718c0f --- /dev/null +++ b/src/examples/player.jsx @@ -0,0 +1,84 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import {connect} from 'react-redux'; + +import AppStateHOC from '../lib/app-state-hoc.jsx'; +import GreenFlag from '../containers/green-flag.jsx'; +import StopAll from '../containers/stop-all.jsx'; +import Stage from '../containers/stage.jsx'; +import Box from '../components/box/box.jsx'; +import GUI from '../containers/gui.jsx'; +import ProjectLoaderHOC from '../lib/project-loader-hoc.jsx'; + +import './player.css'; + +const mapStateToProps = state => ({vm: state.vm}); + +const VMStage = connect(mapStateToProps)(Stage); +const VMGreenFlag = connect(mapStateToProps)(GreenFlag); +const VMStopAll = connect(mapStateToProps)(StopAll); + +class Player extends React.Component { + constructor (props) { + super(props); + this.handleResize = this.handleResize.bind(this); + this.state = this.getWindowSize(); + } + componentDidMount () { + window.addEventListener('resize', this.handleResize); + } + componentWillUnmount () { + window.removeEventListener('resize', this.handleResize); + } + getWindowSize () { + return { + width: window.innerWidth, + height: window.innerHeight + }; + } + handleResize () { + this.setState(this.getWindowSize()); + } + render () { + let height = this.state.height - 40; + let width = height + (height / 3); + if (width > this.state.width) { + width = this.state.width; + height = width * .75; + } + return ( + <GUI + {...this.props} + style={{ + margin: '0 auto' + }} + width={width} + > + <Box height={40}> + <VMGreenFlag + style={{ + marginRight: 10, + height: 40 + }} + /> + <VMStopAll + style={{ + height: 40 + }} + /> + </Box> + <VMStage + height={height} + width={width} + /> + </GUI> + ); + } +} + +const App = AppStateHOC(ProjectLoaderHOC(Player)); + +const appTarget = document.createElement('div'); +document.body.appendChild(appTarget); + +ReactDOM.render(<App />, appTarget); diff --git a/src/index.jsx b/src/index.jsx index 35c7610859e580e01d5e544d298bdb8f709b59bf..6d301f5168501f6b20252a6d0e034f509c5eea77 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -1,79 +1,16 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import {Provider} from 'react-redux'; -import {createStore, applyMiddleware, compose} from 'redux'; -import throttle from 'redux-throttle'; -import {intlInitialState, IntlProvider} from './reducers/intl.js'; +import AppStateHOC from './lib/app-state-hoc.jsx'; import GUI from './containers/gui.jsx'; -import log from './lib/log'; -import ProjectLoader from './lib/project-loader'; -import reducer from './reducers/gui'; +import ProjectLoaderHOC from './lib/project-loader-hoc.jsx'; import styles from './index.css'; -class App extends React.Component { - constructor (props) { - super(props); - this.fetchProjectId = this.fetchProjectId.bind(this); - this.updateProject = this.updateProject.bind(this); - this.state = { - projectId: null, - projectData: this.fetchProjectId().length ? null : JSON.stringify(ProjectLoader.DEFAULT_PROJECT_DATA) - }; - } - componentDidMount () { - window.addEventListener('hashchange', this.updateProject); - this.updateProject(); - } - componentWillUnmount () { - window.removeEventListener('hashchange', this.updateProject); - } - fetchProjectId () { - return window.location.hash.substring(1); - } - updateProject () { - const projectId = this.fetchProjectId(); - if (projectId !== this.state.projectId) { - if (projectId.length < 1) { - return this.setState({ - projectId: projectId, - projectData: JSON.stringify(ProjectLoader.DEFAULT_PROJECT_DATA) - }); - } - ProjectLoader.load(projectId, (err, body) => { - if (err) return log.error(err); - this.setState({projectData: body}); - }); - this.setState({projectId: projectId}); - } - } - render () { - if (this.state.projectData === null) return null; - return ( - <GUI - projectData={this.state.projectData} - /> - ); - } -} +const App = AppStateHOC(ProjectLoaderHOC(GUI)); const appTarget = document.createElement('div'); appTarget.className = styles.app; document.body.appendChild(appTarget); -const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; -const enhancer = composeEnhancers( - applyMiddleware( - throttle(300, {leading: true, trailing: true}) - ) -); -const store = createStore(reducer, intlInitialState, enhancer); - -ReactDOM.render(( - <Provider store={store}> - <IntlProvider> - <App /> - </IntlProvider> - </Provider> -), appTarget); +ReactDOM.render(<App />, appTarget); diff --git a/src/lib/app-state-hoc.jsx b/src/lib/app-state-hoc.jsx new file mode 100644 index 0000000000000000000000000000000000000000..0861f05291f258f8cf625947723da763f9f22d8c --- /dev/null +++ b/src/lib/app-state-hoc.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import {Provider} from 'react-redux'; +import {createStore, applyMiddleware, compose} from 'redux'; +import throttle from 'redux-throttle'; + +import {intlInitialState, IntlProvider} from '../reducers/intl.js'; +import reducer from '../reducers/gui'; + +const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; +const enhancer = composeEnhancers( + applyMiddleware( + throttle(300, {leading: true, trailing: true}) + ) +); +const store = createStore(reducer, intlInitialState, enhancer); + +/* + * Higher Order Component to provide redux state + * @param {React.Component} WrappedComponent - component to provide state for + * @returns {React.Component} component with redux and intl state provided + */ +const AppStateHOC = function (WrappedComponent) { + const AppStateWrapper = ({...props}) => ( + <Provider store={store}> + <IntlProvider> + <WrappedComponent {...props} /> + </IntlProvider> + </Provider> + ); + return AppStateWrapper; +}; + +export default AppStateHOC; diff --git a/src/lib/project-loader-hoc.jsx b/src/lib/project-loader-hoc.jsx new file mode 100644 index 0000000000000000000000000000000000000000..5468e7a09b54c292c9e107183fff80550fb0c29d --- /dev/null +++ b/src/lib/project-loader-hoc.jsx @@ -0,0 +1,85 @@ +import React from 'react'; +import xhr from 'xhr'; + +import log from './log'; +import emptyProject from './empty-project.json'; + +class ProjectLoaderConstructor { + get DEFAULT_PROJECT_DATA () { + return emptyProject; + } + + load (id, callback) { + callback = callback || (err => log.error(err)); + xhr({ + uri: `https://projects.scratch.mit.edu/internalapi/project/${id}/get/` + }, (err, res, body) => { + if (err) return callback(err); + callback(null, body); + }); + } +} + +const ProjectLoader = new ProjectLoaderConstructor(); + +/* Higher Order Component to provide behavior for loading projects by id from + * the window's hash (#this part in the url) + * @param {React.Component} WrappedComponent component to receive projectData prop + * @returns {React.Component} component with project loading behavior + */ +const ProjectLoaderHOC = function (WrappedComponent) { + class ProjectLoaderComponent extends React.Component { + constructor (props) { + super(props); + this.fetchProjectId = this.fetchProjectId.bind(this); + this.updateProject = this.updateProject.bind(this); + this.state = { + projectId: null, + projectData: this.fetchProjectId().length ? null : JSON.stringify(ProjectLoader.DEFAULT_PROJECT_DATA) + }; + } + componentDidMount () { + window.addEventListener('hashchange', this.updateProject); + this.updateProject(); + } + componentWillUnmount () { + window.removeEventListener('hashchange', this.updateProject); + } + fetchProjectId () { + return window.location.hash.substring(1); + } + updateProject () { + const projectId = this.fetchProjectId(); + if (projectId !== this.state.projectId) { + if (projectId.length < 1) { + return this.setState({ + projectId: projectId, + projectData: JSON.stringify(ProjectLoader.DEFAULT_PROJECT_DATA) + }); + } + ProjectLoader.load(projectId, (err, body) => { + if (err) return log.error(err); + this.setState({projectData: body}); + }); + this.setState({projectId: projectId}); + } + } + render () { + if (!this.state.projectData) return null; + return ( + <WrappedComponent + projectData={this.state.projectData} + {...this.props} + /> + ); + } + } + + return ProjectLoaderComponent; +}; + + +export { + ProjectLoaderHOC as default, + ProjectLoader +}; diff --git a/src/lib/project-loader.js b/src/lib/project-loader.js deleted file mode 100644 index 05060732065e890b13e99e0cb9e4b90fe47dff31..0000000000000000000000000000000000000000 --- a/src/lib/project-loader.js +++ /dev/null @@ -1,23 +0,0 @@ -import xhr from 'xhr'; - -import log from './log'; -import emptyProject from './empty-project.json'; - -class ProjectLoader { - constructor () { - this.DEFAULT_PROJECT_DATA = ProjectLoader.DEFAULT_PROJECT_DATA; - } - load (id, callback) { - callback = callback || (err => log.error(err)); - xhr({ - uri: `https://projects.scratch.mit.edu/internalapi/project/${id}/get/` - }, (err, res, body) => { - if (err) return callback(err); - callback(null, body); - }); - } -} - -ProjectLoader.DEFAULT_PROJECT_DATA = emptyProject; - -export default new ProjectLoader(); diff --git a/test/.eslintrc.js b/test/.eslintrc.js new file mode 100644 index 0000000000000000000000000000000000000000..ea7ef84f15c71d2b379ba0e22b700e82cd8a8893 --- /dev/null +++ b/test/.eslintrc.js @@ -0,0 +1,10 @@ +module.exports = { + extends: ['scratch/react', 'scratch/es6'], + env: { + browser: true, + jest: true + }, + rules: { + 'react/prop-types': 0 + } +}; diff --git a/test/__mocks__/audio-buffer-player.js b/test/__mocks__/audio-buffer-player.js index c36092be339f166b2376a5340c1e82abed6c9ca9..dabf72bc37d2ba6553530ada7dc4e9851f4f53a7 100644 --- a/test/__mocks__/audio-buffer-player.js +++ b/test/__mocks__/audio-buffer-player.js @@ -1,4 +1,3 @@ -/* eslint-env jest */ export default class MockAudioBufferPlayer { constructor (samples, sampleRate) { this.samples = samples; diff --git a/test/__mocks__/audio-effects.js b/test/__mocks__/audio-effects.js index bebf0fdc282ede058ca0e4be175d92e5861b565c..70a77ee9a6394897f962a1ba465a3944313ce268 100644 --- a/test/__mocks__/audio-effects.js +++ b/test/__mocks__/audio-effects.js @@ -1,4 +1,3 @@ -/* eslint-env jest */ export default class MockAudioEffects { static get effectTypes () { // @todo can this be imported from the real file? return { @@ -16,7 +15,7 @@ export default class MockAudioEffects { this.name = name; this._mockResult = {}; this._bufferPromise = new Promise(resolve => { // eslint-disable-line no-undef - this._finishProcessing = (newBuffer) => resolve(newBuffer); + this._finishProcessing = newBuffer => resolve(newBuffer); }); this.process = jest.fn(() => this._bufferPromise); MockAudioEffects.instance = this; diff --git a/test/helpers/intl-helpers.js b/test/helpers/intl-helpers.jsx similarity index 54% rename from test/helpers/intl-helpers.js rename to test/helpers/intl-helpers.jsx index d658aeae0981adc70d8de4f57d3ec196a06f2df1..8c9a057b4e54a2d0cabd9c9fb93b8959d2bb65a0 100644 --- a/test/helpers/intl-helpers.js +++ b/test/helpers/intl-helpers.jsx @@ -10,33 +10,27 @@ import {mount, shallow} from 'enzyme'; const intlProvider = new IntlProvider({locale: 'en'}, {}); const {intl} = intlProvider.getChildContext(); -const nodeWithIntlProp = node => { - return React.cloneElement(node, {intl}); -}; +const nodeWithIntlProp = node => React.cloneElement(node, {intl}); -const shallowWithIntl = (node, {context} = {}) => { - return shallow( - nodeWithIntlProp(node), - { - context: Object.assign({}, context, {intl}) - } - ); -}; +const shallowWithIntl = (node, {context} = {}) => shallow( + nodeWithIntlProp(node), + { + context: Object.assign({}, context, {intl}) + } +); -const mountWithIntl = (node, {context, childContextTypes} = {}) => { - return mount( - nodeWithIntlProp(node), - { - context: Object.assign({}, context, {intl}), - childContextTypes: Object.assign({}, {intl: intlShape}, childContextTypes) - } - ); -}; +const mountWithIntl = (node, {context, childContextTypes} = {}) => mount( + nodeWithIntlProp(node), + { + context: Object.assign({}, context, {intl}), + childContextTypes: Object.assign({}, {intl: intlShape}, childContextTypes) + } +); // react-test-renderer component for use with snapshot testing -const componentWithIntl = (children, props = {locale: 'en'}) => { - return renderer.create(<IntlProvider {...props}>{children}</IntlProvider>); -}; +const componentWithIntl = (children, props = {locale: 'en'}) => renderer.create( + <IntlProvider {...props}>{children}</IntlProvider> +); export { componentWithIntl, diff --git a/test/helpers/selenium-helper.js b/test/helpers/selenium-helper.js new file mode 100644 index 0000000000000000000000000000000000000000..390740c3c2e9b7e8b3225388fe8141e55435d3ce --- /dev/null +++ b/test/helpers/selenium-helper.js @@ -0,0 +1,65 @@ +jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; // eslint-disable-line no-undef + +import bindAll from 'lodash.bindall'; +import webdriver from 'selenium-webdriver'; + +const {By, until} = webdriver; + +class SeleniumHelper { + constructor () { + bindAll(this, [ + 'clickText', + 'clickButton', + 'clickXpath', + 'findByXpath', + 'getDriver', + 'getLogs' + ]); + } + + getDriver () { + this.driver = new webdriver.Builder() + .forBrowser('chrome') + .build(); + return this.driver; + } + + findByXpath (xpath) { + return this.driver.wait(until.elementLocated(By.xpath(xpath), 5 * 1000)); + } + + clickXpath (xpath) { + return this.findByXpath(xpath).then(el => el.click()); + } + + clickText (text) { + return this.clickXpath(`//*[contains(text(), '${text}')]`); + } + + clickButton (text) { + return this.clickXpath(`//button[contains(text(), '${text}')]`); + } + + getLogs (whitelist) { + return this.driver.manage() + .logs() + .get('browser') + .then(entries => entries.filter(entry => { + const message = entry.message; + for (let i = 0; i < whitelist.length; i++) { + if (message.indexOf(whitelist[i]) !== -1) { + // eslint-disable-next-line no-console + console.warn(`Ignoring whitelisted error: ${whitelist[i]}`); + return false; + } else if (entry.level !== 'SEVERE') { + // eslint-disable-next-line no-console + console.warn(`Ignoring non-SEVERE entry: ${message}`); + return false; + } + } + return true; + })); + } +} + +export default SeleniumHelper; diff --git a/test/helpers/selenium-helpers.js b/test/helpers/selenium-helpers.js deleted file mode 100644 index 909f3a1553eaca14269f494216a1f55140aff87e..0000000000000000000000000000000000000000 --- a/test/helpers/selenium-helpers.js +++ /dev/null @@ -1,58 +0,0 @@ -/* eslint-env jest */ -jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; // eslint-disable-line no-undef - -import webdriver from 'selenium-webdriver'; - -const {By, until} = webdriver; - -const driver = new webdriver.Builder() - .forBrowser('chrome') - .build(); - -const findByXpath = (xpath) => { - return driver.wait(until.elementLocated(By.xpath(xpath), 5 * 1000)); -}; - -const clickXpath = (xpath) => { - return findByXpath(xpath).then(el => el.click()); -}; - -const clickText = (text) => { - return clickXpath(`//*[contains(text(), '${text}')]`); -}; - -const clickButton = (text) => { - return clickXpath(`//button[contains(text(), '${text}')]`); -}; - -const getLogs = (whitelist) => { - return driver.manage() - .logs() - .get('browser') - .then((entries) => { - return entries.filter((entry) => { - const message = entry.message; - for (let i = 0; i < whitelist.length; i++) { - if (message.indexOf(whitelist[i]) !== -1) { - // eslint-disable-next-line no-console - console.warn('Ignoring whitelisted error: ' + whitelist[i]); - return false; - } else if (entry.level !== 'SEVERE') { - // eslint-disable-next-line no-console - console.warn('Ignoring non-SEVERE entry: ' + message); - return false; - } - } - return true; - }); - }); -}; - -export { - clickText, - clickButton, - clickXpath, - driver, - findByXpath, - getLogs -}; diff --git a/test/integration/examples.test.js b/test/integration/examples.test.js new file mode 100644 index 0000000000000000000000000000000000000000..3299fb27c532df3838a82dd5cfa04528aa3c287b --- /dev/null +++ b/test/integration/examples.test.js @@ -0,0 +1,87 @@ +/* globals Promise */ + +import path from 'path'; +import SeleniumHelper from '../helpers/selenium-helper'; + +const { + clickButton, + clickText, + clickXpath, + findByXpath, + getDriver, + getLogs +} = new SeleniumHelper(); + +const errorWhitelist = [ + 'The play() request was interrupted by a call to pause()' +]; + +let driver; + +describe('player example', () => { + const uri = path.resolve(__dirname, '../../build/player.html'); + + beforeAll(() => { + driver = getDriver(); + }); + + afterAll(async () => { + await driver.quit(); + }); + + test('Load a project by ID', async () => { + const projectId = '96708228'; + await driver.get(`file://${uri}#${projectId}`); + await new Promise(resolve => setTimeout(resolve, 2000)); + await clickXpath('//img[@title="Go"]'); + await new Promise(resolve => setTimeout(resolve, 2000)); + await clickXpath('//img[@title="Stop"]'); + const logs = await getLogs(errorWhitelist); + await expect(logs).toEqual([]); + }); +}); + +describe('blocks example', () => { + const uri = path.resolve(__dirname, '../../build/blocks-only.html'); + + beforeAll(() => { + driver = getDriver(); + }); + + afterAll(async () => { + await driver.quit(); + }); + + test('Load a project by ID', async () => { + const projectId = '96708228'; + await driver.get(`file://${uri}#${projectId}`); + await new Promise(resolve => setTimeout(resolve, 2000)); + await clickXpath('//img[@title="Go"]'); + await new Promise(resolve => setTimeout(resolve, 2000)); + await clickXpath('//img[@title="Stop"]'); + const logs = await getLogs(errorWhitelist); + await expect(logs).toEqual([]); + }); + + test('Change categories', async () => { + await driver.get(`file://${uri}`); + await clickText('Looks'); + await clickText('Sound'); + await clickText('Pen'); + await clickText('Events'); + await clickText('Control'); + await clickText('Sensing'); + await clickText('Operators'); + await clickText('Data'); + await clickText('Create variable...'); + let el = await findByXpath("//input[@placeholder='']"); + await el.sendKeys('score'); + await clickButton('OK'); + await clickText('Create variable...'); + el = await findByXpath("//input[@placeholder='']"); + await el.sendKeys('second variable'); + await clickButton('OK'); + const logs = await getLogs(errorWhitelist); + await expect(logs).toEqual([]); + }); +}); diff --git a/test/integration/test.js b/test/integration/test.js index 7e3e60a651a6e7d64f06ad56c561058449432f20..39e8c6faec5a2451e08fb0133a0dd34462738fb4 100644 --- a/test/integration/test.js +++ b/test/integration/test.js @@ -1,15 +1,14 @@ -/* eslint-env jest */ -/* globals Promise */ - import path from 'path'; -import { +import SeleniumHelper from '../helpers/selenium-helper'; + +const { clickText, clickButton, clickXpath, - driver, findByXpath, + getDriver, getLogs -} from '../helpers/selenium-helpers'; +} = new SeleniumHelper(); const uri = path.resolve(__dirname, '../../build/index.html'); @@ -17,13 +16,19 @@ const errorWhitelist = [ 'The play() request was interrupted by a call to pause()' ]; +let driver; + describe('costumes, sounds and variables', () => { + beforeAll(() => { + driver = getDriver(); + }); + afterAll(async () => { await driver.quit(); }); test('Adding a costume', async () => { - await driver.get('file://' + uri); + await driver.get(`file://${uri}`); await clickText('Costumes'); await clickText('Add Costume'); const el = await findByXpath("//input[@placeholder='what are you looking for?']"); @@ -36,7 +41,7 @@ describe('costumes, sounds and variables', () => { }); test('Adding a sound', async () => { - await driver.get('file://' + uri); + await driver.get(`file://${uri}`); await clickText('Sounds'); await clickText('Add Sound'); const el = await findByXpath("//input[@placeholder='what are you looking for?']"); @@ -62,7 +67,7 @@ describe('costumes, sounds and variables', () => { test('Load a project by ID', async () => { const projectId = '96708228'; - await driver.get('file://' + uri + '#' + projectId); + await driver.get(`file://${uri}#${projectId}`); await new Promise(resolve => setTimeout(resolve, 2000)); await clickXpath('//img[@title="Go"]'); await new Promise(resolve => setTimeout(resolve, 2000)); @@ -72,7 +77,7 @@ describe('costumes, sounds and variables', () => { }); test('Creating variables', async () => { - await driver.get('file://' + uri); + await driver.get(`file://${uri}`); await clickText('Blocks'); await clickText('Data'); await clickText('Create variable...'); diff --git a/test/unit/components/button.test.jsx b/test/unit/components/button.test.jsx index 6f9b7447c4d48a0ce8a82f45fcf3ae6f394fc519..5445891ab1eabb9e9db44a0fc81366622474eb90 100644 --- a/test/unit/components/button.test.jsx +++ b/test/unit/components/button.test.jsx @@ -1,14 +1,13 @@ -/* eslint-env jest */ -import React from 'react'; // eslint-disable-line no-unused-vars +import React from 'react'; import {shallow} from 'enzyme'; -import ButtonComponent from '../../../src/components/button/button'; // eslint-disable-line no-unused-vars +import ButtonComponent from '../../../src/components/button/button'; import renderer from 'react-test-renderer'; describe('ButtonComponent', () => { test('matches snapshot', () => { const onClick = jest.fn(); const component = renderer.create( - <ButtonComponent onClick={onClick}/> + <ButtonComponent onClick={onClick} /> ); expect(component.toJSON()).toMatchSnapshot(); }); @@ -16,7 +15,7 @@ describe('ButtonComponent', () => { test('triggers callback when clicked', () => { const onClick = jest.fn(); const componentShallowWrapper = shallow( - <ButtonComponent onClick={onClick}/> + <ButtonComponent onClick={onClick} /> ); componentShallowWrapper.simulate('click'); expect(onClick).toHaveBeenCalled(); diff --git a/test/unit/components/icon-button.test.jsx b/test/unit/components/icon-button.test.jsx index fe2498a1a36b160759f2a29e1a3c98a5f320fb93..269dfad76fd30fec0a580baa21eca45302142edb 100644 --- a/test/unit/components/icon-button.test.jsx +++ b/test/unit/components/icon-button.test.jsx @@ -1,7 +1,6 @@ -/* eslint-env jest */ -import React from 'react'; // eslint-disable-line no-unused-vars +import React from 'react'; import {shallow} from 'enzyme'; -import IconButton from '../../../src/components/icon-button/icon-button'; // eslint-disable-line no-unused-vars +import IconButton from '../../../src/components/icon-button/icon-button'; import renderer from 'react-test-renderer'; describe('IconButtonComponent', () => { diff --git a/test/unit/components/sound-editor.test.jsx b/test/unit/components/sound-editor.test.jsx index 6c12f473060ed5b7167fe2b353563345b349c92d..387a53c1a73359e3870a288bf1d34adf83fb9985 100644 --- a/test/unit/components/sound-editor.test.jsx +++ b/test/unit/components/sound-editor.test.jsx @@ -1,7 +1,6 @@ -/* eslint-env jest */ -import React from 'react'; // eslint-disable-line no-unused-vars -import {mountWithIntl, componentWithIntl} from '../../helpers/intl-helpers'; -import SoundEditor from '../../../src/components/sound-editor/sound-editor'; // eslint-disable-line no-unused-vars +import React from 'react'; +import {mountWithIntl, componentWithIntl} from '../../helpers/intl-helpers.jsx'; +import SoundEditor from '../../../src/components/sound-editor/sound-editor'; describe('Sound Editor Component', () => { let props; @@ -38,31 +37,55 @@ describe('Sound Editor Component', () => { }); test('trim button appears when trims are null', () => { - const wrapper = mountWithIntl(<SoundEditor {...props} trimStart={null} trimEnd={null} />); + const wrapper = mountWithIntl( + <SoundEditor + {...props} + trimEnd={null} + trimStart={null} + /> + ); wrapper.find('button[title="Trim"]').simulate('click'); expect(props.onActivateTrim).toHaveBeenCalled(); }); test('save button appears when trims are not null', () => { - const wrapper = mountWithIntl(<SoundEditor {...props} trimStart={0.25} trimEnd={0.75} />); + const wrapper = mountWithIntl( + <SoundEditor + {...props} + trimEnd={0.75} + trimStart={0.25} + /> + ); wrapper.find('button[title="Save"]').simulate('click'); expect(props.onActivateTrim).toHaveBeenCalled(); }); test('play button appears when playhead is null', () => { - const wrapper = mountWithIntl(<SoundEditor {...props} playhead={null} />); + const wrapper = mountWithIntl( + <SoundEditor + {...props} + playhead={null} + /> + ); wrapper.find('button[title="Play"]').simulate('click'); expect(props.onPlay).toHaveBeenCalled(); }); test('stop button appears when playhead is not null', () => { - const wrapper = mountWithIntl(<SoundEditor {...props} playhead={0.5} />); + const wrapper = mountWithIntl( + <SoundEditor + {...props} + playhead={0.5} + /> + ); wrapper.find('button[title="Stop"]').simulate('click'); expect(props.onStop).toHaveBeenCalled(); }); test('submitting name calls the callback', () => { - const wrapper = mountWithIntl(<SoundEditor {...props} />); + const wrapper = mountWithIntl( + <SoundEditor {...props} /> + ); wrapper.find('input') .simulate('change', {target: {value: 'hello'}}) .simulate('blur'); @@ -70,7 +93,9 @@ describe('Sound Editor Component', () => { }); test('effect buttons call the correct callbacks', () => { - const wrapper = mountWithIntl(<SoundEditor {...props} />); + const wrapper = mountWithIntl( + <SoundEditor {...props} /> + ); wrapper.find('[children="Reverse"]').simulate('click'); expect(props.onReverse).toHaveBeenCalled(); @@ -95,17 +120,35 @@ describe('Sound Editor Component', () => { }); test('undo and redo buttons can be disabled by canUndo/canRedo', () => { - let wrapper = mountWithIntl(<SoundEditor {...props} canUndo={true} canRedo={false} />); + let wrapper = mountWithIntl( + <SoundEditor + {...props} + canUndo + canRedo={false} + /> + ); expect(wrapper.find('button[title="Undo"]').prop('disabled')).toBe(false); expect(wrapper.find('button[title="Redo"]').prop('disabled')).toBe(true); - wrapper = mountWithIntl(<SoundEditor {...props} canUndo={false} canRedo={true} />); + wrapper = mountWithIntl( + <SoundEditor + {...props} + canRedo + canUndo={false} + /> + ); expect(wrapper.find('button[title="Undo"]').prop('disabled')).toBe(true); expect(wrapper.find('button[title="Redo"]').prop('disabled')).toBe(false); }); test.skip('undo/redo buttons call the correct callback', () => { - let wrapper = mountWithIntl(<SoundEditor {...props} canUndo={true} canRedo={true} />); + const wrapper = mountWithIntl( + <SoundEditor + {...props} + canRedo + canUndo + /> + ); wrapper.find('button[title="Undo"]').simulate('click'); expect(props.onUndo).toHaveBeenCalled(); diff --git a/test/unit/components/sprite-selector-item.test.jsx b/test/unit/components/sprite-selector-item.test.jsx index fe0b1de5238b5076e9938da8303dfb714023212c..e1d7ac7bb73ae23fb10ff018035a097ef0137e06 100644 --- a/test/unit/components/sprite-selector-item.test.jsx +++ b/test/unit/components/sprite-selector-item.test.jsx @@ -1,10 +1,8 @@ -/* eslint-env jest */ -import React from 'react'; // eslint-disable-line no-unused-vars -import {mountWithIntl, shallowWithIntl, componentWithIntl} from '../../helpers/intl-helpers'; -// eslint-disable-next-line no-unused-vars +import React from 'react'; +import {mountWithIntl, shallowWithIntl, componentWithIntl} from '../../helpers/intl-helpers.jsx'; import SpriteSelectorItemComponent from '../../../src/components/sprite-selector-item/sprite-selector-item'; import CostumeCanvas from '../../../src/components/costume-canvas/costume-canvas'; -import CloseButton from '../../../src/components/close-button/close-button'; // eslint-disable-line no-unused-vars +import CloseButton from '../../../src/components/close-button/close-button'; describe('SpriteSelectorItemComponent', () => { let className; @@ -16,13 +14,16 @@ describe('SpriteSelectorItemComponent', () => { // Wrap this in a function so it gets test specific states and can be reused. const getComponent = function () { - return <SpriteSelectorItemComponent - className={className} - costumeURL={costumeURL} - name={name} - onClick={onClick} - onDeleteButtonClick={onDeleteButtonClick} - selected={selected}/>; + return ( + <SpriteSelectorItemComponent + className={className} + costumeURL={costumeURL} + name={name} + selected={selected} + onClick={onClick} + onDeleteButtonClick={onDeleteButtonClick} + /> + ); }; beforeEach(() => { diff --git a/test/unit/containers/green-flag.test.jsx b/test/unit/containers/green-flag.test.jsx index 24c3ae9c546e82d4b89476992bbcf1f159b0413f..4744ebaabd147318afdf430596f53e243b950700 100644 --- a/test/unit/containers/green-flag.test.jsx +++ b/test/unit/containers/green-flag.test.jsx @@ -1,7 +1,6 @@ -/* eslint-env jest */ -import React from 'react'; // eslint-disable-line no-unused-vars +import React from 'react'; import {shallow} from 'enzyme'; -import GreenFlag from '../../../src/containers/green-flag'; // eslint-disable-line no-unused-vars +import GreenFlag from '../../../src/containers/green-flag'; import renderer from 'react-test-renderer'; import VM from 'scratch-vm'; @@ -13,14 +12,20 @@ describe('GreenFlag Container', () => { test('renders active state', () => { const component = renderer.create( - <GreenFlag active={true} vm={vm}/> + <GreenFlag + active + vm={vm} + /> ); expect(component.toJSON()).toMatchSnapshot(); }); test('renders inactive state', () => { const component = renderer.create( - <GreenFlag active={false} vm={vm}/> + <GreenFlag + active={false} + vm={vm} + /> ); expect(component.toJSON()).toMatchSnapshot(); }); @@ -28,7 +33,11 @@ describe('GreenFlag Container', () => { test('triggers onClick when active', () => { const onClick = jest.fn(); const componentShallowWrapper = shallow( - <GreenFlag active={true} onClick={onClick} vm={vm}/> + <GreenFlag + active + vm={vm} + onClick={onClick} + /> ); componentShallowWrapper.simulate('click'); expect(onClick).toHaveBeenCalled(); diff --git a/test/unit/containers/sound-editor.test.jsx b/test/unit/containers/sound-editor.test.jsx index 5a5618ea96cd88491b5a7de40cc0d321632ae1b8..819e38149adfb55e7f90c632aef71caa600b51c0 100644 --- a/test/unit/containers/sound-editor.test.jsx +++ b/test/unit/containers/sound-editor.test.jsx @@ -1,12 +1,10 @@ -/* eslint-env jest */ -import React from 'react'; // eslint-disable-line no-unused-vars -import {mountWithIntl} from '../../helpers/intl-helpers'; +import React from 'react'; +import {mountWithIntl} from '../../helpers/intl-helpers.jsx'; import configureStore from 'redux-mock-store'; import mockAudioBufferPlayer from '../../__mocks__/audio-buffer-player.js'; import mockAudioEffects from '../../__mocks__/audio-effects.js'; -import SoundEditor from '../../../src/containers/sound-editor'; // eslint-disable-line no-unused-vars -// eslint-disable-next-line no-unused-vars +import SoundEditor from '../../../src/containers/sound-editor'; import SoundEditorComponent from '../../../src/components/sound-editor/sound-editor'; jest.mock('../../../src/lib/audio/audio-buffer-player', () => mockAudioBufferPlayer); @@ -17,7 +15,7 @@ describe('Sound Editor Container', () => { let store; let soundIndex; let soundBuffer; - let samples = new Float32Array([0, 0, 0]); // eslint-disable-line no-undef + const samples = new Float32Array([0, 0, 0]); // eslint-disable-line no-undef let vm; beforeEach(() => { @@ -40,7 +38,12 @@ describe('Sound Editor Container', () => { }); test('should pass the correct data to the component from the store', () => { - const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />); + const wrapper = mountWithIntl( + <SoundEditor + soundIndex={soundIndex} + store={store} + /> + ); const componentProps = wrapper.find(SoundEditorComponent).props(); // Data retreived and processed by the `connect` with the store expect(componentProps.name).toEqual('first name'); @@ -54,7 +57,12 @@ describe('Sound Editor Container', () => { }); test('it plays when clicked and stops when clicked again', () => { - const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />); + const wrapper = mountWithIntl( + <SoundEditor + soundIndex={soundIndex} + store={store} + /> + ); const component = wrapper.find(SoundEditorComponent); // Ensure rendering doesn't start playing any sounds expect(mockAudioBufferPlayer.instance.play.mock.calls).toEqual([]); @@ -73,7 +81,12 @@ describe('Sound Editor Container', () => { }); test('it sets the component props for trimming and submits to the vm', () => { - const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />); + const wrapper = mountWithIntl( + <SoundEditor + soundIndex={soundIndex} + store={store} + /> + ); const component = wrapper.find(SoundEditorComponent); component.props().onActivateTrim(); @@ -87,14 +100,24 @@ describe('Sound Editor Container', () => { }); test('it submits name changes to the vm', () => { - const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />); + const wrapper = mountWithIntl( + <SoundEditor + soundIndex={soundIndex} + store={store} + /> + ); const component = wrapper.find(SoundEditorComponent); component.props().onChangeName('hello'); expect(vm.renameSound).toHaveBeenCalledWith(soundIndex, 'hello'); }); - test('it handles an effect by submitting the result and playing', (done) => { - const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />); + test('it handles an effect by submitting the result and playing', done => { + const wrapper = mountWithIntl( + <SoundEditor + soundIndex={soundIndex} + store={store} + /> + ); const component = wrapper.find(SoundEditorComponent); component.props().onReverse(); // Could be any of the effects, just testing the end result mockAudioEffects.instance._finishProcessing(soundBuffer); @@ -106,7 +129,12 @@ describe('Sound Editor Container', () => { }); test('it handles reverse effect correctly', () => { - const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />); + const wrapper = mountWithIntl( + <SoundEditor + soundIndex={soundIndex} + store={store} + /> + ); const component = wrapper.find(SoundEditorComponent); component.props().onReverse(); expect(mockAudioEffects.instance.name).toEqual(mockAudioEffects.effectTypes.REVERSE); @@ -114,7 +142,12 @@ describe('Sound Editor Container', () => { }); test('it handles louder effect correctly', () => { - const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />); + const wrapper = mountWithIntl( + <SoundEditor + soundIndex={soundIndex} + store={store} + /> + ); const component = wrapper.find(SoundEditorComponent); component.props().onLouder(); expect(mockAudioEffects.instance.name).toEqual(mockAudioEffects.effectTypes.LOUDER); @@ -122,7 +155,12 @@ describe('Sound Editor Container', () => { }); test('it handles softer effect correctly', () => { - const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />); + const wrapper = mountWithIntl( + <SoundEditor + soundIndex={soundIndex} + store={store} + /> + ); const component = wrapper.find(SoundEditorComponent); component.props().onSofter(); expect(mockAudioEffects.instance.name).toEqual(mockAudioEffects.effectTypes.SOFTER); @@ -130,7 +168,12 @@ describe('Sound Editor Container', () => { }); test('it handles faster effect correctly', () => { - const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />); + const wrapper = mountWithIntl( + <SoundEditor + soundIndex={soundIndex} + store={store} + /> + ); const component = wrapper.find(SoundEditorComponent); component.props().onFaster(); expect(mockAudioEffects.instance.name).toEqual(mockAudioEffects.effectTypes.FASTER); @@ -138,7 +181,12 @@ describe('Sound Editor Container', () => { }); test('it handles slower effect correctly', () => { - const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />); + const wrapper = mountWithIntl( + <SoundEditor + soundIndex={soundIndex} + store={store} + /> + ); const component = wrapper.find(SoundEditorComponent); component.props().onSlower(); expect(mockAudioEffects.instance.name).toEqual(mockAudioEffects.effectTypes.SLOWER); @@ -146,7 +194,12 @@ describe('Sound Editor Container', () => { }); test('it handles echo effect correctly', () => { - const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />); + const wrapper = mountWithIntl( + <SoundEditor + soundIndex={soundIndex} + store={store} + /> + ); const component = wrapper.find(SoundEditorComponent); component.props().onEcho(); expect(mockAudioEffects.instance.name).toEqual(mockAudioEffects.effectTypes.ECHO); @@ -154,7 +207,12 @@ describe('Sound Editor Container', () => { }); test('it handles robot effect correctly', () => { - const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />); + const wrapper = mountWithIntl( + <SoundEditor + soundIndex={soundIndex} + store={store} + /> + ); const component = wrapper.find(SoundEditorComponent); component.props().onRobot(); expect(mockAudioEffects.instance.name).toEqual(mockAudioEffects.effectTypes.ROBOT); @@ -162,7 +220,12 @@ describe('Sound Editor Container', () => { }); test('undo/redo functionality', () => { - const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />); + const wrapper = mountWithIntl( + <SoundEditor + soundIndex={soundIndex} + store={store} + /> + ); const component = wrapper.find(SoundEditorComponent); // Undo and redo should be disabled initially expect(component.prop('canUndo')).toEqual(false); diff --git a/test/unit/containers/sprite-selector-item.test.jsx b/test/unit/containers/sprite-selector-item.test.jsx index 1efd40cb7cc05a26f9521dee06ad3935225fd7e8..2a371a1a0dfe763c32737ee832a9a3009e57b0e7 100644 --- a/test/unit/containers/sprite-selector-item.test.jsx +++ b/test/unit/containers/sprite-selector-item.test.jsx @@ -1,11 +1,10 @@ -/* eslint-env jest */ -import React from 'react'; // eslint-disable-line no-unused-vars -import {mountWithIntl} from '../../helpers/intl-helpers'; +import React from 'react'; +import {mountWithIntl} from '../../helpers/intl-helpers.jsx'; import configureStore from 'redux-mock-store'; -import {Provider} from 'react-redux'; // eslint-disable-line no-unused-vars +import {Provider} from 'react-redux'; -import SpriteSelectorItem from '../../../src/containers/sprite-selector-item'; // eslint-disable-line no-unused-vars -import CloseButton from '../../../src/components/close-button/close-button'; // eslint-disable-line no-unused-vars +import SpriteSelectorItem from '../../../src/containers/sprite-selector-item'; +import CloseButton from '../../../src/components/close-button/close-button'; describe('SpriteSelectorItem Container', () => { const mockStore = configureStore(); @@ -19,14 +18,19 @@ describe('SpriteSelectorItem Container', () => { let store; // Wrap this in a function so it gets test specific states and can be reused. const getContainer = function () { - return <Provider store={store}><SpriteSelectorItem - className={className} - costumeURL={costumeURL} - id={id} - name={name} - onClick={onClick} - onDeleteButtonClick={onDeleteButtonClick} - selected={selected}/></Provider>; + return ( + <Provider store={store}> + <SpriteSelectorItem + className={className} + costumeURL={costumeURL} + id={id} + name={name} + selected={selected} + onClick={onClick} + onDeleteButtonClick={onDeleteButtonClick} + /> + </Provider> + ); }; beforeEach(() => { diff --git a/test/unit/util/audio-effects.test.js b/test/unit/util/audio-effects.test.js index e30a2e6c14d40528fddf8b5dd2f69ebc93bec5f0..d1057de53777355a9d9f9a7dfbf4babf95c3e63d 100644 --- a/test/unit/util/audio-effects.test.js +++ b/test/unit/util/audio-effects.test.js @@ -1,5 +1,4 @@ -/* eslint-env jest */ -/* global AudioNode AudioContext WebAudioTestAPI */ +/* global WebAudioTestAPI */ import 'web-audio-test-api'; WebAudioTestAPI.setState({ 'OfflineAudioContext#startRendering': 'promise' @@ -11,8 +10,8 @@ import EchoEffect from '../../../src/lib/audio/effects/echo-effect'; import VolumeEffect from '../../../src/lib/audio/effects/volume-effect'; describe('Audio Effects manager', () => { - let audioContext = new AudioContext(); - let audioBuffer = audioContext.createBuffer(1, 400, 44100); + const audioContext = new AudioContext(); + const audioBuffer = audioContext.createBuffer(1, 400, 44100); test('changes buffer length and playback rate for faster effect', () => { const audioEffects = new AudioEffects(audioBuffer, 'faster'); diff --git a/test/unit/util/audio-util.test.js b/test/unit/util/audio-util.test.js index 743a5b1c492894ce420b1f6bb66fa977eb2f4522..24ddcd4559567dce0fd1677a11a809c365233699 100644 --- a/test/unit/util/audio-util.test.js +++ b/test/unit/util/audio-util.test.js @@ -1,4 +1,3 @@ -/* eslint-env jest */ import {computeRMS, computeChunkedRMS} from '../../../src/lib/audio/audio-util'; describe('computeRMS', () => { diff --git a/test/unit/util/project-loader-hoc.test.jsx b/test/unit/util/project-loader-hoc.test.jsx new file mode 100644 index 0000000000000000000000000000000000000000..964894c4369441e328e6e365f97631d3736f15b6 --- /dev/null +++ b/test/unit/util/project-loader-hoc.test.jsx @@ -0,0 +1,51 @@ +import React from 'react'; +import ProjectLoaderHOC, {ProjectLoader} from '../../../src/lib/project-loader-hoc.jsx'; +import {mount} from 'enzyme'; + +describe('ProjectLoaderHOC', () => { + test('when there is no project data, it renders null', () => { + const Component = ({projectData}) => <div>{projectData}</div>; + const WrappedComponent = ProjectLoaderHOC(Component); + window.location.hash = '#winning'; + ProjectLoader.load = jest.fn((id, cb) => cb(null, null)); + const mounted = mount(<WrappedComponent />); + ProjectLoader.load.mockRestore(); + window.location.hash = ''; + expect(mounted.find('div').exists()).toEqual(false); + }); + + test('when there is no hash, it loads the default project', () => { + const Component = ({projectData}) => <div>{projectData}</div>; + const WrappedComponent = ProjectLoaderHOC(Component); + window.location.hash = ''; + const mounted = mount(<WrappedComponent />); + expect(mounted.find('div').text()).toEqual(JSON.stringify(ProjectLoader.DEFAULT_PROJECT_DATA)); + }); + + test('when there is a hash, it tries to load that project', () => { + const Component = ({projectData}) => <div>{projectData}</div>; + const WrappedComponent = ProjectLoaderHOC(Component); + window.location.hash = '#winning'; + ProjectLoader.load = jest.fn((id, cb) => cb(null, id)); + const mounted = mount(<WrappedComponent />); + mounted.update(); + ProjectLoader.load.mockRestore(); + window.location.hash = ''; + expect(mounted + .find('div') + .text() + ).toEqual('winning'); + }); + + test('when hash change happens, the project data state is changed', () => { + const Component = ({projectData}) => <div>{projectData}</div>; + const WrappedComponent = ProjectLoaderHOC(Component); + window.location.hash = ''; + const mounted = mount(<WrappedComponent />); + const before = mounted.find('div').text(); + ProjectLoader.load = jest.fn((id, cb) => cb(null, id)); + window.location.hash = `#winning`; + mounted.node.updateProject(); + expect(mounted.find('div').text()).not.toEqual(before); + }); +}); diff --git a/webpack.config.js b/webpack.config.js index 44595ff2cd505fc28a3e6bc4666732a802c0c86d..f39fc81a9f7cf0f487d01fd6077e8d5a548de77c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -19,7 +19,9 @@ module.exports = { devtool: 'cheap-module-source-map', entry: { lib: ['react', 'react-dom'], - gui: './src/index.jsx' + gui: './src/index.jsx', + blocksonly: './src/examples/blocks-only.jsx', + player: './src/examples/player.jsx' }, output: { path: path.resolve(__dirname, 'build'), @@ -78,9 +80,22 @@ module.exports = { filename: 'lib.min.js' }), new HtmlWebpackPlugin({ + chunks: ['lib', 'gui'], template: 'src/index.ejs', title: 'Scratch 3.0 GUI' }), + new HtmlWebpackPlugin({ + chunks: ['lib', 'blocksonly'], + template: 'src/index.ejs', + filename: 'blocks-only.html', + title: 'Scratch 3.0 GUI: Blocks Only Example' + }), + new HtmlWebpackPlugin({ + chunks: ['lib', 'player'], + template: 'src/index.ejs', + filename: 'player.html', + title: 'Scratch 3.0 GUI: Player Example' + }), new CopyWebpackPlugin([{ from: 'node_modules/scratch-blocks/media', to: 'static/blocks-media'