How to avoid act() with sub component changes? - reactjs

I'm using the Select component from patternfly and attempting to do a test that involves making changes to the selection. I feel like I'm in a catch 22 as I can make the tests pass without the use act() messages if I do this:
await waitFor(() => {
userEvent.click(screen.getByLabelText(labelText));
});
userEvent.click(screen.getAllByRole('option')[optionNumber]);
however, I then get a lint error saying Avoid using side effects within 'waitFor' callback
If I remove the waitFor(), and do
userEvent.click(screen.getByLabelText(labelText));
userEvent.click(screen.getAllByRole('option')[optionNumber]);
My tests give me the console errors saying Warning: An update to Popper inside a test was not wrapped in act(...).
One point worth noting, I'm only having this issue if I'm appending what ends up being the "Popper" menu (where the options reside) to document.body (or anywhere other than "inline") using the Select component's menuAppendTo prop. I have to do this to prevent some clipping issues with the menu being in a modal.
For now I'm just ignoring the lint issue:
await waitFor(() => {
// eslint-disable-next-line testing-library/no-wait-for-side-effects
userEvent.click(screen.getByLabelText(labelText));
});
userEvent.click(screen.getAllByRole('option')[optionNumber]);
How can I satisfy both the test and the lint?

I was able to fix this issue by putting an await on a findBy for the options prior to clicking them, like so:
userEvent.click(screen.getByLabelText(labelText));
const options = await screen.findAllByRole('option');
userEvent.click(options[optionNumber]);

Related

RTK Query Mutation in StrictMode

I use RTK Query for data fetching and I am having a small issue in one of the use cases.
I use a mutation to verify the email address of the app user, as follows:
// ...imports
const ConfirmEmail = () => {
const [params] = useSearchParams();
const [confirmEmail, { isLoading, isSuccess, isUninitialized, error }] = useConfirmEmailMutation();
useEffect(() => {
if (isUninitialized) {
confirmEmail({
email: params.get('email'), // from the verification link
code: params.get('code'), // from the verification link
})
.unwrap()
.then(() => {
// handling success...
})
.catch((e) => {
// handling error...
});
}
});
// ...other component code
}
The problem is that with StrictMode the mutation is running twice in development and it is causing a concurrency error at the API side. I see two network requests in the dev tools one is successful and the other is not and depending on which one runs first the component is showing inconsistent result.
I am aware that this will only happen during development and I tried to follow the instructions in the official react documentation and I tried to use fixedCacheKey as described here but I wasn't able to make this work with RTK Query so that I get a consistent result during development.
Is there a way, or am I missing something?
That's pretty much the point of this strictness check though: to show you that you are automatically doing a problematic api call.
In the future, React can choose to execute a useEffect with an empty dependency array on more occasions than just on first mount - for example in combination with the offscreen features they are working on.
This just tells you in advance that you are doing this in probably a dangerous place.
Can you maybe incorporate this api call in some kind of actual user interaction like a button click? That would be a lot safer going forward.

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

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

Errors when using React Testing Library and renderHook to test hooks with multiple contexts

Solved
Issue is tracked on github
I was attempting to test custom hooks using the react testing library, and the hook I was trying to put under test requires multiple context providers to work correctly. (authentication and notification)
The documentation here only outlines creating a wrapper with a single provider like so:
const wrapper = ({ children }) => <ContextProvider>{children}</ContextProvider>
However, my implementation needed something more complex, like so:
const wrapper = ({ children }) => (
<ToastProvider>
<NotificationProvider>
<AuthProvider>{children}</AuthProvider>
</NotificationProvider>
</ToastProvider>
);
This was was failing at every attempt with the errors:
TypeError: parentInstance.children.indexOf is not a function
OR
Invariant Violation: Drop(...): Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null.
Leading me to believe there was no clear way to provide the right context without abandoning renderHook all together and building a test component that could fire off the necessary behaviors manually.
After a lot more digging I found this error buried in the logs:
Warning: An invalid container has been provided. This may indicate that another renderer is being used in addition to the test renderer. (For example, ReactDOM.createPortal inside of a ReactTestRenderer tree.) This is not supported.
Surely enough, it turns out there is a conflict with react-test-renderer and react-dom which causes calls to ReactDOM.createPortal to fail when under test. I assume this is somewhere in the ToastProvider, but the workaround is pretty simple.
Solved by adding this to the top of my test:
ReactDOM.createPortal = node => node

