Correction des tests pour CurrencyInput

Ajout d'un nouveau test pour le formatage des montants ;

Ré-implémentation de l'activation de `onChange` (seulement quand le
montant a changé — pas son formatage – et avec la valeur non formatée) ;

Désactivation de l'évenement `onBlur` de redux-form, qui recalculait
inutilement la saisie et était à l'origine d'un bug.
pull/551/head
Maxime Quandalle 2019-05-24 22:16:55 +02:00
parent 0a6cd87c47
commit 2117e2b2cf
No known key found for this signature in database
GPG Key ID: 428641C03D29CA10
4 changed files with 70 additions and 28 deletions

View File

@ -15,35 +15,52 @@ let currencyFormat = language => ({
thousandSeparator: Intl.NumberFormat(language)
.format(1000)
.charAt(1),
decimalSeparator: Intl.NumberFormat(language)
.format(0.1)
.charAt(1)
})
class CurrencyInput extends Component {
state = {
value: this.props.storeValue
value: this.props.value || '',
valueHasChanged: false,
}
onChange = this.props.debounce
? debounce(this.props.debounce, this.props.onChange)
: this.props.onChange
componentDidUpdate(prevProps) {
if (
prevProps.storeValue !== this.props.storeValue &&
this.props.storeValue !== this.state.value
) {
this.setState({ value: this.props.storeValue })
handleNextChange = false
value = undefined
handleChange = event => {
// Only trigger the `onChange` event if the value has changed -- and not
// only its formating, we don't want to call it when a dot is added in `12.`
// for instance
if (!this.handleNextChange) {
return
}
this.handleNextChange = false
event.persist()
event.target = {
...event.target,
value: this.value
}
this.onChange(event)
}
render() {
let forwardedProps = omit(
['onChange', 'defaultValue', 'language', 'className', 'value', 'normalizedValueRef'],
['onChange', 'defaultValue', 'language', 'className', 'value'],
this.props
)
const { isCurrencyPrefixed, thousandSeparator } = currencyFormat(
this.props.language
)
const {
isCurrencyPrefixed,
thousandSeparator,
decimalSeparator
} = currencyFormat(this.props.language)
return (
<div
@ -55,14 +72,18 @@ class CurrencyInput extends Component {
<NumberFormat
{...forwardedProps}
thousandSeparator={thousandSeparator}
decimalSeparator={decimalSeparator}
allowNegative={!this.state.valueHasChanged}
className="currencyInput__input"
inputMode="numeric"
onValueChange={({ value }) => {
this.setState({ value })
this.props.normalizedValueRef.current = value
this.setState({valueHasChanged: true, value})
this.value = value.toString().replace(/^\-/,'')
this.handleNextChange = true
}}
onChange={this.onChange}
value={this.state.value}
onChange={this.handleChange}
value={this.state.value.toString()
.replace('.', decimalSeparator)}
/>
{!isCurrencyPrefixed && <>&nbsp;</>}
</div>

View File

@ -1,10 +1,10 @@
import { expect } from 'chai'
import { shallow } from 'enzyme'
import { shallow, mount } from 'enzyme'
import React from 'react'
import { match, spy, useFakeTimers } from 'sinon'
import CurrencyInput from './CurrencyInput'
let getInput = component => shallow(component).find('input')
let getInput = component => mount(component).find('input')
describe('CurrencyInput', () => {
it('should render an input', () => {
expect(getInput(<CurrencyInput />)).to.have.length(1)
@ -13,20 +13,36 @@ describe('CurrencyInput', () => {
it('should accept both . and , as decimal separator', () => {
let onChange = spy()
const input = getInput(<CurrencyInput onChange={onChange} />)
input.simulate('change', { target: { value: '12.1' } })
input.simulate('change', { target: { value: '12.1', focus: () => {} } })
expect(onChange).to.have.been.calledWith(
match.hasNested('target.value', '12.1')
)
input.simulate('change', { target: { value: '12,1' } })
input.simulate('change', { target: { value: '12,1', focus: () => {} } })
expect(onChange).to.have.been.calledWith(
match.hasNested('target.value', '12.1')
)
})
it('should separate thousand groups', () => {
const input1 = getInput(<CurrencyInput value={1000} language="fr" />)
const input2 = getInput(<CurrencyInput value={1000} language="en" />)
const input3 = getInput(<CurrencyInput value={1000.5} language="en" />)
const input4 = getInput(<CurrencyInput value={1000000} language="en" />)
expect(input1.instance().value).to.equal('1 000')
expect(input2.instance().value).to.equal('1,000')
expect(input3.instance().value).to.equal('1,000.5')
expect(input4.instance().value).to.equal('1,000,000')
})
it('should handle decimal separator', () => {
const input = getInput(<CurrencyInput value={0.5} language="fr" />)
expect(input.instance().value).to.equal('0,5')
})
it('should not accept negative number', () => {
let onChange = spy()
const input = getInput(<CurrencyInput onChange={onChange} />)
input.simulate('change', { target: { value: '-12' } })
input.simulate('change', { target: { value: '-12', focus: () => {} } })
expect(onChange).to.have.been.calledWith(
match.hasNested('target.value', '12')
)
@ -35,19 +51,21 @@ describe('CurrencyInput', () => {
it('should not accept anything else than number', () => {
let onChange = spy()
const input = getInput(<CurrencyInput onChange={onChange} />)
input.simulate('change', { target: { value: '*1/2abc3' } })
input.simulate('change', { target: { value: '*1/2abc3', focus: () => {} } })
expect(onChange).to.have.been.calledWith(
match.hasNested('target.value', '123')
)
})
it('should pass other props to the input', () => {
const input = getInput(<CurrencyInput autoFocus />)
expect(input.prop('autoFocus')).to.be.true
})
it('should not call onChange while the decimal part is being written', () => {
let onChange = spy()
const input = getInput(<CurrencyInput onChange={onChange} />)
input.simulate('change', { target: { value: '111,' } })
input.simulate('change', { target: { value: '111,', focus: () => {} } })
expect(onChange).not.to.have.been.called
})
@ -74,10 +92,10 @@ describe('CurrencyInput', () => {
const input = getInput(
<CurrencyInput onChange={onChange} debounce={1000} />
)
input.simulate('change', { target: { value: '1' } })
input.simulate('change', { target: { value: '1', focus: () => {} } })
expect(onChange).not.to.have.been.called
clock.tick(500)
input.simulate('change', { target: { value: '12' } })
input.simulate('change', { target: { value: '12', focus: () => {} } })
clock.tick(600)
expect(onChange).not.to.have.been.called
clock.tick(400)

View File

@ -250,7 +250,7 @@ let CurrencyField = withColours(props => {
}}
debounce={600}
className="targetInput"
storeValue={props.input.value}
value={props.input.value}
{...props.input}
{...props}
/>
@ -267,7 +267,6 @@ let TargetInputOrValue = withLanguage(
firstStepCompleted,
inversionFail
}) => {
let normalizedValueRef = useRef(null)
let inputIsActive = activeInput === target.dottedName
return (
<span className="targetInputOrValue">
@ -275,10 +274,9 @@ let TargetInputOrValue = withLanguage(
<Field
name={target.dottedName}
component={CurrencyField}
normalizedValueRef={normalizedValueRef}
onBlur={event => event.preventDefault()}
{...(inputIsActive ? { autoFocus: true } : {})}
language={language}
normalize={(value) => normalizedValueRef.current || value}
/>
) : (
<TargetValue

View File

@ -35,6 +35,11 @@ export default withLanguage(
this.timeoutId = null
}, 250)
}
componentWillUnmount() {
if (this.timeoutId) {
clearTimeout(this.timeoutId)
}
}
format = value => {
return value == null
? ''