How to test Material-UI Checkbox is checked with react-testing-library?
Asked Answered
M

9

22

Seems like the way Material UI works is it renders a different SVG when the checkbox is clicked, and not changing the attributes or anything on the actual input element. So how do I actually test that the element is checked in line with the react-testing-library philosophy?

Here's a rough example of the

Checkbox component usage

export const CheckBoxContainer = () => (
  <Checkbox inputProps={{ 'data-testid': `clickable-checkbox-1234` }} data-testid={`checkbox-1234`} />
);

Test

test('check the box', async () => {
  const { getByTestId } = render(<CheckBoxContainer />);
  await waitForElement(() => getByTestId(`checkbox-1234`));
  const checkbox = getByTestId(`checkbox-1234`);
  fireEvent.click(getByTestId(`clickable-checkbox-1234`));
  expect(checkbox).toHaveAttribute('checked');
});

Generated HTML by Material UI

<span
  class="MuiButtonBase-root-54 MuiIconButton-root-48 MuiSwitchBase-root-231 MuiCheckbox-root-225 MuiCheckbox-colorSecondary-230 MuiSwitchBase-checked-232 MuiCheckbox-checked-226"
  data-testid="checkbox-1234"
>
  <span class="MuiIconButton-label-53">
    <svg
      class="MuiSvgIcon-root-57"
      focusable="false"
      viewBox="0 0 24 24"
      aria-hidden="true"
      role="presentation"
    >
      <path
        d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"
      ></path>
    </svg>
    <input
      class="MuiSwitchBase-input-234"
      type="checkbox"
      data-indeterminate="false"
      data-testid="clickable-checkbox-1234"
      value=""
    />
  </span>
  <span class="MuiTouchRipple-root-66"> </span>
</span>

Mujik answered 12/11, 2018 at 23:36 Comment(0)
S
27

Since the checkbox is rendering an input I would work with it rather than focusing on the image.

You could do something like this:

const checkbox = within(getByTestId('checkbox-1234')).getByRole('checkbox')
expect(checkbox).toHaveProperty('checked', true)
Spectra answered 13/11, 2018 at 7:51 Comment(5)
The input doesn't change, and never gets a "checked" attribute, the SVG is what actually changesMujik
That's strange. Can you create a reproducible example in codesandbox?Spectra
Found an example from Dan Abramov: codepen.io/gaearon/pen/wgedvV?editors=0010Mujik
Found a way to write this whithout offending the testing-library/no-node-access rule: const checkbox = within(getByTestId('checkbox-1234')).getByRole('checkbox')Prejudge
Please @GioPolvara update the answer with getByRole suggestion in previous commentPharyngoscope
M
10

The easiest way I have found to locate a checkbox is by adding a label to it

<FormControlLabel
  htmlFor="first-checkBox"
  label="First Checkbox"
  control={ <Checkbox
        checked={checked}
        onChange={handleChange}
        inputProps={{ 'aria-label': 'primary checkbox' }}
      />
  }
/>

later in test case

const checkboxEl = screen.getByLabelText('First Checkbox') as HTMLInputElement
expect(checkboxEl).toBeChecked()

if the test fails because doesn't recognize toBeChecked as part of the available assertions is because you need to import

import '@testing-library/jest-dom/extend-expect'

Mastery answered 16/7, 2020 at 20:33 Comment(0)
M
3

Figured it out. Need to use the checked getter on the DOM, and not try to check the checked property. Found this issue explaining it: https://github.com/facebook/react/issues/7051#issuecomment-334018747

and this Codepen demoing how the checked attribute doesn't change in React: https://codepen.io/gaearon/pen/wgedvV?editors=0010

Like this: expect(checkbox.checked).toBe(true)

Mujik answered 13/11, 2018 at 17:18 Comment(1)
I don't understand how is this different from my version. expect(checkbox).toHaveProperty('checked', true) is syntactic sugar for expect(checkbox.checked).toBe(true)Spectra
M
2

It seems that in JSDOM the checked property doesn't get updated e.g.

      fireEvent.click(input)
      console.log(input.checked) //false
      fireEvent.click(input)
      console.log(input.checked) //false

