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:
Ben Wheeler committed 2020-01-28 14:57:09 -05:00
1 parent 97885c7518
commit f2291d963c
10 files changed
+454 -364

No files matched your search

+3
View File
@@ -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,
+5 -16
View File
@@ -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,
+2
View File
@@ -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);
-226
View File
@@ -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));
+241
View File
@@ -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
};
+15 -4
View File
@@ -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
+1 -1
View File
@@ -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();
});
});