Exception handling in Jest/Enzyme

I am testing my React components using Jest/Enzyme.
In some of the test cases, the test fails with some exception.
What is a sane/standard way to handle error in Jest/Enzyme instead of breaking the test case?
e.g - the following case should pass if the 'Grid' component does not get any data as parameter.However, it fails with an exception which is being thrown out of the Grid component.
test('Grid does not render without data',()=>{
const wrapper=shallow(<Grid/>);
expect(wrapper.length.toBe(0));
})
You should not handle error in your test cases. Instead you should expect the code to throw errors.
You code should look something like this
test('Grid should throw when data is not passed',()=>{
expect(() => shallow(<Grid/>)).toThrow();
})
But ideally, when the right prop is not there, the component should not throw, instead, it should not render.

Binding to event handler that calls setState in ComponentDidMount produces warning

I'm using jQuery to create event bindings in a ReactJS component's componentDidMount function, which seems like the right place to do this.
$('body').on('defaultSearchContext.registerQueryEditor', (function(_this) {
return function(event, component) {
_this.setState({
queryEditors: _this.state.queryEditors.concat([component])
});
};
})(this));
This code isn't actually run on componentDidMount, it's simply setting up the binding that later calls setState when the event fires. However, this generates the following warning every time this event triggers, which pollutes my console with dozens of warnings:
Warning: setState(...): Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state.
I have tried moving the setState code to a separate function like onEvent and calling that from the binding in componentDidMount but the warning is still produced.
Ideally, I'd like to create the binding in the proper place, indeed, there is some issue with doing it in componentDidMount. If not, I'd like to know if it's possible to silence the warning, or whether I should perhaps file a bug for ReactJS itself. If it helps, I'm using ReactJS 0.14.3 (latest at this time).
This is similar to, but not the same as React Js onClick inside render. In that case, the solution is to return an anonymous function to onClick, but that doesn't seem applicable to my situation.
You are trying to coordinate events between independent components. This is a fairly standard thing to do, and it doesn't require DOM events. The standard practice for doing this in React is to use a store/dispatcher pattern like Redux or Flux (I personally prefer redux). However, if this is part of a larger, not-completely-React application, then this may not be possible. If it is just for a small piece of an React app, it may still be overkill.
All you need is an object to coordinate events. An event is just a collection of callbacks, possibly typed or keyed. This requires nothing more than an object shared between two places. DOM Events are overkill; jQuery is overkill. You just need to trigger a callback.
This is a VERY SIMPLE event coordinator.
let simpleEventCoordinator = {
callbacks: new Map(),
getHandler(eventKey) {
let handler = this.callbacks.get(eventKey);
if (!handler) {
handler = new Set();
this.callbacks.set(eventKey, handler);
}
return handler;
},
registerCallback(eventKey, callback) {
this.getHandler(eventKey).add(callback);
},
removeCallback(eventKey, callback) {
this.getHandler(eventKey).delete(callback);
},
trigger(eventKey, data) {
this.getHandler(eventKey).forEach(c => c(data));
}
Keep a map of callbacks, which will be nameOfEvent => callback(). Call them when asked. Pretty straightforward.
I know nothing about how your components are structured, but you said they are independent. Let's say they look like this:
React.render((
<div>
<QueryManager />
<button onClick={() => simpleEvent.trigger('event')}>{'Update'}</button>
</div>
), document.body);
This is all your component needs to handle this event
componentDidMount() {
simpleEvent.registerCallback('event', this.update);
}
componentWillUnmount() {
simpleEvent.removeCallback('event', this.update);
}
update() {
//do some stuff
}
I've put together a very simple codepen demonstrating this.
Looking at the source code of where that warning is coming from, it appears that if some reference is maintained before an update is about to happen, it throws that warning. So maybe the way your mixing the jQuery events and react is creating a memory leak? Its hard to say exactly because of the lack of surrounding code to your snippet what else could be going on.

Resources