srph
srph

Reputation: 1322

Mocha - Testing Promise, `done` never gets called in Promise

I'm trying to test a spy that would be called in the .then block of a promise, but the done in the then block doesn't seem to be executed at all.

I'm getting timeout of 2000ms exceeded.

Here's what I'm testing (async):

/**
 * Passed down to the LoginForm component to
 * handle form submission.
 */
_submitHandler(data) {
  return function(evt) {
    evt.preventDefault && evt.preventDefault();
    evt.stopPropagation && evt.stopPropagation();

    return request('post', 'auth', data)
      .then((res) => {
        AuthActions.login();
        return res;
      })
  }
}

Here's my test:

describe('when it succeeds', () => {
  it('should login', (done) => {
    sinon.spy(AuthActions, 'login');

    Instance._submitHandler({})({})
      .then((res) => {
        console.log('Called!!!');
        expect(AuthActions.login.called).to.equal(true);
        AuthActions.login.restore();
        done();
      }, done);
  });
});

I'm using Karma to run my tests; Chai and Sinon.

Upvotes: 2

Views: 632

Answers (2)

morewry
morewry

Reputation: 4450

I also had this issue, the cause was trying to respond to the XHR before the connection was opened, e.g.

This code will throw INVALID_STATE_ERR - 0 from FakeXMLHttpRequest.setResponseHeaders:

describe("get", function () {

  beforeEach(function () {
    this.xhr = sinon.useFakeXMLHttpRequest();
    this.xhr.onCreate = function (request) {
      request.respond(200, null, "");
    };
  });

  afterEach(function () {
    this.xhr.restore();
  });

  it("should make a request", function () {
    myObject.get("testurl");
  });

});

This code works:

describe("get", function () {

  beforeEach(function () {
    this.xhr = sinon.useFakeXMLHttpRequest();
    this.requests = [];
    this.xhr.onCreate = function (request) {
      this.requests.push(request);
    };
  });

  afterEach(function () {
    this.xhr.restore();
  });

  it("should make a request", function () {
    myObject.get("testurl");
    this.requests[0].respond(200, null, "");
  });

});

Reading the documentation, which itself does show the same technique of pushing to a requests array, I had subconsciously and inaccurately came away with the impression that onCreate, despite its name, was more like "on request".

myObject.get = function (url) {
  var http = new XMLHttpRequest(); 
  // the XHR instance exists
  // readyState is 0
  // onCreate runs now
  http.open("GET", url); 
  http.send(); 
  // readyState is 1
  // can call setResponseHeaders and respond 
}

The result is that you have to put your respond code after you call the method that runs XMLHttpRequest's send, as:

myObject.get("testurl");
this.requests[0].respond(200, null, "");

I was using the respond method, but the same is true for setResponseHeaders(respond calls setResponseHeaders)--in your test it was called too early.

Upvotes: 2

srph
srph

Reputation: 1322

I've finally solved this issue after hours. It looks like the then block wasn't being called because an exception was being thrown due to xhr.

Let me elaborate. I was using sinon's FakeXMLHttpRequest, like so:

var requests, xhr;
beforeEach(() => {
  requests = [];
  xhr = sinon.useFakeXMLHttpRequest();
  xhr.onCreate = (req) => {
    req.setResponseHeaders({ /* */ });
    requests.push(req);
  }
});

By putting a console.log on the catch block, I found out that I was getting the error was INVALID_STATE_ERR EXCEPTION 0. This leads me to the conclusion that xhr was the problem all along.

And then I found out about sinon's fakeServer, and used it instead (but I don't think this is actually the solution to this problem). Not really related, but I also used sandbox here because saved me from writing countless of .restore for stubs, etc.

describe('when it succeeds', () => {
  var sandbox, server, Instance;
  beforeEach(() => {
    sandbox = sinon.sandbox.create();
    sandbox.useFakeServer();
    server = sandbox.server;
    server.respondWith([200, { "Content-Type": "application/json" }, JSON.stringify({ data: { token: '' }})]);
    sandbox.spy(AuthActions, 'login');
    Instance = TestUtils.renderIntoDocument(<Component />);
  });
  afterEach(() => {
    sandbox.restore();
  });
  it('should login', (done) => {
    Instance._submitHandler({})({})
      .then(() => {
        expect(AuthActions.login.called).to.equal(true);
        done();
      }, done);

    setTimeout(() => {
      server.respond();
    }, 0);
  });
});

Upvotes: 1

Related Questions