Definindo um manipulador de tempo limite em uma promise em angularjs

Estou tentando definir um tempo limite no meu controlador para que, se uma resposta não for recebida em 250ms, ela falhe. Eu configurei meu teste de unidade para ter um tempo limite de 10000 para que essa condição seja atendida. Alguém pode me apontar na direção certa? (EDIT estou tentando conseguir isso sem usar o serviço $ http que eu sei fornece funcionalidade timeout)

(EDIT – meus outros testes de unidade estavam falhando porque eu não estava chamando timeout.flush neles, agora eu só preciso obter o tempo limite de mensagem chutando quando uma promise indefinida é retornada por promiseService.getPromise (). código inicial da pergunta).

promiseService (promise é uma variável do conjunto de testes que me permite usar um comportamento diferente para a promise em cada suíte de teste antes de aplicar, por exemplo, rejeitar em um, sucesso em outro)

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

Função do controlador que está sendo testada –

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

Teste (normalmente eu resolvo ou rejeito a promise aqui, mas por não configurá-lo estou simulando um tempo limite)

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

Primeiro, gostaria de dizer que a implementação do seu controlador deve ser algo assim:

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

Seus testes:

 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

Outro exemplo com 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 } }); 

Seus testes são semelhantes ao exemplo acima:

 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

O comportamento de “falhar uma promise a menos que seja resolvido com um período de tempo especificado” parece ideal para refatorar em um serviço / fábrica separado. Isso deve tornar o código no novo serviço / fábrica e no controlador mais claro e mais reutilizável.

O controlador, que eu assumi apenas define o sucesso / falha no escopo:

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

E a fábrica, failUnlessResolvedWithin , cria uma nova promise, que efetivamente “intercepta” uma promise de uma function passada. Ele retorna um novo que replica seu comportamento de resolução / rejeição, exceto que ele também rejeita a promise, caso não tenha sido resolvido dentro do tempo limite:

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

Os testes para estes são um pouco complicados (e longos), mas você pode vê-los em http://plnkr.co/edit/3e4htwMI5fh595ggZY7h?p=preview . Os principais pontos dos testes são

  • Os testes para os mocks do controlador failUnlessResolvedWithin com uma chamada para $timeout .

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

    Isso é possível já que ‘failUnlessResolvedWithin’ é (deliberadamente) sintaticamente equivalente a $timeout e feito desde que $timeout fornece a function flush para testar vários casos.

  • Os testes para o próprio serviço usa chamadas $timeout.flush para testar o comportamento dos vários casos em que a promise original está sendo resolvida / rejeitada antes / depois do tempo limite.

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

Você pode ver tudo isso em ação em http://plnkr.co/edit/3e4htwMI5fh595ggZY7h?p=preview

Minha implementação do @Michal Charemza falhaUnlessResolvedWithin com uma amostra real. Ao passar o object diferido para o func, reduz a necessidade de instanciar uma promise no código de uso “ByUserPosition”. Ajuda-me a lidar com o Firefox e a geolocalização.

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