2

Currently, I'm learning how to unit test with React. However, I'd like to learn it with TypeScript, so the course does not cover most errors that occur with TS.

I have a simple testing function configured with Mock Service Worker (msw):

fit("sends username, email and password to backend after clicking the button", async () => {
  let requestBody;
  const server = setupServer(
    rest.post("/api/1.0/users", (req, res, ctx) => {
      requestBody = req.body;
      return res(ctx.status(200));
    })
  );
  server.listen();

  setupAll(); // Gets the elements on the page (like button)

  userEvent.click(button);

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

  expect(requestBody).toEqual({
    username: "LegacyUser",
    email: "legacy@user.com",
    password: "P455w0rd!",
  });
});

In theory, this 'works' (it shows as a Pass in the testing list), but above it errors occur, like:

console.error
  Warning: An update to SignUpPage 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 */

So when I wrap userEvent.click(button) like act(() => userEvent.click(button)), it keeps showing this error message.

The click userEvent triggers an onSubmit handler:

const formSubmitHandler = (event: React.FormEvent<HTMLFormElement>) => {
  event.preventDefault();

  const { username, email, password } = formEntries;
  setIsApiInProgress(true);
  axios
    .post("/api/1.0/users", {
      username,
      email,
      password,
    })
    .then((response) => {
      if (response.status >= 200 && response.status < 300) {
        setIsSignUpSuccessfull(true);
        // console.log("OK", response.data);
        return response.data;
      }
      throw new Error(response.status.toString());
    })
    .catch(({ response }) => {
      console.log("CATCH", response);
    })
    .finally(() => {
      setIsApiInProgress(false);
    });
};

What am I doing wrong here? Also, is there a way to wait for a resolved promise without using timeouts? It feels kind of hacky this way.

Starfish
  • 3,344
  • 1
  • 19
  • 47

1 Answers1

2

It isn't a Typescript related issue. It's this line await new Promise((resolve) => setTimeout(resolve, 250)); that you need to wrap in act i.e. await act(() => new Promise((resolve) => setTimeout(resolve, 250))). You can read more up about it here.

Most relevant to your issue is this part:

So the act warning from React is there to tell us that something happened to our component when we weren't expecting anything to happen. So you're supposed to wrap every interaction you make with your component in act to let React know that we expect our component to perform some updates and when you don't do that and there are updates, React will warn us that unexpected updates happened. This helps us avoid bugs like the one described above.

Luckily for you and me, React automatically handles this for any of your code that's running within the React callstack (like click events where React calls into your event handler code which updates the component), but it cannot handle this for any code running outside of it's own callstack (like asynchronous code that runs as a result of a resolved promise you are managing or if you're using jest fake timers). With those kinds of situations you typically need to wrap that in act(...) or async act(...) yourself. BUT, React Testing Library has async utilities that are wrapped in act automatically!

Note how it specifically mentions how click events are handled for you (hence why wrapping the userEvent didn't solve the issue) and how promises you manage yourself are not handled (hence why we wrap it in act ourselves).

And you are correct that this is a hacky way. You can make use of RTL's async methods which are explained well in their docs.

This:

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

  expect(requestBody).toEqual({
    username: "LegacyUser",
    email: "legacy@user.com",
    password: "P455w0rd!",
  });

becomes something like this:

await waitFor(() => {
  expect(requestBody).toEqual({
    username: "LegacyUser",
    email: "legacy@user.com",
    password: "P455w0rd!",
  });
})

neldeles
  • 588
  • 1
  • 5
  • 12