diff options
| -rw-r--r-- | src/ng/q.js | 48 | ||||
| -rw-r--r-- | test/ng/qSpec.js | 317 | 
2 files changed, 345 insertions, 20 deletions
| diff --git a/src/ng/q.js b/src/ng/q.js index 994ecd8d..fe05b37f 100644 --- a/src/ng/q.js +++ b/src/ng/q.js @@ -199,7 +199,7 @@ function qFactory(nextTick, exceptionHandler) {                var callback;                for (var i = 0, ii = callbacks.length; i < ii; i++) {                  callback = callbacks[i]; -                value.then(callback[0], callback[1]); +                value.then(callback[0], callback[1], callback[2]);                }              });            } @@ -212,8 +212,25 @@ function qFactory(nextTick, exceptionHandler) {        }, +      notify: function(progress) { +        if (pending) { +          var callbacks = pending; + +          if (pending.length) { +            nextTick(function() { +              var callback; +              for (var i = 0, ii = callbacks.length; i < ii; i++) { +                callback = callbacks[i]; +                callback[2](progress); +              } +            }); +          } +        } +      }, + +        promise: { -        then: function(callback, errback) { +        then: function(callback, errback, progressback) {            var result = defer();            var wrappedCallback = function(value) { @@ -234,10 +251,18 @@ function qFactory(nextTick, exceptionHandler) {              }            }; +          var wrappedProgressback = function(progress) { +            try { +              result.notify((progressback || defaultCallback)(progress)); +            } catch(e) { +              exceptionHandler(e); +            } +          }; +            if (pending) { -            pending.push([wrappedCallback, wrappedErrback]); +            pending.push([wrappedCallback, wrappedErrback, wrappedProgressback]);            } else { -            value.then(wrappedCallback, wrappedErrback); +            value.then(wrappedCallback, wrappedErrback, wrappedProgressback);            }            return result.promise; @@ -359,7 +384,7 @@ function qFactory(nextTick, exceptionHandler) {     * @param {*} value Value or a promise     * @returns {Promise} Returns a promise of the passed value or promise     */ -  var when = function(value, callback, errback) { +  var when = function(value, callback, errback, progressback) {      var result = defer(),          done; @@ -381,15 +406,26 @@ function qFactory(nextTick, exceptionHandler) {        }      }; +    var wrappedProgressback = function(progress) { +      try { +        return (progressback || defaultCallback)(progress); +      } catch (e) { +        exceptionHandler(e); +      } +    }; +      nextTick(function() {        ref(value).then(function(value) {          if (done) return;          done = true; -        result.resolve(ref(value).then(wrappedCallback, wrappedErrback)); +        result.resolve(ref(value).then(wrappedCallback, wrappedErrback, wrappedProgressback));        }, function(reason) {          if (done) return;          done = true;          result.resolve(wrappedErrback(reason)); +      }, function(progress) { +        if (done) return; +        result.notify(wrappedProgressback(progress));        });      }); diff --git a/test/ng/qSpec.js b/test/ng/qSpec.js index 8fc5c50a..316f8add 100644 --- a/test/ng/qSpec.js +++ b/test/ng/qSpec.js @@ -43,7 +43,7 @@ describe('q', function() {      return map(sliceArgs(args), _argToString).join(',');    } -  // Help log invocation of success(), always() and error() +  // Help log invocation of success(), always(), progress() and error()    function _logInvocation(funcName, args, returnVal, throwReturnVal) {      var logPrefix = funcName + '(' +  _argumentsToString(args) + ')';      if (throwReturnVal) { @@ -94,6 +94,22 @@ describe('q', function() {    /**     * Creates a callback that logs its invocation in `log`.     * +   * @param {(number|string)} name Suffix for 'progress' name. e.g. progress(1) => progress +   * @param {*=} returnVal Value that the callback should return. If unspecified, the passed in +   *     value is returned. +   * @param {boolean=} throwReturnVal If true, the `returnVal` will be thrown rather than returned. +   */ +  function progress(name, returnVal, throwReturnVal) { +    var returnValDefined = (arguments.length >= 2); +    name = 'progress' + (name || ''); +    return function() { +      return _logInvocation(name, arguments, (returnValDefined ? returnVal : arguments[0]), throwReturnVal); +    } +  } + +  /** +   * Creates a callback that logs its invocation in `log`. +   *     * @param {(number|string)} name Suffix for 'error' name. e.g. error(1) => error     * @param {*=} returnVal Value that the callback should return. If unspecified, the passed in     *     value is first passed to q.reject() and the result is returned. @@ -126,6 +142,13 @@ describe('q', function() {    } +  /** helper for synchronous notification of deferred */ +  function syncNotify(deferred, progress) { +    deferred.notify(progress); +    mockNextTick.flush(); +  } + +    /** converts the `log` to a '; '-separated string */    function logStr() {      return log.join('; '); @@ -377,6 +400,114 @@ describe('q', function() {      }); +    describe('notify', function() { +      it('should execute all progress callbacks in the registration order', +          function() { +        promise.then(success(1), error(1), progress(1)); +        promise.then(success(2), error(2), progress(2)); +        expect(logStr()).toBe(''); + +        deferred.notify('foo'); +        mockNextTick.flush(); +        expect(logStr()).toBe('progress1(foo)->foo; progress2(foo)->foo'); +      }); + + +      it('should do nothing if a promise was previously resolved', function() { +        promise.then(success(1), error(1), progress(1)); +        expect(logStr()).toBe(''); + +        deferred.resolve('foo'); +        mockNextTick.flush(); +        expect(logStr()).toBe('success1(foo)->foo'); + +        log = []; +        deferred.notify('bar'); +        expect(mockNextTick.queue.length).toBe(0); +        expect(logStr()).toBe(''); +      }); + + +      it('should do nothing if a promise was previously rejected', function() { +        promise.then(success(1), error(1), progress(1)); +        expect(logStr()).toBe(''); + +        deferred.reject('foo'); +        mockNextTick.flush(); +        expect(logStr()).toBe('error1(foo)->reject(foo)'); + +        log = []; +        deferred.reject('bar'); +        deferred.resolve('baz'); +        deferred.notify('qux') +        expect(mockNextTick.queue.length).toBe(0); +        expect(logStr()).toBe(''); + +        promise.then(success(2), error(2), progress(2)); +        expect(logStr()).toBe(''); +        mockNextTick.flush(); +        expect(logStr()).toBe('error2(foo)->reject(foo)'); +      }); + + +      it('should not apply any special treatment to promises passed to notify', function() { +        var deferred2 = defer(); +        promise.then(success(), error(), progress()); + +        deferred.notify(deferred2.promise); +        mockNextTick.flush(); +        expect(logStr()).toBe('progress({})->{}'); +      }); + + +      it('should call the progress callbacks in the next turn', function() { +        promise.then(success(), error(), progress(1)); +        promise.then(success(), error(), progress(2)); +        expect(logStr()).toBe(''); + +        deferred.notify('foo'); +        expect(logStr()).toBe(''); + +        mockNextTick.flush(); +        expect(logStr()).toBe('progress1(foo)->foo; progress2(foo)->foo'); +      }); + + +      it('should ignore notifications sent out in the same turn before listener registration', +          function() { +        deferred.notify('foo'); +        promise.then(success(), error(), progress(1)); +        expect(logStr()).toBe(''); +        expect(mockNextTick.queue).toEqual([]); +      }); + + +      it('should support non-bound execution', function() { +        var notify = deferred.notify; +        promise.then(success(), error(), progress()); +        notify('detached'); +        mockNextTick.flush(); +        expect(logStr()).toBe('progress(detached)->detached'); +      }); + + +      it("should not save and re-emit progress notifications between ticks", function () { +        promise.then(success(1), error(1), progress(1)); +        deferred.notify('foo'); +        deferred.notify('bar'); +        mockNextTick.flush(); +        expect(logStr()).toBe('progress1(foo)->foo; progress1(bar)->bar'); + +        log = []; +        promise.then(success(2), error(2), progress(2)); +        deferred.notify('baz'); +        mockNextTick.flush(); +        expect(logStr()).toBe('progress1(baz)->baz; progress2(baz)->baz'); +      }); + +    }); + +      describe('promise', function() {        it('should have a then method', function() {          expect(typeof promise.then).toBe('function'); @@ -388,7 +519,7 @@ describe('q', function() {        describe('then', function() { -        it('should allow registration of a success callback without an errback ' + +        it('should allow registration of a success callback without an errback or progressback ' +            'and resolve', function() {            promise.then(success());            syncResolve(deferred, 'foo'); @@ -404,7 +535,15 @@ describe('q', function() {          }); -        it('should allow registration of an errback without a success callback and ' + +        it('should allow registration of a success callback without an progressback and notify', +            function() { +          promise.then(success()); +          syncNotify(deferred, 'doing'); +          expect(logStr()).toBe(''); +        }); + + +        it('should allow registration of an errback without a success or progress callback and ' +            ' reject', function() {            promise.then(null, error());            syncReject(deferred, 'oops!'); @@ -420,12 +559,44 @@ describe('q', function() {          }); +        it('should allow registration of an errback without a progress callback and notify', +            function() { +          promise.then(null, error()); +          syncNotify(deferred, 'doing'); +          expect(logStr()).toBe(''); +        }); + + +        it('should allow registration of an progressback without a success or error callback and ' + +          'notify', function() { +          promise.then(null, null, progress()); +          syncNotify(deferred, 'doing'); +          expect(logStr()).toBe('progress(doing)->doing'); +        }); + + +        it('should allow registration of an progressback without a success callback and resolve', +            function() { +          promise.then(null, null, progress()); +          syncResolve(deferred, 'done'); +          expect(logStr()).toBe(''); +        }); + + +        it('should allow registration of an progressback without a error callback and reject', +            function() { +          promise.then(null, null, progress()); +          syncReject(deferred, 'oops!'); +          expect(logStr()).toBe(''); +        }); + +          it('should resolve all callbacks with the original value', function() { -          promise.then(success('A', 'aVal'), error()); -          promise.then(success('B', 'bErr', true), error()); -          promise.then(success('C', q.reject('cReason')), error()); -          promise.then(success('D', q.reject('dReason'), true), error()); -          promise.then(success('E', 'eVal'), error()); +          promise.then(success('A', 'aVal'), error(), progress()); +          promise.then(success('B', 'bErr', true), error(), progress()); +          promise.then(success('C', q.reject('cReason')), error(), progress()); +          promise.then(success('D', q.reject('dReason'), true), error(), progress()); +          promise.then(success('E', 'eVal'), error(), progress());            expect(logStr()).toBe('');            syncResolve(deferred, 'yup'); @@ -438,10 +609,10 @@ describe('q', function() {          it('should reject all callbacks with the original reason', function() { -          promise.then(success(), error('A', 'aVal')); -          promise.then(success(), error('B', 'bEr', true)); -          promise.then(success(), error('C', q.reject('cReason'))); -          promise.then(success(), error('D', 'dVal')); +          promise.then(success(), error('A', 'aVal'), progress()); +          promise.then(success(), error('B', 'bEr', true), progress()); +          promise.then(success(), error('C', q.reject('cReason')), progress()); +          promise.then(success(), error('D', 'dVal'), progress());            expect(logStr()).toBe('');            syncReject(deferred, 'noo!'); @@ -449,6 +620,23 @@ describe('q', function() {          }); +        it('should notify all callbacks with the original value', function() { +          promise.then(success(), error(), progress('A', 'aVal')); +          promise.then(success(), error(), progress('B', 'bErr', true)); +          promise.then(success(), error(), progress('C', q.reject('cReason'))); +          promise.then(success(), error(), progress('C_reject', q.reject('cRejectReason'), true)); +          promise.then(success(), error(), progress('Z', 'the end!')); + +          expect(logStr()).toBe(''); +          syncNotify(deferred, 'yup'); +          expect(log).toEqual(['progressA(yup)->aVal', +                               'progressB(yup)->throw(bErr)', +                               'progressC(yup)->{}', +                               'progressC_reject(yup)->throw({})', +                               'progressZ(yup)->the end!']); +        }); + +          it('should propagate resolution and rejection between dependent promises', function() {            promise.then(success(1, 'x'),       error('1')).                    then(success(2, 'y', true), error('2')). @@ -466,6 +654,23 @@ describe('q', function() {          }); +        it('should propagate notification between dependent promises', function() { +          promise.then(success(), error(), progress(1, 'a')). +                  then(success(), error(), progress(2, 'b')). +                  then(success(), error(), progress(3, 'c')). +                  then(success(), error(), progress(4)). +                  then(success(), error(), progress(5)); + +          expect(logStr()).toBe(''); +          syncNotify(deferred, 'wait'); +          expect(log).toEqual(['progress1(wait)->a', +                               'progress2(a)->b', +                               'progress3(b)->c', +                               'progress4(c)->c', +                               'progress5(c)->c']); +        }); + +          it('should reject a derived promise if an exception is thrown while resolving its parent',              function() {            promise.then(success(1, 'oops', true), error(1)). @@ -484,6 +689,18 @@ describe('q', function() {          }); +        it('should stop notification propagation in case of error', function() { +          promise.then(success(), error(), progress(1)). +                  then(success(), error(), progress(2, 'ops!', true)). +                  then(success(), error(), progress(3)); + +          expect(logStr()).toBe(''); +          syncNotify(deferred, 'wait'); +          expect(log).toEqual(['progress1(wait)->wait', +                               'progress2(wait)->throw(ops!)']); +        }); + +          it('should call success callback in the next turn even if promise is already resolved',              function() {            deferred.resolve('done!'); @@ -744,6 +961,18 @@ describe('q', function() {      }); +    describe('notification', function() { +      it('should call the progressback when the value is a promise and gets notified', +          function() { +        q.when(deferred.promise, success(), error(), progress()); +        mockNextTick.flush(); +        expect(logStr()).toBe(''); +        syncNotify(deferred, 'notification'); +        expect(logStr()).toBe('progress(notification)->notification'); +      }); +    }); + +      describe('optional callbacks', function() {        it('should not require success callback and propagate resolution', function() {          q.when('hi', null, error()).then(success(2), error()); @@ -775,6 +1004,16 @@ describe('q', function() {          mockNextTick.flush();          expect(logStr()).toBe('error2(sorry)->reject(sorry)');        }); + + +      it('should not require progressback and propagate notification', function() { +        q.when(deferred.promise). +          then(success(), error(), progress()); +        mockNextTick.flush(); +        expect(logStr()).toBe(''); +        syncNotify(deferred, 'notification'); +        expect(logStr()).toBe('progress(notification)->notification'); +      });      }); @@ -838,9 +1077,10 @@ describe('q', function() {        it('should call success callback only once even if the original promise gets fullfilled ' +            'multiple times', function() {          var evilPromise = { -          then: function(success, error) { +          then: function(success, error, progress) {              evilPromise.success = success;              evilPromise.error = error; +            evilPromise.progress = progress;            }          }; @@ -863,9 +1103,10 @@ describe('q', function() {        it('should call errback only once even if the original promise gets fullfilled multiple ' +            'times', function() {          var evilPromise = { -          then: function(success, error) { +          then: function(success, error, progress) {              evilPromise.success = success;              evilPromise.error = error; +            evilPromise.progress = progress;            }          }; @@ -879,6 +1120,29 @@ describe('q', function() {          evilPromise.success('take this');          expect(logStr()).toBe('error(failed)->reject(failed)');        }); + + +      it('should not call progressback after promise gets fullfilled, even if original promise ' + +          'gets notified multiple times', function() { +        var evilPromise = { +          then: function(success, error, progress) { +            evilPromise.success = success; +            evilPromise.error = error; +            evilPromise.progress = progress; +          } +        }; + +        q.when(evilPromise, success(), error(), progress()); +        mockNextTick.flush(); +        expect(logStr()).toBe(''); +        evilPromise.progress('notification'); +        evilPromise.success('ok'); +        mockNextTick.flush(); +        expect(logStr()).toBe('progress(notification)->notification; success(ok)->ok'); + +        evilPromise.progress('muhaha'); +        expect(logStr()).toBe('progress(notification)->notification; success(ok)->ok'); +      });      });    }); @@ -921,6 +1185,21 @@ describe('q', function() {      }); +    it('should not forward notifications from individual promises to the combined promise', +        function() { +      var deferred1 = defer(), +          deferred2 = defer(); + +      q.all([promise, deferred1.promise, deferred2.promise]).then(success(), error(), progress()); +      expect(logStr()).toBe(''); +      deferred.notify('x'); +      deferred2.notify('y'); +      expect(logStr()).toBe(''); +      mockNextTick.flush(); +      expect(logStr()).toBe(''); +    }); + +      it('should ignore multiple resolutions of an (evil) array promise', function() {        var evilPromise = {          then: function(success, error) { @@ -1064,6 +1343,16 @@ describe('q', function() {        }); +      it('should log exceptions throw in a progressack and stop propagation, but shoud NOT reject ' + +        'the promise', function() { +          promise.then(success(), error(), progress(1, 'failed', true)).then(null, error(1), progress(2)); +          syncNotify(deferred, '10%'); +          expect(logStr()).toBe('progress1(10%)->throw(failed)'); +          expect(mockExceptionLogger.log).toEqual(['failed']); +          log = []; +          syncResolve(deferred, 'ok'); +          expect(logStr()).toBe('success(ok)->ok'); +      });      }); | 
