Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

on change for Material UI Select component not triggered #322

Closed
dimosmera opened this issue Mar 15, 2019 · 43 comments
Closed

on change for Material UI Select component not triggered #322

dimosmera opened this issue Mar 15, 2019 · 43 comments

Comments

@dimosmera
Copy link

  • react-testing-library version: 4.1.3
  • react version: 16.4.1
  • node version: 11.10.1
  • npm (or yarn) version: 6.7.0

Relevant code or config:

    const select = await waitForElement(() =>
      getByTestId("select-testid")
    );

    select.value = "testValue";
    fireEvent.change(select);
   <Select
       className={classes.select}
       onChange={this.handleSelectChange}
       value={selectedValue}
       inputProps={{
         id: "select-id",
         "data-testid": "select-id"
       }}
   >

What you did:

I am trying to fire the onChange method of the Material UI Select.

What happened:

onChange won't fire.
Also tried with

select.dispatchEvent(new Event('change', { bubbles: true }));
@finreinhard
Copy link

The same occurs with the Boostrap(reactstrap) Input Checkbox

@finreinhard
Copy link

finreinhard commented Mar 15, 2019

According to this issue (#275), you should use the onClick event, not the onChange Event, but on reactstrap its not supported right now :/ I hope react-testing-library can implement the onChange function to it like browsers has it.

Edit: While inspecting this, I found out, that onClick isn't supported by react-testing-library as well.

@kentcdodds
Copy link
Member

that onClick isn't supported by react-testing-library as well.

That's not true. fireEvent.click exists.

If a "checkbox" component doesn't handle a click event then that component is inaccessible. This would be a bug in that component and should be fixed.

As for the original post, do this:

    const select = await waitForElement(() =>
      getByTestId("select-testid")
    );

    fireEvent.change(select, {target: {value: 'testValue'}});

Good luck!

@dimosmera
Copy link
Author

As for the original post, do this:

    const select = await waitForElement(() =>
      getByTestId("select-testid")
    );

    fireEvent.change(select, {target: {value: 'testValue'}});

Good luck!

This by itself did not work. That being said, both of the following do work:

    select.value = "testValue";
    fireEvent.change(select, {target: {value: "testValue"}});

or

    select.value = "testValue";
    fireEvent.change(select);

I personally prefer the second option.

Something to note for future readers: If you are using a Material UI select, none of the above will work. You'll have to use the native version of the Select element.
For future reference: https://stackoverflow.com/questions/55184037/react-testing-library-on-change-for-material-ui-select-component

@anandthanki1
Copy link

anandthanki1 commented Mar 29, 2019

Hey @kentcdodds, Can you please brief us on how we can efficiently use react-testing-library with React Material-UI components ? I faced a similar issue of while testing the Material UI select(Not native select) onChange function.

This is the codeSandBox link: https://codesandbox.io/s/q94q9z1849

In the codeSandBox code I have similar Select component which I am using. For your info, the dropdown which appears in Select component is a Paper component. Best regards!!!

@ashoksudani
Copy link

ashoksudani commented Apr 9, 2019

Hi @kentcdodds

Any help on how can we make testing select possible for onchange event when native is not true?

@weyert
Copy link
Contributor

weyert commented Apr 9, 2019

Sure, happy to have a look at it but your CodeSandbox is missing any unit tests at the moment

@anandthanki1
Copy link

Hey @weyert, please feel free to add unit test cases in the Codesandbox example. Thank you.

@weyert
Copy link
Contributor

weyert commented Apr 26, 2019

I assume you already have some unit test cases you tried? The code sandbox you linked doesn't even have react-testing-library as a dependency. I would also suggest to look at https://stackoverflow.com/a/55576660

@anandthanki1
Copy link

anandthanki1 commented Apr 26, 2019

Hey @weyert , I have updated the codesandbox example and have wrote unit test for it. I would like to thank you for taking a look at this issue.

Here is the link for updated codesandbox: https://codesandbox.io/s/q94q9z1849

Please feel free to edit it or suggest improvements on it.

@weyert
Copy link
Contributor

weyert commented Apr 26, 2019

Try this: https://codesandbox.io/embed/94pm1qprmo
I have moved the data-testid to the inputProps-prop of the Select of ControlledOpenSelect-component also I have written a unit test for you which clicks on Thirty and ensures a onChange-props got triggered (I added the onChange to your comp).

Not sure, why it doesn't work in the browser but when you download it and test locally the unit test is successful. I hope it helps!

@ashoksudani
Copy link

ashoksudani commented Apr 26, 2019

Thanks @weyert @anandthanki1
My tests are getting passed through your suggested way of selecting button and then clicking on element found by text. perfect.

@anandthanki1
Copy link

Thank you @weyert

@TidyIQ
Copy link

TidyIQ commented May 6, 2019

The same issue occurs for Input components as well.

fireEvent.change(input, { target: { value: "Valid input value" } }); does nothing.

@weyert
Copy link
Contributor

weyert commented May 6, 2019

What if you actually type it with something like UserEvents type-helper method?

@kentcdodds
Copy link
Member

Maybe @oliviertassinari can help with this

@kentcdodds
Copy link
Member

It would probably be good to have a few examples for testing MaterialUI components because we regularly get reports about difficulties with that library and I don't use it so I don't know why it's so uniquely difficult.

@oliviertassinari
Copy link

oliviertassinari commented May 6, 2019

The solution shared by @weyert looks great to me: https://codesandbox.io/s/94pm1qprmo, thanks! The non-native variation of the select component relies on click/keyboard interactions. You can't fire a DOM change event. The input element present is only here to support HTML POST forms.

@TidyIQ for the Input component, let's move to #359.

@watadarkstar
Copy link

watadarkstar commented May 24, 2019

Similar to what is in codesandbox worked for me:

    const { getByText, getAllByRole, getByTestId, container } = render(
      <Form />,
    );
    const selectNode = getByTestId('select-button-text');
    const selectButton = getAllByRole('button')[0];
    expect(selectButton).not.toBeNull();
    expect(selectNode).not.toBeNull();
    UserEvent.click(selectButton);
    await waitForElement(() => getByText('Custom Text'), { container });
    const itemClickable = getByText('Custom Text');
    UserEvent.click(itemClickable);
    getByTestId('custom1');

@davidgilbertson
Copy link

davidgilbertson commented Feb 3, 2020

I had the problem that the text I want to select is elsewhere on the page, so I needed to target the 'dropdown' directly. Also I wanted it as a separate function, ideally not using the getByText etc. returned by render().

// myTestUtils.js
import {within, waitForElementToBeRemoved} from '@testing-library/react';
import UserEvent from '@testing-library/user-event';

export const selectMaterialUiSelectOption = async (element, optionText) =>
    new Promise(resolve => {
        // The the button that opens the dropdown, which is a sibling of the input
        const selectButton = element.parentNode.querySelector('[role=button]');

        // Open the select dropdown
        UserEvent.click(selectButton);

        // Get the dropdown element. We don't use getByRole() because it includes <select>s too.
        const listbox = document.body.querySelector('ul[role=listbox]');

        // Click the list item
        const listItem = within(listbox).getByText(optionText);
        UserEvent.click(listItem);

        // Wait for the listbox to be removed, so it isn't visible in subsequent calls
        waitForElementToBeRemoved(() => document.body.querySelector('ul[role=listbox]')).then(
            resolve,
        );
    });

Edit: added async, since material-ui can take a tic to remove the listbox.

@kentcdodds
Copy link
Member

Hey @davidgilbertson,

Cool! This would be awesome for https://github.com/kentcdodds/react-testing-library-examples

@davidgilbertson
Copy link

Okey doke I'll do a PR tonight. FYI I updated the above to use querySelector instead of getByRole - it seems that getByRole('listbox') will include a <select> on the page without that role, does that sound right?

@eps1lon
Copy link
Member

eps1lon commented Feb 4, 2020

will include a <select> on the page without that role, does that sound right?

It does have a role implicitly. However, listbox is caused by a bug that is caused by an outdated dependency. Should be fixed soon.

@sateesh-p
Copy link

sateesh-p commented Feb 19, 2020

I had the problem that the text I want to select is elsewhere on the page, so I needed to target the 'dropdown' directly. Also I wanted it as a separate function, ideally not using the getByText etc. returned by render().

// myTestUtils.js
import {within} from '@testing-library/react';
import UserEvent from '@testing-library/user-event';

export const selectMaterialUiSelectOption = (element, optionText) => {
    // The the button that opens the dropdown, which is a sibling of the input
    const selectButton = element.parentNode.querySelector('[role=button]');

    // Open the select dropdown
    UserEvent.click(selectButton);

    // Get the dropdown element. We don't use getByRole() because it includes <select>s too.
    const listbox = document.body.querySelector('ul[role=listbox]');

    // Click the list item
    const listItem = within(listbox).getByText(optionText);
    UserEvent.click(listItem);
};

@davidgilbertson Solution is great.But the listbox is still present in jsdom even after value selected.When I am trying to open other dropdown in same testcase the previous listbox is showing instead of selected one.

@vikeen
Copy link

vikeen commented Feb 19, 2020

@sateesh-p, I had this issue as well. No fix, but I was able to just validate an onChange function callback.

@davidgilbertson, seriously thank you. This was a huge pain in the butt.

@davidgilbertson
Copy link

@sateesh-p and @vikeen I came across the same problem, the trick was to wait for the element to be removed (as far as I could work out, it happens almost immediately). I've updated the code in my original comment above #322 (comment)

So you need to call it like await selectMaterialUiSelectOption(el, 'text'); which is a pain but I couldn't see another way.

@sateesh-p
Copy link

@sateesh-p and @vikeen I came across the same problem, the trick was to wait for the element to be removed (as far as I could work out, it happens almost immediately). I've updated the code in my original comment above #322 (comment)

So you need to call it like await selectMaterialUiSelectOption(el, 'text'); which is a pain but I couldn't see another way.

That's great @davidgilbertson .It's working as expected now.Thanks

@bjunix
Copy link

bjunix commented Mar 25, 2020

Thanks for sharing the snippet @davidgilbertson! I couldn't make it work with my code, but I modified it slightly and the following works for me:

export const selectMaterialUiSelectOption = async (
  container: HTMLElement,
  selectElement: HTMLElement,
  optionText: string,
) => {
  userEvent.click(selectElement);
  const listbox = await within(document.body).findByRole('listbox');
  const listItem = await within(listbox).findByText(optionText);

  userEvent.click(listItem);
  await within(container).findByText(optionText);
};

@mukherjee96
Copy link

Something to note for future readers: If you are using a Material UI select, none of the above will work.

Here's the solution which worked for me. You can mock the component in Jest and implement it as a native element for the duration of the test.

MySelect.js

<Select
  id="demo-simple-select"
  value={age}
  onChange={handleChange}
>
  <MenuItem value={10}>Ten</MenuItem>
  <MenuItem value={20}>Twenty</MenuItem>
  <MenuItem value={30}>Thirty</MenuItem>
</Select>
  1. Create a folder called __mocks__ in the same folder as MySelect.js
  2. Next, copy MySelect.js into __mocks__/MySelect.js
  3. Implement native version of Material UI Select component here.

__mocks__/MySelect.js

<Select
  native
  id="demo-simple-select"
  value={age}
  onChange={handleChange}
>
  <option value={10}>Ten</option>
  <option value={20}>Twenty</option>
  <option value={30}>Thirty</option>
</Select>

In MySelect.test.js, use jest.mock('path/to/original/MySelect.js') at the top.

MySelect.test.js

// imports

jest.mock('path/to/original/MySelect.js')

// tests

Go ahead and use your original MySelect.js for testing. Jest will automatically find your mocked implementation and substitute it.

@peternewnham
Copy link

For anyone coming across this issue like I did and is having trouble removing the list box from jsdom, Material UI uses ReactTransitionGroup under the hood for fading out the the options. You can disable the ReactTransitionGroup transitions by adding:

import { config } from 'react-transition-group';
config.disabled = true;

which solved the issue for me and removed the listbox immediately on clicking an item.

@Paul-Vandell
Copy link

Thx to have share all of this information it help a lot.
Since the last release on Material ui, the previous sibling element of the select input must be trigger by the "mouseDown" method from the fireEvent.
onClick do not trigger anymore the option menu.

@mukherjee96

This comment has been minimized.

@eps1lon

This comment has been minimized.

@oliviertassinari

This comment has been minimized.

@eps1lon

This comment has been minimized.

rkennel added a commit to rkennel/testing-library-docs that referenced this issue May 23, 2020
As a new user of this library I had a difficult time determining how to test outcomes of a drop-down selection.  There is an example if you use the react-select library but very little information on the internet if you use a plain select tag.

I found Kent's comment on an issue in the react-testing-library and thought it would be helpful to others to add it to the FAQs.  testing-library/react-testing-library#322
@pereznieto
Copy link

Thx to have share all of this information it help a lot.
Since the last release on Material ui, the previous sibling element of the select input must be trigger by the "mouseDown" method from the fireEvent.
onClick do not trigger anymore the option menu.

My unit tests broke after updating MaterialUI to the latest version ("@material-ui/core": "^4.11.0"). After a few days of battling with the test, this solved the issue beautifully. Thank you!

SpiritBreaker226 added a commit to SpiritBreaker226/pokemon-rewind that referenced this issue Sep 20, 2020
Add the `selectMaterialUiSelectOption` method to Jest Helpers because
the select Material UI uses is react-select under the hood. As a result,
it is not using select form element but div elements, making it hard to
select a part from that input for testing.

`selectMaterialUiSelectOption` finds text you want to use and makes the
selection for you.

---
Ref:

testing-library/react-testing-library#322 (comment)
SpiritBreaker226 added a commit to SpiritBreaker226/pokemon-rewind that referenced this issue Sep 21, 2020
Add the `selectMaterialUiSelectOption` method to Jest Helpers because
the select Material UI uses is react-select under the hood. As a result,
it is not using select form element but div elements, making it hard to
select a part from that input for testing.

`selectMaterialUiSelectOption` finds text you want to use and makes the
selection for you.

---
Ref:

testing-library/react-testing-library#322 (comment)
@Android3000
Copy link

I had the problem that the text I want to select is elsewhere on the page, so I needed to target the 'dropdown' directly. Also I wanted it as a separate function, ideally not using the getByText etc. returned by render().

// myTestUtils.js
import {within, waitForElementToBeRemoved} from '@testing-library/react';
import UserEvent from '@testing-library/user-event';

export const selectMaterialUiSelectOption = async (element, optionText) =>
    new Promise(resolve => {
        // The the button that opens the dropdown, which is a sibling of the input
        const selectButton = element.parentNode.querySelector('[role=button]');

        // Open the select dropdown
        UserEvent.click(selectButton);

        // Get the dropdown element. We don't use getByRole() because it includes <select>s too.
        const listbox = document.body.querySelector('ul[role=listbox]');

        // Click the list item
        const listItem = within(listbox).getByText(optionText);
        UserEvent.click(listItem);

        // Wait for the listbox to be removed, so it isn't visible in subsequent calls
        waitForElementToBeRemoved(() => document.body.querySelector('ul[role=listbox]')).then(
            resolve,
        );
    });

Edit: added async, since material-ui can take a tic to remove the listbox.

If anyone else comes across this awesome example here, but is using a multi-select, one note of caution -- multi-selects stay open when selecting options, so the waitForElementToBeRemoved will fail as it doesn't close the select.

You could easily modify this to accept multiple options (if you are looking to test clicking one to many items), and then at the end prior to the waitForElementToBeRemoved, you can simulate an "escape" keypress on the listbox to close it:
userEvent.type(listbox, "{esc}");

This will then properly await on the close of the listbox before proceeding.

@OviavoEmir
Copy link

@davidgilbertson i am very new in testing and this issue just came out. can i ask you how should i implement this to my test. i did expect(selectMaterialUiSelectOption).tohavebeencalled(1) but it clearly is wrong way. it would be awesome if you help me out

@oliviertassinari
Copy link

We have fixed this issue in https://github.com/mui-org/material-ui/blob/b5a27b18db60138d7b65a02d04f8162d074f32be/packages/material-ui/src/Select/Select.test.js#L1059-L1078

@zzgab
Copy link

zzgab commented Sep 3, 2022

Using Material UI 5.10.3:

import { fireEvent, render, screen, within } from '@testing-library/react';
import { MenuItem, Select } from '@mui/material';

describe('MUI Select Component', () => {
  it('should have correct options an handle change', () => {
    const spyOnSelectChange = jest.fn();

    const { getByTestId } = render(
      <div>
        <Select
          data-testid={'component-under-test'}
          value={''}
          onChange={(evt) => spyOnSelectChange(evt.target.value)}
        >
          <MenuItem value="menu-a">OptionA</MenuItem>
          <MenuItem value="menu-b">OptionB</MenuItem>
        </Select>
      </div>
    );

    const selectCompoEl = getByTestId('component-under-test');

    const button = within(selectCompoEl).getByRole('button');
    fireEvent.mouseDown(button);

    const listbox = within(screen.getByRole('presentation')).getByRole(
      'listbox'
    );

    const options = within(listbox).getAllByRole('option');
    const optionValues = options.map((li) => li.getAttribute('data-value'));

    expect(optionValues).toEqual(['menu-a', 'menu-b']);

    fireEvent.click(options[1]);
    expect(spyOnSelectChange).toHaveBeenCalledWith('menu-b');
  });
});

@01202581905
Copy link

Using Material UI 5.10.3:

import { fireEvent, render, screen, within } from '@testing-library/react';
import { MenuItem, Select } from '@mui/material';

describe('MUI Select Component', () => {
  it('should have correct options an handle change', () => {
    const spyOnSelectChange = jest.fn();

    const { getByTestId } = render(
      <div>
        <Select
          data-testid={'component-under-test'}
          value={''}
          onChange={(evt) => spyOnSelectChange(evt.target.value)}
        >
          <MenuItem value="menu-a">OptionA</MenuItem>
          <MenuItem value="menu-b">OptionB</MenuItem>
        </Select>
      </div>
    );

    const selectCompoEl = getByTestId('component-under-test');

    const button = within(selectCompoEl).getByRole('button');
    fireEvent.mouseDown(button);

    const listbox = within(screen.getByRole('presentation')).getByRole(
      'listbox'
    );

    const options = within(listbox).getAllByRole('option');
    const optionValues = options.map((li) => li.getAttribute('data-value'));

    expect(optionValues).toEqual(['menu-a', 'menu-b']);

    fireEvent.click(options[1]);
    expect(spyOnSelectChange).toHaveBeenCalledWith('menu-b');
  });
});

Thank you very much, I have done my problem with your solution

@matheusrizzo1
Copy link

I had the problem that the text I want to select is elsewhere on the page, so I needed to target the 'dropdown' directly. Also I wanted it as a separate function, ideally not using the getByText etc. returned by render().

// myTestUtils.js
import {within, waitForElementToBeRemoved} from '@testing-library/react';
import UserEvent from '@testing-library/user-event';

export const selectMaterialUiSelectOption = async (element, optionText) =>
    new Promise(resolve => {
        // The the button that opens the dropdown, which is a sibling of the input
        const selectButton = element.parentNode.querySelector('[role=button]');

        // Open the select dropdown
        UserEvent.click(selectButton);

        // Get the dropdown element. We don't use getByRole() because it includes <select>s too.
        const listbox = document.body.querySelector('ul[role=listbox]');

        // Click the list item
        const listItem = within(listbox).getByText(optionText);
        UserEvent.click(listItem);

        // Wait for the listbox to be removed, so it isn't visible in subsequent calls
        waitForElementToBeRemoved(() => document.body.querySelector('ul[role=listbox]')).then(
            resolve,
        );
    });

Edit: added async, since material-ui can take a tic to remove the listbox.

Thank you @davidgilbertson, @bjunix and @zzgab! Had to change it to use 'fireEvent' instead of 'userEvent' to work for me (using material-ui/core 4.11.3):

// myTestUtils.js
import { fireEvent, within } from '@testing-library/react';

const selectMaterialUiSelectOption = async (element, optionText) => {
  // The the button that opens the dropdown, which is a sibling of the input
  const selectButton = element.parentNode.querySelector('[role=button]');

  // Open the select dropdown
  fireEvent.mouseDown(selectButton);

  // Get the dropdown element
  const listbox = await within(document.body).findByRole('listbox');

  // Click the list item
  const listItem = await within(listbox).findByText(optionText);

  fireEvent.click(listItem);
};

@aotalvaros
Copy link

@matheusrizzo1 Tienes algún ejemplo que puedas compartir de como lo hiciste, porfa?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests