How to test props that are updated by an onChange handler in react testing library? - reactjs

I've got an onChange handler on an input that I'm trying to test based on what I've read in the Dom Testing Library docs here and here.
One difference in my code is that rather than using local state to control the input value, I'm using props. So the onChange function is actually calling another function (also received via props), which updates the state which has been "lifted up" to another component. Ultimately, the value for the input is received as a prop by the component and the input value is updated.
I'm mocking the props and trying to do a few simple tests to prove that the onChange handler is working as expected.
I expect that the function being called in the change handler to be called the same number of times that fireEvent.change is used in the test, and this works with:
const { input } = setup();
fireEvent.change(input, { target: { value: "" } });
expect(handleInstantSearchInputChange).toHaveBeenCalledTimes(1);
I expect that the input.value is read from the original mock prop setup, and this works with:
const { input } = setup();
expect(input.value).toBe("bacon");
However, I'm doing something stupid (not understanding mock functions at all, it would seem), and I can't figure out why the following block does not update the input.value, and continues to read the input.value setup from the original mock prop setup.
This fails with expecting "" / received "bacon" <= set in original prop
fireEvent.change(input, { target: { value: "" } });
expect(input.value).toBe("");
QUESTION: How can I write a test to prove that the input.value has been changed given the code below? I assume that I need the mock handleInstantSearchInputChange function to do something, but I don't really know what I'm doing here quite yet.
Thanks for any advice on how to do and/or better understand this.
Test File
import React from "react";
import InstantSearchForm from "../../components/InstantSearchForm";
import { render, cleanup, fireEvent } from "react-testing-library";
afterEach(cleanup);
let handleInstantSearchInputChange, props;
handleInstantSearchInputChange = jest.fn();
props = {
foodSearch: "bacon",
handleInstantSearchInputChange: handleInstantSearchInputChange
};
const setup = () => {
const utils = render(<InstantSearchForm {...props} />);
const input = utils.getByLabelText("food-search-input");
return {
input,
...utils
};
};
it("should render InstantSearchForm correctly with provided foodSearch prop", () => {
const { input } = setup();
expect(input.value).toBe("bacon");
});
it("should handle change", () => {
const { input } = setup();
fireEvent.change(input, { target: { value: "" } });
expect(input.value).toBe("");
fireEvent.change(input, { target: { value: "snickerdoodle" } });
expect(input.value).toBe("snickerdoodle");
});
Component
import React from "react";
import PropTypes from "prop-types";
const InstantSearchForm = props => {
const handleChange = e => {
props.handleInstantSearchInputChange(e.target.value);
};
return (
<div className="form-group">
<label className="col-form-label col-form-label-lg" htmlFor="food-search">
What did you eat, fatty?
</label>
<input
aria-label="food-search-input"
className="form-control form-control-lg"
onChange={handleChange}
placeholder="e.g. i ate bacon and eggs for breakfast with a glass of whole milk."
type="text"
value={props.foodSearch}
/>
</div>
);
};
InstantSearchForm.propTypes = {
foodSearch: PropTypes.string.isRequired,
handleInstantSearchInputChange: PropTypes.func.isRequired
};
export default InstantSearchForm;

The way you are thinking about your tests is slightly incorrect. The behavior of this component is purely the following:
When passed a text as a prop foodSearch renders it correctly.
Component calls the appropriate handler on change.
So only test for the above.
What happens to the foodSearch prop after the change event is triggered is not the responsibility of this component(InstantSearchForm). That responsibility lies with the method that handles that state. So, you would want to test that handler method specifically as a separate test.

Related

How can I test an input with Jest

