diff --git a/src/ng/q.js b/src/ng/q.js index feeb391827ea..e571636f4d64 100644 --- a/src/ng/q.js +++ b/src/ng/q.js @@ -8,6 +8,46 @@ * @description * A promise/deferred implementation inspired by [Kris Kowal's Q](https://github.com/kriskowal/q). * + * $q can be used in two fashions --- One, which is more similar to Kris Kowal's Q or jQuery's Deferred + * implementations, the other resembles ES6 promises to some degree. + * + * # $q constructor + * + * The streamlined ES6 style promise is essentially just using $q as a constructor which takes a `resolver` + * function as the first argument). This is similar to the native Promise implementation from ES6 Harmony, + * see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). + * + * While the constructor-style use is supported, not all of the supporting methods from Harmony promises are + * available yet. + * + * It can be used like so: + * + * ```js + * return $q(function(resolve, reject) { + * // perform some asynchronous operation, resolve or reject the promise when appropriate. + * setInterval(function() { + * if (pollStatus > 0) { + * resolve(polledValue); + * } else if (pollStatus < 0) { + * reject(polledValue); + * } else { + * pollStatus = pollAgain(function(value) { + * polledValue = value; + * }); + * } + * }, 10000); + * }). + * then(function(value) { + * // handle success + * }, function(reason) { + * // handle failure + * }); + * ``` + * + * Note, progress/notify callbacks are not currently supported via the ES6-style interface. + * + * However, the more traditional CommonJS style usage is still available, and documented below. + * * [The CommonJS Promise proposal](http://wiki.commonjs.org/wiki/Promises) describes a promise as an * interface for interacting with an object that represents the result of an action that is * performed asynchronously, and may or may not be finished at any given point in time. @@ -54,7 +94,6 @@ * For more on this please see the [Q documentation](https://github.com/kriskowal/q) especially the * section on serial or parallel joining of promises. * - * * # The Deferred API * * A new instance of deferred is constructed by calling `$q.defer()`. @@ -163,6 +202,12 @@ * expect(resolvedValue).toEqual(123); * })); * ``` + * + * @param {function(function, function)} resolver Function which is responsible for resolving or + * rejecting the newly created promise. The first parameteter is a function which resolves the + * promise, the second parameter is a function which rejects the promise. + * + * @returns {Promise} The newly created promise. */ function $QProvider() { @@ -519,10 +564,36 @@ function qFactory(nextTick, exceptionHandler) { return deferred.promise; } - return { - defer: defer, - reject: reject, - when: when, - all: all + var $Q = function Q(resolver) { + if (!isFunction(resolver)) { + // TODO(@caitp): minErr this + throw new TypeError('Expected resolverFn'); + } + + if (!(this instanceof Q)) { + // More useful when $Q is the Promise itself. + return new Q(resolver); + } + + var deferred = defer(); + + function resolveFn(value) { + deferred.resolve(value); + } + + function rejectFn(reason) { + deferred.reject(reason); + } + + resolver(resolveFn, rejectFn); + + return deferred.promise; }; + + $Q.defer = defer; + $Q.reject = reject; + $Q.when = when; + $Q.all = all; + + return $Q; } diff --git a/test/ng/qSpec.js b/test/ng/qSpec.js index a327a80714de..f9baf4e2222f 100644 --- a/test/ng/qSpec.js +++ b/test/ng/qSpec.js @@ -196,6 +196,605 @@ describe('q', function() { }); + describe('$Q', function() { + var resolve, reject, resolve2, reject2; + var createPromise = function() { + return q(function(resolveFn, rejectFn) { + if (resolve === null) { + resolve = resolveFn; + reject = rejectFn; + } else if (resolve2 === null) { + resolve2 = resolveFn; + reject2 = rejectFn; + } + }); + }; + + afterEach(function() { + resolve = reject = resolve2 = reject2 = null; + }); + + it('should return a Promise', function() { + var promise = q(noop); + expect(typeof promise.then).toBe('function'); + expect(typeof promise.catch).toBe('function'); + expect(typeof promise.finally).toBe('function'); + }); + + + describe('resolve', function() { + it('should fulfill the promise and execute all success callbacks in the registration order', + function() { + var promise = createPromise(); + promise.then(success(1), error()); + promise.then(success(2), error()); + expect(logStr()).toBe(''); + + resolve('foo'); + mockNextTick.flush(); + expect(logStr()).toBe('success1(foo)->foo; success2(foo)->foo'); + }); + + + it('should do nothing if a promise was previously resolved', function() { + var promise = createPromise(); + promise.then(success(), error()); + expect(logStr()).toBe(''); + + resolve('foo'); + mockNextTick.flush(); + expect(logStr()).toBe('success(foo)->foo'); + + log = []; + resolve('bar'); + reject('baz'); + expect(mockNextTick.queue.length).toBe(0); + expect(logStr()).toBe(''); + }); + + + it('should do nothing if a promise was previously rejected', function() { + var promise = createPromise(); + promise.then(success(), error()); + expect(logStr()).toBe(''); + + reject('foo'); + mockNextTick.flush(); + expect(logStr()).toBe('error(foo)->reject(foo)'); + + log = []; + resolve('bar'); + reject('baz'); + expect(mockNextTick.queue.length).toBe(0); + expect(logStr()).toBe(''); + }); + + + it('should allow deferred resolution with a new promise', function() { + var promise = createPromise(); + + promise.then(success(), error()); + + resolve(createPromise()); + mockNextTick.flush(); + expect(logStr()).toBe(''); + + resolve2('foo'); + mockNextTick.flush(); + expect(logStr()).toBe('success(foo)->foo'); + }); + + + it('should call the callback in the next turn', function() { + var promise = createPromise(); + promise.then(success()); + expect(logStr()).toBe(''); + + resolve('foo'); + expect(logStr()).toBe(''); + + mockNextTick.flush(); + expect(logStr()).toBe('success(foo)->foo'); + }); + + + it('should not break if a callbacks registers another callback', function() { + var promise = createPromise(); + promise.then(function() { + log.push('outer'); + promise.then(function() { + log.push('inner'); + }); + }); + + resolve('foo'); + expect(logStr()).toBe(''); + + mockNextTick.flush(); + expect(logStr()).toBe('outer; inner'); + }); + + + it('should not break if a callbacks tries to resolve the deferred again', function() { + var promise = createPromise(); + promise.then(function(val) { + log.push('then1(' + val + ')->resolve(bar)'); + deferred.resolve('bar'); // nop + }); + + promise.then(success(2)); + + resolve('foo'); + expect(logStr()).toBe(''); + + mockNextTick.flush(); + expect(logStr()).toBe('then1(foo)->resolve(bar); success2(foo)->foo'); + }); + }); + + + describe('reject', function() { + it('should reject the promise and execute all error callbacks in the registration order', + function() { + var promise = createPromise(); + promise.then(success(), error(1)); + promise.then(success(), error(2)); + expect(logStr()).toBe(''); + + reject('foo'); + mockNextTick.flush(); + expect(logStr()).toBe('error1(foo)->reject(foo); error2(foo)->reject(foo)'); + }); + + + it('should do nothing if a promise was previously resolved', function() { + var promise = createPromise(); + promise.then(success(1), error(1)); + expect(logStr()).toBe(''); + + resolve('foo'); + mockNextTick.flush(); + expect(logStr()).toBe('success1(foo)->foo'); + + log = []; + reject('bar'); + resolve('baz'); + expect(mockNextTick.queue.length).toBe(0); + expect(logStr()).toBe(''); + + promise.then(success(2), error(2)); + expect(logStr()).toBe(''); + mockNextTick.flush(); + expect(logStr()).toBe('success2(foo)->foo'); + }); + + + it('should do nothing if a promise was previously rejected', function() { + var promise = createPromise(); + promise.then(success(1), error(1)); + expect(logStr()).toBe(''); + + reject('foo'); + mockNextTick.flush(); + expect(logStr()).toBe('error1(foo)->reject(foo)'); + + log = []; + reject('bar'); + resolve('baz'); + expect(mockNextTick.queue.length).toBe(0); + expect(logStr()).toBe(''); + + promise.then(success(2), error(2)); + expect(logStr()).toBe(''); + mockNextTick.flush(); + expect(logStr()).toBe('error2(foo)->reject(foo)'); + }); + + + it('should not defer rejection with a new promise', function() { + var promise = createPromise(); + promise.then(success(), error()); + + reject(createPromise()); + mockNextTick.flush(); + expect(logStr()).toBe('error({})->reject({})'); + }); + + + it('should call the error callback in the next turn', function() { + var promise = createPromise(); + promise.then(success(), error()); + expect(logStr()).toBe(''); + + reject('foo'); + expect(logStr()).toBe(''); + + mockNextTick.flush(); + expect(logStr()).toBe('error(foo)->reject(foo)'); + }); + + + it('should support non-bound execution', function() { + var promise = createPromise(); + promise.then(success(), error()); + reject('detached'); + mockNextTick.flush(); + expect(logStr()).toBe('error(detached)->reject(detached)'); + }); + }); + + + describe('promise', function() { + describe('then', function() { + it('should allow registration of a success callback without an errback or progressback ' + + 'and resolve', function() { + var promise = createPromise(); + promise.then(success()); + resolve('foo'); + mockNextTick.flush(); + expect(logStr()).toBe('success(foo)->foo'); + }); + + + it('should allow registration of a success callback without an errback and reject', + function() { + var promise = createPromise(); + promise.then(success()); + reject('foo'); + mockNextTick.flush(); + expect(logStr()).toBe(''); + }); + + + it('should allow registration of an errback without a success or progress callback and ' + + ' reject', function() { + var promise = createPromise(); + promise.then(null, error()); + reject('oops!'); + mockNextTick.flush(); + expect(logStr()).toBe('error(oops!)->reject(oops!)'); + }); + + + it('should allow registration of an errback without a success callback and resolve', + function() { + var promise = createPromise(); + promise.then(null, error()); + resolve('done'); + mockNextTick.flush(); + expect(logStr()).toBe(''); + }); + + + it('should allow registration of an progressback without a success callback and resolve', + function() { + var promise = createPromise(); + promise.then(null, null, progress()); + resolve('done'); + mockNextTick.flush(); + expect(logStr()).toBe(''); + }); + + + it('should allow registration of an progressback without a error callback and reject', + function() { + var promise = createPromise(); + promise.then(null, null, progress()); + reject('oops!'); + mockNextTick.flush(); + expect(logStr()).toBe(''); + }); + + + it('should resolve all callbacks with the original value', function() { + var promise = createPromise(); + 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(''); + resolve('yup'); + mockNextTick.flush(); + expect(log).toEqual(['successA(yup)->aVal', + 'successB(yup)->throw(bErr)', + 'successC(yup)->{}', + 'successD(yup)->throw({})', + 'successE(yup)->eVal']); + }); + + + it('should reject all callbacks with the original reason', function() { + var promise = createPromise(); + 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(''); + reject('noo!'); + mockNextTick.flush(); + expect(logStr()).toBe('errorA(noo!)->aVal; errorB(noo!)->throw(bEr); errorC(noo!)->{}; errorD(noo!)->dVal'); + }); + + + it('should propagate resolution and rejection between dependent promises', function() { + var promise = createPromise(); + promise.then(success(1, 'x'), error('1')). + then(success(2, 'y', true), error('2')). + then(success(3), error(3, 'z', true)). + then(success(4), error(4, 'done')). + then(success(5), error(5)); + + expect(logStr()).toBe(''); + resolve('sweet!'); + mockNextTick.flush(); + expect(log).toEqual(['success1(sweet!)->x', + 'success2(x)->throw(y)', + 'error3(y)->throw(z)', + 'error4(z)->done', + 'success5(done)->done']); + }); + + + it('should reject a derived promise if an exception is thrown while resolving its parent', + function() { + var promise = createPromise(); + promise.then(success(1, 'oops', true), error(1)). + then(success(2), error(2)); + resolve('done!'); + mockNextTick.flush(); + expect(logStr()).toBe('success1(done!)->throw(oops); error2(oops)->reject(oops)'); + }); + + + it('should reject a derived promise if an exception is thrown while rejecting its parent', + function() { + var promise = createPromise(); + promise.then(null, error(1, 'oops', true)). + then(success(2), error(2)); + reject('timeout'); + mockNextTick.flush(); + expect(logStr()).toBe('error1(timeout)->throw(oops); error2(oops)->reject(oops)'); + }); + + + it('should call success callback in the next turn even if promise is already resolved', + function() { + var promise = createPromise(); + resolve('done!'); + + promise.then(success()); + expect(logStr()).toBe(''); + + mockNextTick.flush(); + expect(log).toEqual(['success(done!)->done!']); + }); + + + it('should call error callback in the next turn even if promise is already rejected', + function() { + var promise = createPromise(); + reject('oops!'); + + promise.then(null, error()); + expect(logStr()).toBe(''); + + mockNextTick.flush(); + expect(log).toEqual(['error(oops!)->reject(oops!)']); + }); + + it('should forward success resolution when success callbacks are not functions', function() { + var promise = createPromise(); + resolve('yay!'); + + promise.then(1). + then(null). + then({}). + then('gah!'). + then([]). + then(success()); + + expect(logStr()).toBe(''); + + mockNextTick.flush(); + expect(log).toEqual(['success(yay!)->yay!']); + }); + + it('should forward error resolution when error callbacks are not functions', function() { + var promise = createPromise(); + reject('oops!'); + + promise.then(null, 1). + then(null, null). + then(null, {}). + then(null, 'gah!'). + then(null, []). + then(null, error()); + + expect(logStr()).toBe(''); + + mockNextTick.flush(); + expect(log).toEqual(['error(oops!)->reject(oops!)']); + }); + }); + + + describe('finally', function() { + it('should not take an argument', + function() { + var promise = createPromise(); + promise['finally'](fin(1)); + resolve('foo'); + mockNextTick.flush(); + expect(logStr()).toBe('finally1()'); + }); + + describe("when the promise is fulfilled", function () { + it('should call the callback', + function() { + var promise = createPromise(); + promise.then(success(1))['finally'](fin(1)); + resolve('foo'); + mockNextTick.flush(); + expect(logStr()).toBe('success1(foo)->foo; finally1()'); + }); + + it('should fulfill with the original value', + function() { + var promise = createPromise(); + promise['finally'](fin('B', 'b'), error('B')). + then(success('BB', 'bb'), error('BB')); + resolve('RESOLVED_VAL'); + mockNextTick.flush(); + expect(log).toEqual(['finallyB()->b', + 'successBB(RESOLVED_VAL)->bb']); + }); + + + it('should fulfill with the original value (larger test)', + function() { + var promise = createPromise(); + promise.then(success('A', 'a'), error('A')); + promise['finally'](fin('B', 'b'), error('B')). + then(success('BB', 'bb'), error('BB')); + promise.then(success('C', 'c'), error('C'))['finally'](fin('CC', 'IGNORED')) + .then(success('CCC', 'cc'), error('CCC')) + .then(success('CCCC', 'ccc'), error('CCCC')); + resolve('RESOLVED_VAL'); + mockNextTick.flush(); + + expect(log).toEqual(['successA(RESOLVED_VAL)->a', + 'finallyB()->b', + 'successC(RESOLVED_VAL)->c', + 'successBB(RESOLVED_VAL)->bb', + 'finallyCC()->IGNORED', + 'successCCC(c)->cc', + 'successCCCC(cc)->ccc']); + }); + + describe("when the callback returns a promise", function() { + describe("that is fulfilled", function() { + it("should fulfill with the original reason after that promise resolves", + function () { + var promise = createPromise(); + var promise2 = createPromise(); + resolve2('bar'); + + promise['finally'](fin(1, promise)) + .then(success(2)); + + resolve('foo'); + mockNextTick.flush(); + + expect(logStr()).toBe('finally1()->{}; success2(foo)->foo'); + }); + }); + + describe("that is rejected", function() { + it("should reject with this new rejection reason", + function () { + var promise = createPromise(); + var promise2 = createPromise(); + reject2('bar'); + promise['finally'](fin(1, promise2)) + .then(success(2), error(1)); + resolve('foo'); + mockNextTick.flush(); + expect(logStr()).toBe('finally1()->{}; error1(bar)->reject(bar)'); + }); + }); + + }); + + describe("when the callback throws an exception", function() { + it("should reject with this new exception", function() { + var promise = createPromise(); + promise['finally'](fin(1, "exception", true)) + .then(success(1), error(2)); + resolve('foo'); + mockNextTick.flush(); + expect(logStr()).toBe('finally1()->throw(exception); error2(exception)->reject(exception)'); + }); + }); + + }); + + + describe("when the promise is rejected", function () { + it("should call the callback", function () { + var promise = createPromise(); + promise['finally'](fin(1)) + .then(success(2), error(1)); + reject('foo'); + mockNextTick.flush(); + expect(logStr()).toBe('finally1(); error1(foo)->reject(foo)'); + }); + + it('should reject with the original reason', function() { + var promise = createPromise(); + promise['finally'](fin(1), "hello") + .then(success(2), error(2)); + reject('original'); + mockNextTick.flush(); + expect(logStr()).toBe('finally1(); error2(original)->reject(original)'); + }); + + describe("when the callback returns a promise", function() { + describe("that is fulfilled", function() { + it("should reject with the original reason after that promise resolves", function () { + var promise = createPromise(); + var promise2 = createPromise(); + resolve2('bar'); + promise['finally'](fin(1, promise2)) + .then(success(2), error(2)); + reject('original'); + mockNextTick.flush(); + expect(logStr()).toBe('finally1()->{}; error2(original)->reject(original)'); + }); + }); + + describe("that is rejected", function () { + it("should reject with the new reason", function() { + var promise = createPromise(); + var promise2 = createPromise(); + reject2('bar'); + promise['finally'](fin(1, promise2)) + .then(success(2), error(1)); + resolve('foo'); + mockNextTick.flush(); + expect(logStr()).toBe('finally1()->{}; error1(bar)->reject(bar)'); + }); + }); + }); + + describe("when the callback throws an exception", function() { + it("should reject with this new exception", function() { + var promise = createPromise(); + promise['finally'](fin(1, "exception", true)) + .then(success(1), error(2)); + resolve('foo'); + mockNextTick.flush(); + expect(logStr()).toBe('finally1()->throw(exception); error2(exception)->reject(exception)'); + }); + }); + }); + }); + + describe('catch', function() { + it('should be a shorthand for defining promise error handlers', function() { + var promise = createPromise(); + promise['catch'](error(1)).then(null, error(2)); + reject('foo'); + mockNextTick.flush(); + expect(logStr()).toBe('error1(foo)->reject(foo); error2(foo)->reject(foo)'); + }); + }); + }); + }); + + describe('defer', function() { it('should create a new deferred', function() { expect(deferred.promise).toBeDefined();