Check that button is disabled in react-testing-library - reactjs

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)
})

Related

React - How to get unit test coverage for toggling open a popover that uses state or determine if it should be open

I'm trying to get unit test coverage for the code in red (see screenshot) using react-testing-library. Would anyone know what unit test would cover this? I'm still learning the react-testing-library. TIA
screenshot of code here showing red, uncovered code
If you don't open the screenshot above, the code inside this function is what needs to be covered.
const togglePopover = () => {
setToolTipOpen((prev) => !prev);
};
actual full component code block:
import React, { FunctionComponent, useState, KeyboardEvent, useRef, useEffect } from 'react';
import styles from './InfoPopover.module.scss';
import { Popover, PopoverBody } from 'x'
import { PopperPlacementType } from '#material-ui/core';
import { ReactComponent as InfoIcon } from '../../../assets/icons/tooltipIcon.svg';
export interface PopperProps {
placement?: PopperPlacementType;
tipMessage: React.ReactNode | string;
stringedTipMessage: string;
}
const InfoPopover: FunctionComponent<PopperProps> = ({
placement,
tipMessage,
stringedTipMessage
}: PopperProps) => {
const [toolTipOpen, setToolTipOpen] = useState(false);
const togglePopover = () => {
setToolTipOpen((prev) => !prev);
};
const handleBlur = () => {
setToolTipOpen(false);
};
return (
<>
<button
id="popoverTarget"
className={styles.tooltipButton}
onBlur={handleBlur}
aria-label={`Tooltip Content - ${stringedTipMessage}`}
>
<InfoIcon aria-label="status tooltip" />
</button>
<Popover
target="popoverTarget"
trigger="legacy"
toggle={togglePopover}
placement={placement}
isOpen={toolTipOpen}
arrowClassName={styles.toolTipArrow}
popperClassName={styles.toolTipPopout}
>
<PopoverBody>{tipMessage}</PopoverBody>
</Popover>
</>
);
};
export default InfoPopover;
With React Testing Library, the approach is to test what the user can see/do rather than test the internals of your application.
With your example, assuming you are trying to test a simple open/close popup user flow then the user would be seeing a button, and when they activate that button they would see a popover. A simple RTL approach would be as follows:
const popoverTipMessage = "My popover message";
render(<InfoPopover tipMessage={popoverTipMessage} />);
// Popover isn't activated, so it shouldn't be in the DOM
expect(screen.getByText(popoverTipMessage)).not.toBeInDocument();
// Find button and click it to show the Popover
fireEvent.click(screen.getByRole('button', {
name: /tooltip content/i
}));
// Popover should now be activated, so check if it's visible (in the DOM)
await waitFor(() => {
// This relies on RTL's text matching to find the component.
expect(screen.getByText(popoverTipMessage)).toBeInDocument();
});
// Find button and click it again to hide the Popover
fireEvent.click(screen.getByRole('button', {
name: /tooltip content/i
}));
// Popover should now be hidden, so check if the DOM element has gone
// Note: There are other ways of checking appearance/disappearance. Check the RTL docs.
await waitFor(() => {
// This relies on RTL's text matching to find the component but there are other better ways to find an element
expect(screen.getByText(popoverTipMessage)).not.toBeInDocument();
});
The query methods I've used above are some of the basic ones, however RTL has many different queries to find the element you need to target. It has accessibility at the forefront of its design so leans heavily on these. Take a look in the docs: https://testing-library.com/docs/react-testing-library/example-intro

Cleanup does not prevent error for finding multiple elements with the same role

I am trying to run a test with the React Testing Library
It clicks a button inside a component. I map over the component, so the button exists several times.
Even though i have a cleanup set up, i still get the following error:
** TestingLibraryElementError: Found multiple elements with the role "button" **
This is my code:
afterEach(cleanup);
describe("button", () => {
it("calls a function when button is clicked", () => {
const callback = jest.fn();
render(<ProductCard onCartToggleClicked={callback} />);
const { getByRole } = render(<ProductCard />);
fireEvent.click(getByRole("button"));
expect(callback).toHaveBeenCalled();
});
afterEach(cleanup);
});
Since you have multiple buttons in the ProductCard component . You should be using getAllByRole instead of getByRole .
Now you can fireEvent with the index as
fireEvent.click(getByRole("button")[0]);
But if you want to target the button specifically then i would suggest to add data-testid prop to the button.
{someData.map((product, index) => (
<button data-testid={`button-${index}`}> click </button>
))}
Now with this in place you can use the getByTestId query
fireEvent.click(getByTestId("button-0"));
logRoles
If you are not sure how many buttons are present . Then you can use the logRoles .
import { logRoles } from '#testing-library/dom'
const { container } = render(<ProductCard onCartToggleClicked={callback} />)
logRoles(container)
This will give you all the possible roles.

Testing click event in React Testing Library

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
});

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();
});

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