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'