How to test react components props (expect component to be called with) - reactjs

I need to test that a react component is called with opened={true} prop after an button click is fired. I am using testing-library ( testing-library/react + testing-library/jest-dom).
I mocked the Component using something like
import Component from "./path-to-file/component-name"
...
jest.mock("./path-to-file/component-name", () => {
return jest.fn().mockImplementation(() => {
return null
})
})
I first tried with:
expect(Component).toBeCalledWith(expect.objectContaining({"opened": true}))
expect(Component).toHaveBeenCalledWith(expect.objectContaining({"opened": true}))
expect(Component).toHaveBeenLastCalledWith(expect.objectContaining({"opened": true}))
but I got Error: expect(jest.fn()).toBeCalledWith(...expected).
Same went for expect.objectContaining({"opened": expect.anything()})
And even for expect(Component).toBeCalledWith(expect.anything())
And the difference is empty array:
I also tried with expect(ChartMenu.mock).toBeCalledWith(expect.anything()). I got a different error but still not working (this time the error was Error: expect(received).toBeCalledWith(...expected) + Matcher error: received value must be a mock or spy function)
Thank you in advice!
EDIT: here is a simplified version of the component I want to test:
const Component = () => {
const [chartMenuOpened, setChartMenuOpened] = useState(false)
return (
<Flex>
<EllipseIcon onClick={() => setChartMenuOpened(true)}>
+
</EllipseIcon>
<ChartMenu
opened={chartMenuOpened}
close={() => setChartMenuOpened(false)}
/>
</Flex>
)
}
Basically I want to make sure that when the + icon is clicked the menu will be opened (or called with open value). The issue is that I cannot render ChartMenu because it needs multiple props and redux state.
I was able in the end to mock useState in order to check that the setState was properly called from the icon component (in order to make sure there won't be future changes on the component that will break this using this post).
But I would still really appreciate an answer to the question: if there is any way to create a spy or something similar on a react component and check the props it was called with? Mostly because this was a rather simple example and I only have one state. But this might not always be the case. Or any good idea on how to properly test this kind if interaction would be really appeciated.

I think you are on the right track to test if the component has been called with that prop, it's probably the syntax issue in your code
I learn this trick from colleague and you can try to see if this helps fix your issue.
expect(Component).toHaveBeenCalledWith(
expect.objectContaining({
opened: true,
}),
expect.anything()
);

While the question on how to is answered, I did some extra research on this and it seems like in React components there is a second parameter refOrContext (so basically most of the time it's an empty object, but it can also be a ref or a context)
Despite pointing out the reason for the behavior, I also wanted to highlight that it is safer to use expect.anything() as the second argument (rather than just {} which would work only in most of the cases ):
More information about React second argument here

Related

React hooks: am I doing an anti pattern? updating state in function outside the component

I am beginning to use React (hooks only), and facing a strange issue. I am trying to reproduce the problem in a small test code, but can't get it to happen, except in my full blown app. This leads me to wonder if I'm doing something really wrong.
I have an array of objects, declared as a state. I map this array to display its content. Except that nothing gets displayed (the array is filled, but nothing gets displayed). Now if I declare an un-related state, make it a boolean which flips each time my array gets updated, then my array gets displayed properly. As if, in the render phase itself, React did not detect the array's changes.
A few things:
the array gets updated by a socketIO connection, I simulate it here with a timer
I update my array OUTSIDE of my component function, BUT providing the setter function to the update function
I also create part of the render fields outside my component function (this has no effect, just for readability in my full app)
I essence, this is what I am doing:
const updateArray = (setTestArray, setTestTag, addArray) => {
setTestArray(prevTestArray => {
let newTestArray = prevTestArray.map((data, index) => (data + addArray[index]))
return newTestArray
})
setTestTag(prevTag => {
return (!prevTag)
})
}
const renderArray = (currentTestArray) => {
return currentTestArray.map((data, index) => (
<div>
testArray[{index}]={data}
</div>
))
}
function TestPage(props) {
const [testArray, setTestArray] = useState([])
const [testTag, setTestTag] = useState(false)
useEffect(() => {
let samples = 3
let initArray= []
for (let i=0; i<samples;i++) initArray[i] = Math.random()
setTestArray(initArray)
// In real code: setup socket here...
setInterval(() => {
let addArray= []
for (let i=0; i<samples;i++) addArray[i] = Math.random()
updateArray(setTestArray, setTestTag, addArray)
}, 1000)
return (() => {
// In real code, disconnect socket here...
})
}, []);
return (
<Paper>
Array content:
{renderArray(testArray)}
<br/>
Tag: {(testTag)? 'true' : 'false'}
</Paper>
)
}
This works just fine. But, in my full app, if I comment out everything concerning "testTag", then my array content never displays. testArray's content is as expected, updates just fine, but placing a debugger inside the map section show that array as empty.
Thus my questions:
is my updateArray function a bad idea? From what I read, my prevTestArray input will always reflect the latest state value, and setTestArray is never supposed to change... This is the only way I see to handle the async calls my socket connection generate, without placing "testArray" in my useEffect dependencies (thus avoiding continuously connecting/disconnecting the socket?)
rendering outside the component, in renderArray, doesn't affect my tests (same result if I move the code inside my component), but is there a problem with this?
As a side note, my array's content is actually more complex is the real app (array of objects), I have tried placing this in this test code, it works just fine too...
Thank you!
Edit: Note that moving updateArray inside the useEffect seems to be the recommended pattern. I did that in my full app. The hook linter does not complain about any missing dependency, yet this still doesn't work in my full app.
But the question is still whether what I am doing here is wrong or not: I know it goes against the guidelines as it prevents the linter from doing its job, but it looks to me like this will still work, the previous state being accessible by default in the setter functions.
Edit #2: Shame on me... silly mistake in my real app code, the equivalent of updateArray had a shallow array copy at some place instead of a deep copy.
Why adding the flipping tag made it work is beyond me (knowing the data was then indeed properly displayed and updated), but getting rid of this mistake solved it all.
I will leave this question on, as the question still stand: is placing the state update, and part of the rendering outside the component a functional problem, or just something which might mater on hide dependencies (preventing the react hooks linter from doing its job) and thus simply bad practice?
The fact is that things work just fine now with both functions outside the component function, which makes sense based on what I understand from hooks at this point.

How to unit test conditional (branch) inside component render (before return)

I have set a conditional "return null" if no data is found for a function invocation inside my table component render (before return) so that it won't break (extra safety layer)
My problem is that I'm struggling to test this, and my branch coverage needs to 100%.
Easy to test if I make it a function, but I want to avoid using parenthesis everywhere inside props. Also easy to test if I move it above the render, but then the whole point is missed (to stop render from breaking in case data fails to fetch from API)
const data = this.getData()
if (!data) return null
return (
I only need to cover the if !(data) return null
(getData() method already has an early null return)
The way I'd approach it is to test the render result of the component. For example, in your case if data is null, does it render anything? The you can check in tests if component (or its part) is properly rendered.
You can use react-testing-library and test HTML output. This is an awesome library that allows you to unit/integration test your components like 'user would interact with them'.
https://testing-library.com/docs/react-testing-library/intro
Thanks for replying so quickly! I found a solution that properly tests and passes our SonarQube rules:
it('should return null if there is no data', () => {
const wrapper = shallow(<MyComponent />)
const instance = wrapper.instance()
instance.getData = jest.fn()
const renderedWrapper = instance.render()
expect(renderedWrapper).toBeNull()
})

Using this.props.history.push("/path") is re-rendering and then returning

Edited the question after further debugging
I am having a strange issue, tried for a while to figure it out but I can't.
I have a React Component called NewGoal.jsx, after a user submits their new goal I attempt to reroute them to my "goals" page.
The problem: After they submit the browser loads in my goal page, but only for one second. It then continues and goes BACK to the NewGoal page!!
I am trying to understand why this is happening, I am beginning to feel that this might be an async issue.
Here is my code, currently it is using async-await, I also tried the same idea using a .then() but it also didn't work:
async handleSubmit(event)
{
const response = await axios.post("http://localhost:8080/addGoal",
{
goalID: null,
duration: this.state.days,
accomplishedDays: 0,
isPublic: this.state.isPublic,
description: this.state.name,
dateCreated: new Date().toISOString().substring(0,10),
}) */
// push to route
this.props.history.push("/goals");
}
While debugging, I tried taking out the functionality where I post the new message, and just did a history.push, code is below - and this completely worked.
// THIS WORKS
async handleSubmit(event)
{
// push to route
this.props.history.push("/goals");
}
But as soon as I add anything else to the function, whether before the history.push or after, it stops working.
Any advice would be very very appreciated!
Thank you
In the React Router doc's the developers talk about how the history object is mutable. Their recommendation is not to alter it directly.
https://reacttraining.com/react-router/web/api/history#history-history-is-mutable
Fortunately there are few ways to programmatically change the User's location while still working within the lifecycle events of React.
The easiest I've found is also the newest. React Router uses the React Context API to make the history object used by the router available to it's descendents. This will save you passing the history object down your component tree through props.
The only thing you need to do is make sure your AddNewGoalPage uses the history object from context instead of props.
handleSubmit(event)
...
//successful, redirect to all goals
if(res.data)
{
this.context.history.push("/goals")
}
...
})
}
I don't know if you're using a class component or a functional component for the AddNewGoalPage - but your handleSubmit method hints that it's a member of a Class, so the router's history object will be automatically available to you within your class through this.context.history.
If you are using a functional component, you'll need to make sure that the handleSubmit method is properly bound to the functional component otherwise the context the functional component parameter is given by React won't not be available to it.
Feel free to reply to me if this is the case.

