diff options
| author | Julie | 2013-09-13 12:47:05 -0700 | 
|---|---|---|
| committer | Vojta Jina | 2013-10-07 13:45:40 -0700 | 
| commit | 2b5ce84fca7b41fca24707e163ec6af84bc12e83 (patch) | |
| tree | d3fbcfd3c7e2e62fe60df6faa9823abda57b9bd7 | |
| parent | a80e96cea184b392505f0a292785a5c66d45e165 (diff) | |
| download | angular.js-2b5ce84fca7b41fca24707e163ec6af84bc12e83.tar.bz2 | |
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.
| -rwxr-xr-x | angularFiles.js | 1 | ||||
| -rwxr-xr-x | src/AngularPublic.js | 1 | ||||
| -rw-r--r-- | src/ng/interval.js | 90 | ||||
| -rw-r--r-- | src/ngMock/angular-mocks.js | 114 | ||||
| -rw-r--r-- | test/ng/intervalSpec.js | 270 | ||||
| -rw-r--r-- | test/ng/timeoutSpec.js | 14 | ||||
| -rw-r--r-- | test/ngMock/angular-mocksSpec.js | 235 | 
7 files changed, 725 insertions, 0 deletions
| diff --git a/angularFiles.js b/angularFiles.js index cd23aca0..946cff45 100755 --- a/angularFiles.js +++ b/angularFiles.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', diff --git a/src/AngularPublic.js b/src/AngularPublic.js index b225fc85..9bd7fd7d 100755 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -114,6 +114,7 @@ function publishExternalAPI(angular){          $exceptionHandler: $ExceptionHandlerProvider,          $filter: $FilterProvider,          $interpolate: $InterpolateProvider, +        $interval: $IntervalProvider,          $http: $HttpProvider,          $httpBackend: $HttpBackendProvider,          $location: $LocationProvider, diff --git a/src/ng/interval.js b/src/ng/interval.js new file mode 100644 index 00000000..e612f3e4 --- /dev/null +++ b/src/ng/interval.js @@ -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; +  }]; +} diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index 9bc80ff3..11f6f045 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -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, +          iteration = 0, +          skipApply = (isDefined(invokeApply) && !invokeApply); + +      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) { diff --git a/test/ng/intervalSpec.js b/test/ng/intervalSpec.js new file mode 100644 index 00000000..6999f750 --- /dev/null +++ b/test/ng/intervalSpec.js @@ -0,0 +1,270 @@ +'use strict'; + +describe('$interval', function() { + +  beforeEach(module(function($provide){ +    var repeatFns = [], +        nextRepeatId = 0, +        now = 0, +        $window; + +    $window = { +      setInterval: function(fn, delay, count) { +        repeatFns.push({ +          nextTime:(now + delay), +          delay: delay, +          fn: fn, +          id: nextRepeatId, +        }); +        repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;}); + +        return nextRepeatId++; +      }, + +      clearInterval: function(id) { +        var fnIndex; + +        angular.forEach(repeatFns, function(fn, index) { +          if (fn.id === id) fnIndex = index; +        }); + +        if (fnIndex !== undefined) { +          repeatFns.splice(fnIndex, 1); +          return true; +        } + +        return false; +      }, + +      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; +      } +    }; + +    $provide.provider('$interval', $IntervalProvider); +    $provide.value('$window', $window); +  })); + +  it('should run tasks repeatedly', inject(function($interval, $window) { +    var counter = 0; +    $interval(function() { counter++; }, 1000); + +    expect(counter).toBe(0); + +    $window.flush(1000) +    expect(counter).toBe(1); + +    $window.flush(1000); + +    expect(counter).toBe(2); +  })); + +  it('should call $apply after each task is executed', +      inject(function($interval, $rootScope, $window) { +    var applySpy = spyOn($rootScope, '$apply').andCallThrough(); + +    $interval(noop, 1000); +    expect(applySpy).not.toHaveBeenCalled(); + +    $window.flush(1000); +    expect(applySpy).toHaveBeenCalledOnce(); + +    applySpy.reset(); + +    $interval(noop, 1000); +    $interval(noop, 1000); +    $window.flush(1000); +    expect(applySpy.callCount).toBe(3); +  })); + + +  it('should NOT call $apply if invokeApply is set to false', +      inject(function($interval, $rootScope, $window) { +    var applySpy = spyOn($rootScope, '$apply').andCallThrough(); + +    $interval(noop, 1000, 0, false); +    expect(applySpy).not.toHaveBeenCalled(); + +    $window.flush(2000); +    expect(applySpy).not.toHaveBeenCalled(); +  })); + + +  it('should allow you to specify the delay time', inject(function($interval, $window) { +    var counter = 0; +    $interval(function() { counter++; }, 123); + +    expect(counter).toBe(0); + +    $window.flush(122); +    expect(counter).toBe(0); + +    $window.flush(1); +    expect(counter).toBe(1); +  })); + + +  it('should allow you to specify a number of iterations', inject(function($interval, $window) { +    var counter = 0; +    $interval(function() {counter++}, 1000, 2); + +    $window.flush(1000); +    expect(counter).toBe(1); +    $window.flush(1000); +    expect(counter).toBe(2); +    $window.flush(1000); +    expect(counter).toBe(2); +  })); + + +  it('should return a promise which will be updated with the count on each iteration', +      inject(function($interval, $window) { +    var log = [], +        promise = $interval(function() { log.push('tick'); }, 1000); + +    promise.then(function(value) { log.push('promise success: ' + value); }, +                 function(err) { log.push('promise error: ' + err); }, +                 function(note) { log.push('promise update: ' + note); }); +    expect(log).toEqual([]); + +    $window.flush(1000); +    expect(log).toEqual(['tick', 'promise update: 0']); + +    $window.flush(1000); +    expect(log).toEqual(['tick', 'promise update: 0', 'tick', 'promise update: 1']); +  })); + + +  it('should return a promise which will be resolved after the specified number of iterations', +      inject(function($interval, $window) { +    var log = [], +        promise = $interval(function() { log.push('tick'); }, 1000, 2); + +    promise.then(function(value) { log.push('promise success: ' + value); }, +                 function(err) { log.push('promise error: ' + err); }, +                 function(note) { log.push('promise update: ' + note); }); +    expect(log).toEqual([]); + +    $window.flush(1000); +    expect(log).toEqual(['tick', 'promise update: 0']); +    $window.flush(1000); + +    expect(log).toEqual([ +        'tick', 'promise update: 0', 'tick', 'promise update: 1', 'promise success: 2']); + +  })); + + +  describe('exception handling', function() { +    beforeEach(module(function($exceptionHandlerProvider) { +      $exceptionHandlerProvider.mode('log'); +    })); + + +    it('should delegate exception to the $exceptionHandler service', inject( +        function($interval, $exceptionHandler, $window) { +      $interval(function() { throw "Test Error"; }, 1000); +      expect($exceptionHandler.errors).toEqual([]); + +      $window.flush(1000); +      expect($exceptionHandler.errors).toEqual(["Test Error"]); + +      $window.flush(1000); +      expect($exceptionHandler.errors).toEqual(["Test Error", "Test Error"]); +    })); + + +    it('should call $apply even if an exception is thrown in callback', inject( +        function($interval, $rootScope, $window) { +      var applySpy = spyOn($rootScope, '$apply').andCallThrough(); + +      $interval(function() { throw "Test Error"; }, 1000); +      expect(applySpy).not.toHaveBeenCalled(); + +      $window.flush(1000); +      expect(applySpy).toHaveBeenCalled(); +    })); + + +    it('should still update the interval promise when an exception is thrown', +        inject(function($interval, $window) { +      var log = [], +          promise = $interval(function() { throw "Some Error"; }, 1000); + +      promise.then(function(value) { log.push('promise success: ' + value); }, +                 function(err) { log.push('promise error: ' + err); }, +                 function(note) { log.push('promise update: ' + note); }); +      $window.flush(1000); + +      expect(log).toEqual(['promise update: 0']); +    })); +  }); + + +  describe('cancel', function() { +    it('should cancel tasks', inject(function($interval, $window) { +      var task1 = jasmine.createSpy('task1', 1000), +          task2 = jasmine.createSpy('task2', 1000), +          task3 = jasmine.createSpy('task3', 1000), +          promise1, promise3; + +      promise1 = $interval(task1, 200); +      $interval(task2, 1000); +      promise3 = $interval(task3, 333); + +      $interval.cancel(promise3); +      $interval.cancel(promise1); +      $window.flush(1000); + +      expect(task1).not.toHaveBeenCalled(); +      expect(task2).toHaveBeenCalledOnce(); +      expect(task3).not.toHaveBeenCalled(); +    })); + + +    it('should cancel the promise', inject(function($interval, $rootScope, $window) { +      var promise = $interval(noop, 1000), +          log = []; +      promise.then(function(value) { log.push('promise success: ' + value); }, +                 function(err) { log.push('promise error: ' + err); }, +                 function(note) { log.push('promise update: ' + note); }); +      expect(log).toEqual([]); + +      $window.flush(1000); +      $interval.cancel(promise); +      $window.flush(1000); +      $rootScope.$apply(); // For resolving the promise - +                           // necessary since q uses $rootScope.evalAsync. + +      expect(log).toEqual(['promise update: 0', 'promise error: canceled']); +    })); + + +    it('should return true if a task was successfully canceled', +        inject(function($interval, $window) { +      var task1 = jasmine.createSpy('task1'), +          task2 = jasmine.createSpy('task2'), +          promise1, promise2; + +      promise1 = $interval(task1, 1000, 1); +      $window.flush(1000); +      promise2 = $interval(task2, 1000, 1); + +      expect($interval.cancel(promise1)).toBe(false); +      expect($interval.cancel(promise2)).toBe(true); +    })); + + +    it('should not throw a runtime exception when given an undefined promise', +        inject(function($interval) { +      expect($interval.cancel()).toBe(false); +    })); +  }); +}); diff --git a/test/ng/timeoutSpec.js b/test/ng/timeoutSpec.js index 8de63bec..97c8448e 100644 --- a/test/ng/timeoutSpec.js +++ b/test/ng/timeoutSpec.js @@ -165,6 +165,20 @@ describe('$timeout', function() {      })); +    it('should cancel the promise', inject(function($timeout, log) { +      var promise = $timeout(noop); +      promise.then(function(value) { log('promise success: ' + value); }, +                 function(err) { log('promise error: ' + err); }, +                 function(note) { log('promise update: ' + note); }); +      expect(log).toEqual([]); + +      $timeout.cancel(promise); +      $timeout.flush(); + +      expect(log).toEqual(['promise error: canceled']); +    })); + +      it('should return true if a task was successfully canceled', inject(function($timeout) {        var task1 = jasmine.createSpy('task1'),            task2 = jasmine.createSpy('task2'), diff --git a/test/ngMock/angular-mocksSpec.js b/test/ngMock/angular-mocksSpec.js index 1a4290e0..851f7803 100644 --- a/test/ngMock/angular-mocksSpec.js +++ b/test/ngMock/angular-mocksSpec.js @@ -283,6 +283,241 @@ describe('ngMock', function() {    }); +  describe('$interval', function() { +    it('should run tasks repeatedly', inject(function($interval) { +      var counter = 0; +      $interval(function() { counter++; }, 1000); + +      expect(counter).toBe(0); + +      $interval.flush(1000); +      expect(counter).toBe(1); + +      $interval.flush(1000); + +      expect(counter).toBe(2); +    })); + + +    it('should call $apply after each task is executed', inject(function($interval, $rootScope) { +      var applySpy = spyOn($rootScope, '$apply').andCallThrough(); + +      $interval(noop, 1000); +      expect(applySpy).not.toHaveBeenCalled(); + +      $interval.flush(1000); +      expect(applySpy).toHaveBeenCalledOnce(); + +      applySpy.reset(); + +      $interval(noop, 1000); +      $interval(noop, 1000); +      $interval.flush(1000); +      expect(applySpy.callCount).toBe(3); +    })); + + +    it('should NOT call $apply if invokeApply is set to false', +        inject(function($interval, $rootScope) { +      var applySpy = spyOn($rootScope, '$apply').andCallThrough(); + +      $interval(noop, 1000, 0, false); +      expect(applySpy).not.toHaveBeenCalled(); + +      $interval.flush(2000); +      expect(applySpy).not.toHaveBeenCalled(); +    })); + + +    it('should allow you to specify the delay time', inject(function($interval) { +      var counter = 0; +      $interval(function() { counter++; }, 123); + +      expect(counter).toBe(0); + +      $interval.flush(122); +      expect(counter).toBe(0); + +      $interval.flush(1); +      expect(counter).toBe(1); +    })); + + +    it('should allow you to specify a number of iterations', inject(function($interval) { +      var counter = 0; +      $interval(function() {counter++}, 1000, 2); + +      $interval.flush(1000); +      expect(counter).toBe(1); +      $interval.flush(1000); +      expect(counter).toBe(2); +      $interval.flush(1000); +      expect(counter).toBe(2); +    })); + + +    describe('flush', function() { +      it('should move the clock forward by the specified time', inject(function($interval) { +        var counterA = 0; +        var counterB = 0; +        $interval(function() { counterA++; }, 100); +        $interval(function() { counterB++; }, 401); + +        $interval.flush(200); +        expect(counterA).toEqual(2); + +        $interval.flush(201); +        expect(counterA).toEqual(4); +        expect(counterB).toEqual(1); +      })); +    }); + + +    it('should return a promise which will be updated with the count on each iteration', +        inject(function($interval) { +      var log = [], +          promise = $interval(function() { log.push('tick'); }, 1000); + +      promise.then(function(value) { log.push('promise success: ' + value); }, +                   function(err) { log.push('promise error: ' + err); }, +                   function(note) { log.push('promise update: ' + note); }); +      expect(log).toEqual([]); + +      $interval.flush(1000); +      expect(log).toEqual(['tick', 'promise update: 0']); + +      $interval.flush(1000); +      expect(log).toEqual(['tick', 'promise update: 0', 'tick', 'promise update: 1']); +    })); + + +    it('should return a promise which will be resolved after the specified number of iterations', +        inject(function($interval) { +      var log = [], +          promise = $interval(function() { log.push('tick'); }, 1000, 2); + +      promise.then(function(value) { log.push('promise success: ' + value); }, +                   function(err) { log.push('promise error: ' + err); }, +                   function(note) { log.push('promise update: ' + note); }); +      expect(log).toEqual([]); + +      $interval.flush(1000); +      expect(log).toEqual(['tick', 'promise update: 0']); +      $interval.flush(1000); + +      expect(log).toEqual([ +          'tick', 'promise update: 0', 'tick', 'promise update: 1', 'promise success: 2']); + +    })); + + +    describe('exception handling', function() { +      beforeEach(module(function($exceptionHandlerProvider) { +        $exceptionHandlerProvider.mode('log'); +      })); + + +      it('should delegate exception to the $exceptionHandler service', inject( +          function($interval, $exceptionHandler) { +        $interval(function() { throw "Test Error"; }, 1000); +        expect($exceptionHandler.errors).toEqual([]); + +        $interval.flush(1000); +        expect($exceptionHandler.errors).toEqual(["Test Error"]); + +        $interval.flush(1000); +        expect($exceptionHandler.errors).toEqual(["Test Error", "Test Error"]); +      })); + + +      it('should call $apply even if an exception is thrown in callback', inject( +          function($interval, $rootScope) { +        var applySpy = spyOn($rootScope, '$apply').andCallThrough(); + +        $interval(function() { throw "Test Error"; }, 1000); +        expect(applySpy).not.toHaveBeenCalled(); + +        $interval.flush(1000); +        expect(applySpy).toHaveBeenCalled(); +      })); + + +      it('should still update the interval promise when an exception is thrown', +          inject(function($interval) { +        var log = [], +            promise = $interval(function() { throw "Some Error"; }, 1000); + +        promise.then(function(value) { log.push('promise success: ' + value); }, +                   function(err) { log.push('promise error: ' + err); }, +                   function(note) { log.push('promise update: ' + note); }); +        $interval.flush(1000); + +        expect(log).toEqual(['promise update: 0']); +      })); +    }); + + +    describe('cancel', function() { +      it('should cancel tasks', inject(function($interval) { +        var task1 = jasmine.createSpy('task1', 1000), +            task2 = jasmine.createSpy('task2', 1000), +            task3 = jasmine.createSpy('task3', 1000), +            promise1, promise3; + +        promise1 = $interval(task1, 200); +        $interval(task2, 1000); +        promise3 = $interval(task3, 333); + +        $interval.cancel(promise3); +        $interval.cancel(promise1); +        $interval.flush(1000); + +        expect(task1).not.toHaveBeenCalled(); +        expect(task2).toHaveBeenCalledOnce(); +        expect(task3).not.toHaveBeenCalled(); +      })); + + +      it('should cancel the promise', inject(function($interval, $rootScope) { +        var promise = $interval(noop, 1000), +            log = []; +        promise.then(function(value) { log.push('promise success: ' + value); }, +                   function(err) { log.push('promise error: ' + err); }, +                   function(note) { log.push('promise update: ' + note); }); +        expect(log).toEqual([]); + +        $interval.flush(1000); +        $interval.cancel(promise); +        $interval.flush(1000); +        $rootScope.$apply(); // For resolving the promise - +                             // necessary since q uses $rootScope.evalAsync. + +        expect(log).toEqual(['promise update: 0', 'promise error: canceled']); +      })); + + +      it('should return true if a task was successfully canceled', inject(function($interval) { +        var task1 = jasmine.createSpy('task1'), +            task2 = jasmine.createSpy('task2'), +            promise1, promise2; + +        promise1 = $interval(task1, 1000, 1); +        $interval.flush(1000); +        promise2 = $interval(task2, 1000, 1); + +        expect($interval.cancel(promise1)).toBe(false); +        expect($interval.cancel(promise2)).toBe(true); +      })); + + +      it('should not throw a runtime exception when given an undefined promise', +          inject(function($interval) { +        expect($interval.cancel()).toBe(false); +      })); +    }); +  }); + +    describe('defer', function() {      var browser, log;      beforeEach(inject(function($browser) { | 
