Search code examples
reactjstestingjestjsreact-testing-library

How to test trigger on change event by selecting data list


How to test a situation where user selects option from data list? I am asserting onChange callback on the input using @testing-library/user-event.

This is my React component:

// MyInput.js

import React from "react";

const MyInput = ({ value, list, id, onChange }) => {
  return (
    <label>
      <strong>{id}</strong>
      <div>
        <input type="text" value={value} onChange={onChange} list={id} />
        <datalist id={id} aria-label="datalist-items">
          {list.map((item) => (
            <option
              key={`item-${item.id}`}
              aria-label="data-list-item"
              value={item.value}
            />
          ))}
        </datalist>
      </div>
    </label>
  );
};

export default MyInput;

This is my failing test

// MyInput.spec.js

import React from "react";
import { render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import MyInput from "./MyInput";
import data from "./data.json";

describe("MyInput", () => {
  it("should trigger onChange with selected option", () => {
    const onChange = jest.fn();
    const list = [...data.list];
    const screen = render(<MyInput onChange={onChange} list={list} />);

    userEvent.selectOptions(screen.getByLabelText("datalist-items"), "first");

    expect(onChange).toHaveBeenCalledWith("first");
  });
});

Data provided to component:

// data.json

{
  "list": [
    { "id": 1, "value": "first" },
    { "id": 2, "value": "second" }
  ]
}

However that does not work. Test reports failure:

expect(jest.fn()).toHaveBeenCalledWith(...expected)

Expected: "first"

Number of calls: 0

  14 |     userEvent.selectOptions(screen.getByLabelText("datalist-items"), "first");
> 16 |     expect(onChange).toHaveBeenCalledWith("first");
  17 |   });
  18 | });

You can find a live example in this CodeSandbox.

I want to simulate real-life scenario where user clicks into an input, the data list is rendered and user clicks one of the options. Should I be somehow targeting the rendered datalist instead? And how?


Solution

  • The onChange event handler is used by <input type='text'> jsx, so you should use type(element, text, [options]) to writes text inside an <input> or a <textarea>. selectOptions(element, values) is used for selecting the specified option(s) of a <select> or a <select multiple> element.

    MyInput.jsx:

    import React from 'react';
    
    const MyInput = ({ value, list, id, onChange }) => {
      return (
        <label>
          <strong>{id}</strong>
          <div>
            <input type="text" value={value} onChange={onChange} data-testid="test" list={id} />
            <datalist id={id} aria-label="datalist-items">
              {list.map((item) => (
                <option key={`item-${item.id}`} aria-label="data-list-item" value={item.value} />
              ))}
            </datalist>
          </div>
        </label>
      );
    };
    
    export default MyInput;
    

    MyInput.spec.jsx:

    import React from 'react';
    import { render } from '@testing-library/react';
    import userEvent from '@testing-library/user-event';
    
    import MyInput from './MyInput';
    import data from './data.json';
    
    describe('MyInput', () => {
      it('should trigger onChange with selected option', () => {
        expect.assertions(6);
        let events = [];
        const onChange = jest.fn().mockImplementation((e) => {
          e.persist();
          events.push(e);
        });
        const list = [...data.list];
        const screen = render(<MyInput onChange={onChange} list={list} id="test" />);
        userEvent.type(screen.getByTestId('test'), 'first');
        expect(onChange).toBeCalledTimes(5);
        events.forEach((e) => {
          expect(onChange).toHaveBeenCalledWith(e);
        });
      });
    });
    

    test result:

     PASS  examples/65687415/MyInput.spec.jsx
      MyInput
        ✓ should trigger onChange with selected option (44 ms)
    
    -------------|---------|----------|---------|---------|-------------------
    File         | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
    -------------|---------|----------|---------|---------|-------------------
    All files    |     100 |      100 |     100 |     100 |                   
     MyInput.jsx |     100 |      100 |     100 |     100 |                   
    -------------|---------|----------|---------|---------|-------------------
    Test Suites: 1 passed, 1 total
    Tests:       1 passed, 1 total
    Snapshots:   0 total
    Time:        5.175 s
    

    package versions:

    "react": "^16.14.0",
    "@testing-library/react": "^11.2.2",
    "@testing-library/user-event": "^12.6.0",