How to test material ui autocomplete with react testing library
Asked Answered
S

13

66

I am using material-ui autocomplete component and am trying to test it using react-testing-library

Component:

/* eslint-disable no-use-before-define */
import TextField from '@material-ui/core/TextField';
import Autocomplete from '@material-ui/lab/Autocomplete';
import React from 'react';

export default function ComboBox() {
  const [autocompleteInputValue, setAutocompleteInputValue] = React.useState('');
  const [isAutocompleteOpen, setIsAutocompleteOpen] = React.useState(false);
  const renderInput = (params: any) => <TextField {...params} label='openOnFocus: false' variant='outlined' />;

  const getTitle = (option: any) => option.title;

  const handleAutocompleteInputChange = (event: any, value: string) => {
    setAutocompleteInputValue(value);
  };

  const updateAutocompletePopper = () => {
    setIsAutocompleteOpen(!isAutocompleteOpen);
  };

  return (
    <Autocomplete
      id='autocompleteSearch'
      data-testid='autocomplete-search'
      disableClearable={true}
      renderOption={getTitle}
      getOptionLabel={getTitle}
      renderInput={renderInput}
      options={top100Films}
      clearOnEscape={true}
      onInputChange={handleAutocompleteInputChange}
      inputValue={autocompleteInputValue}
      open={isAutocompleteOpen}
      onOpen={updateAutocompletePopper}
      onClose={updateAutocompletePopper}
      style={{ width: 300 }}
      ListboxProps={{ 'data-testid': 'list-box' }}
    />
  );
}

