skip to Main Content

I am trying to use jest and @testing-library/react to verify that a react component (which wraps an HTML input element) is calling it’s onChange callback with the correct value in response to a ChangeEvent.

Here is the react component

export const MyInput = ({ onChange, value }) => {
  const handleChange = (event) => {
    console.log(event.target.value);
    onChange(event);
  };

  return (
    <div>
      <input
        type="text"
        onChange={handleChange}
        label={"test-component"}
        data-testid={"test"}
        value={value}
      />
    </div>
  );
};

And here is the test

test("onChange should work", () => {
  const onChangeMock = jest.fn();
  let inputValue = 1;

  const { getByTestId } = render(
    <MyInput onChange={onChangeMock} value={inputValue} />
  );
  const input = getByTestId("test");

  fireEvent.change(input, { target: { value: "23" } });

  expect(onChangeMock).toHaveBeenCalled();
  expect(onChangeMock).toHaveBeenCalledWith(
    expect.objectContaining({
      target: expect.objectContaining({
        value: "23"
      })
    })
  );
});

And here is a link to a codesandbox which reproduces the problem.

The second assertion in my tests fails, because the onChangeMock‘s caller is an event whose’ target.value is equal to 1, rather than 23.

And what’s even weirder is that when logging event.target.value inside the handleChange function – it has the correct value of 23. Inspecting with a debugger also shows that the jest mock has a correct caller (event with a target value of 23 up until the debugger leaves the scope of the handleChange function). The moment I get back to the test scope and perform the assertion, the mock’s event has a target value of 1.

So I am trying to understand Why am I observing this behavior and what is causing it ?

(I assume that the event’s target gets set (by what though?) to my html input element the moment the event handler scope ends, and jest does not capture a deep clone of the event, as it was passed to the external function, but I could not find any documentation to support this)

2

Answers


  1. Your text input is not effecting changes because your code is not treating it like a Controlled Input.

    In short, the test is expecting the right things, but the component is ignoring changes.

    Try this:

    import React, {useState} from "react";
    
    export const MyInput = ({ onChange, value }) => {
      const [controlledValue, setControlledValue] = useState(value);
      const handleChange = (event) => {
        console.log(event.target.value);
        setControlledValue(event.target.value);
        onChange(event);
      };
    
      return (
        <div>
          <input
            type="text"
            onChange={handleChange}
            label={"test-component"}
            data-testid={"test"}
            value={controlledValue}
          />
        </div>
      );
    };
    

    You’ll see a similar example in Testing Library InputEvent docs

    Login or Signup to reply.
  2. React uses synthetic events instead of native DOM events, they behave similarly but not exactly the same, synthetic events are nullified after the handler handler is executed, for illustration, try this with the event handler from your code:

    const handleChange = (event) => {
      console.log(event.target.value);
      onChange(event); // 23
      setTimeout(() => {
        console.log(event.target.value);
      }, 3000); // 1
    };
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search