diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx
index 9846eed7566356ed1e5395def9b59388bb870958..8524b2ba36cdf19ebe818ece46fa79021042dc64 100644
--- a/src/components/gui/gui.jsx
+++ b/src/components/gui/gui.jsx
@@ -105,6 +105,7 @@ const GUIComponent = props => {
         onRequestCloseTelemetryModal,
         onSeeCommunity,
         onShare,
+        onStartSelectingFileUpload,
         onTelemetryModalCancel,
         onTelemetryModalOptIn,
         onTelemetryModalOptOut,
@@ -226,6 +227,7 @@ const GUIComponent = props => {
                     onProjectTelemetryEvent={onProjectTelemetryEvent}
                     onSeeCommunity={onSeeCommunity}
                     onShare={onShare}
+                    onStartSelectingFileUpload={onStartSelectingFileUpload}
                     onToggleLoginOpen={onToggleLoginOpen}
                 />
                 <Box className={styles.bodyWrapper}>
@@ -399,6 +401,7 @@ GUIComponent.propTypes = {
     onRequestCloseTelemetryModal: PropTypes.func,
     onSeeCommunity: PropTypes.func,
     onShare: PropTypes.func,
+    onStartSelectingFileUpload: PropTypes.func,
     onTabSelect: PropTypes.func,
     onTelemetryModalCancel: PropTypes.func,
     onTelemetryModalOptIn: PropTypes.func,
diff --git a/src/components/menu-bar/menu-bar.jsx b/src/components/menu-bar/menu-bar.jsx
index 98bc128a92f11fe272d674513930e53f897d58bb..05a5294bc30eadfe41a723f7712bc4b3fcb0e837 100644
--- a/src/components/menu-bar/menu-bar.jsx
+++ b/src/components/menu-bar/menu-bar.jsx
@@ -17,7 +17,6 @@ import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx';
 import Divider from '../divider/divider.jsx';
 import LanguageSelector from '../../containers/language-selector.jsx';
 import SaveStatus from './save-status.jsx';
-import SBFileUploader from '../../containers/sb-file-uploader.jsx';
 import ProjectWatcher from '../../containers/project-watcher.jsx';
 import MenuBarMenu from './menu-bar-menu.jsx';
 import {MenuItem, MenuSection} from '../menu/menu.jsx';
@@ -392,22 +391,11 @@ class MenuBar extends React.Component {
                                         </MenuSection>
                                     )}
                                     <MenuSection>
-                                        <SBFileUploader
-                                            canSave={this.props.canSave}
-                                            userOwnsProject={this.props.userOwnsProject}
+                                        <MenuItem
+                                            onClick={this.props.onStartSelectingFileUpload}
                                         >
-                                            {(className, renderFileInput, handleLoadProject) => (
-                                                <MenuItem
-                                                    className={className}
-                                                    onClick={handleLoadProject}
-                                                >
-                                                    {/* eslint-disable max-len */}
-                                                    {this.props.intl.formatMessage(sharedMessages.loadFromComputerTitle)}
-                                                    {/* eslint-enable max-len */}
-                                                    {renderFileInput()}
-                                                </MenuItem>
-                                            )}
-                                        </SBFileUploader>
+                                            {this.props.intl.formatMessage(sharedMessages.loadFromComputerTitle)}
+                                        </MenuItem>
                                         <SB3Downloader>{(className, downloadProjectCallback) => (
                                             <MenuItem
                                                 className={className}
@@ -743,6 +731,7 @@ MenuBar.propTypes = {
     onRequestCloseLogin: PropTypes.func,
     onSeeCommunity: PropTypes.func,
     onShare: PropTypes.func,
+    onStartSelectingFileUpload: PropTypes.func,
     onToggleLoginOpen: PropTypes.func,
     projectTitle: PropTypes.string,
     renderLogin: PropTypes.func,
diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx
index 314c38893518b0a8e5a8bce6a847dc008a6ac5b6..c10df0fb3e9bf73c3ca60d342f8d4f155a6e61c0 100644
--- a/src/containers/gui.jsx
+++ b/src/containers/gui.jsx
@@ -27,6 +27,7 @@ import {
 
 import FontLoaderHOC from '../lib/font-loader-hoc.jsx';
 import LocalizationHOC from '../lib/localization-hoc.jsx';
+import SBFileUploaderHOC from '../lib/sb-file-uploader-hoc.jsx';
 import ProjectFetcherHOC from '../lib/project-fetcher-hoc.jsx';
 import TitledHOC from '../lib/titled-hoc.jsx';
 import ProjectSaverHOC from '../lib/project-saver-hoc.jsx';
@@ -181,6 +182,7 @@ const WrappedGui = compose(
     ProjectSaverHOC,
     vmListenerHOC,
     vmManagerHOC,
+    SBFileUploaderHOC,
     cloudManagerHOC
 )(ConnectedGUI);
 
diff --git a/src/containers/sb-file-uploader.jsx b/src/containers/sb-file-uploader.jsx
deleted file mode 100644
index 2019f719e978a9c485b540488f873cb8046644ee..0000000000000000000000000000000000000000
--- a/src/containers/sb-file-uploader.jsx
+++ /dev/null
@@ -1,226 +0,0 @@
-import bindAll from 'lodash.bindall';
-import PropTypes from 'prop-types';
-import React from 'react';
-import {connect} from 'react-redux';
-import {defineMessages, injectIntl, intlShape} from 'react-intl';
-import {setProjectTitle} from '../reducers/project-title';
-
-import log from '../lib/log';
-import sharedMessages from '../lib/shared-messages';
-
-import {
-    LoadingStates,
-    getIsLoadingUpload,
-    getIsShowingWithoutId,
-    onLoadedProject,
-    requestProjectUpload
-} from '../reducers/project-state';
-
-import {
-    openLoadingProject,
-    closeLoadingProject
-} from '../reducers/modals';
-import {
-    closeFileMenu
-} from '../reducers/menus';
-
-/**
- * SBFileUploader component passes a file input, load handler and props to its child.
- * It expects this child to be a function with the signature
- *     function (renderFileInput, handleLoadProject) {}
- * The component can then be used to attach project loading functionality
- * to any other component:
- *
- * <SBFileUploader>{(className, renderFileInput, handleLoadProject) => (
- *     <MyCoolComponent
- *         className={className}
- *         onClick={handleLoadProject}
- *     >
- *         {renderFileInput()}
- *     </MyCoolComponent>
- * )}</SBFileUploader>
- */
-
-const messages = defineMessages({
-    loadError: {
-        id: 'gui.projectLoader.loadError',
-        defaultMessage: 'The project file that was selected failed to load.',
-        description: 'An error that displays when a local project file fails to load.'
-    }
-});
-
-class SBFileUploader extends React.Component {
-    constructor (props) {
-        super(props);
-        bindAll(this, [
-            'getProjectTitleFromFilename',
-            'renderFileInput',
-            'setFileInput',
-            'handleChange',
-            'handleClick',
-            'onload',
-            'resetFileInput'
-        ]);
-    }
-    componentWillMount () {
-        this.reader = new FileReader();
-        this.reader.onload = this.onload;
-        this.resetFileInput();
-    }
-    componentDidUpdate (prevProps) {
-        if (this.props.isLoadingUpload && !prevProps.isLoadingUpload && this.fileToUpload && this.reader) {
-            this.reader.readAsArrayBuffer(this.fileToUpload);
-        }
-    }
-    componentWillUnmount () {
-        this.reader = null;
-        this.resetFileInput();
-    }
-    resetFileInput () {
-        this.fileToUpload = null;
-        if (this.fileInput) {
-            this.fileInput.value = null;
-        }
-    }
-    getProjectTitleFromFilename (fileInputFilename) {
-        if (!fileInputFilename) return '';
-        // only parse title with valid scratch project extensions
-        // (.sb, .sb2, and .sb3)
-        const matches = fileInputFilename.match(/^(.*)\.sb[23]?$/);
-        if (!matches) return '';
-        return matches[1].substring(0, 100); // truncate project title to max 100 chars
-    }
-    // called when user has finished selecting a file to upload
-    handleChange (e) {
-        const {
-            intl,
-            isShowingWithoutId,
-            loadingState,
-            projectChanged,
-            userOwnsProject
-        } = this.props;
-
-        const thisFileInput = e.target;
-        if (thisFileInput.files) { // Don't attempt to load if no file was selected
-            this.fileToUpload = thisFileInput.files[0];
-
-            // If user owns the project, or user has changed the project,
-            // we must confirm with the user that they really intend to replace it.
-            // (If they don't own the project and haven't changed it, no need to confirm.)
-            let uploadAllowed = true;
-            if (userOwnsProject || (projectChanged && isShowingWithoutId)) {
-                uploadAllowed = confirm( // eslint-disable-line no-alert
-                    intl.formatMessage(sharedMessages.replaceProjectWarning)
-                );
-            }
-            if (uploadAllowed) {
-                this.props.requestProjectUpload(loadingState);
-            } else {
-                this.props.closeFileMenu();
-            }
-        }
-    }
-    // called when file upload raw data is available in the reader
-    onload () {
-        if (this.reader) {
-            this.props.onLoadingStarted();
-            const filename = this.fileToUpload && this.fileToUpload.name;
-            this.props.vm.loadProject(this.reader.result)
-                .then(() => {
-                    this.props.onLoadingFinished(this.props.loadingState, true);
-                    // Reset the file input after project is loaded
-                    // This is necessary in case the user wants to reload a project
-                    if (filename) {
-                        const uploadedProjectTitle = this.getProjectTitleFromFilename(filename);
-                        this.props.onReceivedProjectTitle(uploadedProjectTitle);
-                    }
-                    this.resetFileInput();
-                })
-                .catch(error => {
-                    log.warn(error);
-                    alert(this.props.intl.formatMessage(messages.loadError)); // eslint-disable-line no-alert
-                    this.props.onLoadingFinished(this.props.loadingState, false);
-                    // Reset the file input after project is loaded
-                    // This is necessary in case the user wants to reload a project
-                    this.resetFileInput();
-                });
-        }
-    }
-    handleClick () {
-        // open filesystem browsing window
-        this.fileInput.click();
-    }
-    setFileInput (input) {
-        this.fileInput = input;
-    }
-    renderFileInput () {
-        return (
-            <input
-                accept=".sb,.sb2,.sb3"
-                ref={this.setFileInput}
-                style={{display: 'none'}}
-                type="file"
-                onChange={this.handleChange}
-            />
-        );
-    }
-    render () {
-        return this.props.children(this.props.className, this.renderFileInput, this.handleClick);
-    }
-}
-
-SBFileUploader.propTypes = {
-    canSave: PropTypes.bool, // eslint-disable-line react/no-unused-prop-types
-    children: PropTypes.func,
-    className: PropTypes.string,
-    closeFileMenu: PropTypes.func,
-    intl: intlShape.isRequired,
-    isLoadingUpload: PropTypes.bool,
-    isShowingWithoutId: PropTypes.bool,
-    loadingState: PropTypes.oneOf(LoadingStates),
-    onLoadingFinished: PropTypes.func,
-    onLoadingStarted: PropTypes.func,
-    projectChanged: PropTypes.bool,
-    requestProjectUpload: PropTypes.func,
-    onReceivedProjectTitle: PropTypes.func,
-    userOwnsProject: PropTypes.bool,
-    vm: PropTypes.shape({
-        loadProject: PropTypes.func
-    })
-};
-SBFileUploader.defaultProps = {
-    className: ''
-};
-const mapStateToProps = state => {
-    const loadingState = state.scratchGui.projectState.loadingState;
-    return {
-        isLoadingUpload: getIsLoadingUpload(loadingState),
-        isShowingWithoutId: getIsShowingWithoutId(loadingState),
-        loadingState: loadingState,
-        projectChanged: state.scratchGui.projectChanged,
-        vm: state.scratchGui.vm
-    };
-};
-
-const mapDispatchToProps = (dispatch, ownProps) => ({
-    closeFileMenu: () => dispatch(closeFileMenu()),
-    onLoadingFinished: (loadingState, success) => {
-        dispatch(onLoadedProject(loadingState, ownProps.canSave, success));
-        dispatch(closeLoadingProject());
-        dispatch(closeFileMenu());
-    },
-    requestProjectUpload: loadingState => dispatch(requestProjectUpload(loadingState)),
-    onLoadingStarted: () => dispatch(openLoadingProject()),
-    onReceivedProjectTitle: title => dispatch(setProjectTitle(title))
-});
-
-// Allow incoming props to override redux-provided props. Used to mock in tests.
-const mergeProps = (stateProps, dispatchProps, ownProps) => Object.assign(
-    {}, stateProps, dispatchProps, ownProps
-);
-
-export default connect(
-    mapStateToProps,
-    mapDispatchToProps,
-    mergeProps
-)(injectIntl(SBFileUploader));
diff --git a/src/lib/sb-file-uploader-hoc.jsx b/src/lib/sb-file-uploader-hoc.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..9e34b57684ef9390d6d9517d47e1ea3c1c8863ee
--- /dev/null
+++ b/src/lib/sb-file-uploader-hoc.jsx
@@ -0,0 +1,269 @@
+import bindAll from 'lodash.bindall';
+import React from 'react';
+import PropTypes from 'prop-types';
+import {defineMessages, intlShape, injectIntl} from 'react-intl';
+import {connect} from 'react-redux';
+import log from '../lib/log';
+import sharedMessages from './shared-messages';
+
+import {
+    LoadingStates,
+    getIsLoadingUpload,
+    getIsShowingWithoutId,
+    onLoadedProject,
+    requestProjectUpload
+} from '../reducers/project-state';
+import {
+    openLoadingProject,
+    closeLoadingProject
+} from '../reducers/modals';
+import {
+    closeFileMenu
+} from '../reducers/menus';
+
+const messages = defineMessages({
+    loadError: {
+        id: 'gui.projectLoader.loadError',
+        defaultMessage: 'The project file that was selected failed to load.',
+        description: 'An error that displays when a local project file fails to load.'
+    }
+});
+
+/**
+ * Higher Order Component to provide behavior for loading local project files into editor.
+ * @param {React.Component} WrappedComponent the component to add project file loading functionality to
+ * @returns {React.Component} WrappedComponent with project file loading functionality added
+ *
+ * <SBFileUploaderHOC>
+ *     <WrappedComponent />
+ * </SBFileUploaderHOC>
+ */
+const SBFileUploaderHOC = function (WrappedComponent) {
+    class SBFileUploaderComponent extends React.Component {
+        constructor (props) {
+            super(props);
+            bindAll(this, [
+                'createFileObjects',
+                'getProjectTitleFromFilename',
+                'handleFinishedLoadingUpload',
+                'handleStartSelectingFileUpload',
+                'handleChange',
+                'onload',
+                'removeFileObjects'
+            ]);
+        }
+        componentDidUpdate (prevProps) {
+            if (this.props.isLoadingUpload && !prevProps.isLoadingUpload) {
+                this.handleFinishedLoadingUpload(); // cue step 5 below
+            }
+        }
+        componentWillUnmount () {
+            this.removeFileObjects();
+        }
+        // step 1: this is where the upload process begins
+        handleStartSelectingFileUpload () {
+            this.createFileObjects(); // go to step 2
+        }
+        // step 2: create a FileReader and an <input> element, and issue a
+        // pseudo-click to it. That will open the file chooser dialog.
+        createFileObjects () {
+            // redo step 7, in case it got skipped last time and its objects are
+            // still in memory
+            this.removeFileObjects();
+            // create fileReader
+            this.fileReader = new FileReader();
+            this.fileReader.onload = this.onload;
+            // create <input> element and add it to DOM
+            this.inputElement = document.createElement('input');
+            this.inputElement.accept = '.sb,.sb2,.sb3';
+            this.inputElement.style = 'display: none;';
+            this.inputElement.type = 'file';
+            this.inputElement.onchange = this.handleChange; // connects to step 3
+            document.body.appendChild(this.inputElement);
+            // simulate a click to open file chooser dialog
+            this.inputElement.click();
+        }
+        // step 3: user has picked a file using the file chooser dialog.
+        // We don't actually load the file here, we only decide whether to do so.
+        handleChange (e) {
+            const {
+                intl,
+                isShowingWithoutId,
+                loadingState,
+                projectChanged,
+                userOwnsProject
+            } = this.props;
+            const thisFileInput = e.target;
+            if (thisFileInput.files) { // Don't attempt to load if no file was selected
+                this.fileToUpload = thisFileInput.files[0];
+
+                // If user owns the project, or user has changed the project,
+                // we must confirm with the user that they really intend to
+                // replace it. (If they don't own the project and haven't
+                // changed it, no need to confirm.)
+                let uploadAllowed = true;
+                if (userOwnsProject || (projectChanged && isShowingWithoutId)) {
+                    uploadAllowed = confirm( // eslint-disable-line no-alert
+                        intl.formatMessage(sharedMessages.replaceProjectWarning)
+                    );
+                }
+                if (uploadAllowed) {
+                    // cues step 4
+                    this.props.requestProjectUpload(loadingState);
+                } else {
+                    // skips ahead to step 7
+                    this.removeFileObjects();
+                }
+                this.props.closeFileMenu();
+            }
+        }
+        // step 4 is below, in mapDispatchToProps
+
+        // step 5: called from componentDidUpdate when project state shows
+        // that project data has finished "uploading" into the browser
+        handleFinishedLoadingUpload () {
+            if (this.fileToUpload && this.fileReader) {
+                // begin to read data from the file. When finished,
+                // cues step 6 using the reader's onload callback
+                this.fileReader.readAsArrayBuffer(this.fileToUpload);
+            } else {
+                this.props.cancelFileUpload(this.props.loadingState);
+                // skip ahead to step 7
+                this.removeFileObjects();
+            }
+        }
+        // used in step 6 below
+        getProjectTitleFromFilename (fileInputFilename) {
+            if (!fileInputFilename) return '';
+            // only parse title with valid scratch project extensions
+            // (.sb, .sb2, and .sb3)
+            const matches = fileInputFilename.match(/^(.*)\.sb[23]?$/);
+            if (!matches) return '';
+            return matches[1].substring(0, 100); // truncate project title to max 100 chars
+        }
+        // step 6: attached as a handler on our FileReader object; called when
+        // file upload raw data is available in the reader
+        onload () {
+            if (this.fileReader) {
+                this.props.onLoadingStarted();
+                const filename = this.fileToUpload && this.fileToUpload.name;
+                this.props.vm.loadProject(this.fileReader.result)
+                    .then(() => {
+                        this.props.onLoadingFinished(this.props.loadingState, true);
+                        if (filename) {
+                            const uploadedProjectTitle = this.getProjectTitleFromFilename(filename);
+                            this.props.onUpdateProjectTitle(uploadedProjectTitle);
+                        }
+                    })
+                    .catch(error => {
+                        log.warn(error);
+                        this.props.intl.formatMessage(messages.loadError);
+                        this.props.onLoadingFinished(this.props.loadingState, false);
+                    })
+                    .then(() => {
+                        // go back to step 7: whether project loading succeeded
+                        // or failed, reset file objects
+                        this.removeFileObjects();
+                    });
+            }
+        }
+        // step 7: remove the <input> element from the DOM and clear reader and
+        // fileToUpload reference, so those objects can be garbage collected
+        removeFileObjects () {
+            if (this.inputElement) {
+                this.inputElement.value = null;
+                document.body.removeChild(this.inputElement);
+            }
+            this.inputElement = null;
+            this.fileReader = null;
+            this.fileToUpload = null;
+        }
+        render () {
+            const {
+                /* eslint-disable no-unused-vars */
+                cancelFileUpload,
+                closeFileMenu: closeFileMenuProp,
+                isLoadingUpload,
+                isShowingWithoutId,
+                loadingState,
+                onLoadingFinished,
+                onLoadingStarted,
+                projectChanged,
+                requestProjectUpload: requestProjectUploadProp,
+                userOwnsProject,
+                /* eslint-enable no-unused-vars */
+                ...componentProps
+            } = this.props;
+            return (
+                <React.Fragment>
+                    <WrappedComponent
+                        onStartSelectingFileUpload={this.handleStartSelectingFileUpload}
+                        {...componentProps}
+                    />
+                </React.Fragment>
+            );
+        }
+    }
+
+    SBFileUploaderComponent.propTypes = {
+        canSave: PropTypes.bool,
+        cancelFileUpload: PropTypes.func,
+        closeFileMenu: PropTypes.func,
+        intl: intlShape.isRequired,
+        isLoadingUpload: PropTypes.bool,
+        isShowingWithoutId: PropTypes.bool,
+        loadingState: PropTypes.oneOf(LoadingStates),
+        onLoadingFinished: PropTypes.func,
+        onLoadingStarted: PropTypes.func,
+        onUpdateProjectTitle: PropTypes.func,
+        projectChanged: PropTypes.bool,
+        requestProjectUpload: PropTypes.func,
+        userOwnsProject: PropTypes.bool,
+        vm: PropTypes.shape({
+            loadProject: PropTypes.func
+        })
+    };
+    const mapStateToProps = (state, ownProps) => {
+        const loadingState = state.scratchGui.projectState.loadingState;
+        const user = state.session && state.session.session && state.session.session.user;
+        return {
+            isLoadingUpload: getIsLoadingUpload(loadingState),
+            isShowingWithoutId: getIsShowingWithoutId(loadingState),
+            loadingState: loadingState,
+            projectChanged: state.scratchGui.projectChanged,
+            userOwnsProject: ownProps.authorUsername && user &&
+                (ownProps.authorUsername === user.username),
+            vm: state.scratchGui.vm
+        };
+    };
+    const mapDispatchToProps = (dispatch, ownProps) => ({
+        cancelFileUpload: loadingState => dispatch(onLoadedProject(loadingState, false, false)),
+        closeFileMenu: () => dispatch(closeFileMenu()),
+        // transition project state from loading to regular, and close
+        // loading screen and file menu
+        onLoadingFinished: (loadingState, success) => {
+            dispatch(onLoadedProject(loadingState, ownProps.canSave, success));
+            dispatch(closeLoadingProject());
+            dispatch(closeFileMenu());
+        },
+        // show project loading screen
+        onLoadingStarted: () => dispatch(openLoadingProject()),
+        // step 4: transition the project state so we're ready to handle the new
+        // project data. When this is done, the project state transition will be
+        // noticed by componentDidUpdate()
+        requestProjectUpload: loadingState => dispatch(requestProjectUpload(loadingState))
+    });
+    // Allow incoming props to override redux-provided props. Used to mock in tests.
+    const mergeProps = (stateProps, dispatchProps, ownProps) => Object.assign(
+        {}, stateProps, dispatchProps, ownProps
+    );
+    return injectIntl(connect(
+        mapStateToProps,
+        mapDispatchToProps,
+        mergeProps
+    )(SBFileUploaderComponent));
+};
+
+export {
+    SBFileUploaderHOC as default
+};
diff --git a/test/integration/menu-bar.test.js b/test/integration/menu-bar.test.js
index 3f6457e5208ae171685e62f70327a3f3463a5f4c..c83f1022d214a69f4bfa7cf019971334f4c4e153 100644
--- a/test/integration/menu-bar.test.js
+++ b/test/integration/menu-bar.test.js
@@ -75,6 +75,7 @@ describe('Menu bar settings', () => {
     test('User is not warned before uploading project file over a fresh project', async () => {
         await loadUri(uri);
         await clickText('File');
+        await clickText('Load from your computer');
         const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
         await input.sendKeys(path.resolve(__dirname, '../fixtures/project1.sb3'));
         // No replace alert since no changes were made
@@ -89,6 +90,7 @@ describe('Menu bar settings', () => {
         await clickText('delete', scope.spriteTile);
 
         await clickText('File');
+        await clickText('Load from your computer');
         const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
         await input.sendKeys(path.resolve(__dirname, '../fixtures/project1.sb3'));
         await driver.switchTo().alert()
diff --git a/test/unit/containers/sb-file-uploader.test.jsx b/test/unit/containers/sb-file-uploader.test.jsx
deleted file mode 100644
index c757114b4ca7e387c78e7477931cf31a04c91a9a..0000000000000000000000000000000000000000
--- a/test/unit/containers/sb-file-uploader.test.jsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import React from 'react';
-
-import {shallowWithIntl} from '../../helpers/intl-helpers.jsx';
-import configureStore from 'redux-mock-store';
-import SBFileUploader from '../../../src/containers/sb-file-uploader';
-import {LoadingState} from '../../../src/reducers/project-state';
-
-jest.mock('react-ga'); // must mock this entire library, or lib/analytics causes error
-
-describe('SBFileUploader Container', () => {
-    const mockStore = configureStore();
-    let onLoadingFinished;
-    let onLoadingStarted;
-    let store;
-
-    // Wrap this in a function so it gets test specific states and can be reused.
-    const getContainer = function () {
-        return (
-            <SBFileUploader
-                onLoadingFinished={onLoadingFinished}
-                onLoadingStarted={onLoadingStarted}
-            >
-                {(renderFileInput, loadProject) => (
-                    <div
-                        onClick={loadProject}
-                    />
-                )}
-            </SBFileUploader>
-        );
-    };
-
-    beforeEach(() => {
-        store = mockStore({
-            scratchGui: {
-                projectState: {
-                    loadingState: LoadingState.SHOWING_WITH_ID
-                },
-                vm: {}
-            }
-        });
-        onLoadingFinished = jest.fn();
-        onLoadingStarted = jest.fn();
-    });
-
-    test('correctly sets title with .sb3 filename', () => {
-        const wrapper = shallowWithIntl(getContainer(), {context: {store}});
-        const instance = wrapper
-            .dive() // unwrap redux Connect(InjectIntl(SBFileUploader))
-            .dive() // unwrap InjectIntl(SBFileUploader)
-            .instance(); // SBFileUploader
-        const projectName = instance.getProjectTitleFromFilename('my project is great.sb3');
-        expect(projectName).toBe('my project is great');
-    });
-
-    test('correctly sets title with .sb2 filename', () => {
-        const wrapper = shallowWithIntl(getContainer(), {context: {store}});
-        const instance = wrapper
-            .dive() // unwrap redux Connect(InjectIntl(SBFileUploader))
-            .dive() // unwrap InjectIntl(SBFileUploader)
-            .instance(); // SBFileUploader
-        const projectName = instance.getProjectTitleFromFilename('my project is great.sb2');
-        expect(projectName).toBe('my project is great');
-    });
-
-    test('correctly sets title with .sb filename', () => {
-        const wrapper = shallowWithIntl(getContainer(), {context: {store}});
-        const instance = wrapper
-            .dive() // unwrap redux Connect(InjectIntl(SBFileUploader))
-            .dive() // unwrap InjectIntl(SBFileUploader)
-            .instance(); // SBFileUploader
-        const projectName = instance.getProjectTitleFromFilename('my project is great.sb');
-        expect(projectName).toBe('my project is great');
-    });
-
-    test('sets blank title with filename with no extension', () => {
-        const wrapper = shallowWithIntl(getContainer(), {context: {store}});
-        const instance = wrapper
-            .dive() // unwrap redux Connect(InjectIntl(SBFileUploader))
-            .dive() // unwrap InjectIntl(SBFileUploader)
-            .instance(); // SBFileUploader
-        const projectName = instance.getProjectTitleFromFilename('my project is great');
-        expect(projectName).toBe('');
-    });
-});
diff --git a/test/unit/util/sb-file-uploader-hoc.test.jsx b/test/unit/util/sb-file-uploader-hoc.test.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..c200157c984d5afaf1ff384da292a0b417985210
--- /dev/null
+++ b/test/unit/util/sb-file-uploader-hoc.test.jsx
@@ -0,0 +1,108 @@
+import 'web-audio-test-api';
+
+import React from 'react';
+import configureStore from 'redux-mock-store';
+import {mountWithIntl, shallowWithIntl} from '../../helpers/intl-helpers.jsx';
+import {LoadingState} from '../../../src/reducers/project-state';
+import VM from 'scratch-vm';
+
+import SBFileUploaderHOC from '../../../src/lib/sb-file-uploader-hoc.jsx';
+
+describe('SBFileUploaderHOC', () => {
+    const mockStore = configureStore();
+    let store;
+    let vm;
+
+    // Wrap this in a function so it gets test specific states and can be reused.
+    const getContainer = function () {
+        const Component = () => <div />;
+        return SBFileUploaderHOC(Component);
+    };
+
+    const shallowMountWithContext = component => (
+        shallowWithIntl(component, {context: {store}})
+    );
+
+    const unwrappedInstance = () => {
+        const WrappedComponent = getContainer();
+        // default starting state: looking at a project you created, not logged in
+        const wrapper = shallowMountWithContext(
+            <WrappedComponent
+                projectChanged
+                canSave={false}
+                cancelFileUpload={jest.fn()}
+                closeFileMenu={jest.fn()}
+                requestProjectUpload={jest.fn()}
+                userOwnsProject={false}
+                vm={vm}
+                onLoadingFinished={jest.fn()}
+                onLoadingStarted={jest.fn()}
+                onUpdateProjectTitle={jest.fn()}
+            />
+        );
+        return wrapper
+            .dive() // unwrap intl
+            .dive() // unwrap redux Connect(SBFileUploaderComponent)
+            .instance(); // SBFileUploaderComponent
+    };
+
+    beforeEach(() => {
+        vm = new VM();
+        store = mockStore({
+            scratchGui: {
+                projectState: {
+                    loadingState: LoadingState.SHOWING_WITHOUT_ID
+                },
+                vm: {}
+            },
+            locales: {
+                locale: 'en'
+            }
+        });
+    });
+
+    test('correctly sets title with .sb3 filename', () => {
+        const projectName = unwrappedInstance().getProjectTitleFromFilename('my project is great.sb3');
+        expect(projectName).toBe('my project is great');
+    });
+
+    test('correctly sets title with .sb2 filename', () => {
+        const projectName = unwrappedInstance().getProjectTitleFromFilename('my project is great.sb2');
+        expect(projectName).toBe('my project is great');
+    });
+
+    test('correctly sets title with .sb filename', () => {
+        const projectName = unwrappedInstance().getProjectTitleFromFilename('my project is great.sb');
+        expect(projectName).toBe('my project is great');
+    });
+
+    test('sets blank title with filename with no extension', () => {
+        const projectName = unwrappedInstance().getProjectTitleFromFilename('my project is great');
+        expect(projectName).toBe('');
+    });
+
+    test('if isLoadingUpload becomes true, without fileToUpload set, will call cancelFileUpload', () => {
+        const mockedCancelFileUpload = jest.fn();
+        const WrappedComponent = getContainer();
+        const mounted = mountWithIntl(
+            <WrappedComponent
+                projectChanged
+                canSave={false}
+                cancelFileUpload={mockedCancelFileUpload}
+                closeFileMenu={jest.fn()}
+                isLoadingUpload={false}
+                requestProjectUpload={jest.fn()}
+                store={store}
+                userOwnsProject={false}
+                vm={vm}
+                onLoadingFinished={jest.fn()}
+                onLoadingStarted={jest.fn()}
+                onUpdateProjectTitle={jest.fn()}
+            />
+        );
+        mounted.setProps({
+            isLoadingUpload: true
+        });
+        expect(mockedCancelFileUpload).toHaveBeenCalled();
+    });
+});