I've been trying to figure out how to test different input methods but since I am new to this test methodology, I cannot get even close to the answer. Here is what I have:
const App = (props) => {
const newGame = props.newGame;
const [typeracertext, setTyperacertext] = useState(props.typeracertext);
const [wholeText, setWholeText] = useState("");
const onChange = (e) => {
//here I have code that read the input and is comparing it with variable - typeracertext and if so, it sets the property wholeText to that value
};
return (
<input ref={(node) => this.textInput = node} placeholder="Message..." onChange={onChange}></input>
);
}
so what I am trying to figure out is a test that should set the typeracertext to a certain value (for example "This is a test), and set the input value to "This" so if it passes the onChange() check it should set wholeText to "This". I hope that makes sense.
This is the best I could get and I don't have an idea what should I write on "expect".
test('Test the input value', () => {
const node = this.textInput;
node.value = 'This';
ReactTestUtils.Simulate.change(node);
expect()
});
Since this is a react app, I'll advice you take advantage of react testing library to make this easy
import React from 'react';
import { fireEvent, render, screen } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
// In describe block
test('Test input component', () => {
const onChange = jest.fn();
render(<InputComponent onChange={onChange} data-test-id="input" />);
const input = screen.getByTestId('input');
fireEvent.change(input, { target: { value: 'a value' } });
// You can also do this with userEvent
userEvent.type(input, 'test')
// Check if change event was fired
expect((input as HTMLInputElement).onchange).toHaveBeenCalled();
});
See documentation here

useEffect cleanup runs on every render

I am trying to build a functionality where when a user navigates away from the form i.e when component unmounts it should trigger a save i.e post form data to server. This should happen only if there is any change in form data. Can anyone guide me as to why this is happening. I have tried class based approach which works but I do not want to refactor my production code.
import { useCallback, useEffect, useState } from "react";
import React from "react";
import * as _ from "lodash";
import { useFormik } from "formik";
// for now this is hardcoded here..but let's assume
// this server data will be loaded when component mounts
const serverData = {
choice: "yes",
comment: "some existing comment"
};
const availableChoices = ["yes", "no"];
const Form = () => {
const formik = useFormik({ initialValues: { ...serverData } });
const [isFormChanged, setIsFormChanged] = useState(false);
const valuesHaveChanged = React.memo(() => {
console.log("INIT VALUES= ", formik.initialValues);
console.log("FINAL VALUES = ", formik.values);
return !_.isEqual(formik.initialValues, formik.values);
}, [formik.initialValues, formik.values]);
const triggerSave = () => console.log("Save");
useEffect(() => {
// setForm({ ...serverData });
if (valuesHaveChanged) {
setIsFormChanged(true);
}
return () => {
// when this cleanup function runs
// i.e when this component unmounts,
// i need to check if there
// was any change in the form state
// if there was a change i need to trigger a save
// i.e post form data to server.
if (setIsFormChanged) {
triggerSave();
}
};
});
return (
<form>
<div className="form-group">
{availableChoices.map((choice) => (
<label key={choice}>
{choice}
<input
id="choice"
value={choice}
className="form-control"
type="radio"
name="choice"
checked={choice === formik.values.choice}
onChange={formik.handleChange}
/>
</label>
))}
</div>
<div className="form-group">
<textarea
rows="5"
cols="30"
id="comment"
name="comment"
value={formik.values.comment}
onChange={formik.handleChange}
className="form-control"
placeholder="some text..."
></textarea>
</div>
</form>
);
};
export default Form;
The first problem i spotted is the dependency array.
useEffect(() => {
// the flag can be set anytime upon a field has changed
// maybe formik has a value like that, read doc
if (valuesHaveChanged) {
setIsFormChanged(true);
}
return () => {
if (setIsFormChanged) {
triggerSave();
}
}
// the dependency array is [], can't be missed
}, [])
Currently you are calling this effect and cleanup this effect in every update, ex. if any value changes in this component. But normally you only want to do it once upon dismount.
Even you do the above right, you still need to make sure your code contains no memory leak, because you are trying to do something upon the dismount. So it's better to pass the values:
triggerSave([...formik.values])
And make sure inside triggerSave, you don't accidently call anything about formik or setState.
Try to use useEffect with dependencies
useEffect(() => {
return () => {
// when this cleanup function runs
// i.e when this component unmounts,
// i need to check if there
// was any change in the form state
// if there was a change i need to trigger a save
// i.e post form data to server.
if (!_.isEqual(formik.initialValues, formik.values)) {
triggerSave();
}
};
}, [formik.values]); // won't run on every render but just on formik.values update
Explanation:
useEffect has dependencies as a second argument, if [] is passed - effect is triggered only on mount, if [...] passed, will trigger on the first mount and on any of ... update.
If you don't pass the second agrument, useEffect works as a on-every-render effect.

How to fire and test a real paste event (not simulated by calling the prop) in Jest and Enzyme

I'm trying to unit test a very simple feature in a React app where I'm blocking the user from pasting into a textarea by adding an event.preventDefault() in the event handler, like so:
function handlePaste(event) {
event.preventDefault();
}
// ... pass it down as props
<TextareaComponent onPaste={handlePaste} />
The problem I'm having is that every method I've found of dispatching events in Jest or Enzyme just "simulates" the event by getting the function passed to the onPaste prop and calling it directly with a mock event object. That's not what I'm interested in testing.
Ideally I want to do something like this, testing that the actual value of the input hasn't changed after pasting:
const wrapper = mount(<ParentComponent inputValue="Prefilled text" />);
const input = wrapper.find(TextareaComponent);
expect(input.value).toEqual("Prefilled text")
input.doAPaste("Pasted text")
expect(input.value).not.toEqual("Pasted text")
expect(input.value).toEqual("Prefilled text")
But haven't been able to find a method that works. Any help would be appreciated!
Since you're just testing against a synthetic event (and not some sort of secondary action -- like a pop up that warns the user that pasting is disabled), then the easiest and correct solution is to simulate a paste event, pass it a mocked preventDefault function, and then assert that the mocked function was called.
Attempting to make assertions against a real paste event is pointless as this a React/Javascript implementation (for example, making assertions that a callback function is called when an onPaste/onChange event is triggered). Instead, you'll want to test against what happens as a result of calling the callback function (in this example, making assertions that event.preventDefault was called -- if it wasn't called, then we know the callback function was never executed!).
Working example (click the Tests tab to run the assertions):
To keep it simple, I'm just asserting that the input is initially empty and then only updates the value if an onChange event was triggered. This can very easily be adapted to have some sort of passed in prop influence the default input's value.
App.js
import React, { useCallback, useState } from "react";
const App = () => {
const [value, setValue] = useState("");
const handleChange = useCallback(
({ target: { value } }) => setValue(value),
[]
);
const handlePaste = useCallback((e) => {
e.preventDefault();
}, []);
const resetValue = useCallback(() => {
setValue("");
}, []);
const handleSubmit = useCallback(
(e) => {
e.preventDefault();
console.log(`Submitted value: ${value}`);
setValue("");
},
[value]
);
return (
<form onSubmit={handleSubmit}>
<label htmlFor="foo">
<input
id="foo"
type="text"
data-testid="test-input"
value={value}
onPaste={handlePaste}
onChange={handleChange}
/>
</label>
<br />
<button data-testid="reset-button" type="button" onClick={resetValue}>
Reset
</button>
<button type="submit">Submit</button>
</form>
);
};
export default App;
App.test.js
import React from "react";
import { configure, mount } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import App from "./App";
configure({ adapter: new Adapter() });
const value = "Hello";
describe("App", () => {
let wrapper;
let inputNode;
beforeEach(() => {
wrapper = mount(<App />);
// finding the input node by a 'data-testid'; this is not required, but easier
// when working with multiple form elements and can be easily removed
// when the app is compiled for production
inputNode = () => wrapper.find("[data-testid='test-input']");
});
it("initially displays an empty input", () => {
expect(inputNode()).toHaveLength(1);
expect(inputNode().props().value).toEqual("");
});
it("updates the input's value", () => {
inputNode().simulate("change", { target: { value } });
expect(inputNode().props().value).toEqual(value);
});
it("prevents the input's value from updating from a paste event", () => {
const mockPreventDefault = jest.fn();
const prefilledText = "Goodbye";
// updating input with prefilled text
inputNode().simulate("change", { target: { value: prefilledText } });
// simulating a paste event with a mocked preventDefault
// the target.value isn't required, but included for illustration purposes
inputNode().simulate("paste", {
preventDefault: mockPreventDefault,
target: { value }
});
// asserting that "event.preventDefault" was called
expect(mockPreventDefault).toHaveBeenCalled();
// asserting that the input's value wasn't changed
expect(inputNode().props().value).toEqual(prefilledText);
});
it("resets the input's value", () => {
inputNode().simulate("change", { target: { value } });
wrapper.find("[data-testid='reset-button']").simulate("click");
expect(inputNode().props().value).toEqual("");
});
it("submits the input's value", () => {
inputNode().simulate("change", { target: { value } });
wrapper.find("form").simulate("submit");
expect(inputNode().props().value).toEqual("");
});
});

