how to test react-select with react-testing-library
Asked Answered
O

14

74

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 is not invoked. How can I trigger the onChange callback in the test? What is the preferred way to find the element to fireEvent at? Thank you

Ophidian answered 8/4, 2019 at 14:33 Comment(0)
F
31

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.

Fugazy answered 8/4, 2019 at 15:16 Comment(10)
@GiorgioPolvara-Gpx While I get the approach you are suggesting I am curious to know if that goes actually against the guiding principles of Testing Library. The lib encourages to test what the final user actually interacts with (so to me is more an integration/functional test rather that an unit test). In your approach you are mocking the external dependency (which is good for a unit test) but if the dependency gets updated there is the change to have a successful test on a failing software. What are your thoughts about it?Momentum
@GiorgioPolvara-Gpx I read your blog and I'm using the react-select/async so I used jest.mock("react-select/async",... but I get a Unable to find an element by: [data-testid="select"] when trying fireEvent.change(getByTestId("select"), { target: { value: "foo" } }); I have a render(<MySearchEngine />) and it's like the getByTestId is looking into it instead of the jest.mock block. What have I missed ? thxBrittbritta
It looks like you're either not mocking correctly or not rendering the select. Have you tried to run debug() to see what's on the page?Fugazy
One should have absolutely no confidence in their component's test if they mock the component to this extent. I highly recommend NOT going with this approach. You're testing a completely different component in this situation.Predominance
It's a tradeoff between ease of testing and coverage. If your application is for 90% the select don't mock it. But, if you only use the select in some of your pages I don't think spending the time to figure out how to test the select is worth it. Of course ymmvFugazy
I replaced jest.mock("react-select/async",... with jest.mock("../Shared/LocalitySelect.jsx",... LocalitySelect.jsx being my component that renders the react-select/async component itself. Not sure I'm on the right track here but I now get TypeError: Cannot read property 'map' of undefined at the beginning of {loadOptions.map(({ label, value }) => ( Now how/where do I populate loadOptions if that makes any sense ?...Brittbritta
It's hard for me to help you from here. Open a new question either here or on the official Spectrum pageFugazy
@GiorgioPolvara-Gpx i disagree that you should mock the third party library. if that library changes/breaks, i want to know about it (without necessarily reading the changelog/release notes), and tests are how that is going to happen.Hair
polvara.me/posts/testing-a-custom-select-with-react-testing-library/Bleeding
Another big downside of this approach is that it will force you to always have a label and a value in all components, even if your domain model does not require it... This way should not be takenAssert
S
56

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

  ( ... )

}
Shampoo answered 17/5, 2019 at 9:51 Comment(9)
I personally prefer this solution much more than the accepted answer, because you keep things like they are. On that way you really test things like they would be tested by a user. If you mock react-select you even need to test your own mock, which is somehow counterproductive.. also if you use more complex properties which react-select provides your mock also gets more complex and also hard to maintain IMHOGigantic
This answer works well and doesn't require mocks. Thanks!Faydra
Have you gotten this to work with ant 4? I had a similar solution that worked well, but after upgrading it fails to find the option..Hersh
Although I don't see the other solution as intrinsically wrong, I also prefer this solution as it would be closer to the real-world scenario. Thanks for sharing this, this helped me and my colleague solve something we were bumping our heads against for a while with no success in simulating the selection.Autolycus
Appreciate this solution! Using the new screen import from '@testing-library/react', this can be simplified even further: (tried to show the function, but comment formatting doesn't really allow for it)Sample
Just outstanding solution. By the way the waitforElement() is now deprecated. I did it like: await screen.findByText(itemText);Determine
This answer should be the solution.Olli
await selectItem('Label', 'Option'); what I need to pass in Label and Option ?Wolverhampton
I think, mocking is a better option for a simple use, as we should be __ that react-select covered with tests on their side.Fecundity
F
31

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.

Fugazy answered 8/4, 2019 at 15:16 Comment(10)
@GiorgioPolvara-Gpx While I get the approach you are suggesting I am curious to know if that goes actually against the guiding principles of Testing Library. The lib encourages to test what the final user actually interacts with (so to me is more an integration/functional test rather that an unit test). In your approach you are mocking the external dependency (which is good for a unit test) but if the dependency gets updated there is the change to have a successful test on a failing software. What are your thoughts about it?Momentum
@GiorgioPolvara-Gpx I read your blog and I'm using the react-select/async so I used jest.mock("react-select/async",... but I get a Unable to find an element by: [data-testid="select"] when trying fireEvent.change(getByTestId("select"), { target: { value: "foo" } }); I have a render(<MySearchEngine />) and it's like the getByTestId is looking into it instead of the jest.mock block. What have I missed ? thxBrittbritta
It looks like you're either not mocking correctly or not rendering the select. Have you tried to run debug() to see what's on the page?Fugazy
One should have absolutely no confidence in their component's test if they mock the component to this extent. I highly recommend NOT going with this approach. You're testing a completely different component in this situation.Predominance
It's a tradeoff between ease of testing and coverage. If your application is for 90% the select don't mock it. But, if you only use the select in some of your pages I don't think spending the time to figure out how to test the select is worth it. Of course ymmvFugazy
I replaced jest.mock("react-select/async",... with jest.mock("../Shared/LocalitySelect.jsx",... LocalitySelect.jsx being my component that renders the react-select/async component itself. Not sure I'm on the right track here but I now get TypeError: Cannot read property 'map' of undefined at the beginning of {loadOptions.map(({ label, value }) => ( Now how/where do I populate loadOptions if that makes any sense ?...Brittbritta
It's hard for me to help you from here. Open a new question either here or on the official Spectrum pageFugazy
@GiorgioPolvara-Gpx i disagree that you should mock the third party library. if that library changes/breaks, i want to know about it (without necessarily reading the changelog/release notes), and tests are how that is going to happen.Hair
polvara.me/posts/testing-a-custom-select-with-react-testing-library/Bleeding
Another big downside of this approach is that it will force you to always have a label and a value in all components, even if your domain model does not require it... This way should not be takenAssert
D
31

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!

Dudek answered 28/4, 2020 at 0:47 Comment(4)
works like a charm, eve with Formik and chakra-ui embedded react-selectSpiritualist
good stuff react-select-event, I've been struggling with testing react-select properlyRomaic
react-select is an awesome package if you want something out of the box. Unfortunately, accessibility and testing are painful. Also, it brings emotion to the project, which is not light, Switched to downshift a year ago and will never look back. It requires a little setup, but the result is lighter, easier to test, and accessible out of the box.Dudek
@Dudek I costumized it without using emotion, just normal CSS modulesDisulfiram
C
16

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');
});
Campman answered 26/2, 2020 at 20:56 Comment(3)
I like this answer most, no need to install extra packages is always a plusAncel
this does not work with the latest react-selectCouncillor
This is why I don't like testing-library's philosophy. I'd rather just use Enzyme-Mocha properlyDesexualize
M
5

An easy way to test is by doing what the user should do

  • Click on the select field.
  • Click on one of the items in the dropdown list.
function CustomSelect() {

  const colourOptions = [
    { value: 'orange', label: 'Orange', color: '#FF8B00' },
    { value: 'yellow', label: 'Yellow', color: '#FFC400' }
  ]

  return <Select 
    aria-label="my custom select" 
    options={colourOptions}
    //... props  
  />
}
import { act, render, screen } from '@testing-library/react'; 
import userEvent from '@testing-library/user-event';
// another imports

test('show selected item...', async () => {
  const { getByText, getByLabelText } = render(<CustomSelect />);

  expect(getByText('Orange')).not.toBeInTheDocument();
  
  const myCustomSelect = getByLabelText(/my custom select/i);
  await act(async () => userEvent.click(myCustomSelect));

  const selectedItem = getByText('Orange');
  await act(async () => userEvent.click(selectedItem));

  expect(getByText('Orange')).toBeInTheDocument();
});
Mckellar answered 25/7, 2022 at 16:47 Comment(0)
A
1

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");
Alviani answered 14/6, 2021 at 15:24 Comment(0)
P
1

Because I wanted to test a component that wrapped react-select, mocking it with a regular <select> element wouldn't have worked. So I ended up using the same approach that the package's own tests use, which is supplying a className in props and then using it with querySelector() to access the rendered element in the test:

const BASIC_PROPS: BasicProps = {
  className: 'react-select',
  classNamePrefix: 'react-select', 
  // ...
};

let { container } = render(
  <Select {...props} menuIsOpen escapeClearsValue isClearable />
);
fireEvent.keyDown(container.querySelector('.react-select')!, {
  keyCode: 27,
  key: 'Escape',
});
expect(
  container.querySelector('.react-select__single-value')!.textContent
).toEqual('0');
Pentyl answered 2/6, 2022 at 20:48 Comment(0)
B
0
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') )

Benedetta answered 25/3, 2020 at 19:0 Comment(3)
where did await wait() come from?Coastline
wait() is from react testing library only. better if we combine fireEvent in act ().Benedetta
fireEvent doesn't need to be wrapped in act()Dudek
D
0

This solution worked for me.

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

Hope it might help strugglers.

Decaffeinate answered 19/8, 2020 at 20:19 Comment(2)
react-select doesn't pass any data-testid to any of its children elements, and you can't do so by providing it by yourself. Your solution works for regular select HTML elements, but I'm afraid it won't work for react-select lib.Bunny
@StanleySathler correct, this will not work for react-select, but only an HTML selectCrossfertilization
J
0

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.

Josi answered 28/10, 2020 at 0:22 Comment(0)
M
0

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')
Molina answered 16/2, 2022 at 17:31 Comment(0)
M
0

To anyone out there - I got mine to select by doing fireEvent.mouseDown on the option instead of click.

Matrona answered 27/10, 2022 at 22:5 Comment(0)
H
0

In my case I used react-select with react-testing-library and redux-form, faced various issues in the different methods I tried, the only one that worked:

  // target the <input/> of your element
  const dropdown = screen.getByTestId('my-select-test-id').querySelector('input')

  // opens up the options, notice the "mousedown" event
  fireEvent(
    dropdown,
    new MouseEvent('mousedown', {
      bubbles: true,
      cancelable: true,
    })
  )

  const dropdownOption = await screen.findByText('my option text')

  // selects the option we want, notice again the "mousedown" event
  fireEvent(
    dropdownOption,
    new MouseEvent('mousedown', {
      bubbles: true,
      cancelable: true,
    })
  )
Hafnium answered 5/7, 2023 at 18:29 Comment(0)
P
0

I have tried the following mock, and it has worked for me.

jest.mock('react-select', () => {
  return {
    __esModule: true,
    default: jest.fn((props) => (
      <div
        data-testid={props['data-testid']}
        data-props={JSON.stringify({
          ...props,
          placeholder: expect.anything(),
        })}
      />
    )),
  }
})
Piquant answered 26/10, 2023 at 18:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.