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 React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { newError } from '~/redux/actions';
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';
@observer
export default class AddContract extends Component {
class AddContract extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
}
static propTypes = {
contracts: PropTypes.object.isRequired,
newError: PropTypes.func.isRequired,
onClose: PropTypes.func
};
@ -244,7 +247,7 @@ export default class AddContract extends Component {
this.onClose();
})
.catch((error) => {
newError(error);
this.props.newError(error);
});
}
@ -252,3 +255,14 @@ export default class AddContract extends Component {
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 { CONTRACTS, createApi } from './addContract.test.js';
import { CONTRACTS, createApi, createRedux } from './addContract.test.js';
let api;
let component;
let instance;
let onClose;
let reduxStore;
function renderShallow (props) {
function render (props = {}) {
api = createApi();
onClose = sinon.stub();
reduxStore = createRedux();
component = shallow(
<AddContract
{ ...props }
contracts={ CONTRACTS }
onClose={ onClose } />,
{
context: {
api: createApi()
}
}
);
{ context: { store: reduxStore } }
).find('AddContract').shallow({ context: { api } });
instance = component.instance();
return component;
}
@ -45,11 +48,37 @@ function renderShallow (props) {
describe('modals/AddContract', () => {
describe('rendering', () => {
beforeEach(() => {
renderShallow();
render();
});
it('renders the defauls', () => {
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 {
ABI,
CONTRACTS,
createApi
createApi,
createRedux
};

View File

@ -17,20 +17,24 @@
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
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 { CancelIcon, SaveIcon } from '~/ui/Icons';
import Store from './store';
@observer
export default class EditMeta extends Component {
class EditMeta extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
}
static propTypes = {
account: PropTypes.object.isRequired,
newError: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired
}
@ -138,6 +142,20 @@ export default class EditMeta extends Component {
return this.store
.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 { ACCOUNT, createApi } from './editMeta.test.js';
import { ACCOUNT, createApi, createRedux } from './editMeta.test.js';
let api;
let component;
let instance;
let onClose;
let reduxStore;
function render (props) {
api = createApi();
onClose = sinon.stub();
reduxStore = createRedux();
component = shallow(
<EditMeta
{ ...props }
account={ ACCOUNT }
onClose={ onClose } />,
{
context: {
api: createApi()
}
}
);
{ context: { store: reduxStore } }
).find('EditMeta').shallow({ context: { api } });
instance = component.instance();
return component;
}
@ -56,15 +58,29 @@ describe('modals/EditMeta', () => {
});
describe('onSave', () => {
it('calls store.save() & props.onClose', () => {
const instance = component.instance();
it('calls store.save', () => {
sinon.spy(instance.store, 'save');
instance.onSave().then(() => {
return instance.onSave().then(() => {
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;
});
});
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 {
ACCOUNT,
ADDRESS,
createApi
createApi,
createRedux
};

View File

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

View File

@ -19,8 +19,10 @@ import { Tabs, Tab } from 'material-ui/Tabs';
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
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 Form, { Input } from '~/ui/Form';
import { CancelIcon, CheckIcon, SendIcon } from '~/ui/Icons';
@ -42,13 +44,15 @@ const TABS_ITEM_STYLE = {
};
@observer
export default class PasswordManager extends Component {
class PasswordManager extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
}
static propTypes = {
account: PropTypes.object.isRequired,
openSnackbar: PropTypes.func.isRequired,
newError: PropTypes.func.isRequired,
onClose: PropTypes.func
}
@ -336,7 +340,7 @@ export default class PasswordManager extends Component {
.changePassword()
.then((result) => {
if (result) {
showSnackbar(
this.props.openSnackbar(
<div>
<FormattedMessage
id='passwordChange.success'
@ -347,7 +351,7 @@ export default class PasswordManager extends Component {
}
})
.catch((error) => {
newError(error);
this.props.newError(error);
});
}
@ -355,7 +359,19 @@ export default class PasswordManager extends Component {
return this.store
.testPassword()
.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 { ACCOUNT, createApi } from './passwordManager.test.js';
import { ACCOUNT, createApi, createRedux } from './passwordManager.test.js';
let component;
let instance;
let onClose;
let reduxStore;
function render (props) {
onClose = sinon.stub();
reduxStore = createRedux();
component = shallow(
<PasswordManager
{ ...props }
account={ ACCOUNT }
onClose={ onClose } />,
{
context: {
api: createApi()
}
}
);
{ context: { store: reduxStore } }
).find('PasswordManager').shallow({ context: { api: createApi() } });
instance = component.instance();
return component;
}
@ -56,24 +56,53 @@ describe('modals/PasswordManager', () => {
});
describe('changePassword', () => {
it('calls store.changePassword & props.onClose', () => {
const instance = component.instance();
it('calls store.changePassword', () => {
sinon.spy(instance.store, 'changePassword');
instance.changePassword().then(() => {
return instance.changePassword().then(() => {
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;
});
});
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', () => {
it('calls store.testPassword', () => {
const instance = component.instance();
sinon.spy(instance.store, 'testPassword');
instance.testPassword().then(() => {
return instance.testPassword().then(() => {
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 {
ACCOUNT,
createApi
createApi,
createRedux
};

View File

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

View File

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