20

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.

sruthi
  • 1,809
  • 2
  • 18
  • 42
  • 1
    you could have also other issse for the createRange when trying to change the autoComplete value. put on the configuration : // https://stackoverflow.com/questions/60333156/how-to-fix-typeerror-document-createrange-is-not-a-function-error-while-testi global.document.createRange = () => ({ setStart: () => {}, setEnd: () => {}, commonAncestorContainer: { nodeName: 'BODY', ownerDocument: document, }, }); – Ido Bleicher Sep 09 '20 at 23:47
  • 1
    @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! – Andreas Berger Mar 24 '21 at 11:06
  • No problem at all :) Any time @AndreasBerger, Happy that it helped you! – Ido Bleicher Mar 24 '21 at 23:26
  • does this code work? – Gianmarco May 13 '21 at 21:04

6 Answers6

8

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.value).toEqual('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.

eugsun
  • 10,726
  • 6
  • 50
  • 69
  • 1
    Thanks i owe you! saved me. – Ido Bleicher Sep 09 '20 at 23:46
  • 1
    @Eugen can you send a link or explain why "Since the list items are not "visible in the DOM" ? thanks ! :) – Orville Oct 03 '20 at 05:00
  • Wait() is deprecated. Is there a solution someone has found without using "wait"? Perhaps Julius's solution https://stackoverflow.com/a/62589905/1473597 of rendering the component into the document.body – barnone Nov 16 '20 at 16:03
  • I tried but I get the input value I typed at the beginning and not the value of the item I selected – Gianmarco May 13 '21 at 18:31
5

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' })
Wesley
  • 59
  • 1
  • 3
3
// 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'
Sean M
  • 1,587
  • 16
  • 15
0

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

Julius Koronci
  • 371
  • 3
  • 6
0

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' } } })
  })
-2

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