diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index 8524b2ba36cdf19ebe818ece46fa79021042dc64..9846eed7566356ed1e5395def9b59388bb870958 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -105,7 +105,6 @@ const GUIComponent = props => { onRequestCloseTelemetryModal, onSeeCommunity, onShare, - onStartSelectingFileUpload, onTelemetryModalCancel, onTelemetryModalOptIn, onTelemetryModalOptOut, @@ -227,7 +226,6 @@ const GUIComponent = props => { onProjectTelemetryEvent={onProjectTelemetryEvent} onSeeCommunity={onSeeCommunity} onShare={onShare} - onStartSelectingFileUpload={onStartSelectingFileUpload} onToggleLoginOpen={onToggleLoginOpen} /> <Box className={styles.bodyWrapper}> @@ -401,7 +399,6 @@ 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 05a5294bc30eadfe41a723f7712bc4b3fcb0e837..98bc128a92f11fe272d674513930e53f897d58bb 100644 --- a/src/components/menu-bar/menu-bar.jsx +++ b/src/components/menu-bar/menu-bar.jsx @@ -17,6 +17,7 @@ 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'; @@ -391,11 +392,22 @@ class MenuBar extends React.Component { </MenuSection> )} <MenuSection> - <MenuItem - onClick={this.props.onStartSelectingFileUpload} + <SBFileUploader + canSave={this.props.canSave} + userOwnsProject={this.props.userOwnsProject} > - {this.props.intl.formatMessage(sharedMessages.loadFromComputerTitle)} - </MenuItem> + {(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> <SB3Downloader>{(className, downloadProjectCallback) => ( <MenuItem className={className} @@ -731,7 +743,6 @@ 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 c10df0fb3e9bf73c3ca60d342f8d4f155a6e61c0..314c38893518b0a8e5a8bce6a847dc008a6ac5b6 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -27,7 +27,6 @@ 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'; @@ -182,7 +181,6 @@ 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 new file mode 100644 index 0000000000000000000000000000000000000000..2019f719e978a9c485b540488f873cb8046644ee --- /dev/null +++ b/src/containers/sb-file-uploader.jsx @@ -0,0 +1,226 @@ +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 deleted file mode 100644 index 9e34b57684ef9390d6d9517d47e1ea3c1c8863ee..0000000000000000000000000000000000000000 --- a/src/lib/sb-file-uploader-hoc.jsx +++ /dev/null @@ -1,269 +0,0 @@ -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 c83f1022d214a69f4bfa7cf019971334f4c4e153..3f6457e5208ae171685e62f70327a3f3463a5f4c 100644 --- a/test/integration/menu-bar.test.js +++ b/test/integration/menu-bar.test.js @@ -75,7 +75,6 @@ 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 @@ -90,7 +89,6 @@ 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 new file mode 100644 index 0000000000000000000000000000000000000000..c757114b4ca7e387c78e7477931cf31a04c91a9a --- /dev/null +++ b/test/unit/containers/sb-file-uploader.test.jsx @@ -0,0 +1,84 @@ +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 deleted file mode 100644 index c200157c984d5afaf1ff384da292a0b417985210..0000000000000000000000000000000000000000 --- a/test/unit/util/sb-file-uploader-hoc.test.jsx +++ /dev/null @@ -1,108 +0,0 @@ -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(); - }); -});