how to test react-select with react-testing-library

ReactjsIntegration TestingReact SelectReact Testing-Library

Reactjs Problem Overview


App.js

import React, { Component } from "react";
import Select from "react-select";

const SELECT_OPTIONS = ["FOO", "BAR"].map(e => {
  return { value: e, label: e };
});

class App extends Component {
  state = {
    selected: SELECT_OPTIONS[0].value
  };

  handleSelectChange = e => {
    this.setState({ selected: e.value });
  };
  
  render() {
    const { selected } = this.state;
    const value = { value: selected, label: selected };
    return (
      <div className="App">
        <div data-testid="select">
          <Select
            multi={false}
            value={value}
            options={SELECT_OPTIONS}
            onChange={this.handleSelectChange}
          />
        </div>
        <p data-testid="select-output">{selected}</p>
      </div>
    );
  }
}

export default App;

App.test.js

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

afterEach(cleanup);

const setup = () => {
  const utils = render(<App />);
  const selectOutput = utils.getByTestId("select-output");
  const selectInput = document.getElementById("react-select-2-input");
  return { selectOutput, selectInput };
};

test("it can change selected item", async () => {
  const { selectOutput, selectInput } = setup();
  getByText(selectOutput, "FOO");
  fireEvent.change(selectInput, { target: { value: "BAR" } });
  await waitForElement(() => getByText(selectOutput, "BAR"));
});

This minimal example works as expected in the browser but the test fails. I think the onChange handler in

Reactjs Solutions


Solution 1 - Reactjs

In my project, I'm using react-testing-library and jest-dom. I ran into same problem - after some investigation I found solution, based on thread: https://github.com/airbnb/enzyme/issues/400

Notice that the top-level function for render has to be async, as well as individual steps.

There is no need to use focus event in this case, and it will allow to select multiple values.

Also, there has to be async callback inside getSelectItem.

const DOWN_ARROW = { keyCode: 40 };

