From c754c3c5efaebee7751270fcfeb093521dd5c3f5 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 26 Feb 2018 10:34:26 -0300 Subject: [PATCH 01/16] Update NumericInput's state through props --- src/components/Common/InputField.js | 2 +- src/components/Common/NumericInput.js | 118 ++++++++++++++++++-------- 2 files changed, 82 insertions(+), 38 deletions(-) diff --git a/src/components/Common/InputField.js b/src/components/Common/InputField.js index 4c7445548..3621f62bd 100644 --- a/src/components/Common/InputField.js +++ b/src/components/Common/InputField.js @@ -28,7 +28,7 @@ export const InputField = props => { onPaste={props.onPaste} />

{props.description}

- { props.pristine ? null :

{error}

} + { props.pristine ?

:

{error}

} ); }; diff --git a/src/components/Common/NumericInput.js b/src/components/Common/NumericInput.js index 1c8f0cde2..0c6c8f9d8 100644 --- a/src/components/Common/NumericInput.js +++ b/src/components/Common/NumericInput.js @@ -1,8 +1,8 @@ import React, { Component } from 'react' import { InputField } from './InputField' import { VALIDATION_TYPES } from '../../utils/constants' -import { observer } from 'mobx-react' import { countDecimalPlaces } from '../../utils/utils' +import { observer } from 'mobx-react' const { VALID, INVALID } = VALIDATION_TYPES @@ -12,77 +12,121 @@ export class NumericInput extends Component { super(props) this.state = { - value: '', - pristine: true, - valid: INVALID + value: props.value || '', + pristine: props.pristine || true, + valid: props.valid || VALID } } onPaste = e => { - if (isNaN(parseFloat(e.clipboardData.getData('text/plain')))) + if (isNaN(parseFloat(e.clipboardData.getData('text/plain')))) { e.preventDefault() + } } onKeyPress = e => { - if (!this.props.acceptFloat) { - const firstCharacterIsMinus = e.key === '-' && this.state.value === '' - const acceptsNegativeValues = this.props.min < 0 || this.props.max < 0 + const { value, min, max, acceptFloat } = this.props + const isFirstCharacter = value === '' || value === undefined + const isMinus = e.key === '-' + const allowsNegative = min < 0 || max < 0 - if (!(firstCharacterIsMinus && acceptsNegativeValues) && isNaN(parseInt(e.key, 10))) e.preventDefault() + if (isFirstCharacter) { + if (isMinus) { + if (!allowsNegative) { + e.preventDefault() + } + } + } else { + if (!acceptFloat) { + if (isNaN(parseInt(e.key, 10))) { + e.preventDefault() + } + } } } onChange = e => { - let value = this.props.acceptFloat ? parseFloat(e.target.value) : parseInt(e.target.value, 10) + const value = this.props.acceptFloat ? parseFloat(e.target.value) : parseInt(e.target.value, 10) + this.validate(value) + } + + validate = value => { + const { acceptFloat, maxDecimals, minDecimals, min, max } = this.props let isValid = true - if (this.props.acceptFloat) { - if (this.props.maxDecimals !== undefined) { - isValid = isValid && countDecimalPlaces(value) <= this.props.maxDecimals + if (acceptFloat) { + if (maxDecimals !== undefined) { + isValid = isValid && countDecimalPlaces(value) <= maxDecimals } - if (this.props.minDecimals !== undefined) { - isValid = isValid && countDecimalPlaces(value) >= this.props.minDecimals + if (minDecimals !== undefined) { + isValid = isValid && countDecimalPlaces(value) >= minDecimals } } - if (isValid && this.props.min !== undefined) { - isValid = isValid && value >= this.props.min + if (isValid && min !== undefined) { + isValid = isValid && value >= min } - if (isValid && this.props.max !== undefined) { - isValid = isValid && value <= this.props.max + if (isValid && max !== undefined) { + isValid = isValid && value <= max } - if (isNaN(value)) { - value = '' - isValid = true + const result = { + value: '', + pristine: value === this.props.value && this.state.pristine, + valid: INVALID + } + + if (!isNaN(value)) { + result.value = value + result.valid = isValid ? VALID : INVALID } + this.props.onValueUpdate(result) + } + + componentWillReceiveProps (newProps) { this.setState({ - pristine: false, - valid: isValid ? VALID : INVALID, - value + value: newProps.value, + pristine: newProps.pristine, + valid: newProps.valid }) + } + + componentDidUpdate (prevProps) { + const { acceptFloat, maxDecimals, minDecimals, min, max } = this.props - this.props.onValueUpdate(value) + if ( + prevProps.acceptFloat !== acceptFloat || + prevProps.minDecimals !== minDecimals || + prevProps.maxDecimals !== maxDecimals || + prevProps.min !== min || + prevProps.max !== max + ) { + // re-check validity if any of the props had changed + this.validate(this.state.value) + } } render () { + const { value, pristine, valid } = this.state + const { disabled, side, errorMessage, title, description } = this.props + return ( this.onKeyPress(e)} - onChange={e => this.onChange(e)} - onPaste={e => this.onPaste(e)} - description={this.props.description} + errorMessage={errorMessage} + value={value} + pristine={pristine} + valid={valid} + title={title} + onKeyPress={this.onKeyPress} + onChange={this.onChange} + onPaste={this.onPaste} + description={description} /> ) } From efb7f8b3a5d10bece546b68efff095feff607ae7 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 26 Feb 2018 10:34:42 -0300 Subject: [PATCH 02/16] Update NumericInput spec --- src/components/Common/NumericInput.spec.js | 285 +++++++++++------- .../__snapshots__/NumericInput.spec.js.snap | 11 + 2 files changed, 188 insertions(+), 108 deletions(-) diff --git a/src/components/Common/NumericInput.spec.js b/src/components/Common/NumericInput.spec.js index fb5f0b836..e96a55456 100644 --- a/src/components/Common/NumericInput.spec.js +++ b/src/components/Common/NumericInput.spec.js @@ -8,28 +8,21 @@ import { configure, mount } from 'enzyme' configure({ adapter: new Adapter() }) describe('NumericInput', () => { - const COMPONENT_STATE = { - VALUE: 'value', - PRISTINE: 'pristine', - VALID: 'valid' - } const INPUT_EVENT = { KEYPRESS: 'keypress', CHANGE: 'change', PASTE: 'paste' } - let mock + let changeMock let keypressMock let pasteMock let numericInputComponent - let wrapperMemo - let wrapper - let inputMemo - let input + let wrapperMemo, wrapper + let inputMemo, input beforeEach(() => { - mock = { target: { value: '' } } + changeMock = { target: { value: '' } } keypressMock = { key: '1', preventDefault: jest.fn() } @@ -76,31 +69,27 @@ describe('NumericInput', () => { expect(pasteMock.preventDefault).toHaveBeenCalledTimes(0) }) it('Should call onValueUpdate callback on successful update', () => { - mock.target.value = '10' + changeMock.target.value = '10' - input().simulate(INPUT_EVENT.CHANGE, mock) - expect(wrapper().state(COMPONENT_STATE.VALUE)).toBe(parseFloat(mock.target.value)) - expect(wrapper().state(COMPONENT_STATE.PRISTINE)).toBeFalsy() - expect(wrapper().state(COMPONENT_STATE.VALID)).toBe(VALIDATION_TYPES.VALID) + input().simulate(INPUT_EVENT.CHANGE, changeMock) expect(numericInputComponent.onValueUpdate).toHaveBeenCalledTimes(1) - expect(numericInputComponent.onValueUpdate).toHaveBeenCalledWith(parseFloat(mock.target.value)) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledWith({ + value: 10, + pristine: false, + valid: VALIDATION_TYPES.VALID + }) }) describe('symbols', () => { it('Should consider empty string as valid', () => { - input().simulate(INPUT_EVENT.CHANGE, mock) + input().simulate(INPUT_EVENT.CHANGE, changeMock) expect(numericInputComponent.onValueUpdate).toHaveBeenCalled() - expect(wrapper().state(COMPONENT_STATE.VALID)).toBe(VALIDATION_TYPES.VALID) - expect(wrapper().state(COMPONENT_STATE.PRISTINE)).toBeFalsy() }) it('Should reject "-" symbol if it is not the first character', () => { numericInputComponent.min = -10 - mock.target.value = 5 + numericInputComponent.value = 5 keypressMock.key = '-' - input().simulate(INPUT_EVENT.CHANGE, mock) - expect(wrapper().state(COMPONENT_STATE.VALUE)).toBe(5) - input().simulate(INPUT_EVENT.KEYPRESS, keypressMock) expect(keypressMock.preventDefault).toHaveBeenCalled() }) @@ -113,10 +102,26 @@ describe('NumericInput', () => { input().simulate(INPUT_EVENT.KEYPRESS, keypressMock) expect(keypressMock.preventDefault).toHaveBeenCalledTimes(0) }) + it('Should accept "-" symbol if min is negative and accepts floating numbers', () => { + numericInputComponent.min = -10 + numericInputComponent.acceptFloat = true + keypressMock.key = '-' + + input().simulate(INPUT_EVENT.KEYPRESS, keypressMock) + expect(keypressMock.preventDefault).toHaveBeenCalledTimes(0) + }) it('Should reject "-" symbol if min is not negative', () => { numericInputComponent.min = 0 keypressMock.key = '-' + input().simulate(INPUT_EVENT.KEYPRESS, keypressMock) + expect(keypressMock.preventDefault).toHaveBeenCalled() + }) + it('Should reject "-" symbol if min is not negative and accepts floating numbers', () => { + numericInputComponent.min = 0 + numericInputComponent.acceptFloat = true + keypressMock.key = '-' + input().simulate(INPUT_EVENT.KEYPRESS, keypressMock) expect(keypressMock.preventDefault).toHaveBeenCalled() }) @@ -130,10 +135,26 @@ describe('NumericInput', () => { input().simulate(INPUT_EVENT.KEYPRESS, keypressMock) expect(keypressMock.preventDefault).toHaveBeenCalledTimes(0) }) + it('Should accept "-" symbol if max is negative and accepts floating numbers', () => { + numericInputComponent.max = -10 + numericInputComponent.acceptFloat = true + keypressMock.key = '-' + + input().simulate(INPUT_EVENT.KEYPRESS, keypressMock) + expect(keypressMock.preventDefault).toHaveBeenCalledTimes(0) + }) it('Should reject "-" symbol if max is not negative', () => { numericInputComponent.max = 10 keypressMock.key = '-' + input().simulate(INPUT_EVENT.KEYPRESS, keypressMock) + expect(keypressMock.preventDefault).toHaveBeenCalled() + }) + it('Should reject "-" symbol if max is not negative and accepts floating numbers', () => { + numericInputComponent.max = 0 + numericInputComponent.acceptFloat = true + keypressMock.key = '-' + input().simulate(INPUT_EVENT.KEYPRESS, keypressMock) expect(keypressMock.preventDefault).toHaveBeenCalled() }) @@ -144,42 +165,54 @@ describe('NumericInput', () => { describe('min', () => { it('Should fail if value is lesser than min', () => { numericInputComponent.min = 5 - mock.target.value = '4' - - input().simulate(INPUT_EVENT.CHANGE, mock) - expect(wrapper().state(COMPONENT_STATE.VALUE)).toBe(parseFloat(mock.target.value)) - expect(wrapper().state(COMPONENT_STATE.PRISTINE)).toBeFalsy() - expect(wrapper().state(COMPONENT_STATE.VALID)).toBe(VALIDATION_TYPES.INVALID) + changeMock.target.value = '4' + + input().simulate(INPUT_EVENT.CHANGE, changeMock) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledTimes(1) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledWith({ + value: 4, + pristine: false, + valid: VALIDATION_TYPES.INVALID + }) }) it('Should pass if value is greater than min', () => { numericInputComponent.min = 5 - mock.target.value = '8' - - input().simulate(INPUT_EVENT.CHANGE, mock) - expect(wrapper().state(COMPONENT_STATE.VALUE)).toBe(parseFloat(mock.target.value)) - expect(wrapper().state(COMPONENT_STATE.PRISTINE)).toBeFalsy() - expect(wrapper().state(COMPONENT_STATE.VALID)).toBe(VALIDATION_TYPES.VALID) + changeMock.target.value = '8' + + input().simulate(INPUT_EVENT.CHANGE, changeMock) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledTimes(1) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledWith({ + value: 8, + pristine: false, + valid: VALIDATION_TYPES.VALID + }) }) }) describe('max', () => { it('Should fail if value is greater than max', () => { numericInputComponent.max = 15 - mock.target.value = '20' - - input().simulate(INPUT_EVENT.CHANGE, mock) - expect(wrapper().state(COMPONENT_STATE.VALUE)).toBe(parseFloat(mock.target.value)) - expect(wrapper().state(COMPONENT_STATE.PRISTINE)).toBeFalsy() - expect(wrapper().state(COMPONENT_STATE.VALID)).toBe(VALIDATION_TYPES.INVALID) + changeMock.target.value = '20' + + input().simulate(INPUT_EVENT.CHANGE, changeMock) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledTimes(1) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledWith({ + value: 20, + pristine: false, + valid: VALIDATION_TYPES.INVALID + }) }) it('Should pass if value is lesser than min', () => { numericInputComponent.max = 15 - mock.target.value = '10' - - input().simulate(INPUT_EVENT.CHANGE, mock) - expect(wrapper().state(COMPONENT_STATE.VALUE)).toBe(parseFloat(mock.target.value)) - expect(wrapper().state(COMPONENT_STATE.PRISTINE)).toBeFalsy() - expect(wrapper().state(COMPONENT_STATE.VALID)).toBe(VALIDATION_TYPES.VALID) + changeMock.target.value = '10' + + input().simulate(INPUT_EVENT.CHANGE, changeMock) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledTimes(1) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledWith({ + value: 10, + pristine: false, + valid: VALIDATION_TYPES.VALID + }) }) }) }) @@ -193,89 +226,127 @@ describe('NumericInput', () => { describe('maxDecimals', () => { it('Should fail if value has more decimals', () => { - mock.target.value = '1.12345' - - input().simulate(INPUT_EVENT.CHANGE, mock) - expect(wrapper().state(COMPONENT_STATE.PRISTINE)).toBeFalsy() - expect(wrapper().state(COMPONENT_STATE.VALID)).toBe(VALIDATION_TYPES.INVALID) + changeMock.target.value = '1.12345' + + input().simulate(INPUT_EVENT.CHANGE, changeMock) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledTimes(1) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledWith({ + value: 1.12345, + pristine: false, + valid: VALIDATION_TYPES.INVALID + }) }) it('Should pass if value has less decimals', () => { - mock.target.value = '1.123' - - input().simulate(INPUT_EVENT.CHANGE, mock) - expect(wrapper().state(COMPONENT_STATE.PRISTINE)).toBeFalsy() - expect(wrapper().state(COMPONENT_STATE.VALID)).toBe(VALIDATION_TYPES.VALID) + changeMock.target.value = '1.123' + + input().simulate(INPUT_EVENT.CHANGE, changeMock) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledTimes(1) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledWith({ + value: 1.123, + pristine: false, + valid: VALIDATION_TYPES.VALID + }) }) it('Should pass if value has same decimals', () => { - mock.target.value = '1.1234' - - input().simulate(INPUT_EVENT.CHANGE, mock) - expect(wrapper().state(COMPONENT_STATE.PRISTINE)).toBeFalsy() - expect(wrapper().state(COMPONENT_STATE.VALID)).toBe(VALIDATION_TYPES.VALID) + changeMock.target.value = '1.1234' + + input().simulate(INPUT_EVENT.CHANGE, changeMock) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledTimes(1) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledWith({ + value: 1.1234, + pristine: false, + valid: VALIDATION_TYPES.VALID + }) }) }) describe('minDecimals', () => { it('Should fail if value has less decimals', () => { - mock.target.value = '1.1' - - input().simulate(INPUT_EVENT.CHANGE, mock) - expect(wrapper().state(COMPONENT_STATE.PRISTINE)).toBeFalsy() - expect(wrapper().state(COMPONENT_STATE.VALID)).toBe(VALIDATION_TYPES.INVALID) + changeMock.target.value = '1.1' + + input().simulate(INPUT_EVENT.CHANGE, changeMock) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledTimes(1) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledWith({ + value: 1.1, + pristine: false, + valid: VALIDATION_TYPES.INVALID + }) }) it('Should pass if value has more decimals', () => { - mock.target.value = '1.1234' - - input().simulate(INPUT_EVENT.CHANGE, mock) - expect(wrapper().state(COMPONENT_STATE.PRISTINE)).toBeFalsy() - expect(wrapper().state(COMPONENT_STATE.VALID)).toBe(VALIDATION_TYPES.VALID) + changeMock.target.value = '1.1234' + + input().simulate(INPUT_EVENT.CHANGE, changeMock) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledTimes(1) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledWith({ + value: 1.1234, + pristine: false, + valid: VALIDATION_TYPES.VALID + }) }) it('Should pass if value has same decimals', () => { - mock.target.value = '1.12' - - input().simulate(INPUT_EVENT.CHANGE, mock) - expect(wrapper().state(COMPONENT_STATE.PRISTINE)).toBeFalsy() - expect(wrapper().state(COMPONENT_STATE.VALID)).toBe(VALIDATION_TYPES.VALID) + changeMock.target.value = '1.12' + + input().simulate(INPUT_EVENT.CHANGE, changeMock) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledTimes(1) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledWith({ + value: 1.12, + pristine: false, + valid: VALIDATION_TYPES.VALID + }) }) }) describe('range [minDecimals, maxDecimals]', () => { it('Should fail for: 1.1, if it is outside the range', () => { - mock.target.value = '1.1' - - input().simulate(INPUT_EVENT.CHANGE, mock) - expect(wrapper().state(COMPONENT_STATE.PRISTINE)).toBeFalsy() - expect(wrapper().state(COMPONENT_STATE.VALID)).toBe(VALIDATION_TYPES.INVALID) + changeMock.target.value = '1.1' + + input().simulate(INPUT_EVENT.CHANGE, changeMock) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledTimes(1) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledWith({ + value: 1.1, + pristine: false, + valid: VALIDATION_TYPES.INVALID + }) }) it('Should fail for: 1.12345, if it is outside the range', () => { - mock.target.value = '1.12345' - - input().simulate(INPUT_EVENT.CHANGE, mock) - expect(wrapper().state(COMPONENT_STATE.PRISTINE)).toBeFalsy() - expect(wrapper().state(COMPONENT_STATE.VALID)).toBe(VALIDATION_TYPES.INVALID) + changeMock.target.value = '1.12345' + + input().simulate(INPUT_EVENT.CHANGE, changeMock) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledTimes(1) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledWith({ + value: 1.12345, + pristine: false, + valid: VALIDATION_TYPES.INVALID + }) }) it('Should pass for: 1.12, if it is inside the range', () => { - mock.target.value = '1.12' - - input().simulate(INPUT_EVENT.CHANGE, mock) - expect(wrapper().state(COMPONENT_STATE.PRISTINE)).toBeFalsy() - expect(wrapper().state(COMPONENT_STATE.VALID)).toBe(VALIDATION_TYPES.VALID) + changeMock.target.value = '1.12' + + input().simulate(INPUT_EVENT.CHANGE, changeMock) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledTimes(1) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledWith({ + value: 1.12, + pristine: false, + valid: VALIDATION_TYPES.VALID + }) }) it('Should pass for: 1.123, if it is inside the range', () => { - mock.target.value = '1.123' - - input().simulate(INPUT_EVENT.CHANGE, mock) - expect(wrapper().state(COMPONENT_STATE.PRISTINE)).toBeFalsy() - expect(wrapper().state(COMPONENT_STATE.VALID)).toBe(VALIDATION_TYPES.VALID) + changeMock.target.value = '1.123' + + input().simulate(INPUT_EVENT.CHANGE, changeMock) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledTimes(1) + expect(numericInputComponent.onValueUpdate).toHaveBeenCalledWith({ + value: 1.123, + pristine: false, + valid: VALIDATION_TYPES.VALID + }) }) }) describe('dot notation', () => { it('Should reject float with dot notation', () => { numericInputComponent.acceptFloat = false - - input().simulate(INPUT_EVENT.KEYPRESS, keypressMock) - expect(keypressMock.preventDefault).toHaveBeenCalledTimes(0) + numericInputComponent.value = 1 keypressMock.key = '.' @@ -283,22 +354,21 @@ describe('NumericInput', () => { expect(keypressMock.preventDefault).toHaveBeenCalled() }) it('Should accept float with dot notation', () => { - input().simulate(INPUT_EVENT.KEYPRESS, keypressMock) - expect(keypressMock.preventDefault).toHaveBeenCalledTimes(0) + numericInputComponent.minDecimals = 0 + numericInputComponent.value = 1 keypressMock.key = '.' input().simulate(INPUT_EVENT.KEYPRESS, keypressMock) expect(keypressMock.preventDefault).toHaveBeenCalledTimes(0) + expect() }) }) describe('scientific notation', () => { it('Should reject float with scientific notation', () => { numericInputComponent.acceptFloat = false - - input().simulate(INPUT_EVENT.KEYPRESS, keypressMock) - expect(keypressMock.preventDefault).toHaveBeenCalledTimes(0) + numericInputComponent.value = 1 keypressMock.key = 'e' @@ -306,8 +376,7 @@ describe('NumericInput', () => { expect(keypressMock.preventDefault).toHaveBeenCalled() }) it('Should accept float with scientific notation', () => { - input().simulate(INPUT_EVENT.KEYPRESS, keypressMock) - expect(keypressMock.preventDefault).toHaveBeenCalledTimes(0) + numericInputComponent.value = 1 keypressMock.key = 'e' diff --git a/src/components/Common/__snapshots__/NumericInput.spec.js.snap b/src/components/Common/__snapshots__/NumericInput.spec.js.snap index 4e04fd04f..3d472f370 100644 --- a/src/components/Common/__snapshots__/NumericInput.spec.js.snap +++ b/src/components/Common/__snapshots__/NumericInput.spec.js.snap @@ -24,5 +24,16 @@ exports[`NumericInput Should render the component 1`] = ` > Refers to how divisible a token can be, from 0 (not at all divisible) to 18 (pretty much continuous).

+

`; From a6a2e384af07787d5a690adb3203f89826294025 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 26 Feb 2018 10:37:55 -0300 Subject: [PATCH 03/16] Update stepTwo to handle decimals state --- src/components/stepTwo/index.js | 78 +++++++++++++++++++++++++-- src/stores/TokenStore.js | 8 +-- src/utils/utils.js | 93 +-------------------------------- 3 files changed, 81 insertions(+), 98 deletions(-) diff --git a/src/components/stepTwo/index.js b/src/components/stepTwo/index.js index 4f2dcd3a8..e0def6c02 100644 --- a/src/components/stepTwo/index.js +++ b/src/components/stepTwo/index.js @@ -6,14 +6,38 @@ import { StepNavigation } from '../Common/StepNavigation' import { InputField } from '../Common/InputField' import { NumericInput } from '../Common/NumericInput' import { ReservedTokensInputBlock } from '../Common/ReservedTokensInputBlock' -import { NAVIGATION_STEPS, VALIDATION_MESSAGES, TEXT_FIELDS, DESCRIPTION } from '../../utils/constants' +import { + NAVIGATION_STEPS, + VALIDATION_MESSAGES, + TEXT_FIELDS, + DESCRIPTION, + VALIDATION_TYPES +} from '../../utils/constants' import { inject, observer } from 'mobx-react'; +import update from 'immutability-helper' const { TOKEN_SETUP } = NAVIGATION_STEPS const { NAME, TICKER, DECIMALS } = TEXT_FIELDS +const { VALID, INVALID } = VALIDATION_TYPES -@inject('tokenStore', 'web3Store', 'tierCrowdsaleListStore', 'reservedTokenStore') @observer +@inject('tokenStore', 'web3Store', 'tierCrowdsaleListStore', 'reservedTokenStore') +@observer export class stepTwo extends Component { + constructor(props) { + super(props) + + this.state = { + decimals: '', + validation: { + decimals: { + disabled: false, + pristine: true, + valid: INVALID + } + } + } + } + componentDidMount() { checkWeb3(this.props.web3Store.web3); } @@ -28,8 +52,37 @@ export class stepTwo extends Component { this.props.tokenStore.validateTokens(property); } - updateDecimalsStore = value => { - this.updateTokenStore({ target: { value } }, 'decimals') + updateDecimalsStore = ({ value, pristine, valid }) => { + const newState = update(this.state, { + validation: { + decimals: { + $set: { + pristine: pristine, + valid: valid + }, + }, + }, + }) + newState.decimals = value + + this.setState(newState) + + // TODO: store should only be updated when the user hits 'Continue' + const { tokenStore } = this.props + tokenStore.setProperty('decimals', value) + tokenStore.updateValidity('decimals', valid) + } + + disableDecimals = () => { + if (this.state.decimals === '') { + this.updateDecimalsStore({ value: 0, pristine: false, valid: true }) + } + + this.setState({ validation: { decimals: { disabled: true } } }) + } + + enableDecimals = () => { + this.setState({ validation: { decimals: { disabled: false } } }) } renderLink () { @@ -44,10 +97,22 @@ export class stepTwo extends Component { } removeReservedToken = index => { + const lastReservedItem = this.props.reservedTokenStore.tokens.length === 1 + + if (lastReservedItem) { + this.enableDecimals() + } + this.props.reservedTokenStore.removeToken(index) } addReservedTokensItem = newToken => { + const firstReservedItem = !this.props.reservedTokenStore.tokens.length + + if (firstReservedItem) { + this.disableDecimals() + } + this.props.reservedTokenStore.addToken(newToken) } @@ -82,11 +147,15 @@ export class stepTwo extends Component { description={`${DESCRIPTION.TOKEN_TICKER} There are 11,881,376 combinations for 26 english letters. Be hurry. `} /> @@ -96,6 +165,7 @@ export class stepTwo extends Component { diff --git a/src/stores/TokenStore.js b/src/stores/TokenStore.js index 41e52d815..50edcbb0c 100644 --- a/src/stores/TokenStore.js +++ b/src/stores/TokenStore.js @@ -1,5 +1,5 @@ import { observable, action, computed } from 'mobx'; -import { validateName, validateTicker, validateDecimals } from '../utils/utils' +import { validateName, validateTicker } from '../utils/utils' import { VALIDATION_TYPES } from '../utils/constants' import autosave from './autosave' const { EMPTY, VALID, INVALID } = VALIDATION_TYPES; @@ -42,11 +42,13 @@ class TokenStore { this.validToken[property] = validateName(this.name) ? VALID : INVALID; } else if (property === 'ticker') { this.validToken[property] = validateTicker(this.ticker) ? VALID : INVALID; - } else { - this.validToken[property] = validateDecimals(this.decimals) ? VALID : INVALID; } } + @action updateValidity = (property, validity) => { + this.validToken[property] = validity + } + @action invalidateToken = () => { if (!this.validToken) { return; diff --git a/src/utils/utils.js b/src/utils/utils.js index 670023939..a5fe455f4 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -1,9 +1,7 @@ -import { VALIDATION_TYPES, TOAST } from './constants' +import { TOAST } from './constants' import queryString from 'query-string' import { CrowdsaleConfig } from '../components/Common/config' -const { VALID, INVALID } = VALIDATION_TYPES - export function getQueryVariable(variable) { return queryString.parse(window.location.search)[variable] } @@ -72,22 +70,12 @@ export const getOldState = (props, defaultState) => (props && props.location && export const getStepClass = (step, activeStep) => step === activeStep ? "step-navigation step-navigation_active" : "step-navigation" -export const stepsAreValid = (steps) => { - let newSteps = Object.assign({}, steps) - if (newSteps[0] !== undefined) { - delete newSteps[0] - } - return Object.values(newSteps).length > 3 && Object.values(newSteps).every(step => step === VALID) -} - export const validateTier = (tier) => typeof tier === 'string' && tier.length > 0 && tier.length < 30 export const validateName = (name) => typeof name === 'string' && name.length > 0 && name.length < 30 export const validateSupply = (supply) => isNaN(Number(supply)) === false && Number(supply) > 0 -export const validateDecimals = (decimals) => /^$|^([0-9]|[1][0-8])$/.test(decimals) - export const validateTicker = (ticker) => typeof ticker === 'string' && ticker.length <= 5 && ticker.length > 0 export const validateTime = (time) => getTimeAsNumber(time) > Date.now() @@ -98,84 +86,7 @@ export const validateLaterOrEqualTime = (laterTime, previousTime) => getTimeAsNu export const validateRate = (rate) => isNaN(Number(rate)) === false && Number(rate) > 0 -export const validateAddress = (address) => { - if(!address || address.length !== 42 ) { - return false - } - return true -} - -const inputFieldValidators = { - tier: validateTier, - name: validateName, - ticker: validateTicker, - decimals: validateDecimals, - supply: validateSupply, - startTime: validateTime, - endTime: validateTime, - walletAddress: validateAddress, - rate: validateRate -} - -const isNotWhiteListTierObject = (value) => !(typeof value === 'object' && value.hasOwnProperty('whitelist') === true && value.hasOwnProperty('tier') === true) - -// still thinks that we do not have an array... we do -export const validateValue = (value, property) => { - if (!isNaN(property) - || property === 'reservedTokensInput' - || property === 'reservedTokens' - || property === 'reservedTokensElements') return VALID; - let validationFunction, valueIsValid; - if(isNotWhiteListTierObject(value)) { - validationFunction = inputFieldValidators[property] - if (validationFunction) - valueIsValid = validationFunction(value) - } else if (inputFieldValidators[property]){ - validationFunction = inputFieldValidators[property] - if (validationFunction) - valueIsValid = validationFunction(value[property]) - } - return valueIsValid === true ? VALID : INVALID -} - -export const getNewValue = (value, property) => property === "startTime" || property === "endTime" ? getTimeAsNumber(value) : value - -export const allFieldsAreValid = (parent, state) => { - let newState = { ...state } - let properties = [] - let values = [] - if( Object.prototype.toString.call( newState[parent] ) === '[object Array]' ) { - if (newState[parent].length > 0) { - for (let i = 0; i < newState[parent].length; i++) { - Object.keys(newState[parent][i]).forEach(property => { // eslint-disable-line no-loop-func - values.push(newState[parent][i][property]) - properties.push(property); - }) - } - } - } else { - properties = Object.keys(newState[parent]) - } - let iterator = 0 - let validationValues = properties.map(property => { - if (property === 'startBlock' || property === 'endBlock' || property === 'updatable' || property.toLowerCase().indexOf("whitelist") > -1) { - iterator++ - return VALID - } - let value - if( Object.prototype.toString.call( newState[parent] ) === '[object Array]' ) { - if (newState[parent].length > 0) - value = values[iterator] - } else { - value = newState[parent][property] - } - iterator++ - if (parent === "token" && property === "supply") return VALID - return validateValue(value, property) - }) - - return validationValues.find(value => value === INVALID) === undefined -} +export const validateAddress = (address) => !(!address || address.length !== 42) export function toFixed(x) { if (Math.abs(x) < 1.0) { From 8ea0d3c361962acc5b465d8bc38bc44141d1241a Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 26 Feb 2018 10:38:36 -0300 Subject: [PATCH 04/16] Update ReservedTokensInputBlock handle VALUE state --- .../Common/ReservedTokensInputBlock.js | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src/components/Common/ReservedTokensInputBlock.js b/src/components/Common/ReservedTokensInputBlock.js index ed2aceaa4..a8d57dbf8 100644 --- a/src/components/Common/ReservedTokensInputBlock.js +++ b/src/components/Common/ReservedTokensInputBlock.js @@ -7,6 +7,7 @@ import { TEXT_FIELDS, VALIDATION_TYPES } from '../../utils/constants' import update from 'immutability-helper' import ReservedTokensItem from './ReservedTokensItem' import { observer } from 'mobx-react'; +import { NumericInput } from './NumericInput' const { VALID, INVALID } = VALIDATION_TYPES const { ADDRESS, DIMENSION, VALUE } = TEXT_FIELDS @@ -113,15 +114,13 @@ export class ReservedTokensInputBlock extends Component { this.setState(newState) } - handleValueChange = value => { - const isValueValid = value > 0 ? VALID : INVALID - + handleValueChange = ({ value, pristine, valid }) => { const newState = update(this.state, { validation: { value: { $set: { - pristine: false, - valid: isValueValid + pristine: pristine, + valid: valid } } } @@ -145,6 +144,25 @@ export class ReservedTokensInputBlock extends Component { ) }) + let valueInputParams = null + + if (this.state.dim === 'tokens') { + valueInputParams = { + min: !this.props.decimals ? 0 : Number(`1e-${this.props.decimals}`), + maxDecimals: !this.props.decimals ? 0 : this.props.decimals, + errorMessage: 'Value must be positive and decimals should not exceed the amount of decimals specified', + description: 'Value in tokens. Don\'t forget to click + button for each reserved token.' + } + } else if (this.state.dim === 'percentage') { + valueInputParams = { + min: Number.MIN_VALUE, + errorMessage: 'Value must be positive', + description: 'Value in percentage. Don\'t forget to click + button for each reserved token.' + } + } else { + console.error(`unrecognized dimension '${this.state.dim}'`) + } + return (

@@ -169,16 +187,15 @@ export class ReservedTokensInputBlock extends Component { description="Fixed amount or % of crowdsaled tokens. Will be deposited to the account after finalization of the crowdsale." /> - this.handleValueChange(e.target.value)} - description="Value in tokens or percents. Don't forget to press + button for each reserved token." pristine={this.state.validation.value.pristine} valid={this.state.validation.value.valid} - errorMessage="Value must be positive" + acceptFloat={true} + onValueUpdate={this.handleValueChange} + {...valueInputParams} />
From b8568a0aba7287513b550be71af1638bf0771009 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 26 Feb 2018 10:38:56 -0300 Subject: [PATCH 05/16] Update ReservedTokensInputBlock spec --- .../Common/ReservedTokensInputBlock.spec.js | 713 ++++++++++-------- .../ReservedTokensInputBlock.spec.js.snap | 295 +++++++- 2 files changed, 684 insertions(+), 324 deletions(-) diff --git a/src/components/Common/ReservedTokensInputBlock.spec.js b/src/components/Common/ReservedTokensInputBlock.spec.js index 916358e1a..dd657a4f4 100644 --- a/src/components/Common/ReservedTokensInputBlock.spec.js +++ b/src/components/Common/ReservedTokensInputBlock.spec.js @@ -6,364 +6,433 @@ import Adapter from 'enzyme-adapter-react-15' import { configure, mount } from 'enzyme' const { VALID, INVALID } = VALIDATION_TYPES -let tokenList -let addCallback -let removeCallback - -beforeEach(() => { - tokenList = [ - { - addr: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1', - dim: 'tokens', - value: '120' - }, - { - addr: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1', - dim: 'percentage', - value: '105' - }, - { - addr: '0xffcf8fdee72ac11b5c542428b35eef5769c409f0', - dim: 'percentage', - value: '100' - }, - { - addr: '0x22d491bde2303f2f43325b2108d26f1eaba1e32b', - dim: 'tokens', - value: '20' - } - ] - addCallback = jest.fn() - removeCallback = jest.fn() -}) configure({ adapter: new Adapter() }) describe('ReservedTokensInputBlock', () => { - test('Should render the component', () => { - const component = renderer.create( + let tokenList + let addCallback + let removeCallback + let decimals + let wrapperMemo, wrapper + let addressInputMemo, addressInput + let valueInputMemo, valueInput + let addressStateMemo, addressState + let valueStateMemo, valueState + + beforeEach(() => { + tokenList = [ + { + addr: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1', + dim: 'tokens', + value: '120' + }, + { + addr: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1', + dim: 'percentage', + value: '105' + }, + { + addr: '0xffcf8fdee72ac11b5c542428b35eef5769c409f0', + dim: 'percentage', + value: '100' + }, + { + addr: '0x22d491bde2303f2f43325b2108d26f1eaba1e32b', + dim: 'tokens', + value: '20' + } + ] + addCallback = jest.fn() + removeCallback = jest.fn() + decimals = 3 + + wrapperMemo = undefined + wrapper = () => wrapperMemo || (wrapperMemo = mount( - ) - const tree = component.toJSON() - expect(tree).toMatchSnapshot() - }) + )) - test('Should render all the tokens passed', () => { - const wrapper = mount( - - ) + addressInputMemo = undefined + addressInput = () => addressInputMemo || (addressInputMemo = wrapper().find('input[type="text"]').at(0)) - expect(wrapper.find('.reserved-tokens-item-container')).toHaveLength(tokenList.length) - }) + valueInputMemo = undefined + valueInput = () => valueInputMemo || (valueInputMemo = wrapper().find('input[type="number"]').at(0)) - test('Should validate the address', () => { - const wrapper = mount( - - ) - const wrongAddressEvent = { target: { value: '0x123' } } - const validAddressEvent = { target: { value: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1' } } + addressStateMemo = undefined + addressState = () => addressStateMemo || (addressStateMemo = wrapper().state('validation').address) - const handleAddressChange = jest.spyOn(wrapper.instance(), 'handleAddressChange') - - wrapper.update() - - wrapper.find('input[type="text"]').at(0).simulate('change', wrongAddressEvent) - setTimeout(() => { - expect(handleAddressChange).toHaveBeenCalledTimes(1) - expect(handleAddressChange).toHaveBeenCalledWith(wrongAddressEvent.target.value) - expect(wrapper.state('validation').address.pristine).toBeFalsy() - expect(wrapper.state('validation').address.valid).toBe(INVALID) - - wrapper.find('input[type="text"]').at(0).simulate('change', validAddressEvent) - setTimeout(() => { - expect(handleAddressChange).toHaveBeenCalledTimes(1) - expect(handleAddressChange).toHaveBeenCalledWith(validAddressEvent.target.value) - expect(wrapper.state('validation').address.pristine).toBeFalsy() - expect(wrapper.state('validation').address.valid).toBe(VALID) - }, 10) - }, 10) + valueStateMemo = undefined + valueState = () => valueStateMemo || (valueStateMemo = wrapper().state('validation').value) + }) + describe('render', () => { + let componentMemo, component + beforeEach(() => { + componentMemo = undefined + component = () => componentMemo || (componentMemo = renderer.create( + + )) + }) + + it('Should render the component for tokens', () => { + expect(component().toJSON()).toMatchSnapshot('tokens') + }) + + it('Should render the component for percentage', () => { + wrapper().find('input[type="radio"]').at(1).simulate('change') + expect(component().toJSON()).toMatchSnapshot('percentage') + }) + + it('Should render all the tokens passed', () => { + expect(wrapper().find('.reserved-tokens-item-container')).toHaveLength(tokenList.length) + }) }) - test('Should fail if tokens value is invalid (zero or less)', () => { - const wrapper = mount( - - ) - const valueEventZero = { target: { value: '0' } } - const valueEventNegative = { target: { value: '-10' } } + describe('Address field', () => { + [ + { value: '0x123', expected: false }, + { value: '0x90F8bF6A479f320EAD074411A4B0E7944EA8C9C1', expected: false }, + { value: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1', expected: true }, + { value: '0X90f8bf6a479f320ead074411a4b0e7944ea8c9c1', expected: true }, + { value: '0x90F8BF6A479F320EAD074411A4B0E7944EA8C9C1', expected: true }, + { value: '0X90F8BF6A479F320EAD074411A4B0E7944EA8C9C1', expected: true }, + ].forEach(testCase => { + const action = testCase.expected ? 'pass' : 'fail' + const validity = testCase.expected ? VALID : INVALID - const handleValueChange = jest.spyOn(wrapper.instance(), 'handleValueChange') - const updateReservedTokenInput = jest.spyOn(wrapper.instance(), 'updateReservedTokenInput') + it(`Should ${action} for '${testCase.value}'`, () => { + const handleAddressChange = jest.spyOn(wrapper().instance(), 'handleAddressChange') - wrapper.update() + wrapper().update() - wrapper.find('input[type="radio"]').at(0).simulate('change') - setTimeout(() => { - expect(updateReservedTokenInput).toHaveBeenCalledWith(['tokens', 'dim']) - expect(updateReservedTokenInput).toHaveBeenCalledTimes(1) - expect(wrapper.state('dim')).toBe('tokens') - - wrapper.find('input[type="number"]').at(0).simulate('change', valueEventZero) - setTimeout(() => { - expect(handleValueChange).toHaveBeenCalledWith(valueEventZero.target.value) - expect(handleValueChange).toHaveBeenCalledTimes(1) - expect(wrapper.state('validation').value.pristine).toBeFalsy() - expect(wrapper.state('validation').value.valid).toBe(INVALID) - - wrapper.find('input[type="number"]').at(0).simulate('change', valueEventNegative) - setTimeout(() => { - expect(handleValueChange).toHaveBeenCalledWith(valueEventNegative.target.value) - expect(handleValueChange).toHaveBeenCalledTimes(1) - expect(wrapper.state('validation').value.pristine).toBeFalsy() - expect(wrapper.state('validation').value.valid).toBe(INVALID) - }, 10) - }, 10) - }, 10) + addressInput().simulate('change', { target: { value: testCase.value } }) + expect(handleAddressChange).toHaveBeenCalledTimes(1) + expect(handleAddressChange).toHaveBeenCalledWith(testCase.value) + expect(addressState().pristine).toBeFalsy() + expect(addressState().valid).toBe(validity) + }) + }) }) - test('Should fail if percentage value is invalid (zero or less)', () => { - const wrapper = mount( - - ) - const valueEventZero = { target: { value: '0' } } - const valueEventNegative = { target: { value: '-10' } } - - const handleValueChange = jest.spyOn(wrapper.instance(), 'handleValueChange') - const updateReservedTokenInput = jest.spyOn(wrapper.instance(), 'updateReservedTokenInput') + describe('Dimension selection', () => { + it('Should set "tokens" as selected dimension', () => { + const updateReservedTokenInput = jest.spyOn(wrapper().instance(), 'updateReservedTokenInput') - wrapper.update() + wrapper().update() - wrapper.find('input[type="radio"]').at(1).simulate('change') - setTimeout(() => { - expect(updateReservedTokenInput).toHaveBeenCalledWith(['percentage', 'dim']) + wrapper().find('input[type="radio"]').at(0).simulate('change') + expect(updateReservedTokenInput).toHaveBeenCalledWith('tokens', 'dim') expect(updateReservedTokenInput).toHaveBeenCalledTimes(1) - expect(wrapper.state('dim')).toBe('percentage') - - wrapper.find('input[type="number"]').at(0).simulate('change', valueEventZero) - setTimeout(() => { - expect(handleValueChange).toHaveBeenCalledWith(valueEventZero.target.value) - expect(handleValueChange).toHaveBeenCalledTimes(1) - expect(wrapper.state('validation').value.pristine).toBeFalsy() - expect(wrapper.state('validation').value.valid).toBe(INVALID) - - wrapper.find('input[type="number"]').at(0).simulate('change', valueEventNegative) - setTimeout(() => { - expect(handleValueChange).toHaveBeenCalledWith(valueEventNegative.target.value) - expect(handleValueChange).toHaveBeenCalledTimes(1) - expect(wrapper.state('validation').value.pristine).toBeFalsy() - expect(wrapper.state('validation').value.valid).toBe(INVALID) - }, 10) - }, 10) - }, 10) - }) + expect(wrapper().state('dim')).toBe('tokens') + }) + it('Should set "percentage" as selected dimension', () => { + const updateReservedTokenInput = jest.spyOn(wrapper().instance(), 'updateReservedTokenInput') - test('Should mark as valid if tokens value is valid (greater than zero)', () => { - const wrapper = mount( - - ) - const valueEvent = { target: { value: '150' } } + wrapper().update() - const handleValueChange = jest.spyOn(wrapper.instance(), 'handleValueChange') - const updateReservedTokenInput = jest.spyOn(wrapper.instance(), 'updateReservedTokenInput') + wrapper().find('input[type="radio"]').at(1).simulate('change') + expect(updateReservedTokenInput).toHaveBeenCalledWith('percentage', 'dim') + expect(updateReservedTokenInput).toHaveBeenCalledTimes(1) + expect(wrapper().state('dim')).toBe('percentage') + }) + it('Should set "tokens" as selected dimension after selecting percentage', () => { + const updateReservedTokenInput = jest.spyOn(wrapper().instance(), 'updateReservedTokenInput') - wrapper.update() + wrapper().update() - wrapper.find('input[type="radio"]').at(0).simulate('change') - setTimeout(() => { - expect(updateReservedTokenInput).toHaveBeenCalledWith(['tokens', 'dim']) + wrapper().find('input[type="radio"]').at(1).simulate('change') + expect(updateReservedTokenInput).toHaveBeenCalledWith('percentage', 'dim') expect(updateReservedTokenInput).toHaveBeenCalledTimes(1) - expect(wrapper.state('dim')).toBe('tokens') - - wrapper.find('input[type="number"]').at(0).simulate('change', valueEvent) - setTimeout(() => { - expect(handleValueChange).toHaveBeenCalledWith(valueEvent.target.value) - expect(handleValueChange).toHaveBeenCalledTimes(1) - expect(wrapper.state('validation').value.pristine).toBeFalsy() - expect(wrapper.state('validation').value.valid).toBe(VALID) - }, 10) - }, 10) + expect(wrapper().state('dim')).toBe('percentage') + + wrapper().find('input[type="radio"]').at(0).simulate('change') + expect(updateReservedTokenInput).toHaveBeenCalledWith('tokens', 'dim') + expect(updateReservedTokenInput).toHaveBeenCalledTimes(2) + expect(wrapper().state('dim')).toBe('tokens') + }) + it('Should fail if state.dim is not "percentage" or "tokens"', () => { + wrapper().setState({dim: 'height'}) + const radio = wrapper().find('input[type="radio"]') + const tokens = radio.at(0) + const percentage = radio.at(1) + + expect(tokens.props().checked).toBeFalsy() + expect(percentage.props().checked).toBeFalsy() + }) }) - test('Should mark as valid if percentage value is valid (greater than zero)', () => { - const wrapper = mount( - - ) - const valueEventSmall = { target: { value: '75' } } - const valueEventBig = { target: { value: '180' } } - - const handleValueChange = jest.spyOn(wrapper.instance(), 'handleValueChange') - const updateReservedTokenInput = jest.spyOn(wrapper.instance(), 'updateReservedTokenInput') + describe('Value field', () => { + describe('tokens', () => { + [ + { value: '0', expected: false }, + { value: '-10', expected: false }, + { value: -10, expected: false }, + { value: 123.1234, expected: false }, + { value: '10', expected: true }, + { value: 10, expected: true }, + { value: '0.1', expected: true }, + { value: '1e-3', expected: true }, + { value: '1.3', expected: true }, + ].forEach(testCase => { + const action = testCase.expected ? 'pass' : 'fail' + const validity = testCase.expected ? VALID : INVALID + const calledWith = { + value: testCase.value === '' ? '' : parseFloat(testCase.value), + pristine: false, + valid: validity + } + + it(`Should ${action} for '${testCase.value}' with ${decimals} decimals`, () => { + const handleValueChange = jest.spyOn(wrapper().instance(), 'handleValueChange') + + wrapper().update() + wrapper().find('input[type="radio"]').at(0).simulate('change') + valueInput().simulate('change', { target: { value: testCase.value } }) - wrapper.update() - - wrapper.find('input[type="radio"]').at(1).simulate('change') - setTimeout(() => { - expect(updateReservedTokenInput).toHaveBeenCalledWith(['percentage', 'dim']) - expect(updateReservedTokenInput).toHaveBeenCalledTimes(1) - expect(wrapper.state('dim')).toBe('percentage') - - wrapper.find('input[type="number"]').at(0).simulate('change', valueEventSmall) - setTimeout(() => { - expect(handleValueChange).toHaveBeenCalledWith(valueEventSmall.target.value) - expect(handleValueChange).toHaveBeenCalledTimes(1) - expect(wrapper.state('validation').value.pristine).toBeFalsy() - expect(wrapper.state('validation').value.valid).toBe(VALID) - - wrapper.find('input[type="number"]').at(0).simulate('change', valueEventBig) - setTimeout(() => { - expect(handleValueChange).toHaveBeenCalledWith(valueEventBig.target.value) expect(handleValueChange).toHaveBeenCalledTimes(1) - expect(wrapper.state('validation').value.pristine).toBeFalsy() - expect(wrapper.state('validation').value.valid).toBe(VALID) - }, 10) - }, 10) - }, 10) + expect(handleValueChange).toHaveBeenCalledWith(calledWith) + }) + }) + }) + + describe('percentage', () => { + [ + { value: '0', expected: false }, + { value: '-10', expected: false }, + { value: -10, expected: false }, + { value: '10', expected: true }, + { value: 10, expected: true }, + { value: '0.1', expected: true }, + { value: '1e-3', expected: true }, + { value: '1.3', expected: true }, + { value: 123.1234, expected: true }, + ].forEach(testCase => { + const action = testCase.expected ? 'pass' : 'fail' + const validity = testCase.expected ? VALID : INVALID + const calledWith = { + value: testCase.value === '' ? '' : parseFloat(testCase.value), + pristine: false, + valid: validity + } + + it(`Should ${action} for '${testCase.value}'`, () => { + const handleValueChange = jest.spyOn(wrapper().instance(), 'handleValueChange') + + wrapper().update() + wrapper().find('input[type="radio"]').at(1).simulate('change') + valueInput().simulate('change', { target: { value: testCase.value } }) + + expect(handleValueChange).toHaveBeenCalledTimes(2) + expect(handleValueChange).toHaveBeenCalledWith({ + value: '', + pristine: true, + valid: INVALID + }) + expect(handleValueChange).toHaveBeenCalledWith(calledWith) + }) + }) + }) }) - test('Should call method to add address', () => { - const wrapper = mount( - - ) - const newToken = { - addr: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1', - dim: 'tokens', - val: '120' - } - const addressEvent = { target: { value: newToken.addr } } - const valueEvent = { target: { value: newToken.val } } - - const handleAddressChange = jest.spyOn(wrapper.instance(), 'handleAddressChange') - const handleValueChange = jest.spyOn(wrapper.instance(), 'handleValueChange') - - wrapper.update() - - wrapper.find('input[type="text"]').at(0).simulate('change', addressEvent) - wrapper.find('input[type="number"]').at(0).simulate('change', valueEvent) - wrapper.find('.plus-button-container').childAt(0).simulate('click') - - setTimeout(() => { - expect(wrapper.state('validation').address.pristine).toBeFalsy() - expect(wrapper.state('validation').address.valid).toBe(VALID) - expect(wrapper.state('addr')).toBe(newToken.addr) - expect(handleAddressChange).toHaveBeenCalledTimes(1) - expect(handleAddressChange).toHaveBeenCalledWith(newToken.addr) - - expect(wrapper.state('validation').value.pristine).toBeFalsy() - expect(wrapper.state('validation').value.valid).toBe(VALID) - expect(wrapper.state('val')).toBe(newToken.val) - expect(handleValueChange).toHaveBeenCalledTimes(1) - expect(handleValueChange).toHaveBeenCalledWith(newToken.val) - - expect(wrapper.state('dim')).toBe(newToken.dim) - - expect(addCallback).toHaveBeenCalledWith(newToken) - expect(addCallback).toHaveBeenCalledTimes(1) - }, 10) + describe('Callbacks', () => { + describe('addReservedTokensItem', () => { + [ + { + value: { + addr: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1', + dim: 'tokens', + val: '120' + }, + expected: true + }, + { + value: { + addr: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1', + dim: 'tokens', + val: '0.001' + }, + expected: true + }, + { + value: { + addr: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1', + dim: 'tokens', + val: '' + }, + expected: false + }, + { + value: { + addr: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1', + dim: 'tokens', + val: '0' + }, + expected: false + }, + { + value: { + addr: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1', + dim: 'tokens', + val: '120.1234' + }, + expected: false + }, + { + value: { + addr: '', + dim: 'tokens', + val: '120' + }, + expected: false + }, + { + value: { + addr: '0x90f8bf6a4', + dim: 'tokens', + val: '120' + }, + expected: false + }, + { + value: { + addr: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c10x90f8bf6a479f320ead074411a4b0e7944ea8c9c1', + dim: 'tokens', + val: '120' + }, + expected: false + }, + { + value: { + addr: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1', + dim: 'percentage', + val: '120' + }, + expected: true + }, + { + value: { + addr: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1', + dim: 'percentage', + val: '120.123' + }, + expected: true + }, + { + value: { + addr: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1', + dim: 'percentage', + val: '' + }, + expected: false + }, + { + value: { + addr: '', + dim: 'percentage', + val: '120.123' + }, + expected: false + }, + { + value: { + addr: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1', + dim: 'percentage', + val: '0' + }, + expected: false + }, + ].forEach(testCase => { + const radioIndex = testCase.value.dim === 'tokens' ? 0 : 1 + const action = testCase.expected ? 'call' : 'not call' + const callTimes = testCase.expected ? 1 : 0 + + it(`Should ${action} addReservedTokensItem callback for: ${JSON.stringify(testCase.value)}`, () => { + wrapper().find('input[type="radio"]').at(radioIndex).simulate('change') + + addressInput().simulate('change', { target: { value: testCase.value.addr } }) + valueInput().simulate('change', { target: { value: testCase.value.val } }) + + wrapper().find('.plus-button-container').childAt(0).simulate('click') + + const { value } = testCase + value.val = testCase.value.val === '' ? '' : parseFloat(testCase.value.val) + + if (testCase.expect) { + expect(addCallback).toHaveBeenCalledWith(value) + } + expect(addCallback).toHaveBeenCalledTimes(callTimes) + }) + + }) + }) + + describe('removeReservedToken', () => { + it('Should call removeReservedToken callback', () => { + wrapper().find('.reserved-tokens-item-empty').children('a').at(0).simulate('click') + expect(removeCallback).toHaveBeenCalledTimes(1) + }) + }) }) - test('Should call method to remove address', () => { - const wrapper = mount( - - ) - - wrapper.find('.reserved-tokens-item-empty').children('a').at(0).simulate('click') - expect(removeCallback).toHaveBeenCalledTimes(1) - }) + // it('Should reset state after adding a new address', () => { + // const newReserved = { + // addr: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1', + // dim: 'percentage', + // val: '120' + // } + // const addressEvent = { target: { value: newReserved.addr } } + // const valueEvent = { target: { value: newReserved.val } } + // + // const handleAddressChange = jest.spyOn(wrapper().instance(), 'handleAddressChange') + // const handleValueChange = jest.spyOn(wrapper().instance(), 'handleValueChange') + // + // wrapper().update() + // + // expect(addressState().pristine).toBeTruthy() + // expect(addressState().valid).toBe(INVALID) + // expect(valueState().pristine).toBeTruthy() + // expect(valueState().valid).toBe(INVALID) + // expect(wrapper().state('addr')).toBe('') + // expect(wrapper().state('dim')).toBe('tokens') + // expect(wrapper().state('val')).toBe('') + // + // addressInput().simulate('change', addressEvent) + // valueInput().simulate('change', valueEvent) + // wrapper().find('input[type="radio"]').at(1).simulate('change') + // + // expect(addressState().pristine).toBeFalsy() + // expect(addressState().valid).toBe(VALID) + // expect(wrapper().state('addr')).toBe(newReserved.addr) + // expect(handleAddressChange).toHaveBeenCalledTimes(1) + // expect(handleAddressChange).toHaveBeenCalledWith(newReserved.addr) + // + // expect(valueState().pristine).toBeFalsy() + // expect(valueState().valid).toBe(VALID) + // expect(wrapper().state('val')).toBe(newReserved.val) + // expect(handleValueChange).toHaveBeenCalledTimes(1) + // expect(handleValueChange).toHaveBeenCalledWith(newReserved.val) + // + // expect(wrapper().state('dim')).toBe(newReserved.dim) + // + // wrapper().find('.plus-button-container').childAt(0).simulate('click') + // + // expect(addressState().pristine).toBeTruthy() + // expect(addressState().valid).toBe(INVALID) + // expect(valueState().pristine).toBeTruthy() + // expect(valueState().valid).toBe(INVALID) + // expect(wrapper().state('addr')).toBe('') + // expect(wrapper().state('dim')).toBe('tokens') + // expect(wrapper().state('val')).toBe('') + // }) - test('Should reset state after adding a new address', () => { - const wrapper = mount( - - ) - const newToken = { - addr: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1', - dim: 'percentage', - val: '120' - } - const addressEvent = { target: { value: newToken.addr } } - const valueEvent = { target: { value: newToken.val } } - - const handleAddressChange = jest.spyOn(wrapper.instance(), 'handleAddressChange') - const handleValueChange = jest.spyOn(wrapper.instance(), 'handleValueChange') - - wrapper.update() - - expect(wrapper.state('validation').address.pristine).toBeTruthy() - expect(wrapper.state('validation').address.valid).toBe(INVALID) - expect(wrapper.state('validation').value.pristine).toBeTruthy() - expect(wrapper.state('validation').value.valid).toBe(INVALID) - expect(wrapper.state('addr')).toBe('') - expect(wrapper.state('dim')).toBe('tokens') - expect(wrapper.state('val')).toBe('') - - wrapper.find('input[type="text"]').at(0).simulate('change', addressEvent) - wrapper.find('input[type="number"]').at(0).simulate('change', valueEvent) - wrapper.find('input[type="radio"]').at(1).simulate('change') - - setTimeout(() => { - expect(wrapper.state('validation').address.pristine).toBeFalsy() - expect(wrapper.state('validation').address.valid).toBe(VALID) - expect(wrapper.state('addr')).toBe(newToken.addr) - expect(handleAddressChange).toHaveBeenCalledTimes(1) - expect(handleAddressChange).toHaveBeenCalledWith(newToken.addr) - - expect(wrapper.state('validation').value.pristine).toBeFalsy() - expect(wrapper.state('validation').value.valid).toBe(VALID) - expect(wrapper.state('val')).toBe(newToken.val) - expect(handleValueChange).toHaveBeenCalledTimes(1) - expect(handleValueChange).toHaveBeenCalledWith(newToken.val) - - expect(wrapper.state('dim')).toBe(newToken.dim) - - wrapper.find('.plus-button-container').childAt(0).simulate('click') - setTimeout(() => { - expect(wrapper.state('validation').address.pristine).toBeTruthy() - expect(wrapper.state('validation').address.valid).toBe(INVALID) - expect(wrapper.state('validation').value.pristine).toBeTruthy() - expect(wrapper.state('validation').value.valid).toBe(INVALID) - expect(wrapper.state('addr')).toBe('') - expect(wrapper.state('dim')).toBe('tokens') - expect(wrapper.state('val')).toBe('') - }, 10) - }, 10) - }) }) diff --git a/src/components/Common/__snapshots__/ReservedTokensInputBlock.spec.js.snap b/src/components/Common/__snapshots__/ReservedTokensInputBlock.spec.js.snap index 1d1a4d671..6107b6540 100644 --- a/src/components/Common/__snapshots__/ReservedTokensInputBlock.spec.js.snap +++ b/src/components/Common/__snapshots__/ReservedTokensInputBlock.spec.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ReservedTokensInputBlock Should render the component 1`] = ` +exports[`percentage 1`] = `
@@ -33,6 +33,17 @@ exports[`ReservedTokensInputBlock Should render the component 1`] = ` > Address where to send reserved tokens.

+

Value + +

+ Value in tokens. Don't forget to click + button for each reserved token. +

+

+

+
+
+
+
+
+
+
+ + 0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1 + + + tokens + + +
+
+ + + +
+
+
+
+ + 0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1 + + + percentage + + +
+
+ + + +
+
+
+
+ + 0xffcf8fdee72ac11b5c542428b35eef5769c409f0 + + + percentage + + +
+
+ + + +
+
+
+
+ + 0x22d491bde2303f2f43325b2108d26f1eaba1e32b + + + tokens + + +
+
+ + + +
+
+
+`; + +exports[`tokens 1`] = ` +
+
+
+
+ +

+ Address where to send reserved tokens. +

+

+

+
+ +
+ + +
+

+ Fixed amount or % of crowdsaled tokens. Will be deposited to the account after finalization of the crowdsale. +

+
+
+ +

- Value in tokens or percents. Don't forget to press + button for each reserved token. + Value in tokens. Don't forget to click + button for each reserved token.

+

Date: Mon, 26 Feb 2018 11:17:58 -0300 Subject: [PATCH 06/16] Fix error display when decimals is empty --- src/components/stepTwo/index.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/stepTwo/index.js b/src/components/stepTwo/index.js index e0def6c02..9e019b222 100644 --- a/src/components/stepTwo/index.js +++ b/src/components/stepTwo/index.js @@ -44,6 +44,11 @@ export class stepTwo extends Component { showErrorMessages = (parent) => { this.props.tokenStore.invalidateToken(); + this.updateDecimalsStore({ + value: this.state.decimals, + pristine: false, + valid: this.state.validation.decimals.valid + }) } updateTokenStore = (event, property) => { @@ -57,6 +62,7 @@ export class stepTwo extends Component { validation: { decimals: { $set: { + disabled: this.state.validation.decimals.disabled, pristine: pristine, valid: valid }, @@ -75,7 +81,7 @@ export class stepTwo extends Component { disableDecimals = () => { if (this.state.decimals === '') { - this.updateDecimalsStore({ value: 0, pristine: false, valid: true }) + this.updateDecimalsStore({ value: 0, pristine: false, valid: VALID }) } this.setState({ validation: { decimals: { disabled: true } } }) From 6f24fb06906b2711668a5e991a690f8dcb03ca04 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 26 Feb 2018 11:29:42 -0300 Subject: [PATCH 07/16] Update NumericInput implementation for custom GasPrice value --- src/components/stepThree/index.js | 47 +++++++++++++++++++------------ 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/src/components/stepThree/index.js b/src/components/stepThree/index.js index c59647082..d7d114a45 100644 --- a/src/components/stepThree/index.js +++ b/src/components/stepThree/index.js @@ -22,9 +22,10 @@ import { inject, observer } from "mobx-react"; import { Loader } from '../Common/Loader' import { noGasPriceAvailable, warningOnMainnetAlert } from '../../utils/alerts' import { NumericInput } from '../Common/NumericInput' +import update from 'immutability-helper' const { CROWDSALE_SETUP } = NAVIGATION_STEPS; -const { EMPTY, VALID } = VALIDATION_TYPES; +const { EMPTY, VALID, INVALID } = VALIDATION_TYPES; const { START_TIME, END_TIME, @@ -55,7 +56,13 @@ export class stepThree extends React.Component { this.state = { loading: true, - gasPriceSelected: gasPriceStore.slow.id + gasPriceSelected: gasPriceStore.slow.id, + validation: { + gasPrice: { + pristine: true, + valid: INVALID + } + } } } @@ -100,13 +107,6 @@ export class stepThree extends React.Component { this.addCrowdsaleBlock(num); } - updateCrowdsaleBlockListStore = (event, property, index) => { - const { crowdsaleBlockListStore } = this.props; - const value = event.target.value; - crowdsaleBlockListStore.setCrowdsaleBlockProperty(value, property, index); - crowdsaleBlockListStore.validateCrowdsaleListBlockProperty(property, index); - }; - updateTierStore = (event, property, index) => { const { tierStore } = this.props; const value = event.target.value; @@ -114,12 +114,6 @@ export class stepThree extends React.Component { tierStore.validateTiers(property, index); }; - updatePricingStrategyStore = (event, index, property) => { - const { pricingStrategyStore } = this.props; - const value = event.target.value; - pricingStrategyStore.setStrategyProperty(value, property, index); - }; - goToDeploymentStage = () => { this.props.history.push('/4') } @@ -150,14 +144,17 @@ export class stepThree extends React.Component { e.preventDefault(); e.stopPropagation(); - const { tierStore } = this.props; + const { tierStore, gasPriceStore } = this.props; + const gasPriceIsValid = gasPriceStore.custom.id === this.state.gasPriceSelected && this.state.validation.gasPrice.valid === VALID + + console.log('gasPriceIsValid', gasPriceIsValid) for (let index = 0; index < tierStore.tiers.length; index++) { tierStore.validateTiers("endTime", index); tierStore.validateTiers("startTime", index); } - if (tierStore.areTiersValid) { + if (tierStore.areTiersValid && gasPriceIsValid) { const { reservedTokenStore, deploymentStore } = this.props const tiersCount = tierStore.tiers.length const reservedCount = reservedTokenStore.tokens.length @@ -217,7 +214,19 @@ export class stepThree extends React.Component { } } - updateGasPrice = value => { + updateGasPrice = ({value, pristine, valid}) => { + const newState = update(this.state, { + validation: { + gasPrice: { + $set: { + pristine: pristine, + valid: valid + } + } + } + }) + + this.setState(newState) this.props.generalStore.setGasPrice(gweiToWei(value)) } @@ -284,6 +293,8 @@ export class stepThree extends React.Component { maxDecimals={9} acceptFloat={true} value={weiToGwei(generalStore.gasPrice)} + pristine={this.state.validation.gasPrice.pristine} + valid={this.state.validation.gasPrice.valid} errorMessage="Gas Price must be greater than 0.1 with up to 9 decimals" onValueUpdate={this.updateGasPrice} /> : From d53f56fc73ba4dd7e35fb02297f1274b7c2dc5de Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 26 Feb 2018 14:24:11 -0300 Subject: [PATCH 08/16] Update minCap to properly use of NumericInput --- src/components/stepThree/index.js | 33 ++++++++++++++++++++++++++++--- src/stores/TierStore.js | 15 +++++++------- src/utils/constants.js | 2 +- src/utils/utils.js | 4 +--- 4 files changed, 39 insertions(+), 15 deletions(-) diff --git a/src/components/stepThree/index.js b/src/components/stepThree/index.js index d7d114a45..c1a67a9bb 100644 --- a/src/components/stepThree/index.js +++ b/src/components/stepThree/index.js @@ -38,7 +38,7 @@ const { ENABLE_WHITELISTING } = TEXT_FIELDS; -@inject("contractStore", "crowdsaleBlockListStore", "pricingStrategyStore", "web3Store", "tierStore", "generalStore", "gasPriceStore", "reservedTokenStore", "deploymentStore") +@inject("contractStore", "crowdsaleBlockListStore", "pricingStrategyStore", "web3Store", "tierStore", "generalStore", "gasPriceStore", "reservedTokenStore", "deploymentStore", "tokenStore") @observer export class stepThree extends React.Component { constructor(props) { @@ -57,10 +57,15 @@ export class stepThree extends React.Component { this.state = { loading: true, gasPriceSelected: gasPriceStore.slow.id, + minCap: '', validation: { gasPrice: { pristine: true, valid: INVALID + }, + minCap: { + pristine: true, + valid: INVALID } } } @@ -230,6 +235,23 @@ export class stepThree extends React.Component { this.props.generalStore.setGasPrice(gweiToWei(value)) } + updateMinCap = ({ value, pristine, valid }) => { + const newState = update(this.state, { + validation: { + minCap: { + $set: { + pristine: pristine, + valid: valid + } + } + } + }) + newState.minCap = value + + this.setState(newState) + this.props.tierStore.setGlobalMinCap(value) + } + renderGasPriceInput() { const { generalStore, gasPriceStore } = this.props @@ -337,9 +359,14 @@ export class stepThree extends React.Component { title={MINCAP} description="Minimum amount tokens to buy. Not a minimal size of a transaction. If minCap is 1 and user bought 1 token in a previous transaction and buying 0.1 token it will allow him to buy." disabled={tierStore.tiers[0].whitelistEnabled === "yes"} - min={0} + min={this.props.tokenStore.decimals ? Number(`1e-${this.props.tokenStore.decimals}`) : 1} + acceptFloat={!!this.props.tokenStore.decimals} + maxDecimals={this.props.tokenStore.decimals} + value={this.state.minCap} + pristine={this.state.validation.minCap.pristine} + valid={this.state.validation.minCap.valid} errorMessage={VALIDATION_MESSAGES.MINCAP} - onValueUpdate={tierStore.setGlobalMinCap} + onValueUpdate={this.updateMinCap} /> { - return Object.keys(tier) - .every((key) => { - console.log('key', key, this.validTiers[index][key]) - return this.validTiers[index][key] === VALID - }) - }) + const isValid = this.validTiers.every((tier, index) => { + return Object.keys(tier) + .every((key) => { + console.log('key', key, this.validTiers[index][key]) + return this.validTiers[index][key] === VALID + }) + }) console.log('isValid', isValid) diff --git a/src/utils/constants.js b/src/utils/constants.js index 67481ad74..fd840992c 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -139,7 +139,7 @@ export const VALIDATION_MESSAGES = { EDITED_END_TIME: 'Please enter a valid date later than start time and previous than start time of next tier', EDITED_START_TIME: 'Please enter a valid date later than now, less than end time and later than the end time of the previous tier', RATE: 'Please enter a valid number greater than 0', - MINCAP: 'Please enter a valid number greater than 0' + MINCAP: 'Value must be positive and decimals should not exceed the amount of decimals specified' } //descriptions of input fields diff --git a/src/utils/utils.js b/src/utils/utils.js index 7e0b4f6bc..1d5e6747c 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -76,7 +76,7 @@ export const validateName = (name) => typeof name === 'string' && name.length > export const validateSupply = (supply) => isNaN(Number(supply)) === false && Number(supply) > 0 -export const validateTicker = (ticker) => typeof ticker === 'string' && ticker.length <= 5 && ticker.length > 0 +export const validateTicker = (ticker) => /^[a-z0-9]{1,5}$/i.test(ticker) export const validateTime = (time) => getTimeAsNumber(time) > Date.now() @@ -88,8 +88,6 @@ export const validateRate = (rate) => isNaN(Number(rate)) === false && Number(ra export const validateAddress = (address) => !(!address || address.length !== 42) -export const validateMinCap = minCap => /^$|^([0-9]+)$/.test(minCap) - export function toFixed(x) { if (Math.abs(x) < 1.0) { let e = parseInt(x.toString().split('e-')[1], 10); From 2a0c13dd4f56c033e0111a8476f825178a8aa2ee Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 26 Feb 2018 14:25:40 -0300 Subject: [PATCH 09/16] Fix keyPress NumericInput to allow numbers to be expressed as exponential (i.e.: 1e-10) --- src/components/Common/NumericInput.js | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/src/components/Common/NumericInput.js b/src/components/Common/NumericInput.js index 0c6c8f9d8..3dfe13be2 100644 --- a/src/components/Common/NumericInput.js +++ b/src/components/Common/NumericInput.js @@ -25,22 +25,12 @@ export class NumericInput extends Component { } onKeyPress = e => { - const { value, min, max, acceptFloat } = this.props - const isFirstCharacter = value === '' || value === undefined - const isMinus = e.key === '-' + const { min, max, acceptFloat } = this.props const allowsNegative = min < 0 || max < 0 - if (isFirstCharacter) { - if (isMinus) { - if (!allowsNegative) { - e.preventDefault() - } - } - } else { - if (!acceptFloat) { - if (isNaN(parseInt(e.key, 10))) { - e.preventDefault() - } + if (!acceptFloat && !allowsNegative) { + if (isNaN(parseInt(e.key, 10))) { + e.preventDefault() } } } From 1909acc2cc00971147d86771529e55e15d14b554 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 26 Feb 2018 14:25:57 -0300 Subject: [PATCH 10/16] Update tests --- src/components/Common/NumericInput.spec.js | 24 --------- .../ReservedTokensInputBlock.spec.js.snap | 4 ++ src/utils/utils.spec.js | 50 +------------------ 3 files changed, 5 insertions(+), 73 deletions(-) diff --git a/src/components/Common/NumericInput.spec.js b/src/components/Common/NumericInput.spec.js index e96a55456..3a69c369e 100644 --- a/src/components/Common/NumericInput.spec.js +++ b/src/components/Common/NumericInput.spec.js @@ -85,14 +85,6 @@ describe('NumericInput', () => { input().simulate(INPUT_EVENT.CHANGE, changeMock) expect(numericInputComponent.onValueUpdate).toHaveBeenCalled() }) - it('Should reject "-" symbol if it is not the first character', () => { - numericInputComponent.min = -10 - numericInputComponent.value = 5 - keypressMock.key = '-' - - input().simulate(INPUT_EVENT.KEYPRESS, keypressMock) - expect(keypressMock.preventDefault).toHaveBeenCalled() - }) describe('min', () => { it('Should accept "-" symbol if min is negative', () => { @@ -114,14 +106,6 @@ describe('NumericInput', () => { numericInputComponent.min = 0 keypressMock.key = '-' - input().simulate(INPUT_EVENT.KEYPRESS, keypressMock) - expect(keypressMock.preventDefault).toHaveBeenCalled() - }) - it('Should reject "-" symbol if min is not negative and accepts floating numbers', () => { - numericInputComponent.min = 0 - numericInputComponent.acceptFloat = true - keypressMock.key = '-' - input().simulate(INPUT_EVENT.KEYPRESS, keypressMock) expect(keypressMock.preventDefault).toHaveBeenCalled() }) @@ -147,14 +131,6 @@ describe('NumericInput', () => { numericInputComponent.max = 10 keypressMock.key = '-' - input().simulate(INPUT_EVENT.KEYPRESS, keypressMock) - expect(keypressMock.preventDefault).toHaveBeenCalled() - }) - it('Should reject "-" symbol if max is not negative and accepts floating numbers', () => { - numericInputComponent.max = 0 - numericInputComponent.acceptFloat = true - keypressMock.key = '-' - input().simulate(INPUT_EVENT.KEYPRESS, keypressMock) expect(keypressMock.preventDefault).toHaveBeenCalled() }) diff --git a/src/components/Common/__snapshots__/ReservedTokensInputBlock.spec.js.snap b/src/components/Common/__snapshots__/ReservedTokensInputBlock.spec.js.snap index 6107b6540..7ad506985 100644 --- a/src/components/Common/__snapshots__/ReservedTokensInputBlock.spec.js.snap +++ b/src/components/Common/__snapshots__/ReservedTokensInputBlock.spec.js.snap @@ -25,6 +25,7 @@ exports[`percentage 1`] = ` onChange={[Function]} onKeyPress={undefined} onPaste={undefined} + style={undefined} type="text" value="" /> @@ -108,6 +109,7 @@ exports[`percentage 1`] = ` onChange={[Function]} onKeyPress={[Function]} onPaste={[Function]} + style={undefined} type="number" value="" /> @@ -294,6 +296,7 @@ exports[`tokens 1`] = ` onChange={[Function]} onKeyPress={undefined} onPaste={undefined} + style={undefined} type="text" value="" /> @@ -377,6 +380,7 @@ exports[`tokens 1`] = ` onChange={[Function]} onKeyPress={[Function]} onPaste={[Function]} + style={undefined} type="number" value="" /> diff --git a/src/utils/utils.spec.js b/src/utils/utils.spec.js index 11f0cf72e..29e75bde7 100644 --- a/src/utils/utils.spec.js +++ b/src/utils/utils.spec.js @@ -1,4 +1,4 @@ -import { countDecimalPlaces, validateDecimals, validateTicker, validateMinCap } from './utils' +import { countDecimalPlaces, validateTicker } from './utils' describe('countDecimalPlaces', () => { [ @@ -33,27 +33,6 @@ describe('countDecimalPlaces', () => { }) }) -describe('validateDecimals', () => { - [ - { value: '', expected: true }, - { value: '0', expected: true }, - { value: '9', expected: true }, - { value: '10', expected: true }, - { value: '18', expected: true }, - { value: '19', expected: false }, - { value: '-1', expected: false }, - { value: '0.5', expected: false }, - { value: '3.5', expected: false }, - { value: '1e10', expected: false } - ].forEach(testCase => { - const action = testCase.expected ? 'pass' : 'fail' - - it(`Should ${action} for '${testCase.value}'`, () => { - expect(validateDecimals(testCase.value)).toBe(testCase.expected) - }) - }) -}) - describe('validateTicker', () => { [ {value: '', expected: false}, @@ -76,30 +55,3 @@ describe('validateTicker', () => { }) }) }) - -describe('validateMinCap', () => { - [ - { value: '', expected: true }, - { value: '0', expected: true }, - { value: '00', expected: true }, - { value: '1', expected: true }, - { value: '001', expected: true }, - { value: '150', expected: true }, - { value: '999', expected: true }, - { value: '-10', expected: false }, - { value: .123, expected: false }, - { value: '1.12', expected: false }, - { value: '1.', expected: false }, - { value: '1e10', expected: false }, - { value: '+1', expected: false }, - { value: null, expected: false }, - { value: false, expected: false }, - { value: undefined, expected: false } - ].forEach(testCase => { - const action = testCase.expected ? 'pass' : 'fail' - - it(`Should ${action} for '${testCase.value}'`, () => { - expect(validateMinCap(testCase.value)).toBe(testCase.expected) - }) - }) -}) From ebba9a916a15c61f117742ac2e9bf13b4a53327f Mon Sep 17 00:00:00 2001 From: fernandomg Date: Tue, 27 Feb 2018 14:11:00 -0300 Subject: [PATCH 11/16] Fix enable/disable decimals methods --- src/components/stepTwo/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/stepTwo/index.js b/src/components/stepTwo/index.js index 9e019b222..828440519 100644 --- a/src/components/stepTwo/index.js +++ b/src/components/stepTwo/index.js @@ -84,11 +84,11 @@ export class stepTwo extends Component { this.updateDecimalsStore({ value: 0, pristine: false, valid: VALID }) } - this.setState({ validation: { decimals: { disabled: true } } }) + this.setState(update(this.state, { validation: { decimals: { disabled: { $set: true } } } })) } enableDecimals = () => { - this.setState({ validation: { decimals: { disabled: false } } }) + this.setState(update(this.state, { validation: { decimals: { disabled: { $set: false } } } })) } renderLink () { From 20535ba33c76bb217937b327c0148dfa4288b62f Mon Sep 17 00:00:00 2001 From: fernandomg Date: Wed, 28 Feb 2018 09:07:34 -0300 Subject: [PATCH 12/16] Remove commented code --- .../Common/ReservedTokensInputBlock.spec.js | 52 ------------------- 1 file changed, 52 deletions(-) diff --git a/src/components/Common/ReservedTokensInputBlock.spec.js b/src/components/Common/ReservedTokensInputBlock.spec.js index dd657a4f4..0cd3db329 100644 --- a/src/components/Common/ReservedTokensInputBlock.spec.js +++ b/src/components/Common/ReservedTokensInputBlock.spec.js @@ -383,56 +383,4 @@ describe('ReservedTokensInputBlock', () => { }) }) }) - - // it('Should reset state after adding a new address', () => { - // const newReserved = { - // addr: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1', - // dim: 'percentage', - // val: '120' - // } - // const addressEvent = { target: { value: newReserved.addr } } - // const valueEvent = { target: { value: newReserved.val } } - // - // const handleAddressChange = jest.spyOn(wrapper().instance(), 'handleAddressChange') - // const handleValueChange = jest.spyOn(wrapper().instance(), 'handleValueChange') - // - // wrapper().update() - // - // expect(addressState().pristine).toBeTruthy() - // expect(addressState().valid).toBe(INVALID) - // expect(valueState().pristine).toBeTruthy() - // expect(valueState().valid).toBe(INVALID) - // expect(wrapper().state('addr')).toBe('') - // expect(wrapper().state('dim')).toBe('tokens') - // expect(wrapper().state('val')).toBe('') - // - // addressInput().simulate('change', addressEvent) - // valueInput().simulate('change', valueEvent) - // wrapper().find('input[type="radio"]').at(1).simulate('change') - // - // expect(addressState().pristine).toBeFalsy() - // expect(addressState().valid).toBe(VALID) - // expect(wrapper().state('addr')).toBe(newReserved.addr) - // expect(handleAddressChange).toHaveBeenCalledTimes(1) - // expect(handleAddressChange).toHaveBeenCalledWith(newReserved.addr) - // - // expect(valueState().pristine).toBeFalsy() - // expect(valueState().valid).toBe(VALID) - // expect(wrapper().state('val')).toBe(newReserved.val) - // expect(handleValueChange).toHaveBeenCalledTimes(1) - // expect(handleValueChange).toHaveBeenCalledWith(newReserved.val) - // - // expect(wrapper().state('dim')).toBe(newReserved.dim) - // - // wrapper().find('.plus-button-container').childAt(0).simulate('click') - // - // expect(addressState().pristine).toBeTruthy() - // expect(addressState().valid).toBe(INVALID) - // expect(valueState().pristine).toBeTruthy() - // expect(valueState().valid).toBe(INVALID) - // expect(wrapper().state('addr')).toBe('') - // expect(wrapper().state('dim')).toBe('tokens') - // expect(wrapper().state('val')).toBe('') - // }) - }) From ff17edd1253871ee53d462d3ec856e906b20a936 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Wed, 28 Feb 2018 09:09:49 -0300 Subject: [PATCH 13/16] Fix default pristine value --- src/components/Common/NumericInput.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Common/NumericInput.js b/src/components/Common/NumericInput.js index 3dfe13be2..a6f7bbbeb 100644 --- a/src/components/Common/NumericInput.js +++ b/src/components/Common/NumericInput.js @@ -13,7 +13,7 @@ export class NumericInput extends Component { this.state = { value: props.value || '', - pristine: props.pristine || true, + pristine: props.pristine !== undefined ? props.pristine : true, valid: props.valid || VALID } } From 89d5a3885084591c90d9b167af7e16196f3999af Mon Sep 17 00:00:00 2001 From: fernandomg Date: Wed, 28 Feb 2018 10:04:17 -0300 Subject: [PATCH 14/16] Fix special symbols handling in NumericInput --- src/components/Common/NumericInput.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/Common/NumericInput.js b/src/components/Common/NumericInput.js index a6f7bbbeb..7aa19c353 100644 --- a/src/components/Common/NumericInput.js +++ b/src/components/Common/NumericInput.js @@ -28,10 +28,13 @@ export class NumericInput extends Component { const { min, max, acceptFloat } = this.props const allowsNegative = min < 0 || max < 0 - if (!acceptFloat && !allowsNegative) { - if (isNaN(parseInt(e.key, 10))) { - e.preventDefault() - } + if ((e.key === 'e' || e.key === '.' || e.key === '+') && !acceptFloat) { + e.preventDefault() + } + + // '-' symbol is required for scientific notation and negative values + if (e.key === '-' && !allowsNegative && !acceptFloat) { + e.preventDefault() } } From 7c43800ca0ce8924af3b78c8eee8dc9504059c65 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Wed, 28 Feb 2018 10:04:31 -0300 Subject: [PATCH 15/16] Add tests for NumericInput --- src/components/Common/NumericInput.spec.js | 24 ++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/components/Common/NumericInput.spec.js b/src/components/Common/NumericInput.spec.js index 3a69c369e..8a1773b31 100644 --- a/src/components/Common/NumericInput.spec.js +++ b/src/components/Common/NumericInput.spec.js @@ -135,6 +135,30 @@ describe('NumericInput', () => { expect(keypressMock.preventDefault).toHaveBeenCalled() }) }) + + it('Should prevent "." if no float is allowed', () => { + numericInputComponent.value = '10' + keypressMock.key = '.' + + input().simulate(INPUT_EVENT.KEYPRESS, keypressMock) + expect(keypressMock.preventDefault).toHaveBeenCalledTimes(1) + }) + + it('Should prevent "+" if no float is allowed', () => { + numericInputComponent.value = '10' + keypressMock.key = '+' + + input().simulate(INPUT_EVENT.KEYPRESS, keypressMock) + expect(keypressMock.preventDefault).toHaveBeenCalledTimes(1) + }) + + it('Should prevent "e" if no float is allowed', () => { + numericInputComponent.value = '10' + keypressMock.key = 'e' + + input().simulate(INPUT_EVENT.KEYPRESS, keypressMock) + expect(keypressMock.preventDefault).toHaveBeenCalledTimes(1) + }) }) describe('integer numbers', () => { From 1810e9f5d94b72e4d1d4a1c2a8fe83ba767039e9 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Wed, 28 Feb 2018 10:05:10 -0300 Subject: [PATCH 16/16] Prevent infinite loops if for any reason decimals is not a positive integer --- src/components/stepTwo/index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/stepTwo/index.js b/src/components/stepTwo/index.js index 828440519..25def2124 100644 --- a/src/components/stepTwo/index.js +++ b/src/components/stepTwo/index.js @@ -123,6 +123,10 @@ export class stepTwo extends Component { } render () { + const decimals = this.state.validation.decimals.valid === VALID && this.state.decimals >= 0 + ? parseInt(this.state.decimals, 10) + : 0 + return (
@@ -171,7 +175,7 @@ export class stepTwo extends Component {