How to write Test cases for useEffect Hook in React using Jest & Enzyme?

import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
const InputComponent = ({ item, data }) => {
const [value, setValue] = useState('');
// Binding values for Edit based on unique Key
useEffect(() => {
if (data && data[item.field] && data[item.field] !== 'undefined') {
setValue(data[item.field]);
}
}, [data,item.field]);
//On change setting state
const setState = e => {
setValue(e.target.value);
};
return (
<div className='Input-Containter' data-test='InputBox-tbl-evt'>
<input
data-test='input-ele'
type='text'
value={value}
onChange={e => setState(e)}
/>
</div>
);
};
InputComponent.propTypes = {
item: PropTypes.object,
data: PropTypes.object
};
InputComponent.defaultProps = {
data: {
id: '1',
name: 'd'
},
item:{
field: 'id',
}
};
export default InputComponent;
Can someone help me How to test for setValue() in useEffect
-> Updated complete code for this Component
-> Component will take some data for binding values into input element & in the same
component we can edit values in it as-well.
First, let's take closer look onto useEffect section. What does it mean?
if any of prop is changed
and combination of new values are given some meaningful value(not undefined)
we initialize input's value based on those props even if we have to override custom value
How to test that? Changing prop(s) and validating input's value.
Based on that we may have up to 3(changed only first prop, only second or both) * 2 (if result is undefined or not) * 2 (if there has been already custom value provided and stored in useState or not) = 12. But I believe exhaustive testing is not a good way. So I'd put most checks in single test case:
it('re-init value for nested input after props changes', () => {
const wrapper = mount(<InputComponent />);
function getInput(wrapper) {
return wrapper.find("input").prop("value");
}
expect(getInput(wrapper).props("value")).toEqual("1"); // based on default props
getInput(wrapper).props().onChange({ target: {value: "initial"} }); // imitating manual change
expect(getInput(wrapper).props("value")).toEqual("initial");
wrapper.setProps({data: {a: "some-a", b: "undefined"} });
expect(getInput(wrapper).props("value")).toEqual("initial");
wrapper.setProps({ item: { field: "c" } }); // data[item.field] is undefined
expect(getInput(wrapper).props("value")).toEqual("initial");
wrapper.setProps({ item: {field: "b" } }); // data[item.field] is "undefined"
expect(getInput(wrapper).props("value")).toEqual("initial");
wrapper.setProps({ item: {field: "a" } }); // data[item.field] is meaningful
expect(getInput(wrapper).props("value")).toEqual("some-a");
});
As for getValue helper - it's needed cause we cannot just memoize input element itself like:
const wrapper = mount(...);
const input = wrapper.find("input");
...
expect(input.prop("value")).toEqual();
...
expect(input.prop("value")).toEqual();
Details can be found in Enzyme's docs. Or just know we need to re-run find after any update.
Also beware Enzyme's setProps does not replace current props but update them by merging(as well as React's setState does with state).

