Testing React Select component
Asked Answered
J

17

48

https://github.com/JedWatson/react-select

I would like to use React-Select react component, but I need to add tests.

I've tried several options I found with google, but nothing seems to work. I have the code below, but it's not causing a change event. I have been able to add a focus event, which adds 'is-focussed' class, but the 'is-open' class is still missing.

I have used: https://github.com/JedWatson/react-select/blob/master/test/Select-test.js as a reference

I have tried to use a change event only on the input field, but this has not helped either. I noticed there is a onInputChange={this.change} for the select.

Test

import Home from '../../src/components/home';
import { mount } from 'enzyme'

describe('Home', () => {

it("renders home", () => {

    const component = mount(<Home/>);

    // default class on .Select div
    // "Select foobar Select--single is-searchable"

    const select = component.find('.Select');

    // After focus event
    // "Select foobar Select--single is-searchable is-focussed"
    // missing is-open
    TestUtils.Simulate.focus(select.find('input'));

    //this is not working
    TestUtils.Simulate.keyDown(select.find('.Select-control'), { keyCode: 40, key: 'ArrowDown' });
    TestUtils.Simulate.keyDown(select.find('.Select-control'), { keyCode: 13, key: 'Enter' });

    // as per code below I expect the h2 to have the select value in it eg 'feaure'

});
});

Component under test

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

class Home extends Component {
constructor(props) {
    super(props);

    this.state = {
        message: "Please select option"};
    this.change = this.change.bind(this);
}

change(event) {

    if(event.value) {
        this.setState({message: event.label});
    }
}

render () {

    const options = [ {label: 'bug', value: 1} , {label: 'feature', value: 2 }, {label: 'documents', value: 3}, {label: 'discussion', value: 4}];

    return (
      <div className='content xs-full-height'>
          <div>
              <h2>{this.state.message}</h2>

              <Select
                  name="select"
                  value={this.state.message}
                  options={options}
                  onInputChange={this.change}
                  onChange={this.change}
              />

          </div>
        </div>
    );
}
}

export default Home;

Command line To run test I do:

>> npm run test

and in package.js I have this script:

"test": "mocha --compilers js:babel-core/register -w test/browser.js ./new",

Test setup

and browser.js is:

import 'babel-register';
import jsdom from 'jsdom';

const exposedProperties = ['window', 'navigator', 'document'];

global.document = jsdom.jsdom('<!doctype html><html><body></body></html>');
global.window = document.defaultView;
Object.keys(document.defaultView).forEach((property) => {
   if (typeof global[property] === 'undefined') {
       exposedProperties.push(property);
       global[property] = document.defaultView[property];
   }
});

global.navigator = {
    userAgent: 'node.js'
};

I have also tried using methods for testing outlined here: https://github.com/StephenGrider/ReduxSimpleStarter

Any help will be greatly appreciated

Journeywork answered 1/2, 2017 at 22:32 Comment(0)
L
41

This is a recurrent question. I'm sharing my own code with 100% passing tests which cover 100% of my source code.

My component looks like this

MySelectComponent({ options, onChange }) {

  return <div data-testid="my-select-component">
    <Select
      className="basic-single"
      classNamePrefix="select"
      name="myOptions"
      placeholder="Select an option"
      options={options}
      onChange={e => onChange(e)}
    />
</div>;
}

The reason I'm adding a wrapper on my Select with data-testid="my-select-component" is that the rendered options element will be available on it otherwise I can't check if a text option exist (you'll understand better when you'll see my tests).

This is a live running example and when rendering it'll show a select component with 10 options.

enter image description here

Test 1 : should render without errors

  • I render the component.

  • I search for the placeholder to be present.

Test 2 : should call onChange when the first option is selected

  • I render the component.

  • I check if my mockedOnChange is not yet called.

  • Simulate an ArrowDown event.

  • Click on the first option.

  • I check if mockedOnChange is called 1 time with the 1st option label and value.

Test 3 : should call onChange when the first option is selected then second option then the 9th one

  • I render the component.

  • I simulate a select of the first option.

  • I simulate a select of the 2nd option.

  • I simulate a select of the 9th option.

  • I check if the mockedOnChange is called 3 times with the 9th option bale and value.

