javascriptreactjsjestjsreact-testing-library

Solving act() warnings in React Testing Library


I am trying to test my components and the tests are passing, however I keep getting act warnings about pieces of state that are updating in my terminal: " Warning: An update to Bird inside a test was not wrapped in act(...). "

The warning is referring to the const handleRemoveBird = () => { setShowModal(true); }; state update.

Is this something so do with the state being linked to the Modal component and it's updating outside of the test? If that's the case (or not), what do I need to await/waitFor to fix the act warning?

My test:

import { screen, waitFor } from "@testing-library/react";
import user from "@testing-library/user-event";
import { renderWithProviders } from "../utils/utils-for-tests";
import Bird from "../components/Bird";

const modalContainerMock = document.createElement("div");
modalContainerMock.setAttribute("class", "modal-container");
document.body.appendChild(modalContainerMock);

const birdImgMock = jest.mock("../../components/BirdImg", () => {
  return () => {
    return "Bird Img Component";
  };
});

const renderComponent = (bird) => {
  renderWithProviders(<Bird bird={bird} />);
};

const bird = {
  id: "1",
  name: "blue tit",
  number: "5",
};

test("shows modal before removal", async () => {
  renderComponent(bird);
  const removeButton = await screen.findByTestId(`removeButton-${bird.id}`);
  expect(removeButton).toBeInTheDocument();

  user.click(removeButton);

  await waitFor(() => {
    expect(screen.getByTestId("modal")).toBeInTheDocument();
  });
});
...

My component:

import { useState } from "react";
import { RxCross2 } from "react-icons/rx";
import { useRemoveBirdMutation } from "../store";
import Panel from "./Panel";
import Button from "./Button";
import Modal from "./Modal";
import Sightings from "./Sightings";
import BirdImg from "./BirdImg";

function Bird({ bird }) {
  const [removeBird, removeBirdResults] = useRemoveBirdMutation();
  const [showModal, setShowModal] = useState(false);

  const handleRemoveBird = () => {
    setShowModal(true);
  };
  const handleClose = () => {
    setShowModal(false);
  };
  const handleDelete = () => {
    removeBird(bird);
    setShowModal(false);
  };

  const actionBar = (
    <>
      <Button primary onClick={handleClose}>
        Keep
      </Button>

      <Button secondary onClick={handleDelete}>
        Delete
      </Button>
    </>
  );

  const modal = (
    <Modal actionBar={actionBar} onClose={handleClose}>
      <p>Are you sure you want to delete this bird?</p>
    </Modal>
  );
  return (
    <>
      <Panel
        primary
        data-testid="bird"
        key={bird.id}
        className="grid grid-cols-5 md:gap-5"
      >
        <div className="col-span-2">
          <BirdImg name={bird.name} />
        </div>

        <div className="col-span-2 grid content-around -ml-4 md:-ml-8">
          <h4 className="capitalize text-lg font-bold md:text-3xl ">
            {bird.name}
          </h4>
          <div>
            <Sightings bird={bird} />
          </div>
        </div>
        <div className="col-span-1 justify-self-end">
          <Button
            onClick={handleRemoveBird}
            remove
            loading={removeBirdResults.isLoading}
            data-testid={`removeButton-${bird.id}`}
          >
            <RxCross2 />
          </Button>
        </div>
      </Panel>
      {showModal && modal}
    </>
  );
}
export default Bird;


Solution

  • one solution that works for me, you probably need to wrap the click event inside waitFor.

    test("shows modal before removal", async () => {
      renderComponent(bird);
      const removeButton = await screen.findByTestId(`removeButton-${bird.id}`);
      expect(removeButton).toBeInTheDocument();
    
      await waitFor(async () => {
        await user.click(removeButton);
      });
    
      expect(screen.getByTestId("modal")).toBeInTheDocument();
    });