How to test side-effect when state hook is passed to a child component using React and jest?

Viewed 837

I want to test a child component, but it relies on state (hooks) from the parent.

the child receives isModalOpen and setIsModalOpen as properties. Each time isModalOpen changes, the useEffect inside Child will trigger. However, when I try to test the Child component by itself, I'm not able to force a state change triggering the useEffect hook. How can I achieve this without having to render the Parent in the test environment?

In the below example, I'm able to make the test pass by rendering Parent. I want to only render Child, and still make the test pass.

function Child({ isModalOpen, setIsModalOpen }) {
  useEffect(() => {
    console.log("is the modal open?", isModalOpen);
  }, [isModalOpen]);

  return (
    <button data-testId="myBtn" onClick={() => setIsModalOpen(!isModalOpen)}>
      Click to {isModalOpen ? "close" : "open"} modal
    </button>
  );
}

function Parent() {
    const [isModalOpen, setIsModalOpen] = useState(false);
    return <Child isModalOpen={isModalOpen} setIsModalOpen={setIsModalOpen} />;
}

Test:

it("React test", () => {
  let isModalOpen = false;
  const mockFunction = jest.fn((val) => (isModalOpen = val));

  const { queryByTestId } = render(
    <Child isModalOpen={isModalOpen} setIsModalOpen={mockFunction} />
  );

  const btnEl = queryByTestId("myBtn");

  btnEl.click();
  expect(btnEl.innerHTML).toEqual("Click to close modal");

  // -----
  // -----
  // -----
  // ----- The below code works
  // const { queryByTestId } = render(<Parent />);

  // const btnEl = queryByTestId("myBtn");

  // btnEl.click();
  // expect(btnEl.innerHTML).toEqual("Click to close modal");
});

Here is a working example of the above code snippet: https://codesandbox.io/s/jest-test-forked-cjm3r?file=/index.test.js

1 Answers

Actually to sum it up, what you're trying to test is not related to hooks if the hook is passed this into the child. Think of the child as your subject under test; in other words its the only thing you're trying to test. The "hook" by definition is irrelevant. You pass in a function and you can assert that a function that has been called with the proper value.

Which to sum it up, you have two cases

  1. When the modal is open and you click on the button "Click to close modal" button, the modal will close or otherwise known is calling your mock function with false.
  2. When the modal is closed and you click on the button "Click to close modal" button, the modal will open or otherwise known is calling your mock function with true.

I made minor edits to the test that you have above to demonstrate this. Feel free to copy and paste it in and check for yourself and let me know if you have questions.

Note: Copied and pasted from the sandbox link with minor modifications to not diverge too much from the original code.

it("closes the modal when clicking on the button", () => {
    function Child({ isModalOpen, setIsModalOpen }) {
        useEffect(() => {
            console.log("is the modal open?", isModalOpen);
        }, [isModalOpen]);

        return (
            <button data-testId="myBtn" onClick={() => setIsModalOpen(!isModalOpen)}>
                Click to {isModalOpen ? "close" : "open"} modal
            </button>
        );
    }

    // notice the mock here doesn't take arguments
    const mockFunction = jest.fn();

    const { queryByTestId } = render(
        // notice that I've set the initial value for isModal to true
        <Child isModalOpen={true} setIsModalOpen={mockFunction} />
    );

    const btnEl = queryByTestId("myBtn");

    btnEl.click();
    expect(btnEl.innerHTML).toEqual("Click to close modal");

    // the assertion here
    expect(mockFunction).toHaveBeenCalledWith(false);
});

it("opens the modal when clicking on the bottom", () => {
    function Child({ isModalOpen, setIsModalOpen }) {
        useEffect(() => {
            console.log("is the modal open?", isModalOpen);
        }, [isModalOpen]);

        return (
            <button data-testId="myBtn" onClick={() => setIsModalOpen(!isModalOpen)}>
                Click to {isModalOpen ? "close" : "open"} modal
            </button>
        );
    }

    const mockFunction = jest.fn();

    const { queryByTestId } = render(
        // notice that I've set the initial value for isModal to false
        <Child isModalOpen={false} setIsModalOpen={mockFunction} />
    );

    const btnEl = queryByTestId("myBtn");

    btnEl.click();
    expect(btnEl.innerHTML).toEqual("Click to open modal");

    // the assertion here
    expect(mockFunction).toHaveBeenCalledWith(true);
});
Related