How do I trigger a change event on radio buttons in react-testing-library?
Asked Answered
B

8

40

I'm in the process of moving over to react-testing-library, and have no idea how to trigger this event and get the results of the changes.

I've tried using the fireEvent function to trigger the change, and then tried the rerender function, but I can't seem to get it to work.

App.js

import React, { useState } from "react";
import logo from "./logo.svg";
import "./App.css";

const options = {
  DoTheThing: 'DoTheThing',
  DoOtherThing: 'DoOtherThing',
};

function App() {
  const [action, setAction] = useState(options.DoTheThing);

  return (
    <div className="App">
      <header className="App-header">
        <form>
          <fieldset>
            <label>
              <input
                type="radio"
                name="radio1"
                value={options.DoTheThing}
                checked={action === options.DoTheThing}
                onChange={event => setAction(event.target.value)}
              />
              First
            </label>

            <label>
              <input
                type="radio"
                name="radio1"
                value={options.DoOtherThing}
                checked={action === options.DoOtherThing}
                onChange={event => setAction(event.target.value)}
              />
              Second
            </label>
          </fieldset>
        </form>
      </header>
    </div>
  );
}

export default App;

App.test.js

import React from 'react';
import { render, cleanup, fireEvent } from 'react-testing-library';
import App from './App';

afterEach(cleanup);

it('should change the value ', () => {
  const {getByLabelText, rerender } = render(<App/>);
  const second = getByLabelText(/Second/);

  fireEvent.change(second);
  rerender(<App/>);

  expect(document.forms[0].elements.radio1.value).toEqual("DoOtherThing");

});
Bookstand answered 15/2, 2019 at 0:46 Comment(0)
M
14

Update

As people pointed out my original solution was wrong.

Nowadays I suggest you use userEvent for better-simulating user interactions.

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

test("radio", () => {
  const user = userEvent.setup();
  render(
    <form>
      <label>
        First <input type="radio" name="radio1" value="first" />
      </label>
      <label>
        Second <input type="radio" name="radio1" value="second" />
      </label>
    </form>
  )

  await user.click(screen.getByLabelText("Second"));
});

First, you don't have to call rerender. You use rerender only when you want the component to receive different props. See link.

Whenever you call fireEvent the component will render like it would in your normal app.

It's correct to fire a change event, but you must pass a second parameter with the event data.

This example works:

import React from "react";
import { render, fireEvent } from "react-testing-library";

test("radio", () => {
  const { getByLabelText } = render(
    <form>
      <label>
         First <input type="radio" name="radio1" value="first" />
      </label>
      <label>
        Second <input type="radio" name="radio1" value="second" />
      </label>
    </form>
  );

  const radio = getByLabelText('First')
  fireEvent.change(radio, { target: { value: "second" } });
  expect(radio.value).toBe('second')
});
Moats answered 15/2, 2019 at 9:6 Comment(6)
I originally sent a mock event, but then saw some [presumably] older info showing that was unnecessary. I'd only added the rerender because I couldn't see the change itself.Bookstand
I remember why I had re-render. Ultimately, what I want to test is the appearance of a particular set of inputs after the value has changed. My first step was being able to assert that the change to the radio happened. I was using waitForElement, but it just times out and throws. Should I create a new question for that?Bookstand
waitForElement is useful if something asynchronous happens e.g. you're waiting for a fetch response. In this case, you should not need to wait for anything because the update happens immediately. When waitForElement fails I try to use wait instead. If even that doesn't work the problem is usually somewhere else. Usually, my component doesn't update at all.Moats
This isn't how radio buttons work though. Their value is fixed. Only their checked state changes.Footwall
All this code is doing is changing the value of the first radio button to "second". now you just have 2 radio buttons with the value "second" but neither of them are checked. Also it doesn't cause the react onChange handler to be invoked, so it's not really usefulLil
Agreed, this is not a correct example even if it is an acceptable answer, I have lost hours around this misled by this path.Rob
D
35

if you have a label like with Radio Component of Material-ui, you could use:

const labelRadio: HTMLInputElement = getByLabelText('Label of Radio');
expect(labelRadio.checked).toEqual(false);
fireEvent.click(labelRadio);
expect(androidRadio.checked).toEqual(true);

or you can add https://github.com/testing-library/jest-dom matchers and check it in this way:

