diff options
| -rw-r--r-- | angularFiles.js | 1 | ||||
| -rw-r--r-- | src/AngularPublic.js | 1 | ||||
| -rw-r--r-- | src/ng/defer.js | 6 | ||||
| -rw-r--r-- | src/ng/timeout.js | 87 | ||||
| -rw-r--r-- | src/ngMock/angular-mocks.js | 26 | ||||
| -rw-r--r-- | test/ng/deferSpec.js | 1 | ||||
| -rw-r--r-- | test/ng/timeoutSpec.js | 146 | ||||
| -rw-r--r-- | test/ngMock/angular-mocksSpec.js | 17 | 
8 files changed, 284 insertions, 1 deletions
| diff --git a/angularFiles.js b/angularFiles.js index 537b5bde..e82362cf 100644 --- a/angularFiles.js +++ b/angularFiles.js @@ -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', diff --git a/src/AngularPublic.js b/src/AngularPublic.js index a9124482..54536b43 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -125,6 +125,7 @@ function publishExternalAPI(angular){          $q: $QProvider,          $sniffer: $SnifferProvider,          $templateCache: $TemplateCacheProvider, +        $timeout: $TimeoutProvider,          $window: $WindowProvider        });      } diff --git a/src/ng/defer.js b/src/ng/defer.js index f2a893bc..b5dc8844 100644 --- a/src/ng/defer.js +++ b/src/ng/defer.js @@ -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 @@ -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); diff --git a/src/ng/timeout.js b/src/ng/timeout.js new file mode 100644 index 00000000..ac92bf8c --- /dev/null +++ b/src/ng/timeout.js @@ -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; +  }]; +} diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index 8b5d100a..ef1833e2 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -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 @@ -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; +  });  }); diff --git a/test/ng/deferSpec.js b/test/ng/deferSpec.js index 48c9e912..2e31aadb 100644 --- a/test/ng/deferSpec.js +++ b/test/ng/deferSpec.js @@ -5,6 +5,7 @@ describe('$defer', function() {      $provide.factory('$exceptionHandler', function(){        return jasmine.createSpy('$exceptionHandler');      }); +    $provide.value('$log', {warn: noop});    })); diff --git a/test/ng/timeoutSpec.js b/test/ng/timeoutSpec.js new file mode 100644 index 00000000..19db1227 --- /dev/null +++ b/test/ng/timeoutSpec.js @@ -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); +    })); +  }); +}); diff --git a/test/ngMock/angular-mocksSpec.js b/test/ngMock/angular-mocksSpec.js index 469df91e..62190072 100644 --- a/test/ngMock/angular-mocksSpec.js +++ b/test/ngMock/angular-mocksSpec.js @@ -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; | 
