Permalink
Browse files

feat($interval): add a service wrapping setInterval

The $interval service simplifies creating and testing recurring tasks.
This service does not increment $browser's outstanding request count,
which means that scenario tests and Protractor tests will not timeout
when a site uses a polling function registered by $interval. Provides
a workaround for #2402.

For unit tests, repeated tasks can be controlled using ngMock$interval's
tick(), tickNext(), and tickAll() functions.
  • Loading branch information...
juliemr authored and vojtajina committed Sep 13, 2013
1 parent a80e96c commit 2b5ce84fca7b41fca24707e163ec6af84bc12e83
Showing with 725 additions and 0 deletions.
  1. +1 −0 angularFiles.js
  2. +1 −0 src/AngularPublic.js
  3. +90 −0 src/ng/interval.js
  4. +114 −0 src/ngMock/angular-mocks.js
  5. +270 −0 test/ng/intervalSpec.js
  6. +14 −0 test/ng/timeoutSpec.js
  7. +235 −0 test/ngMock/angular-mocksSpec.js
@@ -20,6 +20,7 @@ angularFiles = {
'src/ng/http.js',
'src/ng/httpBackend.js',
'src/ng/interpolate.js',
'src/ng/interval.js',
'src/ng/locale.js',
'src/ng/location.js',
'src/ng/log.js',
@@ -114,6 +114,7 @@ function publishExternalAPI(angular){
$exceptionHandler: $ExceptionHandlerProvider,
$filter: $FilterProvider,
$interpolate: $InterpolateProvider,
$interval: $IntervalProvider,
$http: $HttpProvider,
$httpBackend: $HttpBackendProvider,
$location: $LocationProvider,
@@ -0,0 +1,90 @@
'use strict';
function $IntervalProvider() {
this.$get = ['$rootScope', '$window', '$q',
function($rootScope, $window, $q) {
var intervals = {};
/**
* @ngdoc function
* @name ng.$interval
*
* @description
* Angular's wrapper for `window.setInterval`. The `fn` function is executed every `delay`
* milliseconds.
*
* The return value of registering an interval function is a promise. This promise will be
* notified upon each tick of the interval, and will be resolved after `count` iterations, or
* run indefinitely if `count` is not defined. The value of the notification will be the
* number of iterations that have run.
* To cancel an interval, call `$interval.cancel(promise)`.
*
* In tests you can use {@link ngMock.$interval#flush `$interval.flush(millis)`} to
* move forward by `millis` milliseconds and trigger any functions scheduled to run in that
* time.
*
* @param {function()} fn A function that should be called repeatedly.
* @param {number} delay Number of milliseconds between each function call.
* @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat
* indefinitely.
* @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
* will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
* @returns {promise} A promise which will be notified on each iteration.
*/
function interval(fn, delay, count, invokeApply) {
var setInterval = $window.setInterval,
clearInterval = $window.clearInterval;
var deferred = $q.defer(),
promise = deferred.promise,
count = (isDefined(count)) ? count : 0,
iteration = 0,
skipApply = (isDefined(invokeApply) && !invokeApply);
promise.then(null, null, fn);
promise.$$intervalId = setInterval(function tick() {
deferred.notify(iteration++);
if (count > 0 && iteration >= count) {
deferred.resolve(iteration);
clearInterval(promise.$$intervalId);
delete intervals[promise.$$intervalId];
}
if (!skipApply) $rootScope.$apply();
}, delay);
intervals[promise.$$intervalId] = deferred;
return promise;
}
/**
* @ngdoc function
* @name ng.$interval#cancel
* @methodOf ng.$interval
*
* @description
* Cancels a task associated with the `promise`.
*
* @param {number} promise Promise returned by the `$interval` function.
* @returns {boolean} Returns `true` if the task was successfully canceled.
*/
interval.cancel = function(promise) {
if (promise && promise.$$intervalId in intervals) {
intervals[promise.$$intervalId].reject('canceled');
clearInterval(promise.$$intervalId);
delete intervals[promise.$$intervalId];
return true;
}
return false;
};
return interval;
}];
}
@@ -438,6 +438,119 @@ angular.mock.$LogProvider = function() {
};
/**
* @ngdoc service
* @name ngMock.$interval
*
* @description
* Mock implementation of the $interval service.
*
* Use {@link ngMock.$interval#flush `$interval.flush(millis)`} to
* move forward by `millis` milliseconds and trigger any functions scheduled to run in that
* time.
*
* @param {function()} fn A function that should be called repeatedly.
* @param {number} delay Number of milliseconds between each function call.
* @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat
* indefinitely.
* @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
* will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
* @returns {promise} A promise which will be notified on each iteration.
*/
angular.mock.$IntervalProvider = function() {
this.$get = ['$rootScope', '$q',
function($rootScope, $q) {
var repeatFns = [],
nextRepeatId = 0,
now = 0;
var $interval = function(fn, delay, count, invokeApply) {
var deferred = $q.defer(),
promise = deferred.promise,
count = (isDefined(count)) ? count : 0,

This comment has been minimized.

@mjtko

mjtko Oct 8, 2013

Contributor

I believe this should be angular.isDefined rather than isDefined. I would (and can) submit a PR for this change, but I am not familiar enough with the tests for ngMocks to be able to also supply a spec, so it may be better that somebody in the angularjs team fix this and cover it with a spec if need be!

iteration = 0,
skipApply = (isDefined(invokeApply) && !invokeApply);

This comment has been minimized.

@mjtko

mjtko Oct 8, 2013

Contributor

@vojtajina Also, as above, this line should be angular.isDefined. Let me know if you want me to PR, though as mentioned above, I'm at a loss as where to begin with specs to cover this. :)

This comment has been minimized.

@mjtko

mjtko Oct 8, 2013

Contributor

Just for good measure, FAO @juliemr too. Thanks!

promise.then(null, null, fn);
promise.$$intervalId = nextRepeatId;
function tick() {
deferred.notify(iteration++);
if (count > 0 && iteration >= count) {
var fnIndex;
deferred.resolve(iteration);
angular.forEach(repeatFns, function(fn, index) {
if (fn.id === promise.$$intervalId) fnIndex = index;
});
if (fnIndex !== undefined) {
repeatFns.splice(fnIndex, 1);
}
}
if (!skipApply) $rootScope.$apply();
};
repeatFns.push({
nextTime:(now + delay),
delay: delay,
fn: tick,
id: nextRepeatId,
deferred: deferred
});
repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;});
nextRepeatId++;
return promise;
};
$interval.cancel = function(promise) {
var fnIndex;
angular.forEach(repeatFns, function(fn, index) {
if (fn.id === promise.$$intervalId) fnIndex = index;
});
if (fnIndex !== undefined) {
repeatFns[fnIndex].deferred.reject('canceled');
repeatFns.splice(fnIndex, 1);
return true;
}
return false;
};
/**
* @ngdoc method
* @name ngMock.$interval#flush
* @methodOf ngMock.$interval
* @description
*
* Runs interval tasks scheduled to be run in the next `millis` milliseconds.
*
* @param {number=} millis maximum timeout amount to flush up until.
*
* @return {number} The amount of time moved forward.
*/
$interval.flush = function(millis) {
now += millis;
while (repeatFns.length && repeatFns[0].nextTime <= now) {
var task = repeatFns[0];
task.fn();
task.nextTime += task.delay;
repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;});
}
return millis;
};
return $interval;
}];
};
(function() {
var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/;
@@ -1581,6 +1694,7 @@ angular.module('ngMock', ['ng']).provider({
$browser: angular.mock.$BrowserProvider,
$exceptionHandler: angular.mock.$ExceptionHandlerProvider,
$log: angular.mock.$LogProvider,
$interval: angular.mock.$IntervalProvider,
$httpBackend: angular.mock.$HttpBackendProvider,
$rootElement: angular.mock.$RootElementProvider
}).config(function($provide) {
Oops, something went wrong.

6 comments on commit 2b5ce84

@carlin-q-scott

This comment has been minimized.

carlin-q-scott replied Aug 25, 2015

So if I make an http request (most common polling scenario), won't the http request increment the $browser's outstanding request count?

@MuraliMolluru

This comment has been minimized.

MuraliMolluru replied Jul 4, 2017

My protractor still getting timeout for long running http requests. We changed angular code to use $interval for making http calls but still same error.
my code looks like this.$interval(() => this.$http.get(this.prefix(url), config), 0, 1);
I am using anguarjs 1.6x and protractor 5.x.
Am i missing anything?

@gkalpak

This comment has been minimized.

Member

gkalpak replied Jul 4, 2017

This doesn't sound related to $timeout. The testability API (used by Protractor under the hood) will also wait for $http requests to settle before considering the app stable.

@gkalpak

This comment has been minimized.

Member

gkalpak replied Jul 4, 2017

I am not sure what the problem is to begin with. Why is the request taking so long? Is it expected? Is it desired? Do you need the request? Do you need protractor to wait for the response?

That said, this is a general support question. You can use one of the appropriate support channels for these types of questions.
GitHub issues are reserved for bug reports and feature requests.

@MuraliMolluru

This comment has been minimized.

MuraliMolluru replied Jul 4, 2017

@gkalpak My application using event-streams and the request keep open so when protractor tests it waits for the request to finish but here it will never finish and finally gets timeout. Yes This is expected.

@gkalpak

This comment has been minimized.

Member

gkalpak replied Jul 4, 2017

@mollurumca, if you don't care about the request in tests, you can mock it out using ngMockE2E's $httpBackend. Again, I am sure you will get plenty of good suggestions and insights on the support channels 😁

Please sign in to comment.