Search code examples
javascriptreactjsunit-testingtestingreact-testing

Why do I need waitFor or act instead of just using await in react testing?


  1. I read that act / waitFor is to wait until the DOM is updated. I've also read that Render is synchronous. I assume that I don't need to wrap a Render with act/waitFor as the DOM will always be updated by the time I do a click or a test ? Is this correct ?

  2. Next, I don't understand why I need act / waitFor ? Can't I just do a await ? For Test A, I use user.click and I saw the online sample code uses await. It seems to work well on my laptop.

    However in Test B, when I see some sample codes for fireEvent.click, they use waitFor. I tried using await, it flag me "await has not effect on this type of expression". But "await waitFor()" works. Why is this ?

Thanks very much in advance !

Test A
it('user.click', async () => {
  render(
    <BrowserRouter>
      <Notes />
    </BrowserRouter>
  );

  const checkBox = screen.getByRole('checkbox');
  await user.click(checkBox);
  const prev = screen.getByRole('button', { name: 'Prev' });
  expect(prev).toBeEnabled();
});

Test B
it('fireEvent.click', async () => {
  render(
    <BrowserRouter>
      <Notes />
    </BrowserRouter>
  );

  const checkBox = screen.getByRole('checkbox');
  await fireEvent.click(checkBox);
  const prev = screen.getByRole('button', { name: 'Prev' });
  expect(prev).toBeEnabled();
});

Solution

  • I tried using await, it flag me "await has not effect on this type of expression". But "await waitFor()" works. Why is this ?

    await is syntactic sugar for attaching callbacks to a promise; the rest of your asynchronous function will continue once the promise has resolved or rejected.

    So, for example, it doesn't make sense to write something like await 3, because 3 isn't a promise; there isn't anything to wait for. (It's actually legal to write await 3, but it doesn't really accomplish anything; you could just write 3.)

    So the reason that await user.click(...) and await waitFor(...) are both fine, whereas await fireEvent.click(...) is not, is that user.click and waitFor both return promises, whereas fireEvent.click does not. fireEvent.click just dispatches the event and returns synchronously. (Actually I'm not sure that user.click does anything inherently asynchronous, either — it doesn't seem to depend on any APIs like fetch — but presumably the Testing Library maintainers want to keep their options open in case there comes to be a real need for user.click to support something that has to be asynchronous. But waitFor genuinely has to be asynchronous no matter what, because its entire purpose is to poll a callback every so often until it succeeds.)

    1. I read that act / waitFor is to wait until the DOM is updated. I've also read that Render is synchronous. I assume that I don't need to wrap a Render with act/waitFor as the DOM will always be updated by the time I do a click or a test ? Is this correct ?

    So, there are two kinds of "asynchronous":

    • There are some things, such as applying state updates, that React does asynchronously by queuing up microtasks; but that's something that React chooses to do, and you can use act to work around it. Specifically: if you wrap those things in act(() => { ... }), then instead of enqueuing microtasks, React will instead put those tasks in a special queue that act owns, and act will execute those tasks before returning. So the end result is that the tasks are still asynchronous from the standpoint of your React component (for example, setState returns immediately without actually changing anything), but it's now synchronous from the standpoint of your test code (all those updates happen before act returns).
    • There are other things that are asynchronous because they depend on truly asynchronous APIs, such as the Fetch API, and there's no way for React to paper over that; so you can't just use act for those, but rather, you need to use await to wait for them to complete.
    • Sometimes you have both of these at once! If you have something asynchronous that then results in a call to setState (which is very common — updating the UI with information fetched asynchronously), then await isn't enough, because setState (intentionally) doesn't return a promise that you could wait for, it just enqueues some changes behind the scenes. For that case, you need to write await act(async () => { ... }). (act has special logic to make this work; it detects that the callback returned a promise, and it ensures that all of React's asynchronous logic is executed before its own promise resolves.)

    Now that you understand that, this question is simple to answer: you don't need to use act with render, because render actually calls act internally for you; and you don't need to use await with render, because rendering doesn't involve any truly asynchronous APIs. So you can just use render directly.