Why is my jest "onClick" test not firing the mocked function? - reactjs

I am using jest to test whether a button is clickable or not. The button is a Material-UI component, but is itself working exactly as intended on the frontend; only the test is failing.
The code for the button:
import { Button as BaseButton } from '#mui/material';
export function Button({ children, isDisabled = false, onClick }) {
return (
<BaseButton disabled={isDisabled} onClick={onClick}>
{children}
</BaseButton>
);
}
And the code for the test:
import { Button } from './Button';
import { render, screen } from '#testing-library/react';
import '#testing-library/jest-dom/extend-expect';
import userEvent from '#testing-library/user-event';
it('should be clickable', () => {
const text = 'Hello World!';
const testFunction = jest.fn();
render(<Button onClick={() => testFunction}>{text}</Button>);
userEvent.click(screen.getByText('Hello World!'));
expect(testFunction).toHaveBeenCalledTimes(1);
});
I have other tests in the same file which are passing, so I don't think it's some type of import issue. The button is definitely rendering properly on the test page, but can't be clicked. I have also tried updating the button locator to userEvent.click(screen.getByRole('button')); to no avail.
I am getting the following error from the test file, which I believe is caused by the click test:
(node:30870) UnhandledPromiseRejectionWarning: Error: Unable to perform pointer interaction as the element has or inherits pointer-events set to "none".
However, the button component does not even have a pointer-events property. The child span element, which forms the button label, does have pointer-events: none, but the parent element shouldn't be inheriting that from a child. In addition, I tested the jest.fn() on a normal, html element and got the exact same error.

You're not actually calling the function, just returning it from the onClick callback (which doesn't do anything)
onClick={() => testFunction}
Is the same as:
onClick={() => {
return testFunction;
}}
That is just returning testFunction. To call it you want to:
onClick={() => testFunction()}
Or, if you want to pass it directly so it get's called you can do:
onClick={testFunction}
Then, when the click fires it will call the passed function. But then it will receive the event arguments etc, which you might not want.

Related

Testing if element is removed when a prop changes is not working in React Testing Library