Test 4 : should call onChange when filtering by input value

  • I render the component.

  • I simulate a change on the input field by typing "option 1".

  • I know, based on my mockedOptions that the filtered result will be "Mocked option 1" and "Mocked option 10".

  • I simulate 2 ArrowDown events.

  • I check that the mockedOnChange is called with the 2nd filtered option with right label and value.

Complete test file

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

afterEach(cleanup);

describe ('Test react-select component', () => {

    const mockedOptions = [
        {label: 'Mocked option 1', value: 'mocked-option-1'},
        {label: 'Mocked option 2', value: 'mocked-option-2'},
        {label: 'Mocked option 3', value: 'mocked-option-3'},
        {label: 'Mocked option 4', value: 'mocked-option-4'},
        {label: 'Mocked option 5', value: 'mocked-option-5'},
        {label: 'Mocked option 6', value: 'mocked-option-6'},
        {label: 'Mocked option 7', value: 'mocked-option-7'},
        {label: 'Mocked option 8', value: 'mocked-option-8'},
        {label: 'Mocked option 9', value: 'mocked-option-9'},
        {label: 'Mocked option 10', value: 'mocked-option-10'},
    ];

    it('should render without errors', async () => {
        const mockedOnChange = jest.fn();
        const { getByText } = render(<MySelectComponent 
            options={mockedOptions} 
            onChange={mockedOnChange} />);

        const placeholder = getByText('Select an option');

        expect(placeholder).toBeTruthy();
    });

    it('should call onChange when the first option is selected', async () => {
        const mockedOnChange = jest.fn();
        const { getByText, queryByTestId } = render(<MySelectComponent 
            options={mockedOptions} 
            onChange={mockedOnChange} />);

        const mySelectComponent = queryByTestId('my-select-component');

        expect(mySelectComponent).toBeDefined();
        expect(mySelectComponent).not.toBeNull();
        expect(mockedOnChange).toHaveBeenCalledTimes(0);

        fireEvent.keyDown(mySelectComponent.firstChild, { key: 'ArrowDown' });
        await waitForElement(() => getByText('Mocked option 1'));
        fireEvent.click(getByText('Mocked option 1'));

        expect(mockedOnChange).toHaveBeenCalledTimes(1);
        expect(mockedOnChange).toHaveBeenCalledWith({label: 'Mocked option 1', value: 'mocked-option-1'});

    });

    it('should call onChange when the first option is selected then second option then the 9th one', async () => {
        const mockedOnChange = jest.fn();
        const { getByText, queryByTestId } = render(<MySelectComponent 
            options={mockedOptions} 
            onChange={mockedOnChange} />);

        const mySelectComponent = queryByTestId('my-select-component');

        expect(mySelectComponent).toBeDefined();
        expect(mySelectComponent).not.toBeNull();
        expect(mockedOnChange).toHaveBeenCalledTimes(0);

        fireEvent.keyDown(mySelectComponent.firstChild, { key: 'ArrowDown' });
        await waitForElement(() => getByText('Mocked option 1'));
        fireEvent.click(getByText('Mocked option 1'));

        fireEvent.keyDown(mySelectComponent.firstChild, { key: 'ArrowDown' });
        await waitForElement(() => getByText('Mocked option 2'));
        fireEvent.click(getByText('Mocked option 2'));

        fireEvent.keyDown(mySelectComponent.firstChild, { key: 'ArrowDown' });
        await waitForElement(() => getByText('Mocked option 9'));
        fireEvent.click(getByText('Mocked option 9'));

        expect(mockedOnChange).toHaveBeenCalledTimes(3);
        expect(mockedOnChange).toHaveBeenCalledWith({label: 'Mocked option 9', value: 'mocked-option-9'});
    });

    it('should call onChange when filtering by input value', async () => {
      const mockedOnChange = jest.fn();
      const { getByText, queryByTestId, container } = render(<MySelectComponent 
        options={mockedOptions} 
        onChange={mockedOnChange} />);

        const mySelectComponent = queryByTestId('my-select-component');

        fireEvent.change(container.querySelector('input'), {
            target: { value: 'option 1' },
        });

        // select Mocked option 1
        fireEvent.keyDown(mySelectComponent.firstChild, { key: 'ArrowDown' });  
        // select Mocked option 10
        fireEvent.keyDown(mySelectComponent.firstChild, { key: 'ArrowDown' });

        await waitForElement(() => getByText('Mocked option 10'));
        fireEvent.click(getByText('Mocked option 10'));

        expect(mockedOnChange).toHaveBeenCalledTimes(1);
        expect(mockedOnChange).toHaveBeenCalledWith({label: 'Mocked option 10', value: 'mocked-option-10'});
    });

});