expect(getByLabelText('Label of Radio')).not.toBeChecked();
fireEvent.click(labelRadio);
expect(getByLabelText('Label of Radio')).toBeChecked();
Dehlia answered 20/5, 2020 at 8:36 Comment(0)
M
14

Update

As people pointed out my original solution was wrong.

Nowadays I suggest you use userEvent for better-simulating user interactions.

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

test("radio", () => {
  const user = userEvent.setup();
  render(
    <form>
      <label>
        First <input type="radio" name="radio1" value="first" />
      </label>
      <label>
        Second <input type="radio" name="radio1" value="second" />
      </label>
    </form>
  )

  await user.click(screen.getByLabelText("Second"));
});

First, you don't have to call rerender. You use rerender only when you want the component to receive different props. See link.

Whenever you call fireEvent the component will render like it would in your normal app.

It's correct to fire a change event, but you must pass a second parameter with the event data.

This example works:

import React from "react";
import { render, fireEvent } from "react-testing-library";

test("radio", () => {
  const { getByLabelText } = render(
    <form>
      <label>
         First <input type="radio" name="radio1" value="first" />
      </label>
      <label>
        Second <input type="radio" name="radio1" value="second" />
      </label>
    </form>
  );

  const radio = getByLabelText('First')
  fireEvent.change(radio, { target: { value: "second" } });
  expect(radio.value).toBe('second')
});
Moats answered 15/2, 2019 at 9:6 Comment(6)
I originally sent a mock event, but then saw some [presumably] older info showing that was unnecessary. I'd only added the rerender because I couldn't see the change itself.Bookstand
I remember why I had re-render. Ultimately, what I want to test is the appearance of a particular set of inputs after the value has changed. My first step was being able to assert that the change to the radio happened. I was using waitForElement, but it just times out and throws. Should I create a new question for that?Bookstand
waitForElement is useful if something asynchronous happens e.g. you're waiting for a fetch response. In this case, you should not need to wait for anything because the update happens immediately. When waitForElement fails I try to use wait instead. If even that doesn't work the problem is usually somewhere else. Usually, my component doesn't update at all.Moats
This isn't how radio buttons work though. Their value is fixed. Only their checked state changes.Footwall
All this code is doing is changing the value of the first radio button to "second". now you just have 2 radio buttons with the value "second" but neither of them are checked. Also it doesn't cause the react onChange handler to be invoked, so it's not really usefulLil
Agreed, this is not a correct example even if it is an acceptable answer, I have lost hours around this misled by this path.Rob
L
6

As of May 2020 using React 16.13 and react-testing-library 10.0, the accepted answer does not work (the test itself passes but doesn't actually do anything meaningful).

I can't find any reference to radio buttons in the documentation for react-testing-library or even React itself. However, here's an example (using Typescript) that works properly as far as I can tell.

import React from "react";
class State {
    radioValue: string = "one"
}
export default class RadioTest extends React.Component<{}, State>
{
    state: State = new State();

    radioClick = (event: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
        this.setState({ radioValue: event.currentTarget.value });
    }

    render() {
        return (<>
            <label>
                One
                <input type="radio" name="radio" onClick={this.radioClick}
                    value="one" onChange={() => {}}
                    checked={this.state.radioValue === "one"} />
            </label>
            <label>
                Two
                <input type="radio" name="radio" onClick={this.radioClick}
                    value="two" onChange={() => {}}
                    checked={this.state.radioValue === "two"} />
            </label>
            <div>current value={this.state.radioValue}</div>
            <button onClick={() => this.setState({radioValue:"one"})}>Click</button>
        </>);
    }
}

test("radiotest", () => {
    const { getByLabelText, queryByText, getByText } = render(<RadioTest />);
    const one = getByLabelText('One') as HTMLInputElement
    const two = getByLabelText('Two') as HTMLInputElement
    expect(one).toBeChecked();
    expect(two).not.toBeChecked();
    expect(queryByText("current value=one")).toBeTruthy();
    fireEvent.click(two);
    expect(one).not.toBeChecked();
    expect(two).toBeChecked();
    expect(queryByText("current value=two")).toBeTruthy();
    fireEvent.click(getByText("Click"))
    expect(one).toBeChecked();
    expect(two).not.toBeChecked();
    expect(queryByText("current value=one")).toBeTruthy();
});

React onChange handlers will work in the browser but not with react-testing-library because they don't fire when you call fireEvent.change()

The dummy onChange handlers are required to avoid a React warning: "If the field should be mutable use defaultChecked". You can't use defaultChecked though, because it stops you setting the state in code (i.e. clicking the button at the bottom doesn't update the radio)

