commit
c2f97717c0
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 670 B |
@ -0,0 +1,34 @@ |
||||
module.exports = setupFetchDebugging |
||||
|
||||
//
|
||||
// This is a utility to help resolve cases where `window.fetch` throws a
|
||||
// `TypeError: Failed to Fetch` without any stack or context for the request
|
||||
// https://github.com/getsentry/sentry-javascript/pull/1293
|
||||
//
|
||||
|
||||
function setupFetchDebugging() { |
||||
if (!global.fetch) return |
||||
const originalFetch = global.fetch |
||||
|
||||
global.fetch = wrappedFetch |
||||
|
||||
async function wrappedFetch(...args) { |
||||
const initialStack = getCurrentStack() |
||||
try { |
||||
return await originalFetch.call(window, ...args) |
||||
} catch (err) { |
||||
console.warn('FetchDebugger - fetch encountered an Error', err) |
||||
console.warn('FetchDebugger - overriding stack to point of original call') |
||||
err.stack = initialStack |
||||
throw err |
||||
} |
||||
} |
||||
} |
||||
|
||||
function getCurrentStack() { |
||||
try { |
||||
throw new Error('Fake error for generating stack trace') |
||||
} catch (err) { |
||||
return err.stack |
||||
} |
||||
} |
@ -1,140 +0,0 @@ |
||||
const reactTriggerChange = require('react-trigger-change') |
||||
const { |
||||
timeout, |
||||
queryAsync, |
||||
findAsync, |
||||
} = require('../../lib/util') |
||||
|
||||
QUnit.module('Add token flow') |
||||
|
||||
QUnit.test('successful add token flow', (assert) => { |
||||
const done = assert.async() |
||||
runAddTokenFlowTest(assert) |
||||
.then(done) |
||||
.catch(err => { |
||||
assert.notOk(err, `Error was thrown: ${err.stack}`) |
||||
done() |
||||
}) |
||||
}) |
||||
|
||||
async function runAddTokenFlowTest (assert, done) { |
||||
const selectState = await queryAsync($, 'select') |
||||
selectState.val('add token') |
||||
reactTriggerChange(selectState[0]) |
||||
|
||||
// Used to set values on TextField input component
|
||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor( |
||||
window.HTMLInputElement.prototype, 'value' |
||||
).set |
||||
|
||||
// Check that no tokens have been added
|
||||
assert.ok($('.token-list-item').length === 0, 'no tokens added') |
||||
|
||||
// Go to Add Token screen
|
||||
let addTokenButton = await queryAsync($, 'button.btn-primary.wallet-view__add-token-button') |
||||
assert.ok(addTokenButton[0], 'add token button present') |
||||
addTokenButton[0].click() |
||||
|
||||
// Verify Add Token screen
|
||||
let addTokenWrapper = await queryAsync($, '.page-container') |
||||
assert.ok(addTokenWrapper[0], 'add token wrapper renders') |
||||
|
||||
let addTokenTitle = await queryAsync($, '.page-container__title') |
||||
assert.equal(addTokenTitle[0].textContent, 'Add Tokens', 'add token title is correct') |
||||
|
||||
// Cancel Add Token
|
||||
const cancelAddTokenButton = await queryAsync($, 'button.btn-default.btn--large.page-container__footer-button') |
||||
assert.ok(cancelAddTokenButton[0], 'cancel add token button present') |
||||
cancelAddTokenButton.click() |
||||
|
||||
assert.ok($('.wallet-view')[0], 'cancelled and returned to account detail wallet view') |
||||
|
||||
// Return to Add Token Screen
|
||||
addTokenButton = await queryAsync($, 'button.btn-primary.wallet-view__add-token-button') |
||||
assert.ok(addTokenButton[0], 'add token button present') |
||||
addTokenButton[0].click() |
||||
|
||||
// Verify Add Token Screen
|
||||
addTokenWrapper = await queryAsync($, '.page-container') |
||||
addTokenTitle = await queryAsync($, '.page-container__title') |
||||
assert.ok(addTokenWrapper[0], 'add token wrapper renders') |
||||
assert.equal(addTokenTitle[0].textContent, 'Add Tokens', 'add token title is correct') |
||||
|
||||
// Search for token
|
||||
const searchInput = (await findAsync(addTokenWrapper, '#search-tokens'))[0] |
||||
searchInput.focus() |
||||
await timeout(1000) |
||||
nativeInputValueSetter.call(searchInput, 'a') |
||||
searchInput.dispatchEvent(new Event('input', { bubbles: true})) |
||||
|
||||
// Click token to add
|
||||
const tokenWrapper = await queryAsync($, 'div.token-list__token') |
||||
assert.ok(tokenWrapper[0], 'token found') |
||||
const tokenImageProp = tokenWrapper.find('.token-list__token-icon').css('background-image') |
||||
const tokenImageUrl = tokenImageProp.slice(5, -2) |
||||
tokenWrapper[0].click() |
||||
|
||||
// Click Next button
|
||||
const nextButton = await queryAsync($, 'button.btn-primary.btn--large') |
||||
assert.equal(nextButton[0].textContent, 'Next', 'next button rendered') |
||||
nextButton[0].click() |
||||
|
||||
// Confirm Add token
|
||||
const confirmAddToken = await queryAsync($, '.confirm-add-token') |
||||
assert.ok(confirmAddToken[0], 'confirm add token rendered') |
||||
assert.ok($('button.btn-primary.btn--large')[0], 'confirm add token button found') |
||||
$('button.btn-primary.btn--large')[0].click() |
||||
|
||||
// Verify added token image
|
||||
let heroBalance = await queryAsync($, '.transaction-view-balance__balance-container') |
||||
assert.ok(heroBalance, 'rendered hero balance') |
||||
assert.ok(tokenImageUrl.indexOf(heroBalance.find('img').attr('src')) > -1, 'token added') |
||||
|
||||
// Return to Add Token Screen
|
||||
addTokenButton = await queryAsync($, 'button.btn-primary.wallet-view__add-token-button') |
||||
assert.ok(addTokenButton[0], 'add token button present') |
||||
addTokenButton[0].click() |
||||
|
||||
addTokenWrapper = await queryAsync($, '.page-container') |
||||
const addTokenTabs = await queryAsync($, '.page-container__tab') |
||||
assert.equal(addTokenTabs.length, 2, 'expected number of tabs') |
||||
assert.equal(addTokenTabs[1].textContent, 'Custom Token', 'Custom Token tab present') |
||||
assert.ok(addTokenTabs[1], 'add custom token tab present') |
||||
addTokenTabs[1].click() |
||||
await timeout(1000) |
||||
|
||||
// Input token contract address
|
||||
const customInput = (await findAsync(addTokenWrapper, '#custom-address'))[0] |
||||
customInput.focus() |
||||
await timeout(1000) |
||||
nativeInputValueSetter.call(customInput, '0x177af043D3A1Aed7cc5f2397C70248Fc6cDC056c') |
||||
customInput.dispatchEvent(new Event('input', { bubbles: true})) |
||||
|
||||
|
||||
// Click Next button
|
||||
// nextButton = await queryAsync($, 'button.btn-primary--lg')
|
||||
// assert.equal(nextButton[0].textContent, 'Next', 'next button rendered')
|
||||
// nextButton[0].click()
|
||||
|
||||
// // Verify symbol length error since contract address won't return symbol
|
||||
const errorMessage = await queryAsync($, '#custom-symbol-helper-text') |
||||
assert.ok(errorMessage[0], 'error rendered') |
||||
|
||||
$('button.btn-default.btn--large')[0].click() |
||||
|
||||
// await timeout(100000)
|
||||
|
||||
// Confirm Add token
|
||||
// assert.equal(
|
||||
// $('.page-container__subtitle')[0].textContent,
|
||||
// 'Would you like to add these tokens?',
|
||||
// 'confirm add token rendered'
|
||||
// )
|
||||
// assert.ok($('button.btn-primary--lg')[0], 'confirm add token button found')
|
||||
// $('button.btn-primary--lg')[0].click()
|
||||
|
||||
// Verify added token image
|
||||
heroBalance = await queryAsync($, '.transaction-view-balance__balance-container') |
||||
assert.ok(heroBalance, 'rendered hero balance') |
||||
assert.ok(heroBalance.find('.identicon')[0], 'token added') |
||||
} |
@ -1,44 +0,0 @@ |
||||
const assert = require('assert') |
||||
const h = require('react-hyperscript') |
||||
const { createMockStore } = require('redux-test-utils') |
||||
const { shallowWithStore } = require('../../lib/render-helpers') |
||||
const BalanceComponent = require('../../../ui/app/components/balance-component') |
||||
const mockState = { |
||||
metamask: { |
||||
accounts: { abc: {} }, |
||||
network: 1, |
||||
selectedAddress: 'abc', |
||||
}, |
||||
} |
||||
|
||||
describe('BalanceComponent', function () { |
||||
let balanceComponent |
||||
let store |
||||
let component |
||||
beforeEach(function () { |
||||
store = createMockStore(mockState) |
||||
component = shallowWithStore(h(BalanceComponent), store) |
||||
balanceComponent = component.dive() |
||||
}) |
||||
|
||||
it('shows token balance and convert to fiat value based on conversion rate', function () { |
||||
const formattedBalance = '1.23 ETH' |
||||
|
||||
const tokenBalance = balanceComponent.instance().getTokenBalance(formattedBalance, false) |
||||
const fiatDisplayNumber = balanceComponent.instance().getFiatDisplayNumber(formattedBalance, 2) |
||||
|
||||
assert.equal('1.23 ETH', tokenBalance) |
||||
assert.equal(2.46, fiatDisplayNumber) |
||||
}) |
||||
|
||||
it('shows only the token balance when conversion rate is not available', function () { |
||||
const formattedBalance = '1.23 ETH' |
||||
|
||||
const tokenBalance = balanceComponent.instance().getTokenBalance(formattedBalance, false) |
||||
const fiatDisplayNumber = balanceComponent.instance().getFiatDisplayNumber(formattedBalance, 0) |
||||
|
||||
assert.equal('1.23 ETH', tokenBalance) |
||||
assert.equal('N/A', fiatDisplayNumber) |
||||
}) |
||||
|
||||
}) |
@ -0,0 +1,34 @@ |
||||
import PropTypes from 'prop-types' |
||||
import React, {PureComponent} from 'react' |
||||
|
||||
export default class AddTokenButton extends PureComponent { |
||||
static contextTypes = { |
||||
t: PropTypes.func.isRequired, |
||||
} |
||||
|
||||
static defaultProps = { |
||||
onClick: () => {}, |
||||
} |
||||
|
||||
static propTypes = { |
||||
onClick: PropTypes.func, |
||||
} |
||||
|
||||
render () { |
||||
const { t } = this.context |
||||
const { onClick } = this.props |
||||
|
||||
return ( |
||||
<div className="add-token-button"> |
||||
<h1 className="add-token-button__help-header">{t('missingYourTokens')}</h1> |
||||
<p className="add-token-button__help-desc">{t('clickToAdd', [t('addToken')])}</p> |
||||
<div |
||||
className="add-token-button__button" |
||||
onClick={onClick} |
||||
> |
||||
{t('addToken')} |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1 @@ |
||||
export { default } from './add-token-button.component' |
@ -0,0 +1,26 @@ |
||||
.add-token-button { |
||||
display: flex; |
||||
flex-direction: column; |
||||
color: lighten($scorpion, 25%); |
||||
width: 185px; |
||||
margin: 36px auto; |
||||
text-align: center; |
||||
|
||||
&__help-header { |
||||
font-weight: bold; |
||||
font-size: 1rem; |
||||
} |
||||
|
||||
&__help-desc { |
||||
font-size: 0.75rem; |
||||
margin-top: 1rem; |
||||
} |
||||
|
||||
&__button { |
||||
font-size: 0.75rem; |
||||
margin: 1rem; |
||||
text-transform: uppercase; |
||||
color: $curious-blue; |
||||
cursor: pointer; |
||||
} |
||||
} |
@ -0,0 +1,10 @@ |
||||
.currency-display-component { |
||||
display: flex; |
||||
align-items: center; |
||||
|
||||
&__text { |
||||
white-space: nowrap; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
} |
||||
} |
@ -0,0 +1,120 @@ |
||||
import React, { PureComponent } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import UnitInput from '../unit-input' |
||||
import CurrencyDisplay from '../currency-display' |
||||
import { getValueFromWeiHex, getWeiHexFromDecimalValue } from '../../helpers/conversions.util' |
||||
import { ETH } from '../../constants/common' |
||||
|
||||
/** |
||||
* Component that allows user to enter currency values as a number, and props receive a converted |
||||
* hex value in WEI. props.value, used as a default or forced value, should be a hex value, which |
||||
* gets converted into a decimal value depending on the currency (ETH or Fiat). |
||||
*/ |
||||
export default class CurrencyInput extends PureComponent { |
||||
static propTypes = { |
||||
conversionRate: PropTypes.number, |
||||
currentCurrency: PropTypes.string, |
||||
onChange: PropTypes.func, |
||||
onBlur: PropTypes.func, |
||||
suffix: PropTypes.string, |
||||
useFiat: PropTypes.bool, |
||||
value: PropTypes.string, |
||||
} |
||||
|
||||
constructor (props) { |
||||
super(props) |
||||
|
||||
const { value: hexValue } = props |
||||
const decimalValue = hexValue ? this.getDecimalValue(props) : 0 |
||||
|
||||
this.state = { |
||||
decimalValue, |
||||
hexValue, |
||||
} |
||||
} |
||||
|
||||
componentDidUpdate (prevProps) { |
||||
const { value: prevPropsHexValue } = prevProps |
||||
const { value: propsHexValue } = this.props |
||||
const { hexValue: stateHexValue } = this.state |
||||
|
||||
if (prevPropsHexValue !== propsHexValue && propsHexValue !== stateHexValue) { |
||||
const decimalValue = this.getDecimalValue(this.props) |
||||
this.setState({ hexValue: propsHexValue, decimalValue }) |
||||
} |
||||
} |
||||
|
||||
getDecimalValue (props) { |
||||
const { value: hexValue, useFiat, currentCurrency, conversionRate } = props |
||||
const decimalValueString = useFiat |
||||
? getValueFromWeiHex({ |
||||
value: hexValue, toCurrency: currentCurrency, conversionRate, numberOfDecimals: 2, |
||||
}) |
||||
: getValueFromWeiHex({ |
||||
value: hexValue, toCurrency: ETH, numberOfDecimals: 6, |
||||
}) |
||||
|
||||
return Number(decimalValueString) || 0 |
||||
} |
||||
|
||||
handleChange = decimalValue => { |
||||
const { useFiat, currentCurrency: fromCurrency, conversionRate, onChange } = this.props |
||||
|
||||
const hexValue = useFiat |
||||
? getWeiHexFromDecimalValue({ |
||||
value: decimalValue, fromCurrency, conversionRate, invertConversionRate: true, |
||||
}) |
||||
: getWeiHexFromDecimalValue({ |
||||
value: decimalValue, fromCurrency: ETH, fromDenomination: ETH, conversionRate, |
||||
}) |
||||
|
||||
this.setState({ hexValue, decimalValue }) |
||||
onChange(hexValue) |
||||
} |
||||
|
||||
handleBlur = () => { |
||||
this.props.onBlur(this.state.hexValue) |
||||
} |
||||
|
||||
renderConversionComponent () { |
||||
const { useFiat, currentCurrency } = this.props |
||||
const { hexValue } = this.state |
||||
let currency, numberOfDecimals |
||||
|
||||
if (useFiat) { |
||||
// Display ETH
|
||||
currency = ETH |
||||
numberOfDecimals = 6 |
||||
} else { |
||||
// Display Fiat
|
||||
currency = currentCurrency |
||||
numberOfDecimals = 2 |
||||
} |
||||
|
||||
return ( |
||||
<CurrencyDisplay |
||||
className="currency-input__conversion-component" |
||||
currency={currency} |
||||
value={hexValue} |
||||
numberOfDecimals={numberOfDecimals} |
||||
/> |
||||
) |
||||
} |
||||
|
||||
render () { |
||||
const { suffix, ...restProps } = this.props |
||||
const { decimalValue } = this.state |
||||
|
||||
return ( |
||||
<UnitInput |
||||
{...restProps} |
||||
suffix={suffix} |
||||
onChange={this.handleChange} |
||||
onBlur={this.handleBlur} |
||||
value={decimalValue} |
||||
> |
||||
{ this.renderConversionComponent() } |
||||
</UnitInput> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,27 @@ |
||||
import { connect } from 'react-redux' |
||||
import CurrencyInput from './currency-input.component' |
||||
import { ETH } from '../../constants/common' |
||||
|
||||
const mapStateToProps = state => { |
||||
const { metamask: { currentCurrency, conversionRate } } = state |
||||
|
||||
return { |
||||
currentCurrency, |
||||
conversionRate, |
||||
} |
||||
} |
||||
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => { |
||||
const { currentCurrency } = stateProps |
||||
const { useFiat } = ownProps |
||||
const suffix = useFiat ? currentCurrency.toUpperCase() : ETH |
||||
|
||||
return { |
||||
...stateProps, |
||||
...dispatchProps, |
||||
...ownProps, |
||||
suffix, |
||||
} |
||||
} |
||||
|
||||
export default connect(mapStateToProps, null, mergeProps)(CurrencyInput) |
@ -0,0 +1 @@ |
||||
export { default } from './currency-input.container' |
@ -0,0 +1,7 @@ |
||||
.currency-input { |
||||
&__conversion-component { |
||||
font-size: 12px; |
||||
line-height: 12px; |
||||
padding-left: 1px; |
||||
} |
||||
} |
@ -0,0 +1,239 @@ |
||||
import React from 'react' |
||||
import assert from 'assert' |
||||
import { shallow, mount } from 'enzyme' |
||||
import sinon from 'sinon' |
||||
import { Provider } from 'react-redux' |
||||
import configureMockStore from 'redux-mock-store' |
||||
import CurrencyInput from '../currency-input.component' |
||||
import UnitInput from '../../unit-input' |
||||
import CurrencyDisplay from '../../currency-display' |
||||
|
||||
describe('CurrencyInput Component', () => { |
||||
describe('rendering', () => { |
||||
it('should render properly without a suffix', () => { |
||||
const wrapper = shallow( |
||||
<CurrencyInput /> |
||||
) |
||||
|
||||
assert.ok(wrapper) |
||||
assert.equal(wrapper.find(UnitInput).length, 1) |
||||
}) |
||||
|
||||
it('should render properly with a suffix', () => { |
||||
const mockStore = { |
||||
metamask: { |
||||
currentCurrency: 'usd', |
||||
conversionRate: 231.06, |
||||
}, |
||||
} |
||||
const store = configureMockStore()(mockStore) |
||||
|
||||
const wrapper = mount( |
||||
<Provider store={store}> |
||||
<CurrencyInput |
||||
suffix="ETH" |
||||
/> |
||||
</Provider> |
||||
) |
||||
|
||||
assert.ok(wrapper) |
||||
assert.equal(wrapper.find('.unit-input__suffix').length, 1) |
||||
assert.equal(wrapper.find('.unit-input__suffix').text(), 'ETH') |
||||
assert.equal(wrapper.find(CurrencyDisplay).length, 1) |
||||
}) |
||||
|
||||
it('should render properly with an ETH value', () => { |
||||
const mockStore = { |
||||
metamask: { |
||||
currentCurrency: 'usd', |
||||
conversionRate: 231.06, |
||||
}, |
||||
} |
||||
const store = configureMockStore()(mockStore) |
||||
|
||||
const wrapper = mount( |
||||
<Provider store={store}> |
||||
<CurrencyInput |
||||
value="de0b6b3a7640000" |
||||
suffix="ETH" |
||||
currentCurrency="usd" |
||||
conversionRate={231.06} |
||||
/> |
||||
</Provider> |
||||
) |
||||
|
||||
assert.ok(wrapper) |
||||
const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance() |
||||
assert.equal(currencyInputInstance.state.decimalValue, 1) |
||||
assert.equal(currencyInputInstance.state.hexValue, 'de0b6b3a7640000') |
||||
assert.equal(wrapper.find('.unit-input__suffix').length, 1) |
||||
assert.equal(wrapper.find('.unit-input__suffix').text(), 'ETH') |
||||
assert.equal(wrapper.find('.unit-input__input').props().value, '1') |
||||
assert.equal(wrapper.find('.currency-display-component').text(), '$231.06 USD') |
||||
}) |
||||
|
||||
it('should render properly with a fiat value', () => { |
||||
const mockStore = { |
||||
metamask: { |
||||
currentCurrency: 'usd', |
||||
conversionRate: 231.06, |
||||
}, |
||||
} |
||||
const store = configureMockStore()(mockStore) |
||||
|
||||
const wrapper = mount( |
||||
<Provider store={store}> |
||||
<CurrencyInput |
||||
value="f602f2234d0ea" |
||||
suffix="USD" |
||||
useFiat |
||||
currentCurrency="usd" |
||||
conversionRate={231.06} |
||||
/> |
||||
</Provider> |
||||
) |
||||
|
||||
assert.ok(wrapper) |
||||
const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance() |
||||
assert.equal(currencyInputInstance.state.decimalValue, 1) |
||||
assert.equal(currencyInputInstance.state.hexValue, 'f602f2234d0ea') |
||||
assert.equal(wrapper.find('.unit-input__suffix').length, 1) |
||||
assert.equal(wrapper.find('.unit-input__suffix').text(), 'USD') |
||||
assert.equal(wrapper.find('.unit-input__input').props().value, '1') |
||||
assert.equal(wrapper.find('.currency-display-component').text(), '0.004328 ETH') |
||||
}) |
||||
}) |
||||
|
||||
describe('handling actions', () => { |
||||
const handleChangeSpy = sinon.spy() |
||||
const handleBlurSpy = sinon.spy() |
||||
|
||||
afterEach(() => { |
||||
handleChangeSpy.resetHistory() |
||||
handleBlurSpy.resetHistory() |
||||
}) |
||||
|
||||
it('should call onChange and onBlur on input changes with the hex value for ETH', () => { |
||||
const mockStore = { |
||||
metamask: { |
||||
currentCurrency: 'usd', |
||||
conversionRate: 231.06, |
||||
}, |
||||
} |
||||
const store = configureMockStore()(mockStore) |
||||
const wrapper = mount( |
||||
<Provider store={store}> |
||||
<CurrencyInput |
||||
onChange={handleChangeSpy} |
||||
onBlur={handleBlurSpy} |
||||
suffix="ETH" |
||||
currentCurrency="usd" |
||||
conversionRate={231.06} |
||||
/> |
||||
</Provider> |
||||
) |
||||
|
||||
assert.ok(wrapper) |
||||
assert.equal(handleChangeSpy.callCount, 0) |
||||
assert.equal(handleBlurSpy.callCount, 0) |
||||
|
||||
const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance() |
||||
assert.equal(currencyInputInstance.state.decimalValue, 0) |
||||
assert.equal(currencyInputInstance.state.hexValue, undefined) |
||||
assert.equal(wrapper.find('.currency-display-component').text(), '$0.00 USD') |
||||
const input = wrapper.find('input') |
||||
assert.equal(input.props().value, 0) |
||||
|
||||
input.simulate('change', { target: { value: 1 } }) |
||||
assert.equal(handleChangeSpy.callCount, 1) |
||||
assert.ok(handleChangeSpy.calledWith('de0b6b3a7640000')) |
||||
assert.equal(wrapper.find('.currency-display-component').text(), '$231.06 USD') |
||||
assert.equal(currencyInputInstance.state.decimalValue, 1) |
||||
assert.equal(currencyInputInstance.state.hexValue, 'de0b6b3a7640000') |
||||
|
||||
assert.equal(handleBlurSpy.callCount, 0) |
||||
input.simulate('blur') |
||||
assert.equal(handleBlurSpy.callCount, 1) |
||||
assert.ok(handleBlurSpy.calledWith('de0b6b3a7640000')) |
||||
}) |
||||
|
||||
it('should call onChange and onBlur on input changes with the hex value for fiat', () => { |
||||
const mockStore = { |
||||
metamask: { |
||||
currentCurrency: 'usd', |
||||
conversionRate: 231.06, |
||||
}, |
||||
} |
||||
const store = configureMockStore()(mockStore) |
||||
const wrapper = mount( |
||||
<Provider store={store}> |
||||
<CurrencyInput |
||||
onChange={handleChangeSpy} |
||||
onBlur={handleBlurSpy} |
||||
suffix="USD" |
||||
currentCurrency="usd" |
||||
conversionRate={231.06} |
||||
useFiat |
||||
/> |
||||
</Provider> |
||||
) |
||||
|
||||
assert.ok(wrapper) |
||||
assert.equal(handleChangeSpy.callCount, 0) |
||||
assert.equal(handleBlurSpy.callCount, 0) |
||||
|
||||
const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance() |
||||
assert.equal(currencyInputInstance.state.decimalValue, 0) |
||||
assert.equal(currencyInputInstance.state.hexValue, undefined) |
||||
assert.equal(wrapper.find('.currency-display-component').text(), '0 ETH') |
||||
const input = wrapper.find('input') |
||||
assert.equal(input.props().value, 0) |
||||
|
||||
input.simulate('change', { target: { value: 1 } }) |
||||
assert.equal(handleChangeSpy.callCount, 1) |
||||
assert.ok(handleChangeSpy.calledWith('f602f2234d0ea')) |
||||
assert.equal(wrapper.find('.currency-display-component').text(), '0.004328 ETH') |
||||
assert.equal(currencyInputInstance.state.decimalValue, 1) |
||||
assert.equal(currencyInputInstance.state.hexValue, 'f602f2234d0ea') |
||||
|
||||
assert.equal(handleBlurSpy.callCount, 0) |
||||
input.simulate('blur') |
||||
assert.equal(handleBlurSpy.callCount, 1) |
||||
assert.ok(handleBlurSpy.calledWith('f602f2234d0ea')) |
||||
}) |
||||
|
||||
it('should change the state and pass in a new decimalValue when props.value changes', () => { |
||||
const mockStore = { |
||||
metamask: { |
||||
currentCurrency: 'usd', |
||||
conversionRate: 231.06, |
||||
}, |
||||
} |
||||
const store = configureMockStore()(mockStore) |
||||
const wrapper = shallow( |
||||
<Provider store={store}> |
||||
<CurrencyInput |
||||
onChange={handleChangeSpy} |
||||
onBlur={handleBlurSpy} |
||||
suffix="USD" |
||||
currentCurrency="usd" |
||||
conversionRate={231.06} |
||||
useFiat |
||||
/> |
||||
</Provider> |
||||
) |
||||
|
||||
assert.ok(wrapper) |
||||
const currencyInputInstance = wrapper.find(CurrencyInput).dive() |
||||
assert.equal(currencyInputInstance.state('decimalValue'), 0) |
||||
assert.equal(currencyInputInstance.state('hexValue'), undefined) |
||||
assert.equal(currencyInputInstance.find(UnitInput).props().value, 0) |
||||
|
||||
currencyInputInstance.setProps({ value: '1ec05e43e72400' }) |
||||
currencyInputInstance.update() |
||||
assert.equal(currencyInputInstance.state('decimalValue'), 2) |
||||
assert.equal(currencyInputInstance.state('hexValue'), '1ec05e43e72400') |
||||
assert.equal(currencyInputInstance.find(UnitInput).props().value, 2) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,55 @@ |
||||
import assert from 'assert' |
||||
import proxyquire from 'proxyquire' |
||||
|
||||
let mapStateToProps, mergeProps |
||||
|
||||
proxyquire('../currency-input.container.js', { |
||||
'react-redux': { |
||||
connect: (ms, md, mp) => { |
||||
mapStateToProps = ms |
||||
mergeProps = mp |
||||
return () => ({}) |
||||
}, |
||||
}, |
||||
}) |
||||
|
||||
describe('CurrencyInput container', () => { |
||||
describe('mapStateToProps()', () => { |
||||
it('should return the correct props', () => { |
||||
const mockState = { |
||||
metamask: { |
||||
conversionRate: 280.45, |
||||
currentCurrency: 'usd', |
||||
}, |
||||
} |
||||
|
||||
assert.deepEqual(mapStateToProps(mockState), { |
||||
conversionRate: 280.45, |
||||
currentCurrency: 'usd', |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('mergeProps()', () => { |
||||
it('should return the correct props', () => { |
||||
const mockStateProps = { |
||||
conversionRate: 280.45, |
||||
currentCurrency: 'usd', |
||||
} |
||||
const mockDispatchProps = {} |
||||
|
||||
assert.deepEqual(mergeProps(mockStateProps, mockDispatchProps, { useFiat: true }), { |
||||
conversionRate: 280.45, |
||||
currentCurrency: 'usd', |
||||
useFiat: true, |
||||
suffix: 'USD', |
||||
}) |
||||
|
||||
assert.deepEqual(mergeProps(mockStateProps, mockDispatchProps, {}), { |
||||
conversionRate: 280.45, |
||||
currentCurrency: 'usd', |
||||
suffix: 'ETH', |
||||
}) |
||||
}) |
||||
}) |
||||
}) |
@ -1,186 +0,0 @@ |
||||
const Component = require('react').Component |
||||
const h = require('react-hyperscript') |
||||
const inherits = require('util').inherits |
||||
const { conversionUtil, multiplyCurrencies } = require('../../../conversion-util') |
||||
const { removeLeadingZeroes } = require('../send.utils') |
||||
const currencyFormatter = require('currency-formatter') |
||||
const currencies = require('currency-formatter/currencies') |
||||
const ethUtil = require('ethereumjs-util') |
||||
const PropTypes = require('prop-types') |
||||
|
||||
CurrencyDisplay.contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
module.exports = CurrencyDisplay |
||||
|
||||
inherits(CurrencyDisplay, Component) |
||||
function CurrencyDisplay () { |
||||
Component.call(this) |
||||
} |
||||
|
||||
function toHexWei (value) { |
||||
return conversionUtil(value, { |
||||
fromNumericBase: 'dec', |
||||
toNumericBase: 'hex', |
||||
toDenomination: 'WEI', |
||||
}) |
||||
} |
||||
|
||||
CurrencyDisplay.prototype.componentWillMount = function () { |
||||
this.setState({ |
||||
valueToRender: this.getValueToRender(this.props), |
||||
}) |
||||
} |
||||
|
||||
CurrencyDisplay.prototype.componentWillReceiveProps = function (nextProps) { |
||||
const currentValueToRender = this.getValueToRender(this.props) |
||||
const newValueToRender = this.getValueToRender(nextProps) |
||||
if (currentValueToRender !== newValueToRender) { |
||||
this.setState({ |
||||
valueToRender: newValueToRender, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
CurrencyDisplay.prototype.getAmount = function (value) { |
||||
const { selectedToken } = this.props |
||||
const { decimals } = selectedToken || {} |
||||
const multiplier = Math.pow(10, Number(decimals || 0)) |
||||
|
||||
const sendAmount = multiplyCurrencies(value || '0', multiplier, {toNumericBase: 'hex'}) |
||||
|
||||
return selectedToken |
||||
? sendAmount |
||||
: toHexWei(value) |
||||
} |
||||
|
||||
CurrencyDisplay.prototype.getValueToRender = function ({ selectedToken, conversionRate, value, readOnly }) { |
||||
if (value === '0x0') return readOnly ? '0' : '' |
||||
const { decimals, symbol } = selectedToken || {} |
||||
const multiplier = Math.pow(10, Number(decimals || 0)) |
||||
|
||||
return selectedToken |
||||
? conversionUtil(ethUtil.addHexPrefix(value), { |
||||
fromNumericBase: 'hex', |
||||
toNumericBase: 'dec', |
||||
toCurrency: symbol, |
||||
conversionRate: multiplier, |
||||
invertConversionRate: true, |
||||
}) |
||||
: conversionUtil(ethUtil.addHexPrefix(value), { |
||||
fromNumericBase: 'hex', |
||||
toNumericBase: 'dec', |
||||
fromDenomination: 'WEI', |
||||
numberOfDecimals: 9, |
||||
conversionRate, |
||||
}) |
||||
} |
||||
|
||||
CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValue) { |
||||
const { primaryCurrency, convertedCurrency, conversionRate } = this.props |
||||
|
||||
if (conversionRate === 0 || conversionRate === null || conversionRate === undefined) { |
||||
if (nonFormattedValue !== 0) { |
||||
return null |
||||
} |
||||
} |
||||
|
||||
let convertedValue = conversionUtil(nonFormattedValue, { |
||||
fromNumericBase: 'dec', |
||||
fromCurrency: primaryCurrency, |
||||
toCurrency: convertedCurrency, |
||||
numberOfDecimals: 2, |
||||
conversionRate, |
||||
}) |
||||
|
||||
convertedValue = Number(convertedValue).toFixed(2) |
||||
const upperCaseCurrencyCode = convertedCurrency.toUpperCase() |
||||
return currencies.find(currency => currency.code === upperCaseCurrencyCode) |
||||
? currencyFormatter.format(Number(convertedValue), { |
||||
code: upperCaseCurrencyCode, |
||||
}) |
||||
: convertedValue |
||||
} |
||||
|
||||
CurrencyDisplay.prototype.handleChange = function (newVal) { |
||||
this.setState({ valueToRender: removeLeadingZeroes(newVal) }) |
||||
this.props.onChange(this.getAmount(newVal)) |
||||
} |
||||
|
||||
CurrencyDisplay.prototype.getInputWidth = function (valueToRender, readOnly) { |
||||
const valueString = String(valueToRender) |
||||
const valueLength = valueString.length || 1 |
||||
const decimalPointDeficit = valueString.match(/\./) ? -0.5 : 0 |
||||
return (valueLength + decimalPointDeficit + 0.75) + 'ch' |
||||
} |
||||
|
||||
CurrencyDisplay.prototype.onlyRenderConversions = function (convertedValueToRender) { |
||||
const { |
||||
convertedBalanceClassName = 'currency-display__converted-value', |
||||
convertedCurrency, |
||||
} = this.props |
||||
return h('div', { |
||||
className: convertedBalanceClassName, |
||||
}, convertedValueToRender == null |
||||
? this.context.t('noConversionRateAvailable') |
||||
: `${convertedValueToRender} ${convertedCurrency.toUpperCase()}` |
||||
) |
||||
} |
||||
|
||||
CurrencyDisplay.prototype.render = function () { |
||||
const { |
||||
className = 'currency-display', |
||||
primaryBalanceClassName = 'currency-display__input', |
||||
primaryCurrency, |
||||
readOnly = false, |
||||
inError = false, |
||||
onBlur, |
||||
step, |
||||
} = this.props |
||||
const { valueToRender } = this.state |
||||
|
||||
const convertedValueToRender = this.getConvertedValueToRender(valueToRender) |
||||
|
||||
return h('div', { |
||||
className, |
||||
style: { |
||||
borderColor: inError ? 'red' : null, |
||||
}, |
||||
onClick: () => { |
||||
this.currencyInput && this.currencyInput.focus() |
||||
}, |
||||
}, [ |
||||
|
||||
h('div.currency-display__primary-row', [ |
||||
|
||||
h('div.currency-display__input-wrapper', [ |
||||
|
||||
h('input', { |
||||
className: primaryBalanceClassName, |
||||
value: `${valueToRender}`, |
||||
placeholder: '0', |
||||
type: 'number', |
||||
readOnly, |
||||
...(!readOnly ? { |
||||
onChange: e => this.handleChange(e.target.value), |
||||
onBlur: () => onBlur(this.getAmount(valueToRender)), |
||||
} : {}), |
||||
ref: input => { this.currencyInput = input }, |
||||
style: { |
||||
width: this.getInputWidth(valueToRender, readOnly), |
||||
}, |
||||
min: 0, |
||||
step, |
||||
}), |
||||
|
||||
h('span.currency-display__currency-symbol', primaryCurrency), |
||||
|
||||
]), |
||||
|
||||
]), this.onlyRenderConversions(convertedValueToRender), |
||||
|
||||
]) |
||||
|
||||
} |
||||
|
@ -1 +0,0 @@ |
||||
export { default } from './currency-display.js' |
@ -1,91 +0,0 @@ |
||||
import React from 'react' |
||||
import assert from 'assert' |
||||
import sinon from 'sinon' |
||||
import { shallow, mount } from 'enzyme' |
||||
import CurrencyDisplay from '../currency-display' |
||||
|
||||
describe('', () => { |
||||
|
||||
const token = { |
||||
address: '0xTest', |
||||
symbol: 'TST', |
||||
decimals: '13', |
||||
} |
||||
|
||||
it('retuns ETH value for wei value', () => { |
||||
const wrapper = mount(<CurrencyDisplay />, {context: {t: str => str + '_t'}}) |
||||
|
||||
const value = wrapper.instance().getValueToRender({ |
||||
// 1000000000000000000
|
||||
value: 'DE0B6B3A7640000', |
||||
}) |
||||
|
||||
assert.equal(value, 1) |
||||
}) |
||||
|
||||
it('returns value of token based on token decimals', () => { |
||||
const wrapper = mount(<CurrencyDisplay />, {context: {t: str => str + '_t'}}) |
||||
|
||||
const value = wrapper.instance().getValueToRender({ |
||||
selectedToken: token, |
||||
// 1000000000000000000
|
||||
value: 'DE0B6B3A7640000', |
||||
}) |
||||
|
||||
assert.equal(value, 100000) |
||||
}) |
||||
|
||||
it('returns hex value with decimal adjustment', () => { |
||||
|
||||
const wrapper = mount( |
||||
<CurrencyDisplay |
||||
selectedToken={token} |
||||
/>, {context: {t: str => str + '_t'}}) |
||||
|
||||
const value = wrapper.instance().getAmount(1) |
||||
// 10000000000000
|
||||
assert.equal(value, '9184e72a000') |
||||
}) |
||||
|
||||
it('#getConvertedValueToRender converts input value based on conversionRate', () => { |
||||
|
||||
const wrapper = mount( |
||||
<CurrencyDisplay |
||||
primaryCurrency={'usd'} |
||||
convertedCurrency={'ja'} |
||||
conversionRate={2} |
||||
/>, {context: {t: str => str + '_t'}}) |
||||
|
||||
const value = wrapper.instance().getConvertedValueToRender(32) |
||||
|
||||
assert.equal(value, 64) |
||||
}) |
||||
|
||||
it('#onlyRenderConversions renders single element for converted currency and value', () => { |
||||
const wrapper = mount( |
||||
<CurrencyDisplay |
||||
convertedCurrency={'test'} |
||||
/>, {context: {t: str => str + '_t'}}) |
||||
|
||||
const value = wrapper.instance().onlyRenderConversions(10) |
||||
assert.equal(value.props.className, 'currency-display__converted-value') |
||||
assert.equal(value.props.children, '10 TEST') |
||||
}) |
||||
|
||||
it('simulates change value in input', () => { |
||||
const handleChangeSpy = sinon.spy() |
||||
|
||||
const wrapper = shallow( |
||||
<CurrencyDisplay |
||||
onChange={handleChangeSpy} |
||||
/>, {context: {t: str => str + '_t'}}) |
||||
|
||||
const input = wrapper.find('input') |
||||
input.simulate('focus') |
||||
input.simulate('change', { target: { value: '100' } }) |
||||
|
||||
assert.equal(wrapper.state().valueToRender, '100') |
||||
assert.equal(wrapper.find('input').prop('value'), '100') |
||||
}) |
||||
|
||||
}) |
@ -0,0 +1 @@ |
||||
export { default } from './token-input.container' |
@ -0,0 +1,308 @@ |
||||
import React from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import assert from 'assert' |
||||
import { shallow, mount } from 'enzyme' |
||||
import sinon from 'sinon' |
||||
import { Provider } from 'react-redux' |
||||
import configureMockStore from 'redux-mock-store' |
||||
import TokenInput from '../token-input.component' |
||||
import UnitInput from '../../unit-input' |
||||
import CurrencyDisplay from '../../currency-display' |
||||
|
||||
describe('TokenInput Component', () => { |
||||
const t = key => `translate ${key}` |
||||
|
||||
describe('rendering', () => { |
||||
it('should render properly without a token', () => { |
||||
const wrapper = shallow( |
||||
<TokenInput />, |
||||
{ context: { t } } |
||||
) |
||||
|
||||
assert.ok(wrapper) |
||||
assert.equal(wrapper.find(UnitInput).length, 1) |
||||
}) |
||||
|
||||
it('should render properly with a token', () => { |
||||
const mockStore = { |
||||
metamask: { |
||||
currentCurrency: 'usd', |
||||
conversionRate: 231.06, |
||||
}, |
||||
} |
||||
const store = configureMockStore()(mockStore) |
||||
|
||||
const wrapper = mount( |
||||
<Provider store={store}> |
||||
<TokenInput |
||||
selectedToken={{ |
||||
address: '0x1', |
||||
decimals: '4', |
||||
symbol: 'ABC', |
||||
}} |
||||
suffix="ABC" |
||||
/> |
||||
</Provider>, |
||||
{ context: { t }, |
||||
childContextTypes: { |
||||
t: PropTypes.func, |
||||
}, |
||||
}, |
||||
) |
||||
|
||||
assert.ok(wrapper) |
||||
assert.equal(wrapper.find('.unit-input__suffix').length, 1) |
||||
assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC') |
||||
assert.equal(wrapper.find('.currency-input__conversion-component').length, 1) |
||||
assert.equal(wrapper.find('.currency-input__conversion-component').text(), 'translate noConversionRateAvailable') |
||||
}) |
||||
|
||||
it('should render properly with a token and selectedTokenExchangeRate', () => { |
||||
const mockStore = { |
||||
metamask: { |
||||
currentCurrency: 'usd', |
||||
conversionRate: 231.06, |
||||
}, |
||||
} |
||||
const store = configureMockStore()(mockStore) |
||||
|
||||
const wrapper = mount( |
||||
<Provider store={store}> |
||||
<TokenInput |
||||
selectedToken={{ |
||||
address: '0x1', |
||||
decimals: '4', |
||||
symbol: 'ABC', |
||||
}} |
||||
suffix="ABC" |
||||
selectedTokenExchangeRate={2} |
||||
/> |
||||
</Provider>, |
||||
{ context: { t }, |
||||
childContextTypes: { |
||||
t: PropTypes.func, |
||||
}, |
||||
}, |
||||
) |
||||
|
||||
assert.ok(wrapper) |
||||
assert.equal(wrapper.find('.unit-input__suffix').length, 1) |
||||
assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC') |
||||
assert.equal(wrapper.find(CurrencyDisplay).length, 1) |
||||
}) |
||||
|
||||
it('should render properly with a token value for ETH', () => { |
||||
const mockStore = { |
||||
metamask: { |
||||
currentCurrency: 'usd', |
||||
conversionRate: 231.06, |
||||
}, |
||||
} |
||||
const store = configureMockStore()(mockStore) |
||||
|
||||
const wrapper = mount( |
||||
<Provider store={store}> |
||||
<TokenInput |
||||
value="2710" |
||||
selectedToken={{ |
||||
address: '0x1', |
||||
decimals: '4', |
||||
symbol: 'ABC', |
||||
}} |
||||
suffix="ABC" |
||||
selectedTokenExchangeRate={2} |
||||
/> |
||||
</Provider> |
||||
) |
||||
|
||||
assert.ok(wrapper) |
||||
const tokenInputInstance = wrapper.find(TokenInput).at(0).instance() |
||||
assert.equal(tokenInputInstance.state.decimalValue, 1) |
||||
assert.equal(tokenInputInstance.state.hexValue, '2710') |
||||
assert.equal(wrapper.find('.unit-input__suffix').length, 1) |
||||
assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC') |
||||
assert.equal(wrapper.find('.unit-input__input').props().value, '1') |
||||
assert.equal(wrapper.find('.currency-display-component').text(), '2 ETH') |
||||
}) |
||||
|
||||
it('should render properly with a token value for fiat', () => { |
||||
const mockStore = { |
||||
metamask: { |
||||
currentCurrency: 'usd', |
||||
conversionRate: 231.06, |
||||
}, |
||||
} |
||||
const store = configureMockStore()(mockStore) |
||||
|
||||
const wrapper = mount( |
||||
<Provider store={store}> |
||||
<TokenInput |
||||
value="2710" |
||||
selectedToken={{ |
||||
address: '0x1', |
||||
decimals: '4', |
||||
symbol: 'ABC', |
||||
}} |
||||
suffix="ABC" |
||||
selectedTokenExchangeRate={2} |
||||
showFiat |
||||
/> |
||||
</Provider> |
||||
) |
||||
|
||||
assert.ok(wrapper) |
||||
const tokenInputInstance = wrapper.find(TokenInput).at(0).instance() |
||||
assert.equal(tokenInputInstance.state.decimalValue, 1) |
||||
assert.equal(tokenInputInstance.state.hexValue, '2710') |
||||
assert.equal(wrapper.find('.unit-input__suffix').length, 1) |
||||
assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC') |
||||
assert.equal(wrapper.find('.unit-input__input').props().value, '1') |
||||
assert.equal(wrapper.find('.currency-display-component').text(), '$462.12 USD') |
||||
}) |
||||
}) |
||||
|
||||
describe('handling actions', () => { |
||||
const handleChangeSpy = sinon.spy() |
||||
const handleBlurSpy = sinon.spy() |
||||
|
||||
afterEach(() => { |
||||
handleChangeSpy.resetHistory() |
||||
handleBlurSpy.resetHistory() |
||||
}) |
||||
|
||||
it('should call onChange and onBlur on input changes with the hex value for ETH', () => { |
||||
const mockStore = { |
||||
metamask: { |
||||
currentCurrency: 'usd', |
||||
conversionRate: 231.06, |
||||
}, |
||||
} |
||||
const store = configureMockStore()(mockStore) |
||||
const wrapper = mount( |
||||
<Provider store={store}> |
||||
<TokenInput |
||||
onChange={handleChangeSpy} |
||||
onBlur={handleBlurSpy} |
||||
selectedToken={{ |
||||
address: '0x1', |
||||
decimals: '4', |
||||
symbol: 'ABC', |
||||
}} |
||||
suffix="ABC" |
||||
selectedTokenExchangeRate={2} |
||||
/> |
||||
</Provider> |
||||
) |
||||
|
||||
assert.ok(wrapper) |
||||
assert.equal(handleChangeSpy.callCount, 0) |
||||
assert.equal(handleBlurSpy.callCount, 0) |
||||
|
||||
const tokenInputInstance = wrapper.find(TokenInput).at(0).instance() |
||||
assert.equal(tokenInputInstance.state.decimalValue, 0) |
||||
assert.equal(tokenInputInstance.state.hexValue, undefined) |
||||
assert.equal(wrapper.find('.currency-display-component').text(), '0 ETH') |
||||
const input = wrapper.find('input') |
||||
assert.equal(input.props().value, 0) |
||||
|
||||
input.simulate('change', { target: { value: 1 } }) |
||||
assert.equal(handleChangeSpy.callCount, 1) |
||||
assert.ok(handleChangeSpy.calledWith('2710')) |
||||
assert.equal(wrapper.find('.currency-display-component').text(), '2 ETH') |
||||
assert.equal(tokenInputInstance.state.decimalValue, 1) |
||||
assert.equal(tokenInputInstance.state.hexValue, '2710') |
||||
|
||||
assert.equal(handleBlurSpy.callCount, 0) |
||||
input.simulate('blur') |
||||
assert.equal(handleBlurSpy.callCount, 1) |
||||
assert.ok(handleBlurSpy.calledWith('2710')) |
||||
}) |
||||
|
||||
it('should call onChange and onBlur on input changes with the hex value for fiat', () => { |
||||
const mockStore = { |
||||
metamask: { |
||||
currentCurrency: 'usd', |
||||
conversionRate: 231.06, |
||||
}, |
||||
} |
||||
const store = configureMockStore()(mockStore) |
||||
const wrapper = mount( |
||||
<Provider store={store}> |
||||
<TokenInput |
||||
onChange={handleChangeSpy} |
||||
onBlur={handleBlurSpy} |
||||
selectedToken={{ |
||||
address: '0x1', |
||||
decimals: '4', |
||||
symbol: 'ABC', |
||||
}} |
||||
suffix="ABC" |
||||
selectedTokenExchangeRate={2} |
||||
showFiat |
||||
/> |
||||
</Provider> |
||||
) |
||||
|
||||
assert.ok(wrapper) |
||||
assert.equal(handleChangeSpy.callCount, 0) |
||||
assert.equal(handleBlurSpy.callCount, 0) |
||||
|
||||
const tokenInputInstance = wrapper.find(TokenInput).at(0).instance() |
||||
assert.equal(tokenInputInstance.state.decimalValue, 0) |
||||
assert.equal(tokenInputInstance.state.hexValue, undefined) |
||||
assert.equal(wrapper.find('.currency-display-component').text(), '$0.00 USD') |
||||
const input = wrapper.find('input') |
||||
assert.equal(input.props().value, 0) |
||||
|
||||
input.simulate('change', { target: { value: 1 } }) |
||||
assert.equal(handleChangeSpy.callCount, 1) |
||||
assert.ok(handleChangeSpy.calledWith('2710')) |
||||
assert.equal(wrapper.find('.currency-display-component').text(), '$462.12 USD') |
||||
assert.equal(tokenInputInstance.state.decimalValue, 1) |
||||
assert.equal(tokenInputInstance.state.hexValue, '2710') |
||||
|
||||
assert.equal(handleBlurSpy.callCount, 0) |
||||
input.simulate('blur') |
||||
assert.equal(handleBlurSpy.callCount, 1) |
||||
assert.ok(handleBlurSpy.calledWith('2710')) |
||||
}) |
||||
|
||||
it('should change the state and pass in a new decimalValue when props.value changes', () => { |
||||
const mockStore = { |
||||
metamask: { |
||||
currentCurrency: 'usd', |
||||
conversionRate: 231.06, |
||||
}, |
||||
} |
||||
const store = configureMockStore()(mockStore) |
||||
const wrapper = shallow( |
||||
<Provider store={store}> |
||||
<TokenInput |
||||
onChange={handleChangeSpy} |
||||
onBlur={handleBlurSpy} |
||||
selectedToken={{ |
||||
address: '0x1', |
||||
decimals: '4', |
||||
symbol: 'ABC', |
||||
}} |
||||
suffix="ABC" |
||||
selectedTokenExchangeRate={2} |
||||
showFiat |
||||
/> |
||||
</Provider> |
||||
) |
||||
|
||||
assert.ok(wrapper) |
||||
const tokenInputInstance = wrapper.find(TokenInput).dive() |
||||
assert.equal(tokenInputInstance.state('decimalValue'), 0) |
||||
assert.equal(tokenInputInstance.state('hexValue'), undefined) |
||||
assert.equal(tokenInputInstance.find(UnitInput).props().value, 0) |
||||
|
||||
tokenInputInstance.setProps({ value: '2710' }) |
||||
tokenInputInstance.update() |
||||
assert.equal(tokenInputInstance.state('decimalValue'), 1) |
||||
assert.equal(tokenInputInstance.state('hexValue'), '2710') |
||||
assert.equal(tokenInputInstance.find(UnitInput).props().value, 1) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,129 @@ |
||||
import assert from 'assert' |
||||
import proxyquire from 'proxyquire' |
||||
|
||||
let mapStateToProps, mergeProps |
||||
|
||||
proxyquire('../token-input.container.js', { |
||||
'react-redux': { |
||||
connect: (ms, md, mp) => { |
||||
mapStateToProps = ms |
||||
mergeProps = mp |
||||
return () => ({}) |
||||
}, |
||||
}, |
||||
}) |
||||
|
||||
describe('TokenInput container', () => { |
||||
describe('mapStateToProps()', () => { |
||||
it('should return the correct props when send is empty', () => { |
||||
const mockState = { |
||||
metamask: { |
||||
currentCurrency: 'usd', |
||||
tokens: [ |
||||
{ |
||||
address: '0x1', |
||||
decimals: '4', |
||||
symbol: 'ABC', |
||||
}, |
||||
], |
||||
selectedTokenAddress: '0x1', |
||||
contractExchangeRates: {}, |
||||
send: {}, |
||||
}, |
||||
} |
||||
|
||||
assert.deepEqual(mapStateToProps(mockState), { |
||||
currentCurrency: 'usd', |
||||
selectedToken: { |
||||
address: '0x1', |
||||
decimals: '4', |
||||
symbol: 'ABC', |
||||
}, |
||||
selectedTokenExchangeRate: 0, |
||||
}) |
||||
}) |
||||
|
||||
it('should return the correct props when selectedTokenAddress is not found and send is populated', () => { |
||||
const mockState = { |
||||
metamask: { |
||||
currentCurrency: 'usd', |
||||
tokens: [ |
||||
{ |
||||
address: '0x1', |
||||
decimals: '4', |
||||
symbol: 'ABC', |
||||
}, |
||||
], |
||||
selectedTokenAddress: '0x2', |
||||
contractExchangeRates: {}, |
||||
send: { |
||||
token: { address: 'test' }, |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
assert.deepEqual(mapStateToProps(mockState), { |
||||
currentCurrency: 'usd', |
||||
selectedToken: { |
||||
address: 'test', |
||||
}, |
||||
selectedTokenExchangeRate: 0, |
||||
}) |
||||
}) |
||||
|
||||
it('should return the correct props when contractExchangeRates is populated', () => { |
||||
const mockState = { |
||||
metamask: { |
||||
currentCurrency: 'usd', |
||||
tokens: [ |
||||
{ |
||||
address: '0x1', |
||||
decimals: '4', |
||||
symbol: 'ABC', |
||||
}, |
||||
], |
||||
selectedTokenAddress: '0x1', |
||||
contractExchangeRates: { |
||||
'0x1': 5, |
||||
}, |
||||
send: {}, |
||||
}, |
||||
} |
||||
|
||||
assert.deepEqual(mapStateToProps(mockState), { |
||||
currentCurrency: 'usd', |
||||
selectedToken: { |
||||
address: '0x1', |
||||
decimals: '4', |
||||
symbol: 'ABC', |
||||
}, |
||||
selectedTokenExchangeRate: 5, |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('mergeProps()', () => { |
||||
it('should return the correct props', () => { |
||||
const mockStateProps = { |
||||
currentCurrency: 'usd', |
||||
selectedToken: { |
||||
address: '0x1', |
||||
decimals: '4', |
||||
symbol: 'ABC', |
||||
}, |
||||
selectedTokenExchangeRate: 5, |
||||
} |
||||
|
||||
assert.deepEqual(mergeProps(mockStateProps, {}, {}), { |
||||
currentCurrency: 'usd', |
||||
selectedToken: { |
||||
address: '0x1', |
||||
decimals: '4', |
||||
symbol: 'ABC', |
||||
}, |
||||
selectedTokenExchangeRate: 5, |
||||
suffix: 'ABC', |
||||
}) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,136 @@ |
||||
import React, { PureComponent } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import UnitInput from '../unit-input' |
||||
import CurrencyDisplay from '../currency-display' |
||||
import { getWeiHexFromDecimalValue } from '../../helpers/conversions.util' |
||||
import ethUtil from 'ethereumjs-util' |
||||
import { conversionUtil, multiplyCurrencies } from '../../conversion-util' |
||||
import { ETH } from '../../constants/common' |
||||
|
||||
/** |
||||
* Component that allows user to enter token values as a number, and props receive a converted |
||||
* hex value. props.value, used as a default or forced value, should be a hex value, which |
||||
* gets converted into a decimal value. |
||||
*/ |
||||
export default class TokenInput extends PureComponent { |
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
static propTypes = { |
||||
currentCurrency: PropTypes.string, |
||||
onChange: PropTypes.func, |
||||
onBlur: PropTypes.func, |
||||
value: PropTypes.string, |
||||
suffix: PropTypes.string, |
||||
showFiat: PropTypes.bool, |
||||
selectedToken: PropTypes.object, |
||||
selectedTokenExchangeRate: PropTypes.number, |
||||
} |
||||
|
||||
constructor (props) { |
||||
super(props) |
||||
|
||||
const { value: hexValue } = props |
||||
const decimalValue = hexValue ? this.getDecimalValue(props) : 0 |
||||
|
||||
this.state = { |
||||
decimalValue, |
||||
hexValue, |
||||
} |
||||
} |
||||
|
||||
componentDidUpdate (prevProps) { |
||||
const { value: prevPropsHexValue } = prevProps |
||||
const { value: propsHexValue } = this.props |
||||
const { hexValue: stateHexValue } = this.state |
||||
|
||||
if (prevPropsHexValue !== propsHexValue && propsHexValue !== stateHexValue) { |
||||
const decimalValue = this.getDecimalValue(this.props) |
||||
this.setState({ hexValue: propsHexValue, decimalValue }) |
||||
} |
||||
} |
||||
|
||||
getDecimalValue (props) { |
||||
const { value: hexValue, selectedToken: { decimals, symbol } = {} } = props |
||||
|
||||
const multiplier = Math.pow(10, Number(decimals || 0)) |
||||
const decimalValueString = conversionUtil(ethUtil.addHexPrefix(hexValue), { |
||||
fromNumericBase: 'hex', |
||||
toNumericBase: 'dec', |
||||
toCurrency: symbol, |
||||
conversionRate: multiplier, |
||||
invertConversionRate: true, |
||||
}) |
||||
|
||||
return Number(decimalValueString) || 0 |
||||
} |
||||
|
||||
handleChange = decimalValue => { |
||||
const { selectedToken: { decimals } = {}, onChange } = this.props |
||||
|
||||
const multiplier = Math.pow(10, Number(decimals || 0)) |
||||
const hexValue = multiplyCurrencies(decimalValue || 0, multiplier, { toNumericBase: 'hex' }) |
||||
|
||||
this.setState({ hexValue, decimalValue }) |
||||
onChange(hexValue) |
||||
} |
||||
|
||||
handleBlur = () => { |
||||
this.props.onBlur(this.state.hexValue) |
||||
} |
||||
|
||||
renderConversionComponent () { |
||||
const { selectedTokenExchangeRate, showFiat, currentCurrency } = this.props |
||||
const { decimalValue } = this.state |
||||
let currency, numberOfDecimals |
||||
|
||||
if (showFiat) { |
||||
// Display Fiat
|
||||
currency = currentCurrency |
||||
numberOfDecimals = 2 |
||||
} else { |
||||
// Display ETH
|
||||
currency = ETH |
||||
numberOfDecimals = 6 |
||||
} |
||||
|
||||
const decimalEthValue = (decimalValue * selectedTokenExchangeRate) || 0 |
||||
const hexWeiValue = getWeiHexFromDecimalValue({ |
||||
value: decimalEthValue, |
||||
fromCurrency: ETH, |
||||
fromDenomination: ETH, |
||||
}) |
||||
|
||||
return selectedTokenExchangeRate |
||||
? ( |
||||
<CurrencyDisplay |
||||
className="currency-input__conversion-component" |
||||
currency={currency} |
||||
value={hexWeiValue} |
||||
numberOfDecimals={numberOfDecimals} |
||||
/> |
||||
) : ( |
||||
<div className="currency-input__conversion-component"> |
||||
{ this.context.t('noConversionRateAvailable') } |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
render () { |
||||
const { suffix, ...restProps } = this.props |
||||
const { decimalValue } = this.state |
||||
|
||||
return ( |
||||
<UnitInput |
||||
{...restProps} |
||||
suffix={suffix} |
||||
onChange={this.handleChange} |
||||
onBlur={this.handleBlur} |
||||
value={decimalValue} |
||||
> |
||||
{ this.renderConversionComponent() } |
||||
</UnitInput> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,27 @@ |
||||
import { connect } from 'react-redux' |
||||
import TokenInput from './token-input.component' |
||||
import { getSelectedToken, getSelectedTokenExchangeRate } from '../../selectors' |
||||
|
||||
const mapStateToProps = state => { |
||||
const { metamask: { currentCurrency } } = state |
||||
|
||||
return { |
||||
currentCurrency, |
||||
selectedToken: getSelectedToken(state), |
||||
selectedTokenExchangeRate: getSelectedTokenExchangeRate(state), |
||||
} |
||||
} |
||||
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => { |
||||
const { selectedToken } = stateProps |
||||
const suffix = selectedToken && selectedToken.symbol |
||||
|
||||
return { |
||||
...stateProps, |
||||
...dispatchProps, |
||||
...ownProps, |
||||
suffix, |
||||
} |
||||
} |
||||
|
||||
export default connect(mapStateToProps, null, mergeProps)(TokenInput) |
@ -0,0 +1 @@ |
||||
export { default } from './unit-input.component' |
@ -0,0 +1,44 @@ |
||||
.unit-input { |
||||
min-height: 54px; |
||||
border: 1px solid #dedede; |
||||
border-radius: 4px; |
||||
background-color: #fff; |
||||
color: #4d4d4d; |
||||
font-size: 1rem; |
||||
padding: 8px 10px; |
||||
position: relative; |
||||
|
||||
input[type="number"] { |
||||
-moz-appearance: textfield; |
||||
} |
||||
|
||||
input[type="number"]::-webkit-inner-spin-button { |
||||
-webkit-appearance: none; |
||||
-moz-appearance: none; |
||||
display: none; |
||||
} |
||||
|
||||
input[type="number"]:hover::-webkit-inner-spin-button { |
||||
-webkit-appearance: none; |
||||
-moz-appearance: none; |
||||
display: none; |
||||
} |
||||
|
||||
&__input { |
||||
color: #4d4d4d; |
||||
font-size: 1rem; |
||||
font-family: Roboto; |
||||
border: none; |
||||
outline: 0 !important; |
||||
max-width: 22ch; |
||||
} |
||||
|
||||
&__input-container { |
||||
display: flex; |
||||
align-items: center; |
||||
} |
||||
|
||||
&--error { |
||||
border-color: $red; |
||||
} |
||||
} |
@ -0,0 +1,146 @@ |
||||
import React from 'react' |
||||
import assert from 'assert' |
||||
import { shallow, mount } from 'enzyme' |
||||
import sinon from 'sinon' |
||||
import UnitInput from '../unit-input.component' |
||||
|
||||
describe('UnitInput Component', () => { |
||||
describe('rendering', () => { |
||||
it('should render properly without a suffix', () => { |
||||
const wrapper = shallow( |
||||
<UnitInput /> |
||||
) |
||||
|
||||
assert.ok(wrapper) |
||||
assert.equal(wrapper.find('.unit-input__suffix').length, 0) |
||||
}) |
||||
|
||||
it('should render properly with a suffix', () => { |
||||
const wrapper = shallow( |
||||
<UnitInput |
||||
suffix="ETH" |
||||
/> |
||||
) |
||||
|
||||
assert.ok(wrapper) |
||||
assert.equal(wrapper.find('.unit-input__suffix').length, 1) |
||||
assert.equal(wrapper.find('.unit-input__suffix').text(), 'ETH') |
||||
}) |
||||
|
||||
it('should render properly with a child omponent', () => { |
||||
const wrapper = shallow( |
||||
<UnitInput> |
||||
<div className="testing"> |
||||
TESTCOMPONENT |
||||
</div> |
||||
</UnitInput> |
||||
) |
||||
|
||||
assert.ok(wrapper) |
||||
assert.equal(wrapper.find('.testing').length, 1) |
||||
assert.equal(wrapper.find('.testing').text(), 'TESTCOMPONENT') |
||||
}) |
||||
|
||||
it('should render with an error class when props.error === true', () => { |
||||
const wrapper = shallow( |
||||
<UnitInput |
||||
error |
||||
/> |
||||
) |
||||
|
||||
assert.ok(wrapper) |
||||
assert.equal(wrapper.find('.unit-input--error').length, 1) |
||||
}) |
||||
}) |
||||
|
||||
describe('handling actions', () => { |
||||
const handleChangeSpy = sinon.spy() |
||||
const handleBlurSpy = sinon.spy() |
||||
|
||||
afterEach(() => { |
||||
handleChangeSpy.resetHistory() |
||||
handleBlurSpy.resetHistory() |
||||
}) |
||||
|
||||
it('should focus the input on component click', () => { |
||||
const wrapper = mount( |
||||
<UnitInput /> |
||||
) |
||||
|
||||
assert.ok(wrapper) |
||||
const handleFocusSpy = sinon.spy(wrapper.instance(), 'handleFocus') |
||||
wrapper.instance().forceUpdate() |
||||
wrapper.update() |
||||
assert.equal(handleFocusSpy.callCount, 0) |
||||
wrapper.find('.unit-input').simulate('click') |
||||
assert.equal(handleFocusSpy.callCount, 1) |
||||
}) |
||||
|
||||
it('should call onChange on input changes with the value', () => { |
||||
const wrapper = mount( |
||||
<UnitInput |
||||
onChange={handleChangeSpy} |
||||
/> |
||||
) |
||||
|
||||
assert.ok(wrapper) |
||||
assert.equal(handleChangeSpy.callCount, 0) |
||||
const input = wrapper.find('input') |
||||
input.simulate('change', { target: { value: 123 } }) |
||||
assert.equal(handleChangeSpy.callCount, 1) |
||||
assert.ok(handleChangeSpy.calledWith(123)) |
||||
assert.equal(wrapper.state('value'), 123) |
||||
}) |
||||
|
||||
it('should call onBlur on blur with the value', () => { |
||||
const wrapper = mount( |
||||
<UnitInput |
||||
onChange={handleChangeSpy} |
||||
onBlur={handleBlurSpy} |
||||
/> |
||||
) |
||||
|
||||
assert.ok(wrapper) |
||||
assert.equal(handleChangeSpy.callCount, 0) |
||||
assert.equal(handleBlurSpy.callCount, 0) |
||||
const input = wrapper.find('input') |
||||
input.simulate('change', { target: { value: 123 } }) |
||||
assert.equal(handleChangeSpy.callCount, 1) |
||||
assert.ok(handleChangeSpy.calledWith(123)) |
||||
assert.equal(wrapper.state('value'), 123) |
||||
input.simulate('blur') |
||||
assert.equal(handleBlurSpy.callCount, 1) |
||||
assert.ok(handleBlurSpy.calledWith(123)) |
||||
}) |
||||
|
||||
it('should set the component state value with props.value', () => { |
||||
const wrapper = mount( |
||||
<UnitInput |
||||
value={123} |
||||
/> |
||||
) |
||||
|
||||
assert.ok(wrapper) |
||||
assert.equal(wrapper.state('value'), 123) |
||||
}) |
||||
|
||||
it('should update the component state value with props.value', () => { |
||||
const wrapper = mount( |
||||
<UnitInput |
||||
onChange={handleChangeSpy} |
||||
/> |
||||
) |
||||
|
||||
assert.ok(wrapper) |
||||
assert.equal(handleChangeSpy.callCount, 0) |
||||
const input = wrapper.find('input') |
||||
input.simulate('change', { target: { value: 123 } }) |
||||
assert.equal(wrapper.state('value'), 123) |
||||
assert.equal(handleChangeSpy.callCount, 1) |
||||
assert.ok(handleChangeSpy.calledWith(123)) |
||||
wrapper.setProps({ value: 456 }) |
||||
assert.equal(wrapper.state('value'), 456) |
||||
assert.equal(handleChangeSpy.callCount, 1) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,104 @@ |
||||
import React, { PureComponent } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import classnames from 'classnames' |
||||
import { removeLeadingZeroes } from '../send/send.utils' |
||||
|
||||
/** |
||||
* Component that attaches a suffix or unit of measurement trailing user input, ex. 'ETH'. Also |
||||
* allows rendering a child component underneath the input to, for example, display conversions of |
||||
* the shown suffix. |
||||
*/ |
||||
export default class UnitInput extends PureComponent { |
||||
static propTypes = { |
||||
children: PropTypes.node, |
||||
error: PropTypes.bool, |
||||
onBlur: PropTypes.func, |
||||
onChange: PropTypes.func, |
||||
placeholder: PropTypes.string, |
||||
suffix: PropTypes.string, |
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), |
||||
} |
||||
|
||||
static defaultProps = { |
||||
placeholder: '0', |
||||
} |
||||
|
||||
constructor (props) { |
||||
super(props) |
||||
|
||||
this.state = { |
||||
value: props.value || '', |
||||
} |
||||
} |
||||
|
||||
componentDidUpdate (prevProps) { |
||||
const { value: prevPropsValue } = prevProps |
||||
const { value: propsValue } = this.props |
||||
const { value: stateValue } = this.state |
||||
|
||||
if (prevPropsValue !== propsValue && propsValue !== stateValue) { |
||||
this.setState({ value: propsValue }) |
||||
} |
||||
} |
||||
|
||||
handleFocus = () => { |
||||
this.unitInput.focus() |
||||
} |
||||
|
||||
handleChange = event => { |
||||
const { value: userInput } = event.target |
||||
let value = userInput |
||||
|
||||
if (userInput.length && userInput.length > 1) { |
||||
value = removeLeadingZeroes(userInput) |
||||
} |
||||
|
||||
this.setState({ value }) |
||||
this.props.onChange(value) |
||||
} |
||||
|
||||
handleBlur = event => { |
||||
const { onBlur } = this.props |
||||
typeof onBlur === 'function' && onBlur(this.state.value) |
||||
} |
||||
|
||||
getInputWidth (value) { |
||||
const valueString = String(value) |
||||
const valueLength = valueString.length || 1 |
||||
const decimalPointDeficit = valueString.match(/\./) ? -0.5 : 0 |
||||
return (valueLength + decimalPointDeficit + 0.75) + 'ch' |
||||
} |
||||
|
||||
render () { |
||||
const { error, placeholder, suffix, children } = this.props |
||||
const { value } = this.state |
||||
|
||||
return ( |
||||
<div |
||||
className={classnames('unit-input', { 'unit-input--error': error })} |
||||
onClick={this.handleFocus} |
||||
> |
||||
<div className="unit-input__input-container"> |
||||
<input |
||||
type="number" |
||||
className="unit-input__input" |
||||
value={value} |
||||
placeholder={placeholder} |
||||
onChange={this.handleChange} |
||||
onBlur={this.handleBlur} |
||||
style={{ width: this.getInputWidth(value) }} |
||||
ref={ref => { this.unitInput = ref }} |
||||
/> |
||||
{ |
||||
suffix && ( |
||||
<div className="unit-input__suffix"> |
||||
{ suffix } |
||||
</div> |
||||
) |
||||
} |
||||
</div> |
||||
{ children } |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1 @@ |
||||
export { default } from './user-preferenced-currency-display.container' |
@ -0,0 +1,34 @@ |
||||
import React from 'react' |
||||
import assert from 'assert' |
||||
import { shallow } from 'enzyme' |
||||
import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display.component' |
||||
import CurrencyDisplay from '../../currency-display' |
||||
|
||||
describe('UserPreferencedCurrencyDisplay Component', () => { |
||||
describe('rendering', () => { |
||||
it('should render properly', () => { |
||||
const wrapper = shallow( |
||||
<UserPreferencedCurrencyDisplay /> |
||||
) |
||||
|
||||
assert.ok(wrapper) |
||||
assert.equal(wrapper.find(CurrencyDisplay).length, 1) |
||||
}) |
||||
|
||||
it('should pass all props to the CurrencyDisplay child component', () => { |
||||
const wrapper = shallow( |
||||
<UserPreferencedCurrencyDisplay |
||||
prop1={true} |
||||
prop2="test" |
||||
prop3={1} |
||||
/> |
||||
) |
||||
|
||||
assert.ok(wrapper) |
||||
assert.equal(wrapper.find(CurrencyDisplay).length, 1) |
||||
assert.equal(wrapper.find(CurrencyDisplay).props().prop1, true) |
||||
assert.equal(wrapper.find(CurrencyDisplay).props().prop2, 'test') |
||||
assert.equal(wrapper.find(CurrencyDisplay).props().prop3, 1) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,105 @@ |
||||
import assert from 'assert' |
||||
import proxyquire from 'proxyquire' |
||||
|
||||
let mapStateToProps, mergeProps |
||||
|
||||
proxyquire('../user-preferenced-currency-display.container.js', { |
||||
'react-redux': { |
||||
connect: (ms, md, mp) => { |
||||
mapStateToProps = ms |
||||
mergeProps = mp |
||||
return () => ({}) |
||||
}, |
||||
}, |
||||
}) |
||||
|
||||
describe('UserPreferencedCurrencyDisplay container', () => { |
||||
describe('mapStateToProps()', () => { |
||||
it('should return the correct props', () => { |
||||
const mockState = { |
||||
metamask: { |
||||
preferences: { |
||||
useETHAsPrimaryCurrency: true, |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
assert.deepEqual(mapStateToProps(mockState), { |
||||
useETHAsPrimaryCurrency: true, |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('mergeProps()', () => { |
||||
it('should return the correct props', () => { |
||||
const mockDispatchProps = {} |
||||
|
||||
const tests = [ |
||||
{ |
||||
stateProps: { |
||||
useETHAsPrimaryCurrency: true, |
||||
}, |
||||
ownProps: { |
||||
type: 'PRIMARY', |
||||
}, |
||||
result: { |
||||
currency: 'ETH', |
||||
numberOfDecimals: 6, |
||||
prefix: undefined, |
||||
}, |
||||
}, |
||||
{ |
||||
stateProps: { |
||||
useETHAsPrimaryCurrency: false, |
||||
}, |
||||
ownProps: { |
||||
type: 'PRIMARY', |
||||
}, |
||||
result: { |
||||
currency: undefined, |
||||
numberOfDecimals: 2, |
||||
prefix: undefined, |
||||
}, |
||||
}, |
||||
{ |
||||
stateProps: { |
||||
useETHAsPrimaryCurrency: true, |
||||
}, |
||||
ownProps: { |
||||
type: 'SECONDARY', |
||||
fiatNumberOfDecimals: 4, |
||||
fiatPrefix: '-', |
||||
}, |
||||
result: { |
||||
currency: undefined, |
||||
numberOfDecimals: 4, |
||||
prefix: '-', |
||||
}, |
||||
}, |
||||
{ |
||||
stateProps: { |
||||
useETHAsPrimaryCurrency: false, |
||||
}, |
||||
ownProps: { |
||||
type: 'SECONDARY', |
||||
fiatNumberOfDecimals: 4, |
||||
numberOfDecimals: 3, |
||||
fiatPrefix: 'a', |
||||
prefix: 'b', |
||||
}, |
||||
result: { |
||||
currency: 'ETH', |
||||
numberOfDecimals: 3, |
||||
prefix: 'b', |
||||
}, |
||||
}, |
||||
] |
||||
|
||||
tests.forEach(({ stateProps, ownProps, result }) => { |
||||
assert.deepEqual(mergeProps({ ...stateProps }, mockDispatchProps, { ...ownProps }), { |
||||
...result, |
||||
}) |
||||
}) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,45 @@ |
||||
import React, { PureComponent } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import { PRIMARY, SECONDARY, ETH } from '../../constants/common' |
||||
import CurrencyDisplay from '../currency-display' |
||||
|
||||
export default class UserPreferencedCurrencyDisplay extends PureComponent { |
||||
static propTypes = { |
||||
className: PropTypes.string, |
||||
prefix: PropTypes.string, |
||||
value: PropTypes.string, |
||||
numberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), |
||||
hideLabel: PropTypes.bool, |
||||
style: PropTypes.object, |
||||
showEthLogo: PropTypes.bool, |
||||
ethLogoHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), |
||||
// Used in container
|
||||
type: PropTypes.oneOf([PRIMARY, SECONDARY]), |
||||
ethNumberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), |
||||
fiatNumberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), |
||||
ethPrefix: PropTypes.string, |
||||
fiatPrefix: PropTypes.string, |
||||
// From container
|
||||
currency: PropTypes.string, |
||||
} |
||||
|
||||
renderEthLogo () { |
||||
const { currency, showEthLogo, ethLogoHeight = 12 } = this.props |
||||
|
||||
return currency === ETH && showEthLogo && ( |
||||
<img |
||||
src="/images/eth.svg" |
||||
height={ethLogoHeight} |
||||
/> |
||||
) |
||||
} |
||||
|
||||
render () { |
||||
return ( |
||||
<CurrencyDisplay |
||||
{...this.props} |
||||
prefixComponent={this.renderEthLogo()} |
||||
/> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,52 @@ |
||||
import { connect } from 'react-redux' |
||||
import UserPreferencedCurrencyDisplay from './user-preferenced-currency-display.component' |
||||
import { preferencesSelector } from '../../selectors' |
||||
import { ETH, PRIMARY, SECONDARY } from '../../constants/common' |
||||
|
||||
const mapStateToProps = (state, ownProps) => { |
||||
const { useETHAsPrimaryCurrency } = preferencesSelector(state) |
||||
|
||||
return { |
||||
useETHAsPrimaryCurrency, |
||||
} |
||||
} |
||||
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => { |
||||
const { useETHAsPrimaryCurrency, ...restStateProps } = stateProps |
||||
const { |
||||
type, |
||||
numberOfDecimals: propsNumberOfDecimals, |
||||
ethNumberOfDecimals, |
||||
fiatNumberOfDecimals, |
||||
ethPrefix, |
||||
fiatPrefix, |
||||
prefix: propsPrefix, |
||||
...restOwnProps |
||||
} = ownProps |
||||
|
||||
let currency, numberOfDecimals, prefix |
||||
|
||||
if (type === PRIMARY && useETHAsPrimaryCurrency || |
||||
type === SECONDARY && !useETHAsPrimaryCurrency) { |
||||
// Display ETH
|
||||
currency = ETH |
||||
numberOfDecimals = propsNumberOfDecimals || ethNumberOfDecimals || 6 |
||||
prefix = propsPrefix || ethPrefix |
||||
} else if (type === SECONDARY && useETHAsPrimaryCurrency || |
||||
type === PRIMARY && !useETHAsPrimaryCurrency) { |
||||
// Display Fiat
|
||||
numberOfDecimals = propsNumberOfDecimals || fiatNumberOfDecimals || 2 |
||||
prefix = propsPrefix || fiatPrefix |
||||
} |
||||
|
||||
return { |
||||
...restStateProps, |
||||
...dispatchProps, |
||||
...restOwnProps, |
||||
currency, |
||||
numberOfDecimals, |
||||
prefix, |
||||
} |
||||
} |
||||
|
||||
export default connect(mapStateToProps, null, mergeProps)(UserPreferencedCurrencyDisplay) |
@ -0,0 +1 @@ |
||||
export { default } from './user-preferenced-currency-input.container' |
@ -0,0 +1,32 @@ |
||||
import React from 'react' |
||||
import assert from 'assert' |
||||
import { shallow } from 'enzyme' |
||||
import UserPreferencedCurrencyInput from '../user-preferenced-currency-input.component' |
||||
import CurrencyInput from '../../currency-input' |
||||
|
||||
describe('UserPreferencedCurrencyInput Component', () => { |
||||
describe('rendering', () => { |
||||
it('should render properly', () => { |
||||
const wrapper = shallow( |
||||
<UserPreferencedCurrencyInput /> |
||||
) |
||||
|
||||
assert.ok(wrapper) |
||||
assert.equal(wrapper.find(CurrencyInput).length, 1) |
||||
}) |
||||
|
||||
it('should render useFiat for CurrencyInput based on preferences.useETHAsPrimaryCurrency', () => { |
||||
const wrapper = shallow( |
||||
<UserPreferencedCurrencyInput |
||||
useETHAsPrimaryCurrency |
||||
/> |
||||
) |
||||
|
||||
assert.ok(wrapper) |
||||
assert.equal(wrapper.find(CurrencyInput).length, 1) |
||||
assert.equal(wrapper.find(CurrencyInput).props().useFiat, false) |
||||
wrapper.setProps({ useETHAsPrimaryCurrency: false }) |
||||
assert.equal(wrapper.find(CurrencyInput).props().useFiat, true) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,31 @@ |
||||
import assert from 'assert' |
||||
import proxyquire from 'proxyquire' |
||||
|
||||
let mapStateToProps |
||||
|
||||
proxyquire('../user-preferenced-currency-input.container.js', { |
||||
'react-redux': { |
||||
connect: ms => { |
||||
mapStateToProps = ms |
||||
return () => ({}) |
||||
}, |
||||
}, |
||||
}) |
||||
|
||||
describe('UserPreferencedCurrencyInput container', () => { |
||||
describe('mapStateToProps()', () => { |
||||
it('should return the correct props', () => { |
||||
const mockState = { |
||||
metamask: { |
||||
preferences: { |
||||
useETHAsPrimaryCurrency: true, |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
assert.deepEqual(mapStateToProps(mockState), { |
||||
useETHAsPrimaryCurrency: true, |
||||
}) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,20 @@ |
||||
import React, { PureComponent } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import CurrencyInput from '../currency-input' |
||||
|
||||
export default class UserPreferencedCurrencyInput extends PureComponent { |
||||
static propTypes = { |
||||
useETHAsPrimaryCurrency: PropTypes.bool, |
||||
} |
||||
|
||||
render () { |
||||
const { useETHAsPrimaryCurrency, ...restProps } = this.props |
||||
|
||||
return ( |
||||
<CurrencyInput |
||||
{...restProps} |
||||
useFiat={!useETHAsPrimaryCurrency} |
||||
/> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,13 @@ |
||||
import { connect } from 'react-redux' |
||||
import UserPreferencedCurrencyInput from './user-preferenced-currency-input.component' |
||||
import { preferencesSelector } from '../../selectors' |
||||
|
||||
const mapStateToProps = state => { |
||||
const { useETHAsPrimaryCurrency } = preferencesSelector(state) |
||||
|
||||
return { |
||||
useETHAsPrimaryCurrency, |
||||
} |
||||
} |
||||
|
||||
export default connect(mapStateToProps)(UserPreferencedCurrencyInput) |
@ -0,0 +1 @@ |
||||
export { default } from './user-preferenced-token-input.container' |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue