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..90dd1ff5382acc3b7cbff02aeae46292d5bf1e1f --- /dev/null +++ b/src/lib/sb-file-uploader-hoc.jsx @@ -0,0 +1,241 @@ +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, [ + 'getProjectTitleFromFilename', + 'handleStartSelectingFileUpload', + 'setFileInput', + 'handleChange', + 'onload', + 'resetFileInput' + ]); + } + componentWillMount () { + this.reader = new FileReader(); + this.reader.onload = this.onload; + this.resetFileInput(); + } + componentDidUpdate (prevProps) { + if (this.props.isLoadingUpload && !prevProps.isLoadingUpload) { + if (this.fileToUpload && this.reader) { + this.reader.readAsArrayBuffer(this.fileToUpload); + } else { + this.props.cancelFileUpload(this.props.loadingState); + } + } + } + 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.resetFileInput(); + } + 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.onUpdateProjectTitle(uploadedProjectTitle); + } + this.resetFileInput(); + }) + .catch(error => { + log.warn(error); + 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(); + }); + } + } + handleStartSelectingFileUpload () { + // open filesystem browsing window + this.fileInput.click(); + } + setFileInput (input) { + this.fileInput = input; + } + render () { + const fileInput = ( + <input + accept=".sb,.sb2,.sb3" + ref={this.setFileInput} + style={{display: 'none'}} + type="file" + onChange={this.handleChange} + /> + ); + const { + /* eslint-disable no-unused-vars */ + 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} + /> + {fileInput} + </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()), + onLoadingFinished: (loadingState, success) => { + dispatch(onLoadedProject(loadingState, ownProps.canSave, success)); + dispatch(closeLoadingProject()); + dispatch(closeFileMenu()); + }, + onLoadingStarted: () => dispatch(openLoadingProject()), + 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/src/reducers/project-state.js b/src/reducers/project-state.js index 91e51bbe48e6b8fa1c0cb1b254cbce7350f4a246..a26d7c97184cf4b93f2f1b3c7c4d50a56353454a 100644 --- a/src/reducers/project-state.js +++ b/src/reducers/project-state.js @@ -17,7 +17,7 @@ const START_AUTO_UPDATING = 'scratch-gui/project-state/START_AUTO_UPDATING'; const START_CREATING_NEW = 'scratch-gui/project-state/START_CREATING_NEW'; const START_ERROR = 'scratch-gui/project-state/START_ERROR'; const START_FETCHING_NEW = 'scratch-gui/project-state/START_FETCHING_NEW'; -const START_LOADING_VM_FILE_UPLOAD = 'scratch-gui/project-state/START_LOADING_FILE_UPLOAD'; +const START_LOADING_VM_FILE_UPLOAD = 'scratch-gui/project-state/START_LOADING_VM_FILE_UPLOAD'; const START_MANUAL_UPDATING = 'scratch-gui/project-state/START_MANUAL_UPDATING'; const START_REMIXING = 'scratch-gui/project-state/START_REMIXING'; const START_UPDATING_BEFORE_CREATING_COPY = 'scratch-gui/project-state/START_UPDATING_BEFORE_CREATING_COPY'; @@ -435,10 +435,21 @@ const onLoadedProject = (loadingState, canSave, success) => { default: return; } + } else { + switch (loadingState) { + case LoadingState.LOADING_VM_WITH_ID: + case LoadingState.LOADING_VM_FILE_UPLOAD: + return { + type: RETURN_TO_SHOWING + }; + case LoadingState.LOADING_VM_NEW_DEFAULT: + return { + type: START_ERROR + }; + default: + return; + } } - return { - type: RETURN_TO_SHOWING - }; }; const doneUpdatingProject = loadingState => { 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/reducers/project-state-reducer.test.js b/test/unit/reducers/project-state-reducer.test.js index 2683c5beec159d4a08a86b003f15905071b82648..6938085a3456da6c8c74d5646d32e6976f5951f9 100644 --- a/test/unit/reducers/project-state-reducer.test.js +++ b/test/unit/reducers/project-state-reducer.test.js @@ -92,16 +92,49 @@ test('onFetchedProjectData new loads project data into vm', () => { expect(resultState.projectData).toBe('1010101'); }); -test('onLoadedProject upload, with canSave false, shows without id', () => { +// onLoadedProject: LOADING_VM_WITH_ID + +test('onLoadedProject (LOADING_VM_WITH_ID, true, true) shows with id', () => { const initialState = { - loadingState: LoadingState.LOADING_VM_FILE_UPLOAD + loadingState: LoadingState.LOADING_VM_WITH_ID }; - const action = onLoadedProject(initialState.loadingState, false, true); + const action = onLoadedProject(initialState.loadingState, true, true); + const resultState = projectStateReducer(initialState, action); + expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID); +}); + +test('onLoadedProject (LOADING_VM_WITH_ID, false, true) shows with id', () => { + const initialState = { + loadingState: LoadingState.LOADING_VM_WITH_ID + }; + const action = onLoadedProject(initialState.loadingState, true, true); + const resultState = projectStateReducer(initialState, action); + expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID); +}); + +test('onLoadedProject (LOADING_VM_WITH_ID, false, false), with project id, shows with id', () => { + const initialState = { + loadingState: LoadingState.LOADING_VM_WITH_ID, + projectId: '12345' + }; + const action = onLoadedProject(initialState.loadingState, false, false); + const resultState = projectStateReducer(initialState, action); + expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID); +}); + +test('onLoadedProject (LOADING_VM_WITH_ID, false, false), with no project id, shows without id', () => { + const initialState = { + loadingState: LoadingState.LOADING_VM_WITH_ID, + projectId: null + }; + const action = onLoadedProject(initialState.loadingState, false, false); const resultState = projectStateReducer(initialState, action); expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITHOUT_ID); }); -test('onLoadedProject upload, with canSave true, prepares to save', () => { +// onLoadedProject: LOADING_VM_FILE_UPLOAD + +test('onLoadedProject(LOADING_VM_FILE_UPLOAD, true, true) prepares to save', () => { const initialState = { loadingState: LoadingState.LOADING_VM_FILE_UPLOAD }; @@ -110,25 +143,38 @@ test('onLoadedProject upload, with canSave true, prepares to save', () => { expect(resultState.loadingState).toBe(LoadingState.AUTO_UPDATING); }); -test('onLoadedProject with id shows with id', () => { +test('onLoadedProject (LOADING_VM_FILE_UPLOAD, false, true) shows without id', () => { const initialState = { - loadingState: LoadingState.LOADING_VM_WITH_ID + loadingState: LoadingState.LOADING_VM_FILE_UPLOAD }; - const action = onLoadedProject(initialState.loadingState, true, true); + const action = onLoadedProject(initialState.loadingState, false, true); + const resultState = projectStateReducer(initialState, action); + expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITHOUT_ID); +}); + +test('onLoadedProject (LOADING_VM_FILE_UPLOAD, false, false), with project id, shows with id', () => { + const initialState = { + loadingState: LoadingState.LOADING_VM_FILE_UPLOAD, + projectId: '12345' + }; + const action = onLoadedProject(initialState.loadingState, false, false); const resultState = projectStateReducer(initialState, action); expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID); }); -test('onLoadedProject new shows without id', () => { +test('onLoadedProject (LOADING_VM_FILE_UPLOAD, false, false), with no project id, shows without id', () => { const initialState = { - loadingState: LoadingState.LOADING_VM_NEW_DEFAULT + loadingState: LoadingState.LOADING_VM_FILE_UPLOAD, + projectId: null }; - const action = onLoadedProject(initialState.loadingState, false, true); + const action = onLoadedProject(initialState.loadingState, false, false); const resultState = projectStateReducer(initialState, action); expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITHOUT_ID); }); -test('onLoadedProject new, to save shows without id', () => { +// onLoadedProject: LOADING_VM_NEW_DEFAULT + +test('onLoadedProject (LOADING_VM_NEW_DEFAULT, true, true) shows without id', () => { const initialState = { loadingState: LoadingState.LOADING_VM_NEW_DEFAULT }; @@ -137,26 +183,26 @@ test('onLoadedProject new, to save shows without id', () => { expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITHOUT_ID); }); -test('onLoadedProject with success false, no project id, shows without id', () => { +test('onLoadedProject (LOADING_VM_NEW_DEFAULT, false, true) shows without id', () => { const initialState = { - loadingState: LoadingState.LOADING_VM_WITH_ID, - projectId: null + loadingState: LoadingState.LOADING_VM_NEW_DEFAULT }; - const action = onLoadedProject(initialState.loadingState, false, false); + const action = onLoadedProject(initialState.loadingState, false, true); const resultState = projectStateReducer(initialState, action); expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITHOUT_ID); }); -test('onLoadedProject with success false, valid project id, shows with id', () => { +test('onLoadedProject (LOADING_VM_NEW_DEFAULT, false, false) shows error', () => { const initialState = { - loadingState: LoadingState.LOADING_VM_WITH_ID, - projectId: '12345' + loadingState: LoadingState.LOADING_VM_NEW_DEFAULT }; const action = onLoadedProject(initialState.loadingState, false, false); const resultState = projectStateReducer(initialState, action); - expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID); + expect(resultState.loadingState).toBe(LoadingState.ERROR); }); +// doneUpdatingProject + test('doneUpdatingProject with id shows with id', () => { const initialState = { loadingState: LoadingState.MANUAL_UPDATING diff --git a/test/unit/util/project-saver-hoc.test.jsx b/test/unit/util/project-saver-hoc.test.jsx index 0ec3190bf773fdf74aa6cc39c671b27e0df32beb..f5ac7b680e669c758c6af021643be10fc4d35692 100644 --- a/test/unit/util/project-saver-hoc.test.jsx +++ b/test/unit/util/project-saver-hoc.test.jsx @@ -56,7 +56,7 @@ describe('projectSaverHOC', () => { expect(mockedUpdateProject).toHaveBeenCalled(); }); - test('if canSave is alreatdy true and we show a project with an id, project will NOT be saved', () => { + test('if canSave is already true and we show a project with an id, project will NOT be saved', () => { const mockedSaveProject = jest.fn(); const Component = () => <div />; const WrappedComponent = projectSaverHOC(Component); 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(); + }); +});