I'm trying to apply a unit test in my custom input component
If I pass the value prop in the <Input /> in the render function, the test fails.
Does anyone know the reason?
In controlled components like this input, what we can test to have valid unit tests?
Thanks
Input.tsx
import React, {ChangeEvent, FunctionComponent} from 'react';
import styles from './Input.module.css'
export interface InputProps {
type?: string
disabled?: boolean
value?: string
onChange?: (value: string) => void
}
const Input: FunctionComponent<InputProps> = ({
type= 'text',
disabled = false,
value,
onChange,
}) => {
const handleOnChange = (e: ChangeEvent<HTMLInputElement>) => {
const value = e.currentTarget.value
if (onChange) {
onChange(value)
}
}
return (
<div className={styles.inputWrapper}>
<input
className={styles.input}
type={type}
disabled={disabled}
value={value}
onChange={handleOnChange}
/>
</div>
);
}
export default Input;
Input.test.tsx
// PASS WITHOUT ANY ERRORS
it('should has the new value', async () => {
const user = userEvent.setup()
const onChangeMock = jest.fn();
const { getByRole } = render(<Input onChange={onChangeMock} />)
const input = getByRole('textbox')
expect(input).toHaveValue('')
await user.type(input, 'new input value')
expect(input).toHaveValue('new input value')
})
// FAILS
it('should has the new value', async () => {
const user = userEvent.setup()
const onChangeMock = jest.fn();
const { getByRole } = render(<Input value="" onChange={onChangeMock} />)
const input = getByRole('textbox')
expect(input).toHaveValue('')
await user.type(input, 'new input value')
expect(input).toHaveValue('new input value')
})
You can use rerender function that is returned from render.
With this, you send new value into your component, input has a new value and then you can assert for that value in the input.
In your case, your test needs to have also checking if onChange method is called when you do userEvent.type() or if that does not work you can use fireEvent.change(input...).
it('should has the new value', async () => {
const user = userEvent.setup();
const onChangeMock = jest.fn();
const { getByRole, rerender } = render(<Input value="" onChange={onChangeMock} />);
const input = getByRole('textbox');
expect(input).toHaveValue('');
rerender(<Input value="new input value" onChange={onChangeMock} />);
expect(input).toHaveValue('new input value');
});
I am trying to build a small app to test my jest and enzyme knowledge. However, I have run into an issue.
I have the following Homepage.js:
const Homepage = props => {
const [input, setInput] = React.useState({
type: 'input',
config: {
required: true,
placeholder: 'Type a GitHub username'
},
value: ''
});
const onChangeInput = event => {
setInput(prevState => {
return {
...prevState,
value: event.target.value
};
});
};
return (
<section className={styles.Homepage}>
<form className={styles.SearchBox} onSubmit={(event) => props.getUser(event, input.value)} data-test="component-searchBox">
<h4>Find a GitHub user</h4>
<Input {...input} action={onChangeInput}/>
{props.error ? <p>{`Error: ${props.error}`}</p> : null}
<Button>Submit</Button>
</form>
</section>
);
};
I want to test that the input field fires a function when I type in it. The Input.js component is the following:
const input = props => {
switch (props.type) {
case 'input':
return <input className={styles.Input} {...props.config} value={props.value} onChange={props.action} data-test="component-input"/>
default: return null;
};
};
You can find my test below:
const mountSetup = () => {
const mockGetUser = jest.fn()
return mount(
<Homepage type='input' getUser={mockGetUser}>
<Input type='input'/>
</Homepage>
);
};
test('State updates with input box upon change', () => {
const mockSetInput = jest.fn();
React.useState = jest.fn(() => ["", mockSetInput]);
const wrapper = mountSetup();
const inputBox = findByTestAttr(wrapper, 'component-input');
inputBox.simulate("change");
expect(mockSetInput).toHaveBeenCalled();
});
The problem here is that in the Input.js the switch is always returning null even though I am passing the props type='input'. As such I get the error Method “simulate” is meant to be run on 1 node. 0 found instead.
Can someone help me with this?
Thanks
You need not pass the Input inside the HomePage. Homepage has input child and it renders
<Homepage type='input' getUser={mockGetUser}>
</Homepage>
You can try passing the data in the react Usestate mock.
React.useState = jest.fn(() => [{
type: 'input'
}, mockSetInput]);
I have a simple functional component which I need to test.
const Product = () => {
const handleOnChange = (value) => {
console.log(value);
}
return (
<div>
<input type="text" onChange={(e) => {handleOnChange(e.target.value)}} />
</div>
)
}
I'd like to test if "handleOnChange" function is called when the input changes its value. I tried that:
let wrapper;
beforeEach(() => {
wrapper = shallow(<Product />);
});
describe('Product interactions', () => {
it('should call handleOnChange function on input change', () => {
const mockedhandleOnChange = jest.fn();
wrapper.handleOnChange = mockedhandleOnChange;
wrapper.find('input').simulate('change', {target: {value: 10}});
expect(mockedhandleOnChange).toHaveBeenCalledTimes(1);
});
});
Of course it doesn't work as I cannot reach the function via "wrapper.handleOnChange".
Please help!
it('should call handleOnChange function on input change', () => {
const wrapper = shallow(<Product />);
const input = wrapper.find('input');
input.simulate('change', { target: { value: 10 } });
input = wrapper.find('input');
expect(input.props().value).toEqual(10);
});
You should make React state single source of truth. Currently, input in your program is maintaining its own state. Better option would be to use it as Controlled Components. Once you are using input as a controlled component, your program would be something like below:
class Product extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};
}
handleOnChange = (value) => {
this.setState({ value })
}
return (
<div>
<input id="input1" type="text" value={this.state.value} onChange={(e) => {handleOnChange(e.target.value)}} />
</div>
)
}
Once you have the above code, it will be much simpler to do validation on input and also to test. You need to call onChange of input, after which you can validate state. Your test will look something like below:
let wrapper;
beforeEach(() => {
wrapper = shallow(<Product />);
});
describe('Product interactions', () => {
it('should call handleOnChange function on input change', () => {
wrapper.find('#input1').getNode().props.onChange({
target: { value: '123' }
});
expect(wrapper.state().value).toEqual('123');
});
});
I am trying to do a complete istanbul coverage test with jest. At this moment I have a component almost all tested but there is a handleSubmit function where I make a dispatch receiving form event data and when I run the test it tells me
TypeError: Cannot read property 'value' of undefined
10 | payload: {
11 | name: name.value,
> 12 | item: item.value,
| ^
13 | status: status.value }
14 | })
15 | }
I am loading a mockstore, mounted all the component, its all tested but the submit still fails. My test function is as simple as:
it('testing submit', () => {
const form = component.find(`[data-test="submit"]`).first()
form.simulate('submit')
... and continues expecting some existences, but there aren't problems there
I already tried this: enzyme simulate submit form, Cannot read property 'value' of undefined
And tried to parse the event values in the simulate action...
The complete module code is...
class Filters extends Component {
handleSubmit = event => {
event.preventDefault()
const {name, items, status} = event.target;
this.props.dispatch({
type: 'SEARCH_PLAYER',
payload: {
name: name.value,
item: item.value,
status: status.value }
})
}
render() {
return(
<div>
<form onSubmit={this.handleSubmit} data-test="submit">
<div className="form-group col-md-12 text-center"> ...
Another really crazy thing is that my test recognize the "event.target.name.value" and not the items and status. In fact if i delete items and status from the dispatch the test runs successfully.
Looks like you are using item on line 12, but extracting items from the event.target.
The way you chose to handle values is a bit strange. Instead, handle values through state like so: Controlled Components
Then you can test that this.props.dispatch() was called with the correct values.
Side note: Avoid using data attributes when unnecessary, as they'll start to clog up your DOM with superfluous attributes. You have plenty of options to find by element, element.className, className, ...and so on.
Working example: https://codesandbox.io/s/5j4474rkk (you can run the test defined below by clicking on the Tests tab at the bottom left of the screen.
components/Form/Form.js
import React, { Component } from "react";
import PropTypes from "prop-types";
export default class Form extends Component {
state = {
test: ""
};
static propTypes = {
dispatch: PropTypes.func.isRequired
};
handleChange = ({ target: { name, value } }) => {
this.setState({ [name]: value });
};
handleSubmit = e => {
e.preventDefault();
this.props.dispatch({
type: "SEARCH_PLAYER",
payload: {
test: this.state.test
}
});
};
render = () => (
<form onSubmit={this.handleSubmit} className="form-container">
<h1>Form Testing</h1>
<input
className="uk-input input"
type="text"
name="test"
placeholder="Type something..."
onChange={this.handleChange}
value={this.state.test}
/>
<button type="submit" className="uk-button uk-button-primary submit">
Submit
</button>
</form>
);
}
components/Form/__tests__/Form.js (shallowWrap and checkProps are custom functions that can be found in test/utils/index.js)
import React from "react";
import { shallowWrap, checkProps } from "../../../test/utils";
import Form from "../Form";
const dispatch = jest.fn();
const initialProps = {
dispatch
};
const initialState = {
test: ""
};
const wrapper = shallowWrap(<Form {...initialProps} />, initialState);
describe("Form", () => {
it("renders without errors", () => {
const formComponent = wrapper.find(".form-container");
expect(formComponent).toHaveLength(1);
});
it("does not throw PropType warnings", () => {
checkProps(Form, initialProps);
});
it("submits correct values to dispatch", () => {
const name = "test";
const value = "Hello World!";
const finalValues = {
type: "SEARCH_PLAYER",
payload: {
[name]: value
}
};
wrapper.find("input").simulate("change", { target: { name, value } }); // simulates an input onChange event with supplied: name (event.target.name) and value (event.target.value)
wrapper
.find(".form-container")
.simulate("submit", { preventDefault: () => null }); // simulates a form submission that has a mocked preventDefault (event.preventDefault()) to avoid errors about it being undefined upon form submission
expect(dispatch).toBeCalledWith(finalValues); // expect dispatch to be called with the values defined above
});
});
I am using React and have formatted a controlled input field, which works fine when I write some numbers and click outside the input field. However, when I want to edit the input, the cursor jumps to the front of the value in the input field. This only occur in IE, and not in e.g. Chrome. I've seen that for some programmers the cursor jumps to the back of the value. So I think the reason that my cursor is jumping to the front is because the value is aligned to the right instead of to the left in the input field. Here is a senario:
My first input is 1000
Then I want to edit it to 10003, but the result is
31000
Is there a way to controll that the cursor should not jump?
Here's a drop-in replacement for the <input/> tag. It's a simple functional component that uses hooks to preserve and restore the cursor position:
import React, { useEffect, useRef, useState } from 'react';
const ControlledInput = (props) => {
const { value, onChange, ...rest } = props;
const [cursor, setCursor] = useState(null);
const ref = useRef(null);
useEffect(() => {
const input = ref.current;
if (input) input.setSelectionRange(cursor, cursor);
}, [ref, cursor, value]);
const handleChange = (e) => {
setCursor(e.target.selectionStart);
onChange && onChange(e);
};
return <input ref={ref} value={value} onChange={handleChange} {...rest} />;
};
export default ControlledInput;
Taking a guess by your question, your code most likely looks similar to this:
<input
autoFocus="autofocus"
type="text"
value={this.state.value}
onChange={(e) => this.setState({value: e.target.value})}
/>
This may vary in behaviour if your event is handled with onBlur but essentially its the same issue. The behaviour here, which many have stated as a React "bug", is actually expected behaviour.
Your input control's value is not an initial value of the control when its loaded, but rather an underlying value bound to this.state. And when the state changes the control is re-rendered by React.
Essentially this means that the control is recreated by React and populated by the state's value. The problem is that it has no way of knowing what the cursor position was before it was recreated.
One way of solving this which I found to work is remembering the cursor position before it was re-rendered as follows:
<input
autoFocus="autofocus"
type="text"
value={ this.state.value }
onChange={(e) => {
this.cursor = e.target.selectionStart;
this.setState({value: e.target.value});
}
}
onFocus={(e) => {
e.target.selectionStart = this.cursor;
}
}
/>
This is my solution:
import React, { Component } from "react";
class App extends Component {
constructor(props) {
super(props);
this.state = {
name: ""
};
//get reference for input
this.nameRef = React.createRef();
//Setup cursor position for input
this.cursor;
}
componentDidUpdate() {
this._setCursorPositions();
}
_setCursorPositions = () => {
//reset the cursor position for input
this.nameRef.current.selectionStart = this.cursor;
this.nameRef.current.selectionEnd = this.cursor;
};
handleInputChange = (key, val) => {
this.setState({
[key]: val
});
};
render() {
return (
<div className="content">
<div className="form-group col-md-3">
<label htmlFor="name">Name</label>
<input
ref={this.nameRef}
type="text"
autoComplete="off"
className="form-control"
id="name"
value={this.state.name}
onChange={event => {
this.cursor = event.target.selectionStart;
this.handleInputChange("name", event.currentTarget.value);
}}
/>
</div>
</div>
);
}
}
export default App;
This is an easy solution. Worked for me.
<Input
ref={input=>input && (input.input.selectionStart=input.input.selectionEnd=this.cursor)}
value={this.state.inputtext}
onChange={(e)=>{
this.cursor = e.target.selectionStart;
this.setState({inputtext: e.target.value})
/>
Explanation:
What we are doing here is we save the cursor position in onChange(), now when the tag re-renders due to a change in the state value, the ref code is executed, and inside the ref code we restore out cursor position.
If you're using textarea, then here's the hook based on Daniel Loiterton's code using TypeScript:
interface IControlledTextArea {
value: string
onChange: ChangeEventHandler<HTMLTextAreaElement> | undefined
[x: string]: any
}
const ControlledTextArea = ({ value, onChange, ...rest }: IControlledTextArea) => {
const [cursor, setCursor] = useState(0)
const ref = useRef(null)
useEffect(() => {
const input: any = ref.current
if (input) {
input.setSelectionRange(cursor, cursor)
}
}, [ref, cursor, value])
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
setCursor(e.target.selectionStart)
onChange && onChange(e)
}
return <textarea ref={ref} value={value} onChange={handleChange} {...rest} />
}
As (I think) others have mentioned, React will keep track of this if you make your changes synchronously. Unfortunately, that's not always feasible. The other solutions suggest tracking the cursor position independently, but this will not work for input types like 'email' which will not allow you to use cursor properties/methods like selectionStart, setSelectionRange or whatever.
Instead, I did something like this:
const Input = (props) => {
const { onChange: _onChange, value } = props;
const [localValue, setLocalValue] = useState(value);
const onChange = useCallback(
e => {
setLocalValue(e.target.value);
_onChange(e.target.value);
},
[_onChange]
);
useEffect(() => {
setLocalValue(value);
}, [value]);
// Use JSX here if you prefer
return react.createElement('input', {
...props,
value: localValue,
onChange
});
};
This allows you to delegate the cursor positioning back to React, but make your async changes.
My cursor jumped always to the end of the line. This solution seems to fix the problem (from github):
import * as React from "react";
import * as ReactDOM from "react-dom";
class App extends React.Component<{}, { text: string }> {
private textarea: React.RefObject<HTMLTextAreaElement>;
constructor(props) {
super(props);
this.state = { text: "" };
this.textarea = React.createRef();
}
handleChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
const cursor = e.target.selectionStart;
this.setState({ text: e.target.value }, () => {
if (this.textarea.current != null)
this.textarea.current.selectionEnd = cursor;
});
}
render() {
return (
<textarea
ref={this.textarea}
value={this.state.text}
onChange={this.handleChange.bind(this)}
/>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
Here is my solution
const Input = () => {
const [val, setVal] = useState('');
const inputEl = useRef(null);
const handleInputChange = e => {
const { value, selectionEnd } = e.target;
const rightCharsCount = value.length - selectionEnd;
const formattedValue = parseInt(value.replace(/\D/g, ''), 10).toLocaleString();
const newPosition = formattedValue.length - rightCharsCount;
setVal(formattedValue);
setTimeout(() => {
inputEl.current.setSelectionRange(newPosition, newPosition);
}, 0);
};
return <input ref={inputEl} value={val} onChange={handleInputChange} />;
};
// Here is a custom hook to overcome this problem:
import { useRef, useCallback, useLayoutEffect } from 'react'
/**
* This hook overcomes this issue {#link https://github.com/reduxjs/react-redux/issues/525}
* This is not an ideal solution. We need to figure out why the places where this hook is being used
* the controlled InputText fields are losing their cursor position when being remounted to the DOM
* #param {Function} callback - the onChangeCallback for the inputRef
* #returns {Function} - the newCallback that fixes the cursor position from being reset
*/
const useControlledInputOnChangeCursorFix = callback => {
const inputCursor = useRef(0)
const inputRef = useRef(null)
const newCallback = useCallback(
e => {
inputCursor.current = e.target.selectionStart
if (e.target.type === 'text') {
inputRef.current = e.target
}
callback(e)
},
[callback],
)
useLayoutEffect(() => {
if (inputRef.current) {
inputRef.current.setSelectionRange(inputCursor.current, inputCursor.current)
}
})
return newCallback
}
export default useControlledInputOnChangeCursorFix
// Usage:
import React, { useReducer, useCallback } from 'react'
import useControlledInputOnChangeCursorFix from '../path/to/hookFolder/useControlledInputOnChangeCursorFix'
// Mimics this.setState for a class Component
const setStateReducer = (state, action) => ({ ...state, ...action })
const initialState = { street: '', address: '' }
const SomeComponent = props => {
const [state, setState] = useReducer(setStateReducer, initialState)
const handleOnChange = useControlledInputOnChangeCursorFix(
useCallback(({ target: { name, value } }) => {
setState({ [name]: value })
}, []),
)
const { street, address } = state
return (
<form>
<input name='street' value={street} onChange={handleOnChange} />
<input name='address' value={address} onChange={handleOnChange} />
</form>
)
}
For anybody having this issue in react-native-web here is solution written in TypeScript
const CursorFixTextInput = React.forwardRef((props: TextInputProps, refInput: ForwardedRef<TextInput>) => {
if(typeof refInput === "function") {
console.warn("CursorFixTextInput needs a MutableRefObject as reference to work!");
return <TextInput key={"invalid-ref"} {...props} />;
}
if(!("HTMLInputElement" in self)) {
return <TextInput key={"no-web"} {...props} />;
}
const { value, onChange, ...restProps } = props;
const defaultRefObject = useRef<TextInput>(null);
const refObject: RefObject<TextInput> = refInput || defaultRefObject;
const [ selection, setSelection ] = useState<SelectionState>(kInitialSelectionState);
useEffect(() => {
if(refObject.current instanceof HTMLInputElement) {
refObject.current.setSelectionRange(selection.start, selection.end);
}
}, [ refObject, selection, value ]);
return (
<TextInput
ref={refObject}
value={value}
onChange={event => {
const eventTarget = event.target as any;
if(eventTarget instanceof HTMLInputElement) {
setSelection({
start: eventTarget.selectionStart,
end: eventTarget.selectionEnd
});
}
if(onChange) {
onChange(event);
}
}}
{...restProps}
/>
)
});
The simplest and safest way of doing this is probably to save the cursor position before React renders the input and then setting it again after React finishes rendering.
import React, {ReactElement, useEffect, useRef} from "react";
/**
* Text input that preserves cursor position during rendering.
*
* This will not preserve a selection.
*/
function TextInputWithStableCursor(
props: React.InputHTMLAttributes<HTMLInputElement> & {type?: "text"}
): ReactElement {
const inputRef = useRef<HTMLInputElement>(null);
// Save the cursor position before rendering
const cursorPosition = inputRef.current?.selectionStart;
// Set it to the same value after rendering
useEffect(function () {
if (
typeof cursorPosition === "number" &&
document.activeElement === inputRef.current
) {
inputRef.current?.setSelectionRange(cursorPosition, cursorPosition);
}
});
return <input ref={inputRef} {...props} />;
}
If you faced an issue with the cursor jumping to the end after updating the input state and updating the cursor using refs -> I found a workaround for it by setting the cursor in Promise.resolve's microtask.
<input
value={value}
onChange={handleValueUpdate}
ref={inputRef}
/>
const handleValueUpdate = (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
// ...
// some value handling logic
setValue(newValue)
const cursorPosition = getCursorPositionLogic();
/**
* HACK: set the cursor on the next tick to make sure that the value is updated
* useTimeout with 0ms provides issues when two keys are pressed same time
*/
Promise.resolve().then(() => {
inputRef.current?.setSelectionRange(cursorPosition, cursorPosition);
});
}
I know the OP is 5 years old but some are still facing the same kind of issue and this page has an high visibility on Google search.
Try by replacing :
<input value={...}
with
<input defaultValue={...}
This will solve most of the cases i've seen around there.
I tried all of the above solutions and none of them worked for me. Instead I updated both the e.currentTarget.selectionStart & e.currentTarget.selectionEnd on the onKeyUp React synthetic event type. For example:
const [cursorState, updateCursorState] = useState({});
const [formState, updateFormState] = useState({ "email": "" });
const handleOnChange = (e) => {
// Update your state & cursor state in your onChange handler
updateCursorState(e.target.selectionStart);
updateFormState(e.target.value);
}
<input
name="email"
value={formState.email}
onChange={(e) => handleOnChange(e)}
onKeyUp={(e) => {
// You only need to update your select position in the onKeyUp handler:
e.currentTarget.selectionStart = cursorState.cursorPosition;
e.currentTarget.selectionEnd = cursorState.cursorPosition;
}}
/>
Also, be aware that selectionStart & selectionEnd getters are not available on input fields of type email.