Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

Commit

Permalink
feat($interval): add a service wrapping setInterval
Browse files Browse the repository at this point in the history
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 Oct 7, 2013
1 parent a80e96c commit 2b5ce84
Show file tree
Hide file tree
Showing 7 changed files with 725 additions and 0 deletions.
1 change: 1 addition & 0 deletions angularFiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/AngularPublic.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ function publishExternalAPI(angular){
$exceptionHandler: $ExceptionHandlerProvider,
$filter: $FilterProvider,
$interpolate: $InterpolateProvider,
$interval: $IntervalProvider,
$http: $HttpProvider,
$httpBackend: $HttpBackendProvider,
$location: $LocationProvider,
Expand Down
90 changes: 90 additions & 0 deletions src/ng/interval.js
Original file line number Diff line number Diff line change
@@ -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;
}];
}
114 changes: 114 additions & 0 deletions src/ngMock/angular-mocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Copy link
@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.

Copy link
@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.

Copy link
@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)))?$/;

Expand Down Expand Up @@ -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) {
Expand Down
Loading

6 comments on commit 2b5ce84

@carlin-q-scott
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@MuraliMolluru
Copy link

@MuraliMolluru MuraliMolluru commented on 2b5ce84 Jul 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Member

@gkalpak gkalpak commented on 2b5ce84 Jul 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Member

@gkalpak gkalpak commented on 2b5ce84 Jul 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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
Copy link
Member

@gkalpak gkalpak commented on 2b5ce84 Jul 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.