commit
e7c2710a55
@ -0,0 +1,6 @@ |
||||
#!/usr/bin/env bash |
||||
|
||||
echo "Downloading firefox..." |
||||
wget https://ftp.mozilla.org/pub/firefox/releases/58.0/linux-x86_64/en-US/firefox-58.0.tar.bz2 \ |
||||
&& tar xjf firefox-58.0.tar.bz2 |
||||
echo "firefox download complete" |
@ -0,0 +1,8 @@ |
||||
#!/usr/bin/env bash |
||||
|
||||
echo "Installing firefox..." |
||||
sudo rm -r /opt/firefox |
||||
sudo mv firefox /opt/firefox58 |
||||
sudo mv /usr/bin/firefox /usr/bin/firefox-old |
||||
sudo ln -s /opt/firefox58/firefox /usr/bin/firefox |
||||
echo "Firefox installed." |
@ -0,0 +1,24 @@ |
||||
const Config = require('./recipient-blacklist-config.json') |
||||
|
||||
/** @module*/ |
||||
module.exports = { |
||||
checkAccount, |
||||
} |
||||
|
||||
/** |
||||
* Checks if a specified account on a specified network is blacklisted. |
||||
@param networkId {number} |
||||
@param account {string} |
||||
*/ |
||||
function checkAccount (networkId, account) { |
||||
|
||||
const mainnetId = 1 |
||||
if (networkId !== mainnetId) { |
||||
return |
||||
} |
||||
|
||||
const accountToCheck = account.toLowerCase() |
||||
if (Config.blacklist.includes(accountToCheck)) { |
||||
throw new Error('Recipient is a public account') |
||||
} |
||||
} |
@ -0,0 +1,14 @@ |
||||
{ |
||||
"blacklist": [ |
||||
"0x627306090abab3a6e1400e9345bc60c78a8bef57", |
||||
"0xf17f52151ebef6c7334fad080c5704d77216b732", |
||||
"0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef", |
||||
"0x821aea9a577a9b44299b9c15c88cf3087f3b5544", |
||||
"0x0d1d4e623d10f9fba5db95830f7d3839406c6af2", |
||||
"0x2932b7a2355d6fecc4b5c0b6bd44cc31df247a2e", |
||||
"0x2191ef87e392377ec08e7c08eb105ef5448eced5", |
||||
"0x0f4f2ac550a1b4e2280d04c21cea7ebd822934b5", |
||||
"0x6330a553fc93768f612722bb8c2ec78ac90b3bbc", |
||||
"0x5aeda56215b167893e80b4fe645ba6d5bab767de" |
||||
] |
||||
} |
@ -0,0 +1,234 @@ |
||||
const fs = require('fs') |
||||
const async = require('async') |
||||
const path = require('path') |
||||
const promisify = require('pify') |
||||
|
||||
// start(/\.selectors.js/, generateSelectorTest).catch(console.error)
|
||||
// start(/\.utils.js/, generateUtilTest).catch(console.error)
|
||||
startContainer(/\.container.js/, generateContainerTest).catch(console.error) |
||||
|
||||
async function getAllFileNames (dirName) { |
||||
const rootPath = path.join(__dirname, dirName) |
||||
const allNames = (await promisify(fs.readdir)(dirName)) |
||||
const fileNames = allNames.filter(name => name.match(/^.+\./)) |
||||
const dirNames = allNames.filter(name => name.match(/^[^.]+$/)) |
||||
|
||||
const fullPathDirNames = dirNames.map(d => `${dirName}/${d}`) |
||||
const subNameArrays = await promisify(async.map)(fullPathDirNames, getAllFileNames) |
||||
let subNames = [] |
||||
subNameArrays.forEach(subNameArray => subNames = [...subNames, ...subNameArray]) |
||||
|
||||
return [ |
||||
...fileNames.map(name => dirName + '/' + name), |
||||
...subNames, |
||||
] |
||||
} |
||||
|
||||
async function start (fileRegEx, testGenerator) { |
||||
const fileNames = await getAllFileNames('./ui/app') |
||||
const sFiles = fileNames.filter(name => name.match(fileRegEx)) |
||||
|
||||
let sFileMethodNames |
||||
let testFilePath |
||||
async.each(sFiles, async (sFile, cb) => { |
||||
let [, sRootPath, sPath] = sFile.match(/^(.+\/)([^/]+)$/) |
||||
sFileMethodNames = Object.keys(require(__dirname + '/' + sFile)) |
||||
|
||||
testFilePath = sPath.replace('.', '-').replace('.', '.test.') |
||||
|
||||
await promisify(fs.writeFile)( |
||||
`${__dirname}/${sRootPath}tests/${testFilePath}`, |
||||
testGenerator(sPath, sFileMethodNames), |
||||
'utf8' |
||||
) |
||||
}, (err) => { |
||||
console.log(err) |
||||
}) |
||||
|
||||
} |
||||
|
||||
async function startContainer (fileRegEx, testGenerator) { |
||||
const fileNames = await getAllFileNames('./ui/app') |
||||
const sFiles = fileNames.filter(name => name.match(fileRegEx)) |
||||
|
||||
let sFileMethodNames |
||||
async.each(sFiles, async (sFile, cb) => { |
||||
console.log(`sFile`, sFile); |
||||
let [, sRootPath, sPath] = sFile.match(/^(.+\/)([^/]+)$/) |
||||
|
||||
let testFilePath = sPath.replace('.', '-').replace('.', '.test.') |
||||
|
||||
await promisify(fs.readFile)( |
||||
__dirname + '/' + sFile, |
||||
'utf8', |
||||
async (err, result) => { |
||||
console.log(`result`, result.length); |
||||
const returnObjectStrings = result |
||||
.match(/return\s(\{[\s\S]+?})\n}/g) |
||||
.map(str => { |
||||
return str |
||||
.slice(0, str.length - 1) |
||||
.slice(7) |
||||
.replace(/\n/g, '') |
||||
.replace(/\s\s+/g, ' ') |
||||
|
||||
}) |
||||
const mapStateToPropsAssertionObject = returnObjectStrings[0] |
||||
.replace(/\w+:\s\w+\([\w,\s]+\),/g, str => { |
||||
const strKey = str.match(/^\w+/)[0] |
||||
return strKey + ': \'mock' + str.match(/^\w+/)[0].replace(/^./, c => c.toUpperCase()) + ':mockState\',\n' |
||||
}) |
||||
.replace(/{\s\w.+/, firstLinePair => `{\n ${firstLinePair.slice(2)}`) |
||||
.replace(/\w+:.+,/g, s => ` ${s}`) |
||||
.replace(/}/g, s => ` ${s}`) |
||||
let mapDispatchToPropsMethodNames |
||||
if (returnObjectStrings[1]) { |
||||
mapDispatchToPropsMethodNames = returnObjectStrings[1].match(/\s\w+:\s/g).map(str => str.match(/\w+/)[0]) |
||||
} |
||||
const proxyquireObject = ('{\n ' + result |
||||
.match(/import\s{[\s\S]+?}\sfrom\s.+/g) |
||||
.map(s => s.replace(/\n/g, '')) |
||||
.map((s, i) => { |
||||
const proxyKeys = s.match(/{.+}/)[0].match(/\w+/g) |
||||
return '\'' + s.match(/'(.+)'/)[1] + '\': { ' + (proxyKeys.length > 1 |
||||
? '\n ' + proxyKeys.join(': () => {},\n ') + ': () => {},\n ' |
||||
: proxyKeys[0] + ': () => {},') + ' }' |
||||
}) |
||||
.join(',\n ') + '\n}') |
||||
.replace('{ connect: () => {}, },', `{
|
||||
connect: (ms, md) => { |
||||
mapStateToProps = ms |
||||
mapDispatchToProps = md |
||||
return () => ({}) |
||||
}, |
||||
},`)
|
||||
// console.log(`proxyquireObject`, proxyquireObject);
|
||||
// console.log(`mapStateToPropsAssertionObject`, mapStateToPropsAssertionObject);
|
||||
// console.log(`mapDispatchToPropsMethodNames`, mapDispatchToPropsMethodNames);
|
||||
|
||||
const containerTest = generateContainerTest(sPath, { |
||||
mapStateToPropsAssertionObject, |
||||
mapDispatchToPropsMethodNames, |
||||
proxyquireObject, |
||||
}) |
||||
// console.log(`containerTest`, `${__dirname}/${sRootPath}tests/${testFilePath}`, containerTest);
|
||||
console.log('----') |
||||
console.log(`sRootPath`, sRootPath); |
||||
console.log(`testFilePath`, testFilePath); |
||||
await promisify(fs.writeFile)( |
||||
`${__dirname}/${sRootPath}tests/${testFilePath}`, |
||||
containerTest, |
||||
'utf8' |
||||
) |
||||
} |
||||
) |
||||
}, (err) => { |
||||
console.log('123', err) |
||||
}) |
||||
|
||||
} |
||||
|
||||
function generateMethodList (methodArray) { |
||||
return methodArray.map(n => ' ' + n).join(',\n') + ','
|
||||
} |
||||
|
||||
function generateMethodDescribeBlock (methodName, index) { |
||||
const describeBlock = |
||||
`${index ? ' ' : ''}describe('${methodName}()', () => {
|
||||
it('should', () => { |
||||
const state = {} |
||||
|
||||
assert.equal(${methodName}(state), ) |
||||
}) |
||||
})` |
||||
return describeBlock |
||||
} |
||||
|
||||
function generateDispatchMethodDescribeBlock (methodName, index) { |
||||
const describeBlock = |
||||
`${index ? ' ' : ''}describe('${methodName}()', () => {
|
||||
it('should dispatch an action', () => { |
||||
mapDispatchToPropsObject.${methodName}() |
||||
assert(dispatchSpy.calledOnce) |
||||
}) |
||||
})` |
||||
return describeBlock |
||||
} |
||||
|
||||
function generateMethodDescribeBlocks (methodArray) { |
||||
return methodArray |
||||
.map((methodName, index) => generateMethodDescribeBlock(methodName, index)) |
||||
.join('\n\n') |
||||
} |
||||
|
||||
function generateDispatchMethodDescribeBlocks (methodArray) { |
||||
return methodArray |
||||
.map((methodName, index) => generateDispatchMethodDescribeBlock(methodName, index)) |
||||
.join('\n\n') |
||||
} |
||||
|
||||
function generateSelectorTest (name, methodArray) { |
||||
return `import assert from 'assert'
|
||||
import { |
||||
${generateMethodList(methodArray)} |
||||
} from '../${name}' |
||||
|
||||
describe('${name.match(/^[^.]+/)} selectors', () => { |
||||
|
||||
${generateMethodDescribeBlocks(methodArray)} |
||||
|
||||
})` |
||||
} |
||||
|
||||
function generateUtilTest (name, methodArray) { |
||||
return `import assert from 'assert'
|
||||
import { |
||||
${generateMethodList(methodArray)} |
||||
} from '../${name}' |
||||
|
||||
describe('${name.match(/^[^.]+/)} utils', () => { |
||||
|
||||
${generateMethodDescribeBlocks(methodArray)} |
||||
|
||||
})` |
||||
} |
||||
|
||||
function generateContainerTest (sPath, { |
||||
mapStateToPropsAssertionObject, |
||||
mapDispatchToPropsMethodNames, |
||||
proxyquireObject, |
||||
}) { |
||||
return `import assert from 'assert'
|
||||
import proxyquire from 'proxyquire' |
||||
import sinon from 'sinon' |
||||
|
||||
let mapStateToProps |
||||
let mapDispatchToProps |
||||
|
||||
proxyquire('../${sPath}', ${proxyquireObject}) |
||||
|
||||
describe('${sPath.match(/^[^.]+/)} container', () => { |
||||
|
||||
describe('mapStateToProps()', () => { |
||||
|
||||
it('should map the correct properties to props', () => { |
||||
assert.deepEqual(mapStateToProps('mockState'), ${mapStateToPropsAssertionObject}) |
||||
}) |
||||
|
||||
}) |
||||
|
||||
describe('mapDispatchToProps()', () => { |
||||
let dispatchSpy |
||||
let mapDispatchToPropsObject |
||||
|
||||
beforeEach(() => { |
||||
dispatchSpy = sinon.spy() |
||||
mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) |
||||
}) |
||||
|
||||
${mapDispatchToPropsMethodNames ? generateDispatchMethodDescribeBlocks(mapDispatchToPropsMethodNames) : 'delete'} |
||||
|
||||
}) |
||||
|
||||
})` |
||||
} |
@ -0,0 +1,77 @@ |
||||
const assert = require('assert') |
||||
const recipientBlackListChecker = require('../../../../../app/scripts/controllers/transactions/lib/recipient-blacklist-checker') |
||||
const { |
||||
ROPSTEN_CODE, |
||||
RINKEYBY_CODE, |
||||
KOVAN_CODE, |
||||
} = require('../../../../../app/scripts/controllers/network/enums') |
||||
|
||||
const KeyringController = require('eth-keyring-controller') |
||||
|
||||
describe('Recipient Blacklist Checker', function () { |
||||
|
||||
let publicAccounts |
||||
|
||||
before(async function () { |
||||
const damnedMnemonic = 'candy maple cake sugar pudding cream honey rich smooth crumble sweet treat' |
||||
const keyringController = new KeyringController({}) |
||||
const Keyring = keyringController.getKeyringClassForType('HD Key Tree') |
||||
const opts = { |
||||
mnemonic: damnedMnemonic, |
||||
numberOfAccounts: 10, |
||||
} |
||||
const keyring = new Keyring(opts) |
||||
publicAccounts = await keyring.getAccounts() |
||||
}) |
||||
|
||||
describe('#checkAccount', function () { |
||||
it('does not fail on test networks', function () { |
||||
let callCount = 0 |
||||
const networks = [ROPSTEN_CODE, RINKEYBY_CODE, KOVAN_CODE] |
||||
for (let networkId in networks) { |
||||
publicAccounts.forEach((account) => { |
||||
recipientBlackListChecker.checkAccount(networkId, account) |
||||
callCount++ |
||||
}) |
||||
} |
||||
assert.equal(callCount, 30) |
||||
}) |
||||
|
||||
it('fails on mainnet', function () { |
||||
const mainnetId = 1 |
||||
let callCount = 0 |
||||
publicAccounts.forEach((account) => { |
||||
try { |
||||
recipientBlackListChecker.checkAccount(mainnetId, account) |
||||
assert.fail('function should have thrown an error') |
||||
} catch (err) { |
||||
assert.equal(err.message, 'Recipient is a public account') |
||||
} |
||||
callCount++ |
||||
}) |
||||
assert.equal(callCount, 10) |
||||
}) |
||||
|
||||
it('fails for public account - uppercase', function () { |
||||
const mainnetId = 1 |
||||
const publicAccount = '0X0D1D4E623D10F9FBA5DB95830F7D3839406C6AF2' |
||||
try { |
||||
recipientBlackListChecker.checkAccount(mainnetId, publicAccount) |
||||
assert.fail('function should have thrown an error') |
||||
} catch (err) { |
||||
assert.equal(err.message, 'Recipient is a public account') |
||||
} |
||||
})
|
||||
|
||||
it('fails for public account - lowercase', async function () { |
||||
const mainnetId = 1 |
||||
const publicAccount = '0x0d1d4e623d10f9fba5db95830f7d3839406c6af2' |
||||
try { |
||||
await recipientBlackListChecker.checkAccount(mainnetId, publicAccount) |
||||
assert.fail('function should have thrown an error') |
||||
} catch (err) { |
||||
assert.equal(err.message, 'Recipient is a public account') |
||||
} |
||||
})
|
||||
}) |
||||
}) |
@ -1,113 +0,0 @@ |
||||
const Component = require('react').Component |
||||
const h = require('react-hyperscript') |
||||
const inherits = require('util').inherits |
||||
|
||||
module.exports = CurrencyInput |
||||
|
||||
inherits(CurrencyInput, Component) |
||||
function CurrencyInput (props) { |
||||
Component.call(this) |
||||
|
||||
const sanitizedValue = sanitizeValue(props.value) |
||||
|
||||
this.state = { |
||||
value: sanitizedValue, |
||||
emptyState: false, |
||||
focused: false, |
||||
} |
||||
} |
||||
|
||||
function removeNonDigits (str) { |
||||
return str.match(/\d|$/g).join('') |
||||
} |
||||
|
||||
// Removes characters that are not digits, then removes leading zeros
|
||||
function sanitizeInteger (val) { |
||||
return String(parseInt(removeNonDigits(val) || '0', 10)) |
||||
} |
||||
|
||||
function sanitizeDecimal (val) { |
||||
return removeNonDigits(val) |
||||
} |
||||
|
||||
// Take a single string param and returns a non-negative integer or float as a string.
|
||||
// Breaks the input into three parts: the integer, the decimal point, and the decimal/fractional part.
|
||||
// Removes leading zeros from the integer, and non-digits from the integer and decimal
|
||||
// The integer is returned as '0' in cases where it would be empty. A decimal point is
|
||||
// included in the returned string if one is included in the param
|
||||
// Examples:
|
||||
// sanitizeValue('0') -> '0'
|
||||
// sanitizeValue('a') -> '0'
|
||||
// sanitizeValue('010.') -> '10.'
|
||||
// sanitizeValue('0.005') -> '0.005'
|
||||
// sanitizeValue('22.200') -> '22.200'
|
||||
// sanitizeValue('.200') -> '0.200'
|
||||
// sanitizeValue('a.b.1.c,89.123') -> '0.189123'
|
||||
function sanitizeValue (value) { |
||||
let [ , integer, point, decimal] = (/([^.]*)([.]?)([^.]*)/).exec(value) |
||||
|
||||
integer = sanitizeInteger(integer) || '0' |
||||
decimal = sanitizeDecimal(decimal) |
||||
|
||||
return `${integer}${point}${decimal}` |
||||
} |
||||
|
||||
CurrencyInput.prototype.handleChange = function (newValue) { |
||||
const { onInputChange } = this.props |
||||
const { value } = this.state |
||||
|
||||
let parsedValue = newValue |
||||
const newValueLastIndex = newValue.length - 1 |
||||
|
||||
if (value === '0' && newValue[newValueLastIndex] === '0') { |
||||
parsedValue = parsedValue.slice(0, newValueLastIndex) |
||||
} |
||||
const sanitizedValue = sanitizeValue(parsedValue) |
||||
this.setState({ |
||||
value: sanitizedValue, |
||||
emptyState: newValue === '' && sanitizedValue === '0', |
||||
}) |
||||
onInputChange(sanitizedValue) |
||||
} |
||||
|
||||
// If state.value === props.value plus a decimal point, or at least one
|
||||
// zero or a decimal point and at least one zero, then this returns state.value
|
||||
// after it is sanitized with getValueParts
|
||||
CurrencyInput.prototype.getValueToRender = function () { |
||||
const { value } = this.props |
||||
const { value: stateValue } = this.state |
||||
|
||||
const trailingStateString = (new RegExp(`^${value}(.+)`)).exec(stateValue) |
||||
const trailingDecimalAndZeroes = trailingStateString && (/^[.0]0*/).test(trailingStateString[1]) |
||||
|
||||
return sanitizeValue(trailingDecimalAndZeroes |
||||
? stateValue |
||||
: value) |
||||
} |
||||
|
||||
CurrencyInput.prototype.render = function () { |
||||
const { |
||||
className, |
||||
placeholder, |
||||
readOnly, |
||||
inputRef, |
||||
type, |
||||
} = this.props |
||||
const { emptyState, focused } = this.state |
||||
|
||||
const inputSizeMultiplier = readOnly ? 1 : 1.2 |
||||
|
||||
const valueToRender = this.getValueToRender() |
||||
return h('input', { |
||||
className, |
||||
type, |
||||
value: emptyState ? '' : valueToRender, |
||||
placeholder: focused ? '' : placeholder, |
||||
size: valueToRender.length * inputSizeMultiplier, |
||||
readOnly, |
||||
onFocus: () => this.setState({ focused: true, emptyState: valueToRender === '0' }), |
||||
onBlur: () => this.setState({ focused: false, emptyState: false }), |
||||
onChange: e => this.handleChange(e.target.value), |
||||
ref: inputRef, |
||||
}) |
||||
} |
@ -0,0 +1 @@ |
||||
export { default } from './page-container.component' |
@ -0,0 +1,18 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
|
||||
export default class PageContainerContent extends Component { |
||||
|
||||
static propTypes = { |
||||
children: PropTypes.node.isRequired, |
||||
}; |
||||
|
||||
render () { |
||||
return ( |
||||
<div className="page-container__content"> |
||||
{this.props.children} |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
} |
@ -0,0 +1 @@ |
||||
export { default } from './page-container-footer.component' |
@ -0,0 +1,54 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import Button from '../../button' |
||||
|
||||
export default class PageContainerFooter extends Component { |
||||
|
||||
static propTypes = { |
||||
onCancel: PropTypes.func, |
||||
cancelText: PropTypes.string, |
||||
onSubmit: PropTypes.func, |
||||
submitText: PropTypes.string, |
||||
disabled: PropTypes.bool, |
||||
} |
||||
|
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
render () { |
||||
const { |
||||
onCancel, |
||||
cancelText, |
||||
onSubmit, |
||||
submitText, |
||||
disabled, |
||||
} = this.props |
||||
|
||||
return ( |
||||
<div className="page-container__footer"> |
||||
|
||||
<Button |
||||
type="default" |
||||
large={true} |
||||
className="page-container__footer-button" |
||||
onClick={() => onCancel()} |
||||
> |
||||
{ cancelText || this.context.t('cancel') } |
||||
</Button> |
||||
|
||||
<Button |
||||
type="primary" |
||||
large={true} |
||||
className="page-container__footer-button" |
||||
disabled={disabled} |
||||
onClick={e => onSubmit(e)} |
||||
> |
||||
{ submitText || this.context.t('next') } |
||||
</Button> |
||||
|
||||
</div> |
||||
) |
||||
} |
||||
|
||||
} |
@ -0,0 +1,35 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
|
||||
export default class PageContainerHeader extends Component { |
||||
|
||||
static propTypes = { |
||||
title: PropTypes.string, |
||||
subtitle: PropTypes.string, |
||||
onClose: PropTypes.func, |
||||
}; |
||||
|
||||
render () { |
||||
const { title, subtitle, onClose } = this.props |
||||
|
||||
return ( |
||||
<div className="page-container__header"> |
||||
|
||||
<div className="page-container__title"> |
||||
{title} |
||||
</div> |
||||
|
||||
<div className="page-container__subtitle"> |
||||
{subtitle} |
||||
</div> |
||||
|
||||
<div |
||||
className="page-container__header-close" |
||||
onClick={() => onClose()} |
||||
/> |
||||
|
||||
</div> |
||||
) |
||||
} |
||||
|
||||
} |
@ -0,0 +1 @@ |
||||
export { default } from './page-container-header.component' |
@ -0,0 +1,57 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
|
||||
export default class PageContainerHeader extends Component { |
||||
|
||||
static propTypes = { |
||||
title: PropTypes.string.isRequired, |
||||
subtitle: PropTypes.string, |
||||
onClose: PropTypes.func, |
||||
showBackButton: PropTypes.bool, |
||||
onBackButtonClick: PropTypes.func, |
||||
backButtonStyles: PropTypes.object, |
||||
backButtonString: PropTypes.string, |
||||
}; |
||||
|
||||
renderHeaderRow () { |
||||
const { showBackButton, onBackButtonClick, backButtonStyles, backButtonString } = this.props |
||||
|
||||
return showBackButton && ( |
||||
<div className="page-container__header-row"> |
||||
<span |
||||
className="page-container__back-button" |
||||
onClick={onBackButtonClick} |
||||
style={backButtonStyles} |
||||
> |
||||
{ backButtonString || 'Back' } |
||||
</span> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
render () { |
||||
const { title, subtitle, onClose } = this.props |
||||
|
||||
return ( |
||||
<div className="page-container__header"> |
||||
|
||||
{ this.renderHeaderRow() } |
||||
|
||||
<div className="page-container__title"> |
||||
{title} |
||||
</div> |
||||
|
||||
<div className="page-container__subtitle"> |
||||
{subtitle} |
||||
</div> |
||||
|
||||
<div |
||||
className="page-container__header-close" |
||||
onClick={() => onClose()} |
||||
/> |
||||
|
||||
</div> |
||||
) |
||||
} |
||||
|
||||
} |
@ -0,0 +1,72 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
|
||||
import PageContainerHeader from './page-container-header' |
||||
import PageContainerFooter from './page-container-footer' |
||||
|
||||
export default class PageContainer extends Component { |
||||
|
||||
static propTypes = { |
||||
// PageContainerHeader props
|
||||
title: PropTypes.string.isRequired, |
||||
subtitle: PropTypes.string, |
||||
onClose: PropTypes.func, |
||||
showBackButton: PropTypes.bool, |
||||
onBackButtonClick: PropTypes.func, |
||||
backButtonStyles: PropTypes.object, |
||||
backButtonString: PropTypes.string, |
||||
// Content props
|
||||
ContentComponent: PropTypes.func, |
||||
contentComponentProps: PropTypes.object, |
||||
// PageContainerFooter props
|
||||
onCancel: PropTypes.func, |
||||
cancelText: PropTypes.string, |
||||
onSubmit: PropTypes.func, |
||||
submitText: PropTypes.string, |
||||
disabled: PropTypes.bool, |
||||
}; |
||||
|
||||
render () { |
||||
const { |
||||
title, |
||||
subtitle, |
||||
onClose, |
||||
showBackButton, |
||||
onBackButtonClick, |
||||
backButtonStyles, |
||||
backButtonString, |
||||
ContentComponent, |
||||
contentComponentProps, |
||||
onCancel, |
||||
cancelText, |
||||
onSubmit, |
||||
submitText, |
||||
disabled, |
||||
} = this.props |
||||
|
||||
return ( |
||||
<div className="page-container"> |
||||
<PageContainerHeader |
||||
title={title} |
||||
subtitle={subtitle} |
||||
onClose={onClose} |
||||
showBackButton={showBackButton} |
||||
onBackButtonClick={onBackButtonClick} |
||||
backButtonStyles={backButtonStyles} |
||||
backButtonString={backButtonString} |
||||
/> |
||||
<div className="page-container__content"> |
||||
<ContentComponent { ...contentComponentProps } /> |
||||
</div> |
||||
<PageContainerFooter |
||||
onCancel={onCancel} |
||||
cancelText={cancelText} |
||||
onSubmit={onSubmit} |
||||
submitText={submitText} |
||||
disabled={disabled} |
||||
/> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
} |
@ -1,74 +0,0 @@ |
||||
const Component = require('react').Component |
||||
const h = require('react-hyperscript') |
||||
const inherits = require('util').inherits |
||||
const connect = require('react-redux').connect |
||||
const { checksumAddress } = require('../../util') |
||||
const Identicon = require('../identicon') |
||||
const CurrencyDisplay = require('./currency-display') |
||||
const { conversionRateSelector, getCurrentCurrency } = require('../../selectors') |
||||
|
||||
inherits(AccountListItem, Component) |
||||
function AccountListItem () { |
||||
Component.call(this) |
||||
} |
||||
|
||||
function mapStateToProps (state) { |
||||
return { |
||||
conversionRate: conversionRateSelector(state), |
||||
currentCurrency: getCurrentCurrency(state), |
||||
} |
||||
} |
||||
|
||||
module.exports = connect(mapStateToProps)(AccountListItem) |
||||
|
||||
AccountListItem.prototype.render = function () { |
||||
const { |
||||
className, |
||||
account, |
||||
handleClick, |
||||
icon = null, |
||||
conversionRate, |
||||
currentCurrency, |
||||
displayBalance = true, |
||||
displayAddress = false, |
||||
} = this.props |
||||
|
||||
const { name, address, balance } = account || {} |
||||
|
||||
return h('div.account-list-item', { |
||||
className, |
||||
onClick: () => handleClick({ name, address, balance }), |
||||
}, [ |
||||
|
||||
h('div.account-list-item__top-row', {}, [ |
||||
|
||||
h( |
||||
Identicon, |
||||
{ |
||||
address, |
||||
diameter: 18, |
||||
className: 'account-list-item__identicon', |
||||
}, |
||||
), |
||||
|
||||
h('div.account-list-item__account-name', {}, name || address), |
||||
|
||||
icon && h('div.account-list-item__icon', [icon]), |
||||
|
||||
]), |
||||
|
||||
displayAddress && name && h('div.account-list-item__account-address', checksumAddress(address)), |
||||
|
||||
displayBalance && h(CurrencyDisplay, { |
||||
primaryCurrency: 'ETH', |
||||
convertedCurrency: currentCurrency, |
||||
value: balance, |
||||
conversionRate, |
||||
readOnly: true, |
||||
className: 'account-list-item__account-balances', |
||||
primaryBalanceClassName: 'account-list-item__account-primary-balance', |
||||
convertedBalanceClassName: 'account-list-item__account-secondary-balance', |
||||
}, name), |
||||
|
||||
]) |
||||
} |
@ -1,72 +0,0 @@ |
||||
const Component = require('react').Component |
||||
const h = require('react-hyperscript') |
||||
const inherits = require('util').inherits |
||||
const AccountListItem = require('./account-list-item') |
||||
|
||||
module.exports = FromDropdown |
||||
|
||||
inherits(FromDropdown, Component) |
||||
function FromDropdown () { |
||||
Component.call(this) |
||||
} |
||||
|
||||
FromDropdown.prototype.getListItemIcon = function (currentAccount, selectedAccount) { |
||||
const listItemIcon = h(`i.fa.fa-check.fa-lg`, { style: { color: '#02c9b1' } }) |
||||
|
||||
return currentAccount.address === selectedAccount.address |
||||
? listItemIcon |
||||
: null |
||||
} |
||||
|
||||
FromDropdown.prototype.renderDropdown = function () { |
||||
const { |
||||
accounts, |
||||
selectedAccount, |
||||
closeDropdown, |
||||
onSelect, |
||||
} = this.props |
||||
|
||||
return h('div', {}, [ |
||||
|
||||
h('div.send-v2__from-dropdown__close-area', { |
||||
onClick: closeDropdown, |
||||
}), |
||||
|
||||
h('div.send-v2__from-dropdown__list', {}, [ |
||||
|
||||
...accounts.map(account => h(AccountListItem, { |
||||
className: 'account-list-item__dropdown', |
||||
account, |
||||
handleClick: () => { |
||||
onSelect(account) |
||||
closeDropdown() |
||||
}, |
||||
icon: this.getListItemIcon(account, selectedAccount), |
||||
})), |
||||
|
||||
]), |
||||
|
||||
]) |
||||
} |
||||
|
||||
FromDropdown.prototype.render = function () { |
||||
const { |
||||
selectedAccount, |
||||
openDropdown, |
||||
dropdownOpen, |
||||
} = this.props |
||||
|
||||
return h('div.send-v2__from-dropdown', {}, [ |
||||
|
||||
h(AccountListItem, { |
||||
account: selectedAccount, |
||||
handleClick: openDropdown, |
||||
icon: h(`i.fa.fa-caret-down.fa-lg`, { style: { color: '#dedede' } }), |
||||
}), |
||||
|
||||
dropdownOpen && this.renderDropdown(), |
||||
|
||||
]) |
||||
|
||||
} |
||||
|
@ -1,106 +0,0 @@ |
||||
const Component = require('react').Component |
||||
const PropTypes = require('prop-types') |
||||
const h = require('react-hyperscript') |
||||
const inherits = require('util').inherits |
||||
const InputNumber = require('../input-number.js') |
||||
const connect = require('react-redux').connect |
||||
|
||||
GasTooltip.contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
module.exports = connect()(GasTooltip) |
||||
|
||||
|
||||
inherits(GasTooltip, Component) |
||||
function GasTooltip () { |
||||
Component.call(this) |
||||
this.state = { |
||||
gasLimit: 0, |
||||
gasPrice: 0, |
||||
} |
||||
|
||||
this.updateGasPrice = this.updateGasPrice.bind(this) |
||||
this.updateGasLimit = this.updateGasLimit.bind(this) |
||||
this.onClose = this.onClose.bind(this) |
||||
} |
||||
|
||||
GasTooltip.prototype.componentWillMount = function () { |
||||
const { gasPrice = 0, gasLimit = 0} = this.props |
||||
|
||||
this.setState({ |
||||
gasPrice: parseInt(gasPrice, 16) / 1000000000, |
||||
gasLimit: parseInt(gasLimit, 16), |
||||
}) |
||||
} |
||||
|
||||
GasTooltip.prototype.updateGasPrice = function (newPrice) { |
||||
const { onFeeChange } = this.props |
||||
const { gasLimit } = this.state |
||||
|
||||
this.setState({ gasPrice: newPrice }) |
||||
onFeeChange({ |
||||
gasLimit: gasLimit.toString(16), |
||||
gasPrice: (newPrice * 1000000000).toString(16), |
||||
}) |
||||
} |
||||
|
||||
GasTooltip.prototype.updateGasLimit = function (newLimit) { |
||||
const { onFeeChange } = this.props |
||||
const { gasPrice } = this.state |
||||
|
||||
this.setState({ gasLimit: newLimit }) |
||||
onFeeChange({ |
||||
gasLimit: newLimit.toString(16), |
||||
gasPrice: (gasPrice * 1000000000).toString(16), |
||||
}) |
||||
} |
||||
|
||||
GasTooltip.prototype.onClose = function (e) { |
||||
e.stopPropagation() |
||||
this.props.onClose() |
||||
} |
||||
|
||||
GasTooltip.prototype.render = function () { |
||||
const { gasPrice, gasLimit } = this.state |
||||
|
||||
return h('div.gas-tooltip', {}, [ |
||||
h('div.gas-tooltip-close-area', { |
||||
onClick: this.onClose, |
||||
}), |
||||
h('div.customize-gas-tooltip-container', {}, [ |
||||
h('div.customize-gas-tooltip', {}, [ |
||||
h('div.gas-tooltip-header.gas-tooltip-label', {}, ['Customize Gas']), |
||||
h('div.gas-tooltip-input-label', {}, [ |
||||
h('span.gas-tooltip-label', {}, ['Gas Price']), |
||||
h('i.fa.fa-info-circle'), |
||||
]), |
||||
h(InputNumber, { |
||||
unitLabel: 'GWEI', |
||||
step: 1, |
||||
min: 0, |
||||
placeholder: '0', |
||||
value: gasPrice, |
||||
onChange: (newPrice) => this.updateGasPrice(newPrice), |
||||
}), |
||||
h('div.gas-tooltip-input-label', { |
||||
style: { |
||||
'marginTop': '81px', |
||||
}, |
||||
}, [ |
||||
h('span.gas-tooltip-label', {}, [this.context.t('gasLimit')]), |
||||
h('i.fa.fa-info-circle'), |
||||
]), |
||||
h(InputNumber, { |
||||
unitLabel: 'UNITS', |
||||
step: 1, |
||||
min: 0, |
||||
placeholder: '0', |
||||
value: gasLimit, |
||||
onChange: (newLimit) => this.updateGasLimit(newLimit), |
||||
}), |
||||
]), |
||||
h('div.gas-tooltip-arrow', {}), |
||||
]), |
||||
]) |
||||
} |
@ -1,33 +0,0 @@ |
||||
// const Component = require('react').Component
|
||||
// const h = require('react-hyperscript')
|
||||
// const inherits = require('util').inherits
|
||||
// const Identicon = require('../identicon')
|
||||
|
||||
// module.exports = MemoTextArea
|
||||
|
||||
// inherits(MemoTextArea, Component)
|
||||
// function MemoTextArea () {
|
||||
// Component.call(this)
|
||||
// }
|
||||
|
||||
// MemoTextArea.prototype.render = function () {
|
||||
// const { memo, identities, onChange } = this.props
|
||||
|
||||
// return h('div.send-v2__memo-text-area', [
|
||||
|
||||
// h('textarea.send-v2__memo-text-area__input', {
|
||||
// placeholder: 'Optional',
|
||||
// value: memo,
|
||||
// onChange,
|
||||
// // onBlur: () => {
|
||||
// // this.setErrorsFor('memo')
|
||||
// // },
|
||||
// onFocus: event => {
|
||||
// // this.clearErrorsFor('memo')
|
||||
// },
|
||||
// }),
|
||||
|
||||
// ])
|
||||
|
||||
// }
|
||||
|
@ -1,78 +0,0 @@ |
||||
const { |
||||
addCurrencies, |
||||
conversionUtil, |
||||
conversionGTE, |
||||
multiplyCurrencies, |
||||
} = require('../../conversion-util') |
||||
const { |
||||
calcTokenAmount, |
||||
} = require('../../token-util') |
||||
|
||||
function isBalanceSufficient ({ |
||||
amount = '0x0', |
||||
gasTotal = '0x0', |
||||
balance, |
||||
primaryCurrency, |
||||
amountConversionRate, |
||||
conversionRate, |
||||
}) { |
||||
const totalAmount = addCurrencies(amount, gasTotal, { |
||||
aBase: 16, |
||||
bBase: 16, |
||||
toNumericBase: 'hex', |
||||
}) |
||||
|
||||
const balanceIsSufficient = conversionGTE( |
||||
{ |
||||
value: balance, |
||||
fromNumericBase: 'hex', |
||||
fromCurrency: primaryCurrency, |
||||
conversionRate, |
||||
}, |
||||
{ |
||||
value: totalAmount, |
||||
fromNumericBase: 'hex', |
||||
conversionRate: amountConversionRate || conversionRate, |
||||
fromCurrency: primaryCurrency, |
||||
}, |
||||
) |
||||
|
||||
return balanceIsSufficient |
||||
} |
||||
|
||||
function isTokenBalanceSufficient ({ |
||||
amount = '0x0', |
||||
tokenBalance, |
||||
decimals, |
||||
}) { |
||||
const amountInDec = conversionUtil(amount, { |
||||
fromNumericBase: 'hex', |
||||
}) |
||||
|
||||
const tokenBalanceIsSufficient = conversionGTE( |
||||
{ |
||||
value: tokenBalance, |
||||
fromNumericBase: 'dec', |
||||
}, |
||||
{ |
||||
value: calcTokenAmount(amountInDec, decimals), |
||||
fromNumericBase: 'dec', |
||||
}, |
||||
) |
||||
|
||||
return tokenBalanceIsSufficient |
||||
} |
||||
|
||||
function getGasTotal (gasLimit, gasPrice) { |
||||
return multiplyCurrencies(gasLimit, gasPrice, { |
||||
toNumericBase: 'hex', |
||||
multiplicandBase: 16, |
||||
multiplierBase: 16, |
||||
}) |
||||
} |
||||
|
||||
module.exports = { |
||||
getGasTotal, |
||||
isBalanceSufficient, |
||||
isTokenBalanceSufficient, |
||||
} |
@ -1,89 +0,0 @@ |
||||
const connect = require('react-redux').connect |
||||
const actions = require('../../actions') |
||||
const abi = require('ethereumjs-abi') |
||||
const SendEther = require('../../send-v2') |
||||
const { withRouter } = require('react-router-dom') |
||||
const { compose } = require('recompose') |
||||
|
||||
const { |
||||
accountsWithSendEtherInfoSelector, |
||||
getCurrentAccountWithSendEtherInfo, |
||||
conversionRateSelector, |
||||
getSelectedToken, |
||||
getSelectedAddress, |
||||
getAddressBook, |
||||
getSendFrom, |
||||
getCurrentCurrency, |
||||
getSelectedTokenToFiatRate, |
||||
getSelectedTokenContract, |
||||
} = require('../../selectors') |
||||
|
||||
module.exports = compose( |
||||
withRouter, |
||||
connect(mapStateToProps, mapDispatchToProps) |
||||
)(SendEther) |
||||
|
||||
function mapStateToProps (state) { |
||||
const fromAccounts = accountsWithSendEtherInfoSelector(state) |
||||
const selectedAddress = getSelectedAddress(state) |
||||
const selectedToken = getSelectedToken(state) |
||||
const conversionRate = conversionRateSelector(state) |
||||
|
||||
let data |
||||
let primaryCurrency |
||||
let tokenToFiatRate |
||||
if (selectedToken) { |
||||
data = Array.prototype.map.call( |
||||
abi.rawEncode(['address', 'uint256'], [selectedAddress, '0x0']), |
||||
x => ('00' + x.toString(16)).slice(-2) |
||||
).join('') |
||||
|
||||
primaryCurrency = selectedToken.symbol |
||||
|
||||
tokenToFiatRate = getSelectedTokenToFiatRate(state) |
||||
} |
||||
|
||||
return { |
||||
...state.metamask.send, |
||||
from: getSendFrom(state) || getCurrentAccountWithSendEtherInfo(state), |
||||
fromAccounts, |
||||
toAccounts: [...fromAccounts, ...getAddressBook(state)], |
||||
conversionRate, |
||||
selectedToken, |
||||
primaryCurrency, |
||||
convertedCurrency: getCurrentCurrency(state), |
||||
data, |
||||
selectedAddress, |
||||
amountConversionRate: selectedToken ? tokenToFiatRate : conversionRate, |
||||
tokenContract: getSelectedTokenContract(state), |
||||
unapprovedTxs: state.metamask.unapprovedTxs, |
||||
network: state.metamask.network, |
||||
} |
||||
} |
||||
|
||||
function mapDispatchToProps (dispatch) { |
||||
return { |
||||
showCustomizeGasModal: () => dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' })), |
||||
estimateGas: params => dispatch(actions.estimateGas(params)), |
||||
getGasPrice: () => dispatch(actions.getGasPrice()), |
||||
signTokenTx: (tokenAddress, toAddress, amount, txData) => ( |
||||
dispatch(actions.signTokenTx(tokenAddress, toAddress, amount, txData)) |
||||
), |
||||
signTx: txParams => dispatch(actions.signTx(txParams)), |
||||
updateAndApproveTx: txParams => dispatch(actions.updateAndApproveTx(txParams)), |
||||
updateTx: txData => dispatch(actions.updateTransaction(txData)), |
||||
setSelectedAddress: address => dispatch(actions.setSelectedAddress(address)), |
||||
addToAddressBook: (address, nickname) => dispatch(actions.addToAddressBook(address, nickname)), |
||||
updateGasTotal: newTotal => dispatch(actions.updateGasTotal(newTotal)), |
||||
updateGasPrice: newGasPrice => dispatch(actions.updateGasPrice(newGasPrice)), |
||||
updateGasLimit: newGasLimit => dispatch(actions.updateGasLimit(newGasLimit)), |
||||
updateSendTokenBalance: tokenBalance => dispatch(actions.updateSendTokenBalance(tokenBalance)), |
||||
updateSendFrom: newFrom => dispatch(actions.updateSendFrom(newFrom)), |
||||
updateSendTo: (newTo, nickname) => dispatch(actions.updateSendTo(newTo, nickname)), |
||||
updateSendAmount: newAmount => dispatch(actions.updateSendAmount(newAmount)), |
||||
updateSendMemo: newMemo => dispatch(actions.updateSendMemo(newMemo)), |
||||
updateSendErrors: newError => dispatch(actions.updateSendErrors(newError)), |
||||
clearSend: () => dispatch(actions.clearSend()), |
||||
setMaxModeTo: bool => dispatch(actions.setMaxModeTo(bool)), |
||||
} |
||||
} |
@ -0,0 +1,74 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import { checksumAddress } from '../../../util' |
||||
import Identicon from '../../identicon' |
||||
import CurrencyDisplay from '../../send/currency-display' |
||||
|
||||
export default class AccountListItem extends Component { |
||||
|
||||
static propTypes = { |
||||
account: PropTypes.object, |
||||
className: PropTypes.string, |
||||
conversionRate: PropTypes.number, |
||||
currentCurrency: PropTypes.string, |
||||
displayAddress: PropTypes.bool, |
||||
displayBalance: PropTypes.bool, |
||||
handleClick: PropTypes.func, |
||||
icon: PropTypes.node, |
||||
}; |
||||
|
||||
render () { |
||||
const { |
||||
account, |
||||
className, |
||||
conversionRate, |
||||
currentCurrency, |
||||
displayAddress = false, |
||||
displayBalance = true, |
||||
handleClick, |
||||
icon = null, |
||||
} = this.props |
||||
|
||||
const { name, address, balance } = account || {} |
||||
|
||||
return (<div |
||||
className={`account-list-item ${className}`} |
||||
onClick={() => handleClick({ name, address, balance })} |
||||
> |
||||
|
||||
<div className="account-list-item__top-row"> |
||||
<Identicon |
||||
address={address} |
||||
className="account-list-item__identicon" |
||||
diameter={18} |
||||
/> |
||||
|
||||
<div className="account-list-item__account-name">{ name || address }</div> |
||||
|
||||
{icon && <div className="account-list-item__icon">{ icon }</div>} |
||||
|
||||
</div> |
||||
|
||||
{displayAddress && name && <div className="account-list-item__account-address"> |
||||
{ checksumAddress(address) } |
||||
</div>} |
||||
|
||||
{displayBalance && <CurrencyDisplay |
||||
className="account-list-item__account-balances" |
||||
conversionRate={conversionRate} |
||||
convertedBalanceClassName="account-list-item__account-secondary-balance" |
||||
convertedCurrency={currentCurrency} |
||||
primaryBalanceClassName="account-list-item__account-primary-balance" |
||||
primaryCurrency="ETH" |
||||
readOnly={true} |
||||
value={balance} |
||||
/>} |
||||
|
||||
</div>) |
||||
} |
||||
} |
||||
|
||||
AccountListItem.contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
@ -0,0 +1,15 @@ |
||||
import { connect } from 'react-redux' |
||||
import { |
||||
getConversionRate, |
||||
getConvertedCurrency, |
||||
} from '../send.selectors.js' |
||||
import AccountListItem from './account-list-item.component' |
||||
|
||||
export default connect(mapStateToProps)(AccountListItem) |
||||
|
||||
function mapStateToProps (state) { |
||||
return { |
||||
conversionRate: getConversionRate(state), |
||||
currentCurrency: getConvertedCurrency(state), |
||||
} |
||||
} |
@ -0,0 +1 @@ |
||||
export { default } from './account-list-item.container' |
@ -0,0 +1,138 @@ |
||||
import React from 'react' |
||||
import assert from 'assert' |
||||
import { shallow } from 'enzyme' |
||||
import sinon from 'sinon' |
||||
import proxyquire from 'proxyquire' |
||||
import Identicon from '../../../identicon' |
||||
import CurrencyDisplay from '../../../send/currency-display' |
||||
|
||||
const utilsMethodStubs = { |
||||
checksumAddress: sinon.stub().returns('mockCheckSumAddress'), |
||||
} |
||||
|
||||
const AccountListItem = proxyquire('../account-list-item.component.js', { |
||||
'../../../util': utilsMethodStubs, |
||||
}).default |
||||
|
||||
|
||||
const propsMethodSpies = { |
||||
handleClick: sinon.spy(), |
||||
} |
||||
|
||||
describe('AccountListItem Component', function () { |
||||
let wrapper |
||||
|
||||
beforeEach(() => { |
||||
wrapper = shallow(<AccountListItem |
||||
account={ { address: 'mockAddress', name: 'mockName', balance: 'mockBalance' } } |
||||
className={'mockClassName'} |
||||
conversionRate={4} |
||||
currentCurrency={'mockCurrentyCurrency'} |
||||
displayAddress={false} |
||||
displayBalance={false} |
||||
handleClick={propsMethodSpies.handleClick} |
||||
icon={<i className="mockIcon" />} |
||||
/>, { context: { t: str => str + '_t' } }) |
||||
}) |
||||
|
||||
afterEach(() => { |
||||
propsMethodSpies.handleClick.resetHistory() |
||||
}) |
||||
|
||||
describe('render', () => { |
||||
it('should render a div with the passed className', () => { |
||||
assert.equal(wrapper.find('.mockClassName').length, 1) |
||||
assert(wrapper.find('.mockClassName').is('div')) |
||||
assert(wrapper.find('.mockClassName').hasClass('account-list-item')) |
||||
}) |
||||
|
||||
it('should call handleClick with the expected props when the root div is clicked', () => { |
||||
const { onClick } = wrapper.find('.mockClassName').props() |
||||
assert.equal(propsMethodSpies.handleClick.callCount, 0) |
||||
onClick() |
||||
assert.equal(propsMethodSpies.handleClick.callCount, 1) |
||||
assert.deepEqual( |
||||
propsMethodSpies.handleClick.getCall(0).args, |
||||
[{ address: 'mockAddress', name: 'mockName', balance: 'mockBalance' }] |
||||
) |
||||
}) |
||||
|
||||
it('should have a top row div', () => { |
||||
assert.equal(wrapper.find('.mockClassName > .account-list-item__top-row').length, 1) |
||||
assert(wrapper.find('.mockClassName > .account-list-item__top-row').is('div')) |
||||
}) |
||||
|
||||
it('should have an identicon, name and icon in the top row', () => { |
||||
const topRow = wrapper.find('.mockClassName > .account-list-item__top-row') |
||||
assert.equal(topRow.find(Identicon).length, 1) |
||||
assert.equal(topRow.find('.account-list-item__account-name').length, 1) |
||||
assert.equal(topRow.find('.account-list-item__icon').length, 1) |
||||
}) |
||||
|
||||
it('should show the account name if it exists', () => { |
||||
const topRow = wrapper.find('.mockClassName > .account-list-item__top-row') |
||||
assert.equal(topRow.find('.account-list-item__account-name').text(), 'mockName') |
||||
}) |
||||
|
||||
it('should show the account address if there is no name', () => { |
||||
wrapper.setProps({ account: { address: 'addressButNoName' } }) |
||||
const topRow = wrapper.find('.mockClassName > .account-list-item__top-row') |
||||
assert.equal(topRow.find('.account-list-item__account-name').text(), 'addressButNoName') |
||||
}) |
||||
|
||||
it('should render the passed icon', () => { |
||||
const topRow = wrapper.find('.mockClassName > .account-list-item__top-row') |
||||
assert(topRow.find('.account-list-item__icon').childAt(0).is('i')) |
||||
assert(topRow.find('.account-list-item__icon').childAt(0).hasClass('mockIcon')) |
||||
}) |
||||
|
||||
it('should not render an icon if none is passed', () => { |
||||
wrapper.setProps({ icon: null }) |
||||
const topRow = wrapper.find('.mockClassName > .account-list-item__top-row') |
||||
assert.equal(topRow.find('.account-list-item__icon').length, 0) |
||||
}) |
||||
|
||||
it('should render the account address as a checksumAddress if displayAddress is true and name is provided', () => { |
||||
wrapper.setProps({ displayAddress: true }) |
||||
assert.equal(wrapper.find('.account-list-item__account-address').length, 1) |
||||
assert.equal(wrapper.find('.account-list-item__account-address').text(), 'mockCheckSumAddress') |
||||
assert.deepEqual( |
||||
utilsMethodStubs.checksumAddress.getCall(0).args, |
||||
['mockAddress'] |
||||
) |
||||
}) |
||||
|
||||
it('should not render the account address as a checksumAddress if displayAddress is false', () => { |
||||
wrapper.setProps({ displayAddress: false }) |
||||
assert.equal(wrapper.find('.account-list-item__account-address').length, 0) |
||||
}) |
||||
|
||||
it('should not render the account address as a checksumAddress if name is not provided', () => { |
||||
wrapper.setProps({ account: { address: 'someAddressButNoName' } }) |
||||
assert.equal(wrapper.find('.account-list-item__account-address').length, 0) |
||||
}) |
||||
|
||||
it('should render a CurrencyDisplay with the correct props if displayBalance is true', () => { |
||||
wrapper.setProps({ displayBalance: true }) |
||||
assert.equal(wrapper.find(CurrencyDisplay).length, 1) |
||||
assert.deepEqual( |
||||
wrapper.find(CurrencyDisplay).props(), |
||||
{ |
||||
className: 'account-list-item__account-balances', |
||||
conversionRate: 4, |
||||
convertedBalanceClassName: 'account-list-item__account-secondary-balance', |
||||
convertedCurrency: 'mockCurrentyCurrency', |
||||
primaryBalanceClassName: 'account-list-item__account-primary-balance', |
||||
primaryCurrency: 'ETH', |
||||
readOnly: true, |
||||
value: 'mockBalance', |
||||
} |
||||
) |
||||
}) |
||||
|
||||
it('should not render a CurrencyDisplay if displayBalance is false', () => { |
||||
wrapper.setProps({ displayBalance: false }) |
||||
assert.equal(wrapper.find(CurrencyDisplay).length, 0) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,32 @@ |
||||
import assert from 'assert' |
||||
import proxyquire from 'proxyquire' |
||||
|
||||
let mapStateToProps |
||||
|
||||
proxyquire('../account-list-item.container.js', { |
||||
'react-redux': { |
||||
connect: (ms, md) => { |
||||
mapStateToProps = ms |
||||
return () => ({}) |
||||
}, |
||||
}, |
||||
'../send.selectors.js': { |
||||
getConversionRate: (s) => `mockConversionRate:${s}`, |
||||
getConvertedCurrency: (s) => `mockCurrentCurrency:${s}`, |
||||
}, |
||||
}) |
||||
|
||||
describe('account-list-item container', () => { |
||||
|
||||
describe('mapStateToProps()', () => { |
||||
|
||||
it('should map the correct properties to props', () => { |
||||
assert.deepEqual(mapStateToProps('mockState'), { |
||||
conversionRate: 'mockConversionRate:mockState', |
||||
currentCurrency: 'mockCurrentCurrency:mockState', |
||||
}) |
||||
}) |
||||
|
||||
}) |
||||
|
||||
}) |
@ -0,0 +1 @@ |
||||
export { default } from './send.container' |
@ -0,0 +1 @@ |
||||
export { default } from './send-content.component' |
@ -0,0 +1,54 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
|
||||
export default class AmountMaxButton extends Component { |
||||
|
||||
static propTypes = { |
||||
balance: PropTypes.string, |
||||
gasTotal: PropTypes.string, |
||||
maxModeOn: PropTypes.bool, |
||||
selectedToken: PropTypes.object, |
||||
setAmountToMax: PropTypes.func, |
||||
setMaxModeTo: PropTypes.func, |
||||
tokenBalance: PropTypes.string, |
||||
}; |
||||
|
||||
setMaxAmount () { |
||||
const { |
||||
balance, |
||||
gasTotal, |
||||
selectedToken, |
||||
setAmountToMax, |
||||
tokenBalance, |
||||
} = this.props |
||||
|
||||
setAmountToMax({ |
||||
balance, |
||||
gasTotal, |
||||
selectedToken, |
||||
tokenBalance, |
||||
}) |
||||
} |
||||
|
||||
render () { |
||||
const { setMaxModeTo, maxModeOn } = this.props |
||||
|
||||
return ( |
||||
<div |
||||
className="send-v2__amount-max" |
||||
onClick={(event) => { |
||||
event.preventDefault() |
||||
setMaxModeTo(true) |
||||
this.setMaxAmount() |
||||
}} |
||||
> |
||||
{!maxModeOn ? this.context.t('max') : ''} |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
} |
||||
|
||||
AmountMaxButton.contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
@ -0,0 +1,40 @@ |
||||
import { connect } from 'react-redux' |
||||
import { |
||||
getGasTotal, |
||||
getSelectedToken, |
||||
getSendFromBalance, |
||||
getTokenBalance, |
||||
} from '../../../send.selectors.js' |
||||
import { getMaxModeOn } from './amount-max-button.selectors.js' |
||||
import { calcMaxAmount } from './amount-max-button.utils.js' |
||||
import { |
||||
updateSendAmount, |
||||
setMaxModeTo, |
||||
} from '../../../../../actions' |
||||
import AmountMaxButton from './amount-max-button.component' |
||||
import { |
||||
updateSendErrors, |
||||
} from '../../../../../ducks/send.duck' |
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AmountMaxButton) |
||||
|
||||
function mapStateToProps (state) { |
||||
|
||||
return { |
||||
balance: getSendFromBalance(state), |
||||
gasTotal: getGasTotal(state), |
||||
maxModeOn: getMaxModeOn(state), |
||||
selectedToken: getSelectedToken(state), |
||||
tokenBalance: getTokenBalance(state), |
||||
} |
||||
} |
||||
|
||||
function mapDispatchToProps (dispatch) { |
||||
return { |
||||
setAmountToMax: maxAmountDataObject => { |
||||
dispatch(updateSendErrors({ amount: null })) |
||||
dispatch(updateSendAmount(calcMaxAmount(maxAmountDataObject))) |
||||
}, |
||||
setMaxModeTo: bool => dispatch(setMaxModeTo(bool)), |
||||
} |
||||
} |
@ -0,0 +1,9 @@ |
||||
const selectors = { |
||||
getMaxModeOn, |
||||
} |
||||
|
||||
module.exports = selectors |
||||
|
||||
function getMaxModeOn (state) { |
||||
return state.metamask.send.maxModeOn |
||||
} |
@ -0,0 +1,22 @@ |
||||
const { |
||||
multiplyCurrencies, |
||||
subtractCurrencies, |
||||
} = require('../../../../../conversion-util') |
||||
const ethUtil = require('ethereumjs-util') |
||||
|
||||
function calcMaxAmount ({ balance, gasTotal, selectedToken, tokenBalance }) { |
||||
const { decimals } = selectedToken || {} |
||||
const multiplier = Math.pow(10, Number(decimals || 0)) |
||||
|
||||
return selectedToken |
||||
? multiplyCurrencies(tokenBalance, multiplier, {toNumericBase: 'hex'}) |
||||
: subtractCurrencies( |
||||
ethUtil.addHexPrefix(balance), |
||||
ethUtil.addHexPrefix(gasTotal), |
||||
{ toNumericBase: 'hex' } |
||||
) |
||||
} |
||||
|
||||
module.exports = { |
||||
calcMaxAmount, |
||||
} |
@ -0,0 +1 @@ |
||||
export { default } from './amount-max-button.container' |
@ -0,0 +1,90 @@ |
||||
import React from 'react' |
||||
import assert from 'assert' |
||||
import { shallow } from 'enzyme' |
||||
import sinon from 'sinon' |
||||
import AmountMaxButton from '../amount-max-button.component.js' |
||||
|
||||
const propsMethodSpies = { |
||||
setAmountToMax: sinon.spy(), |
||||
setMaxModeTo: sinon.spy(), |
||||
} |
||||
|
||||
const MOCK_EVENT = { preventDefault: () => {} } |
||||
|
||||
sinon.spy(AmountMaxButton.prototype, 'setMaxAmount') |
||||
|
||||
describe('AmountMaxButton Component', function () { |
||||
let wrapper |
||||
let instance |
||||
|
||||
beforeEach(() => { |
||||
wrapper = shallow(<AmountMaxButton |
||||
balance={'mockBalance'} |
||||
gasTotal={'mockGasTotal'} |
||||
maxModeOn={false} |
||||
selectedToken={ { address: 'mockTokenAddress' } } |
||||
setAmountToMax={propsMethodSpies.setAmountToMax} |
||||
setMaxModeTo={propsMethodSpies.setMaxModeTo} |
||||
tokenBalance={'mockTokenBalance'} |
||||
/>, { context: { t: str => str + '_t' } }) |
||||
instance = wrapper.instance() |
||||
}) |
||||
|
||||
afterEach(() => { |
||||
propsMethodSpies.setAmountToMax.resetHistory() |
||||
propsMethodSpies.setMaxModeTo.resetHistory() |
||||
AmountMaxButton.prototype.setMaxAmount.resetHistory() |
||||
}) |
||||
|
||||
describe('setMaxAmount', () => { |
||||
|
||||
it('should call setAmountToMax with the correct params', () => { |
||||
assert.equal(propsMethodSpies.setAmountToMax.callCount, 0) |
||||
instance.setMaxAmount() |
||||
assert.equal(propsMethodSpies.setAmountToMax.callCount, 1) |
||||
assert.deepEqual( |
||||
propsMethodSpies.setAmountToMax.getCall(0).args, |
||||
[{ |
||||
balance: 'mockBalance', |
||||
gasTotal: 'mockGasTotal', |
||||
selectedToken: { address: 'mockTokenAddress' }, |
||||
tokenBalance: 'mockTokenBalance', |
||||
}] |
||||
) |
||||
}) |
||||
|
||||
}) |
||||
|
||||
describe('render', () => { |
||||
it('should render a div with a send-v2__amount-max class', () => { |
||||
assert.equal(wrapper.find('.send-v2__amount-max').length, 1) |
||||
assert(wrapper.find('.send-v2__amount-max').is('div')) |
||||
}) |
||||
|
||||
it('should call setMaxModeTo and setMaxAmount when the send-v2__amount-max div is clicked', () => { |
||||
const { |
||||
onClick, |
||||
} = wrapper.find('.send-v2__amount-max').props() |
||||
|
||||
assert.equal(AmountMaxButton.prototype.setMaxAmount.callCount, 0) |
||||
assert.equal(propsMethodSpies.setMaxModeTo.callCount, 0) |
||||
onClick(MOCK_EVENT) |
||||
assert.equal(AmountMaxButton.prototype.setMaxAmount.callCount, 1) |
||||
assert.equal(propsMethodSpies.setMaxModeTo.callCount, 1) |
||||
assert.deepEqual( |
||||
propsMethodSpies.setMaxModeTo.getCall(0).args, |
||||
[true] |
||||
) |
||||
}) |
||||
|
||||
it('should not render text when maxModeOn is true', () => { |
||||
wrapper.setProps({ maxModeOn: true }) |
||||
assert.equal(wrapper.find('.send-v2__amount-max').text(), '') |
||||
}) |
||||
|
||||
it('should render the expected text when maxModeOn is false', () => { |
||||
wrapper.setProps({ maxModeOn: false }) |
||||
assert.equal(wrapper.find('.send-v2__amount-max').text(), 'max_t') |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,91 @@ |
||||
import assert from 'assert' |
||||
import proxyquire from 'proxyquire' |
||||
import sinon from 'sinon' |
||||
|
||||
let mapStateToProps |
||||
let mapDispatchToProps |
||||
|
||||
const actionSpies = { |
||||
setMaxModeTo: sinon.spy(), |
||||
updateSendAmount: sinon.spy(), |
||||
} |
||||
const duckActionSpies = { |
||||
updateSendErrors: sinon.spy(), |
||||
} |
||||
|
||||
proxyquire('../amount-max-button.container.js', { |
||||
'react-redux': { |
||||
connect: (ms, md) => { |
||||
mapStateToProps = ms |
||||
mapDispatchToProps = md |
||||
return () => ({}) |
||||
}, |
||||
}, |
||||
'../../../send.selectors.js': { |
||||
getGasTotal: (s) => `mockGasTotal:${s}`, |
||||
getSelectedToken: (s) => `mockSelectedToken:${s}`, |
||||
getSendFromBalance: (s) => `mockBalance:${s}`, |
||||
getTokenBalance: (s) => `mockTokenBalance:${s}`, |
||||
}, |
||||
'./amount-max-button.selectors.js': { getMaxModeOn: (s) => `mockMaxModeOn:${s}` }, |
||||
'./amount-max-button.utils.js': { calcMaxAmount: (mockObj) => mockObj.val + 1 }, |
||||
'../../../../../actions': actionSpies, |
||||
'../../../../../ducks/send.duck': duckActionSpies, |
||||
}) |
||||
|
||||
describe('amount-max-button container', () => { |
||||
|
||||
describe('mapStateToProps()', () => { |
||||
|
||||
it('should map the correct properties to props', () => { |
||||
assert.deepEqual(mapStateToProps('mockState'), { |
||||
balance: 'mockBalance:mockState', |
||||
gasTotal: 'mockGasTotal:mockState', |
||||
maxModeOn: 'mockMaxModeOn:mockState', |
||||
selectedToken: 'mockSelectedToken:mockState', |
||||
tokenBalance: 'mockTokenBalance:mockState', |
||||
}) |
||||
}) |
||||
|
||||
}) |
||||
|
||||
describe('mapDispatchToProps()', () => { |
||||
let dispatchSpy |
||||
let mapDispatchToPropsObject |
||||
|
||||
beforeEach(() => { |
||||
dispatchSpy = sinon.spy() |
||||
mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) |
||||
}) |
||||
|
||||
describe('setAmountToMax()', () => { |
||||
it('should dispatch an action', () => { |
||||
mapDispatchToPropsObject.setAmountToMax({ val: 11, foo: 'bar' }) |
||||
assert(dispatchSpy.calledTwice) |
||||
assert(duckActionSpies.updateSendErrors.calledOnce) |
||||
assert.deepEqual( |
||||
duckActionSpies.updateSendErrors.getCall(0).args[0], |
||||
{ amount: null } |
||||
) |
||||
assert(actionSpies.updateSendAmount.calledOnce) |
||||
assert.equal( |
||||
actionSpies.updateSendAmount.getCall(0).args[0], |
||||
12 |
||||
) |
||||
}) |
||||
}) |
||||
|
||||
describe('setMaxModeTo()', () => { |
||||
it('should dispatch an action', () => { |
||||
mapDispatchToPropsObject.setMaxModeTo('mockVal') |
||||
assert(dispatchSpy.calledOnce) |
||||
assert.equal( |
||||
actionSpies.setMaxModeTo.getCall(0).args[0], |
||||
'mockVal' |
||||
) |
||||
}) |
||||
}) |
||||
|
||||
}) |
||||
|
||||
}) |
@ -0,0 +1,22 @@ |
||||
import assert from 'assert' |
||||
import { |
||||
getMaxModeOn, |
||||
} from '../amount-max-button.selectors.js' |
||||
|
||||
describe('amount-max-button selectors', () => { |
||||
|
||||
describe('getMaxModeOn()', () => { |
||||
it('should', () => { |
||||
const state = { |
||||
metamask: { |
||||
send: { |
||||
maxModeOn: null, |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
assert.equal(getMaxModeOn(state), null) |
||||
}) |
||||
}) |
||||
|
||||
}) |
@ -0,0 +1,27 @@ |
||||
import assert from 'assert' |
||||
import { |
||||
calcMaxAmount, |
||||
} from '../amount-max-button.utils.js' |
||||
|
||||
describe('amount-max-button utils', () => { |
||||
|
||||
describe('calcMaxAmount()', () => { |
||||
it('should calculate the correct amount when no selectedToken defined', () => { |
||||
assert.deepEqual(calcMaxAmount({ |
||||
balance: 'ffffff', |
||||
gasTotal: 'ff', |
||||
selectedToken: false, |
||||
}), 'ffff00') |
||||
}) |
||||
|
||||
it('should calculate the correct amount when a selectedToken is defined', () => { |
||||
assert.deepEqual(calcMaxAmount({ |
||||
selectedToken: { |
||||
decimals: 10, |
||||
}, |
||||
tokenBalance: 100, |
||||
}), 'e8d4a51000') |
||||
}) |
||||
}) |
||||
|
||||
}) |
@ -0,0 +1 @@ |
||||
export { default } from './send-amount-row.container' |
@ -0,0 +1,96 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import SendRowWrapper from '../send-row-wrapper/' |
||||
import AmountMaxButton from './amount-max-button/' |
||||
import CurrencyDisplay from '../../../send/currency-display' |
||||
|
||||
export default class SendAmountRow extends Component { |
||||
|
||||
static propTypes = { |
||||
amount: PropTypes.string, |
||||
amountConversionRate: PropTypes.oneOfType([ |
||||
PropTypes.string, |
||||
PropTypes.number, |
||||
]), |
||||
balance: PropTypes.string, |
||||
conversionRate: PropTypes.number, |
||||
convertedCurrency: PropTypes.string, |
||||
gasTotal: PropTypes.string, |
||||
inError: PropTypes.bool, |
||||
primaryCurrency: PropTypes.string, |
||||
selectedToken: PropTypes.object, |
||||
setMaxModeTo: PropTypes.func, |
||||
tokenBalance: PropTypes.string, |
||||
updateSendAmount: PropTypes.func, |
||||
updateSendAmountError: PropTypes.func, |
||||
} |
||||
|
||||
validateAmount (amount) { |
||||
const { |
||||
amountConversionRate, |
||||
balance, |
||||
conversionRate, |
||||
gasTotal, |
||||
primaryCurrency, |
||||
selectedToken, |
||||
tokenBalance, |
||||
updateSendAmountError, |
||||
} = this.props |
||||
|
||||
updateSendAmountError({ |
||||
amount, |
||||
amountConversionRate, |
||||
balance, |
||||
conversionRate, |
||||
gasTotal, |
||||
primaryCurrency, |
||||
selectedToken, |
||||
tokenBalance, |
||||
}) |
||||
} |
||||
|
||||
updateAmount (amount) { |
||||
const { updateSendAmount, setMaxModeTo } = this.props |
||||
|
||||
setMaxModeTo(false) |
||||
updateSendAmount(amount) |
||||
} |
||||
|
||||
render () { |
||||
const { |
||||
amount, |
||||
amountConversionRate, |
||||
convertedCurrency, |
||||
gasTotal, |
||||
inError, |
||||
primaryCurrency, |
||||
selectedToken, |
||||
} = this.props |
||||
|
||||
return ( |
||||
<SendRowWrapper |
||||
label={`${this.context.t('amount')}:`} |
||||
showError={inError} |
||||
errorType={'amount'} |
||||
> |
||||
{!inError && gasTotal && <AmountMaxButton />} |
||||
<CurrencyDisplay |
||||
conversionRate={amountConversionRate} |
||||
convertedCurrency={convertedCurrency} |
||||
onBlur={newAmount => this.updateAmount(newAmount)} |
||||
onChange={newAmount => this.validateAmount(newAmount)} |
||||
inError={inError} |
||||
primaryCurrency={primaryCurrency || 'ETH'} |
||||
selectedToken={selectedToken} |
||||
value={amount || '0x0'} |
||||
/> |
||||
</SendRowWrapper> |
||||
) |
||||
} |
||||
|
||||
} |
||||
|
||||
SendAmountRow.contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
@ -0,0 +1,51 @@ |
||||
import { connect } from 'react-redux' |
||||
import { |
||||
getAmountConversionRate, |
||||
getConversionRate, |
||||
getConvertedCurrency, |
||||
getGasTotal, |
||||
getPrimaryCurrency, |
||||
getSelectedToken, |
||||
getSendAmount, |
||||
getSendFromBalance, |
||||
getTokenBalance, |
||||
} from '../../send.selectors' |
||||
import { |
||||
sendAmountIsInError, |
||||
} from './send-amount-row.selectors' |
||||
import { getAmountErrorObject } from '../../send.utils' |
||||
import { |
||||
setMaxModeTo, |
||||
updateSendAmount, |
||||
} from '../../../../actions' |
||||
import { |
||||
updateSendErrors, |
||||
} from '../../../../ducks/send.duck' |
||||
import SendAmountRow from './send-amount-row.component' |
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SendAmountRow) |
||||
|
||||
function mapStateToProps (state) { |
||||
return { |
||||
amount: getSendAmount(state), |
||||
amountConversionRate: getAmountConversionRate(state), |
||||
balance: getSendFromBalance(state), |
||||
conversionRate: getConversionRate(state), |
||||
convertedCurrency: getConvertedCurrency(state), |
||||
gasTotal: getGasTotal(state), |
||||
inError: sendAmountIsInError(state), |
||||
primaryCurrency: getPrimaryCurrency(state), |
||||
selectedToken: getSelectedToken(state), |
||||
tokenBalance: getTokenBalance(state), |
||||
} |
||||
} |
||||
|
||||
function mapDispatchToProps (dispatch) { |
||||
return { |
||||
setMaxModeTo: bool => dispatch(setMaxModeTo(bool)), |
||||
updateSendAmount: newAmount => dispatch(updateSendAmount(newAmount)), |
||||
updateSendAmountError: (amountDataObject) => { |
||||
dispatch(updateSendErrors(getAmountErrorObject(amountDataObject))) |
||||
}, |
||||
} |
||||
} |
@ -0,0 +1,9 @@ |
||||
const selectors = { |
||||
sendAmountIsInError, |
||||
} |
||||
|
||||
module.exports = selectors |
||||
|
||||
function sendAmountIsInError (state) { |
||||
return Boolean(state.send.errors.amount) |
||||
} |
@ -0,0 +1,164 @@ |
||||
import React from 'react' |
||||
import assert from 'assert' |
||||
import { shallow } from 'enzyme' |
||||
import sinon from 'sinon' |
||||
import SendAmountRow from '../send-amount-row.component.js' |
||||
|
||||
import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component' |
||||
import AmountMaxButton from '../amount-max-button/amount-max-button.container' |
||||
import CurrencyDisplay from '../../../../send/currency-display' |
||||
|
||||
const propsMethodSpies = { |
||||
setMaxModeTo: sinon.spy(), |
||||
updateSendAmount: sinon.spy(), |
||||
updateSendAmountError: sinon.spy(), |
||||
} |
||||
|
||||
sinon.spy(SendAmountRow.prototype, 'updateAmount') |
||||
sinon.spy(SendAmountRow.prototype, 'validateAmount') |
||||
|
||||
describe('SendAmountRow Component', function () { |
||||
let wrapper |
||||
let instance |
||||
|
||||
beforeEach(() => { |
||||
wrapper = shallow(<SendAmountRow |
||||
amount={'mockAmount'} |
||||
amountConversionRate={'mockAmountConversionRate'} |
||||
balance={'mockBalance'} |
||||
conversionRate={7} |
||||
convertedCurrency={'mockConvertedCurrency'} |
||||
gasTotal={'mockGasTotal'} |
||||
inError={false} |
||||
primaryCurrency={'mockPrimaryCurrency'} |
||||
selectedToken={ { address: 'mockTokenAddress' } } |
||||
setMaxModeTo={propsMethodSpies.setMaxModeTo} |
||||
tokenBalance={'mockTokenBalance'} |
||||
updateSendAmount={propsMethodSpies.updateSendAmount} |
||||
updateSendAmountError={propsMethodSpies.updateSendAmountError} |
||||
/>, { context: { t: str => str + '_t' } }) |
||||
instance = wrapper.instance() |
||||
}) |
||||
|
||||
afterEach(() => { |
||||
propsMethodSpies.setMaxModeTo.resetHistory() |
||||
propsMethodSpies.updateSendAmount.resetHistory() |
||||
propsMethodSpies.updateSendAmountError.resetHistory() |
||||
SendAmountRow.prototype.validateAmount.resetHistory() |
||||
SendAmountRow.prototype.updateAmount.resetHistory() |
||||
}) |
||||
|
||||
describe('validateAmount', () => { |
||||
|
||||
it('should call updateSendAmountError with the correct params', () => { |
||||
assert.equal(propsMethodSpies.updateSendAmountError.callCount, 0) |
||||
instance.validateAmount('someAmount') |
||||
assert.equal(propsMethodSpies.updateSendAmountError.callCount, 1) |
||||
assert.deepEqual( |
||||
propsMethodSpies.updateSendAmountError.getCall(0).args, |
||||
[{ |
||||
amount: 'someAmount', |
||||
amountConversionRate: 'mockAmountConversionRate', |
||||
balance: 'mockBalance', |
||||
conversionRate: 7, |
||||
gasTotal: 'mockGasTotal', |
||||
primaryCurrency: 'mockPrimaryCurrency', |
||||
selectedToken: { address: 'mockTokenAddress' }, |
||||
tokenBalance: 'mockTokenBalance', |
||||
}] |
||||
) |
||||
}) |
||||
|
||||
}) |
||||
|
||||
describe('updateAmount', () => { |
||||
|
||||
it('should call setMaxModeTo', () => { |
||||
assert.equal(propsMethodSpies.setMaxModeTo.callCount, 0) |
||||
instance.updateAmount('someAmount') |
||||
assert.equal(propsMethodSpies.setMaxModeTo.callCount, 1) |
||||
assert.deepEqual( |
||||
propsMethodSpies.setMaxModeTo.getCall(0).args, |
||||
[false] |
||||
) |
||||
}) |
||||
|
||||
it('should call updateSendAmount', () => { |
||||
assert.equal(propsMethodSpies.updateSendAmount.callCount, 0) |
||||
instance.updateAmount('someAmount') |
||||
assert.equal(propsMethodSpies.updateSendAmount.callCount, 1) |
||||
assert.deepEqual( |
||||
propsMethodSpies.updateSendAmount.getCall(0).args, |
||||
['someAmount'] |
||||
) |
||||
}) |
||||
|
||||
}) |
||||
|
||||
describe('render', () => { |
||||
it('should render a SendRowWrapper component', () => { |
||||
assert.equal(wrapper.find(SendRowWrapper).length, 1) |
||||
}) |
||||
|
||||
it('should pass the correct props to SendRowWrapper', () => { |
||||
const { |
||||
errorType, |
||||
label, |
||||
showError, |
||||
} = wrapper.find(SendRowWrapper).props() |
||||
|
||||
assert.equal(errorType, 'amount') |
||||
|
||||
assert.equal(label, 'amount_t:') |
||||
|
||||
assert.equal(showError, false) |
||||
}) |
||||
|
||||
it('should render an AmountMaxButton as the first child of the SendRowWrapper', () => { |
||||
assert(wrapper.find(SendRowWrapper).childAt(0).is(AmountMaxButton)) |
||||
}) |
||||
|
||||
it('should render a CurrencyDisplay as the second child of the SendRowWrapper', () => { |
||||
assert(wrapper.find(SendRowWrapper).childAt(1).is(CurrencyDisplay)) |
||||
}) |
||||
|
||||
it('should render the CurrencyDisplay with the correct props', () => { |
||||
const { |
||||
conversionRate, |
||||
convertedCurrency, |
||||
onBlur, |
||||
onChange, |
||||
inError, |
||||
primaryCurrency, |
||||
selectedToken, |
||||
value, |
||||
} = wrapper.find(SendRowWrapper).childAt(1).props() |
||||
assert.equal(conversionRate, 'mockAmountConversionRate') |
||||
assert.equal(convertedCurrency, 'mockConvertedCurrency') |
||||
assert.equal(inError, false) |
||||
assert.equal(primaryCurrency, 'mockPrimaryCurrency') |
||||
assert.deepEqual(selectedToken, { address: 'mockTokenAddress' }) |
||||
assert.equal(value, 'mockAmount') |
||||
assert.equal(SendAmountRow.prototype.updateAmount.callCount, 0) |
||||
onBlur('mockNewAmount') |
||||
assert.equal(SendAmountRow.prototype.updateAmount.callCount, 1) |
||||
assert.deepEqual( |
||||
SendAmountRow.prototype.updateAmount.getCall(0).args, |
||||
['mockNewAmount'] |
||||
) |
||||
assert.equal(SendAmountRow.prototype.validateAmount.callCount, 0) |
||||
onChange('mockNewAmount') |
||||
assert.equal(SendAmountRow.prototype.validateAmount.callCount, 1) |
||||
assert.deepEqual( |
||||
SendAmountRow.prototype.validateAmount.getCall(0).args, |
||||
['mockNewAmount'] |
||||
) |
||||
}) |
||||
|
||||
it('should pass the default primaryCurrency to the CurrencyDisplay if primaryCurrency is falsy', () => { |
||||
wrapper.setProps({ primaryCurrency: null }) |
||||
const { primaryCurrency } = wrapper.find(SendRowWrapper).childAt(1).props() |
||||
assert.equal(primaryCurrency, 'ETH') |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,109 @@ |
||||
import assert from 'assert' |
||||
import proxyquire from 'proxyquire' |
||||
import sinon from 'sinon' |
||||
|
||||
let mapStateToProps |
||||
let mapDispatchToProps |
||||
|
||||
const actionSpies = { |
||||
setMaxModeTo: sinon.spy(), |
||||
updateSendAmount: sinon.spy(), |
||||
} |
||||
const duckActionSpies = { |
||||
updateSendErrors: sinon.spy(), |
||||
} |
||||
|
||||
proxyquire('../send-amount-row.container.js', { |
||||
'react-redux': { |
||||
connect: (ms, md) => { |
||||
mapStateToProps = ms |
||||
mapDispatchToProps = md |
||||
return () => ({}) |
||||
}, |
||||
}, |
||||
'../../send.selectors': { |
||||
getAmountConversionRate: (s) => `mockAmountConversionRate:${s}`, |
||||
getConversionRate: (s) => `mockConversionRate:${s}`, |
||||
getConvertedCurrency: (s) => `mockConvertedCurrency:${s}`, |
||||
getGasTotal: (s) => `mockGasTotal:${s}`, |
||||
getPrimaryCurrency: (s) => `mockPrimaryCurrency:${s}`, |
||||
getSelectedToken: (s) => `mockSelectedToken:${s}`, |
||||
getSendAmount: (s) => `mockAmount:${s}`, |
||||
getSendFromBalance: (s) => `mockBalance:${s}`, |
||||
getTokenBalance: (s) => `mockTokenBalance:${s}`, |
||||
}, |
||||
'./send-amount-row.selectors': { sendAmountIsInError: (s) => `mockInError:${s}` }, |
||||
'../../send.utils': { getAmountErrorObject: (mockDataObject) => ({ ...mockDataObject, mockChange: true }) }, |
||||
'../../../../actions': actionSpies, |
||||
'../../../../ducks/send.duck': duckActionSpies, |
||||
}) |
||||
|
||||
describe('send-amount-row container', () => { |
||||
|
||||
describe('mapStateToProps()', () => { |
||||
|
||||
it('should map the correct properties to props', () => { |
||||
assert.deepEqual(mapStateToProps('mockState'), { |
||||
amount: 'mockAmount:mockState', |
||||
amountConversionRate: 'mockAmountConversionRate:mockState', |
||||
balance: 'mockBalance:mockState', |
||||
conversionRate: 'mockConversionRate:mockState', |
||||
convertedCurrency: 'mockConvertedCurrency:mockState', |
||||
gasTotal: 'mockGasTotal:mockState', |
||||
inError: 'mockInError:mockState', |
||||
primaryCurrency: 'mockPrimaryCurrency:mockState', |
||||
selectedToken: 'mockSelectedToken:mockState', |
||||
tokenBalance: 'mockTokenBalance:mockState', |
||||
}) |
||||
}) |
||||
|
||||
}) |
||||
|
||||
describe('mapDispatchToProps()', () => { |
||||
let dispatchSpy |
||||
let mapDispatchToPropsObject |
||||
|
||||
beforeEach(() => { |
||||
dispatchSpy = sinon.spy() |
||||
mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) |
||||
}) |
||||
|
||||
describe('setMaxModeTo()', () => { |
||||
it('should dispatch an action', () => { |
||||
mapDispatchToPropsObject.setMaxModeTo('mockBool') |
||||
assert(dispatchSpy.calledOnce) |
||||
assert(actionSpies.setMaxModeTo.calledOnce) |
||||
assert.equal( |
||||
actionSpies.setMaxModeTo.getCall(0).args[0], |
||||
'mockBool' |
||||
) |
||||
}) |
||||
}) |
||||
|
||||
describe('updateSendAmount()', () => { |
||||
it('should dispatch an action', () => { |
||||
mapDispatchToPropsObject.updateSendAmount('mockAmount') |
||||
assert(dispatchSpy.calledOnce) |
||||
assert(actionSpies.updateSendAmount.calledOnce) |
||||
assert.equal( |
||||
actionSpies.updateSendAmount.getCall(0).args[0], |
||||
'mockAmount' |
||||
) |
||||
}) |
||||
}) |
||||
|
||||
describe('updateSendAmountError()', () => { |
||||
it('should dispatch an action', () => { |
||||
mapDispatchToPropsObject.updateSendAmountError({ some: 'data' }) |
||||
assert(dispatchSpy.calledOnce) |
||||
assert(duckActionSpies.updateSendErrors.calledOnce) |
||||
assert.deepEqual( |
||||
duckActionSpies.updateSendErrors.getCall(0).args[0], |
||||
{ some: 'data', mockChange: true } |
||||
) |
||||
}) |
||||
}) |
||||
|
||||
}) |
||||
|
||||
}) |
@ -0,0 +1,34 @@ |
||||
import assert from 'assert' |
||||
import { |
||||
sendAmountIsInError, |
||||
} from '../send-amount-row.selectors.js' |
||||
|
||||
describe('send-amount-row selectors', () => { |
||||
|
||||
describe('sendAmountIsInError()', () => { |
||||
it('should return true if send.errors.amount is truthy', () => { |
||||
const state = { |
||||
send: { |
||||
errors: { |
||||
amount: 'abc', |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
assert.equal(sendAmountIsInError(state), true) |
||||
}) |
||||
|
||||
it('should return false if send.errors.amount is falsy', () => { |
||||
const state = { |
||||
send: { |
||||
errors: { |
||||
amount: null, |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
assert.equal(sendAmountIsInError(state), false) |
||||
}) |
||||
}) |
||||
|
||||
}) |
@ -0,0 +1,28 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import PageContainerContent from '../../page-container/page-container-content.component' |
||||
import SendAmountRow from './send-amount-row/' |
||||
import SendFromRow from './send-from-row/' |
||||
import SendGasRow from './send-gas-row/' |
||||
import SendToRow from './send-to-row/' |
||||
|
||||
export default class SendContent extends Component { |
||||
|
||||
static propTypes = { |
||||
updateGas: PropTypes.func, |
||||
}; |
||||
|
||||
render () { |
||||
return ( |
||||
<PageContainerContent> |
||||
<div className="send-v2__form"> |
||||
<SendFromRow /> |
||||
<SendToRow updateGas={(updateData) => this.props.updateGas(updateData)} /> |
||||
<SendAmountRow /> |
||||
<SendGasRow /> |
||||
</div> |
||||
</PageContainerContent> |
||||
) |
||||
} |
||||
|
||||
} |
@ -0,0 +1 @@ |
||||
export { default } from './send-dropdown-list.component' |
@ -0,0 +1,52 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import AccountListItem from '../../account-list-item/' |
||||
|
||||
export default class SendDropdownList extends Component { |
||||
|
||||
static propTypes = { |
||||
accounts: PropTypes.array, |
||||
closeDropdown: PropTypes.func, |
||||
onSelect: PropTypes.func, |
||||
activeAddress: PropTypes.string, |
||||
}; |
||||
|
||||
getListItemIcon (accountAddress, activeAddress) { |
||||
return accountAddress === activeAddress |
||||
? <i className={`fa fa-check fa-lg`} style={ { color: '#02c9b1' } }/> |
||||
: null |
||||
} |
||||
|
||||
render () { |
||||
const { |
||||
accounts, |
||||
closeDropdown, |
||||
onSelect, |
||||
activeAddress, |
||||
} = this.props |
||||
|
||||
return (<div> |
||||
<div |
||||
className="send-v2__from-dropdown__close-area" |
||||
onClick={() => closeDropdown()} |
||||
/> |
||||
<div className="send-v2__from-dropdown__list"> |
||||
{accounts.map((account, index) => <AccountListItem |
||||
account={account} |
||||
className="account-list-item__dropdown" |
||||
handleClick={() => { |
||||
onSelect(account) |
||||
closeDropdown() |
||||
}} |
||||
icon={this.getListItemIcon(account.address, activeAddress)} |
||||
key={`send-dropdown-account-#${index}`} |
||||
/>)} |
||||
</div> |
||||
</div>) |
||||
} |
||||
|
||||
} |
||||
|
||||
SendDropdownList.contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
@ -0,0 +1,105 @@ |
||||
import React from 'react' |
||||
import assert from 'assert' |
||||
import { shallow } from 'enzyme' |
||||
import sinon from 'sinon' |
||||
import SendDropdownList from '../send-dropdown-list.component.js' |
||||
|
||||
import AccountListItem from '../../../account-list-item/account-list-item.container' |
||||
|
||||
const propsMethodSpies = { |
||||
closeDropdown: sinon.spy(), |
||||
onSelect: sinon.spy(), |
||||
} |
||||
|
||||
sinon.spy(SendDropdownList.prototype, 'getListItemIcon') |
||||
|
||||
describe('SendDropdownList Component', function () { |
||||
let wrapper |
||||
|
||||
beforeEach(() => { |
||||
wrapper = shallow(<SendDropdownList |
||||
accounts={[ |
||||
{ address: 'mockAccount0' }, |
||||
{ address: 'mockAccount1' }, |
||||
{ address: 'mockAccount2' }, |
||||
]} |
||||
closeDropdown={propsMethodSpies.closeDropdown} |
||||
onSelect={propsMethodSpies.onSelect} |
||||
activeAddress={'mockAddress2'} |
||||
/>, { context: { t: str => str + '_t' } }) |
||||
}) |
||||
|
||||
afterEach(() => { |
||||
propsMethodSpies.closeDropdown.resetHistory() |
||||
propsMethodSpies.onSelect.resetHistory() |
||||
SendDropdownList.prototype.getListItemIcon.resetHistory() |
||||
}) |
||||
|
||||
describe('getListItemIcon', () => { |
||||
it('should return check icon if the passed addresses are the same', () => { |
||||
assert.deepEqual( |
||||
wrapper.instance().getListItemIcon('mockAccount0', 'mockAccount0'), |
||||
<i className={`fa fa-check fa-lg`} style={ { color: '#02c9b1' } }/> |
||||
) |
||||
}) |
||||
|
||||
it('should return null if the passed addresses are different', () => { |
||||
assert.equal( |
||||
wrapper.instance().getListItemIcon('mockAccount0', 'mockAccount1'), |
||||
null |
||||
) |
||||
}) |
||||
}) |
||||
|
||||
describe('render', () => { |
||||
it('should render a single div with two children', () => { |
||||
assert(wrapper.is('div')) |
||||
assert.equal(wrapper.children().length, 2) |
||||
}) |
||||
|
||||
it('should render the children with the correct classes', () => { |
||||
assert(wrapper.childAt(0).hasClass('send-v2__from-dropdown__close-area')) |
||||
assert(wrapper.childAt(1).hasClass('send-v2__from-dropdown__list')) |
||||
}) |
||||
|
||||
it('should call closeDropdown onClick of the send-v2__from-dropdown__close-area', () => { |
||||
assert.equal(propsMethodSpies.closeDropdown.callCount, 0) |
||||
wrapper.childAt(0).props().onClick() |
||||
assert.equal(propsMethodSpies.closeDropdown.callCount, 1) |
||||
}) |
||||
|
||||
it('should render an AccountListItem for each item in accounts', () => { |
||||
assert.equal(wrapper.childAt(1).children().length, 3) |
||||
assert(wrapper.childAt(1).children().every(AccountListItem)) |
||||
}) |
||||
|
||||
it('should pass the correct props to the AccountListItem', () => { |
||||
wrapper.childAt(1).children().forEach((accountListItem, index) => { |
||||
const { |
||||
account, |
||||
className, |
||||
handleClick, |
||||
} = accountListItem.props() |
||||
assert.deepEqual(account, { address: 'mockAccount' + index }) |
||||
assert.equal(className, 'account-list-item__dropdown') |
||||
assert.equal(propsMethodSpies.onSelect.callCount, 0) |
||||
handleClick() |
||||
assert.equal(propsMethodSpies.onSelect.callCount, 1) |
||||
assert.deepEqual(propsMethodSpies.onSelect.getCall(0).args[0], { address: 'mockAccount' + index }) |
||||
propsMethodSpies.onSelect.resetHistory() |
||||
propsMethodSpies.closeDropdown.resetHistory() |
||||
assert.equal(propsMethodSpies.closeDropdown.callCount, 0) |
||||
handleClick() |
||||
assert.equal(propsMethodSpies.closeDropdown.callCount, 1) |
||||
propsMethodSpies.onSelect.resetHistory() |
||||
propsMethodSpies.closeDropdown.resetHistory() |
||||
}) |
||||
}) |
||||
|
||||
it('should call this.getListItemIcon for each AccountListItem', () => { |
||||
assert.equal(SendDropdownList.prototype.getListItemIcon.callCount, 3) |
||||
const getListItemIconCalls = SendDropdownList.prototype.getListItemIcon.getCalls() |
||||
assert(getListItemIconCalls.every(({ args }, index) => args[0] === 'mockAccount' + index)) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,46 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import AccountListItem from '../../../account-list-item/' |
||||
import SendDropdownList from '../../send-dropdown-list/' |
||||
|
||||
export default class FromDropdown extends Component { |
||||
|
||||
static propTypes = { |
||||
accounts: PropTypes.array, |
||||
closeDropdown: PropTypes.func, |
||||
dropdownOpen: PropTypes.bool, |
||||
onSelect: PropTypes.func, |
||||
openDropdown: PropTypes.func, |
||||
selectedAccount: PropTypes.object, |
||||
}; |
||||
|
||||
render () { |
||||
const { |
||||
accounts, |
||||
closeDropdown, |
||||
dropdownOpen, |
||||
openDropdown, |
||||
selectedAccount, |
||||
onSelect, |
||||
} = this.props |
||||
|
||||
return <div className="send-v2__from-dropdown"> |
||||
<AccountListItem |
||||
account={selectedAccount} |
||||
handleClick={openDropdown} |
||||
icon={<i className={`fa fa-caret-down fa-lg`} style={ { color: '#dedede' } }/>} |
||||
/> |
||||
{dropdownOpen && <SendDropdownList |
||||
accounts={accounts} |
||||
closeDropdown={closeDropdown} |
||||
onSelect={onSelect} |
||||
activeAddress={selectedAccount.address} |
||||
/>} |
||||
</div> |
||||
} |
||||
|
||||
} |
||||
|
||||
FromDropdown.contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue