JimmyMow
7 years ago
committed by
GitHub
11 changed files with 453 additions and 30 deletions
@ -0,0 +1,164 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
|
|||
class AmountInput extends React.Component { |
|||
constructor(props) { |
|||
super(props) |
|||
this.handleChange = this.handleChange.bind(this) |
|||
this.handleBlur = this.handleBlur.bind(this) |
|||
this.handleKeyDown = this.handleKeyDown.bind(this) |
|||
} |
|||
|
|||
setRules() { |
|||
const { currency } = this.props |
|||
switch (currency) { |
|||
case 'btc': |
|||
this.rules = { |
|||
precision: 8, |
|||
placeholder: '0.00000000', |
|||
pattern: '[0-9.]*' |
|||
} |
|||
break |
|||
case 'bits': |
|||
this.rules = { |
|||
precision: 2, |
|||
placeholder: '0.00', |
|||
pattern: '[0-9.]*' |
|||
} |
|||
break |
|||
case 'sats': |
|||
this.rules = { |
|||
precision: 0, |
|||
placeholder: '00000000', |
|||
pattern: '[0-9]*' |
|||
} |
|||
break |
|||
default: |
|||
this.rules = { |
|||
precision: 2, |
|||
pattern: '[0-9]*' |
|||
} |
|||
} |
|||
} |
|||
|
|||
parseNumber(_value) { |
|||
let value = _value || '' |
|||
if (typeof _value === 'string') { |
|||
value = _value.replace(/[^0-9.]/g, '') |
|||
} |
|||
let integer = null |
|||
let fractional = null |
|||
if (value * 1.0 < 0) { |
|||
value = '0.0' |
|||
} |
|||
// pearse integer and fractional value so that we can reproduce the same string value afterwards
|
|||
// [0, 0] === 0.0
|
|||
// [0, ''] === 0.
|
|||
// [0, null] === 0
|
|||
if (value.match(/^[0-9]*\.[0-9]*$/)) { |
|||
;[integer, fractional] = value.toString().split(/\./) |
|||
if (!fractional) { |
|||
fractional = '' |
|||
} |
|||
} else { |
|||
integer = value |
|||
} |
|||
// Limit fractional precision to the correct number of decimal places.
|
|||
if (fractional && fractional.length > this.rules.precision) { |
|||
fractional = fractional.substring(0, this.rules.precision) |
|||
} |
|||
|
|||
return [integer, fractional] |
|||
} |
|||
|
|||
formatValue(integer, fractional) { |
|||
let value |
|||
if (fractional && fractional.length > 0) { |
|||
value = `${integer}.${fractional}` |
|||
} else { |
|||
// Empty string means `XYZ.` instead of just plain `XYZ`.
|
|||
if (fractional === '') { |
|||
value = `${integer}.` |
|||
} else { |
|||
value = `${integer}` |
|||
} |
|||
} |
|||
return value |
|||
} |
|||
|
|||
handleChange(e) { |
|||
let { value } = e.target |
|||
const [integer, fractional] = this.parseNumber(value) |
|||
value = this.formatValue(integer, fractional) |
|||
|
|||
const { onChangeEvent } = this.props |
|||
if (onChangeEvent) { |
|||
onChangeEvent(value) |
|||
} |
|||
} |
|||
|
|||
handleBlur(e) { |
|||
let { value } = e.target |
|||
const [integer, fractional] = this.parseNumber(value) |
|||
value = this.formatValue(integer, fractional) |
|||
|
|||
const { onBlurEvent } = this.props |
|||
if (onBlurEvent) { |
|||
onBlurEvent(value) |
|||
} |
|||
} |
|||
|
|||
handleKeyDown(e) { |
|||
// Do nothing if the user did select all key combo.
|
|||
if (e.metaKey && e.key === 'a') { |
|||
return |
|||
} |
|||
|
|||
// Do not allow multiple dots.
|
|||
let { value } = e.target |
|||
if (e.key === '.') { |
|||
if (value.search(/\./) >= 0) { |
|||
e.preventDefault() |
|||
} |
|||
return |
|||
} |
|||
|
|||
if (e.key.length === 1 && !e.key.match(/^[0-9.]$/)) { |
|||
e.preventDefault() |
|||
return |
|||
} |
|||
} |
|||
|
|||
render() { |
|||
let { amount = '', readOnly } = this.props |
|||
if (amount === null) { |
|||
amount = '' |
|||
} |
|||
this.setRules() |
|||
|
|||
return ( |
|||
<input |
|||
id="amount" |
|||
onChange={this.handleChange} |
|||
onBlur={this.handleBlur} |
|||
onKeyDown={this.handleKeyDown} |
|||
readOnly={readOnly} |
|||
type="text" |
|||
required |
|||
value={amount} |
|||
pattern={this.rules.pattern} |
|||
placeholder={this.rules.placeholder} |
|||
/> |
|||
) |
|||
} |
|||
} |
|||
|
|||
AmountInput.propTypes = { |
|||
amount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), |
|||
currency: PropTypes.string.isRequired, |
|||
onChangeEvent: PropTypes.func, |
|||
onBlurEvent: PropTypes.func, |
|||
readOnly: PropTypes.bool |
|||
} |
|||
|
|||
export default AmountInput |
@ -0,0 +1,3 @@ |
|||
import AmountInput from './AmountInput' |
|||
|
|||
export default AmountInput |
@ -0,0 +1,251 @@ |
|||
import React from 'react' |
|||
import { configure, mount } from 'enzyme' |
|||
import Adapter from 'enzyme-adapter-react-16' |
|||
|
|||
import AmountInput from 'components/AmountInput' |
|||
|
|||
configure({ adapter: new Adapter() }) |
|||
|
|||
let component |
|||
let wrapped |
|||
let input |
|||
|
|||
const defaultProps = { |
|||
amount: '', |
|||
currency: '', |
|||
|
|||
onChange: () => {} |
|||
} |
|||
|
|||
const mountInput = props => { |
|||
wrapped = mount(<AmountInput {...defaultProps} {...props} />) |
|||
input = wrapped.find('input') |
|||
component = wrapped.instance() |
|||
} |
|||
|
|||
describe('AmountInput', () => { |
|||
describe('render', () => { |
|||
it('renders amount', () => { |
|||
mountInput({ amount: '0.1234' }) |
|||
expect(input.props().value).toEqual('0.1234') |
|||
}) |
|||
}) |
|||
|
|||
describe('input properties', () => { |
|||
it('passes readOnly to input', () => { |
|||
mountInput({ readOnly: true }) |
|||
expect(input.props().readOnly).toEqual(true) |
|||
}) |
|||
}) |
|||
|
|||
describe('parseNumber', () => { |
|||
const test = (from, to) => { |
|||
expect(component.parseNumber(from)).toEqual(to) |
|||
} |
|||
beforeEach(() => mountInput()) |
|||
|
|||
it('splits number into integer and fraction', () => { |
|||
test(null, ['', null]) |
|||
test('', ['', null]) |
|||
test('0', ['0', null]) |
|||
test('00', ['00', null]) |
|||
test('0.', ['0', '']) |
|||
|
|||
test('1', ['1', null]) |
|||
test('01', ['01', null]) |
|||
test('1.', ['1', '']) |
|||
|
|||
test('0.0', ['0', '0']) |
|||
test('0.1', ['0', '1']) |
|||
|
|||
test('0.01', ['0', '01']) |
|||
test('0.10', ['0', '10']) |
|||
|
|||
test('1.0', ['1', '0']) |
|||
test('01.0', ['01', '0']) |
|||
}) |
|||
|
|||
describe('bitcoin', () => { |
|||
beforeEach(() => { |
|||
let props = { ...defaultProps, currency: 'btc' } |
|||
component = mount(<AmountInput {...props} />).instance() |
|||
}) |
|||
it('has a precision of 8', () => { |
|||
test('1.1', ['1', '1']) |
|||
test('1.12', ['1', '12']) |
|||
test('1.123', ['1', '123']) |
|||
test('1.1234', ['1', '1234']) |
|||
test('1.12345', ['1', '12345']) |
|||
test('1.123456', ['1', '123456']) |
|||
test('1.1234567', ['1', '1234567']) |
|||
test('1.12345678', ['1', '12345678']) |
|||
test('1.123456789', ['1', '12345678']) |
|||
test('1.1234567890', ['1', '12345678']) |
|||
}) |
|||
}) |
|||
|
|||
describe('bits', () => { |
|||
beforeEach(() => { |
|||
let props = { ...defaultProps, currency: 'bits' } |
|||
component = mount(<AmountInput {...props} />).instance() |
|||
}) |
|||
it('has a precision of 2', () => { |
|||
test('1.1', ['1', '1']) |
|||
test('1.12', ['1', '12']) |
|||
test('1.123', ['1', '12']) |
|||
test('1.1234', ['1', '12']) |
|||
}) |
|||
}) |
|||
|
|||
describe('sats', () => { |
|||
beforeEach(() => { |
|||
let props = { ...defaultProps, currency: 'sats' } |
|||
component = mount(<AmountInput {...props} />).instance() |
|||
}) |
|||
it('has a precision of 0', () => { |
|||
test('1.1', ['1', '']) |
|||
test('1.12', ['1', '']) |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
describe('formatValue', () => { |
|||
const component = mount(<AmountInput {...defaultProps} />).instance() |
|||
const test = (from, to) => { |
|||
expect(component.formatValue(from[0], from[1])).toEqual(to) |
|||
} |
|||
|
|||
it('turns parsed number into a string', () => { |
|||
test(['0', null], '0') |
|||
test(['00', null], '00') |
|||
test(['0', ''], '0.') |
|||
|
|||
test(['1', null], '1') |
|||
test(['01', null], '01') |
|||
test(['1', ''], '1.') |
|||
|
|||
test(['0', '0'], '0.0') |
|||
test(['0', '1'], '0.1') |
|||
|
|||
test(['0', '01'], '0.01') |
|||
test(['0', '10'], '0.10') |
|||
|
|||
test(['1', '0'], '1.0') |
|||
test(['01', '0'], '01.0') |
|||
}) |
|||
}) |
|||
|
|||
describe('handleChange', () => { |
|||
let onChangeEvent |
|||
let component |
|||
beforeEach(() => { |
|||
onChangeEvent = jest.fn() |
|||
let props = { ...defaultProps, onChangeEvent } |
|||
component = mount(<AmountInput {...props} />).instance() |
|||
}) |
|||
|
|||
it('sends value to parseNumber', () => { |
|||
jest.spyOn(component, 'parseNumber') |
|||
|
|||
component.handleChange({ target: { value: '123.0' } }) |
|||
expect(component.parseNumber).toHaveBeenCalledTimes(1) |
|||
expect(component.parseNumber).toHaveBeenCalledWith('123.0') |
|||
}) |
|||
|
|||
it('calls formatValue with presult from parseNumber', () => { |
|||
jest.spyOn(component, 'formatValue') |
|||
|
|||
component.handleChange({ target: { value: '123.0' } }) |
|||
expect(component.formatValue).toHaveBeenCalledTimes(1) |
|||
expect(component.formatValue).toHaveBeenCalledWith('123', '0') |
|||
}) |
|||
|
|||
it('calls props.onChange with formatValue result', () => { |
|||
jest.spyOn(component, 'formatValue').mockReturnValue('456') |
|||
|
|||
component.handleChange({ target: { value: '123.0' } }) |
|||
expect(onChangeEvent).toHaveBeenCalledWith('456') |
|||
}) |
|||
}) |
|||
|
|||
describe('handleBlur', () => { |
|||
let onBlurEvent |
|||
let component |
|||
beforeEach(() => { |
|||
onBlurEvent = jest.fn() |
|||
let props = { ...defaultProps, onBlurEvent } |
|||
component = mount(<AmountInput {...props} />).instance() |
|||
}) |
|||
|
|||
it('calls props.onBlurEvent with formatValue result', () => { |
|||
jest.spyOn(component, 'formatValue').mockReturnValue('456') |
|||
|
|||
component.handleBlur({ target: { value: '123.0' } }) |
|||
expect(onBlurEvent).toHaveBeenCalledWith('456') |
|||
}) |
|||
}) |
|||
|
|||
describe('handleKeyDown', () => { |
|||
let event |
|||
let preventDefault |
|||
|
|||
beforeEach(() => { |
|||
wrapped = mount(<AmountInput {...defaultProps} />) |
|||
component = wrapped.instance() |
|||
|
|||
preventDefault = jest.fn() |
|||
}) |
|||
|
|||
describe('dot', () => { |
|||
beforeEach(() => { |
|||
wrapped = mount(<AmountInput {...defaultProps} />) |
|||
component = wrapped.instance() |
|||
preventDefault = jest.fn() |
|||
|
|||
event = { |
|||
key: '.', |
|||
target: { value: '123' }, |
|||
preventDefault |
|||
} |
|||
}) |
|||
|
|||
it('allows adding dot to a number that has no dot', () => { |
|||
component.handleKeyDown({ ...event, target: { value: '123' } }) // no dot
|
|||
expect(preventDefault).not.toHaveBeenCalled() |
|||
}) |
|||
|
|||
it('does not allow multiple dots in the number', () => { |
|||
component.handleKeyDown({ ...event, target: { value: '123.' } }) // has a dot
|
|||
expect(preventDefault).toHaveBeenCalled() |
|||
}) |
|||
}) |
|||
|
|||
describe('Non-supported keys', () => { |
|||
beforeEach(() => { |
|||
wrapped = mount(<AmountInput {...defaultProps} />) |
|||
component = wrapped.instance() |
|||
preventDefault = jest.fn() |
|||
|
|||
event = { |
|||
key: '.', |
|||
target: { value: '123' }, |
|||
preventDefault |
|||
} |
|||
}) |
|||
|
|||
it('prevents event', () => { |
|||
component.handleKeyDown({ ...event, key: 'b' }) |
|||
expect(preventDefault).toHaveBeenCalled() |
|||
|
|||
component.handleKeyDown({ ...event, key: 'B' }) |
|||
expect(preventDefault).toHaveBeenCalled() |
|||
|
|||
component.handleKeyDown({ ...event, key: '-' }) |
|||
expect(preventDefault).toHaveBeenCalled() |
|||
|
|||
component.handleKeyDown({ ...event, key: '?' }) |
|||
expect(preventDefault).toHaveBeenCalled() |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
Loading…
Reference in new issue