I use EmberJS a lot in my day-to-day work, and one of the things I find myself constantly doing is writing acceptance tests (aka integration tests) that click through my application to ensure the various planned out user workflows are operating as expected.
However, because controller must handle user interaction and map that to necessarily async ajax (or websocket) calls, EmberJS's standard suite of acceptance test helpers are sometimes not enough to ensure proper waiting.
For example:
Suppose I have a very complex DS.Model which, at creation, must not only create itself in my rails back-end via ajax, but also communicate to a firebase server somewhere using websockets.
StationsWeighticketTrucksNewController = Ember.Controller.extend
...
actions:
finish: ->
throttle @, 200, ->
@get("truck").save()
.then (truck) ->
truck.gotoDock()
.then (truck) ->
truck.get "entryScaleIdPromise"
.then (entryScaleId) =>
Ember.assert "there is a entry scale id #{entryScaleId}", Ember.isPresent entryScaleId
@transitionToRoute "stations.station", entryScaleId
Here, the finish action represents about 3 ajax requests, 2 websocket messages, and 1 router transition. (throttle is just Ember.run.throttle with the arguments slightly reversed for coffeescript reasons).
In my acceptance test, I would have something like:
...
before (done) ->
click "finish"
andThen -> done()
it "should create the truck and whatever", ->
expect @truck
.to.whatever
Unfortunately, this test does not consistently for me because the 2 websocket from firebase are not being properly waited on by the andThen helper.
EmberJS's wait test helper is reproduced here:
// https://github.com/emberjs/ember.js/blob/master/packages/ember-testing/lib/helpers.js#L201
function wait(app, value) {
return new RSVP.Promise(function(resolve) {
// Every 10ms, poll for the async thing to have finished
var watcher = setInterval(function() {
var router = app.__container__.lookup('router:main');
// 1. If the router is loading, keep polling
var routerIsLoading = router.router && !!router.router.activeTransition;
if (routerIsLoading) { return; }
// 2. If there are pending Ajax requests, keep polling
if (Test.pendingAjaxRequests) { return; }
// 3. If there are scheduled timers or we are inside of a run loop, keep polling
if (run.hasScheduledTimers() || run.currentRunLoop) { return; }
if (Test.waiters && Test.waiters.any(function(waiter) {
var context = waiter[0];
var callback = waiter[1];
return !callback.call(context);
})) {
return;
}
// Stop polling
clearInterval(watcher);
// Synchronously resolve the promise
run(null, resolve, value);
}, 10);
});
}
Ember checks for pending routes, ajax requests, and run loops, but nowhere does it deal with other forms of synchronicity such as websockets, webrtc, image.onload, and the (lolwhoamikidding) future web-worker... so what do you do if you must test but your application depends on these other forms of async behavior?
TL;DR: make the application route aware of other forms of async behavior and monkey patch andThen to check the application controller during its polling.
# route/application.coffee
ApplicationRoute = Ember.Controller.extend
init: ->
@_super arguments...
@controllerPen = ControllerPen.create()
isBusy: Ember.computed.alias "controllerPen.isBusy"
actions:
controllerWorking: (controller) ->
@controllerPen.makeBusy controller
controllerFinished: (controller) ->
@controllerPen.makeFree controller
# utils/controller-pen.coffee
ControllerPen = Ember.Object.extend
init: ->
@busyControllers = 0
@ctrlCenter = new Ember.Map()
isBusy: Ember.computed.not "isFree"
isFree: Ember.computed.equal "busyControllers", 0
makeBusy: (ctrl) ->
return if @ctrlCenter.get(ctrl) is "busy"
@ctrlCenter.set ctrl, "busy"
@incrementProperty "busyControllers", 1
makeFree: (ctrl) ->
throw new Error freeMsg(ctrl) unless @ctrlCenter.has ctrl
return if @ctrlCenter.get(ctrl) is "free"
@ctrlCenter.set ctrl, "free"
@decrementProperty "busyControllers", 1
throw new Error negMsg if @get("busyControllers") < 0
# controllers/[all-other-controller]
StationsTruckExitController = Ember.Controller.extend AtomicMixin,
actions:
killTruck: ->
@atomically =>
@get "truck.exitScaleIdPromise"
.then (exitScaleId) =>
@get "truck"
.destroyRecord()
.then ->
exitScaleId
.then (scaleId) =>
@transitionToRoute "stations.station", scaleId
# utils/atomic.coffee
AtomicMixin = Ember.Mixin.create
isPending: Ember.computed.or "model.isPending", "model.isSaving", "isBusy"
atomically: (action) ->
return if @get "isBusy"
Ember.run =>
@send "controllerWorking", @
@set "isBusy", true
assertThenable action()
.finally =>
@set "isBusy", false
@send "controllerFinished", @
# helpers/and-then
andThenOld = andThen
andThen = (action) ->
poll applicationRoute
.until (route) -> route.get("isBusy") is false
.then ->
andThenOld -> action
Now, it's up to each controller to inform the application route whether it is busy or free, which opens the door for all kinds of different html5 async behavior