Fix newError noops when not bound to dispacher (#4013)

* AddContract properly binds newError

* EditMeta properly binds newError

* PasswordManager properly binds newError

* pass null instead of empty mapStateToProps

* Add openSnackbar test & binded prop
This commit is contained in:
Jaco Greeff 2017-01-03 17:41:21 +01:00 committed by GitHub
parent 9db3f383e3
commit 04ed53e0f2
12 changed files with 202 additions and 48 deletions

View File

@ -17,6 +17,8 @@
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { newError } from '~/redux/actions'; import { newError } from '~/redux/actions';
import { Button, Modal, Form, Input, InputAddress, RadioButtons } from '~/ui'; import { Button, Modal, Form, Input, InputAddress, RadioButtons } from '~/ui';
@ -25,13 +27,14 @@ import { AddIcon, CancelIcon, NextIcon, PrevIcon } from '~/ui/Icons';
import Store from './store'; import Store from './store';
@observer @observer
export default class AddContract extends Component { class AddContract extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired api: PropTypes.object.isRequired
} }
static propTypes = { static propTypes = {
contracts: PropTypes.object.isRequired, contracts: PropTypes.object.isRequired,
newError: PropTypes.func.isRequired,
onClose: PropTypes.func onClose: PropTypes.func
}; };
@ -244,7 +247,7 @@ export default class AddContract extends Component {
this.onClose(); this.onClose();
}) })
.catch((error) => { .catch((error) => {
newError(error); this.props.newError(error);
}); });
} }
@ -252,3 +255,14 @@ export default class AddContract extends Component {
this.props.onClose(); this.props.onClose();
} }
} }
function mapDispatchToProps (dispatch) {
return bindActionCreators({
newError
}, dispatch);
}
export default connect(
null,
mapDispatchToProps
)(AddContract);

View File

@ -20,24 +20,27 @@ import sinon from 'sinon';
import AddContract from './'; import AddContract from './';
import { CONTRACTS, createApi } from './addContract.test.js'; import { CONTRACTS, createApi, createRedux } from './addContract.test.js';
let api;
let component; let component;
let instance;
let onClose; let onClose;
let reduxStore;
function renderShallow (props) { function render (props = {}) {
api = createApi();
onClose = sinon.stub(); onClose = sinon.stub();
reduxStore = createRedux();
component = shallow( component = shallow(
<AddContract <AddContract
{ ...props } { ...props }
contracts={ CONTRACTS } contracts={ CONTRACTS }
onClose={ onClose } />, onClose={ onClose } />,
{ { context: { store: reduxStore } }
context: { ).find('AddContract').shallow({ context: { api } });
api: createApi() instance = component.instance();
}
}
);
return component; return component;
} }
@ -45,11 +48,37 @@ function renderShallow (props) {
describe('modals/AddContract', () => { describe('modals/AddContract', () => {
describe('rendering', () => { describe('rendering', () => {
beforeEach(() => { beforeEach(() => {
renderShallow(); render();
}); });
it('renders the defauls', () => { it('renders the defauls', () => {
expect(component).to.be.ok; expect(component).to.be.ok;
}); });
}); });
describe('onAdd', () => {
it('calls store addContract', () => {
sinon.stub(instance.store, 'addContract').resolves(true);
return instance.onAdd().then(() => {
expect(instance.store.addContract).to.have.been.called;
instance.store.addContract.restore();
});
});
it('calls closes dialog on success', () => {
sinon.stub(instance.store, 'addContract').resolves(true);
return instance.onAdd().then(() => {
expect(onClose).to.have.been.called;
instance.store.addContract.restore();
});
});
it('adds newError on failure', () => {
sinon.stub(instance.store, 'addContract').rejects('test');
return instance.onAdd().then(() => {
expect(reduxStore.dispatch).to.have.been.calledWith({ error: new Error('test'), type: 'newError' });
instance.store.addContract.restore();
});
});
});
}); });

View File

@ -31,8 +31,19 @@ function createApi () {
}; };
} }
function createRedux () {
return {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {};
}
};
}
export { export {
ABI, ABI,
CONTRACTS, CONTRACTS,
createApi createApi,
createRedux
}; };

View File

@ -17,20 +17,24 @@
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { newError } from '~/redux/actions';
import { Button, Form, Input, InputChip, Modal } from '~/ui'; import { Button, Form, Input, InputChip, Modal } from '~/ui';
import { CancelIcon, SaveIcon } from '~/ui/Icons'; import { CancelIcon, SaveIcon } from '~/ui/Icons';
import Store from './store'; import Store from './store';
@observer @observer
export default class EditMeta extends Component { class EditMeta extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired api: PropTypes.object.isRequired
} }
static propTypes = { static propTypes = {
account: PropTypes.object.isRequired, account: PropTypes.object.isRequired,
newError: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired onClose: PropTypes.func.isRequired
} }
@ -138,6 +142,20 @@ export default class EditMeta extends Component {
return this.store return this.store
.save() .save()
.then(() => this.props.onClose()); .then(() => this.props.onClose())
.catch((error) => {
this.props.newError(error);
});
} }
} }
function mapDispatchToProps (dispatch) {
return bindActionCreators({
newError
}, dispatch);
}
export default connect(
null,
mapDispatchToProps
)(EditMeta);