Jest/Enzyme Shallow testing RFC - not firing jest.fn()

I'm trying to test the onChange prop (and the value) of an input on an RFC. On the tests, trying to simulate the event doesn't fire the jest mock function.
The actual component is connected (with redux) but I'm exporting it also as an unconnected component so I can do a shallow unit test. I'm also using some react-spring hooks for animation.
I've also tried to mount instead of shallow the component but I still get the same problem.
MY Component
export const UnconnectedSearchInput: React.FC<INT.IInputProps> = ({ scrolled, getUserInputRequest }): JSX.Element => {
const [change, setChange] = useState<string>('')
const handleChange = (e: InputVal): void => {
setChange(e.target.value)
}
const handleKeyUp = (): void => {
getUserInputRequest(change)
}
return (
<animated.div
className="search-input"
data-test="component-search-input"
style={animateInputContainer}>
<animated.input
type="text"
name="search"
className="search-input__inp"
data-test="search-input"
style={animateInput}
onChange={handleChange}
onKeyUp={handleKeyUp}
value={change}
/>
</animated.div>
)
}
export default connect(null, { getUserInputRequest })(UnconnectedSearchInput);
My Tests
Here you can see the test that is failing. Commented out code is other things that I-ve tried so far without any luck.
describe('test input and dispatch action', () => {
let changeValueMock
let wrapper
const userInput = 'matrix'
beforeEach(() => {
changeValueMock = jest.fn()
const props = {
handleChange: changeValueMock
}
wrapper = shallow(<UnconnectedSearchInput {...props} />).dive()
// wrapper = mount(<UnconnectedSearchInput {...props} />)
})
test('should update input value', () => {
const input = findByTestAttr(wrapper, 'search-input').dive()
// const component = findByTestAttr(wrapper, 'search-input').last()
expect(input.name()).toBe('input')
expect(changeValueMock).not.toHaveBeenCalled()
input.props().onChange({ target: { value: userInput } }) // not geting called
// input.simulate('change', { target: { value: userInput } })
// used with mount
// act(() => {
// input.props().onChange({ target: { value: userInput } })
// })
// wrapper.update()
expect(changeValueMock).toBeCalledTimes(1)
// expect(input.prop('value')).toBe(userInput);
})
})
Test Error
Nothing too special here.
expect(jest.fn()).toBeCalledTimes(1)
Expected mock function to have been called one time, but it was called zero times.
71 | // wrapper.update()
72 |
> 73 | expect(changeValueMock).toBeCalledTimes(1)
Any help would be greatly appreciated since it's been 2 days now and I cn't figure this out.
you don't have to interact with component internals; instead better use public interface: props and render result
test('should update input value', () => {
expect(findByTestAttr(wrapper, 'search-input').dive().props().value).toEqual('');
findByTestAttr(wrapper, 'search-input').dive().props().onChange({ target: {value: '_test_'} });
expect(findByTestAttr(wrapper, 'search-input').dive().props().value).toEqual('_test_');
}
See you don't need to check if some internal method has been called, what's its name or argument. If you get what you need - and you require to have <input> with some expected value - it does not matter how it happened.
But if function is passed from the outside(through props) you will definitely want to verify if it's called at some expected case
test('should call getUserInputRequest prop on keyUp event', () => {
const getUserInputRequest = jest.fn();
const mockedEvent = { target: { key: 'A' } };
const = wrapper = shallow(<UnconnectedSearchInput getUserInputRequest={getUserInputRequest } />).dive()
findByTestAttr(wrapper, 'search-input').dive().props().onKeyUp(mockedEvent)
expect(getUserInputRequest).toHaveBeenCalledTimes(1);
expect(getUserInputRequest).toHaveBeenCalledWith(mockedEvent);
}
[UPD] seems like caching selector in interm variable like
const input = findByTestAttr(wrapper, 'search-input').dive();
input.props().onChange({ target: {value: '_test_'} });
expect(input.props().value).toEqual('_test_');
does not pass since input refers to stale old object where value does not update.
At enzyme's github I've been answered that it's expected behavior:
This is intended behavior in enzyme v3 - see https://github.com/airbnb/enzyme/blob/master/docs/guides/migration-from-2-to-3.md#calling-props-after-a-state-change.
So yes, exactly - everything must be re-found from the root if anything has changed.

Resources