How to test Component which requires hook using react testing library? - reactjs

I have built a component which requires a methods and data supplied by custom hook
const MyComponent = () => {
const { data, methods } = useMyCustomHook({ defaultValues: defaultValues });
return <MyAnotherComponent data={data} label="Some Text" method={methods} />;
}
I am writing test using react testing library to test MyComponent or to be more specific to test MyAnotherComponent
Here is my testing code
test("Test MyAnotherComponent Label", () => {
const { data, methods } = useMyCustomHook({ defaultValues: defaultValues });
render(
<MyAnotherComponent
data={data}
label="Some Text"
method={methods}
/>
);
expect(screen.getByLabelText("Some Text")).toBeInTheDocument();
});
My testcase fails with an error saying Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons
I have looked up for solutions but some are too simple and some are too complex. Thanks in advance

The error is caused by the fact that you are calling the hook inside a function, not inside a React component.
Since you only want to test MyAnotherComponent, I don't see why you'd need to call the hook and not mock the data and methods directly to pass them to MyAnotherComponent because what you are currently doing is to re-write MyComponent.

Related

Using hooks in nested functions

I'm trying to rewrite a React class component into a functional hooks-based component, but i cannot figure out how to do it. The component logic and JSX looks something like this:
export class LeftPanel extends React.Component<ILeftPanelProps, ILeftPanelState> {
const [menuItemsFullList, setMenuItemsFullList] = useState([{links: []}] as any[]);
useEffect(() => {
const { links } = props;
setMenuItemsFullList(links);
}, props.links);
....
return (<>
<SearchBox
onChange={_onSearch}
onClear={_onClearSearchBox}
/>
<NavList
listEntries={[menuItems]}
/>
</>)
Where the function i'm currently rewriting is onClearSearchBox:
private _onClearSearchBox() {
this.setState({ menuItems: { ...this.state.menuItemsFullList } });
}
I tried naively rewriting it using hooks which turned the setState into this:
function onClearSearchBox() {
useEffect(() => setMenuItems(menuItemsFullList));
}
This does not work and i do not know how to restructure the code, as i cannot call hooks inside a non-React component function. Moving it into the React component function as an inner function does not work either.
The error message i'm getting is:
Uncaught Invariant Violation: Invalid hook call. Hooks can only be
called inside of the body of a function component...
I believe my mindset is still stuck to the class-based structure, as i cannot figure out how i would go about and refactoring the LeftPanel. How should i go about refactoring _onClearSearchBox to make it work with hooks?
useEffect is the wrong hook for this, from the docs:
If you’re familiar with React class lifecycle methods, you can think of useEffect Hook as componentDidMount, componentDidUpdate, and componentWillUnmount combined.
In your example, you need control over when to want to call the code e.g. on a button click. I'd say useCallback would be the most appropriate hook here:
const onClearSearchbox = useCallback(() => {
setMenuItemsFullList(props.items);
}, [props.items]);
...
<SearchBox onClear={onClearSearchBox} ... />

Warning when using react hooks in HoC

I've created a a higher order component that is supposed to add some additional functionality to my components. However, when I use react hooks in this component, I get the following eslint warning.
React Hook "React.useEffect" cannot be called inside a callback. React
Hooks must be called in a React function component or a custom React
Hook function. (react-hooks/rules-of-hooks)
Why am I getting this warning? Is it considered bad practice to use hooks in a HoC?
Minimal example:
const Hello = props => <p>Greetings {props.name}</p>;
const Wrapper = Component => props => {
React.useEffect(() => {
// Do something here
}, []);
return <Component {...props} />;
};
export default Wrapper(Hello)
codesandbox:
https://codesandbox.io/s/proud-tree-5kscc
Convert the
props => {
React.useEffect(() => {
// Do something here
}, []);
return <Component {...props} />;
};
inside your HOC to a function (react-hooks/rules-of-hooks is throwing that warning you showed when used in an arrow function returned by a HOC)
So, change it to
const Wrapper = Component =>
function Comp(props) {
React.useEffect(() => {
console.log("useEffect");
}, []);
return <Component {...props} />;
};
and the effect gets triggered.
Here is a working example on codesandbox
The official React Hooks documentation says:
Don’t call Hooks from regular JavaScript functions. Instead, you can:
✅ Call Hooks from React function components.
✅ Call Hooks from custom Hooks.
As #AsafAviv said, you should refactor your HOC into a custom hook to avoid violation the Rules of Hooks.
The reason is described in the FAQ by the way:
How does React associate Hook calls with components?
React keeps track of the currently rendering component. Thanks to the Rules of Hooks, we know that Hooks are only called from React components (or custom Hooks — which are also only called from React components).
There is an internal list of “memory cells” associated with each component. They’re just JavaScript objects where we can put some data. When you call a Hook like useState(), it reads the current cell (or initializes it during the first render), and then moves the pointer to the next one. This is how multiple useState() calls each get independent local state.
You can use react hooks in the functional components or in the custom Hooks.
rewrite your HOC:
const Hello = props => <p>Greetings {props.name}</p>;
const HookDoSomething = () => {
React.useEffect(() => {
// Do something here
}, []);
}
const Wrapper = Component => props => {
HookDoSomething()
return <Component {...props} />;
};
export default Wrapper(Hello)
Inside the file where you have your HoC defined, simply add the following to the top of the file:
/* eslint-disable react-hooks/rules-of-hooks */
Hooks and higher-order components are two completely different things. Anyone who says a HoC can be replaced by a hook has either never actually written a HoC or playing semantics games.
When I write a HoC, I often have to disable the rules-of-hooks eslint rule because the rule is too stringent wrt what it thinks is a hook or component. HoC is more akin to a component than a hook, but the rule does not recognize this.
Short Answer: You just need to change the callback to a PascalCase function or useSomething function. This is because eslint rule has some heuristic that you need to follow in order for it to detect components.
You will need to change your code to
const Wrapper = Component => {
return function WithWrapper(props){
React.useEffect(() => {
// Do something here
}, []);
return <Component {...props} />;
}
}
just change the name to lower case
like :
withWrrapperHOC = Comp => props => {
useEffect(()=>{
//...
},[])
return (<Comp {...props}>)
}

Testing functions inside stateless React component with Enzyme

I have a stateless component:
export default function TripReportFooter(props) {
const { tripReport, user, toggleFavorite, navigation } = props;
handleShare = async slug => {
try {
const result = await Share.share({
message: `Check out this Trip Report:\n/p/${slug}/`
});
if (result.action === Share.sharedAction) {
if (result.activityType) {
} else {
// shared
}
} else if (result.action === Share.dismissedAction) {
}
} catch (error) {
alert(error.message);
}
};
handleFavorite = async id => {
const token = await AsyncStorage.getItem("token");
toggleFavorite(id, token);
};
return (
... // handleFavorite and handleShare called with TouchableOpacities.
);
}
It has two functions inside, handleShare and handleFavorite. I want to test these functions are called, and also that handleFavorite calls the prop function toggle favorite.
I tried wrapper.instance().handleFavorite(), but since it is a stateless component, it returns null.
Next someone on Stack Overflow suggested using a spy like so:
wrapper = shallow(<TripReportFooter {...props} handleFavorite={press} />);
wrapper
.find("TouchableOpacity")
.at(0)
.simulate("press");
expect(press.called).to.equal(true);
but this returned
'TypeError: Cannot read property 'equal' of undefined'.
What's the proper way to call these functions?
You first need to think about what you want to test. Is it implementation details or the interaction with your component? The latter is a much better mindset and standpoint so what I would do is to test the component from the interaction point of view.
I would (for handleShare):
Mock the Share object methods that are being called inside the share function;
Select the button I want to click/touch
Click/touch the button
Assert that the methods were called.
Now for the handleFavorite:
Mock AsyncStorage.getItem;
Create a fake toggleFavorite function that I would pass as props;
Select the button I want to click/touch
Click/touch the button
Assert my toggleFavorite function has been called
If you want to test these functions individually you would have to extract them to the outside of the component and test them individually. But I would not advise this as it is not clean and extra work.
Hope it helps!
Functions within a functional component aren't defined on the prototype or the functional component instance, you cannot directly spy on them
The solution here is to test out the internal implementation of the individual functions
For instance for handleFavourite function you can mock AsynStorage and pass on a mock function for toggleFavourite and then asset it its called on TouchableOpacity onPress simulation
You can check how to mock AsyncStore in this post:
How to test Async Storage with Jest?
const mocktToggleFavourite = jest.fn();
wrapper = shallow(<TripReportFooter {...props} toggleFavourite={mocktToggleFavourite} />);
wrapper
.find("TouchableOpacity")
.at(0)
.simulate("press");
expect(mockToggleFavourite).toHaveBeenCalled();
Similarly you can test the individual functionalities within handleShare by first mocking Share.share and then checking against each condition.For instance you can add an spy on window.alert and see if that is called
const windowSpy = jest.spyOn(window, 'alert');
wrapper = shallow(<TripReportFooter {...props} toggleFavourite={mocktToggleFavourite} />);
//Simulate event that calls handleShare
// Mock Share to give required result
expect(windowSpy).toBeCalledWith(expectedValue);

Jest mocking with react calling actual event handler after calling mock function

I have this component test
const component = mount(<MemoryRouter><Login/></MemoryRouter>);
const emailField = component.find('input[type="email"]');
const passwordField = component.find('input[type="password"]');
const mockedSubmitFunction=jest.fn();
component.find(Login).instance().onSubmit=mockedSubmitFunction;
emailField.simulate('change', { target: { value: testEmail } });
passwordField.simulate('change', { target: { value: testPassword } });
component.find('form').simulate('submit');
expect(mockedSubmitFunction).toBeCalled();
and in the component i have
in constructor :-
this.onSubmit = this.onSubmit.bind(this);
and the eventhandler
onSubmit(event) {
event.preventDefault();
when i put a breakpoint in onSubmit it is coming to the component function after executing the mocked onSubmit, why is this happening.
I assumed it will only call the mocked onSubmit.
What am I doing differently?
CodeSandbox :https://codesandbox.io/s/q95lv7vlrw
But the sandbox is showing Could not find module in path: 'object-inspect/util.inspect' relative to '/node_modules/object-inspect/index.js' for some reason, which is unrelated i guess
So you got function mocked, but actual onSubmit is called. Instead if you want to call only mocked fn you have to provide it (as a prop in your test spec for example).
const mockedSubmitFunction = jest.fn(event => {
console.log("Mocked function");
});
const component = mount(
<MemoryRouter>
<Login login={mockedSubmitFunction} />
</MemoryRouter>
);
I updated sandbox for you.
You can additionally check this explained example on form testing.
Update: i suppose that the actual problem OP has was that mock function was firing, but it was copied to instance, thus expect...toBeCalled() fails (actual mockedFn was not called). You can avoid these problems by passing mocked function as a prop, spying on a function, etc.

Why doesn't my React onChange method match my arrow function in enzymes containsAllMatchingElements test

I have written a test case which is trying to test if react is rendering all my elements correctly.
Code that is being tested:
...
eventDateChange(moment, dateType) {
const {handleEventChange} = this.props;
let {event} = this.state;
event[dateType] = moment.format("YYYY-MM-DD HH:mm");
this.setState({event});
handleEventChange(event);
};
render() {
return (
<div className="event-input">
<DateTime
onChange={moment => this.eventDateChange(moment,'startDate')}
inputProps={{placeholder: 'From:'}}
dateFormat="YYYY-MM-DD"/>
</div>
)
}
...
Test code:
import React from "react";
import {expect} from "chai";
import {shallow} from "enzyme";
import EventInput from "../../../blog/event/EventInput.jsx";
import DateTime from "react-datetime";
describe('EventInput', () => {
it('is rendering an calendar icon', () => {
const wrapper = shallow(<EventInput/>);
expect(wrapper.containsAllMatchingElements([
<DateTime
onChange={moment => wrapper.instance.eventDateChange(moment,'startDate')}
inputProps={{placeholder: 'From:'}}
dateFormat="YYYY-MM-DD"/>
])).to.equal(true);
});
});
The problem is that my onChange method is failing the tests. If I remove the onChange method from the code and the test, the test is succeeding.
As you can see I was using Mocha, Chai, Enzyme in the tests.
From what I can see all the props are the same except for the onChange where I can't use this in the test and need to change it to the instance.
Looking at the implementation of how enzyme compares nodes (https://github.com/airbnb/enzyme/blob/d34630e9c3e07ca7983d37695d5668377f94a793/src/Utils.js#L102), it looks like enzyme requires props of type "function" to exactly match when doing this comparison (right[prop] === left[prop]) and will return false if this condition is not met.
Your lambda functions are not identical, so the === comparison will fail. Whenever you use lambda, you are creating a new anonymous function, so even if the parameters and body are the same between two lambda declarations, they are actually two separate function instances.
To get around this problem, you could either 1) create a reference to the exact function instance used in your component so that your test can reference it, or 2) use a different enzyme API here. I would suggest #2. It seems like you are overtesting a bit by checking all property values, and you could use something like this instead:
it('is rendering an calendar icon', () => {
const wrapper = shallow(<EventInput/>);
expect(wrapper.matchesElement(
<div>
<DateTime />
</div>
)).to.equal(true);
});
If you want to test individual props on the DateTime, you could additionally do something like: expect(wrapper.find("DateTime").props().dateFormat).to.equal("YYYY-MM-DD").

Resources