The dreaded act(...)
warning is really annoying, but actually really important. I've yet to see it be wrong (even when I thought it must be!). Every time it's pointing out something that is important that I'm missing in my test. In this lesson we'll identify a missing piece to our test and use act to make sure we avoid that in the future.
This is based on my blog post: Fix the "not wrapped in act(...)" warning
Instructor: [0:00] Here we have a user form class component and it's rendering a form that accepts a username. Then when you click submit, it handles that submit with an async function. Here we're preventing the default. We get the new username that they submitted and then we set our status depending.
[0:17] Then we call update username with that new username. When that promise resolves, then we set our status to fulfilled, or set our status to rejected if the prompt is rejected. Then based on that status, we have a saving message show up or we display the error message.
[0:33] We already have a test for this. Here we're verifying that this component calls the update username prompt with the new username. Here we're verifying that after the user has typed into that input and clicked on the submit button.
[0:47] If we pop up our test, we'll see that our tests are passing. Now we have a function component version of the same class component already written right here in username form. It's a complete refactor meaning the previous API and behavior is exactly the same. It's just using a function component with a hook.
[1:05] Let's go ahead and try our test with this version of our component. If we save this and open up our test, we're going to get this warning that, "An update to UsernameForm inside a test was not wrapped in act."
[1:18] Now, our test is passing, but we're getting this warning and it's cluttering up our terminal output. It's a little bit confusing because this was just a straight-up refactor. We weren't testing implementation details and so we would expect this test to continue to pass without any changes necessary.
[1:33] What's happening here is React is trying to warn us that something happened to our component, when we weren't expecting anything to happen at all. We're supposed to wrap every interaction we make with our component in an act function call, so that React know that we expect our component to perform some updates.
[1:50] When we don't do that and there are updates, React will give us this warning that some unexpected updates happened. This helps us to avoid some bugs.
[2:00] For example, if we switch back to our class version and we save this, our tests are passing again. Then, we'll go back to our class. What happens if I were to make a typo here or accidentally remove this line of code that updates the status to be fulfilled after the username has been updated?
[2:19] Well, if we look down in here what would happen is we remain in a pending state for our status, because that was the last status we set it to. Therefore, this saving would be there indefinitely. That's not at all the behavior that we want.
[2:33] If I save this and open up our test again, we're going to see that our tests are passing, even though we've broken our application code. It's not entirely obvious that we've missed some update to our state, which impacts the experience of the user.
[2:47] Whereas, if we go to our test and use the function version of our component, then we're going to get this warning indicating to us, after our test is finished, there is something happening that was unexpected and needs to be handled.
[3:01] One way to fix this is to say, "Hey, let's wait until the promise that's returned from this update username is finished before we say that the test is done." Let's come down here and we'll take the promise that we're returning from our handle update username and we'll make that a variable. We'll call it Promise.
[3:18] Then at the end of our test, we'll await that promise. We can make this async. Then we can await the promise. We're still going to get that error but now we're waiting until this promise resolves before we tell Jester that our test is done.
[3:32] The next thing that we need to do is let React know that there's going to be something happening when this promise resolves. What we can do is import act from react-testing-library. Ultimately, this simply calls ReactON TestUtils.act because this is a re-export from react-dom/test-utils.
[3:52] Now we await act and we return the promise and React will batch up every state update that happens while this promise is in flight. When this promise resolves, React will flush all of those changes. So we make sure that the UI is stable.
[4:06] If we save this and open our tests, we'll notice that our tests are now passing and we don't get that act warning. Even with this, we could still make that bug come to life if we comment this out, because we haven't made any assertion on the output of our component. Specifically, we haven't asserted that the saving shows up and disappears while that promise is in flight.
[4:26] Let's go back here. Instead of waiting for this promise to resolve, why don't we wait for that saving text to be removed from the page? There's a utility from react-testing-library called waitForElementToBeRemoved. I'll pull that in and instead of await act promise, we'll say waitForElementToBeRemoved.
[4:46] Then we'll look for the screen .getByText saving. What this is going to do is react-testing-library will wait until this callback no longer returns a DOM element. Remember our test is going to fail because we have this commented out and that's exactly what we're looking for.
[5:06] If we look at our test, we're going to see that it timed out in waitForElementToBeRemoved. That's because the element is never removed. To fix this, it's simple. We just re-comment this code back in and our test is now passing and we're avoiding the act warning.
[5:20] We have a test that more accurately resembles the way that our component is intended to function, and we're avoiding the act warning because we're using this utility from react-testing-library called waitForElementToBeRemoved.
[5:32] Every async query and utility from React testing library has act built in so you normally don't have to worry about using act manually.
[5:41] In review, we started with a class component and when we refactored it to a hooks component, we started to see a new error arise because we weren't accounting for every state update that was going to happen in our component based on the interactions that we had with our component.
[5:55] We fixed that by making sure that we were accounting for those state changes and using React testing libraries built in asynchronous utilities to make sure that that code is wrapped in an act function call.