enzymejs / enzyme

JavaScript Testing utilities for React
https://enzymejs.github.io/enzyme/
MIT License
19.96k stars 2.01k forks source link

`wrapper.act()` API proposal #2171

Open robertknight opened 5 years ago

robertknight commented 5 years ago

Is your feature request related to a problem? Please describe.

A common pattern in our Enzyme tests for components which use hooks is this:

  1. Render a component in some initial state
  2. Trigger an update via simulating a user input event, network fetch returning, timer ticking etc.
  3. Inspect the output

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.

ljharb commented 5 years ago

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.

danoc commented 5 years ago

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.

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.

robertknight commented 5 years ago

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.

ZeroDarkThirty commented 5 years ago

Is this proposal tied to https://github.com/airbnb/enzyme/issues/2073 ?