diff --git a/js/src/modals/AddContract/addContract.js b/js/src/modals/AddContract/addContract.js index c5e7aac6170..49d3a41434c 100644 --- a/js/src/modals/AddContract/addContract.js +++ b/js/src/modals/AddContract/addContract.js @@ -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); diff --git a/js/src/modals/AddContract/addContract.spec.js b/js/src/modals/AddContract/addContract.spec.js index d65e06f6785..3e0bdf527c7 100644 --- a/js/src/modals/AddContract/addContract.spec.js +++ b/js/src/modals/AddContract/addContract.spec.js @@ -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( , - { - 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(); + }); + }); + }); }); diff --git a/js/src/modals/AddContract/addContract.test.js b/js/src/modals/AddContract/addContract.test.js index 5dc2af29308..250d9895c01 100644 --- a/js/src/modals/AddContract/addContract.test.js +++ b/js/src/modals/AddContract/addContract.test.js @@ -31,8 +31,19 @@ function createApi () { }; } +function createRedux () { + return { + dispatch: sinon.stub(), + subscribe: sinon.stub(), + getState: () => { + return {}; + } + }; +} + export { ABI, CONTRACTS, - createApi + createApi, + createRedux }; diff --git a/js/src/modals/EditMeta/editMeta.js b/js/src/modals/EditMeta/editMeta.js index 8b5e14939c8..a232492ea7e 100644 --- a/js/src/modals/EditMeta/editMeta.js +++ b/js/src/modals/EditMeta/editMeta.js @@ -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); diff --git a/js/src/modals/EditMeta/editMeta.spec.js b/js/src/modals/EditMeta/editMeta.spec.js index 917f3207637..e6dac1f23b0 100644 --- a/js/src/modals/EditMeta/editMeta.spec.js +++ b/js/src/modals/EditMeta/editMeta.spec.js @@ -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( , - { - 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(); + }); + }); }); }); }); diff --git a/js/src/modals/EditMeta/editMeta.test.js b/js/src/modals/EditMeta/editMeta.test.js index 98bd61fea1c..369debe1f9a 100644 --- a/js/src/modals/EditMeta/editMeta.test.js +++ b/js/src/modals/EditMeta/editMeta.test.js @@ -50,8 +50,19 @@ function createApi () { }; } +function createRedux () { + return { + dispatch: sinon.stub(), + subscribe: sinon.stub(), + getState: () => { + return {}; + } + }; +} + export { ACCOUNT, ADDRESS, - createApi + createApi, + createRedux }; diff --git a/js/src/modals/EditMeta/store.js b/js/src/modals/EditMeta/store.js index b2d71c63a45..58c5998defd 100644 --- a/js/src/modals/EditMeta/store.js +++ b/js/src/modals/EditMeta/store.js @@ -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; }); } } diff --git a/js/src/modals/PasswordManager/passwordManager.js b/js/src/modals/PasswordManager/passwordManager.js index 0f7a08f279d..9bd6c08df0e 100644 --- a/js/src/modals/PasswordManager/passwordManager.js +++ b/js/src/modals/PasswordManager/passwordManager.js @@ -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(
{ - 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); diff --git a/js/src/modals/PasswordManager/passwordManager.spec.js b/js/src/modals/PasswordManager/passwordManager.spec.js index 744d0ffa37a..bdb8828dd14 100644 --- a/js/src/modals/PasswordManager/passwordManager.spec.js +++ b/js/src/modals/PasswordManager/passwordManager.spec.js @@ -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( , - { - 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(); }); }); }); diff --git a/js/src/modals/PasswordManager/passwordManager.test.js b/js/src/modals/PasswordManager/passwordManager.test.js index 9a8f883ecf8..409756fde06 100644 --- a/js/src/modals/PasswordManager/passwordManager.test.js +++ b/js/src/modals/PasswordManager/passwordManager.test.js @@ -37,7 +37,18 @@ function createApi (result = true) { }; } +function createRedux () { + return { + dispatch: sinon.stub(), + subscribe: sinon.stub(), + getState: () => { + return {}; + } + }; +} + export { ACCOUNT, - createApi + createApi, + createRedux }; diff --git a/js/src/redux/actions.js b/js/src/redux/actions.js index 0618adfd3bb..773ade5fcbe 100644 --- a/js/src/redux/actions.js +++ b/js/src/redux/actions.js @@ -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, diff --git a/js/src/redux/providers/snackbarActions.js b/js/src/redux/providers/snackbarActions.js index 0048854da3d..98dcf554f2b 100644 --- a/js/src/redux/providers/snackbarActions.js +++ b/js/src/redux/providers/snackbarActions.js @@ -20,7 +20,7 @@ export function showSnackbar (message, cooldown) { }; } -function openSnackbar (message, cooldown) { +export function openSnackbar (message, cooldown) { return { type: 'openSnackbar', message, cooldown