Is your feature request related to a problem? Please describe.
A common pattern in our Enzyme tests for components which use hooks is this:
For components that use useEffect or useState, step (2) needs to be wrapped in an act call in order to flush state and effects and followed by an explicit call to wrapper.update() to update the wrapper.
Describe the solution you'd like
Add a wrapper.act(callback) API which runs the provided callback, flushes any state or effect updates and then updates the wrapper. The current version of act is synchronous in React, but support has been added for React v16.9.0 for it to be async or even nested (see PR and umbrella issue.
Example usage:
// User input which triggers effects/state updates that are implemented with `useEffect`
it('focuses the input field when the button is clicked', () => {
const wrapper = mount(<Widget/>, { attachTo: container });
assert.notEqual(document.activeElement, wrapper.find('input').getDOMNode());
wrapper.act(() => {
wrapper.find('button').props().onClick();
});
assert.equal(document.activeElement, wrapper.find('input').getDOMNode());
});
// A network fetch that triggers a state update for a `useState` setter when it resolves
it('displays a loading indicator when fetching data', async () => {
const wrapper = shallow(<Feed username="jim"/>);
assert.isTrue(wrapper.exists(LoadingSpinner));
wrapper.act(async () => {
await fakeAPI.fetchData;
});
assert.isFalse(wrapper.exists(LoadingSpinner));
});
Describe alternatives you've considered
Require consumers to continue importing and calling act() from React's test utils followed by wrapper.update(). This doesn't require a lot of code, but the main issue is that I think it is not obvious for beginners that this is needed.
Caveats/issues
The act API is quite new in React. It could potentially change in future, although I think the concept of "a function which runs a callback and then flushes state updates & effects afterwards" seems pretty likely to stick around.
I'm happy to draft a PR, but I wanted to get feedback on the API first.
It's an interesting idea - one difficulty, however, is that as you point out, in 16.8, act is sync and doesn't allow a return value; in 16.9, it's more complex than that. If enzyme's going to have the API, it would ideally need to abstract over those differences.
Adapters do have an optional wrapInvoke method already that wraps this, so adding it to enzyme would be trivial once the API is designed.
For components that use
useEffectoruseState, step (2) needs to be wrapped in anactcall in order to flush state and effects and followed by an explicit call towrapper.update()to update the wrapper.
Is this definitely true? The README seems to indicate that the act wrapping happens automatically with mount.
In a recent test a colleague wrote, our calls to .simulate (on a component that uses useState) worked as it did in a pre-Hooks world. Our call to jest.runAllTimers however had to be wrapped with act and followed by an .update.
Here's a snippet from our test:
// `mount` call and a few `.simulate` calls.
// ...
// This `simulate` causes a `setTimeout`, so we'll need to `runAllTimers` right after.
button.simulate('mouseleave');
// We were getting React `act` console errors until we wrapped the `runAllTimers` in `act`.
act(() => {
jest.runAllTimers();
});
// If we were to do a `wrapper.debug()` here, it would look like the`.simulate`
// and `jest.runAllTimers` had no effect. Running `.update` gets us into the expected state.
wrapper.update();
// ...
Regardless, I agree that it's a bit confusing. I'd be happy to help improve the README once I better understand the issue.
Is this definitely true? The README seems to indicate that the act wrapping happens automatically with mount.
You're right, it does. I should clarify that in step (2) I was referring to updates which happen a) _after_ the initial render/mount and b) are triggered by an external event that doesn't go through an Enzyme API such as wrapper.simulate. In your example, this would include advancing timers with jest. A manual act(...) + wrapper.update() would also be required if you called an on<event> prop directly rather than using "simulate", or triggered a state update by resolving a network fetch or something like that.
Is this proposal tied to https://github.com/airbnb/enzyme/issues/2073 ?
Most helpful comment
You're right, it does. I should clarify that in step (2) I was referring to updates which happen a) _after_ the initial render/mount and b) are triggered by an external event that doesn't go through an Enzyme API such as
wrapper.simulate. In your example, this would include advancing timers with jest. A manualact(...)+wrapper.update()would also be required if you called anon<event>prop directly rather than using "simulate", or triggered a state update by resolving a network fetch or something like that.