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. If I do a setTimeout()
of 1ms to put my expect()
calls in; the test passes.
Just wondering where the break down is? I'm sure there is some simple concept here I'm not considering. I'm thinking jsdom or enzyme does not wait around for the event handler to finish? A problem compounded by the length of time mocking fetch()
with fetch-mock
is taking (because it is asynchronous normally).
Is it resolvable without setTimeout()
, sinon
or lolex
, and if not; is it possible with simon
/ lolex
?
Tomorrow I expect I'll refactor it to avoid mocking fetch() in the tests.
Test output
</div>
1) flashes a nice message upon success
Success now!!
End of function now.
10 passing (4s)
1 failing
1) <Signup /> flashes a nice message upon success:
Uncaught AssertionError: expected { Object (root, unrendered, ...) } to have a length of 1 but got 0
at test/integration/jsx/components/signup.test.js:38:54
at _combinedTickCallback (internal/process/next_tick.js:67:7)
at process._tickDomainCallback (internal/process/next_tick.js:122:9)
Bootstrap
require('babel-register')();
require('babel-polyfill');
...
var jsdom = require('jsdom').jsdom;
var exposedProperties = ['window', 'navigator', 'document'];
global.document = jsdom('');
global.window = document.defaultView;
global.FormData = document.defaultView.FormData;
Object.keys(document.defaultView).forEach((property) => {
if (typeof global[property] === 'undefined') {
exposedProperties.push(property);
global[property] = document.defaultView[property];
}
});
global.navigator = {
userAgent: 'node.js'
};
documentRef = document;
Test
import React from 'react';
import { expect } from 'chai';
import { shallow, mount, render } from 'enzyme';
import Signup from '../../../../assets/js/components/signup.jsx';
import fetchMock from 'fetch-mock';
import sinon from 'sinon';
import 'isomorphic-fetch';
...
it("flashes a nice message upon success", function(){
fetchMock.mock("*", {body: {}});
const wrapper = shallow(<Signup />);
wrapper.find('#email').simulate('change', {target: {id: 'email', value: validUser.email}});
const signupEvent = {preventDefault: sinon.spy()};
wrapper.find('#signupForm').simulate('submit', signupEvent);
wrapper.update();
console.log(wrapper.debug());
expect(signupEvent.preventDefault.calledOnce).to.be.true;
expect(wrapper.find('.alert-success')).to.have.length(1);
expect(wrapper.find('.alert-success').text()).to.contain('Your sign up was successful!');
fetchMock.restore();
});
Component
async handleSubmit(e) {
e.preventDefault();
this.setState({ type: 'info', message: 'Sending...', shouldValidateForm: true });
let form = new FormData(this.form);
let response;
let responseJson = {};
try {
response = await fetch("/signup", {
method: "POST",
body: form
});
responseJson = await response.json();
if(!response.ok){
throw new Error("There was a non networking error. ");
}
this.setState({ type: 'success', message: 'Your sign up was successful!' });
console.log("Success now!!");
} catch(err) {
this.setState({ type: 'danger', message: "There was a technical problem. "});
}
console.log("End of function now. ");
}
...
<form method="POST" onSubmit={this.handleSubmit} ref={(form) => {this.form = form;} } id="signupForm">
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:
... fails because at that time the following line has not yet been executed:
I assume here that this
setState
call will add thealert-success
class to the message element.To see why this state has not yet been set, consider the execution flow:
This will trigger what is specified in the
onsubmit
attribute of the form:So
handleSubmit
is called. Then a state is set:... but this is not the state you need: it does not add the
alert-success
class. Then an Ajax call is made:fetch
returns a promise, andawait
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 tohandleSubmit
. In this case that means your test continues, and eventually executes:...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 thefetch
's internal implementation has a callback notifying that the response has arrived, and thus it resolves the promise. This makes the functionhandleSubmit
"wake up", as theawait
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: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)
, wherems
should be a number of milliseconds that is great enough to ensure the Ajax response has become available.It appears to me that unlike
ReactTestUtils
(which @trincot's answer is based on), enzyme'ssimulate()
is in fact synchronous. However my mocked call tofetch()
was asynchronous and the promises were resolving on the next event loop. Wrapping the expectations or assertions in asetTimeout(()=>done(), 0)
should suffice and perhaps is more reliable thansetImmediate()
which seemed to have a higher priority thansetTimeout()
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
The Component
The Test