Skip to content Skip to sidebar Skip to footer

Why Does My Test Finish Before My (enzyme Simulated Event) Synchronous Event Handler?

I have a mocha based test which finishes before my onChange handler in a jsdom based enzyme test of my React component, despite that handler being synchronous using babel+ES2017. I

Solution 1:

My first answer focussed on the asynchronous nature of simulate, but from comments it became clear enzyme's implementation of that method is not asynchronous as it just calls the click handler synchronously. So this is a rewrite of my answer, focussing on the other causes for asynchronous behaviour.

This test:

expect(wrapper.find('.alert-success')).to.have.length(1);

... fails because at that time the following line has not yet been executed:

this.setState({ type: 'success', message: 'Your sign up was successful!' });

I assume here that this setState call will add the alert-success class to the message element.

To see why this state has not yet been set, consider the execution flow:

wrapper.find('#signupForm').simulate('submit', signupEvent);

This will trigger what is specified in the onsubmit attribute of the form:

onSubmit={this.handleSubmit} 

So handleSubmit is called. Then a state is set:

this.setState({ type: 'info', message: 'Sending...', shouldValidateForm: true });

... but this is not the state you need: it does not add the alert-success class. Then an Ajax call is made:

response = await fetch("/signup", {
    method: "POST",
    body: form
});

fetch returns a promise, and await will pause the execution of the function until that promise is resolved. In the mean time, execution continues with any code that is to be executed following the call to handleSubmit. In this case that means your test continues, and eventually executes:

expect(wrapper.find('.alert-success')).to.have.length(1);

...which fails. The event signalling that the pending Ajax request has a response might have arrived on the event queue, but it will only be processed after the currently executing code has finished. So after the test has failed, the promise, that was returned by fetch, gets resolved. This is because the fetch's internal implementation has a callback notifying that the response has arrived, and thus it resolves the promise. This makes the function handleSubmit "wake up", as the await now unblocks the execution.

There is a second await for getting the JSON, which again will introduce a event queue cycle. Eventually (pun not intended), the code will resume and execute the state the test was looking for:

this.setState({ type: 'success', message: 'Your sign up was successful!' });

So... for the test to succeed, it must have an asynchronous callback implemented that waits long enough for the Ajax call to get a response.

This can be done with setTimeout(done, ms), where ms should be a number of milliseconds that is great enough to ensure the Ajax response has become available.


Solution 2:

It appears to me that unlike ReactTestUtils (which @trincot's answer is based on), enzyme's simulate() is in fact synchronous. However my mocked call to fetch() was asynchronous and the promises were resolving on the next event loop. Wrapping the expectations or assertions in a setTimeout(()=>done(), 0) should suffice and perhaps is more reliable than setImmediate() which seemed to have a higher priority than setTimeout() to me (even though they are both probably executing on the same event loop).

Here is a component and test I wrote to demonstrate.

The Test Output

<Example />
updated asynchronously
onChangeError ran. 
SUCCESS SOON: Taking a break...
Setting delayed success. 
      ✓ has a rendered success message on the next event loop 
    updated synchronously
onChangeError ran. 
Setting success. 
      ✓ has a rendered success message on this loop
onChangeError ran. 
onChangeError ran. 
onChangeError ran.  
...
onChangeError ran. 
onChangeError ran. 
onChangeError ran. 
Setting success. 
      ✓ has a rendered success message on this loop despite a large simulation workload (2545ms)

   3 passing (6s)

The Component

import React from 'react';
export default class Example extends React.Component {
  constructor(props){
    super(props);
    this.onChangeError = this.onChangeError.bind(this);
    this.onChangeSuccess = this.onChangeSuccess.bind(this);
    this.onChangeDelayedSuccess = this.onChangeDelayedSuccess.bind(this);
    this.state = { message: "Initial message. " };
  }
  onChangeError(e){
    console.log("onChangeError ran. ");
    this.setState({message: "Error: There was an error. "})
  }
  onChangeSuccess(e) {
    console.log("Setting success. ");
    this.setState({message: "The thing was a success!"});
  };
  onChangeDelayedSuccess(e){
    console.log('SUCCESS SOON: Taking a break...');
    setTimeout(() =>{
      console.log("Setting delayed success. ");
      this.setState({message: "The thing was a success!"});
    }, 0);
  }
  render(){
    return(
     <div>
       <p>{ this.state.message}</p>
       <input type="text" id="forceError" onChange={this.onChangeError} />
       <input type="text" id="forceSuccess" onChange={this.onChangeSuccess} />
       <input type="text" id="forceDelayedSuccess" onChange={this.onChangeDelayedSuccess} />
     </div>
    );
  }
}

The Test

import React from 'react';
import { expect } from 'chai';
import { shallow, mount, render } from 'enzyme';
import Example from '../../../../assets/js/components/example.jsx';

describe("<Example />", function() {
  describe("updated asynchronously", function() {
    it("has a rendered success message on the next event loop ", function(done) {
      const wrapper = shallow(<Example />);
      wrapper.find('#forceError').simulate('change', {target: {value: ""}});
      wrapper.find('#forceDelayedSuccess').simulate('change', {target: {value: ""}});

      setTimeout(function(){
        expect(wrapper.find('p').text()).to.contain('The thing was a success!');
        done();
      }, 0);
    });
  });
  describe("updated synchronously", function(){
    it("has a rendered success message on this loop", function(done) {
      const wrapper = shallow(<Example />);
      wrapper.find('#forceError').simulate('change', {target: {value: ""}});
      wrapper.find('#forceSuccess').simulate('change', {target: {value: ""}});

      expect(wrapper.find('p').text()).to.contain('The thing was a success!');
      done();
    });
    it("has a rendered success message on this loop despite a large simulation workload", function(done) {
      this.timeout(100000);
      const wrapper = shallow(<Example />);
      for(var i=1; i<=10000;i++){
        wrapper.find('#forceError').simulate('change', {target: {value: ""}});
      }
      wrapper.find('#forceSuccess').simulate('change', {target: {value: ""}});

      expect(wrapper.find('p').text()).to.contain('The thing was a success!');
      done();
    });
  });
 });

Post a Comment for "Why Does My Test Finish Before My (enzyme Simulated Event) Synchronous Event Handler?"