So on the whole, it looks like React wants you to use onChange but react-testing-library only works with onClick, so this is a bit of a fudge.

Lil answered 1/5, 2020 at 10:21 Comment(1)
This was the solution for our issue. .change does not trigger the behaviourWeichsel
B
1

Can use like mentioned below solution for RadioGroup. It will work for RTL.

render(component);
const fieldset = screen.getByLabelText(/other applicable cards/i);
expect(fieldset.checked).toEqual(false);
fireEvent.click(fieldset, { target: { value: '2' } });
expect(fieldset.value).toEqual('2');

Can use for manage multiple radio click event.

  render(component);
  const fieldset = screen.getByLabelText(/other applicable cards/i);
  expect(fieldset.checked).toEqual(false);
  fireEvent.click(fieldset, { target: { value: '2' } });
  expect(fieldset.value).toEqual('2');
  const fieldset1 = screen.getByLabelText(/original amex platinum/i);
  expect(fieldset1.checked).toEqual(false);
  fireEvent.click(fieldset1, { target: { value: '1' } });
  expect(fieldset1.value).toEqual('1');
Brahe answered 15/2, 2019 at 0:47 Comment(0)
A
1

Complementing @andy's answer, this should test two radios effectly:

  it('should render successfully', async () => {
    render(
      <YourRadioGroup />
    );

    expect(screen.getByText('option 1')).toBeInTheDocument();
    expect(screen.getByText('option 2')).toBeInTheDocument();
  });

  it('should change checked option', () => {
    render(
      <YourRadioGroup />
    );

    const secondRadio = screen.getByLabelText('option 2');
    fireEvent.click(secondRadio);
    expect(secondRadio).toBeChecked();

    const firstRadio = screen.getByLabelText('option 1');
    fireEvent.click(firstRadio);
    expect(firstRadio).toBeChecked();
    expect(secondRadio).not.toBeChecked();
  });
Allative answered 20/10, 2021 at 18:38 Comment(0)
S
0

This worked for me (working with the radio buttons, not the radio groups):

// some code here to make sure the screen has finished rendering, and I have all radio buttons in the DOM (I am expecting 5 containers):
await waitFor(() => expect(screen.getAllByTestId('some-slow-loading-container')).toHaveLength(5))

// get all "true" labeled radio buttons by test id (or by role + name or whatever):
const allTrueLabelRadioButtons = screen.getAllByTestId('true-label-radio-button');

// loop over the array of HTML elements found:
for (const trueLabelRadioButton of allTrueLabelRadioButtons) {
   // using fireEvent instead of userEvent because of some bugs with the components library I am stuck with. Usually I use userEvent:
   fireEvent.click(trueLabelRadioButton)
}

// check whatever you are expecting to happen after all radio buttons are set to "true".
//...
Smudge answered 9/2, 2022 at 13:23 Comment(0)
S
-1

Please try this from react-testing-library docs, "render" should work fine. Agree with @Gpx

fireEvent.change(input, { target: { value: 'your_value_goes_here' } })
expect(input.value).toBe('expected_value')
Saskatoon answered 15/2, 2019 at 9:7 Comment(0)
J
-1

I've also had this work for me:

test('Does stuff', async () => {
// ... test prep ...

const formEl = screen.getByTestId('form_test_id')

// You can use screen.getByLabelText here instead of DOM queries 
// if you've got a nicely laid out form
const defaultInput = formEl.querySelector('input[value="default_value"]')
const newValueInput = formEl.querySelector('input[value="new_value"]')

// Confirm your baseline
expect(defaultInput.checked).toEqual(true)
expect(newValueInput.checked).toEqual(false)

// Fire the change event
await act(async () => {
fireEvent.change(newValueInput, { target: { checked: true } }) 
// To trigger any onChange listeners
fireEvent.blur(newValueInput)
})

// Confirm expected form state(s)
expect(defaultInput.checked).toEqual(false)
expect(newValueInput.checked).toEqual(true)

})
Justice answered 7/4, 2020 at 17:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.