Testing click event in React Testing Library - reactjs

Here is a simple subcomponent that reveals an answer to a question when the button is clicked:
const Question = ({ question, answer }) => {
const [showAnswer, setShowAnswer] = useState(false)
return (
<>
<article>
<header>
<h2 data-testid="question">{question}</h2>
<button onClick={() => setShowAnswer(!showAnswer)}>
{
!showAnswer ? <FiPlusCircle /> : <FiMinusCircle />
}
</button>
</header>
{
showAnswer && <p data-testid="answer">{answer}</p>
}
</article>
</>
)
}
export default Question;
I am trying to test that when the button is clicked, the onClick attached is called once and the a <p> element appears on the screen:
const onClick = jest.fn()
test('clicking the button toggles an answer on/off', () => {
render(<Question />);
const button = screen.getByRole('button')
fireEvent.click(button)
expect(onClick).toHaveBeenCalledTimes(1);
expect(screen.getByTestId('answer')).toBeInTheDocument()
fireEvent.click(button)
expect(screen.getByTestId('answer')).not.toBeInTheDocument()
screen.debug()
})
RTL says that onClick is not called at all (in the UI it is, as the result is as expected)
Also, if I want to test that this button really toggles the answer element (message should come on and off) how would I test for that?
If I add another fireEvent.click() to the test (simulating the second click on the button which should trigger the answer element off), and add
expect(screen.getByTestId('answer')).not.toBeInTheDocument()
RTL will just not find that element (which is good, I guess, it means it has been really toggled off the DOM). What assertion would you use for this test to pass for that case?

Couple of issues with your approach.
First, creating an onClick mock like that won't mock your button's onClick callback. The callback is internal to the component and you don't have access to it from the test. What you could do instead is test the result of triggering the onClick event, which in this case means verifying that <FiMinusCircle /> is rendered instead of <FiPlusCircle />.
Second, p is not a valid role - RTL tells you which roles are available in the DOM if it fails to find the one you searched for. The paragraph element doesn't have an inherent accessibility role, so you're better off accessing it by its content with getByText instead.
Here's an updated version of the test:
test('clicking the button toggles an answer on/off', () => {
render(<Question question="Is RTL great?" answer="Yes, it is." />);
const button = screen.getByRole('button')
fireEvent.click(button)
// Here you'd want to test if `<FiMinusCircle />` is rendered.
expect(/* something from FiMinusCircle */).toBeInTheDocument()
expect(screen.getByText('Yes, it is.')).toBeInTheDocument()
fireEvent.click(button)
// Here you'd want to test if `<FiPlusCircle />` is rendered.
expect(/* something from FiPlusCircle */).toBeInTheDocument();
expect(screen.queryByText('Yes, it is.')).not.toBeInTheDocument()
})

In my case this worked:
it('Does click event', () => {
const { container } = render(<Component />);
fireEvent.click(container.querySelector('.your-btn-classname'));
// click evt was triggered
});

Related

How to handle onClick event to display text after the click

Using react typescript and I’m confused that when I click a button I want some text to appear below the button or at-least anywhere so I made a function to handle the onClick from the button and returned a h1 from the function but turns out no h1 appears on screen after button click. Any idea why?
const handleOnClick=(id:any)=>{
console.log("button clicked" + id)
return(
<h1>Clicked it</h1>
);
}
My Function is this and in another function I have
<button onClick={()=>{handleOnClick(someId)}}>a</button>
I can see the console log but the h1 doesn’t work. Any ideas?
If you think about it, what your handleOnClick doing is returning a bunch of jsx, where do you think these jsx will appear since we didn't specify any location for them? Now if you try something like this:
<button>{ handleOnClick('someId') }</button>
You will see the h1 on the screen because you specify that's where you want to render it, right inside the button element.
A classic way in js to render out something on button click is like this:
const handleOnClick=(e)=>{
// create the element
const newEle = document.createElement('h1');
newEle.innerText = 'Hello';
// append it inside the button
e.target.appendChild(newEle);
}
export default function App() {
const [nameId, setNameId] = useState<String>("");
const handleClick = (id: String) => () => {
console.log("button clicked: ", id);
setNameId(nameId ? "" : id);
};
return (
<>
<button onClick={handleClick("Rohan")}>
{nameId ? "Hide" : "Greet"}
</button>
{!!nameId && <h1>Hello {nameId} Haldiya</h1>}
</>
);
}
When the click is triggered, you need to add the <h1> element into your JSX code, and returning it from the click handler is not enough because you need to tell it where is should be added.
A good way of doing that in React is by using a state which tells you if the button was clicked or not, and if it was, then you display the <h1> element onto the screen. See the code below:
const [isActive, setIsActive] = useState(false);
const handleOnClick = (id) => {
console.log("button clicked" + id);
setIsActive(true);
};
and in your JSX code, below the button, you just need to add the second line of the following:
<button onClick={()=>{handleOnClick(someId)}}>a</button>
{isActive && <h1>Button was clicked.</h1>}
And if you want to toggle the click, So the first time you click the <h1> is showing , but if you click again it disappears, then you could simply do this in the handleOnClick function instead of the above:
const handleOnClick = (id) => {
console.log("button clicked" + id);
setIsActive((prevState) => (prevState === false ? true : false));
};
Hope this helps!

