Setting a timeout handler on a promise in angularjs Setting a timeout handler on a promise in angularjs angularjs angularjs

Setting a timeout handler on a promise in angularjs


First, I would like to say that your controller implementation should be something like this:

$scope.qPromiseCall = function() {    var timeoutPromise = $timeout(function() {      canceler.resolve(); //aborts the request when timed out      console.log("Timed out");    }, 250); //we set a timeout for 250ms and store the promise in order to be cancelled later if the data does not arrive within 250ms    var canceler = $q.defer();    $http.get("data.js", {timeout: canceler.promise} ).success(function(data){      console.log(data);      $timeout.cancel(timeoutPromise); //cancel the timer when we get a response within 250ms    });  }

Your tests:

it('Timeout occurs', function() {    spyOn(console, 'log');    $scope.qPromiseCall();    $timeout.flush(251); //timeout occurs after 251ms    //there is no http response to flush because we cancel the response in our code. Trying to  call $httpBackend.flush(); will throw an exception and fail the test    $scope.$apply();    expect(console.log).toHaveBeenCalledWith("Timed out");  })  it('Timeout does not occur', function() {    spyOn(console, 'log');    $scope.qPromiseCall();    $timeout.flush(230); //set the timeout to occur after 230ms    $httpBackend.flush(); //the response arrives before the timeout    $scope.$apply();    expect(console.log).not.toHaveBeenCalledWith("Timed out");  })

DEMO

Another example with promiseService.getPromise:

app.factory("promiseService", function($q,$timeout,$http) {  return {    getPromise: function() {      var timeoutPromise = $timeout(function() {        console.log("Timed out");        defer.reject("Timed out"); //reject the service in case of timeout      }, 250);      var defer = $q.defer();//in a real implementation, we would call an async function and                              // resolve the promise after the async function finishes      $timeout(function(data){//simulating an asynch function. In your app, it could be                              // $http or something else (this external service should be injected                              //so that we can mock it in unit testing)        $timeout.cancel(timeoutPromise); //cancel the timeout          defer.resolve(data);      });      return defer.promise;    }  };});app.controller('MainCtrl', function($scope, $timeout, promiseService) {  $scope.qPromiseCall = function() {    promiseService.getPromise().then(function(data) {      console.log(data);     });//you could pass a second callback to handle error cases including timeout  }});

Your tests are similar to the above example:

it('Timeout occurs', function() {    spyOn(console, 'log');    spyOn($timeout, 'cancel');    $scope.qPromiseCall();    $timeout.flush(251); //set it to timeout    $scope.$apply();    expect(console.log).toHaveBeenCalledWith("Timed out");  //expect($timeout.cancel).not.toHaveBeenCalled();   //I also use $timeout to simulate in the code so I cannot check it here because the $timeout is flushed  //In real app, it is a different service  })it('Timeout does not occur', function() {    spyOn(console, 'log');    spyOn($timeout, 'cancel');    $scope.qPromiseCall();    $timeout.flush(230);//not timeout    $scope.$apply();    expect(console.log).not.toHaveBeenCalledWith("Timed out");    expect($timeout.cancel).toHaveBeenCalled(); //also need to check whether cancel is called  })

DEMO


The behaviour of "failing a promise unless it is resolved with a specified timeframe" seems ideal for refactoring into a separate service/factory. This should make the code in both the new service/factory and controller clearer and more re-usable.

The controller, which I've assumed just sets the success/failure on the scope:

app.controller('MainCtrl', function($scope, failUnlessResolvedWithin, myPromiseService) {  failUnlessResolvedWithin(function() {    return myPromiseService.getPromise();  }, 250).then(function(result) {    $scope.result = result;  }, function(error) {    $scope.error = error;  });});

And the factory, failUnlessResolvedWithin, creates a new promise, which effectively "intercepts" a promise from a passed in function. It returns a new one that replicates its resolve/reject behaviour, except that it also rejects the promise if it hasn't been resolved within the timeout:

app.factory('failUnlessResolvedWithin', function($q, $timeout) {  return function(func, time) {    var deferred = $q.defer();    $timeout(function() {      deferred.reject('Not resolved within ' + time);    }, time);    $q.when(func()).then(function(results) {      deferred.resolve(results);    }, function(failure) {      deferred.reject(failure);    });    return deferred.promise;  };});

The tests for these are a bit tricky (and long), but you can see them at http://plnkr.co/edit/3e4htwMI5fh595ggZY7h?p=preview . The main points of the tests are

  • The tests for the controller mocks failUnlessResolvedWithin with a call to $timeout.

    $provide.value('failUnlessResolvedWithin', function(func, time) {  return $timeout(func, time);});

    This is possible since 'failUnlessResolvedWithin' is (deliberately) syntactically equivalent to $timeout, and done since $timeout provides the flush function to test various cases.

  • The tests for the service itself uses calls $timeout.flush to test behaviour of the various cases of the original promise being resolved/rejected before/after the timeout.

    beforeEach(function() {  failUnlessResolvedWithin(func, 2)  .catch(function(error) {    failResult = error;  });});beforeEach(function() {  $timeout.flush(3);  $rootScope.$digest();});it('the failure callback should be called with the error from the service', function() {  expect(failResult).toBe('Not resolved within 2');});   

You can see all this in action at http://plnkr.co/edit/3e4htwMI5fh595ggZY7h?p=preview


My implementation of @Michal Charemza 's failUnlessResolvedWithin with a real sample.By passing deferred object to the func it reduces having to instantiate a promise in usage code "ByUserPosition". Helps me deal with firefox and geolocation.

.factory('failUnlessResolvedWithin', ['$q', '$timeout', function ($q, $timeout) {    return function(func, time) {        var deferred = $q.defer();        $timeout(function() {            deferred.reject('Not resolved within ' + time);        }, time);        func(deferred);        return deferred.promise;    }}])            $scope.ByUserPosition = function () {                var resolveBy = 1000 * 30;                failUnlessResolvedWithin(function (deferred) {                    navigator.geolocation.getCurrentPosition(                    function (position) {                        deferred.resolve({ latitude: position.coords.latitude, longitude: position.coords.longitude });                    },                    function (err) {                        deferred.reject(err);                    }, {                        enableHighAccuracy : true,                        timeout: resolveBy,                        maximumAge: 0                    });                }, resolveBy).then(findByPosition, function (data) {                    console.log('error', data);                });            };