View File

@ -20,25 +20,27 @@ import sinon from 'sinon';
import EditMeta from './'; import EditMeta from './';
import { ACCOUNT, createApi } from './editMeta.test.js'; import { ACCOUNT, createApi, createRedux } from './editMeta.test.js';
let api;
let component; let component;
let instance;
let onClose; let onClose;
let reduxStore;
function render (props) { function render (props) {
api = createApi();
onClose = sinon.stub(); onClose = sinon.stub();
reduxStore = createRedux();
component = shallow( component = shallow(
<EditMeta <EditMeta
{ ...props } { ...props }
account={ ACCOUNT } account={ ACCOUNT }
onClose={ onClose } />, onClose={ onClose } />,
{ { context: { store: reduxStore } }
context: { ).find('EditMeta').shallow({ context: { api } });
api: createApi() instance = component.instance();
}
}
);
return component; return component;
} }
@ -56,15 +58,29 @@ describe('modals/EditMeta', () => {
}); });
describe('onSave', () => { describe('onSave', () => {
it('calls store.save() & props.onClose', () => { it('calls store.save', () => {
const instance = component.instance();
sinon.spy(instance.store, 'save'); sinon.spy(instance.store, 'save');
instance.onSave().then(() => { return instance.onSave().then(() => {
expect(instance.store.save).to.have.been.called; expect(instance.store.save).to.have.been.called;
instance.store.save.restore();
});
});
it('closes the dialog on success', () => {
return instance.onSave().then(() => {
expect(onClose).to.have.been.called; expect(onClose).to.have.been.called;
}); });
}); });
it('adds newError on failure', () => {
sinon.stub(instance.store, 'save').rejects('test');
return instance.onSave().then(() => {
expect(reduxStore.dispatch).to.have.been.calledWith({ error: new Error('test'), type: 'newError' });
instance.store.save.restore();
});
});
}); });
}); });
}); });

View File

@ -50,8 +50,19 @@ function createApi () {
}; };
} }
function createRedux () {
return {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {};
}
};
}
export { export {
ACCOUNT, ACCOUNT,
ADDRESS, ADDRESS,
createApi createApi,
createRedux
}; };

View File

@ -16,7 +16,6 @@
import { action, computed, observable, transaction } from 'mobx'; import { action, computed, observable, transaction } from 'mobx';
import { newError } from '~/redux/actions';
import { validateName } from '~/util/validation'; import { validateName } from '~/util/validation';
export default class Store { export default class Store {
@ -92,8 +91,7 @@ export default class Store {
]) ])
.catch((error) => { .catch((error) => {
console.error('onSave', error); console.error('onSave', error);
throw error;
newError(error);
}); });
} }
} }

View File

@ -19,8 +19,10 @@ import { Tabs, Tab } from 'material-ui/Tabs';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { newError, showSnackbar } from '~/redux/actions'; import { newError, openSnackbar } from '~/redux/actions';
import { Button, Modal, IdentityName, IdentityIcon } from '~/ui'; import { Button, Modal, IdentityName, IdentityIcon } from '~/ui';
import Form, { Input } from '~/ui/Form'; import Form, { Input } from '~/ui/Form';
import { CancelIcon, CheckIcon, SendIcon } from '~/ui/Icons'; import { CancelIcon, CheckIcon, SendIcon } from '~/ui/Icons';
@ -42,13 +44,15 @@ const TABS_ITEM_STYLE = {
}; };
@observer @observer
export default class PasswordManager extends Component { class PasswordManager extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired api: PropTypes.object.isRequired
} }
static propTypes = { static propTypes = {
account: PropTypes.object.isRequired, account: PropTypes.object.isRequired,
openSnackbar: PropTypes.func.isRequired,
newError: PropTypes.func.isRequired,
onClose: PropTypes.func onClose: PropTypes.func
} }
@ -336,7 +340,7 @@ export default class PasswordManager extends Component {
.changePassword() .changePassword()
.then((result) => { .then((result) => {
if (result) { if (result) {
showSnackbar( this.props.openSnackbar(
<div> <div>
<FormattedMessage <FormattedMessage
id='passwordChange.success' id='passwordChange.success'
@ -347,7 +351,7 @@ export default class PasswordManager extends Component {
} }
}) })
.catch((error) => { .catch((error) => {
newError(error); this.props.newError(error);
}); });
} }
@ -355,7 +359,19 @@ export default class PasswordManager extends Component {
return this.store return this.store
.testPassword() .testPassword()
.catch((error) => { .catch((error) => {
newError(error); this.props.newError(error);
}); });
} }
} }
function mapDispatchToProps (dispatch) {
return bindActionCreators({
openSnackbar,
newError
}, dispatch);
}
export default connect(
null,
mapDispatchToProps
)(PasswordManager);