Not able to click list child element from side menu using react testing library

It's my first week using the react library and I'm a bit confused with this piece where I have a list of items that are part of a side menu. I can assert their texts identifying them as the child of the parent List tag. But when I try to fire a click event this way it won't navigate me to the next page. It will only work if I target that list item by their text directly, not as the child of the parent list, which I'm trying to avoid because there may be other clickable elements with that same title on the page. If anyone could please point me towards what I may be lacking in understanding here please.
describe('App when it is rendered in a certain state:', () => {
beforeAll(() => {
render(<Root/>);
});
it('should render the dashboard after logging in', async () => {
expect(screen.queryByTestId('dashboard')).toBeInTheDocument();
expect(screen.getByTestId('side-menu')).toBeInTheDocument();
const liArr = screen.queryAllByRole('listitem')
expect(liArr[0].textContent).toBe('Dashboard')
expect(liArr[1].textContent).toBe('Schools')
expect(liArr[2].textContent).toBe('Teachers')
const teachersLink = liArr[2];
expect(teachersLink).toBeInTheDocument();
// await fireEvent.click(screen.getByText('Teachers')) // This navigates me fine
await fireEvent.click(teachersLink); // This does not navigate me
expect(screen.getByText('Teachers page')).toBeInTheDocument();
});
});
This is the SideMenu component where the links come from:
export default function SideMenu() {
const classes = useStyles();
return (
<div className={classes.root} data-testid="side-menu">
<List>
<ListItemLink to="/dashboard" primary="Dashboard"/>
<ListItemLink to="/schools" primary="Schools"/>
<ListItemLink to="/teachers" primary="Teachers"/>
</List>
</div>
);
}
Thanks very much.
It seems it was not working because the link item was actually not clickable, not being an anchor element. Changed it to the below piece and it worked, warranting uniqueness through the role of a button.
async function navigateToTeachersPage() {
await fireEvent.click(screen.getByRole('button', { name: 'Teachers' }));
expect(screen.getByTestId('teachers')).toBeInTheDocument();
}

How to test for tooltip title in jest and testing/library

