10
votes

In my Ember app, I currently have a model that has a findResults function that returns a promise that is wrapping a Google Places library to fetch results for auto-completion. To use this in my UI, I setup a PromiseMixin controller. I instruct the controller to watch the searchText value, and when that changes I update the controller's promise value to be the promise returned by the findResults function, but with the new value from searchText. This works nicely when I'm playing with the app in the browser, however when I run my acceptance tests the test seems to finish before the promise is returned and therefore the tests fail. I'll include the relevant files below.

I'm not sure how to tell Ember to wait for the promise to resolve during testing.

app/services/google-autocomplete-location.js

import Ember from "ember";

var googleAutocompleteLocation = Ember.Object.extend({
  placeId: null,
  description: null
});

googleAutocompleteLocation.reopenClass({
  findResults: function(query) {
    var self = this;
    var promise = new Ember.RSVP.Promise(function(resolve, reject) {
      var autocompleteService = new google.maps.places.AutocompleteService();

      return autocompleteService.getPlacePredictions({ input: query },
        function(predictions, status) {
          if (status !== google.maps.places.PlacesServiceStatus.OK) {
            Ember.run(null, reject, status);
          }
          else {
            Ember.run(null, resolve, self._decorateGoogleResults(predictions));
          }
        });
    });

    return promise;
  },

  _decorateGoogleResults: function(predictions) {
    var locations = [];

    predictions.forEach(function(prediction) {
      locations.push(
        googleAutocompleteLocation.create({
          placeId: prediction.place_id,
          description: prediction.description
        })
      );
    });


    return locations;
   }
});

export default googleAutocompleteLocation;

app/controllers/index.js

import Ember from "ember";
import GoogleLocation from "../services/google-location";
import GoogleAutocompleteLocation from '../services/google-autocomplete-location';

export default Ember.ArrayController.extend(Ember.PromiseProxyMixin, {
  searchText: '',
  map: null,
  mapUrl: null,

  actions: {
    submit: function() {
      return this.transitionToRoute('entries.new');
    }
  },

  highlightedResult: function() {
    if (this.get('model').length) {
      return this.get('model')[0];
    } else {
      return null;
    }
  }.property('model'),

  setMap: (function() {
    if (this.get('highlightedResult') === null) {
      return this.set('map', null);
    } else {
      if (this.get('map') === null) {
        return this.set('map', GoogleLocation.create({
          mapContainer: Ember.$('.maps-info'),
          placeId: this.get('highlightedResult').placeId
        }));
      } else {
        return this.get('map').set('placeId', this.get('highlightedResult').placeId);
      }
    }
  }).observes('highlightedResult'),

  searchTextChanged: (function() {
    if (this.get('searchText').length) {
      this.set('promise',
        GoogleAutocompleteLocation.findResults(this.get('searchText')));
      console.log(this.get('promise'));
    } else {
      this.set('model', []);
    }
  }).observes('searchText')
});

tests/acceptance/create-new-entry-test.js

test('finding a location', function() {
  expect(1);
  visit('/');
  click('.location-input input');
  fillIn('.location-input input', "Los Angeles, CA");

  andThen(function() {
    var searchResult = find('.search-results ul li:first a').text();

    equal(searchResult, 'Los Angeles, CA, United States');
  });
});
4
I've opened an issue on this ember testing behavior here, feel free to chime in: github.com/emberjs/ember.js/issues/10578DanF

4 Answers

15
votes

The best way to go about this is likely to register your own async test helper. I've prepared a JSBin with a simulation of your code and a solution here: http://jsbin.com/ziceratana/3/edit?html,js,output

The code used to create the helper is this:

Ember.Test.registerAsyncHelper('waitForControllerWithPromise', function(app, controllerName) {
  return new Ember.Test.promise(function(resolve) {

    // inform the test framework that there is an async operation in progress,
    // so it shouldn't consider the test complete
    Ember.Test.adapter.asyncStart();

    // get a handle to the promise we want to wait on
    var controller = app.__container__.lookup('controller:' + controllerName);
    var promise = controller.get('promise');

    promise.then(function(){

      // wait until the afterRender queue to resolve this promise,
      // to give any side effects of the promise resolving a chance to
      // occur and settle
      Ember.run.schedule('afterRender', null, resolve);

      // inform the test framework that this async operation is complete
      Ember.Test.adapter.asyncEnd();
    });
  });
});

