Testing Asynchronous Behavior with Ember

I’ve found system testing Ember.js applications to be quite enjoyable—the Ember run loop and test helpers make tests deterministic and fast. That is, of course, when your application code lives happily within the confines of the run loop. But what happens when your application generates asynchronous behavior? How do you test that?

We ran into this problem recently using animations. Our code uses a Bootstrap modal, and our tests need to do some work after the modal is hidden. Of course, there’s a synchronization problem here since the modal animates on the way out. How could we instrument our tests to continue execution only after the modal had been fully removed?

Fortunately, Bootstrap provides a “hidden.bs.modal” event when a modal is closed, and so we were able to hook this event to resolve a promise when the modal finishes animating:

// Create a promise, store the resolve function, and set 
// the promise to a property on the route or controller.
var closed;
this.setProperty("dialogClosed", 
  new Ember.RSVP.Promise((r) => { closed = r; }));

// Create the modal
modalDiv.modal("show");

// Hook the "hidden" event.
modalDiv.one("hidden.bs.modal", () => {
  Ember.run(() => {
    // Disconnect the outlet, etc, then call closed() to resolve the promise.
    closed();
  });
});

Next we can write a helper to wait for our promise:

Ember.Test.registerAsyncHelper("waitForPromise", (app, promise) => {
  return new Ember.Test.promise((resolve) => {
    Ember.Test.adapter.asyncStart();
    promise.then(() => {
      Ember.run.schedule('afterRender', null, resolve);
      Ember.Test.adapter.asyncEnd();
    });
  });  
});

And now we can safely wait for our dialog to be hidden with something like:

let promise = app.__container__.lookup("route:application").get("dialogClosed");
click(".close-dialog-button");
waitForPromise(promise);

So if you can provide a promise that’s resolved when your asynchronous process completes, you should be able to use this mechanism to integrate the asynchronous operation with the test queue and see deterministic behavior. Hooray, deterministic behavior!