enzyme: Simulate events returning promises?

Hi there - really enjoying the library! I have a question about testing simulate events. Many of the events in my code trigger functions that wait for promises.

onClick = async (e) => {
   data = await fetchData(e.target.value)
   this.setState({data})
}

In the live react, events clearly don’t return promises, but I’m not sure if enzyme’s simulate is also event-driven. Is there a way that I can wait for a simulate event to return a promise? I know there is an alternative, but it’s a little bit more overhead.

someFunc = async () => {
  data = await fetchData(e.target.value)
   this.setState({data})
}
onClick = (e) => {
   someFunc()
}
// test onClick calls someFunc()
// test someFunc()

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 8
  • Comments: 39 (17 by maintainers)

Commits related to this issue

Most upvoted comments

@jdgreenberger If you’ve got your fetchData mocked such that it returns an immediately resolved promise, this test approach might work for you: https://github.com/airbnb/enzyme/blob/7cd2e9a8d03d32d3518918c446906c82c68be052/test/ShallowWrapper-spec.jsx#L4181-L4188

It would rely on the fact that resolved Promise callbacks fire before the setImmediate callback. This is currently a safe assumption in v8, but less so in other JS implementations.

edit: Oops, didn’t see this was from over a month ago, sorry for the mention.

That’s how you can test async eventHandlers and stop execution of setImmediate after tests passing:

it('formSubmitHandler changes the state', () => {
    const wrapper = mount(<MyComponent />);
    const instance = wrapper.instance();

    wrapper.find('.form').simulate('submit');

    const asyncCheck = setImmediate(() => {
      wrapper.update();
      expect(instance.state).toEqual({
       ...
    });
    global.clearImmediate(asyncCheck);
  });

Since simulate doesn’t return a promise, the await only serves to delay a tick, which means you got lucky and won your race condition.

This is what worked for me:

it('renders state of File paragraph after button click', () => {
        const wrapper = shallow(<App appInit={appInit}/>);
        const statusParagraph = <p className="App-intro">status: barso</p>;
        wrapper.find('#queryServer').simulate('click');
        setImmediate(() => {
            wrapper.update();
            expect(wrapper.contains(statusParagraph)).toBe(true);
        });
});

We can use async-await instead of done() callback:

  it("Change state of 'muted' attribute when clicked.", async () => {
    const mutedState = wrapper.state().muted;
    await wrapper.simulate("click");
    expect(wrapper.state().muted).toBe(!mutedState);
  });

setImmediate for some reason didn’t worked for me. Not sure if what I tried is the best approach but any suggestions will help. Below is my code

const sleep = (ms) =>
  new Promise((resolve) => setTimeout(resolve, ms));

it('Should do something', async () => {
   // ...some code here

   // trigger click event
   wrapper.simulate("click");

   // wait and assuming every server call is mocked it shouldn't take too much time
   await sleep(20);
  
   expect(something).toEqual(nothing);
});

Returning the value returned from the handler would be confusing? It seems like the obvious thing. I rather expected it to already do that.

To be clear: I’m not asking for Enzyme to know anything about promises or async behavior. I’m just asking for .simulate() to return the value returned from the handler.

I have a case

  onPress = async () => {
    const { client, onUpload } = this.props;

    const body = await Cropper.getPhotoFromAlbum();
    const reply = await client.fetch('/upload', {
      method: 'POST',
      body: JSON.stringify({ body }),
    });

    onUpload(body);
  }

testing

  it('renders correctly', async () => {
    const onUpload = jest.fn();
    const component = shallow(
      <Component onUpload={onUpload} client={client} />,
    );

    Cropper.getPhotoFromAlbum.mockClear();
    Cropper.getPhotoFromAlbum.mockReturnValueOnce(Promise.resolve());
    fetch.mockClear();
    fetch.mockReturnValueOnce(Promise.resolve());

    await component.find('TouchableOpacity').simulate('press');
    expect(Cropper.getPhotoFromAlbum).toHaveBeenCalledTimes(1);
    expect(fetch).toHaveBeenCalledTimes(1);

    await Promise.resolve(); // added it
    expect(onUpload).toHaveBeenCalledTimes(1);
  });

await Promise.resolve(); add it will pass, thank @jwbay

Wrap the assertion in a setTimeOut

it('should set the state to "success-response" when the reject button is clicked with a success reponse', async done => {
      const mock = new MockAdapter(axios);
      mock.onPost('http://localhost:9080/cmp/api/service/v1/workflow/reject').reply(200, { success: true });

      yesRejectButton.simulate('click');

      setTimeout(() => {
        expect(wrapper.state('modalStatus')).toBe('success-response');
        done();
      });

    });

If you avoid simulate, and instead explicitly invoke the prop, you can do that on your own.

https://github.com/airbnb/enzyme/blob/master/src/ShallowWrapper.js#L620

this code should return a Promise, and then testing await component.find('TouchableOpacity').simulate('press');

just a idea 😄

@Glaadiss,

wrapper.simulate("click") does not return whatever is returned by the onClick handler. So, await in your example is not doing anything meaningful.

There still is, because you’re racing between that resolved promise, and your await.

I had some joy with async await in test functions like so:

it('should set credentials and instance details after mounting', async () => {
    // assign
    const wrapper = shallow(<MyComponent />)
    
    // act
    await wrapper.update()

    // assert
    expect(httpService.getInstance).toBeCalled()
    expect(wrapper.state().credentials).toEqual(credentials)
  })

Here httpService.getInstance returns a promise. Awaiting the wrapper update allowed the setState updates to complete.

I agree with @Peeja . It would be extremely helpful if simulate would return the value from the listener. The caller can then use that value however he wants, weather it’s a promise or any other value.

@ljharb:

Events are not appropriate to model as Promises. Events happen multiple times; a promise resolves once.

I don’t quite follow that. A single event happens once, no? You can have several clicks over time, but a single .simulate('click') is a one-time thing.

Given that the return value of React event handlers appears to be ignored, it would be nice to be able to return a Promise which resolves when all side-effects of the event have completed, for all the use cases above. (Or, to have .simulate() return whatever the handler returns, which would let you return a Promise if you wanted to.)