And it would be used like so:

test('visiting / and searching', function() {
  expect(1);
  visit('/');
  click('.location-input input');
  fillIn('.location-input input', "Los Angeles, CA");
  waitForControllerWithPromise('index'); // <-- simple & elegant!
  andThen(function(){
    var searchResult = find('.search-results ul li:first').text();
    equal(searchResult, 'Los Angeles, CA, United States');
  });
});

In ember-testing, an async-helper will automatically wait on previous promises and subsequent async helpers will wait on it when the test is executed. For excellent background on this, see Cory Forsyth's Demystifying Async Testing

10
votes

I'm new to this stuff and have been having similar difficulties today. I found that andThen will only wait for promises created using Ember Test promises,

var promise = Ember.Test.promise(function (resolve, reject) {...});

and not those where the promise is instantiated directly, i.e.

var promise = new Ember.RSVP.Promise(function (resolve, reject) {...});

Ember.Test.promise returns a new Ember.RSVP.Promise, but also makes the step of setting Ember.Test.lastPromise to the promise instance before returning it. Maybe the answer here is for you to set Ember.Test.lastPromise to the promise you're waiting on?

Incidentally I also had to use stop() and start() in my case to prevent the test exiting before the second assert was called. I also needed to wrap the second assert in a run.next call to give the properties/DOM a chance to update:

test('shows content when unresolved promise resolves true', function() {
  expect(2);

  var resolveTestPromise;
  var testPromise = Ember.Test.promise(function (resolve) {
    resolveTestPromise = resolve;
  });

  // creates the component instance, stubbing the security service and template properties
  var component = this.subject({
    securityService: CreateMockSecurityService(testPromise),
    template: Ember.Handlebars.compile('<div id="if-may-test-div" />')
  });

  // appends the component to the page
  var $component = this.append();

  // our div shouldn't be visible yet
  equal($component.find('div#if-may-test-div').length, 0);

  stop();
  Ember.run.later(null, function () {resolveTestPromise(true);}, 1000);

  andThen(function () {
    Ember.run.next(function () {
      // div should be visible now
      equal($component.find('div#if-may-test-div').length, 1);
      start();
    });
  });
});

Hope that helps!

1
votes

I cannot reproduce your problem in JSBin but have you tried to stop() and start(). In your case:

test('finding a location', function() {
  expect(1);
  stop();
  visit('/')
   .click('.location-input input')
   .fillIn('.location-input input', "Los Angeles, CA")
   .then(function() {
     var searchResult = find('.search-results ul li:first a').text();
     equal(searchResult, 'Los Angeles, CA, United States');
     start();
   });
});
1
votes

In case you're waiting on a promise that won't resolve but reject, here is a patch to catch the error and still pass in the andThen ember test helper.

Ember.Test.registerAsyncHelper('waitForControllerWithPromise', function(app, controllerName) {
  return new Ember.Test.promise(function(resolve, reject) {

    // inform the test framework that there is an async operation in progress,
    // so it shouldn't consider the test complete
    Ember.Test.adapter.asyncStart();

    // get a handle to the promise we want to wait on
    var controller = app.__container__.lookup('controller:' + controllerName);
    var promise = controller.get('promise');

    promise.then(function(){
      // wait until the afterRender queue to resolve this promise,
      // to give any side effects of the promise resolving a chance to
      // occur and settle
      Ember.run.schedule('afterRender', null, resolve);

      // inform the test framework that this async operation is complete
      Ember.Test.adapter.asyncEnd();
    }).catch(function() {
      // when the promise is rejected, resolve it to pass in `andThen()`
      Ember.run.schedule('afterRender', null, resolve);
      Ember.Test.adapter.asyncEnd();
    });
  });
});