diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index 08a74dfca382cd6e3c963d257b075734ed20cd72..0ef0801a4f377d87b00979ac223630b8b8ea094b 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -368,6 +368,7 @@ GUIComponent.defaultProps = { canSave: false, canSaveAsCopy: false, canShare: false, + onUpdateProjectTitle: () => {}, stageSizeMode: STAGE_SIZE_MODES.large }; diff --git a/src/components/menu-bar/menu-bar.jsx b/src/components/menu-bar/menu-bar.jsx index e33868b96bb0ad76fbefdd993badcd603c7fe64b..f046710e0f17ad4f24304e613373b6f40a4da604 100644 --- a/src/components/menu-bar/menu-bar.jsx +++ b/src/components/menu-bar/menu-bar.jsx @@ -333,7 +333,7 @@ class MenuBar extends React.Component { )} </MenuSection> <MenuSection> - <SBFileUploader> + <SBFileUploader onUpdateProjectTitle={this.props.onUpdateProjectTitle}> {(renderFileInput, loadProject) => ( <MenuItem onClick={loadProject} diff --git a/src/containers/sb-file-uploader.jsx b/src/containers/sb-file-uploader.jsx index a5d8c99a70078a94be55d7570087a73c80bfe636..46be8172465a65d2542d9f2c957eccad7e10f4d7 100644 --- a/src/containers/sb-file-uploader.jsx +++ b/src/containers/sb-file-uploader.jsx @@ -6,7 +6,6 @@ import {defineMessages, injectIntl, intlShape} from 'react-intl'; import analytics from '../lib/analytics'; import log from '../lib/log'; -import {setProjectTitle} from '../reducers/project-title'; import {LoadingStates, onLoadedProject, onProjectUploadStarted} from '../reducers/project-state'; import { @@ -42,12 +41,20 @@ class SBFileUploader extends React.Component { constructor (props) { super(props); bindAll(this, [ + 'getProjectTitleFromFilename', 'renderFileInput', 'setFileInput', 'handleChange', 'handleClick' ]); } + getProjectTitleFromFilename (fileInputFilename) { + if (!fileInputFilename) return ''; + // only parse title from files like "filename.sb2" or "filename.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) { // Remove the hash if any (without triggering a hash change event or a reload) @@ -77,14 +84,8 @@ class SBFileUploader extends React.Component { if (thisFileInput.files) { // Don't attempt to load if no file was selected this.props.onLoadingStarted(); reader.readAsArrayBuffer(thisFileInput.files[0]); - // extract the title from the file and set it as current project title - if (thisFileInput.files[0].name) { - const matches = thisFileInput.files[0].name.match(/^(.*)\.sb3$/); - if (matches) { - const truncatedProjectTitle = matches[1].substring(0, 100); - this.props.onSetProjectTitle(truncatedProjectTitle); - } - } + const uploadedProjectTitle = this.getProjectTitleFromFilename(thisFileInput.files[0].name); + this.props.onUpdateProjectTitle(uploadedProjectTitle); } } handleClick () { @@ -116,7 +117,7 @@ SBFileUploader.propTypes = { loadingState: PropTypes.oneOf(LoadingStates), onLoadingFinished: PropTypes.func, onLoadingStarted: PropTypes.func, - onSetProjectTitle: PropTypes.func, + onUpdateProjectTitle: PropTypes.func, vm: PropTypes.shape({ loadProject: PropTypes.func }) @@ -131,14 +132,19 @@ const mapDispatchToProps = dispatch => ({ dispatch(onLoadedProject(loadingState)); dispatch(closeLoadingProject()); }, - onSetProjectTitle: title => dispatch(setProjectTitle(title)), onLoadingStarted: () => { dispatch(openLoadingProject()); dispatch(onProjectUploadStarted()); } }); +// 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 + mapDispatchToProps, + mergeProps )(injectIntl(SBFileUploader)); 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..856b8f058f88412db5c2371c9f734581c5eacd1f --- /dev/null +++ b/test/unit/containers/sb-file-uploader.test.jsx @@ -0,0 +1,87 @@ +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 onUpdateProjectTitle; + 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} + onUpdateProjectTitle={onUpdateProjectTitle} + > + {(renderFileInput, loadProject) => ( + <div + onClick={loadProject} + /> + )} + </SBFileUploader> + ); + }; + + beforeEach(() => { + store = mockStore({ + scratchGui: { + projectState: { + loadingState: LoadingState.SHOWING_WITH_ID + }, + vm: {} + } + }); + onUpdateProjectTitle = jest.fn(); + 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('sets blank 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(''); + }); + + 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(''); + }); +});