I want to test that the tooltip title is equal to a specific text or not.
This is my antd tooltip I want to write a test for that:
<Tooltip
title={
this.props.connection ? "Connected" : "Disconnected (Try again)"
}>
<Badge status="default" data-testid="connection-sign" />
</Tooltip>
and this is my test in jest:
test("Show error title in tooltip", async () => {
baseDom = render(cardComponent);
fireEvent.mouseMove(await baseDom.findByTestId("connection-sign")); //To hover element and show tooltip
expect(
baseDom.getByTitle(
"Disconnected (Try again)"
)
).toBeInTheDocument();
});
but this test failed and unable to find an element with this title. How can I test that my tooltip contain "Disconnected (Try again)"?
There are multiple mistakes in your test.
Passing component type instead of component instance to render
// this is wrong, passing component type
baseDom = render(cardComponent);
// this is right, passing component instance created with JSX
baseDom = render(<cardComponent />);
Using mouseMove instead of mouseOver event
Searching element by title and passing text instead of searching by text
// wrong, because, the prop on the Tooltip is called 'title'
// but it is actually text as 'getByTitle' looks for HTML
// title attribute
baseDom.getByTitle("Disconnected (Try again)");
// right, because you are actually looking for text not
// HTML title attribute (but wrong see (4))
baseDom.getByText("Disconnected (Try again)");
Using sync method for Tooltip search instead of async
// wrong, because, Tooltip is added dynamically to the DOM
baseDom.getByText("Disconnected (Try again)");
// right
await baseDom.findByText("Disconnected (Try again)");
To sum up, with all mistakes fixed the test should look like this:
import React from "react";
import { render, fireEvent } from "#testing-library/react";
import App from "./App";
test("Show error title in tooltip", async () => {
const baseDom = render(<cardComponent />);
fireEvent.mouseOver(baseDom.getByTestId("connection-sign"));
expect(
await baseDom.findByText("Disconnected (Try again)")
).toBeInTheDocument();
});
In addition to the accepted answer, it's important to make sure if you set the prop getPopupContainer for an Antd tooltip, the popup might not be visible to react testing library as it happened in my case since the DOM container set may not be available in the body when testing the component especially if it's a unit test. e.g
In my case I had
<Popover
getPopupContainer={() => document.getElementById('post--component-drawer')}
content={<h1>Hello world</h1>}>
<span data-testid="symbol-input--color-input">Click me</span>
</Popover>
div post--component-drawer was not available for that unit test. So I had to mock Popover to make sure I override prop getPopupContainer to null so that the popup would be visible
So at the beginning of my test file, I mocked Popover
jest.mock('antd', () => {
const antd = jest.requireActual('antd');
/** We need to mock Popover in order to override getPopupContainer to null. getPopContainer
* sets the DOM container and if this prop is set, the popup div may not be available in the body
*/
const Popover = (props) => {
return <antd.Popover {...props} getPopupContainer={null} />;
};
return {
__esModule: true,
...antd,
Popover,
};
});
test('popver works', async () => {
render(<MyComponent/>);
fireEvent.mouseOver(screen.getByTestId('symbol-input--color-input'));
await waitFor(() => {
expect(screen.getByRole('heading', {level: 1})).toBeInTheDocument();
});
});
I was tried many ways but didn't work, therefore I tried
mouse enter instead of mouseOver or mouseMove and it's worked for me.
here is a solution to test tooltip content, like as:
import { render, cleanup, waitFor, fireEvent, screen } from '#testing-library/react';
// Timeline component should render correctly with tool-tip
test('renders TimeLine component with mouse over(tool-tip)', async () => {
const { getByTestId, getByText, getByRole } = render(
<TimeLine
items={timeLineItems()}
currentItem={currentTimeLineItem()}
onTimeLineItemChange={() => {}}
/>
);
const courseTitle = "Course collection-3";
fireEvent.mouseEnter(getByRole('button'));
await waitFor(() => getByText(courseTitle));
expect(screen.getByText(courseTitle)).toBeInTheDocument();
});
I found this to be the most up to date way. You've got to do async() on the test and then await a findByRole since it isn't instantaneous!
render(<LogoBar />);
fireEvent.mouseEnter(screen.getByLabelText('DaLabel'));
await screen.findByRole(/tooltip/);
expect(screen.getByRole(/tooltip/)).toBeInTheDocument();
This is a slight modification to the Akshay Pal's solution:
import { render, cleanup, waitFor, fireEvent, screen } from '#testing-library/react';
// Timeline component should render correctly with tool-tip
test('renders TimeLine component with mouse over(tool-tip)', async () => {
render(
<TimeLine
items={timeLineItems()}
currentItem={currentTimeLineItem()}
onTimeLineItemChange={() => {}}
/>
);
const courseTitle = "Course collection-3";
fireEvent.mouseEnter(screen.getByRole('button'));
expect(await screen.getByText(courseTitle)).toBeTruthy();
});

Check that button is disabled in react-testing-library