// Top 100 films as rated by IMDb users. http://www.imdb.com/chart/top
export const top100Films = [
  { title: 'The Shawshank Redemption', year: 1994 },
  { title: 'The Godfather', year: 1972 },
  { title: 'The Godfather: Part II', year: 1974 },
  { title: 'The Dark Knight', year: 2008 },
  { title: '12 Angry Men', year: 1957 },
  { title: 'Schindlers List', year: 1993 },
  { title: 'Pulp Fiction', year: 1994 },
  { title: 'The Lord of the Rings: The Return of the King', year: 2003 },
  { title: 'The Good, the Bad and the Ugly', year: 1966 },
  { title: 'Fight Club', year: 1999 },
  { title: 'The Lord of the Rings: The Fellowship of the Ring', year: 2001 },
  { title: 'Star Wars: Episode V - The Empire Strikes Back', year: 1980 },
  { title: 'Forrest Gump', year: 1994 },
  { title: 'Inception', year: 2010 },
  { title: 'The Lord of the Rings: The Two Towers', year: 2002 },
  { title: 'One Flew Over the Cuckoos Nest', year: 1975 },
  { title: 'Goodfellas', year: 1990 },
  { title: 'The Matrix', year: 1999 },
  { title: 'Seven Samurai', year: 1954 },
  { title: 'Star Wars: Episode IV - A New Hope', year: 1977 },
  { title: 'City of God', year: 2002 },
  { title: 'Se7en', year: 1995 },
  { title: 'The Silence of the Lambs', year: 1991 },
  { title: 'Its a Wonderful Life', year: 1946 },
  { title: 'Life Is Beautiful', year: 1997 },
  { title: 'The Usual Suspects', year: 1995 },
  { title: 'Léon: The Professional', year: 1994 },
  { title: 'Spirited Away', year: 2001 },
  { title: 'Saving Private Ryan', year: 1998 },
  { title: 'Once Upon a Time in the West', year: 1968 },
  { title: 'American History X', year: 1998 },
  { title: 'Interstellar', year: 2014 },
  { title: 'Casablanca', year: 1942 },
  { title: 'City Lights', year: 1931 },
  { title: 'Psycho', year: 1960 },
  { title: 'The Green Mile', year: 1999 },
  { title: 'The Intouchables', year: 2011 },
  { title: 'Modern Times', year: 1936 },
  { title: 'Raiders of the Lost Ark', year: 1981 },
  { title: 'Rear Window', year: 1954 },
  { title: 'The Pianist', year: 2002 },
  { title: 'The Departed', year: 2006 },
  { title: 'Terminator 2: Judgment Day', year: 1991 },
  { title: 'Back to the Future', year: 1985 },
  { title: 'Whiplash', year: 2014 },
  { title: 'Gladiator', year: 2000 },
  { title: 'Memento', year: 2000 },
  { title: 'The Prestige', year: 2006 },
  { title: 'The Lion King', year: 1994 },
  { title: 'Apocalypse Now', year: 1979 },
  { title: 'Alien', year: 1979 },
  { title: 'Sunset Boulevard', year: 1950 },
  {
    title: 'Dr. Strangelove or: How I Learned to Stop Worrying and Love the Bomb',
    year: 1964,
  },
  { title: 'The Great Dictator', year: 1940 },
  { title: 'Cinema Paradiso', year: 1988 },
  { title: 'The Lives of Others', year: 2006 },
  { title: 'Grave of the Fireflies', year: 1988 },
  { title: 'Paths of Glory', year: 1957 },
  { title: 'Django Unchained', year: 2012 },
  { title: 'The Shining', year: 1980 },
  { title: 'WALL·E', year: 2008 },
  { title: 'American Beauty', year: 1999 },
  { title: 'The Dark Knight Rises', year: 2012 },
  { title: 'Princess Mononoke', year: 1997 },
  { title: 'Aliens', year: 1986 },
  { title: 'Oldboy', year: 2003 },
  { title: 'Once Upon a Time in America', year: 1984 },
  { title: 'Witness for the Prosecution', year: 1957 },
  { title: 'Das Boot', year: 1981 },
  { title: 'Citizen Kane', year: 1941 },
  { title: 'North by Northwest', year: 1959 },
  { title: 'Vertigo', year: 1958 },
  { title: 'Star Wars: Episode VI - Return of the Jedi', year: 1983 },
  { title: 'Reservoir Dogs', year: 1992 },
  { title: 'Braveheart', year: 1995 },
  { title: 'M', year: 1931 },
  { title: 'Requiem for a Dream', year: 2000 },
  { title: 'Amélie', year: 2001 },
  { title: 'A Clockwork Orange', year: 1971 },
  { title: 'Like Stars on Earth', year: 2007 },
  { title: 'Taxi Driver', year: 1976 },
  { title: 'Lawrence of Arabia', year: 1962 },
  { title: 'Double Indemnity', year: 1944 },
  { title: 'Eternal Sunshine of the Spotless Mind', year: 2004 },
  { title: 'Amadeus', year: 1984 },
  { title: 'To Kill a Mockingbird', year: 1962 },
  { title: 'Toy Story 3', year: 2010 },
  { title: 'Logan', year: 2017 },
  { title: 'Full Metal Jacket', year: 1987 },
  { title: 'Dangal', year: 2016 },
  { title: 'The Sting', year: 1973 },
  { title: '2001: A Space Odyssey', year: 1968 },
  { title: 'Singin in the Rain', year: 1952 },
  { title: 'Toy Story', year: 1995 },
  { title: 'Bicycle Thieves', year: 1948 },
  { title: 'The Kid', year: 1921 },
  { title: 'Inglourious Basterds', year: 2009 },
  { title: 'Snatch', year: 2000 },
  { title: '3 Idiots', year: 2009 },
  { title: 'Monty Python and the Holy Grail', year: 1975 },
];


Depending on the option chosen from the autocomplete, I am doing some other stuff, like rendering a chip, another component etc. But to make matters simple, initially I am only testing that, when user focuses in the input field, the pop up is shown so that later, I can click on an option in this popup and test that everything else is working as expected. I am verifying for the popup using data-testid I assigned to list box through ListboxProps prop of autocomplete:

