continuing sb file uploader refactor into hoc
draft of refactor to make custom confirm modal work for upload; need to further edit sb-file-uploader Add support for custom confirm modal to sb-file-uploader Show custom confirm modal when confirming project upload got custom upload refactor of file upload working, and tested failure case cleaned up debug code, make project file upload work simplified logic around canceling file upload removed old sb-file-uploader.jsx; fixed linter errors reverted two components to use old strings, not shared (can do that refactor another time) removed unnecessary confirm container removed stray comment moved, updated sb file uploader tests, related project state tests removed custom modal, to reduce scope of changes sb file uploader test uses intl
This commit is contained in:
1 parent
97885c7518
commit
f2291d963c
10 files changed
+454
-364
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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));
|
||||
@@ -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
|
||||
};
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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('');
|
||||
});
|
||||
});
|
||||
@@ -92,25 +92,9 @@ test('onFetchedProjectData new loads project data into vm', () => {
|
||||
expect(resultState.projectData).toBe('1010101');
|
||||
});
|
||||
|
||||
test('onLoadedProject upload, with canSave false, shows without id', () => {
|
||||
const initialState = {
|
||||
loadingState: LoadingState.LOADING_VM_FILE_UPLOAD
|
||||
};
|
||||
const action = onLoadedProject(initialState.loadingState, false, true);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITHOUT_ID);
|
||||
});
|
||||
// onLoadedProject: LOADING_VM_WITH_ID
|
||||
|
||||
test('onLoadedProject upload, with canSave true, prepares to save', () => {
|
||||
const initialState = {
|
||||
loadingState: LoadingState.LOADING_VM_FILE_UPLOAD
|
||||
};
|
||||
const action = onLoadedProject(initialState.loadingState, true, true);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.AUTO_UPDATING);
|
||||
});
|
||||
|
||||
test('onLoadedProject with id shows with id', () => {
|
||||
test('onLoadedProject (LOADING_VM_WITH_ID, true, true) shows with id', () => {
|
||||
const initialState = {
|
||||
loadingState: LoadingState.LOADING_VM_WITH_ID
|
||||
};
|
||||
@@ -119,25 +103,26 @@ test('onLoadedProject with id shows with id', () => {
|
||||
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID);
|
||||
});
|
||||
|
||||
test('onLoadedProject new shows without id', () => {
|
||||
test('onLoadedProject (LOADING_VM_WITH_ID, false, true) shows with id', () => {
|
||||
const initialState = {
|
||||
loadingState: LoadingState.LOADING_VM_NEW_DEFAULT
|
||||
};
|
||||
const action = onLoadedProject(initialState.loadingState, false, true);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITHOUT_ID);
|
||||
});
|
||||
|
||||
test('onLoadedProject new, to save shows without id', () => {
|
||||
const initialState = {
|
||||
loadingState: LoadingState.LOADING_VM_NEW_DEFAULT
|
||||
loadingState: LoadingState.LOADING_VM_WITH_ID
|
||||
};
|
||||
const action = onLoadedProject(initialState.loadingState, true, true);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITHOUT_ID);
|
||||
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID);
|
||||
});
|
||||
|
||||
test('onLoadedProject with success false, no project id, shows without 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
|
||||
@@ -147,9 +132,29 @@ test('onLoadedProject with success false, no project id, shows without id', () =
|
||||
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITHOUT_ID);
|
||||
});
|
||||
|
||||
test('onLoadedProject with success false, valid project id, shows with id', () => {
|
||||
// onLoadedProject: LOADING_VM_FILE_UPLOAD
|
||||
|
||||
test('onLoadedProject(LOADING_VM_FILE_UPLOAD, true, true) prepares to save', () => {
|
||||
const initialState = {
|
||||
loadingState: LoadingState.LOADING_VM_WITH_ID,
|
||||
loadingState: LoadingState.LOADING_VM_FILE_UPLOAD
|
||||
};
|
||||
const action = onLoadedProject(initialState.loadingState, true, true);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.AUTO_UPDATING);
|
||||
});
|
||||
|
||||
test('onLoadedProject (LOADING_VM_FILE_UPLOAD, false, true) shows without id', () => {
|
||||
const initialState = {
|
||||
loadingState: LoadingState.LOADING_VM_FILE_UPLOAD
|
||||
};
|
||||
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);
|
||||
@@ -157,6 +162,47 @@ test('onLoadedProject with success false, valid project id, shows with id', () =
|
||||
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITH_ID);
|
||||
});
|
||||
|
||||
test('onLoadedProject (LOADING_VM_FILE_UPLOAD, false, false), with no project id, shows without id', () => {
|
||||
const initialState = {
|
||||
loadingState: LoadingState.LOADING_VM_FILE_UPLOAD,
|
||||
projectId: null
|
||||
};
|
||||
const action = onLoadedProject(initialState.loadingState, false, false);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.SHOWING_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
|
||||
};
|
||||
const action = onLoadedProject(initialState.loadingState, true, true);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITHOUT_ID);
|
||||
});
|
||||
|
||||
test('onLoadedProject (LOADING_VM_NEW_DEFAULT, false, true) shows without id', () => {
|
||||
const initialState = {
|
||||
loadingState: LoadingState.LOADING_VM_NEW_DEFAULT
|
||||
};
|
||||
const action = onLoadedProject(initialState.loadingState, false, true);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.SHOWING_WITHOUT_ID);
|
||||
});
|
||||
|
||||
test('onLoadedProject (LOADING_VM_NEW_DEFAULT, false, false) shows error', () => {
|
||||
const initialState = {
|
||||
loadingState: LoadingState.LOADING_VM_NEW_DEFAULT
|
||||
};
|
||||
const action = onLoadedProject(initialState.loadingState, false, false);
|
||||
const resultState = projectStateReducer(initialState, action);
|
||||
expect(resultState.loadingState).toBe(LoadingState.ERROR);
|
||||
});
|
||||
|
||||
// doneUpdatingProject
|
||||
|
||||
test('doneUpdatingProject with id shows with id', () => {
|
||||
const initialState = {
|
||||
loadingState: LoadingState.MANUAL_UPDATING
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user