Is it wrong to call component methods passed as arguments inside of redux actions?

I'm struggling with one task I've been appointed to and the only workaround I found is to call the action argument callback inside the action. Is this a bad idea from a design point of view, because the code itself works and passes numerous tests? The general purpose of this solution is to somehow trigger the component function when a certain logic is being followed.
export function myAction(componentClb: () => void): any {
return (dispatch: Dispatch<AppStore>): void => {
someRESTAPIcall()
.then((condition) => {
condition
? dispatch(anotherActionThatTakesCallbackAsArgument(componentClb))
: componentClb();
})
.catch((error: Error) => {
dispatch(myErrorAction());
});
};
}
The biggest mistake about this is to take React components as functional classes. Components really are just to handle the WHAT and HOW of page rendering. Anything outside of rendering logics should be removed from it.
Instead of working as callback, this anotherActionThatTakesCallbackAsArgument should update the redux store.
The container that componentClb belongs to should be connected to redux store and selects the fields from the stores, and pass these fields to componentClb as props, so that componentClb can handle the rendering based on the response of myAction
With the info you provided, it's hard to give a concrete code example. Maybe describe the scenario you are trying to solve and people are able to give you more direct feedbacks.

RX + React setState - avoid “Cannot update during an existing state transition”?

Problem
public componentDidMount() {
// after component mounts I want to get updates from RX source
const that = this;
SomeExternalObservable.subscribe((value) => {
that.setState((prev, props) => ({
currentValue: value
});
});
}
In some cases, I get warning Cannot update during an existing state transition” React with Observable.
I checked SO for answers to that problem, but most of them just suggested to move my code to componentDidMount.
What happens here
it's not obvious, but subscription on observable can be executed (it's very likely to) synchronously (see here https://github.com/tc39/proposal-observable/issues/38)
it's even more likely if an observable has already set value (like BehaviourSubject - see tip here BehaviorSubject vs Observable?)
The problem occurred as well when using libraries like https://github.com/jayphelps/react-observable-subscribe.
An alternative would be to use props instead of setState.
For managing React state with Rx I prefer to use the mapPropsStream recompose helper. It can be a bit confusing if you don't tend to use HOCs, but it is actually pretty simple.
const withSomeExternalValue = mapPropsStream(props$ =>
Observable.combineLatest(
props$,
someExternal$.startWith(null),
(props, someExternalValue) => ({...props, someExternalValue})
)
);
const MyEnhancedComponent = withSomeExternalValue(MyComponent);
MyEnhancedComponent is a new Component type. When it will mount, it will also subscribe to the external Observable. Any emitted value will render the component with the new value as prop.
Additional explanation and notes:
The props$ is an Observable of props. Any change of a prop will result in a new item emitted for prop$
The returned Observable is a new props Observable that its emitted items will be used to render the enhanced component.
The .startWith(null), is to make sure the enhanced component will be rendered before the first value of someExternal$ is emitted. In this case, the someExternalValue prop will start with null
withSomeExternalValue HOC can be reused if these external values are needed by another component. withSomeExternalValue(MyAnotherComponent)
The mapPropsStream can be used with other Observable implementation - so it should be configured. Here is how to use it with Rx -
Can be configured globally:
import rxjsconfig from 'recompose/rxjsObservableConfig'
setObservableConfig(rxjsconfig)
Or by using mapPropsStreamWithConfig:
import rxjsConfig from 'recompose/rxjsObservableConfig'
const mapPropsStream = mapPropsStreamWithConfig(rxjsConfig)
I post the question with the answer as it seems allowed, supported
here
https://meta.stackexchange.com/questions/17463/can-i-answer-my-own-questions-even-if-i-knew-the-answer-before-asking;
spent some time to find the issue and couldn't find similar SO post,
so I hope it will help someone.
Solution
Because we can't expect the subscription to be triggered asynchronously working solution is to force it, i.e. by using Scheduler:
public componentDidMount() {
// after component mounts I want to get updates from RX source
const that = this;
SomeExternalObservable
.subscribeOn(Scheduler.asap) // here
.subscribe((value) => {
that.setState((prev, props) => ({
currentValue: value
});
});
}
Why
componentDidMount happens directly after rendering and change to state will cause additional render - which can happen in the same tick (https://reactjs.org/docs/react-component.html#componentdidmount)
setState should work asynchronously, but doesn't have to; React will decide about it at runtime (https://reactjs.org/docs/react-component.html#setstate)
Subscription on Observable by default is synchronous.
Wrap up question
Is there React point of view right way to fix that issue, different than solution presented?

Resources