Test:

import {
    fireEvent,
    getByRole as globalGetByRole,
    getByText as globalGetByText,
    render,
} from '@testing-library/react';
import React from 'react';
import ComboBox, { top100Films } from './AutoComplete';

test('that autocomplete works', async () => {
    const { getByTestId, getByRole, queryByRole } = render(<ComboBox />, {});

    const AutoCompleteSearch = getByTestId('autocomplete-search');
    const Input = globalGetByRole(AutoCompleteSearch, 'textbox');

    expect(queryByRole('listbox')).toBeNull();

    fireEvent.mouseDown(Input);
    const ListBox = getByRole('listbox');
    expect(ListBox).toBeDefined();
    const menuItem1 = globalGetByText(ListBox, top100Films[0].title);
    fireEvent.click(menuItem1);
    expect(queryByRole('listbox')).toBeNull();

    fireEvent.mouseDown(Input);
    const ListBoxAfter = getByRole('listbox');
    expect(ListBoxAfter).toBeDefined();
    const menuItem2 = globalGetByText(ListBoxAfter, top100Films[1].title);
    fireEvent.click(menuItem2);
    expect(queryByRole('listbox')).toBeNull();
});

But this is failing with: Unable to find an element by: [data-testid="list-box"]. What am I doing wrong?

EDIT: I fired mouseDown on Input and was successfully able to test that the popup is opened. I used listbox role instead of a data-testid to verify that the popup has opened. The same can be done with data-testid as well. Then, I chose an item from autocomplete options and the popup closed. Now, I tried to open the popup again for the 2nd time and here, it fails again. Not able to open in for the 2nd time using mouseDown event.

Scanderbeg answered 27/3, 2020 at 8:10 Comment(4)
you could have also other issse for the createRange when trying to change the autoComplete value. put on the configuration : // #60333656 global.document.createRange = () => ({ setStart: () => {}, setEnd: () => {}, commonAncestorContainer: { nodeName: 'BODY', ownerDocument: document, }, });Truckage
@IdoBleicher Thank you so much! I got the error message "Cannot read property 'nodeName' of undefined" and the solution in your posted link saved my day!Indoaryan
No problem at all :) Any time @AndreasBerger, Happy that it helped you!Truckage
does this code work?Handwriting
M
48

First of all, you need to make sure the options are not empty array, then do the following:

const autocomplete = getByTestId('autocomplete');
const input = within(autocomplete).getByRole('textbox')
autocomplete.focus()
// the value here can be any string you want, so you may also consider to 
// wrapper it as a function and pass in inputValue as parameter
fireEvent.change(input, { target: { value: 'a' } })
fireEvent.keyDown(autocomplete, { key: 'ArrowDown' })
fireEvent.keyDown(autocomplete, { key: 'Enter' })
Moller answered 4/11, 2020 at 4:22 Comment(3)
where is element defined?Homeric
My input doesn't seem to have a role attribute. I used autocomplete.closest("input") to find the element.Katiekatina
For those who use "@mui/material": "^5.5.0", within(autocomplete).getByRole('textbox') should be within(autocomplete).getByRole('combobox')Jahdal
C
23

Since the list items are not "visible" in the DOM itself you need to use a different approach.

You have to locate the autocomplete and input DOM elements on which you will trigger events.

The autocomplete is usually found in the DOM by the role attribute e.g. role="combobox" but it's best to give it a unique identifier such as data-testid="autocomplete"

The following code shows how to test item selection in autocomplete:

   const autocomplete = getByTestId('autocomplete');
   const input = within(autocomplete).querySelector('input')

   autocomplete.focus()
   // assign value to input field
   fireEvent.change(input, { target: { value: value } })
   await wait()
   // navigate to the first item in the autocomplete box
   fireEvent.keyDown(autocomplete, { key: 'ArrowDown' })
   await wait()
   // select the first item
   fireEvent.keyDown(autocomplete, { key: 'Enter' })
   await wait()
   // check the new value of the input field
   expect(input).toHaveValue('some_value')