I hope that this help.

Lustrum answered 1/5, 2020 at 22:42 Comment(1)
Thanks for your answer, it works very well with @testing-library/react and the latest react-select versions. However, for anyone who is coming here, it is not recommended to test the onChange method (toHaveBeenCalled) with mocks. You should instead test it by finding if the option has been selected with getByText and queryByText methods exposed from @testing-library/react. In my case, I make sure that the placeholder is removed: expect(screen.queryByText('placeholder')).not.toBeInTheDocument() and replaced by the option: expect(screen.getByText('opt')).toBeVisible();Enenstein
D
25

I've tried both answers listed above, and still no luck.

What did work for me was:

  1. Add classNamePrefix prop - i.e list (as mentioned in the other answers) :

    <Select
       classNamePrefix='list'
       options={[
         { label: 'one', value: 'one' },
         { label: 'two', value: 'two' }
    ]}/>
    
  2. select the dropdown indicator & simulate a mouseDown => opened dropdown:

    wrapper
      .find('.list__dropdown-indicator')
      .simulate('mouseDown', {
        button: 0 
      });
    
  3. expect things to happen i.e. in my case I was checking for the number of dropdown options

    expect(wrapper.find('.list__option').length).toEqual(2);
    

    if you have control over the props being sent, you can add a menuIsOpen prop to always have the menu open (aka step 2 in the list).

To select a value from the dropdown, after opening the dropdown:

wrapper.find('.list__option').last().simulate('click', null);

then you can test either:

expect(wrapper.find('.list__value-container').text()).toEqual('two');

or

expect(wrapper.find('.list__single-value').text()).toEqual('two');
Dimaggio answered 24/10, 2018 at 20:40 Comment(4)
Setting menuIsOpen prop to true can't be done easily as it alters component behaviour. Instead your solution using classNamePrefix worked like a charm! Thank you!Mindymine
This worked but I had to change it to expect(wrapper.update().find('.list__option').length).toEqual(2); for it to work correctlyAcidulant
might be because you're checking the .length , not the .text() @fabian-enos ?:) , granted, I should've given better text examples.Dimaggio
Ah no, I think it is because I was checking for the existence of the single-value before and after a selection. I just tried it without the update() in a few places but it still seems to need it to find it correctly.Acidulant
I
23

From https://github.com/JedWatson/react-select/issues/1357

Only solution I found was to simulate a selection through key-down events:

wrapper.find('.Select-control').simulate('keyDown', { keyCode: 40 });
// you can use 'input' instead of '.Select-control'
wrapper.find('.Select-control').simulate('keyDown', { keyCode: 13 });
expect(size).to.eql('your first value in the list')
Intort answered 13/9, 2017 at 15:34 Comment(1)
Important note here is that both of those keyDown simulations are needed to trigger the change event.Unprovided
C
13

Using testing-library and v2.0

Trying to avoid using anything very specific like classNamePrefix or hacking into the way the component operates by looking for the onChange prop or whatever.

const callback = jest.fn();
const { container, getByText} = render(<Select ... onChange={callback} />);

Now we basically pretend to be a screen reader and focus, and press the down arrow.

fireEvent.focus(container.querySelector('input'));
fireEvent.keyDown(container.querySelector('input'), { key: 'ArrowDown', code: 40 });

And now click on the value you want

fireEvent.click(getByText('Option Two'));

And assert.

expect(callback).toHaveBeenCalledWith({ value: 'two', label: 'Option Two'});
Cruller answered 28/8, 2019 at 19:39 Comment(2)
Your solution does open the menu but not click on option two (not clicked at any option. It is just close the menu) @dmitriyHypesthesia
For me it works very well, all they way (option is clicked correctly). However, it's a pity we are forced to do the first part via keyboard events. It feels like we're testing only the a11y aspect, which would be a great additional test case, but not the primary one. However, I guess we can rely on react-select to have proper test coverage itself and safely use this method/workaround.Hyetograph
L
5

