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

feat($timeout): add $timeout service that superceeds $deferred #981

Merged
merged 2 commits into from
May 23, 2012
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions angularFiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ angularFiles = {
'src/ng/http.js',
'src/ng/httpBackend.js',
'src/ng/locale.js',
'src/ng/timeout.js',

'src/ng/filter.js',
'src/ng/filter/filter.js',
Expand Down
2 changes: 1 addition & 1 deletion gen_docs.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/bash
if [ ! -e gen_docs.disable ]; then
jasmine-node docs/spec --noColor && node docs/src/gen-docs.js
./node_modules/.bin/jasmine-node docs/spec --noColor && node docs/src/gen-docs.js
fi
9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "AngularJS",
"version": "0.0.0",
"dependencies" : {
"jasmine-node" : "*",
"q-fs" : "*",
"qq" : "*"
}
}
1 change: 1 addition & 0 deletions src/AngularPublic.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ function publishExternalAPI(angular){
$q: $QProvider,
$sniffer: $SnifferProvider,
$templateCache: $TemplateCacheProvider,
$timeout: $TimeoutProvider,
$window: $WindowProvider
});
}
Expand Down
6 changes: 5 additions & 1 deletion src/ng/defer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
/**
* @ngdoc function
* @name angular.module.ng.$defer
* @deprecated Made obsolete by $timeout service. Please migrate your code. This service will be
* removed with 1.0 final.
* @requires $browser
*
* @description
Expand All @@ -29,7 +31,9 @@
* @returns {boolean} Returns `true` if the task hasn't executed yet and was successfuly canceled.
*/
function $DeferProvider(){
this.$get = ['$rootScope', '$browser', function($rootScope, $browser) {
this.$get = ['$rootScope', '$browser', '$log', function($rootScope, $browser, $log) {
$log.warn('$defer service has been deprecated, migrate to $timeout');

function defer(fn, delay) {
return $browser.defer(function() {
$rootScope.$apply(fn);
Expand Down
87 changes: 87 additions & 0 deletions src/ng/timeout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
'use strict';


function $TimeoutProvider() {
this.$get = ['$rootScope', '$browser', '$q', '$exceptionHandler',
function($rootScope, $browser, $q, $exceptionHandler) {
var deferreds = {};


/**
* @ngdoc function
* @name angular.module.ng.$timeout
* @requires $browser
*
* @description
* Angular's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch
* block and delegates any exceptions to
* {@link angular.module.ng.$exceptionHandler $exceptionHandler} service.
*
* The return value of registering a timeout function is a promise which will be resolved when
* the timeout is reached and the timeout function is executed.
*
* To cancel a the timeout request, call `$timeout.cancel(promise)`.
*
* In tests you can use {@link angular.module.ngMock.$timeout `$timeout.flush()`} to
* synchronously flush the queue of deferred functions.
*
* @param {function()} fn A function, who's execution should be delayed.
* @param {number=} [delay=0] Delay in milliseconds.
* @param {boolean=} [invokeApply=true] If set to false skips model dirty checking, otherwise
* will invoke `fn` within the {@link angular.module.ng.$rootScope.Scope#$apply $apply} block.
* @returns {*} Promise that will be resolved when the timeout is reached. The value this
* promise will be resolved with is the return value of the `fn` function.
*/
function timeout(fn, delay, invokeApply) {
var deferred = $q.defer(),
promise = deferred.promise,
skipApply = (isDefined(invokeApply) && !invokeApply),
timeoutId, cleanup;

timeoutId = $browser.defer(function() {
try {
deferred.resolve(fn());
} catch(e) {
deferred.reject(e);
$exceptionHandler(e);
}

if (!skipApply) $rootScope.$apply();
}, delay);

cleanup = function() {
delete deferreds[promise.$$timeoutId];
};

promise.$$timeoutId = timeoutId;
deferreds[timeoutId] = deferred;
promise.then(cleanup, cleanup);

return promise;
}


/**
* @ngdoc function
* @name angular.module.ng.$timeout#cancel
* @methodOf angular.module.ng.$timeout
*
* @description
* Cancels a task associated with the `promise`. As a result of this the promise will be
* resolved with a rejection.
*
* @param {Promise} promise Promise returned by the `$timeout` function.
* @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully
* canceled.
*/
timeout.cancel = function(promise) {
if (promise.$$timeoutId in deferreds) {
deferreds[promise.$$timeoutId].reject('canceled');
return $browser.defer.cancel(promise.$$timeoutId);
}
return false;
};

return timeout;
}];
}
26 changes: 26 additions & 0 deletions src/ngMock/angular-mocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -1328,6 +1328,25 @@ function MockXhr() {
this.abort = angular.noop;
}


/**
* @ngdoc function
* @name angular.module.ngMock.$timeout
* @description
*
* This service is just a simple decorator for {@link angular.module.ng.$timeout $timeout} service
* that adds a "flush" method.
*/

/**
* @ngdoc method
* @name angular.module.ngMock.$timeout#flush
* @methodOf angular.module.ngMock.$timeout
* @description
*
* Flushes the queue of pending tasks.
*/

/**
* @ngdoc overview
* @name angular.module.ngMock
Expand All @@ -1341,6 +1360,13 @@ angular.module('ngMock', ['ng']).provider({
$exceptionHandler: angular.mock.$ExceptionHandlerProvider,
$log: angular.mock.$LogProvider,
$httpBackend: angular.mock.$HttpBackendProvider
}).config(function($provide) {
$provide.decorator('$timeout', function($delegate, $browser) {
$delegate.flush = function() {
$browser.defer.flush();
};
return $delegate;
});
});


Expand Down
1 change: 1 addition & 0 deletions test/ng/deferSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ describe('$defer', function() {
$provide.factory('$exceptionHandler', function(){
return jasmine.createSpy('$exceptionHandler');
});
$provide.value('$log', {warn: noop});
}));


Expand Down
146 changes: 146 additions & 0 deletions test/ng/timeoutSpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
'use strict';

describe('$timeout', function() {

beforeEach(module(provideLog));


it('should delegate functions to $browser.defer', inject(function($timeout, $browser) {
var counter = 0;
$timeout(function() { counter++; });

expect(counter).toBe(0);

$browser.defer.flush();
expect(counter).toBe(1);

expect(function() {$browser.defer.flush();}).toThrow('No deferred tasks to be flushed');
expect(counter).toBe(1);
}));


it('should call $apply after each callback is executed', inject(function($timeout, $rootScope) {
var applySpy = spyOn($rootScope, '$apply').andCallThrough();

$timeout(function() {});
expect(applySpy).not.toHaveBeenCalled();

$timeout.flush();
expect(applySpy).toHaveBeenCalledOnce();

applySpy.reset();

$timeout(function() {});
$timeout(function() {});
$timeout.flush();
expect(applySpy.callCount).toBe(2);
}));


it('should NOT call $apply if skipApply is set to true', inject(function($timeout, $rootScope) {
var applySpy = spyOn($rootScope, '$apply').andCallThrough();

$timeout(function() {}, 12, false);
expect(applySpy).not.toHaveBeenCalled();

$timeout.flush();
expect(applySpy).not.toHaveBeenCalled();
}));


it('should allow you to specify the delay time', inject(function($timeout, $browser) {
var defer = spyOn($browser, 'defer');
$timeout(noop, 123);
expect(defer.callCount).toEqual(1);
expect(defer.mostRecentCall.args[1]).toEqual(123);
}));


it('should return a promise which will be resolved with return value of the timeout callback',
inject(function($timeout, log) {
var promise = $timeout(function() { log('timeout'); return 'buba'; });

promise.then(function(value) { log('promise success: ' + value); }, log.fn('promise error'));
expect(log).toEqual([]);

$timeout.flush();
expect(log).toEqual(['timeout', 'promise success: buba']);
}));


describe('exception handling', function() {

beforeEach(module(function($exceptionHandlerProvider) {
$exceptionHandlerProvider.mode('log');
}));


it('should delegate exception to the $exceptionHandler service', inject(
function($timeout, $exceptionHandler) {
$timeout(function() {throw "Test Error";});
expect($exceptionHandler.errors).toEqual([]);

$timeout.flush();
expect($exceptionHandler.errors).toEqual(["Test Error"]);
}));


it('should call $apply even if an exception is thrown in callback', inject(
function($timeout, $rootScope) {
var applySpy = spyOn($rootScope, '$apply').andCallThrough();

$timeout(function() {throw "Test Error";});
expect(applySpy).not.toHaveBeenCalled();

$timeout.flush();
expect(applySpy).toHaveBeenCalled();
}));


it('should reject the timeout promise when an exception is thrown in the timeout callback',
inject(function($timeout, log) {
var promise = $timeout(function() { throw "Some Error"; });

promise.then(log.fn('success'), function(reason) { log('error: ' + reason); });
$timeout.flush();

expect(log).toEqual('error: Some Error');
}));
});


describe('cancel', function() {
it('should cancel tasks', inject(function($timeout) {
var task1 = jasmine.createSpy('task1'),
task2 = jasmine.createSpy('task2'),
task3 = jasmine.createSpy('task3'),
promise1, promise3;

promise1 = $timeout(task1);
$timeout(task2);
promise3 = $timeout(task3, 333);

$timeout.cancel(promise3);
$timeout.cancel(promise1);
$timeout.flush();

expect(task1).not.toHaveBeenCalled();
expect(task2).toHaveBeenCalledOnce();
expect(task3).not.toHaveBeenCalled();
}));


it('should return true if a task was successfully canceled', inject(function($timeout) {
var task1 = jasmine.createSpy('task1'),
task2 = jasmine.createSpy('task2'),
promise1, promise2;

promise1 = $timeout(task1);
$timeout.flush();
promise2 = $timeout(task2);

expect($timeout.cancel(promise1)).toBe(false);
expect($timeout.cancel(promise2)).toBe(true);
}));
});
});
17 changes: 17 additions & 0 deletions test/ngMock/angular-mocksSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,23 @@ describe('ngMock', function() {
});


describe('$timeout', function() {
it('should expose flush method that will flush the pending queue of tasks', inject(
function($timeout) {
var logger = [],
logFn = function(msg) { return function() { logger.push(msg) }};

$timeout(logFn('t1'));
$timeout(logFn('t2'), 200);
$timeout(logFn('t3'));
expect(logger).toEqual([]);

$timeout.flush();
expect(logger).toEqual(['t1', 't3', 't2']);
}));
});


describe('angular.mock.dump', function(){
var d = angular.mock.dump;

Expand Down