You need to insert a value on the input element then trigger the change. After that the listbox opens which permits selecting the first value by firing the enter key. The selected value will replace the entered initial value used to search/open the autocomplete.

Chadwick answered 4/9, 2020 at 21:3 Comment(4)
@Eugen can you send a link or explain why "Since the list items are not "visible in the DOM" ? thanks ! :)Unstopped
Wait() is deprecated. Is there a solution someone has found without using "wait"? Perhaps Julius's solution https://mcmap.net/q/295917/-how-to-test-material-ui-autocomplete-with-react-testing-library of rendering the component into the document.bodyHomeric
I tried but I get the input value I typed at the beginning and not the value of the item I selectedHandwriting
getting an error Property 'querySelector' does not existSwinger
S
10
// make sure autocomplete reactions/results do not already exist
expect(screen.queryByText(/Loading/)).not.toBeInTheDocument()
expect(screen.queryByText(/Van Halen/)).not.toBeInTheDocument()

// fill out autocomplete
const faveBand = screen.getByLabelText(/Favorite Band/)
userEvent.type(faveBand, 'Van H')
expect(faveBand).toHaveValue('Van H')

// witness autocomplete working
expect(screen.getByText(/Loading/)).toBeInTheDocument()

// wait for response (i used an async Material-UI autocomplete)
// favebands is a data-testid attribute value in my autocomplete 
// component, e.g. ListboxProps={{ 'data-testid': 'favebands' }}
await waitFor(() => getByTestId('favebands'))

// verify autocomplete items are visible
expect(screen.getByText(/Van Halen/)).toBeInTheDocument()

// click on autocomplete item
const faveBandItem = screen.getByText('Van Halen')
userEvent.click(faveBandItem)

// verify autocomplete has new value
expect(faveBand).toHaveValue('Van Halen')    

I import userEvent, waitFor, and screen like so...

import { screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
Sphygmomanometer answered 29/9, 2020 at 16:13 Comment(1)
instead of await waitFor(() => getByTestId('favebands')) you can also do await screen.findByTestId('favebands');Inmate
U
2

the issue here is that autocomplete by default uses portals and renders options into body..which is not present in the rendered container, you have to render the autocomplete into document body

Uptake answered 26/6, 2020 at 7:22 Comment(1)
Could you supply an example of how to do this?Cytologist
J
2

I was able to find a solution which allows waiting for the options menu to appear and doing validations on those options.

The key steps to be able to use the options returned by the mocked API were:

  1. Click on the "Open" button that is part of the Autocomplete component
  2. waitFor() elements with role="option" to appear in the DOM

I also cast the input to HTMLInputElement since my tests are written in TypeScript.

test("sends API request on search", async () => {
  // arrange
  render(<Dropdown />);
  // act
  const autocomplete = screen.getByRole("combobox");
  const input: HTMLInputElement = within(autocomplete).getByLabelText(
    "Select a template"
  ) as HTMLInputElement;

  const searchValue = "react-boiler";
  const templateValue = "react-boilerplate";
  autocomplete.focus();
  // open autocomplete dropdown menu
  within(autocomplete).getByLabelText("Open").click();
  const options = await screen.findAllByRole("option");
  // Perform some tests specific to the options provided to the dropdown
  // expect(options).toHaveLength(7);
  // assign value to input field
  fireEvent.change(input, { target: { value: searchValue } });
  // navigate to the first item in the autocomplete box
  fireEvent.keyDown(autocomplete, { key: "ArrowDown" });
  // select the first item
  fireEvent.keyDown(autocomplete, { key: "Enter" });
  // check the new value of the input field
  expect(input.value).toEqual(templateValue);
});
Jurisdiction answered 9/3, 2022 at 14:56 Comment(0)
M
1

Here's my take on it using react-testing-library -> user-event. To make it work i had to assign data-testid to the menu options shown in the autocomplete.

    const autoComplete = screen.getByRole("combobox");

    expect(autoComplete).toBeVisible();

    const autoCompleteDropdown = screen.getByRole("button", { name: "Open" });

    // Autocomplete dropdown button.
    expect(autoCompleteDropdown).toBeVisible();
    userEvent.click(autoCompleteDropdown);

    // Autocomplete dropdown view.
    expect(screen.getByRole("presentation")).toBeVisible();

    // click on administrator menu option in autocomplete.
    userEvent.click(screen.getByTestId("option1"));
    
    // imitate click away(this is only required if you have disableCloseOnSelect is enabled.
    userEvent.click(document.body);
    expect(screen.queryByRole("presentation")).not.toBeInTheDocument();

    //Verify autocomplete shows the correct value.
    expect(screen.getByText("option1")).toBeVisible();
Meyerbeer answered 2/6, 2021 at 16:9 Comment(0)
S
1

haven't tested yet on the mui example but did on my app and the following works:

userEvent.type(screen.getByRole("textbox", {name: /attendees/i}), "Bertrand")
userEvent.click(screen.getByText(/bertrand/i))

simulate a type from userEvent (here, Bertrand, in your example: Schindlers), where the name given to screen.getByRole is the value of the label of your autocomplete (in your example: openOnFocus: false. then simulate a click on the expected text (i.e: a text that is present in your options, for example Schindlers).

NB: i have tested it on @mui/material AutoComplete not from the lab

Selfstyled answered 31/10, 2021 at 13:36 Comment(1)
the only answer who is working fine with my test case ! thanks . Just to elaborate your response in case someone wonder what it is, userEvent.type(screen.getByRole("textbox", {name: /attendees/i}), "Bertrand") Bertrand , the last argument you pass in , it's the text the user type in the autocomplete . ( since it's an autocomplete you can directly type in the input box ) Thanks again 💪Gillan
S
1

I wouldn't bother verifying if the popup is shown or not. This is taken care of by the Autocomplete component, and I expect it to work correctly. The popup is just a container of options, and the options are the ones that are important.

So for verifying the options, I could imagine two types of tests:

  • Verify that the form behaves correctly when the user selects an option.
  • Verify the correct list of options are shown as the user types.

Verify the form behaves correctly

To do this, first just click the Textbox then click the correct option.

const user = userEvent.setup();
const { findByLabelText, findByRole } = render(
  <Autocomplete 
    options={["option 1", "option 2"]} 
    renderInput={(params) => <TextField {...params} label="label" />}
  />)
user.click(getByLabelText("label"))
user.click(getByRole("option", { name: "option 1" }))

You can do this multiple times, if you want to verify some logic that occurs when the user changes between several values.

Verifying the correct list of options

If the list of options is just a static list, I wouldn't bother testing this. I would trust the Autocomplete to do its job correctly.

Where I think this type of test makes sense is if the options are fetched from a back-end, or even calculated dynamically depending on what you type. For example, a google place search.

In this case, just get all the presented options.

// Type in something
user.type(getByLabelText("label"), "opt")
// Potentially wait
// Get the now narrows down list of options.
const options = await findAllByRole("option") // or queryAllByRole

I'm making the assumption here, that only one set of options are shown. But since on Autocomplete closes its popup when a new one opens, this should work.

Be aware, that in case of retrieving data dynamically, you may need to wait in the test for the async code to have settled and updated the options list.

p.s. I wrote this after spending half a day figuring this out. All other resources, including the previous answers to this question, blog posts, etc., were too coupled to implementation details, such as triggering individual keypresses, dependencies on data-testid attributes (which should only be used as a last resort).

The intention of my test was to verify the behavior of my form. Ideally, I should have been able to use selectOption, but that didn't work for MUI autocomplete

Schou answered 21/5, 2022 at 8:13 Comment(0)
D
0

In my case, I needed to enter values in two autocomplete fields in order to enable the next page button. I modified the top answers as it wasn't working for me.

const autocompleteClient = renderResult.getByTestId('client');
const inputClient = autocompleteClient.querySelector('input');

autocompleteClient.focus();
// assign value to input field of client
fireEvent.change(inputClient!!, { target: { value: 'Client 1' } })
// presentation role is for the options of the autocomplete
await renderResult.findAllByRole('presentation');

fireEvent.keyDown(autocompleteClient, { key: 'ArrowDown' })
fireEvent.keyDown(autocompleteClient, { key: 'Enter' })

const autocompleteProposal = renderResult.getByTestId('proposal-no');
const inputProposal = autocompleteProposal.querySelector('input');

autocompleteProposal.focus();
fireEvent.change(inputProposal!!, { target: { value: 'proposal 1' } })
await renderResult.findAllByRole('presentation');

fireEvent.keyDown(autocompleteProposal, { key: 'ArrowDown' })
fireEvent.keyDown(autocompleteProposal, { key: 'Enter' })

expect(screen.getByText('Next')).toBeEnabled();
fireEvent.click(screen.getByText('Next'));
expect(screen.getByText('Generate')).toBeDisabled();

Where renderResult = render(<YourComponent/>);

Defect answered 28/2, 2022 at 18:19 Comment(0)
B
0

First of all, you need to make sure the options are not empty array and that the value you want to select is in the list, then do the following:

const autocomplete = getByTestId('autocomplete');

// Step 1 - Click on your Autocomplete Field

  fireEvent.click(autocomplete);

// Step 2 - Type your value using change event

  fireEvent.change(autocomplete, {
    target: {
      value: 'a',
    },
  });

// Step 3 - Click Enter Key using keyDown event

  fireEvent.keyDown(autocomplete, {
     key: 'Enter',
     code: 'Enter',
  });
Blueweed answered 29/12, 2023 at 11:24 Comment(0)
D
-1

You can use the onInputChange function to find out if the input has been changed.

 test('my test', () => {
    const { container } = render(
      <Autocomplete
        noOptionsText="no Option"
        getOptionLabel={(option) => option.name}
        onInputChange={(__, value) => {
          //implements logic when value is selected
        }}
        multiple
        id="my-id"
        options={[{ name: 'My name 1' }]}
        renderInput={(params) => <TextField {...params} label="My Label" />}
      />
    )
    const input = container.querySelector('#my-id')
    fireEvent.change(input, { target: { value: { name: 'My name 1' } } })
  })
Dib answered 27/11, 2020 at 4:7 Comment(0)
L
-1

i found this one as my solution

const autoComplete = getByLabelText('component-autoComplete');
const input = within(autoComplete).getByRole('textbox');

autoComplete.focus();
fireEvent.change(input, { target: { value: 'mockValue' } });
fireEvent.keyDown(autoComplete, { key: 'ArrowDown' });
fireEvent.keyDown(autoComplete, { key: 'Enter' });
expect(input).toHaveValue('mockValue');
Lamond answered 10/8, 2021 at 14:13 Comment(1)
Welcome to SO. In most cases, code-only answers suffer from a lack of explanation; consider explaining how this answers the question to add value for other users down the road.Patella
M
-3

you need go by following:

  1. click autocomplete
  2. search option
  3. select option
const label = 'label of your autocomplete'
const textBox = screen.getByRole('textbox', {
  name: label,
});

userEvent.click(textBox);

// wait for option to appear
await waitFor(() => {
  screen.getByRole('listbox');
});
// grab option
const opt = screen.getByRole('option', {
  name: /The Great Dictator/i,
});
// select it
userEvent.click(opt); 
Marquess answered 18/2, 2021 at 8:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.