View File

@ -20,25 +20,25 @@ import sinon from 'sinon';
import PasswordManager from './'; import PasswordManager from './';
import { ACCOUNT, createApi } from './passwordManager.test.js'; import { ACCOUNT, createApi, createRedux } from './passwordManager.test.js';
let component; let component;
let instance;
let onClose; let onClose;
let reduxStore;
function render (props) { function render (props) {
onClose = sinon.stub(); onClose = sinon.stub();
reduxStore = createRedux();
component = shallow( component = shallow(
<PasswordManager <PasswordManager
{ ...props } { ...props }
account={ ACCOUNT } account={ ACCOUNT }
onClose={ onClose } />, onClose={ onClose } />,
{ { context: { store: reduxStore } }
context: { ).find('PasswordManager').shallow({ context: { api: createApi() } });
api: createApi() instance = component.instance();
}
}
);
return component; return component;
} }
@ -56,24 +56,53 @@ describe('modals/PasswordManager', () => {
}); });
describe('changePassword', () => { describe('changePassword', () => {
it('calls store.changePassword & props.onClose', () => { it('calls store.changePassword', () => {
const instance = component.instance();
sinon.spy(instance.store, 'changePassword'); sinon.spy(instance.store, 'changePassword');
instance.changePassword().then(() => { return instance.changePassword().then(() => {
expect(instance.store.changePassword).to.have.been.called; expect(instance.store.changePassword).to.have.been.called;
instance.store.changePassword.restore();
});
});
it('closes the dialog on success', () => {
return instance.changePassword().then(() => {
expect(onClose).to.have.been.called; expect(onClose).to.have.been.called;
}); });
}); });
it('shows snackbar on success', () => {
return instance.changePassword().then(() => {
expect(reduxStore.dispatch).to.have.been.calledWithMatch({ type: 'openSnackbar' });
});
});
it('adds newError on failure', () => {
sinon.stub(instance.store, 'changePassword').rejects('test');
return instance.changePassword().then(() => {
expect(reduxStore.dispatch).to.have.been.calledWith({ error: new Error('test'), type: 'newError' });
instance.store.changePassword.restore();
});
});
}); });
describe('testPassword', () => { describe('testPassword', () => {
it('calls store.testPassword', () => { it('calls store.testPassword', () => {
const instance = component.instance();
sinon.spy(instance.store, 'testPassword'); sinon.spy(instance.store, 'testPassword');
instance.testPassword().then(() => { return instance.testPassword().then(() => {
expect(instance.store.testPassword).to.have.been.called; expect(instance.store.testPassword).to.have.been.called;
instance.store.testPassword.restore();
});
});
it('adds newError on failure', () => {
sinon.stub(instance.store, 'testPassword').rejects('test');
return instance.testPassword().then(() => {
expect(reduxStore.dispatch).to.have.been.calledWith({ error: new Error('test'), type: 'newError' });
instance.store.testPassword.restore();
}); });
}); });
}); });

View File

@ -37,7 +37,18 @@ function createApi (result = true) {
}; };
} }
function createRedux () {
return {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {};
}
};
}
export { export {
ACCOUNT, ACCOUNT,
createApi createApi,
createRedux
}; };

View File

@ -16,7 +16,7 @@
import { newError } from '~/ui/Errors/actions'; import { newError } from '~/ui/Errors/actions';
import { setAddressImage } from './providers/imagesActions'; import { setAddressImage } from './providers/imagesActions';
import { showSnackbar } from './providers/snackbarActions'; import { openSnackbar, showSnackbar } from './providers/snackbarActions';
import { clearStatusLogs, toggleStatusLogs, toggleStatusRefresh } from './providers/statusActions'; import { clearStatusLogs, toggleStatusLogs, toggleStatusRefresh } from './providers/statusActions';
import { toggleView } from '~/views/Settings/actions'; import { toggleView } from '~/views/Settings/actions';
@ -24,6 +24,7 @@ export {
newError, newError,
clearStatusLogs, clearStatusLogs,
setAddressImage, setAddressImage,
openSnackbar,
showSnackbar, showSnackbar,
toggleStatusLogs, toggleStatusLogs,
toggleStatusRefresh, toggleStatusRefresh,

View File

@ -20,7 +20,7 @@ export function showSnackbar (message, cooldown) {
}; };
} }
function openSnackbar (message, cooldown) { export function openSnackbar (message, cooldown) {
return { return {
type: 'openSnackbar', type: 'openSnackbar',
message, cooldown message, cooldown