Which makes it hard to establish the current state of a checkbox. As fireEvent.click(input) is essentially a toggle it makes it tricky if you have a test that needs to set the value to true (tick) regardless of the initial state of the checkbox. You may have an update form where the checkbox's initial state could be either checked / not checked based on many different server data fixtures. Luckily with MUI we can determine the state by detecting some surrounding DOM classes. So I have made the following helper function...

const setMuiCheckBox = (input: HTMLInputElement, newValue: boolean) => {
  // find the root span of our checkbox
  const root = input.closest('.MuiButtonBase-root')
  if (root) {
    const isChecked = root.classList.contains('Mui-checked')
    if (isChecked !== newValue) {
      fireEvent.click(input)
    }
  }
}
Minuteman answered 5/6, 2020 at 11:5 Comment(0)
G
1

Since clicking on checkbox does not impact 'input' element. I noticed an additional class 'Mui-checked' is added to the containing the input.

In my case I did following changes:

test('check the box', async () => {      
const { getByTestId } = render(<CheckBoxContainer />);      
await waitForElement(() => getByTestId(`checkbox-1234`)); 
const checkbox = getByTestId(`checkbox-1234`); 
fireEvent.click(getByTestId(`clickable-checkbox-1234`));


expect(Object.values(checkboxNTB.classList).includes('Mui-checked')).toEqual(true) });

You can find which class is getting added when you check the checkbox and replace 'Mui-checked' with that class, if 'Mui-checked' doesn't work for you. You can use inspect element in browser to check this.

Guenevere answered 15/11, 2022 at 7:26 Comment(0)
M
0

Try this,

import React from 'react';
import {useState, useEffect} from 'react';

import FormControlLabel from '@material-ui/core/FormControlLabel';
import Checkbox from '@material-ui/core/Checkbox';

const CheckBox = () => {
    const [rememberMe, setRememberMe] = useState(false);
    
    const handleRememberMeChange = () => {
        if (rememberMe === true || rememberMe === false){
            setRememberMe(!rememberMe);
        }
        else{
            setRememberMe(true);
        }
    };

    return (
        <FormControlLabel
            control={<Checkbox value="remember" color="primary" />}
            label="Remember me"
            onChange = {handleRememberMeChange}
        />
    );
}

export default CheckBox;

The value of the Checkbox is stored in the state as a Boolean value.

Update: Apparently you can get the value of the Checkbox using event.target.checked in the handler. So the above handler can instead be,

    const handleRememberMeChange = (event) => {
        setRememberMe(event.target.checked);
    };
Misdoubt answered 30/5, 2021 at 5:56 Comment(0)
O
0

My checkbox component is controlled and click handler changes the state of the parent component which is then passed to the checkbox. So I used mockImplementation to rerender the component with needed checked value

test('calls handleSelect with correct argument on click', () => {
    const mockSelectOne = jest.fn();
    const { getByRole, rerender } = render(<Checkbox onClick={() => mockSelectOne('id')} />);
    expect(getByRole('checkbox')).not.toBeChecked();
    mockSelectOne.mockImplementation(() => rerender(<Checkbox onClick={() => mockSelectOne('id')} checked />));
    userEvent.click(getByRole('checkbox'));
    expect(mockSelectOne).toBeCalledTimes(1);
    expect(mockSelectOne).toBeCalledWith('id');
    expect(getByRole('checkbox')).toBeChecked();
  });
Oruntha answered 2/7, 2021 at 12:12 Comment(0)
A
0

As the react-testing-lib documentation mentions, using TestId should be the last thing we should test.

It can be done only with getByRole

const checkbox = getByRole('checkbox', { name: LABEL });
expect(checkbox).toBeChecked()
Aurea answered 1/2, 2023 at 17:38 Comment(0)
C
-1

In the OnChange function you have access to the event object and the checked value.

OnChange(event:object, checked:boolean)

This is written in the documentations.

An alternative way is to access the checked attribute from the event, as followed:

event.target.checked

this, too, is specified in the docs.

Docs: https://material-ui.com/api/checkbox/

Chokedamp answered 22/6, 2019 at 8:1 Comment(1)
Irrelevant answerAlvardo

© 2022 - 2024 — McMap. All rights reserved.