To add to what Keith has said, using the simulate method does seem to be the only way to exercise the functionality. However when I tried this in my solution it didn't work - I am using Typescript though so not sure if this has a bearing but I found that it is necessary to also pass the key property when simulating the event:

wrapper.find('.Select-control').simulate('keyDown', { key: 'ArrowDown', keyCode: 40 });
wrapper.find('.Select-control').simulate('keyDown', { key: 'Enter', keyCode: 13 });

I also found that setting the classNamePrefix property made it much easier to do a simple test to give me confidence that the component was responding correctly to the simulated events. When setting this prefix useful parts of the component are decorated with class names providing easy access to them (you can identify these useful class names by inspecting the elements in google dev tools). A simple Jest test:

it('react-select will respond to events correctly', () => {
    const sut = Enzyme.mount(
    <Select 
        classNamePrefix="list" 
        options={[{ label: 'item 1', value: 1 }]}
    />);

    // The intereactive element uses the suffix __control **note there are two underscores** 
    sut.find('.list__control').first().simulate('keyDown', { key: 'ArrowDown', keyCode: 40 });
    sut.find('.list__control').first().simulate('keyDown', { key: 'Enter', keyCode: 13 });

    // the selected value uses the suffix __single-value **note there are two underscores** 
    expect(sut.find('.list__single-value').first().text()).toEqual('item 1');
});
Landmark answered 2/10, 2018 at 21:33 Comment(0)
M
4

For newer version of react-select (2.x+), the above method doesn't work because react-select uses emotion. Thus, wrapper.find('.Select-control') or wrapper.find('.list__option') no longer works. react-select 2.x+ wraps the Select component inside of a state manager but you can access the onChange method of the Select component. I use the following code to trigger selection:

wrapper.find(Select).props().onChange({ value: ... })
Mantoman answered 13/3, 2019 at 4:49 Comment(4)
How do you define Select in this test ?Anthroposophy
@Anthroposophy Either import it from 'react-select' or place it inside 'Select' string.Reannareap
what are you testing if you're just manually invoking its onChange though?Panicle
For example, if you are testing the validation of a form and if it displays an error. The method in which you invoke onChange doesn't matter because that's not the point of the test. The point of the test is to test whether it displays an error when invalid value is entered (i.e. invalid email address).Mantoman
T
2

Just want to add, I actually had to add the classNamePrefix prop to the Select component otherwise I didn't get any *__control classes to latch onto.

Trichromatism answered 19/10, 2018 at 3:16 Comment(0)
C
2

In case somebody is using enzyme, this worked for me:

  wrapper.find('Select').simulate('change', {
    target: { name: 'select', value: 1 },
  });

where "select" is the name of the attribute as defined here:

  <Select
      name="select"
      ...

and "value" is the desired option's value.

Callisto answered 14/5, 2019 at 21:47 Comment(1)
In my case, It worked with wrapper.find('Select').simulate('change', {value: 1, label: 'label' });. Without wrapping it in target.Westerfield
C
2

With react-testing-library:

<Select id='myId' onChange=(list: ReactSelectOption[]) => {
                        props.changeGroupItem(
                            {
                                items: list ? list.map((item) => item.value) : [],
                            }
                        );
                    }
/>

And then in my test

const selectInput = container.querySelector(`#myId input`) as HTMLInputElement;
    fireEvent.focus(selectInput);
    fireEvent.mouseDown(selectInput);
    fireEvent.click(getByText("myValue"));
    expect(props.changeGroupItem).toHaveBeenCalledWith(
        {
            items: ["myDefaultValue", "myValue"],
        }
    );
Chokedamp answered 8/10, 2020 at 10:25 Comment(0)
B
2

There is a library to Simulate user events on react-select elements, for use with react-testing-library. Works with react select 2+.

https://www.npmjs.com/package/react-select-event

Like so:

