Testing onChange event with Jest - reactjs

According to my code coverage, I need to test the function called in my onChange event. This is actually where I update my functional component's state using the useState hook.
Here is my component :
const Component:React.FC<{}> = () => {
const {value, setState} = useState('');
return(
<View>
<CustomComponent
onChange={(value) => setState(value)}
/>
</View>
)
}
The customComponent is a component derived from the React Input component.
When text is changed inside of it, it calls the onChange function passed as its prop with the text. This is how it comes up to the parent Component which sets the value of the text input in its state as displayed above.
My code coverage returns this analysis :
onChange={//(value) => setState(value)//}
Where the code between the // has to be covered. I don't really understand how I can cover this. First thought was use mock functions, but I can't seem to find how to mock it to the onChange event since I don't pass anything as prop to the main Component.

After a few tests, I finally understood that the coverage wasn't asking for the actual test of the onChange function but actually the value that is evaluated. Therefore, here is what I am doing:
Fetching the TextInput Child component
Changing its Text
Evaluating what it renders
I am using #testing-library/react-native here because it makes selecting tree components easier with the use of accessibilityLabel for example (It actually made me understand the importance of that prop).
Here is what a test looks like:
describe('Testing useState functions', () => {
test('test', () => {
//Rendering the component and its tree
const { container, getByLabelText } = render(<SignupView />);
//Extracting the child, username_input component with his accessibilityLabel
const username_input = getByLabelText('username_input');
const email_input = getByLabelText('email_input');
//Fire a native changeText event with a specific value
fireEvent.changeText(username_input, 'doe');
fireEvent.changeText(email_input, 'doe#joe.com');
//Checking the rendered value
expect(username_input.props.value).toEqual('doe');
expect(email_input.props.value).toEqual('doe#joe.com');
});
});

Related

Test React Form internal submit

I have this component:
function Form() {
const [value, setValue] = useState('');
const hanleSubmit = (ev) => {
ev.preventDefault();
console.log('submit', value);
}
return (
<form onSubmit={props.onSubmit}>
<input type="text" value={value} onChange={e => setValue(e.target.value)} />
<button type="submit">Add</button>
</form>
);
}
and the following test:
import React from "react";
import { render, fireEvent } from "react-testing-library";
import Form from "./Form";
it("submits", () => {
const onSubmit = jest.fn();
const { getByText } = render(<Form />);
fireEvent.click(getByText("Add"));
expect(onSubmit).toHaveBeenCalled(); // test fail with 0 called
});
I know I gotta to pass this mock function as prop.
But I'd like to know if exist a way to test internal function like handleSubmit without pass as prop?
I'm using react-testing-library.
You can use jest.spyOn(console, 'log') and check the handleSubmit is triggered indirectly. In fact, handleSubmit should have some more meaningful logic, not just console.log(). We should test the meaningful logic. But for the code you posted, then only we can do is above.
You should test the behavior of the component rather than an implementation detail.
The behavior of the component is: when you trigger the event handler, some states may change, the component will re-render, UI structure change, assert UI changes.
The implementation detail is: assert some function is called or not. This makes test cases vulnerable.
Test behavior or function, you don't need to pay attention to implementation details, regardless of the implementation details, we only test the final result.
If your component is just a presentation component with no state, just perform a Snapshot test to check the UI hierarchy.
If there are states, you need to test different states of the component.
If the state is promoted to the parent, then you test the parent and the state of the child on the UI when the parent state changes
Passing the mock onSubmit as a prop is a simple and common way.
If you want to handle some logic in Form component and call the onSubmit function passed from parent component in the same time.
You should write it like this:
const hanleSubmit = (ev) => {
ev.preventDefault();
// some logic for Form component itself, you should test these logic when you test Form component rather than `props.onSubmit()`.
console.log('submit', value);
// Parent component only care about the submitted value.
props?.onSubmit(value)
}

What is the pattern for ensuring a user action triggers a Recoil state update in a Jest test

Suppose I have a (rather contrived) controller and view
const useController = () => {
const setId = useSetRecoilState(idState)
return {
setId
}
}
const MyComponent = () => {
const { setId } = useController()
return (
<button onClick={() => setId(1)}>Click me!</button>
)
}
What is the correct pattern for testing
describe('my component', () => {
test.todo('when click button, state updated to 1')
})
This pattern comes up a lot for me when I have a component that sets state and I want to verify that the component is actually updating the state (say, via an <input> and that I haven't forgotten to add the recoil setter into the onChange event handler.
I can imagine rendering that component under test inside a custom wrapper that sets the whole state as the value of an INPUT or something and then in my test I get that INPUT's value and I have the state, but is there a better way?
I'm essentially trying to spy on the atom's setter I guess.
Edit To clarify, I'm following what even the recoil testing library says: if you're testing a hook that is just for one component, you should test them together. So my "unit tests" treat the unit as "controller-as-hook" and component (the view).
You don't need to spy on anything. Just act on the component or hook via react-test-library and trigger an action with that. Then you can use recoils snapshot_UNSTABLE() to generate a new snapshot of the recoil state which then allows you to test the value of an atom, selector, etc. against a expected value, like so:
const initialSnapshot = snapshot_UNSTABLE();
expect(initialSnapshot.getLoadable(idState).valueOrThrow()).toBe(0);
act(() => {/* act on your component or hook to mutate the state */});
const nextSnapshot = snapshot_UNSTABLE();
expect(nextSnapshot.getLoadable(idState).valueOrThrow()).toBe(1);

Not simulating 'change' using enzyme

I have a Reactjs component and it has one button and a date-range picker.
I want to simulate onclick and onchange events of button and picker respectively.
I'm able to simulate onclick of the button. but on change of datepicker is not working
I have tried this
headerComponent.find(`#prev_button`).at(1).simulate("click");
headerComponent.find(`#dropdown`).at(1).simulate("change", { value: "t" });
please see this sandbox click here for full code and test file
Based on Enzyme documentation you make a mistake on your second argument on simulate function.
To simulate changes on the input, you should change it like this :
headerComponent.find(`#dropdown`).at(1).simulate("change", { target: { value: "t" } });
Testing with enzyme is tricky. You should try not to test dependencies because you trust those are already tested. Having said that, you could shallow render instead of mounting and look for the RangePicker component in the shallow tree, get the handler you are passing in the onChange prop and call it manually, then check the callback prop you pass to your component is called with the expected value.
describe.only("test", () => {
it("should render", () => {
const callBackToSetDates = jest.fn();
const callBackToSetFilter = jest.fn();
const wrapper = shallow(
<Header
{...headerProps1}
callBackToSetDates={callBackToSetDates}
callBackToSetFilter={callBackToSetFilter}
/>
);
const rangePickerOnChange = wrapper.find("RangePicker").prop("onChange");
rangePickerOnChange("someValue");
expect(callBackToSetDates).toHaveBeenCalledWith("someValue");
});
});
the purpose is to test only the logic you add inside your component, i.e., you transform the value you get from the RangePicker to something else
<RangePicker
...
onChange={(value) => {
callBackToSetDates(`I'm transforming ${value}`);
}}
/>
and in your test
rangePickerOnChange("someValue");
expect(callBackToSetDates).toHaveBeenCalledWith("I'm transforming someValue");
you can see it working here https://codesandbox.io/s/cool-rosalind-uec6t?file=/src/tests/index.test.js
If you really want to keep testing what the actual user sees, you'll need to fire the events that the user does when using the component. In this case: you need to click the input, look for a date, click it, then click another date to completely fire the onChange event of the RangePicker component. You might look at how antd test it and copy the necessary jest configuration they have to mock some DOM APIs

Child re-rendering because of function?

I'm having trouble navigating these concepts where a child keeps re-rendering because I'm passing it a function from the parent. This parent function references an editor's value, draftjs.
function Parent() {
const [doSomethingValue, setDoSomethingValue] = React.useState("");
const [editorState, setEditorState] = React.useState(
EditorState.createEmpty()
);
const editorRef = useRef<HTMLInputElement>(null);
const doSomething = () => {
// get draftjs editor current value and make a fetch call
let userResponse = editorState.getCurrentContent().getPlainText("\u0001");
// do something with userResponse
setDoSomethingValue(someValue);
}
return (
<React.Fragment>
<Child doSomething={doSomething} />
<Editor
ref={editorRef}
editorState={editorState}
onChange={setEditorState}
placeholder="Start writing..." />
<AnotherChild doSomethingValue={doSomethingValue}
<React.Fragment>
}
}
My Child component is simply a button that calls the parent's doSomething and thats it.
doSomething does its thing and then makes a change to the state which is then passed to AnotherChild.
My problem is that anytime the editorState is updated (which is every time you type within the editor), my Child component re-renders. Isn't that unnecessary? And if so, how could I avoid this?
If I was passing my Child component a string and leveraged React.Memo, it does not re-render unless the string changes.
So what am I missing with passing a function to the child? Should my child be re-rendering everytime?
React works on reference change detection to re-render components.
Child.js: Wrap it under React.memo so it becomes Pure Component.
const Child = ({doSomething}) => <button onClick={doSomething}>Child Button Name</button>;
export default React.memo(Child);
Parent.js -> doSomething: On every (re)render, callbacks are also recreated. Make use of useCallback so that your function is not recreated on every render.
const doSomething = React.useCallback(() => {
let userResponse = editorState.getCurrentContent().getPlainText("\u0001");
setDoSomethingValue(someValue);
}, [editorState]);
Side Note
On broader lines, memo is HOC and makes a component Pure Component. useMemo is something which cache the output of the function. Whereas useCallback caches the instance of the function.
Hope it helps.
If you do not want your component to be re-rendered every time your parent is re-rendered, you should take a look at useMemo.
This function will only recalculate its value (in your case, your component) whenever its second argument changes (here, the only thing it depends on, doSomething()).
function Parent() {
const [editorState, setEditorState] = React.useState(
EditorState.createEmpty()
);
const editorRef = useRef<HTMLInputElement>(null);
const doSomething = () => {
// get draftjs editor current value and make a fetch call
let userResponse = editorState.getCurrentContent().getPlainText("\u0001");
// do something with userResponse
}
const childComp = useMemo(() => <Child doSomething={doSomething} />, [doSomething])
return (
<React.Fragment>
{childComp}
<Editor
ref={editorRef}
editorState={editorState}
onChange={setEditorState}
placeholder="Start writing..." />
<React.Fragment>
}
}
If doSomething does not change, your component does not re-render.
You may also want to use useCallback for your function if it is doing heavy calculations, to avoid having it re-compiled every time your component renders: https://reactjs.org/docs/hooks-reference.html#usecallback
Take a look into PureComponent, useMemo, or shouldComponentUpdate
I would also add, instead of passing down the function to do the rendering of a top level component, pass the value and define the function later down in the component tree.
if you want to avoid unnecessary re-renders, you can use React.memo and the hook useCallback. Take a look at the following sandbox.
The button1 is always re-rendered because it take a callback that is not memoized with useCallback and the button2 is just rendered the first time even if the state of the parent has changed (take a look at the console to check the re-renders). You have to use React.memo in the Child component that is the responsible to render the buttons.
I hope it helps.

How to test a function that has been passed by props

If I have a component like this
import React from "react";
const GettingStarted = ({ object }) => <Button onPress={() => object.next()} />;
How would you test with jest that object.next() gets called inside the component?
The basic idea would be to spy on the function being passed to the onPress. Then, you would simulate an onPress event on the button and check that the spied upon function was called with any parameters, etc. Then you would test the actual output of the function. So, for example, if the function changes the text in the button from 'Click Me' to 'Clicked!', you would assert on the first text property before the click and then check the updated one.
Example with Jest:
const onPressSpy = jest.fn();
const gettingStartedButton = shallow(<GettingStarted object={onPressSpy} />);
expect(gettingStartedButton.find('button').children().text()).toBe('Click Me!');
gettingStartedButton.find('button').simulate('press');
expect(onPressSpy).toHaveBeenCalled();
expect(gettingStartedButton.find('button').children().text()).toBe('Clicked!');

Resources