Установка обработчика тайм-аута по обещанию в angularjs

Я пытаюсь установить тайм-аут в своем контроллере, чтобы, если ответ не получен в 250 мс, он должен выйти из строя. Я установил, чтобы мой unit test имел тайм-аут 10000, чтобы это условие было выполнено, может ли кто-нибудь указать мне в правильном направлении? (EDIT Я пытаюсь достичь этого, не используя службу $http, которую я знаю, обеспечивает функциональность тайм-аута)

(EDIT - мои другие юнит-тесты потерпели неудачу, потому что я не призывал к ним timeout.flush, теперь мне просто нужно получить сообщение о тайм-ауте, когда обещание undefined возвращается обещаниемService.getPromise(). Я удалил ранний код из вопроса).

promService (обещание - это переменная набора тестов, позволяющая мне использовать другое поведение для обещания в каждом наборе тестов до применения, например, отклонять в одном, успех в другом)

    mockPromiseService = jasmine.createSpyObj('promiseService', ['getPromise']);
    mockPromiseService.getPromise.andCallFake( function() {
        promise = $q.defer();
        return promise.promise;
    })

Контроллер, который тестируется -

$scope.qPromiseCall = function() {
    var timeoutdata = null;
    $timeout(function() {
        promise = promiseService.getPromise();
        promise.then(function (data) {
                timeoutdata = data;
                if (data == "promise success!") {
                    console.log("success");
                } else {
                    console.log("function failure");
                }
            }, function (error) {
                console.log("promise failure")
            }

        )
    }, 250).then(function (data) {
        if(typeof timeoutdata === "undefined" ) {
            console.log("Timed out")
        }
    },function( error ){
        console.log("timed out!");
    });
}

Тест (обычно я разрешаю или отвергаю обещание здесь, но не устанавливая его, я имитирую таймаут)

it('Timeout logs promise failure', function(){
    spyOn(console, 'log');
    scope.qPromiseCall();
    $timeout.flush(251);
    $rootScope.$apply();
    expect(console.log).toHaveBeenCalledWith("Timed out");
})

Ответы

Ответ 1

Во-первых, я хотел бы сказать, что реализация вашего контроллера должна быть примерно такой:

$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
    });
  }

Ваши тесты:

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

Другой пример с 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

  }
});

Ваши тесты аналогичны приведенному выше примеру:

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

Ответ 2

Поведение "отказ от обещания, если оно не разрешено с заданным таймфреймом", кажется идеальным для реорганизации в отдельную службу / factory. Это должно сделать код как в новом сервисе /factory, так и в контроллере более четким и более повторно используемым.

Контроллер, который я предположил, просто устанавливает успех/сбой в области:

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

И factory, failUnlessResolvedWithin создает новое обещание, которое эффективно "перехватывает" обещание от переданной функции. Он возвращает новый, который реплицирует поведение разрешения/отклонения, за исключением того, что он также отвергает обещание, если оно не было разрешено в течение таймаута:

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;
  };
});

Тесты для них немного сложны (и длинны), но вы можете видеть их в http://plnkr.co/edit/3e4htwMI5fh595ggZY7h?p=preview. Основные моменты тестов:

  • Тесты для контроллера mocks failUnlessResolvedWithin с вызовом $timeout.

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

    Это возможно, поскольку "failUnlessResolvedWithin" (намеренно) синтаксически эквивалентен $timeout и выполняется, поскольку $timeout предоставляет функцию flush для проверки различных случаев.

  • Тесты для самой службы используют вызовы $timeout.flush для проверки поведения различных случаев первоначального обещания, которое разрешается/отклоняется до/после таймаута.

    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');
    });   
    

Вы можете увидеть все это в действии на http://plnkr.co/edit/3e4htwMI5fh595ggZY7h?p=preview

Ответ 3

Моя реализация @Michal Charemza failUnlessResolvedWithin с реальной выборкой. Передавая отложенный объект функции func, он уменьшает необходимость создания обещания в коде использования "ByUserPosition". Помогает мне справиться с firefox и геолокацией.

.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);
                });
            };