I have a React component that generates a button whose content contains a <span> element like this one:
function Click(props) {
return (
<button disable={props.disable}>
<span>Click me</span>
</button>
);
}
I want to test the logic of this component with the use of react-testing-library and mocha + chai.
The problem at which I stuck at the moment is that the getByText("Click me") selector returns the <span> DOM node, but for the tests, I need to check the disable attribute of the <button> node. What is the best practice for handling such test cases? I see a couple of solutions, but all of them sound a little bit off:
Use data-test-id for <button> element
Select one of the ancestors of the <Click /> component and then select the button within(...) this scope
Click on the selected element with fireEvent and check that nothing has happened
Can you suggest a better approach?
Assert if button is disabled
You can use the toHaveAttribute and closest to test it.
import { render } from '#testing-library/react';
const { getByText } = render(Click);
expect(getByText(/Click me/i).closest('button')).toHaveAttribute('disabled');
or toBeDisabled
expect(getByText(/Click me/i).closest('button')).toBeDisabled();
Assert if button is enabled
To check if the button is enabled, use not as follows
expect(getByText(/Click me/i).closest('button')).not.toBeDisabled();
You can use toBeDisabled() from #testing-library/jest-dom, it is a custom jest matcher to test the state of the DOM:
https://github.com/testing-library/jest-dom
Example:
<button>Submit</button>
expect(getByText(/submit/i)).toBeDisabled()
For someone who is looking for the test in which the button is not disabled.
import { render } from '#testing-library/react';
const { getByText } = render(Click);
expect(getByText(/Click me/i).getAttribute("disabled")).toBe(null)
I would politely argue you are testing an implementation detail, which react-testing-library discourages.
The more your tests resemble the way your software is used, the more confidence they can give you.
If a button is disabled, a user doesn't see a disabled prop, instead they see nothing happen. If a button is enabled, a user doesn't see the omission of a disabled prop, instead they see something happen.
I believe you should be testing for this instead:
const Button = (props) => (
<button
type="submit"
onClick={props.onClick}
disabled={props.disabled}
>
Click me
</button>
);
describe('Button', () => {
it('will call onClick when enabled', () => {
const onClick = jest.fn();
render(<Button onClick={onClick} disabled={false} />);
userEvent.click(getByRole('button', /click me/i));
expect(onClick).toHaveBeenCalledTimes(1);
});
it('will not call onClick when disabled', () => {
const onClick = jest.fn();
render(<Button onClick={onClick} disabled={true} />);
userEvent.click(getByRole('button', /click me/i));
expect(onClick).not.toHaveBeenCalled();
});
})
toHaveAttribute is good option in using attribute.
<button data-testid="ok-button" type="submit" disabled>ok</button>
const button = getByTestId('ok-button')
//const button = getByRole('button');
expect(button).toHaveAttribute('disabled')
expect(button).toHaveAttribute('type', 'submit')
expect(button).not.toHaveAttribute('type', 'button')
expect(button).toHaveAttribute('type', expect.stringContaining('sub'))
expect(button).toHaveAttribute('type', expect.not.stringContaining('but'))
Hope this will be helpful.
You can test the disable prop of the button just by using #testing-library/react as follows.
example:
import { render } from '#testing-library/react';
const {getByText} = render(<Click/>)
expect(getByText('Click me').closest('button').disabled).toBeTruthy()
Another way to fix this would be to grab by the role and check the innerHTML like,
const { getByRole } = render(<Click />)
const button = getByRole('button')
// will make sure the 'Click me' text is in there somewhere
expect(button.innerHTML).toMatch(/Click me/))
This isn't the best solution for your specific case, but it's one to keep in your back pocket if you have to deal with a button component that's not an actual button, e.g.,
<div role="button"><span>Click Me</span></div>
My solution, It seems to me that this case covers well what is necessary. Check that the button is disabled, so toHaveBeenCalledTimes must receive 0
test('Will not call onClick when disabled', () => {
const mockHandler = jest.fn()
render(<Button title="Disabled account" disabled={true} onClick={mockHandler} />)
const button = screen.getByText("Disabled account")
fireEvent.click(button)
expect(mockHandler).toHaveBeenCalledTimes(0)
expect(button).toHaveProperty('disabled', true)
})

onClick event not called when clicking

React onClick event not working when clicking on glyphicon.
const SortableItem = SortableElement(({value, parentId}) =>
<ListGroupItem bsStyle='success' style={{width:'300px',textAlign:'left'}}>
{value}
{parentId === null && <span className='glyphicon glyphicon-remove' style={{float:'right',width:'10px',height:'10px'}}
onClick={e => {e.preventDefault(); console.log('yes')}}/>}
</ListGroupItem>
);
I ran into something similar. My onClick events on my <a> elements were not getting triggered when a user clicked them.
This is what I was doing wrong and maybe you are doing the same mistake as what I was doing. Without more context, it's impossible to diagnose what your actual problem is.
(code is basically saying, when I click the background, stop propagation of the click)
// Example of what I was doing WRONG
const Dialog = ({ onClose, children }) => {
const $overlay = React.useRef(null)
// on mount
React.useEffect(() => {
const handleBackgroundClick = (e) => {
e.stopPropagation() // <<-- This was the line causing the issue
onClose()
})
$overlay.current?.addEventListener('click', handleBackgroundClick)
// on unmount
return () => {
$overlay.current?.removeEventListener('click', handleBackgroundClick)
}
}, [])
return (
<div className="Dialog" ref={$overlay}>{children}</div>
)
}
Basically what I'm saying is this:
Do not use event.stopPropagation() directly on DOM elements in React
The events need to be able to bubble all the way to the top level or in-built React event's will stop working.
I ended up getting around the issue by adding an extra div inside the dialog to act as the overlay.
// How I fixed the issue
const Dialog = ({ onClose, children }) => {
return (
<div className="Dialog">
<div className="Dialog__overlay" onClick={()=> onClose()}></div>
<div className="Dialog__content">{children}</div>
</div>
)
}
Note: These are simplified examples to demonstrate a point. The exact code used was more complex. Using this code as displayed here would cause accessibility issues on your website.

Resources