it('renders and values can be filled then submitted', async () => {
  const {
    asFragment,
    getByLabelText,
    getByText,
  } = render(<MyComponent />);

  ( ... )

  // the function
  const getSelectItem = (getByLabelText, getByText) => async (selectLabel, itemText) => {
    fireEvent.keyDown(getByLabelText(selectLabel), DOWN_ARROW);
    await waitForElement(() => getByText(itemText));
    fireEvent.click(getByText(itemText));
  }

  // usage
  const selectItem = getSelectItem(getByLabelText, getByText);

  await selectItem('Label', 'Option');

  ( ... )

}

Solution 2 - Reactjs

This got to be the most asked question about RTL :D

The best strategy is to use jest.mock (or the equivalent in your testing framework) to mock the select and render an HTML select instead.

For more info on why this is the best approach, I wrote something that applies to this case too. The OP asked about a select in Material-UI but the idea is the same.

Original question and my answer:

> Because you have no control over that UI. It's defined in a 3rd party module. > > So, you have two options: > > You can figure out what HTML the material library creates and then use container.querySelector to find its elements and interact with it. It takes a while but it should be possible. After you have done all of that you have to hope that at every new release they don't change the DOM structure too much or you might have to update all your tests. > > The other option is to trust that Material-UI is going to make a component that works and that your users can use. Based on that trust you can simply replace that component in your tests for a simpler one. > > Yes, option one tests what the user sees but option two is easier to maintain. > > In my experience the second option is just fine but of course, your use-case might be different and you might have to test the actual component.

This is an example of how you could mock a select:

jest.mock("react-select", () => ({ options, value, onChange }) => {
  function handleChange(event) {
    const option = options.find(
      option => option.value === event.currentTarget.value
    );
    onChange(option);
  }
  return (
    <select data-testid="select" value={value} onChange={handleChange}>
      {options.map(({ label, value }) => (
        <option key={value} value={value}>
          {label}
        </option>
      ))}
    </select>
  );
});

You can read more here.

Solution 3 - Reactjs

Finally, there is a library that helps us with that: https://testing-library.com/docs/ecosystem-react-select-event. Works perfectly for both single select or select-multiple:

From @testing-library/react docs:

import React from 'react'
import Select from 'react-select'
import { render } from '@testing-library/react'
import selectEvent from 'react-select-event'

const { getByTestId, getByLabelText } = render(
  <form data-testid="form">
    <label htmlFor="food">Food</label>
    <Select options={OPTIONS} name="food" inputId="food" isMulti />
  </form>
)
expect(getByTestId('form')).toHaveFormValues({ food: '' }) // empty select

// select two values...
await selectEvent.select(getByLabelText('Food'), ['Strawberry', 'Mango'])
expect(getByTestId('form')).toHaveFormValues({ food: ['strawberry', 'mango'] })

// ...and add a third one
await selectEvent.select(getByLabelText('Food'), 'Chocolate')
expect(getByTestId('form')).toHaveFormValues({
  food: ['strawberry', 'mango', 'chocolate'],
})

Thanks https://github.com/romgain/react-select-event for such an awesome package!

Solution 4 - Reactjs

Similar to @momimomo's answer, I wrote a small helper to pick an option from react-select in TypeScript.

Helper file:

import { getByText, findByText, fireEvent } from '@testing-library/react';

const keyDownEvent = {
    key: 'ArrowDown',
};

export async function selectOption(container: HTMLElement, optionText: string) {
    const placeholder = getByText(container, 'Select...');
    fireEvent.keyDown(placeholder, keyDownEvent);
    await findByText(container, optionText);
    fireEvent.click(getByText(container, optionText));
}

Usage:

export const MyComponent: React.FunctionComponent = () => {
    return (
        <div data-testid="day-selector">
            <Select {...reactSelectOptions} />
        </div>
    );
};
it('can select an option', async () => {
    const { getByTestId } = render(<MyComponent />);
    // Open the react-select options then click on "Monday".
    await selectOption(getByTestId('day-selector'), 'Monday');
});

Solution 5 - Reactjs

This solution worked for me.

fireEvent.change(getByTestId("select-test-id"), { target: { value: "1" } });

Hope it might help strugglers.

Solution 6 - Reactjs

export async function selectOption(container: HTMLElement, optionText: string) {
  let listControl: any = '';
  await waitForElement(
    () => (listControl = container.querySelector('.Select-control')),
  );
  fireEvent.mouseDown(listControl);
  await wait();
  const option = getByText(container, optionText);
  fireEvent.mouseDown(option);
  await wait();
}

NOTE: container: container for select box ( eg: container = getByTestId('seclectTestId') )

Solution 7 - Reactjs

An alternative solution which worked for my use case and requires no react-select mocking or separate library (thanks to @Steve Vaughan) found on the react-testing-library spectrum chat.

The downside to this is we have to use container.querySelector which RTL advises against in favour of its more resillient selectors.

Solution 8 - Reactjs

In case you are not using a label element, the way to go with react-select-event is:

const select = screen.container.querySelector(
  "input[name='select']"
);

selectEvent.select(select, "Value");

Solution 9 - Reactjs

if for whatever reason there is a label with the same name use this

const [firstLabel, secondLabel] = getAllByLabelText('State');
    await act(async () => {
      fireEvent.focus(firstLabel);
      fireEvent.keyDown(firstLabel, {
        key: 'ArrowDown',
        keyCode: 40,
        code: 40,
      });

      await waitFor(() => {
        fireEvent.click(getByText('Alabama'));
      });

      fireEvent.focus(secondLabel);
      fireEvent.keyDown(secondLabel, {
        key: 'ArrowDown',
        keyCode: 40,
        code: 40,
      });

      await waitFor(() => {
        fireEvent.click(getByText('Alaska'));
      });
    });

or If you have a way to query your section—for example with a data-testid—you could use within:

within(getByTestId('id-for-section-A')).getByLabelText('Days')
within(getByTestId('id-for-section-B')).getByLabelText('Days')

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionuser2133814View Question on Stackoverflow
Solution 1 - ReactjsmomimomoView Answer on Stackoverflow
Solution 2 - ReactjsGiorgio Polvara - GpxView Answer on Stackoverflow
Solution 3 - ReactjsConstantinView Answer on Stackoverflow
Solution 4 - ReactjsVestrideView Answer on Stackoverflow
Solution 5 - ReactjsMalaji NagarajuView Answer on Stackoverflow
Solution 6 - Reactjstushar kumarView Answer on Stackoverflow
Solution 7 - ReactjsjameshelouView Answer on Stackoverflow
Solution 8 - ReactjsTudor MorarView Answer on Stackoverflow
Solution 9 - ReactjsyoullbehauntedView Answer on Stackoverflow