120

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:

  1. Use data-test-id for <button> element
  2. Select one of the ancestors of the <Click /> component and then select the button within(...) this scope
  3. Click on the selected element with fireEvent and check that nothing has happened

Can you suggest a better approach?

Community
  • 1
  • 1
Nikita Sivukhin
  • 2,370
  • 3
  • 16
  • 33
  • Test that the disabled attribute of the button element is true? – jonrsharpe Jun 14 '19 at 07:58
  • 1
    Not OP's case, but for future visitors that came here looking to test `aria-disabled`, `.toBeDisabled()` won't work. The workaround is `.toHaveAttribute('aria-disabled', 'true')`. See [jest-dom #144](https://github.com/testing-library/jest-dom/issues/144#issuecomment-577235097). – ggorlen Dec 15 '22 at 19:29

9 Answers9

205

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();
johnny peter
  • 4,634
  • 1
  • 26
  • 38
  • 2
    Just a note: there must be a relatevily fresh version of the `jsdom` library to be able to use `closest(...)` function in your example (v 11.12.0 or higher) – Nikita Sivukhin Jun 14 '19 at 08:16
  • 13
    Note that `toHaveAttribute` requires that https://github.com/testing-library/jest-dom be installed. – Sam Dec 09 '19 at 12:45
  • Thanks! that helps to test Material-ui buttons – misolo Sep 10 '20 at 14:17
  • 3
    For everyone using a different framework than jest, e.g. Jasmine, something like this should do the job: `expect(getByText(/Click me/i).closest('button').hasAttribute('disabled')).toBeTrue();` – René Schubert Feb 17 '21 at 10:57
  • While this answer works, personally I feel like its not aligned to the ethos of react-testing-library. I've provided an alternative answer below that, to me, is more in-tune with rtl (not saying better or worse, just an alternative) – Chris Aug 08 '21 at 10:26
  • `TS2339: Property 'toBeDisabled' does not exist on type 'JestMatchers '.` – Dimitri Kopriwa Jan 12 '22 at 13:45
  • @DimitriKopriwa have you added `@testing-library/jest-dom`? – johnny peter Jan 17 '22 at 05:21
  • What I find funny is that even after the assertion of being disabled passes I can click the button and it triggers the callback... why? – Watchmaker Mar 09 '22 at 12:08
  • if you want to check for not enabled elements then do this -------------------------- expect(getByText(/Click me/i).getAttribute("disabled")).toBeDefined() – Rishab Surana Mar 24 '22 at 10:07
  • 1
    I felt similarly to others about this approach being focused on implementation details, but it's noteworthy that in testing-library's own jest-dom implementation, `.toBeDisabled()` is [just checking the attribute](https://github.com/testing-library/jest-dom/blob/87ffd2a639dcf91fb59e38066136976e163df618/src/to-be-disabled.js#L44), and they recommend using this matcher for checking disabled state [in their own guide](https://testing-library.com/docs/react-testing-library/example-intro/#quickstart). – Shawn Erquhart Nov 03 '22 at 14:31
39

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()
Towko
  • 33
  • 7
Meysam Izadmehr
  • 3,103
  • 17
  • 25
18

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)
lpham2525
  • 5
  • 3
Tony Nguyen
  • 3,298
  • 11
  • 19
13

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();
  });
})
Chris
  • 54,599
  • 30
  • 149
  • 186
  • 3
    Good point! Altough I think that in the case of disabled button you can sacrifice the purity of concept and test "implementation details", especially when you test `disabled` attribute which is the part of the HTML specification with fixed behaviour. Can you provide some example that will shows cons of testing `disabled` prop and pros of mocking `click` callback, just for my interest? – Nikita Sivukhin Aug 09 '21 at 19:20
  • As @NikitaSivukhin stated, you are testing the HTML fixed behavior when you check if clicking a disabled button will trigger OnClick. While the user cannot see if a prop is disabled, they will see the visual effect of the disabled prop. – Logan Cundiff Mar 01 '22 at 19:32
  • @LoganCundiff Of course. Basically "never say never" - there will always be exceptions to the "rules". I personally wouldn't check for the disabled attribute but there may be cases where it makes more sense to sacrifice the purity of the concept as Nikita said. Ultimately I don't know your codebase and you should do what fits within your team and projects – Chris Mar 01 '22 at 23:04
  • 1
    @Chris Yea I think it is good you brought up this point. It's worth asking these questions when considering how to implement a test instead of taking a one fit all approach. Good point. – Logan Cundiff Mar 02 '22 at 19:47
6

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.

Ultimate Fire
  • 281
  • 4
  • 8
3

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()
Satish
  • 39
  • 1
1

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>

Yatrix
  • 13,361
  • 16
  • 48
  • 78
0

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

Current answer:

expect(await screen.findByRole('button', { name: 'Click me' })).toBeEnabled();
batbrain9392
  • 565
  • 6
  • 15