diff options
author | pldespaigne <pl.despaigne@gmail.com> | 2019-05-31 00:22:55 +0800 |
---|---|---|
committer | pldespaigne <pl.despaigne@gmail.com> | 2019-05-31 00:22:55 +0800 |
commit | 9a658ee53d1f75ce07c33581ac1189fa8c4fd173 (patch) | |
tree | ea92ef1971ffaa72c29bf16904906bc1841654c7 /ui/app/pages/send/send-footer | |
parent | 9b87aaae1907eb04ca0a4055b5bb2c863e56aa39 (diff) | |
parent | 681f3f67b89b64fc837df1103198b641c7e7b2d6 (diff) | |
download | tangerine-wallet-browser-9a658ee53d1f75ce07c33581ac1189fa8c4fd173.tar tangerine-wallet-browser-9a658ee53d1f75ce07c33581ac1189fa8c4fd173.tar.gz tangerine-wallet-browser-9a658ee53d1f75ce07c33581ac1189fa8c4fd173.tar.bz2 tangerine-wallet-browser-9a658ee53d1f75ce07c33581ac1189fa8c4fd173.tar.lz tangerine-wallet-browser-9a658ee53d1f75ce07c33581ac1189fa8c4fd173.tar.xz tangerine-wallet-browser-9a658ee53d1f75ce07c33581ac1189fa8c4fd173.tar.zst tangerine-wallet-browser-9a658ee53d1f75ce07c33581ac1189fa8c4fd173.zip |
merge
Diffstat (limited to 'ui/app/pages/send/send-footer')
11 files changed, 1057 insertions, 0 deletions
diff --git a/ui/app/pages/send/send-footer/README.md b/ui/app/pages/send/send-footer/README.md new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/pages/send/send-footer/README.md diff --git a/ui/app/pages/send/send-footer/index.js b/ui/app/pages/send/send-footer/index.js new file mode 100644 index 000000000..58e91d622 --- /dev/null +++ b/ui/app/pages/send/send-footer/index.js @@ -0,0 +1 @@ +export { default } from './send-footer.container' diff --git a/ui/app/pages/send/send-footer/send-footer.component.js b/ui/app/pages/send/send-footer/send-footer.component.js new file mode 100644 index 000000000..16a8fdde2 --- /dev/null +++ b/ui/app/pages/send/send-footer/send-footer.component.js @@ -0,0 +1,142 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import PageContainerFooter from '../../../components/ui/page-container/page-container-footer' +import { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } from '../../../helpers/constants/routes' + +export default class SendFooter extends Component { + + static propTypes = { + addToAddressBookIfNew: PropTypes.func, + amount: PropTypes.string, + data: PropTypes.string, + clearSend: PropTypes.func, + disabled: PropTypes.bool, + editingTransactionId: PropTypes.string, + errors: PropTypes.object, + from: PropTypes.object, + gasLimit: PropTypes.string, + gasPrice: PropTypes.string, + gasTotal: PropTypes.string, + history: PropTypes.object, + inError: PropTypes.bool, + selectedToken: PropTypes.object, + sign: PropTypes.func, + to: PropTypes.string, + toAccounts: PropTypes.array, + tokenBalance: PropTypes.string, + unapprovedTxs: PropTypes.object, + update: PropTypes.func, + sendErrors: PropTypes.object, + gasChangedLabel: PropTypes.string, + } + + static contextTypes = { + t: PropTypes.func, + metricsEvent: PropTypes.func, + }; + + onCancel () { + this.props.clearSend() + this.props.history.push(DEFAULT_ROUTE) + } + + onSubmit (event) { + event.preventDefault() + const { + addToAddressBookIfNew, + amount, + data, + editingTransactionId, + from: {address: from}, + gasLimit: gas, + gasPrice, + selectedToken, + sign, + to, + unapprovedTxs, + // updateTx, + update, + toAccounts, + history, + gasChangedLabel, + } = this.props + const { metricsEvent } = this.context + + // Should not be needed because submit should be disabled if there are errors. + // const noErrors = !amountError && toError === null + + // if (!noErrors) { + // return + // } + + // TODO: add nickname functionality + addToAddressBookIfNew(to, toAccounts) + const promise = editingTransactionId + ? update({ + amount, + data, + editingTransactionId, + from, + gas, + gasPrice, + selectedToken, + to, + unapprovedTxs, + }) + : sign({ data, selectedToken, to, amount, from, gas, gasPrice }) + + Promise.resolve(promise) + .then(() => { + metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Edit Screen', + name: 'Complete', + }, + customVariables: { + gasChanged: gasChangedLabel, + }, + }) + history.push(CONFIRM_TRANSACTION_ROUTE) + }) + } + + formShouldBeDisabled () { + const { data, inError, selectedToken, tokenBalance, gasTotal, to } = this.props + const missingTokenBalance = selectedToken && !tokenBalance + const shouldBeDisabled = inError || !gasTotal || missingTokenBalance || !(data || to) + return shouldBeDisabled + } + + componentDidUpdate (prevProps) { + const { inError, sendErrors } = this.props + const { metricsEvent } = this.context + if (!prevProps.inError && inError) { + const errorField = Object.keys(sendErrors).find(key => sendErrors[key]) + const errorMessage = sendErrors[errorField] + + metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Edit Screen', + name: 'Error', + }, + customVariables: { + errorField, + errorMessage, + }, + }) + } + } + + render () { + return ( + <PageContainerFooter + onCancel={() => this.onCancel()} + onSubmit={e => this.onSubmit(e)} + disabled={this.formShouldBeDisabled()} + /> + ) + } + +} diff --git a/ui/app/pages/send/send-footer/send-footer.container.js b/ui/app/pages/send/send-footer/send-footer.container.js new file mode 100644 index 000000000..68f4dc7c3 --- /dev/null +++ b/ui/app/pages/send/send-footer/send-footer.container.js @@ -0,0 +1,120 @@ +import { connect } from 'react-redux' +import ethUtil from 'ethereumjs-util' +import { + addToAddressBook, + clearSend, + signTokenTx, + signTx, + updateTransaction, +} from '../../../store/actions' +import SendFooter from './send-footer.component' +import { + getGasLimit, + getGasPrice, + getGasTotal, + getSelectedToken, + getSendAmount, + getSendEditingTransactionId, + getSendFromObject, + getSendTo, + getSendToAccounts, + getSendHexData, + getTokenBalance, + getUnapprovedTxs, + getSendErrors, +} from '../send.selectors' +import { + isSendFormInError, +} from './send-footer.selectors' +import { + addressIsNew, + constructTxParams, + constructUpdatedTx, +} from './send-footer.utils' +import { + getRenderableEstimateDataForSmallButtonsFromGWEI, + getDefaultActiveButtonIndex, +} from '../../../selectors/custom-gas' + +export default connect(mapStateToProps, mapDispatchToProps)(SendFooter) + +function mapStateToProps (state) { + const gasButtonInfo = getRenderableEstimateDataForSmallButtonsFromGWEI(state) + const gasPrice = getGasPrice(state) + const activeButtonIndex = getDefaultActiveButtonIndex(gasButtonInfo, gasPrice) + const gasChangedLabel = activeButtonIndex >= 0 + ? gasButtonInfo[activeButtonIndex].labelKey + : 'custom' + + return { + amount: getSendAmount(state), + data: getSendHexData(state), + editingTransactionId: getSendEditingTransactionId(state), + from: getSendFromObject(state), + gasLimit: getGasLimit(state), + gasPrice: getGasPrice(state), + gasTotal: getGasTotal(state), + inError: isSendFormInError(state), + selectedToken: getSelectedToken(state), + to: getSendTo(state), + toAccounts: getSendToAccounts(state), + tokenBalance: getTokenBalance(state), + unapprovedTxs: getUnapprovedTxs(state), + sendErrors: getSendErrors(state), + gasChangedLabel, + } +} + +function mapDispatchToProps (dispatch) { + return { + clearSend: () => dispatch(clearSend()), + sign: ({ selectedToken, to, amount, from, gas, gasPrice, data }) => { + const txParams = constructTxParams({ + amount, + data, + from, + gas, + gasPrice, + selectedToken, + to, + }) + + selectedToken + ? dispatch(signTokenTx(selectedToken.address, to, amount, txParams)) + : dispatch(signTx(txParams)) + }, + update: ({ + amount, + data, + editingTransactionId, + from, + gas, + gasPrice, + selectedToken, + to, + unapprovedTxs, + }) => { + const editingTx = constructUpdatedTx({ + amount, + data, + editingTransactionId, + from, + gas, + gasPrice, + selectedToken, + to, + unapprovedTxs, + }) + + return dispatch(updateTransaction(editingTx)) + }, + + addToAddressBookIfNew: (newAddress, toAccounts, nickname = '') => { + const hexPrefixedAddress = ethUtil.addHexPrefix(newAddress) + if (addressIsNew(toAccounts, hexPrefixedAddress)) { + // TODO: nickname, i.e. addToAddressBook(recipient, nickname) + dispatch(addToAddressBook(hexPrefixedAddress, nickname)) + } + }, + } +} diff --git a/ui/app/pages/send/send-footer/send-footer.scss b/ui/app/pages/send/send-footer/send-footer.scss new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/pages/send/send-footer/send-footer.scss diff --git a/ui/app/pages/send/send-footer/send-footer.selectors.js b/ui/app/pages/send/send-footer/send-footer.selectors.js new file mode 100644 index 000000000..e20addfdc --- /dev/null +++ b/ui/app/pages/send/send-footer/send-footer.selectors.js @@ -0,0 +1,11 @@ +const { getSendErrors } = require('../send.selectors') + +const selectors = { + isSendFormInError, +} + +module.exports = selectors + +function isSendFormInError (state) { + return Object.values(getSendErrors(state)).some(n => n) +} diff --git a/ui/app/pages/send/send-footer/send-footer.utils.js b/ui/app/pages/send/send-footer/send-footer.utils.js new file mode 100644 index 000000000..91ac29014 --- /dev/null +++ b/ui/app/pages/send/send-footer/send-footer.utils.js @@ -0,0 +1,88 @@ +const ethAbi = require('ethereumjs-abi') +const ethUtil = require('ethereumjs-util') +const { TOKEN_TRANSFER_FUNCTION_SIGNATURE } = require('../send.constants') + +function addHexPrefixToObjectValues (obj) { + return Object.keys(obj).reduce((newObj, key) => { + return { ...newObj, [key]: ethUtil.addHexPrefix(obj[key]) } + }, {}) +} + +function constructTxParams ({ selectedToken, data, to, amount, from, gas, gasPrice }) { + const txParams = { + data, + from, + value: '0', + gas, + gasPrice, + } + + if (!selectedToken) { + txParams.value = amount + txParams.to = to + } + + return addHexPrefixToObjectValues(txParams) +} + +function constructUpdatedTx ({ + amount, + data, + editingTransactionId, + from, + gas, + gasPrice, + selectedToken, + to, + unapprovedTxs, +}) { + const unapprovedTx = unapprovedTxs[editingTransactionId] + const txParamsData = unapprovedTx.txParams.data ? unapprovedTx.txParams.data : data + + const editingTx = { + ...unapprovedTx, + txParams: Object.assign( + unapprovedTx.txParams, + addHexPrefixToObjectValues({ + data: txParamsData, + to, + from, + gas, + gasPrice, + value: amount, + }) + ), + } + + if (selectedToken) { + const data = TOKEN_TRANSFER_FUNCTION_SIGNATURE + Array.prototype.map.call( + ethAbi.rawEncode(['address', 'uint256'], [to, ethUtil.addHexPrefix(amount)]), + x => ('00' + x.toString(16)).slice(-2) + ).join('') + + Object.assign(editingTx.txParams, addHexPrefixToObjectValues({ + value: '0', + to: selectedToken.address, + data, + })) + } + + if (typeof editingTx.txParams.data === 'undefined') { + delete editingTx.txParams.data + } + + return editingTx +} + +function addressIsNew (toAccounts, newAddress) { + const newAddressNormalized = newAddress.toLowerCase() + const foundMatching = toAccounts.some(({ address }) => address === newAddressNormalized) + return !foundMatching +} + +module.exports = { + addressIsNew, + constructTxParams, + constructUpdatedTx, + addHexPrefixToObjectValues, +} diff --git a/ui/app/pages/send/send-footer/tests/send-footer-component.test.js b/ui/app/pages/send/send-footer/tests/send-footer-component.test.js new file mode 100644 index 000000000..56fc95df2 --- /dev/null +++ b/ui/app/pages/send/send-footer/tests/send-footer-component.test.js @@ -0,0 +1,233 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import sinon from 'sinon' +import { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } from '../../../../helpers/constants/routes' +import SendFooter from '../send-footer.component.js' + +import PageContainerFooter from '../../../../components/ui/page-container/page-container-footer' + +const propsMethodSpies = { + addToAddressBookIfNew: sinon.spy(), + clearSend: sinon.spy(), + sign: sinon.spy(), + update: sinon.spy(), +} +const historySpies = { + push: sinon.spy(), +} +const MOCK_EVENT = { preventDefault: () => {} } + +sinon.spy(SendFooter.prototype, 'onCancel') +sinon.spy(SendFooter.prototype, 'onSubmit') + +describe('SendFooter Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow(<SendFooter + addToAddressBookIfNew={propsMethodSpies.addToAddressBookIfNew} + amount={'mockAmount'} + clearSend={propsMethodSpies.clearSend} + disabled={true} + editingTransactionId={'mockEditingTransactionId'} + errors={{}} + from={ { address: 'mockAddress', balance: 'mockBalance' } } + gasLimit={'mockGasLimit'} + gasPrice={'mockGasPrice'} + gasTotal={'mockGasTotal'} + history={historySpies} + inError={false} + selectedToken={{ mockProp: 'mockSelectedTokenProp' }} + sign={propsMethodSpies.sign} + to={'mockTo'} + toAccounts={['mockAccount']} + tokenBalance={'mockTokenBalance'} + unapprovedTxs={['mockTx']} + update={propsMethodSpies.update} + sendErrors={{}} + />, { context: { t: str => str, metricsEvent: () => ({}) } }) + }) + + afterEach(() => { + propsMethodSpies.clearSend.resetHistory() + propsMethodSpies.addToAddressBookIfNew.resetHistory() + propsMethodSpies.clearSend.resetHistory() + propsMethodSpies.sign.resetHistory() + propsMethodSpies.update.resetHistory() + historySpies.push.resetHistory() + SendFooter.prototype.onCancel.resetHistory() + SendFooter.prototype.onSubmit.resetHistory() + }) + + describe('onCancel', () => { + it('should call clearSend', () => { + assert.equal(propsMethodSpies.clearSend.callCount, 0) + wrapper.instance().onCancel() + assert.equal(propsMethodSpies.clearSend.callCount, 1) + }) + + it('should call history.push', () => { + assert.equal(historySpies.push.callCount, 0) + wrapper.instance().onCancel() + assert.equal(historySpies.push.callCount, 1) + assert.equal(historySpies.push.getCall(0).args[0], DEFAULT_ROUTE) + }) + }) + + + describe('formShouldBeDisabled()', () => { + const config = { + 'should return true if inError is truthy': { + inError: true, + expectedResult: true, + }, + 'should return true if gasTotal is falsy': { + inError: false, + gasTotal: false, + expectedResult: true, + }, + 'should return true if to is truthy': { + to: '0xsomevalidAddress', + inError: false, + gasTotal: false, + expectedResult: true, + }, + 'should return true if selectedToken is truthy and tokenBalance is falsy': { + selectedToken: true, + tokenBalance: null, + expectedResult: true, + }, + 'should return false if inError is false and all other params are truthy': { + inError: false, + gasTotal: '0x123', + selectedToken: true, + tokenBalance: 123, + expectedResult: false, + }, + } + Object.entries(config).map(([description, obj]) => { + it(description, () => { + wrapper.setProps(obj) + assert.equal(wrapper.instance().formShouldBeDisabled(), obj.expectedResult) + }) + }) + }) + + describe('onSubmit', () => { + it('should call addToAddressBookIfNew with the correct params', () => { + wrapper.instance().onSubmit(MOCK_EVENT) + assert(propsMethodSpies.addToAddressBookIfNew.calledOnce) + assert.deepEqual( + propsMethodSpies.addToAddressBookIfNew.getCall(0).args, + ['mockTo', ['mockAccount']] + ) + }) + + it('should call props.update if editingTransactionId is truthy', () => { + wrapper.instance().onSubmit(MOCK_EVENT) + assert(propsMethodSpies.update.calledOnce) + assert.deepEqual( + propsMethodSpies.update.getCall(0).args[0], + { + data: undefined, + amount: 'mockAmount', + editingTransactionId: 'mockEditingTransactionId', + from: 'mockAddress', + gas: 'mockGasLimit', + gasPrice: 'mockGasPrice', + selectedToken: { mockProp: 'mockSelectedTokenProp' }, + to: 'mockTo', + unapprovedTxs: ['mockTx'], + } + ) + }) + + it('should not call props.sign if editingTransactionId is truthy', () => { + assert.equal(propsMethodSpies.sign.callCount, 0) + }) + + it('should call props.sign if editingTransactionId is falsy', () => { + wrapper.setProps({ editingTransactionId: null }) + wrapper.instance().onSubmit(MOCK_EVENT) + assert(propsMethodSpies.sign.calledOnce) + assert.deepEqual( + propsMethodSpies.sign.getCall(0).args[0], + { + data: undefined, + amount: 'mockAmount', + from: 'mockAddress', + gas: 'mockGasLimit', + gasPrice: 'mockGasPrice', + selectedToken: { mockProp: 'mockSelectedTokenProp' }, + to: 'mockTo', + } + ) + }) + + it('should not call props.update if editingTransactionId is falsy', () => { + assert.equal(propsMethodSpies.update.callCount, 0) + }) + + it('should call history.push', done => { + Promise.resolve(wrapper.instance().onSubmit(MOCK_EVENT)) + .then(() => { + assert.equal(historySpies.push.callCount, 1) + assert.equal(historySpies.push.getCall(0).args[0], CONFIRM_TRANSACTION_ROUTE) + done() + }) + }) + }) + + describe('render', () => { + beforeEach(() => { + sinon.stub(SendFooter.prototype, 'formShouldBeDisabled').returns('formShouldBeDisabledReturn') + wrapper = shallow(<SendFooter + addToAddressBookIfNew={propsMethodSpies.addToAddressBookIfNew} + amount={'mockAmount'} + clearSend={propsMethodSpies.clearSend} + disabled={true} + editingTransactionId={'mockEditingTransactionId'} + errors={{}} + from={ { address: 'mockAddress', balance: 'mockBalance' } } + gasLimit={'mockGasLimit'} + gasPrice={'mockGasPrice'} + gasTotal={'mockGasTotal'} + history={historySpies} + inError={false} + selectedToken={{ mockProp: 'mockSelectedTokenProp' }} + sign={propsMethodSpies.sign} + to={'mockTo'} + toAccounts={['mockAccount']} + tokenBalance={'mockTokenBalance'} + unapprovedTxs={['mockTx']} + update={propsMethodSpies.update} + />, { context: { t: str => str, metricsEvent: () => ({}) } }) + }) + + afterEach(() => { + SendFooter.prototype.formShouldBeDisabled.restore() + }) + + it('should render a PageContainerFooter component', () => { + assert.equal(wrapper.find(PageContainerFooter).length, 1) + }) + + it('should pass the correct props to PageContainerFooter', () => { + const { + onCancel, + onSubmit, + disabled, + } = wrapper.find(PageContainerFooter).props() + assert.equal(disabled, 'formShouldBeDisabledReturn') + + assert.equal(SendFooter.prototype.onSubmit.callCount, 0) + onSubmit(MOCK_EVENT) + assert.equal(SendFooter.prototype.onSubmit.callCount, 1) + + assert.equal(SendFooter.prototype.onCancel.callCount, 0) + onCancel() + assert.equal(SendFooter.prototype.onCancel.callCount, 1) + }) + }) +}) diff --git a/ui/app/pages/send/send-footer/tests/send-footer-container.test.js b/ui/app/pages/send/send-footer/tests/send-footer-container.test.js new file mode 100644 index 000000000..118ebf356 --- /dev/null +++ b/ui/app/pages/send/send-footer/tests/send-footer-container.test.js @@ -0,0 +1,205 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' +import sinon from 'sinon' + +let mapStateToProps +let mapDispatchToProps + +const actionSpies = { + addToAddressBook: sinon.spy(), + clearSend: sinon.spy(), + signTokenTx: sinon.spy(), + signTx: sinon.spy(), + updateTransaction: sinon.spy(), +} +const utilsStubs = { + addressIsNew: sinon.stub().returns(true), + constructTxParams: sinon.stub().returns({ + value: 'mockAmount', + }), + constructUpdatedTx: sinon.stub().returns('mockConstructedUpdatedTxParams'), +} + +proxyquire('../send-footer.container.js', { + 'react-redux': { + connect: (ms, md) => { + mapStateToProps = ms + mapDispatchToProps = md + return () => ({}) + }, + }, + '../../../store/actions': actionSpies, + '../send.selectors': { + getGasLimit: (s) => `mockGasLimit:${s}`, + getGasPrice: (s) => `mockGasPrice:${s}`, + getGasTotal: (s) => `mockGasTotal:${s}`, + getSelectedToken: (s) => `mockSelectedToken:${s}`, + getSendAmount: (s) => `mockAmount:${s}`, + getSendEditingTransactionId: (s) => `mockEditingTransactionId:${s}`, + getSendFromObject: (s) => `mockFromObject:${s}`, + getSendTo: (s) => `mockTo:${s}`, + getSendToAccounts: (s) => `mockToAccounts:${s}`, + getTokenBalance: (s) => `mockTokenBalance:${s}`, + getSendHexData: (s) => `mockHexData:${s}`, + getUnapprovedTxs: (s) => `mockUnapprovedTxs:${s}`, + getSendErrors: (s) => `mockSendErrors:${s}`, + }, + './send-footer.selectors': { isSendFormInError: (s) => `mockInError:${s}` }, + './send-footer.utils': utilsStubs, + '../../../selectors/custom-gas': { + getRenderableEstimateDataForSmallButtonsFromGWEI: (s) => ([{ labelKey: `mockLabel:${s}` }]), + getDefaultActiveButtonIndex: () => 0, + }, +}) + +describe('send-footer container', () => { + + describe('mapStateToProps()', () => { + + it('should map the correct properties to props', () => { + assert.deepEqual(mapStateToProps('mockState'), { + amount: 'mockAmount:mockState', + data: 'mockHexData:mockState', + selectedToken: 'mockSelectedToken:mockState', + editingTransactionId: 'mockEditingTransactionId:mockState', + from: 'mockFromObject:mockState', + gasLimit: 'mockGasLimit:mockState', + gasPrice: 'mockGasPrice:mockState', + gasTotal: 'mockGasTotal:mockState', + inError: 'mockInError:mockState', + to: 'mockTo:mockState', + toAccounts: 'mockToAccounts:mockState', + tokenBalance: 'mockTokenBalance:mockState', + unapprovedTxs: 'mockUnapprovedTxs:mockState', + sendErrors: 'mockSendErrors:mockState', + gasChangedLabel: 'mockLabel:mockState', + }) + }) + + }) + + describe('mapDispatchToProps()', () => { + let dispatchSpy + let mapDispatchToPropsObject + + beforeEach(() => { + dispatchSpy = sinon.spy() + mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) + }) + + describe('clearSend()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.clearSend() + assert(dispatchSpy.calledOnce) + assert(actionSpies.clearSend.calledOnce) + }) + }) + + describe('sign()', () => { + it('should dispatch a signTokenTx action if selectedToken is defined', () => { + mapDispatchToPropsObject.sign({ + selectedToken: { + address: '0xabc', + }, + to: 'mockTo', + amount: 'mockAmount', + from: 'mockFrom', + gas: 'mockGas', + gasPrice: 'mockGasPrice', + }) + assert(dispatchSpy.calledOnce) + assert.deepEqual( + utilsStubs.constructTxParams.getCall(0).args[0], + { + data: undefined, + selectedToken: { + address: '0xabc', + }, + to: 'mockTo', + amount: 'mockAmount', + from: 'mockFrom', + gas: 'mockGas', + gasPrice: 'mockGasPrice', + } + ) + assert.deepEqual( + actionSpies.signTokenTx.getCall(0).args, + [ '0xabc', 'mockTo', 'mockAmount', { value: 'mockAmount' } ] + ) + }) + + it('should dispatch a sign action if selectedToken is not defined', () => { + utilsStubs.constructTxParams.resetHistory() + mapDispatchToPropsObject.sign({ + to: 'mockTo', + amount: 'mockAmount', + from: 'mockFrom', + gas: 'mockGas', + gasPrice: 'mockGasPrice', + }) + assert(dispatchSpy.calledOnce) + assert.deepEqual( + utilsStubs.constructTxParams.getCall(0).args[0], + { + data: undefined, + selectedToken: undefined, + to: 'mockTo', + amount: 'mockAmount', + from: 'mockFrom', + gas: 'mockGas', + gasPrice: 'mockGasPrice', + } + ) + assert.deepEqual( + actionSpies.signTx.getCall(0).args, + [ { value: 'mockAmount' } ] + ) + }) + }) + + describe('update()', () => { + it('should dispatch an updateTransaction action', () => { + mapDispatchToPropsObject.update({ + to: 'mockTo', + amount: 'mockAmount', + from: 'mockFrom', + gas: 'mockGas', + gasPrice: 'mockGasPrice', + editingTransactionId: 'mockEditingTransactionId', + selectedToken: 'mockSelectedToken', + unapprovedTxs: 'mockUnapprovedTxs', + }) + assert(dispatchSpy.calledOnce) + assert.deepEqual( + utilsStubs.constructUpdatedTx.getCall(0).args[0], + { + data: undefined, + to: 'mockTo', + amount: 'mockAmount', + from: 'mockFrom', + gas: 'mockGas', + gasPrice: 'mockGasPrice', + editingTransactionId: 'mockEditingTransactionId', + selectedToken: 'mockSelectedToken', + unapprovedTxs: 'mockUnapprovedTxs', + } + ) + assert.equal(actionSpies.updateTransaction.getCall(0).args[0], 'mockConstructedUpdatedTxParams') + }) + }) + + describe('addToAddressBookIfNew()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.addToAddressBookIfNew('mockNewAddress', 'mockToAccounts', 'mockNickname') + assert(dispatchSpy.calledOnce) + assert.equal(utilsStubs.addressIsNew.getCall(0).args[0], 'mockToAccounts') + assert.deepEqual( + actionSpies.addToAddressBook.getCall(0).args, + [ '0xmockNewAddress', 'mockNickname' ] + ) + }) + }) + + }) + +}) diff --git a/ui/app/pages/send/send-footer/tests/send-footer-selectors.test.js b/ui/app/pages/send/send-footer/tests/send-footer-selectors.test.js new file mode 100644 index 000000000..8de032f57 --- /dev/null +++ b/ui/app/pages/send/send-footer/tests/send-footer-selectors.test.js @@ -0,0 +1,24 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' + +const { + isSendFormInError, +} = proxyquire('../send-footer.selectors', { + '../send.selectors': { + getSendErrors: (mockState) => mockState.errors, + }, +}) + +describe('send-footer selectors', () => { + + describe('getTitleKey()', () => { + it('should return true if any of the values of the object returned by getSendErrors are truthy', () => { + assert.equal(isSendFormInError({ errors: { a: 'abc', b: false} }), true) + }) + + it('should return false if all of the values of the object returned by getSendErrors are falsy', () => { + assert.equal(isSendFormInError({ errors: { a: false, b: null} }), false) + }) + }) + +}) diff --git a/ui/app/pages/send/send-footer/tests/send-footer-utils.test.js b/ui/app/pages/send/send-footer/tests/send-footer-utils.test.js new file mode 100644 index 000000000..f4705e691 --- /dev/null +++ b/ui/app/pages/send/send-footer/tests/send-footer-utils.test.js @@ -0,0 +1,233 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' +import sinon from 'sinon' +const { TOKEN_TRANSFER_FUNCTION_SIGNATURE } = require('../../send.constants') + +const stubs = { + rawEncode: sinon.stub().callsFake((arr1, arr2) => { + return [ ...arr1, ...arr2 ] + }), +} + +const sendUtils = proxyquire('../send-footer.utils.js', { + 'ethereumjs-abi': { + rawEncode: stubs.rawEncode, + }, +}) +const { + addressIsNew, + constructTxParams, + constructUpdatedTx, + addHexPrefixToObjectValues, +} = sendUtils + +describe('send-footer utils', () => { + + describe('addHexPrefixToObjectValues()', () => { + it('should return a new object with the same properties with a 0x prefix', () => { + assert.deepEqual( + addHexPrefixToObjectValues({ + prop1: '0x123', + prop2: '456', + prop3: 'x', + }), + { + prop1: '0x123', + prop2: '0x456', + prop3: '0xx', + } + ) + }) + }) + + describe('addressIsNew()', () => { + it('should return false if the address exists in toAccounts', () => { + assert.equal( + addressIsNew([ + { address: '0xabc' }, + { address: '0xdef' }, + { address: '0xghi' }, + ], '0xdef'), + false + ) + }) + + it('should return true if the address does not exists in toAccounts', () => { + assert.equal( + addressIsNew([ + { address: '0xabc' }, + { address: '0xdef' }, + { address: '0xghi' }, + ], '0xxyz'), + true + ) + }) + }) + + describe('constructTxParams()', () => { + it('should return a new txParams object with data if there data is given', () => { + assert.deepEqual( + constructTxParams({ + data: 'someData', + selectedToken: false, + to: 'mockTo', + amount: 'mockAmount', + from: 'mockFrom', + gas: 'mockGas', + gasPrice: 'mockGasPrice', + }), + { + data: '0xsomeData', + to: '0xmockTo', + value: '0xmockAmount', + from: '0xmockFrom', + gas: '0xmockGas', + gasPrice: '0xmockGasPrice', + } + ) + }) + + it('should return a new txParams object with value and to properties if there is no selectedToken', () => { + assert.deepEqual( + constructTxParams({ + selectedToken: false, + to: 'mockTo', + amount: 'mockAmount', + from: 'mockFrom', + gas: 'mockGas', + gasPrice: 'mockGasPrice', + }), + { + data: undefined, + to: '0xmockTo', + value: '0xmockAmount', + from: '0xmockFrom', + gas: '0xmockGas', + gasPrice: '0xmockGasPrice', + } + ) + }) + + it('should return a new txParams object without a to property and a 0 value if there is a selectedToken', () => { + assert.deepEqual( + constructTxParams({ + selectedToken: true, + to: 'mockTo', + amount: 'mockAmount', + from: 'mockFrom', + gas: 'mockGas', + gasPrice: 'mockGasPrice', + }), + { + data: undefined, + value: '0x0', + from: '0xmockFrom', + gas: '0xmockGas', + gasPrice: '0xmockGasPrice', + } + ) + }) + }) + + describe('constructUpdatedTx()', () => { + it('should return a new object with an updated txParams', () => { + const result = constructUpdatedTx({ + amount: 'mockAmount', + editingTransactionId: '0x456', + from: 'mockFrom', + gas: 'mockGas', + gasPrice: 'mockGasPrice', + selectedToken: false, + to: 'mockTo', + unapprovedTxs: { + '0x123': {}, + '0x456': { + unapprovedTxParam: 'someOtherParam', + txParams: { + data: 'someData', + }, + }, + }, + }) + assert.deepEqual(result, { + unapprovedTxParam: 'someOtherParam', + txParams: { + from: '0xmockFrom', + gas: '0xmockGas', + gasPrice: '0xmockGasPrice', + value: '0xmockAmount', + to: '0xmockTo', + data: '0xsomeData', + }, + }) + }) + + it('should not have data property if there is non in the original tx', () => { + const result = constructUpdatedTx({ + amount: 'mockAmount', + editingTransactionId: '0x456', + from: 'mockFrom', + gas: 'mockGas', + gasPrice: 'mockGasPrice', + selectedToken: false, + to: 'mockTo', + unapprovedTxs: { + '0x123': {}, + '0x456': { + unapprovedTxParam: 'someOtherParam', + txParams: { + from: 'oldFrom', + gas: 'oldGas', + gasPrice: 'oldGasPrice', + }, + }, + }, + }) + + assert.deepEqual(result, { + unapprovedTxParam: 'someOtherParam', + txParams: { + from: '0xmockFrom', + gas: '0xmockGas', + gasPrice: '0xmockGasPrice', + value: '0xmockAmount', + to: '0xmockTo', + }, + }) + }) + + it('should have token property values if selectedToken is truthy', () => { + const result = constructUpdatedTx({ + amount: 'mockAmount', + editingTransactionId: '0x456', + from: 'mockFrom', + gas: 'mockGas', + gasPrice: 'mockGasPrice', + selectedToken: { + address: 'mockTokenAddress', + }, + to: 'mockTo', + unapprovedTxs: { + '0x123': {}, + '0x456': { + unapprovedTxParam: 'someOtherParam', + txParams: {}, + }, + }, + }) + + assert.deepEqual(result, { + unapprovedTxParam: 'someOtherParam', + txParams: { + from: '0xmockFrom', + gas: '0xmockGas', + gasPrice: '0xmockGasPrice', + value: '0x0', + to: '0xmockTokenAddress', + data: `${TOKEN_TRANSFER_FUNCTION_SIGNATURE}ss56Tont`, + }, + }) + }) + }) + +}) |