Consider I have a function that is called from inside the ComponentDidUpdate. I need to unit test the canCheck function.
ComponentDidUpdate(prevProps,prevState){
if(prevState.isFull){
callA();
}
if(canCheck(prevProps)){
callB()
}
}
The canCheck function is as follows
canCheck(prevProps){
const {
isFocused,
getTheStartState,
index,
loading
} = this.props;
if (!isFocused) {
return false;
}
return (
index === 0 &&
prevProps.getTheStartState &&
!getTheStartState &&
!loading
);
}
Things I have tried include
const ShallowRenderer = require('react-test-renderer/shallow');
describe('canCheck', () => {
test('should return correctly value', () => {
const renderer = new ShallowRenderer();
renderer.render(
<TestContainer
isFocused
getTheStart
{...dummy}
/>
);
const instance = renderer.getMountedInstance();
instance.componentDidUpdate(
{
getTheStartState: false
loading: true
},
{ someState: true }
);
expect(renderer.getMountedInstance().canCheck()).toBeFalsy();
});
});
Though I have mentioned getTheStartState in the prevProps of componentDidUpdate I always get the error as cannot read getTheStartState of undefined. Are there any suggestions of how we can unit test such function, I could not find much resource for this online!
I have a component with functionality which validates input element onblur and adds error class if validation fails:
TableRows = (props )=>(
return <input class="inputElement" onBlur="this.validate()" />
)
validate function is as follows:
validate = async ({ target }: ChangeEvent<HTMLInputElement>) => {
try {
const element = target;
this.setState({ loading: true });
const { value } = target;
const match = value.match(/^\d+(?:\.\d{0,2})?$/g);
if (!match || match.length === 0) {
element.className += ' inputError';
} else {
element.className = target.className.replace(' inputError', '');
}
const { data: updatedValues } = await sendforSaving({inputValue: value});
this.setState({ newValues: data });
} finally {
this.setState({ loading: false });
}
};
I am trying to write a unit test with enzyme as follows:
it('should add error class on invalid input onblur', () => {
const mockVal4Test = {
localCurrency: 'USD',
value: '20.02.1',
} as any;
component = shallow(
<TableRows {...defaultProps} initialValues={mockVal4Test} currencyType={CurrencyType.LOCAL} />
);
const myInput = component.find('.inputElement');
myInput.simulate('blur');
expect(saleTarget.hasClass('inputError')).toBe(true);
});
I get the myInput element but after simulating blur I am expecting the 'validate' function to be called and error class "inputError" to be added to the element.
I used a mock event for blur event to pass to simulate. And later the same mock event is used to check the changes, as the blur event changes the passed event object. Here is my solution.
it('should add error class on invalid input for StoreTarget input on blur', () => {
component = shallow(
<TableRows {...defaultProps} initialValues={mockVal4Test} currencyType={CurrencyType.LOCAL} />
)
const mockedEvent = {
target: {
value: '1.2.1.2',
className:'inputClass'
}
} as any;
const myInput = component.find('. inputElement');
myInput.simulate('blur', mockedEvent );
expect(mockedEvent.target.className.includes('inputError')).toBe(true);
});
This is the updated code now. Let me know if something is not correct as I am able to compile but the issue still persists
constructor(props) {
super(props);
this.polyglot = PolyglotFactory.getPolyglot(props.pageLang);
this.state = {
otherInvestorSubtype: props.otherInvestorSubtype,
};
}
shouldRenderOtherSubtype = () => this.props.otherInvestorSubtype === OTHER_INVESTOR_SUBTYPE;
shouldRenderSubtype = () => {
const { investorTypeOptions, investorType } = this.props;
const investorTypeOption = investorTypeOptions.find(({ value }) => value === investorType);
return investorTypeOption !== undefined && investorTypeOption.subtypes.length > 0;
}
handleOtherInvestorSubtypeChange = (e) => {
this.setState({
otherInvestorSubtype: e.target.value,
});
this.props.handleOtherInvestorSubtypeChange();
}
renderSelectOtherSubtype = () => {
const { handleOtherInvestorSubtypeChange,
showError, registerChildValidator, pageLang } = this.props;
const { otherInvestorSubtype } = this.state;
return (
<ValidatedText
name="investor_subtype_other"
value={otherInvestorSubtype}
onChange={this.handleOtherInvestorSubtypeChange}
showError={showError}
registerValidation={registerChildValidator}
validation={validation(this.polyglot.t('inputs.investorTypes'), pageLang, rules.required())}
required
/>
);
}
This is the only information I have got for this. Let me know if something is missing.
It seems like you've set the textbox value as otherInvestorSubtype which is provided by the props. This way, the component (so that the textbox value) is not updated on textbox value change. You need to store the otherInvestorSubtype value provided by props in the InvestorType component's state as the following, in order to update the InvestorType component every time the user types something:
constructor(props) {
...
this.state {
otherInvestorSubtype: props.otherInvestorSubtype
}
}
change the renderSelectOtherSubtype method as the following:
renderSelectOtherSubtype = () => {
const { handleOtherInvestorSubtypeChange, showError, registerChildValidator, pageLang } = this.props;
const { otherInvestorSubtype } = this.state
return (
<ValidatedText
...
value={ otherInvestorSubtype }
onChange={ this.handleOtherInvestorSubtypeChange }
...
/>
);
}
and finally handle the textbox change on this component:
handleOtherInvestorSubtypeChange = (e) => {
this.setState({
otherInvestorSubtype: e.target.value
});
this.props.handleOtherInvestorSubtypeChange();
}
Hope this helps, and sorry if I have any typos.
I recently wanted to design an input component with react hooks.
The component would check validation after entering input in 0.5 second.
my code like
const inputField = ({ name, type, hint, inputValue, setInput }) => {
// if there is default value, set default value to state
const [value, setValue] = useState(inputValue);
// all of validation are true for testing
const validCheck = () => true;
let timeout;
const handleChange = e => {
clearTimeout(timeout);
const v = e.target.value;
setValue(v);
timeout = setTimeout(() => {
// if valid
if (validCheck()) {
// do something...
}
}, 500);
};
return (
<SCinputField>
<input type={type} onChange={handleChange} />
</SCinputField>
);
};
unfortunately, it's not worked, because the timeout variable would renew every time after setValue.
I found react-hooks provide some feature like useRef to store variable.
Should I use it or shouldn't use react-hooks in this case?
Update
add useEffect
const inputField = ({ name, type, hint, inputValue, setInput }) => {
// if there is default value, set default value to state
const [value, setValue] = useState(inputValue);
// all of validation are true for testing
const validCheck = () => true;
let timeout;
const handleChange = e => {
const v = e.target.value;
setValue(v);
};
// handle timeout
useEffect(() => {
let timeout;
if (inputValue !== value) {
timeout = setTimeout(() => {
const valid = validCheck(value);
console.log('fire after a moment');
setInput({
key: name,
valid,
value
});
}, 1000);
}
return () => {
clearTimeout(timeout);
};
});
return (
<SCinputField>
<input type={type} onChange={handleChange} />
</SCinputField>
);
};
It looks worked, but I am not sure about it's a right way to use.
Here's how I would do it:
import React, {useState, useEffect, useRef} from 'react';
function InputField() {
const [value, setValue] = useState(''); // STATE FOR THE INPUT VALUE
const timeoutRef = useRef(null); // REF TO KEEP TRACK OF THE TIMEOUT
function validate() { // VALIDATE FUNCTION
console.log('Validating after 500ms...');
}
useEffect(() => { // EFFECT TO RUN AFTER CHANGE IN VALUE
if (timeoutRef.current !== null) { // IF THERE'S A RUNNING TIMEOUT
clearTimeout(timeoutRef.current); // THEN, CANCEL IT
}
timeoutRef.current = setTimeout(()=> { // SET A TIMEOUT
timeoutRef.current = null; // RESET REF TO NULL WHEN IT RUNS
value !== '' ? validate() : null; // VALIDATE ANY NON-EMPTY VALUE
},500); // AFTER 500ms
},[value]); // RUN EFFECT AFTER CHANGE IN VALUE
return( // SIMPLE TEXT INPUT
<input type='text'
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}
WORKING EXAMPLE ON SNIPPET BELOW:
function InputField() {
const [value, setValue] = React.useState('');
const timeoutRef = React.useRef(null);
function validate() {
console.log('Validating after 500ms...');
}
React.useEffect(() => {
if (timeoutRef.current !== null) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(()=> {
timeoutRef.current = null;
value !== '' ? validate() : null;
},500);
},[value]);
return(
<input type='text' value={value} onChange={(e) => setValue(e.target.value)}/>
);
}
ReactDOM.render(<InputField/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>
You don't need to keep the reference to the timeout between renders. You can just return a function from the useEffect to clear it:
React.useEffect(() => {
const timeout = setTimeout(()=> {
if (value !== '') {
validate();
}
}, 500);
return () => {
clearTimeout(timeout); // this guarantees to run right before the next effect
}
},[value, validate]);
Also, don't forget to pass all the dependencies to the effect, including the validate function.
Ideally, you would pass the value as a parameter to the validate function: validate(value) - this way, the function has fewer dependencies, and could even be pure and moved outside the component.
Alternatively, if you have internal dependencies (like another setState or an onError callback from props), create the validate function with a useCallback() hook :
const validate = useCallback((value) => {
// do something with the `value` state
if ( /* value is NOT valid */ ) {
onError(); // call the props for an error
} else {
onValid();
}
}, [onError, onValid]); // and any other dependencies your function may use
This will keep the same function reference between the renders if the dependencies don't change.
You can move timeout variable inside handleChange method.
const inputField = ({ name, type, hint, inputValue, setInput }) => {
// if there is default value, set default value to state
const [value, setValue] = useState(inputValue);
// all of validation are true for testing
const validCheck = () => true;
const handleChange = e => {
let timeout;
clearTimeout(timeout);
const v = e.target.value;
setValue(v);
timeout = setTimeout(() => {
// if valid
if (validCheck()) {
// do something...
}
}, 500);
};
return (
<SCinputField>
<input type={type} onChange={handleChange} />
</SCinputField>
);
};
I've started using Flow type on top of a project created with create-react-app tool. I struggle to make a simple scenario work where a class property is filled with element reference in render method but throws 2 errors. What am I doing wrong? All the checks should prevent those warnings.
class MyComponent extends React.Component<*> {
input: ?HTMLInputElement;
componentDidUpdate = () => {
if (this.input) {
this.input.focus();
if (this.input.value) {
const valueLength = this.input.value.length;
this.input.setSelectionRange(valueLength, valueLength);
}
}
};
render() {
return <input ref={ref => (this.input = ref)} />;
}
}
Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ src/todo/index.js:38:28
Property value is missing in null or undefined [1].
[1] 33│ input: ?HTMLInputElement;
34│
35│ componentDidUpdate = () => {
36│ if (this.input) {
37│ this.input.focus();
38│ if (this.input.value) {
39│ const valueLength = this.input.value.length;
40│ this.input.setSelectionRange(valueLength, valueLength);
41│ }
Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ src/todo/index.js:40:28
Cannot call this.input.setSelectionRange because property setSelectionRange is missing in null or undefined [1].
[1] 33│ input: ?HTMLInputElement;
34│
35│ componentDidUpdate = () => {
36│ if (this.input) {
37│ this.input.focus();
38│ if (this.input.value) {
39│ const valueLength = this.input.value.length;
40│ this.input.setSelectionRange(valueLength, valueLength);
41│ }
42│ }
43│ };
Since you're calling methods, flow assumes that things could change at anytime. You need to keep a reference to the input and then you're all good. Something like below:
class MyComponent extends React.Component<*> {
input: ?HTMLInputElement;
componentDidUpdate = () => {
const { input } = this
if (input) {
input.focus();
if (input.value) {
const valueLength = input.value.length;
input.setSelectionRange(valueLength, valueLength);
}
}
};
render() {
return <input ref={ref => (this.input = ref)} />;
}
}