I'm trying to test if a Material UI component is removed when a button is clicked and can't seem to get the test to provide a reliable answer. Here is the (simplified) React component I'm testing:
import Snackbar from '#mui/material/Snackbar';
import MuiAlert from '#mui/material/Alert';
import IconButton from '#mui/material/IconButton';
import CloseIcon from '#mui/icons-material/Close';
const ResultMessage = (props) => {
const handleClose = () => {
props.onClose();
};
return (
<Snackbar
open={props.open}
onClose={handleClose}
action={
<IconButton role='button'>
<CloseIcon />
</IconButton>
}>
<MuiAlert onClose={handleClose}>
{props.message}
</MuiAlert>
</Snackbar>
);
}
export default ResultMessage;
And here is the contents of my test file:
import { render, screen, fireEvent } from "#testing-library/react";
import { unmountComponentAtNode } from "react-dom";
import userEvent from "#testing-library/user-event";
import ResultMessage from "../ResultMessage";
let container = null;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
unmountComponentAtNode(container);
container.remove();
container = null;
});
describe('Tests ResultMessage Component', () => {
it('should confirm message is removed when close button is clicked', () => {
let show = true;
const handleClose = () => { show = false }
const {rerender} = render(<ResultMessage
open={show}
onClose={handleClose} />, container);
expect(screen.getByRole('presentation')).toBeInTheDocument();
expect(screen.getByRole('button')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button'));
// userEvent.click(screen.getByRole('button'));
rerender(<ResultMessage
open={show}
onClose={handleClose} />, container);
setTimeout(() => {
expect(screen.queryByRole('presentation')).toBeNull();
expect(screen.getByRole('presentation')).toBeInTheDocument();
}, 0);
});
});
I'm attempting to use a variable to control showing and hiding of the Snackbar component. That component has an open property that either shows or hides the component if the value is true or false. In my actual app, I use useState to control this functionality but as far as I can tell, I can't use React Hooks in my tests. Perhaps this is related to the problem.
The idea here is that I want to initially render the component and display it by setting the 'show' variable to true. I confirm that the snackbar with a role of 'presentation' and the button are present. All good so far.
I then trigger a button click (either with fireEvent or userEvent) that changes the value of the 'show' variable to false. I then attempt to remove the snackbar by "rerender"-ing the same component as stated in the RTL documentation, but this time with the hope that the 'show' variable will be false and therefore the component should be removed. (that is how it works in React at any rate).
Where it gets weird is the expectations at the bottom of the code - both should be the opposite of one another - somehow they both succeed when I run the test. Not sure how I could test for the component to be null and in the document at the same time and it succeed. I've inserted that logic into setTimeouts thinking that perhaps the problem might be related to the state change not being performed immediately. If I remove the setTimeouts the null check fails implying it's still there. I've also tried 'waitFor' with no success.
My current thought is maybe a new component is being generated instead of updating the state of the initially created component and therefore the test has two components to test and returning that odd result I mentioned. If so, I cannot figure out how to generate only one component and update that props state. Thank you for any help you can provide!

How to test button that on click renders html element within the same component in React testing library, Jest?

My OrderModuleHeader is a clickable div that renders div when clicked like shown below:
const [orderModuleIsOpen, setOrderModuleIsOpen] = useState(true);
render (
<div>
<OrderModuleHeader data-testid="orderModuleHeaderButton"
onClick={() => setOrderModuleIsOpen(!orderModuleIsOpen)}
>
<NavigationArrow>
<FontAwesomeIcon
icon={orderModuleIsOpen ? faChevronUp : faChevronDown}
className="fa-xs"
/>
</NavigationArrow>
</OrderModuleHeader>
{orderModuleIsOpen && (
<OrderModuleBody>Some content</OrderModuleBody>
)
</div>)
So div OrderModuleBody is displayed only when we click the button.
I started my test by the following:
describe('Expand body button', () => {
it('should render OrderModuleBody when clicked', () => {
const button = screen.getByTestId('orderModuleHeaderButton');
fireEvent.change() ...
});
}
I realize that I made div clickable and in test I call it a button.
I don't know what exactly I need to write in fireEvent to test if OrderModuleBody (div) renders on click.
NOTE: I'm not allowed to bring in shallow method from Enzyme or use any other testing frameworks except Jest, React testing library.
Any advice is appreciated.
You'll need to render your component in order to test it. You then access the button after which you simulate a button click, and then eventually expect your change to occur.
The testing library has a function named userEvent which can simulate a button click.
import { screen } from "#testing-library/react";
import userEvent from "#testing-library/user-event";
describe('Expand body button', async () => {
it('should render OrderModuleBody when clicked', () => {
// render your component
render(<YourComponent />)
// access your button
const button = screen.getByTestId('orderModuleHeaderButton')
// simulate button click
userEvent.click(button);
// expect result
await waitFor(() =>
expect(screen.getByText("Some content")).toBeInTheDocument();
});
}

Set react native component prop through passed reference

I need to show/hide a modal based on user interaction (ie - a button press) in a component which is neither a parent or child of the modal itself. I'm currently trying to do so by passing the modal as a prop to the modal controller, but the following errors are thrown depending on which method I call:
TypeError: modal.setNativeProps is not a function
TypeError: modal.setState is not a function
Is there a way to show the modal given how this is structured?
import Modal from 'react-native-modal'
const modalRef = React.createRef();
const modal = <Modal ref={modalRef} isVisible={false}>
<ModalController modal={modalRef} />
export const ModalController = ({modal}) => {
function onButtonPress(){
modal.setState({isVisible:true})
modal.setNativeProps({isVisible:true})
}
return (
<Button title='Show Modal' onPress={onButtonPress()} />
)
}
Ciao, in case there is no parent/child relation between components, the only way I found to pass/set data from one component to another is use a global state manager like react-redux. With redux you can easly define a global state and set it from component that fire modal open/close. Modal component reads this state and open/close itself.
So I think you've gotten a little confused.
Firstly, remember that everything inside ModalController is going to execute on every render. Your function onButtonPress will be created every render (this is fine), but you are actually calling that function when you pass it to onPress render . This means you're executing onButtonPress on every render, which is probably not what you want.
This is an easy fix - you just remove the () so it's just onPress={onButtonPress}. Now it'll only trigger when the button is pressed.
More fundamentally, the solution to your problem is much simpler than what you've done in your code. Generally 'refs' are only used in special cases where you really want to tell your components what to do (like telling a ScrollView to scroll to a particular position, or telling an input to focus so the keyboard shows). If you're using a ref it should be very intentional.
So a simple solution to have a component with a button that shows a modal could look like:
import React, {useState} from 'react';
import {View, Button, Text} from 'react-native';
import Modal from 'react-native-modal';
export const ModalController = () => {
const [isModalVisible, setIsModalVisible] = useState(false);
function onButtonPress() {
setIsModalVisible(true);
}
return (
<View>
<Button title='Show Modal' onPress={onButtonPress} />
{isModalVisible && <MyModal />}
</View>
);
};
const MyModal = () => (
<Modal>
<Text>Hey I am a modal</Text>
</Modal>
);
Notice the use of useState. This is how you hold 'state' in a functional component like this (as opposed to a class component where you would use setState.
I hope this helps. Let me know if you have questions!

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!');

How to use enzyme ShallowWrapper to find a React Component stored as a prop in another React Component?

I have a jest/enzyme test which creates a ShallowWrapper around a component, finds a specified semantic-ui-react Button (by id), simulates a click on the button, and then looks to see if the click toggled certain content.
Sample JSX:
<Popup
trigger={<Button onClick={this.toggleShowThing} id="special-btn">a button</Button>}
content="Popup Words"
/>
{this.state.showThing &&
<div className="special-thing">The Thing's Words</div>
}
Sample Test:
it('shows the thing when the button is clicked', () => {
const wrapper = shallow(<MyComponent />);
wrapper.find('#special-btn').simulate('click', { preventDefault() {} });
expect(wrapper.find('.special-thing').exists()).toBe(true);
});
This test worked when I just had the Button. When I added the Popup and the Button was placed into the trigger prop then I received an error because #special-btn could not be found.
Error: Method “props” is only meant to be run on a single node. 0 found instead.
An enzyme snapshot of the component shows that the Popup looks like this:
<Popup
content="Popup Words"
on="hover"
position="top left"
trigger={
<Button
id="special-btn"
onClick={[Function]}
>
a button
</Button>
}
/>
I need my test to work again. How do I gain access to the #special-btn again in the test so that I can call .simulate('click') on it?
This is what worked for me, although there is no documentation for it:
import {shallow, ShallowWrapper} from "enzyme";
it('shows the thing when the button is clicked', () => {
const wrapper = shallow(<MyComponent />);
const button = new ShallowWrapper(
wrapper.find('Popup').prop('trigger'), wrapper
);
button.simulate('click', { preventDefault() {} });
expect(wrapper.find('.special-thing').exists()).toBe(true);
});
In other words:
Find the Popup component.
Get the component rendered in its trigger prop. Note that this is not yet a shallow wrapper, so no fancy APIs yet.
Manually create the wrapper using ShallowWrapper (it's important to pass the second argument).
Now you can access all the enzyme APIs to interact with the button.
Note, it seems that you can avoid using the constructor and use wrap() utility method instead (also not documented):
const button = wrapper.wrap(wrapper.find('Popup').prop('trigger'));
Assuming Popup is some third-party component that has already been tested, I would approach testing the following way:
(1) Find the Popup and check if the trigger prop's Button's onClick prop is componentWrapper.instance().toggleShowThing
(2) As a separate thing, set this.state.showThing to false and verify no div with className special-thing is rendered; set this.state.showThing to true and verify it is rendered.
(*) this.toggleShowThing should also be tested on its own.
The problem is that you cannot do it. You need to rewrite your test. Your button is now wrapped by a Popup component thus you don't have access to it. But you can move your selector to the Popup and test if clicking the popup triggers required change. There's no other way around.
// JSX
<Popup
trigger={<Button onClick={this.toggleShowThing} id="special-btn">a button</Button>}
content="Popup Words"
id="popup"
/>
{this.state.showThing &&
<div className="special-thing">The Thing's Words</div>
}
// test
it('shows the thing when the button is clicked', () => {
const wrapper = shallow(<MyComponent />);
wrapper.find('#popup').simulate('click', { preventDefault() {} });
expect(wrapper.find('.special-thing').exists()).toBe(true);
});

Resources