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

4

8

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.

Candra answered 30/3, 2022 at 16:42 Comment(3)
We've had issues not wrapping calls to close dialogs and the like in act because react state ends up updated and it causes these same types of problems. Our solution is to not end a test with something like this userEvent.click(screen.getByLabelText("Close alert")); but instead after that, we have a waitFor that waits for the dialog to be closed.Brokaw
That's a great point @Brokaw but I still get the warning when I comment that out.Candra
I wish I had more advice, but every one of the things we had a problem with was a problem with state being updated and us not waiting for the update to finish. But that took a lot of debugging, and the occasional wrapping of the render itself in act =) That said, our error told us to wrap in act, not that it was the WRONG act.Brokaw
C
2

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.

Candra answered 31/3, 2022 at 18:41 Comment(0)
A
9

You probably just need to await the click call.

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

In version 14 methods like click and type return a Promise. That was different in version 13.

Armourer answered 17/8, 2022 at 21:6 Comment(0)
C
2

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.

Candra answered 31/3, 2022 at 18:41 Comment(0)
T
1

My issue was similar in that I was getting the "use act(...)" error from within an act(...) block, but the fix was slightly different.

According to this https://github.com/testing-library/user-event/issues/255#issuecomment-624847626 which states,

if you wrap userEvent calls with act, it should work. fireEvent from react-testing-library does this automatically, but fireEvent from dom-testing-library does not (because act is a react feature). user-event calls fireEvent from dom-testing-library, since this library is not react-specific.

So I replaced userEvent.click and went back to fireEvent.click, plus your fix of using a macrotask to wait until the microtask queue was empty with await new Promise(r => setTimeout(r)); and the whole thing finally worked.

The rest of my test uses await ...find*(..) instead of get or query, and always re-finds a DOM element instead of putting it into a variable, and every event is wrapped in an act via a helper function... what a mess.

It feels like @testing-library/react is counting React re-renders or something instead of waiting for React to settle down. I've had a working test fail just by adding a silly await Promise.resolve(); in an onClick handler that was already async. wtf.

Transfiguration answered 27/4, 2022 at 2:58 Comment(0)
M
0

I had same issues in different type of tests. some of them fixed by using await and waitFor for assertions

using await waitFor(()=> expect(...))

And for remaining tests fixed with updating react testing library to version 14 solved the issue

 "@testing-library/react": "^14.0.0",
Martguerita answered 1/9, 2023 at 21:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.