const { getByRole, getByLabelText } = render(
  <form role="form">
    <label htmlFor="food">Food</label>
    <Select options={OPTIONS} name="food" inputId="food" isMulti />
  </form>
);
expect(getByRole("form")).toHaveFormValues({ food: "" });
 
await selectEvent.select(getByLabelText("Food"), ["Strawberry", "Mango"]);
expect(getByRole("form")).toHaveFormValues({ food: ["strawberry", "mango"] });
 
await selectEvent.select(getByLabelText("Food"), "Chocolate");
expect(getByRole("form")).toHaveFormValues({
  food: ["strawberry", "mango", "chocolate"],
});
Belton answered 14/10, 2020 at 9:26 Comment(0)
G
2

I had the same problem to test react-select, so my solution was this.

my react-select component:

      <Select
      options={options}
      placeholder="Select an Option"
    />

my test:

  it('should select an option', () => {
    const { getByText } = render(
      <MySelect/>
    );

    fireEvent.focus(getByText('Select an Option'));

    fireEvent.keyDown(getByText('Select an Option'), {
      key: 'ArrowDown',
      code: 40,
    });

    fireEvent.click(getByText("my wanted option"));
    expect(getByText("my wanted option")).toBeDefined();
  }

Garrek answered 26/7, 2022 at 22:37 Comment(0)
R
1

For those who is using enzyme v3.11.0 and react-select v3.0.8, this worked for me

component.find('Select').simulate('keyDown', { key: 'ArrowDown', keyCode: 40 });

Here is my Select. I am using it together with redux-form

<Select
    {...input}
    styles={customStyles}
    options={props.options}
    formatGroupLabel={formatGroupLabel}
    placeholder="Custom Search..."
    isSearchable={true}
    onBlur={handleBlur}
/>

Sample of the tests

it('should render dropdown on keyDown', () => {
    expect(component.find('MenuList').length).toEqual(1);
});

it('should render the correct amount of options', () => {
    expect(component.find('Option').length).toEqual(optionsLength);
});
Rhoda answered 25/2, 2020 at 16:20 Comment(0)
T
1

https://mcmap.net/q/352603/-testing-react-select-component almost works for me. I just added keyDown event for opening a select menu.

it('my test', () => {
  const { container } = getShallow();
  const elSelector = container.querySelector('.Select-input');

  expect(propsComponent.onPageSizeChange).toHaveBeenCalledTimes(0);

  // open select menu
  fireEvent.keyDown(elSelector, { keyCode: 13 });

  // choose next option
  fireEvent.keyDown(elSelector, { key: 'ArrowDown', code: 40 });

  // send the option
  fireEvent.keyDown(elSelector, { keyCode: 13 });

  expect(propsComponent.onPageSizeChange).toHaveBeenCalledTimes(1);
});
Toneless answered 28/7, 2020 at 10:27 Comment(0)
S
1
wrapper.find(ReactSelectComponent.instance().onChange(...params));
Subsocial answered 5/8, 2020 at 17:27 Comment(0)
B
1

I tried all solutions here - nothing worked for me.

I was able to solve the problem with the user-event library. Checkout the selectOptions function.

Bookstand answered 11/1, 2021 at 15:47 Comment(0)
K
0

I had a similar issue with my implementation MUI react Select component. I couldn't find a good solution other than this:

  it('changes the state when an option is checked', async () => {
    const result = renderWithContext(
      <div>
        <MySelectImplementation  />
      </div>
    );

    const selectOptions= screen.getByRole('button', {
      name: `${btnText}`,
    });

    await act(async () => {
      await fireEvent.keyDown(selectOptions.firstChild as ChildNode, {
        key: 'ArrowDown',
      });
    });

    await waitFor(() => screen.getByText('Text In My List'));
    const checkBoxes = screen.getAllByRole('checkbox');
// check the first checkbox
    fireEvent.click(checkBoxes[0]);

    expect(
      result.store.getState().optionState
    ).toEqual(['Text In My List']);
  });

Kirkuk answered 4/5, 2023 at 22:9 Comment(0)
H
0

I was still struggling with the same issue but after spending some time on it found a way to handle react-select dropdown for automation testing, you can find the solution here.

It is worth mentioning that no extra dom is needed to achieve this.

Hecklau answered 16/5, 2023 at 9:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.