Search code examples
reactjsjestjsreact-testing-library

React Testing Library with userEvent.click wrong act() warning


I have several tests written with Jest and React Testing Library. They all mock fetch and use a userEvent.click call to fire a submit button which makes a fetch request. State gets updated in the component and I make my assertions. I am using a useEffect hook to populate a data array but it only runs on initial load given that I'm passing an empty dependency array to it. All of my tests currently pass. If I run them all together, I get a wrong act() error that stems from useEffect:

Warning: It looks like you're using the wrong act() around your test interactions.
Be sure to use the matching version of act() corresponding to your renderer:

// for react-dom:
import {act} from 'react-dom/test-utils';
// ...
act(() => ...);

// for react-test-renderer:
import TestRenderer from react-test-renderer';
const {act} = TestRenderer;
// ...
act(() => ...);

However, when I run just one of them alone, I don't get the warning. I can run any one of them on their own and I get no warning. I only get the warning when I run two or more tests together.

My tests are:

describe("CartDetail", () => {
  test("Order is submitted when user clicks Place Order button.", async () => {
    global.fetch = jest.fn().mockImplementationOnce(() =>
      Promise.resolve({
        status: 200,
      })
    );
    
    renderComponent();

    await act(async () => {
      userEvent.click(await screen.findByRole("button", { name: "Place Order" }));
    });

    expect(screen.queryByText("Your meal order was successfully processed.")).toBeInTheDocument();
  });

  test("Error message is displayed to user when order fails with a 400.", async () => {
    global.fetch = jest.fn().mockImplementationOnce(() =>
      Promise.resolve({
        status: 400,
      })
    );
    
    renderComponent();

    await act(async () => {
      userEvent.click(await screen.findByRole("button", { name: "Place Order" }));
    });

    expect(screen.queryByText("Please confirm that you are ordering at least one of each meal you have in your cart.")).toBeInTheDocument();
    userEvent.click(screen.getByLabelText("Close alert"));
  });

  test("Error message is displayed to user when API fails.", async () => {
    global.fetch = jest.fn().mockRejectedValueOnce(() =>
      Promise.reject({
        status: 500,
      })
    );
    
    renderComponent();

    await act(async () => {
      userEvent.click(await screen.findByRole("button", { name: "Place Order" }));
    });

    expect(screen.queryByText("Your order failed.")).toBeInTheDocument();
    userEvent.click(screen.getByLabelText("Close alert"));
  });
});

I've read that you don't have to wrap userEvent in act() because it already is under the hood. However, if I don't wrap it in act, my tests fail and throw:

Warning: An update to CartDetail inside a test was not wrapped in act(...).
    
When testing, code that causes React state updates should be wrapped into act(...):
    
act(() => {
  /* fire events that update state */
});
/* assert on the output */

Even if I comment out my assertions, my tests pass (of course) but I still get the wrong act() warning. The problem is coming directly from:

await act(async () => {
  userEvent.click(await screen.findByRole("button", { name: "Place Order" }));
});

I don't understand how the issue stems from useEffect when it executes on initial load and doesn't run ever again, including when the button is clicked via userEvent.click(). I've tried using waitFor() instead and that yields the same exact results. I've scoured the internet and nothing has gotten me closer than this. This GitHub thread mentions that it's a known issue but it's a bit old now so I don't know if it's still valid.


Solution

  • I was finally able to track down a workaround. This GitHub post mentions wrapping userEvent() calls in act() which I've already done. The workaround to the Warning: It looks like you're using the wrong act() around your test interactions. console error is to add the following code just after the userEvent() call and before the assertions:

    await new Promise((resolve) => setTimeout(resolve, 0));
    

    My updated tests now look like this:

    test("Order is submitted when user clicks Place Order button.", async () => {
        global.fetch = jest.fn().mockImplementationOnce(() =>
          Promise.resolve({
            status: 200,
          })
        );
        
        renderComponent();
    
        await act(async () => {
          userEvent.click(await screen.findByRole("button", { name: "Place Order" }));
        });
    
        await new Promise((resolve) => setTimeout(resolve, 0));
    
        expect(screen.queryByText("Your meal order was successfully processed.")).toBeInTheDocument();
      });
    

    The wrong act() warning is now gone.