good doc! some quick answers -
It seems like act() is being recommended for wrapping all state updates in React tests, but is it necessary to use it everywhere if you can use waitForElement to turn the whole test async?
This is a very good question, and something I grappled with earlier. A couple of things that stood out for me -
-
waiting for an element is indeed pretty close to what a user's experience is like; ie - a user 'waits' for the form to show itself, after which they fill it in and click a button, then 'wait' for the success screen etc. Ultimately,
act()
makes this test stronger - it'll ensure that effects, and queued promises, have been flushed before you interact with the element. wrapping waitForElement withact()
(the async version, ie), will make this invisible to the user, but with the guarantee that their UI is 'stable'. -
I couldn't assume that all tests would use
waitForElement
. For example, using timers is common for testing transitions and such. In these scenarios too, act() is useful to ensure a stable UI.
Developers need to manually choose how to group updates to resemble batch patterns the app will actually experience - is there some way to "fuzz" this and somehow trigger more batching in JSDOM automatically? Maybe by removing this maximum frame rate check in test mode?
This is just the nature of unit tests for async/"time travel" based stuff. I don't have a particularly good answer here; the 'ideal' method would be to have a whole new language and infra. And maybe an actual time machine haha. What's interesting is act
now provides a way to actually batch these actions and surface issues that would not have been apparent in tests before.
Doesn't batching promise updates gloss over some possible states that may be perceptible to the user? For example, an async call in cDM might be mocked by a Promise that resolves in 1 tick in test world, but 0.5s in real life. Squashing those updates seems wrong if there is no way to inspect the intermediate state.
Yes :D But those are usually explicit (and you'll note you have this same problem with waitForElement
btw) It works relatively well for the needs of facebook.com and their many thousand tests. I don't have a better answer without getting too philosophical about the nature of tests and mocks, but like before, I'm happy we now have an actual primitive to batch these things.
Some cases, like Promises that cause state updates, aren't currently wrappable with synchronous act() and still cause a warning - do these cases actually cause bugs if you are using something like waitForElement?
The async version should capture this warning (unless you're willing to reproduce an environment described in react-act-examples, in which case you can use jest's fake timers to 'advance' synchronously). Like I mentioned above, it'll guarantee that the ui is relatively 'stable' at that point.
Is there anything that can be done to make this less obtrusive, either on the the react-testing-library side or through lint rules, async act(), etc.? Prior to this API, there were no React-specific details required in react-testing-library other than render.
There still mostly shouldn't, especially after the async version lands. It would be interesting if there were wrappers for jest's runAllTimers/advanceTimersByTime calls as well, but not many people use it in oss, so that maybe overkill.
❤️ thank you @threepointone
cc @kentcdodds
In that case, do we need to wrap the fireEvent calls, or would the waitForElement batch everything appropriately? If that works, I'm thinking some clarity could be achieved by pushing all the async functionality into async-by-default queries: testing-library/dom-testing-library#203 while leaving other parts of the library (events, render) unwrapped. Again, contingent on this achieving the same behavior.
Same question - do we have to wrap the originating event with
act
or can we wrap the followup-query?⏳ ok
Wrapping
render
makes this less explicit, right? And you can't opt-out without reimplementing render? Does FB use a similar shared render util?Seems like a workaround we can recommend to make this more explicit would be adding timers to mocks instead of mocking highly async promises with
Promise.resolve()
(one tick).I think timers are going to be important in the pre-suspense-for-data era, in order to differentiate between side-effects that trigger high-latency (noticeable loading state) vs low-latency async requests. We can add more examples using fake timers.