From 9632f5c1c73c2628121d49aa2d6868fc5d8aef1a Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 12 Dec 2011 08:08:38 -0800 Subject: style(q): rename src/Deferred.js to src/service/q.js --- angularFiles.js | 2 +- src/Deferred.js | 387 ------------------------ src/service/q.js | 387 ++++++++++++++++++++++++ test/DeferredSpec.js | 823 -------------------------------------------------- test/service/qSpec.js | 823 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1211 insertions(+), 1211 deletions(-) delete mode 100644 src/Deferred.js create mode 100644 src/service/q.js delete mode 100644 test/DeferredSpec.js create mode 100644 test/service/qSpec.js diff --git a/angularFiles.js b/angularFiles.js index 0f0b4509..8d35764b 100644 --- a/angularFiles.js +++ b/angularFiles.js @@ -9,7 +9,6 @@ angularFiles = { 'src/jqLite.js', 'src/apis.js', 'src/service/autoScroll.js', - 'src/Deferred.js', 'src/service/browser.js', 'src/service/cacheFactory.js', 'src/service/compiler.js', @@ -29,6 +28,7 @@ angularFiles = { 'src/service/log.js', 'src/service/resource.js', 'src/service/parse.js', + 'src/service/q.js', 'src/service/route.js', 'src/service/routeParams.js', 'src/service/scope.js', diff --git a/src/Deferred.js b/src/Deferred.js deleted file mode 100644 index f0c030b0..00000000 --- a/src/Deferred.js +++ /dev/null @@ -1,387 +0,0 @@ -'use strict'; - -/** - * @ngdoc service - * @name angular.module.ng.$q - * @requires $rootScope - * - * @description - * A promise/deferred implementation inspired by [Kris Kowal's Q](https://github.com/kriskowal/q). - * - * [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. - * - * From the perspective of dealing with error handling, deferred and promise apis are to - * asynchronous programing what `try`, `catch` and `throw` keywords are to synchronous programing. - * - *
- *   // for the purpose of this example let's assume that variables `$q` and `scope` are
- *   // available in the current lexical scope (they could have been injected or passed in).
- *
- *   function asyncGreet(name) {
- *     var deferred = $q.defer();
- *
- *     setTimeout(function() {
- *       // since this fn executes async in a future turn of the event loop, we need to wrap
- *       // our code into an $apply call so that the model changes are properly observed.
- *       scope.$apply(function() {
- *         if (okToGreet(name)) {
- *           deferred.resolve('Hello, ' + name + '!');
- *         } else {
- *           deferred.reject('Greeting ' + name + ' is not allowed.');
- *         }
- *       });
- *     }, 1000);
- *
- *     return deferred.promise;
- *   }
- *
- *   var promise = asyncGreet('Robin Hood');
- *   promise.then(function(greeting) {
- *     alert('Success: ' + greeting);
- *   }, function(reason) {
- *     alert('Failed: ' + reason);
- *   );
- * 
- * - * At first it might not be obvious why this extra complexity is worth the trouble. The payoff - * comes in the way of - * [guarantees that promise and deferred apis make](https://github.com/kriskowal/uncommonjs/blob/master/promises/specification.md). - * - * Additionally the promise api allows for composition that is very hard to do with the - * traditional callback ([CPS](http://en.wikipedia.org/wiki/Continuation-passing_style)) approach. - * 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()`. - * - * The purpose of the deferred object is to expose the associated Promise instance as well as apis - * that can be used for signaling the successful or unsuccessful completion of the task. - * - * **Methods** - * - * - `resolve(value)` – resolves the derived promise with the `value`. If the value is a rejection - * constructed via `$q.reject`, the promise will be rejected instead. - * - `reject(reason)` – rejects the derived promise with the `reason`. This is equivalent to - * resolving it with a rejection constructed via `$q.reject`. - * - * **Properties** - * - * - promise – `{Promise}` – promise object associated with this deferred. - * - * - * # The Promise API - * - * A new promise instance is created when a deferred instance is created and can be retrieved by - * calling `deferred.promise`. - * - * The purpose of the promise object is to allow for interested parties to get access to the result - * of the deferred task when it completes. - * - * **Methods** - * - * - `then(successCallback, errorCallback)` – regardless of when the promise was or will be resolved - * or rejected calls one of the success or error callbacks asynchronously as soon as the result - * is available. The callbacks are called with a single argument the result or rejection reason. - * - * This method *returns a new promise* which is resolved or rejected via the return value of the - * `successCallback` or `errorCallback`. - * - * - * # Chaining promises - * - * Because calling `then` api of a promise returns a new derived promise, it is easily possible - * to create a chain of promises: - * - *
- *   promiseB = promiseA.then(function(result) {
- *     return result + 1;
- *   });
- *
- *   // promiseB will be resolved immediately after promiseA is resolved and it's value will be
- *   // the result of promiseA incremented by 1
- * 
- * - * It is possible to create chains of any length and since a promise can be resolved with another - * promise (which will defer its resolution further), it is possible to pause/defer resolution of - * the promises at any point in the chain. This makes it possible to implement powerful apis like - * $http's response interceptors. - * - * - * # Differences between Kris Kowal's Q and $q - * - * There are three main differences: - * - * - $q is integrated with the {@link angular.module.ng.$rootScope.Scope} Scope model observation - * mechanism in angular, which means faster propagation of resolution or rejection into your - * models and avoiding unnecessary browser repaints, which would result in flickering UI. - * - $q promises are recognized by the templating engine in angular, which means that in templates - * you can treat promises attached to a scope as if they were the resulting values. - * - Q has many more features that $q, but that comes at a cost of bytes. $q is tiny, but contains - * all the important functionality needed for common async tasks. - */ -function $QProvider() { - - this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) { - return qFactory(function(callback) { - $rootScope.$evalAsync(callback); - }, $exceptionHandler); - }]; -} - - -/** - * Constructs a promise manager. - * - * @param {function(function)} nextTick Function for executing functions in the next turn. - * @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for - * debugging purposes. - * @returns {object} Promise manager. - */ -function qFactory(nextTick, exceptionHandler) { - - /** - * @ngdoc - * @name angular.module.ng.$q#defer - * @methodOf angular.module.ng.$q - * @description - * Creates a `Deferred` object which represents a task which will finish in the future. - * - * @returns {Deferred} Returns a new instance of deferred. - */ - var defer = function() { - var pending = [], - value, deferred; - - deferred = { - - resolve: function(val) { - if (pending) { - var callbacks = pending; - pending = undefined; - value = ref(val); - - if (callbacks.length) { - nextTick(function() { - var callback; - for (var i = 0, ii = callbacks.length; i < ii; i++) { - callback = callbacks[i]; - value.then(callback[0], callback[1]); - } - }); - } - } - }, - - - reject: function(reason) { - deferred.resolve(reject(reason)); - }, - - - promise: { - then: function(callback, errback) { - var result = defer(); - - var wrappedCallback = function(value) { - try { - result.resolve((callback || defaultCallback)(value)); - } catch(e) { - exceptionHandler(e); - result.reject(e); - } - }; - - var wrappedErrback = function(reason) { - try { - result.resolve((errback || defaultErrback)(reason)); - } catch(e) { - exceptionHandler(e); - result.reject(e); - } - }; - - if (pending) { - pending.push([wrappedCallback, wrappedErrback]); - } else { - value.then(wrappedCallback, wrappedErrback); - } - - return result.promise; - } - } - }; - - return deferred; - }; - - - var ref = function(value) { - if (value && value.then) return value; - return { - then: function(callback) { - var result = defer(); - nextTick(function() { - result.resolve(callback(value)); - }); - return result.promise; - } - }; - }; - - - /** - * @ngdoc - * @name angular.module.ng.$q#reject - * @methodOf angular.module.ng.$q - * @description - * Creates a promise that is resolved as rejected with the specified `reason`. This api should be - * used to forward rejection in a chain of promises. If you are dealing with the last promise in - * a promise chain, you don't need to worry about it. - * - * When comparing deferreds/promises to the familiar behavior of try/catch/throw, think of - * `reject` as the `throw` keyword in JavaScript. This also means that if you "catch" an error via - * a promise error callback and you want to forward the error to the promise derived from the - * current promise, you have to "rethrow" the error by returning a rejection constructed via - * `reject`. - * - *
-   *   promiseB = promiseA.then(function(result) {
-   *     // success: do something and resolve promiseB
-   *     //          with the old or a new result
-   *     return result;
-   *   }, function(reason) {
-   *     // error: handle the error if possible and
-   *     //        resolve promiseB with newPromiseOrValue,
-   *     //        otherwise forward the rejection to promiseB
-   *     if (canHandle(reason)) {
-   *      // handle the error and recover
-   *      return newPromiseOrValue;
-   *     }
-   *     return $q.reject(reason);
-   *   });
-   * 
- * - * @param {*} reason Constant, message, exception or an object representing the rejection reason. - * @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`. - */ - var reject = function(reason) { - return { - then: function(callback, errback) { - var result = defer(); - nextTick(function() { - result.resolve(errback(reason)); - }); - return result.promise; - } - }; - }; - - - /** - * @ngdoc - * @name angular.module.ng.$q#when - * @methodOf angular.module.ng.$q - * @description - * Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise. - * This is useful when you are dealing with on object that might or might not be a promise, or if - * the promise comes from a source that can't be trusted. - * - * @param {*} value Value or a promise - * @returns {Promise} Returns a single promise that will be resolved with an array of values, - * each value coresponding to the promise at the same index in the `promises` array. If any of - * the promises is resolved with a rejection, this resulting promise will be resolved with the - * same rejection. - */ - var when = function(value, callback, errback) { - var result = defer(), - done; - - var wrappedCallback = function(value) { - try { - return (callback || defaultCallback)(value); - } catch (e) { - exceptionHandler(e); - return reject(e); - } - }; - - var wrappedErrback = function(reason) { - try { - return (errback || defaultErrback)(reason); - } catch (e) { - exceptionHandler(e); - return reject(e); - } - }; - - nextTick(function() { - ref(value).then(function(value) { - if (done) return; - done = true; - result.resolve(ref(value).then(wrappedCallback, wrappedErrback)); - }, function(reason) { - if (done) return; - done = true; - result.resolve(wrappedErrback(reason)); - }); - }); - - return result.promise; - }; - - - function defaultCallback(value) { - return value; - } - - - function defaultErrback(reason) { - return reject(reason); - } - - - /** - * @ngdoc - * @name angular.module.ng.$q#all - * @methodOf angular.module.ng.$q - * @description - * Combines multiple promises into a single promise that is resolved when all of the input - * promises are resolved. - * - * @param {Array.} promises An array of promises. - * @returns {Promise} Returns a single promise that will be resolved with an array of values, - * each value coresponding to the promise at the same index in the `promises` array. If any of - * the promises is resolved with a rejection, this resulting promise will be resolved with the - * same rejection. - */ - function all(promises) { - var deferred = defer(), - counter = promises.length, - results = []; - - forEach(promises, function(promise, index) { - promise.then(function(value) { - if (index in results) return; - results[index] = value; - if (!(--counter)) deferred.resolve(results); - }, function(reason) { - if (index in results) return; - deferred.reject(reason); - }); - }); - - return deferred.promise; - } - - return { - defer: defer, - reject: reject, - when: when, - all: all - }; -} diff --git a/src/service/q.js b/src/service/q.js new file mode 100644 index 00000000..f0c030b0 --- /dev/null +++ b/src/service/q.js @@ -0,0 +1,387 @@ +'use strict'; + +/** + * @ngdoc service + * @name angular.module.ng.$q + * @requires $rootScope + * + * @description + * A promise/deferred implementation inspired by [Kris Kowal's Q](https://github.com/kriskowal/q). + * + * [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. + * + * From the perspective of dealing with error handling, deferred and promise apis are to + * asynchronous programing what `try`, `catch` and `throw` keywords are to synchronous programing. + * + *
+ *   // for the purpose of this example let's assume that variables `$q` and `scope` are
+ *   // available in the current lexical scope (they could have been injected or passed in).
+ *
+ *   function asyncGreet(name) {
+ *     var deferred = $q.defer();
+ *
+ *     setTimeout(function() {
+ *       // since this fn executes async in a future turn of the event loop, we need to wrap
+ *       // our code into an $apply call so that the model changes are properly observed.
+ *       scope.$apply(function() {
+ *         if (okToGreet(name)) {
+ *           deferred.resolve('Hello, ' + name + '!');
+ *         } else {
+ *           deferred.reject('Greeting ' + name + ' is not allowed.');
+ *         }
+ *       });
+ *     }, 1000);
+ *
+ *     return deferred.promise;
+ *   }
+ *
+ *   var promise = asyncGreet('Robin Hood');
+ *   promise.then(function(greeting) {
+ *     alert('Success: ' + greeting);
+ *   }, function(reason) {
+ *     alert('Failed: ' + reason);
+ *   );
+ * 
+ * + * At first it might not be obvious why this extra complexity is worth the trouble. The payoff + * comes in the way of + * [guarantees that promise and deferred apis make](https://github.com/kriskowal/uncommonjs/blob/master/promises/specification.md). + * + * Additionally the promise api allows for composition that is very hard to do with the + * traditional callback ([CPS](http://en.wikipedia.org/wiki/Continuation-passing_style)) approach. + * 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()`. + * + * The purpose of the deferred object is to expose the associated Promise instance as well as apis + * that can be used for signaling the successful or unsuccessful completion of the task. + * + * **Methods** + * + * - `resolve(value)` – resolves the derived promise with the `value`. If the value is a rejection + * constructed via `$q.reject`, the promise will be rejected instead. + * - `reject(reason)` – rejects the derived promise with the `reason`. This is equivalent to + * resolving it with a rejection constructed via `$q.reject`. + * + * **Properties** + * + * - promise – `{Promise}` – promise object associated with this deferred. + * + * + * # The Promise API + * + * A new promise instance is created when a deferred instance is created and can be retrieved by + * calling `deferred.promise`. + * + * The purpose of the promise object is to allow for interested parties to get access to the result + * of the deferred task when it completes. + * + * **Methods** + * + * - `then(successCallback, errorCallback)` – regardless of when the promise was or will be resolved + * or rejected calls one of the success or error callbacks asynchronously as soon as the result + * is available. The callbacks are called with a single argument the result or rejection reason. + * + * This method *returns a new promise* which is resolved or rejected via the return value of the + * `successCallback` or `errorCallback`. + * + * + * # Chaining promises + * + * Because calling `then` api of a promise returns a new derived promise, it is easily possible + * to create a chain of promises: + * + *
+ *   promiseB = promiseA.then(function(result) {
+ *     return result + 1;
+ *   });
+ *
+ *   // promiseB will be resolved immediately after promiseA is resolved and it's value will be
+ *   // the result of promiseA incremented by 1
+ * 
+ * + * It is possible to create chains of any length and since a promise can be resolved with another + * promise (which will defer its resolution further), it is possible to pause/defer resolution of + * the promises at any point in the chain. This makes it possible to implement powerful apis like + * $http's response interceptors. + * + * + * # Differences between Kris Kowal's Q and $q + * + * There are three main differences: + * + * - $q is integrated with the {@link angular.module.ng.$rootScope.Scope} Scope model observation + * mechanism in angular, which means faster propagation of resolution or rejection into your + * models and avoiding unnecessary browser repaints, which would result in flickering UI. + * - $q promises are recognized by the templating engine in angular, which means that in templates + * you can treat promises attached to a scope as if they were the resulting values. + * - Q has many more features that $q, but that comes at a cost of bytes. $q is tiny, but contains + * all the important functionality needed for common async tasks. + */ +function $QProvider() { + + this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) { + return qFactory(function(callback) { + $rootScope.$evalAsync(callback); + }, $exceptionHandler); + }]; +} + + +/** + * Constructs a promise manager. + * + * @param {function(function)} nextTick Function for executing functions in the next turn. + * @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for + * debugging purposes. + * @returns {object} Promise manager. + */ +function qFactory(nextTick, exceptionHandler) { + + /** + * @ngdoc + * @name angular.module.ng.$q#defer + * @methodOf angular.module.ng.$q + * @description + * Creates a `Deferred` object which represents a task which will finish in the future. + * + * @returns {Deferred} Returns a new instance of deferred. + */ + var defer = function() { + var pending = [], + value, deferred; + + deferred = { + + resolve: function(val) { + if (pending) { + var callbacks = pending; + pending = undefined; + value = ref(val); + + if (callbacks.length) { + nextTick(function() { + var callback; + for (var i = 0, ii = callbacks.length; i < ii; i++) { + callback = callbacks[i]; + value.then(callback[0], callback[1]); + } + }); + } + } + }, + + + reject: function(reason) { + deferred.resolve(reject(reason)); + }, + + + promise: { + then: function(callback, errback) { + var result = defer(); + + var wrappedCallback = function(value) { + try { + result.resolve((callback || defaultCallback)(value)); + } catch(e) { + exceptionHandler(e); + result.reject(e); + } + }; + + var wrappedErrback = function(reason) { + try { + result.resolve((errback || defaultErrback)(reason)); + } catch(e) { + exceptionHandler(e); + result.reject(e); + } + }; + + if (pending) { + pending.push([wrappedCallback, wrappedErrback]); + } else { + value.then(wrappedCallback, wrappedErrback); + } + + return result.promise; + } + } + }; + + return deferred; + }; + + + var ref = function(value) { + if (value && value.then) return value; + return { + then: function(callback) { + var result = defer(); + nextTick(function() { + result.resolve(callback(value)); + }); + return result.promise; + } + }; + }; + + + /** + * @ngdoc + * @name angular.module.ng.$q#reject + * @methodOf angular.module.ng.$q + * @description + * Creates a promise that is resolved as rejected with the specified `reason`. This api should be + * used to forward rejection in a chain of promises. If you are dealing with the last promise in + * a promise chain, you don't need to worry about it. + * + * When comparing deferreds/promises to the familiar behavior of try/catch/throw, think of + * `reject` as the `throw` keyword in JavaScript. This also means that if you "catch" an error via + * a promise error callback and you want to forward the error to the promise derived from the + * current promise, you have to "rethrow" the error by returning a rejection constructed via + * `reject`. + * + *
+   *   promiseB = promiseA.then(function(result) {
+   *     // success: do something and resolve promiseB
+   *     //          with the old or a new result
+   *     return result;
+   *   }, function(reason) {
+   *     // error: handle the error if possible and
+   *     //        resolve promiseB with newPromiseOrValue,
+   *     //        otherwise forward the rejection to promiseB
+   *     if (canHandle(reason)) {
+   *      // handle the error and recover
+   *      return newPromiseOrValue;
+   *     }
+   *     return $q.reject(reason);
+   *   });
+   * 
+ * + * @param {*} reason Constant, message, exception or an object representing the rejection reason. + * @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`. + */ + var reject = function(reason) { + return { + then: function(callback, errback) { + var result = defer(); + nextTick(function() { + result.resolve(errback(reason)); + }); + return result.promise; + } + }; + }; + + + /** + * @ngdoc + * @name angular.module.ng.$q#when + * @methodOf angular.module.ng.$q + * @description + * Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise. + * This is useful when you are dealing with on object that might or might not be a promise, or if + * the promise comes from a source that can't be trusted. + * + * @param {*} value Value or a promise + * @returns {Promise} Returns a single promise that will be resolved with an array of values, + * each value coresponding to the promise at the same index in the `promises` array. If any of + * the promises is resolved with a rejection, this resulting promise will be resolved with the + * same rejection. + */ + var when = function(value, callback, errback) { + var result = defer(), + done; + + var wrappedCallback = function(value) { + try { + return (callback || defaultCallback)(value); + } catch (e) { + exceptionHandler(e); + return reject(e); + } + }; + + var wrappedErrback = function(reason) { + try { + return (errback || defaultErrback)(reason); + } catch (e) { + exceptionHandler(e); + return reject(e); + } + }; + + nextTick(function() { + ref(value).then(function(value) { + if (done) return; + done = true; + result.resolve(ref(value).then(wrappedCallback, wrappedErrback)); + }, function(reason) { + if (done) return; + done = true; + result.resolve(wrappedErrback(reason)); + }); + }); + + return result.promise; + }; + + + function defaultCallback(value) { + return value; + } + + + function defaultErrback(reason) { + return reject(reason); + } + + + /** + * @ngdoc + * @name angular.module.ng.$q#all + * @methodOf angular.module.ng.$q + * @description + * Combines multiple promises into a single promise that is resolved when all of the input + * promises are resolved. + * + * @param {Array.} promises An array of promises. + * @returns {Promise} Returns a single promise that will be resolved with an array of values, + * each value coresponding to the promise at the same index in the `promises` array. If any of + * the promises is resolved with a rejection, this resulting promise will be resolved with the + * same rejection. + */ + function all(promises) { + var deferred = defer(), + counter = promises.length, + results = []; + + forEach(promises, function(promise, index) { + promise.then(function(value) { + if (index in results) return; + results[index] = value; + if (!(--counter)) deferred.resolve(results); + }, function(reason) { + if (index in results) return; + deferred.reject(reason); + }); + }); + + return deferred.promise; + } + + return { + defer: defer, + reject: reject, + when: when, + all: all + }; +} diff --git a/test/DeferredSpec.js b/test/DeferredSpec.js deleted file mode 100644 index e592ab87..00000000 --- a/test/DeferredSpec.js +++ /dev/null @@ -1,823 +0,0 @@ -'use strict'; - -/** - http://wiki.commonjs.org/wiki/Promises - http://www.slideshare.net/domenicdenicola/callbacks-promises-and-coroutines-oh-my-the-evolution-of-asynchronicity-in-javascript - - Q: https://github.com/kriskowal/q - https://github.com/kriskowal/q/blob/master/design/README.js - https://github.com/kriskowal/uncommonjs/blob/master/promises/specification.md - http://jsconf.eu/2010/speaker/commonjs_i_promise_by_kris_kow.html - - good walkthrough of the Q api's and design, jump to 15:30 - - twisted: http://twistedmatrix.com/documents/11.0.0/api/twisted.internet.defer.Deferred.html - dojo: https://github.com/dojo/dojo/blob/master/_base/Deferred.js - http://dojotoolkit.org/api/1.6/dojo/Deferred - http://dojotoolkit.org/documentation/tutorials/1.6/promises/ - when.js: https://github.com/briancavalier/when.js - DART: http://www.dartlang.org/docs/api/Promise.html#Promise::Promise - http://code.google.com/p/dart/source/browse/trunk/dart/corelib/src/promise.dart - http://codereview.chromium.org/8271014/patch/11003/12005 - https://chromereviews.googleplex.com/3365018/ - WinJS: http://msdn.microsoft.com/en-us/library/windows/apps/br211867.aspx - - http://download.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/Future.html - http://en.wikipedia.org/wiki/Futures_and_promises - http://wiki.ecmascript.org/doku.php?id=strawman:deferred_functions - http://wiki.ecmascript.org/doku.php?id=strawman:async_functions - - - http://jsperf.com/throw-vs-return -*/ - -describe('q', function() { - var q, defer, deferred, promise, log; - - /** - * Creates a callback that logs its invocation in `log`. - * - * @param {(number|string)} name Suffix for 'success' name. e.g. success(1) => success1 - * @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 success(name, returnVal, throwReturnVal) { - var returnValDefined = (arguments.length >= 2); - - return function() { - name = 'success' + (name || ''); - var args = toJson(sliceArgs(arguments)).replace(/(^\[|"|\]$)/g, ''); - log.push(name + '(' + args + ')'); - returnVal = returnValDefined ? returnVal : arguments[0]; - if (throwReturnVal) throw returnVal; - return returnVal; - } - } - - - /** - * Creates a callback that logs its invocation in `log`. - * - * @param {(number|string)} name Suffix for 'error' name. e.g. error(1) => error1 - * @param {*=} returnVal Value that the callback should return. If unspecified, the passed in - * value is rethrown. - * @param {boolean=} throwReturnVal If true, the `returnVal` will be thrown rather than returned. - */ - function error(name, returnVal, throwReturnVal) { - var returnValDefined = (arguments.length >= 2); - - return function(){ - name = 'error' + (name || ''); - log.push(name + '(' + [].join.call(arguments, ',') + ')'); - returnVal = returnValDefined ? returnVal : q.reject(arguments[0]); - if (throwReturnVal) throw returnVal; - return returnVal; - } - } - - - /** helper for synchronous resolution of deferred */ - function syncResolve(deferred, result) { - deferred.resolve(result); - mockNextTick.flush(); - } - - - /** helper for synchronous rejection of deferred */ - function syncReject(deferred, reason) { - deferred.reject(reason); - mockNextTick.flush(); - } - - - /** converts the `log` to a '; '-separated string */ - function logStr() { - return log.join('; '); - } - - - var mockNextTick = { - nextTick: function(task) { - mockNextTick.queue.push(task); - }, - queue: [], - flush: function() { - if (!mockNextTick.queue.length) throw new Error('Nothing to be flushed!'); - while (mockNextTick.queue.length) { - var queue = mockNextTick.queue; - mockNextTick.queue = []; - forEach(queue, function(task) { - try { - task(); - } catch(e) { - dump('exception in mockNextTick:', e, e.name, e.message, task); - } - }); - } - } - } - - - beforeEach(function() { - q = qFactory(mockNextTick.nextTick, noop), - defer = q.defer; - deferred = defer() - promise = deferred.promise; - log = []; - mockNextTick.queue = []; - }); - - - afterEach(function() { - expect(mockNextTick.queue.length).toBe(0); - }); - - - describe('defer', function() { - it('should create a new deferred', function() { - expect(deferred.promise).toBeDefined(); - expect(deferred.resolve).toBeDefined(); - expect(deferred.reject).toBeDefined(); - }); - - - describe('resolve', function() { - it('should fulfill the promise and execute all success callbacks in the registration order', - function() { - promise.then(success(1), error()); - promise.then(success(2), error()); - expect(logStr()).toBe(''); - - deferred.resolve('foo'); - mockNextTick.flush(); - expect(logStr()).toBe('success1(foo); success2(foo)'); - }); - - - it('should do nothing if a promise was previously resolved', function() { - promise.then(success(), error()); - expect(logStr()).toBe(''); - - deferred.resolve('foo'); - mockNextTick.flush(); - expect(logStr()).toBe('success(foo)'); - - log = []; - deferred.resolve('bar'); - deferred.reject('baz'); - expect(mockNextTick.queue.length).toBe(0); - expect(logStr()).toBe(''); - }); - - - it('should do nothing if a promise was previously rejected', function() { - promise.then(success(), error()); - expect(logStr()).toBe(''); - - deferred.reject('foo'); - mockNextTick.flush(); - expect(logStr()).toBe('error(foo)'); - - log = []; - deferred.resolve('bar'); - deferred.reject('baz'); - expect(mockNextTick.queue.length).toBe(0); - expect(logStr()).toBe(''); - }); - - - it('should allow deferred resolution with a new promise', function() { - var deferred2 = defer(); - promise.then(success(), error()); - - deferred.resolve(deferred2.promise); - mockNextTick.flush(); - expect(logStr()).toBe(''); - - deferred2.resolve('foo'); - mockNextTick.flush(); - expect(logStr()).toBe('success(foo)'); - }); - - - it('should call the callback in the next turn', function() { - promise.then(success()); - expect(logStr()).toBe(''); - - deferred.resolve('foo'); - expect(logStr()).toBe(''); - - mockNextTick.flush(); - expect(logStr()).toBe('success(foo)'); - }); - - - it('should support non-bound execution', function() { - var resolver = deferred.resolve; - promise.then(success(), error()); - resolver('detached'); - mockNextTick.flush(); - expect(logStr()).toBe('success(detached)'); - }); - - - it('should not break if a callbacks registers another callback', function() { - promise.then(function() { - log.push('outer'); - promise.then(function() { - log.push('inner'); - }); - }); - - deferred.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() { - promise.then(function(val) { - log.push('success1(' + val + ')'); - deferred.resolve('bar'); - }); - - promise.then(success(2)); - - deferred.resolve('foo'); - expect(logStr()).toBe(''); - - mockNextTick.flush(); - expect(logStr()).toBe('success1(foo); success2(foo)'); - }); - }); - - - describe('reject', function() { - it('should reject the promise and execute all error callbacks in the registration order', - function() { - promise.then(success(), error(1)); - promise.then(success(), error(2)); - expect(logStr()).toBe(''); - - deferred.reject('foo'); - mockNextTick.flush(); - expect(logStr()).toBe('error1(foo); error2(foo)'); - }); - - - it('should do nothing if a promise was previously resolved', function() { - promise.then(success(1), error(1)); - expect(logStr()).toBe(''); - - deferred.resolve('foo'); - mockNextTick.flush(); - expect(logStr()).toBe('success1(foo)'); - - log = []; - deferred.reject('bar'); - deferred.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)'); - }); - - - it('should do nothing if a promise was previously rejected', function() { - promise.then(success(1), error(1)); - expect(logStr()).toBe(''); - - deferred.reject('foo'); - mockNextTick.flush(); - expect(logStr()).toBe('error1(foo)'); - - log = []; - deferred.reject('bar'); - deferred.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)'); - }); - - - it('should not defer rejection with a new promise', function() { - var deferred2 = defer(); - promise.then(success(), error()); - - deferred.reject(deferred2.promise); - mockNextTick.flush(); - expect(logStr()).toBe('error([object Object])'); - }); - - - it('should call the error callback in the next turn', function() { - promise.then(success(), error()); - expect(logStr()).toBe(''); - - deferred.reject('foo'); - expect(logStr()).toBe(''); - - mockNextTick.flush(); - expect(logStr()).toBe('error(foo)'); - }); - - - it('should support non-bound execution', function() { - var rejector = deferred.reject; - promise.then(success(), error()); - rejector('detached'); - mockNextTick.flush(); - expect(logStr()).toBe('error(detached)'); - }); - }); - - - describe('promise', function() { - it('should have a then method', function() { - expect(typeof promise.then).toBe('function'); - }); - - - describe('then', function() { - it('should allow registration of a success callback without an errback and resolve', - function() { - promise.then(success()); - syncResolve(deferred, 'foo'); - expect(logStr()).toBe('success(foo)'); - }); - - it('should allow registration of a success callback without an errback and reject', - function() { - promise.then(success()); - syncReject(deferred, 'foo'); - expect(logStr()).toBe(''); - }); - - - it('should allow registration of an errback without a success callback and reject', - function() { - promise.then(null, error()); - syncReject(deferred, 'oops!'); - expect(logStr()).toBe('error(oops!)'); - }); - - - it('should allow registration of an errback without a success callback and resolve', - function() { - promise.then(null, error()); - syncResolve(deferred, 'done'); - 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', 'dVal'), error()); - - expect(logStr()).toBe(''); - syncResolve(deferred, 'yup'); - expect(logStr()).toBe('successA(yup); successB(yup); successC(yup); successD(yup)'); - }); - - - 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')); - - expect(logStr()).toBe(''); - syncReject(deferred, 'noo!'); - expect(logStr()).toBe('errorA(noo!); errorB(noo!); errorC(noo!); errorD(noo!)'); - }); - - - it('should propagate resolution and rejection between dependent promises', function() { - 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(''); - syncResolve(deferred, 'sweet!'); - expect(log).toEqual(['success1(sweet!)', - 'success2(x)', - 'error3(y)', - 'error4(z)', - 'success5(done)']); - }); - - - it('should reject a derived promise if an exception is thrown while resolving its parent', - function() { - promise.then(success(1, 'oops', true)). - then(success(2), error(2)); - syncResolve(deferred, 'done!'); - expect(logStr()).toBe('success1(done!); error2(oops)'); - }); - - - it('should reject a derived promise if an exception is thrown while rejecting its parent', - function() { - promise.then(null, error(1, 'oops', true)). - then(success(2), error(2)); - syncReject(deferred, 'timeout'); - expect(logStr()).toBe('error1(timeout); error2(oops)'); - }); - - - it('should call success callback in the next turn even if promise is already resolved', - function() { - deferred.resolve('done!'); - - promise.then(success()); - expect(logStr()).toBe(''); - - mockNextTick.flush(); - expect(log).toEqual(['success(done!)']); - }); - - - it('should call errpr callback in the next turn even if promise is already rejected', - function() { - deferred.reject('oops!'); - - promise.then(null, error()); - expect(logStr()).toBe(''); - - mockNextTick.flush(); - expect(log).toEqual(['error(oops!)']); - }); - }); - }); - }); - - - describe('reject', function() { - it('should package a string into a rejected promise', function() { - var rejectedPromise = q.reject('not gonna happen'); - promise.then(success(), error()); - syncResolve(deferred, rejectedPromise); - expect(log).toEqual(['error(not gonna happen)']); - }); - - - it('should package an exception into a rejected promise', function() { - var rejectedPromise = q.reject(Error('not gonna happen')); - promise.then(success(), error()); - syncResolve(deferred, rejectedPromise); - expect(log).toEqual(['error(Error: not gonna happen)']); - }); - }); - - - describe('when', function() { - describe('resolution', function() { - it('should call the success callback in the next turn when the value is a non-promise', - function() { - q.when('hello', success(), error()); - expect(logStr()).toBe(''); - mockNextTick.flush(); - expect(logStr()).toBe('success(hello)'); - }); - - - it('should call the success callback in the next turn when the value is a resolved promise', - function() { - deferred.resolve('hello'); - q.when(deferred.promise, success(), error()); - expect(logStr()).toBe(''); - mockNextTick.flush(); - expect(logStr()).toBe('success(hello)'); - }); - - - it('should call the errback in the next turn when the value is a rejected promise', function() { - deferred.reject('nope'); - q.when(deferred.promise, success(), error()); - expect(logStr()).toBe(''); - mockNextTick.flush(); - expect(logStr()).toBe('error(nope)'); - }); - - - it('should call the success callback after the original promise is resolved', - function() { - q.when(deferred.promise, success(), error()); - expect(logStr()).toBe(''); - mockNextTick.flush(); - expect(logStr()).toBe(''); - syncResolve(deferred, 'hello'); - expect(logStr()).toBe('success(hello)'); - }); - - - it('should call the errback after the orignal promise is rejected', - function() { - q.when(deferred.promise, success(), error()); - expect(logStr()).toBe(''); - mockNextTick.flush(); - expect(logStr()).toBe(''); - syncReject(deferred, 'nope'); - expect(logStr()).toBe('error(nope)'); - }); - }); - - - describe('optional callbacks', function() { - it('should not require success callback and propagate resolution', function() { - q.when('hi', null, error()).then(success(2), error()); - expect(logStr()).toBe(''); - mockNextTick.flush(); - expect(logStr()).toBe('success2(hi)'); - }); - - - it('should not require success callback and propagate rejection', function() { - q.when(q.reject('sorry'), null, error(1)).then(success(), error(2)); - expect(logStr()).toBe(''); - mockNextTick.flush(); - expect(logStr()).toBe('error1(sorry); error2(sorry)'); - }); - - - it('should not require errback and propagate resolution', function() { - q.when('hi', success(1, 'hello')).then(success(2), error()); - expect(logStr()).toBe(''); - mockNextTick.flush(); - expect(logStr()).toBe('success1(hi); success2(hello)'); - }); - - - it('should not require errback and propagate rejection', function() { - q.when(q.reject('sorry'), success()).then(success(2), error(2)); - expect(logStr()).toBe(''); - mockNextTick.flush(); - expect(logStr()).toBe('error2(sorry)'); - }); - }); - - - describe('returned promise', function() { - it('should return a promise that can be resolved with a value returned from the success ' + - 'callback', function() { - q.when('hello', success(1, 'hi'), error()).then(success(2), error()); - mockNextTick.flush(); - expect(logStr()).toBe('success1(hello); success2(hi)'); - }); - - - it('should return a promise that can be rejected with a rejected promise returned from the ' + - 'success callback', function() { - q.when('hello', success(1, q.reject('sorry')), error()).then(success(), error(2)); - mockNextTick.flush(); - expect(logStr()).toBe('success1(hello); error2(sorry)'); - }); - - - it('should return a promise that can be resolved with a value returned from the errback', - function() { - q.when(q.reject('sorry'), success(), error(1, 'hi')).then(success(2), error()); - mockNextTick.flush(); - expect(logStr()).toBe('error1(sorry); success2(hi)'); - }); - - - it('should return a promise that can be rejected with a rejected promise returned from the ' + - 'errback', function() { - q.when(q.reject('sorry'), success(), error(1, q.reject('sigh'))).then(success(), error(2)); - mockNextTick.flush(); - expect(logStr()).toBe('error1(sorry); error2(sigh)'); - }); - - - it('should return a promise that can be resolved with a promise returned from the success ' + - 'callback', function() { - var deferred2 = defer(); - q.when('hi', success(1, deferred2.promise), error()).then(success(2), error()); - mockNextTick.flush(); - expect(logStr()).toBe('success1(hi)'); - syncResolve(deferred2, 'finally!'); - expect(logStr()).toBe('success1(hi); success2(finally!)'); - }); - - - it('should return a promise that can be resolved with promise returned from the errback ' + - 'callback', function() { - var deferred2 = defer(); - q.when(q.reject('sorry'), success(), error(1, deferred2.promise)).then(success(2), error()); - mockNextTick.flush(); - expect(logStr()).toBe('error1(sorry)'); - syncResolve(deferred2, 'finally!'); - expect(logStr()).toBe('error1(sorry); success2(finally!)'); - }); - }); - - - describe('security', function() { - it('should call success callback only once even if the original promise gets fullfilled ' + - 'multiple times', function() { - var evilPromise = { - then: function(success, error) { - evilPromise.success = success; - evilPromise.error = error; - } - } - - q.when(evilPromise, success(), error()); - mockNextTick.flush(); - expect(logStr()).toBe(''); - evilPromise.success('done'); - mockNextTick.flush(); // TODO(i) wrong queue, evil promise would be resolved outside of the - // scope.$apply lifecycle and in that case we should have some kind - // of fallback queue for calling our callbacks from. Otherwise the - // application will get stuck until something triggers next $apply. - expect(logStr()).toBe('success(done)'); - - evilPromise.success('evil is me'); - evilPromise.error('burn burn'); - expect(logStr()).toBe('success(done)'); - }); - - - it('should call errback only once even if the original promise gets fullfilled multiple ' + - 'times', function() { - var evilPromise = { - then: function(success, error) { - evilPromise.success = success; - evilPromise.error = error; - } - } - - q.when(evilPromise, success(), error()); - mockNextTick.flush(); - expect(logStr()).toBe(''); - evilPromise.error('failed'); - expect(logStr()).toBe('error(failed)'); - - evilPromise.error('muhaha'); - evilPromise.success('take this'); - expect(logStr()).toBe('error(failed)'); - }); - }); - }); - - - describe('all', function() { - it('should take an array of promises and return a promise for an array of results', function() { - var deferred1 = defer(), - deferred2 = defer(); - - q.all([promise, deferred1.promise, deferred2.promise]).then(success(), error()); - expect(logStr()).toBe(''); - syncResolve(deferred, 'hi'); - expect(logStr()).toBe(''); - syncResolve(deferred2, 'cau'); - expect(logStr()).toBe(''); - syncResolve(deferred1, 'hola'); - expect(logStr()).toBe('success([hi,hola,cau])'); - }); - - - it('should reject the derived promise if at least one of the promises in the array is rejected', - function() { - var deferred1 = defer(), - deferred2 = defer(); - - q.all([promise, deferred1.promise, deferred2.promise]).then(success(), error()); - expect(logStr()).toBe(''); - syncResolve(deferred2, 'cau'); - expect(logStr()).toBe(''); - syncReject(deferred1, 'oops'); - expect(logStr()).toBe('error(oops)'); - }); - - - it('should ignore multiple resolutions of an (evil) array promise', function() { - var evilPromise = { - then: function(success, error) { - evilPromise.success = success; - evilPromise.error = error; - } - } - - q.all([promise, evilPromise]).then(success(), error()); - expect(logStr()).toBe(''); - - evilPromise.success('first'); - evilPromise.success('muhaha'); - evilPromise.error('arghhh'); - expect(logStr()).toBe(''); - - syncResolve(deferred, 'done'); - expect(logStr()).toBe('success([done,first])'); - }); - }); - - - describe('exception logging', function() { - var mockExceptionLogger = { - log: [], - logger: function(e) { - mockExceptionLogger.log.push(e); - } - } - - - beforeEach(function() { - q = qFactory(mockNextTick.nextTick, mockExceptionLogger.logger), - defer = q.defer; - deferred = defer() - promise = deferred.promise; - log = []; - mockExceptionLogger.log = []; - }); - - - describe('in then', function() { - it('should log exceptions thrown in a success callback and reject the derived promise', - function() { - var success1 = success(1, 'oops', true); - promise.then(success1).then(success(2), error(2)); - syncResolve(deferred, 'done'); - expect(logStr()).toBe('success1(done); error2(oops)'); - expect(mockExceptionLogger.log).toEqual(['oops']); - }); - - - it('should NOT log exceptions when a success callback returns rejected promise', function() { - promise.then(success(1, q.reject('rejected'))).then(success(2), error(2)); - syncResolve(deferred, 'done'); - expect(logStr()).toBe('success1(done); error2(rejected)'); - expect(mockExceptionLogger.log).toEqual([]); - }); - - - it('should log exceptions thrown in a errback and reject the derived promise', function() { - var error1 = error(1, 'oops', true); - promise.then(null, error1).then(success(2), error(2)); - syncReject(deferred, 'nope'); - expect(logStr()).toBe('error1(nope); error2(oops)'); - expect(mockExceptionLogger.log).toEqual(['oops']); - }); - - - it('should NOT log exceptions when an errback returns a rejected promise', function() { - promise.then(null, error(1, q.reject('rejected'))).then(success(2), error(2)); - syncReject(deferred, 'nope'); - expect(logStr()).toBe('error1(nope); error2(rejected)'); - expect(mockExceptionLogger.log).toEqual([]); - }); - }); - - - describe('in when', function() { - it('should log exceptions thrown in a success callback and reject the derived promise', - function() { - var success1 = success(1, 'oops', true); - q.when('hi', success1, error()).then(success(), error(2)); - mockNextTick.flush(); - expect(logStr()).toBe('success1(hi); error2(oops)'); - expect(mockExceptionLogger.log).toEqual(['oops']); - }); - - - it('should NOT log exceptions when a success callback returns rejected promise', function() { - q.when('hi', success(1, q.reject('rejected'))).then(success(2), error(2)); - mockNextTick.flush(); - expect(logStr()).toBe('success1(hi); error2(rejected)'); - expect(mockExceptionLogger.log).toEqual([]); - }); - - - it('should log exceptions thrown in a errback and reject the derived promise', function() { - var error1 = error(1, 'oops', true); - q.when(q.reject('sorry'), success(), error1).then(success(), error(2)); - mockNextTick.flush(); - expect(logStr()).toBe('error1(sorry); error2(oops)'); - expect(mockExceptionLogger.log).toEqual(['oops']); - }); - - - it('should NOT log exceptions when an errback returns a rejected promise', function() { - q.when(q.reject('sorry'), success(), error(1, q.reject('rejected'))). - then(success(2), error(2)); - mockNextTick.flush(); - expect(logStr()).toBe('error1(sorry); error2(rejected)'); - expect(mockExceptionLogger.log).toEqual([]); - }); - }); - }); -}); diff --git a/test/service/qSpec.js b/test/service/qSpec.js new file mode 100644 index 00000000..e592ab87 --- /dev/null +++ b/test/service/qSpec.js @@ -0,0 +1,823 @@ +'use strict'; + +/** + http://wiki.commonjs.org/wiki/Promises + http://www.slideshare.net/domenicdenicola/callbacks-promises-and-coroutines-oh-my-the-evolution-of-asynchronicity-in-javascript + + Q: https://github.com/kriskowal/q + https://github.com/kriskowal/q/blob/master/design/README.js + https://github.com/kriskowal/uncommonjs/blob/master/promises/specification.md + http://jsconf.eu/2010/speaker/commonjs_i_promise_by_kris_kow.html + - good walkthrough of the Q api's and design, jump to 15:30 + + twisted: http://twistedmatrix.com/documents/11.0.0/api/twisted.internet.defer.Deferred.html + dojo: https://github.com/dojo/dojo/blob/master/_base/Deferred.js + http://dojotoolkit.org/api/1.6/dojo/Deferred + http://dojotoolkit.org/documentation/tutorials/1.6/promises/ + when.js: https://github.com/briancavalier/when.js + DART: http://www.dartlang.org/docs/api/Promise.html#Promise::Promise + http://code.google.com/p/dart/source/browse/trunk/dart/corelib/src/promise.dart + http://codereview.chromium.org/8271014/patch/11003/12005 + https://chromereviews.googleplex.com/3365018/ + WinJS: http://msdn.microsoft.com/en-us/library/windows/apps/br211867.aspx + + http://download.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/Future.html + http://en.wikipedia.org/wiki/Futures_and_promises + http://wiki.ecmascript.org/doku.php?id=strawman:deferred_functions + http://wiki.ecmascript.org/doku.php?id=strawman:async_functions + + + http://jsperf.com/throw-vs-return +*/ + +describe('q', function() { + var q, defer, deferred, promise, log; + + /** + * Creates a callback that logs its invocation in `log`. + * + * @param {(number|string)} name Suffix for 'success' name. e.g. success(1) => success1 + * @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 success(name, returnVal, throwReturnVal) { + var returnValDefined = (arguments.length >= 2); + + return function() { + name = 'success' + (name || ''); + var args = toJson(sliceArgs(arguments)).replace(/(^\[|"|\]$)/g, ''); + log.push(name + '(' + args + ')'); + returnVal = returnValDefined ? returnVal : arguments[0]; + if (throwReturnVal) throw returnVal; + return returnVal; + } + } + + + /** + * Creates a callback that logs its invocation in `log`. + * + * @param {(number|string)} name Suffix for 'error' name. e.g. error(1) => error1 + * @param {*=} returnVal Value that the callback should return. If unspecified, the passed in + * value is rethrown. + * @param {boolean=} throwReturnVal If true, the `returnVal` will be thrown rather than returned. + */ + function error(name, returnVal, throwReturnVal) { + var returnValDefined = (arguments.length >= 2); + + return function(){ + name = 'error' + (name || ''); + log.push(name + '(' + [].join.call(arguments, ',') + ')'); + returnVal = returnValDefined ? returnVal : q.reject(arguments[0]); + if (throwReturnVal) throw returnVal; + return returnVal; + } + } + + + /** helper for synchronous resolution of deferred */ + function syncResolve(deferred, result) { + deferred.resolve(result); + mockNextTick.flush(); + } + + + /** helper for synchronous rejection of deferred */ + function syncReject(deferred, reason) { + deferred.reject(reason); + mockNextTick.flush(); + } + + + /** converts the `log` to a '; '-separated string */ + function logStr() { + return log.join('; '); + } + + + var mockNextTick = { + nextTick: function(task) { + mockNextTick.queue.push(task); + }, + queue: [], + flush: function() { + if (!mockNextTick.queue.length) throw new Error('Nothing to be flushed!'); + while (mockNextTick.queue.length) { + var queue = mockNextTick.queue; + mockNextTick.queue = []; + forEach(queue, function(task) { + try { + task(); + } catch(e) { + dump('exception in mockNextTick:', e, e.name, e.message, task); + } + }); + } + } + } + + + beforeEach(function() { + q = qFactory(mockNextTick.nextTick, noop), + defer = q.defer; + deferred = defer() + promise = deferred.promise; + log = []; + mockNextTick.queue = []; + }); + + + afterEach(function() { + expect(mockNextTick.queue.length).toBe(0); + }); + + + describe('defer', function() { + it('should create a new deferred', function() { + expect(deferred.promise).toBeDefined(); + expect(deferred.resolve).toBeDefined(); + expect(deferred.reject).toBeDefined(); + }); + + + describe('resolve', function() { + it('should fulfill the promise and execute all success callbacks in the registration order', + function() { + promise.then(success(1), error()); + promise.then(success(2), error()); + expect(logStr()).toBe(''); + + deferred.resolve('foo'); + mockNextTick.flush(); + expect(logStr()).toBe('success1(foo); success2(foo)'); + }); + + + it('should do nothing if a promise was previously resolved', function() { + promise.then(success(), error()); + expect(logStr()).toBe(''); + + deferred.resolve('foo'); + mockNextTick.flush(); + expect(logStr()).toBe('success(foo)'); + + log = []; + deferred.resolve('bar'); + deferred.reject('baz'); + expect(mockNextTick.queue.length).toBe(0); + expect(logStr()).toBe(''); + }); + + + it('should do nothing if a promise was previously rejected', function() { + promise.then(success(), error()); + expect(logStr()).toBe(''); + + deferred.reject('foo'); + mockNextTick.flush(); + expect(logStr()).toBe('error(foo)'); + + log = []; + deferred.resolve('bar'); + deferred.reject('baz'); + expect(mockNextTick.queue.length).toBe(0); + expect(logStr()).toBe(''); + }); + + + it('should allow deferred resolution with a new promise', function() { + var deferred2 = defer(); + promise.then(success(), error()); + + deferred.resolve(deferred2.promise); + mockNextTick.flush(); + expect(logStr()).toBe(''); + + deferred2.resolve('foo'); + mockNextTick.flush(); + expect(logStr()).toBe('success(foo)'); + }); + + + it('should call the callback in the next turn', function() { + promise.then(success()); + expect(logStr()).toBe(''); + + deferred.resolve('foo'); + expect(logStr()).toBe(''); + + mockNextTick.flush(); + expect(logStr()).toBe('success(foo)'); + }); + + + it('should support non-bound execution', function() { + var resolver = deferred.resolve; + promise.then(success(), error()); + resolver('detached'); + mockNextTick.flush(); + expect(logStr()).toBe('success(detached)'); + }); + + + it('should not break if a callbacks registers another callback', function() { + promise.then(function() { + log.push('outer'); + promise.then(function() { + log.push('inner'); + }); + }); + + deferred.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() { + promise.then(function(val) { + log.push('success1(' + val + ')'); + deferred.resolve('bar'); + }); + + promise.then(success(2)); + + deferred.resolve('foo'); + expect(logStr()).toBe(''); + + mockNextTick.flush(); + expect(logStr()).toBe('success1(foo); success2(foo)'); + }); + }); + + + describe('reject', function() { + it('should reject the promise and execute all error callbacks in the registration order', + function() { + promise.then(success(), error(1)); + promise.then(success(), error(2)); + expect(logStr()).toBe(''); + + deferred.reject('foo'); + mockNextTick.flush(); + expect(logStr()).toBe('error1(foo); error2(foo)'); + }); + + + it('should do nothing if a promise was previously resolved', function() { + promise.then(success(1), error(1)); + expect(logStr()).toBe(''); + + deferred.resolve('foo'); + mockNextTick.flush(); + expect(logStr()).toBe('success1(foo)'); + + log = []; + deferred.reject('bar'); + deferred.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)'); + }); + + + it('should do nothing if a promise was previously rejected', function() { + promise.then(success(1), error(1)); + expect(logStr()).toBe(''); + + deferred.reject('foo'); + mockNextTick.flush(); + expect(logStr()).toBe('error1(foo)'); + + log = []; + deferred.reject('bar'); + deferred.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)'); + }); + + + it('should not defer rejection with a new promise', function() { + var deferred2 = defer(); + promise.then(success(), error()); + + deferred.reject(deferred2.promise); + mockNextTick.flush(); + expect(logStr()).toBe('error([object Object])'); + }); + + + it('should call the error callback in the next turn', function() { + promise.then(success(), error()); + expect(logStr()).toBe(''); + + deferred.reject('foo'); + expect(logStr()).toBe(''); + + mockNextTick.flush(); + expect(logStr()).toBe('error(foo)'); + }); + + + it('should support non-bound execution', function() { + var rejector = deferred.reject; + promise.then(success(), error()); + rejector('detached'); + mockNextTick.flush(); + expect(logStr()).toBe('error(detached)'); + }); + }); + + + describe('promise', function() { + it('should have a then method', function() { + expect(typeof promise.then).toBe('function'); + }); + + + describe('then', function() { + it('should allow registration of a success callback without an errback and resolve', + function() { + promise.then(success()); + syncResolve(deferred, 'foo'); + expect(logStr()).toBe('success(foo)'); + }); + + it('should allow registration of a success callback without an errback and reject', + function() { + promise.then(success()); + syncReject(deferred, 'foo'); + expect(logStr()).toBe(''); + }); + + + it('should allow registration of an errback without a success callback and reject', + function() { + promise.then(null, error()); + syncReject(deferred, 'oops!'); + expect(logStr()).toBe('error(oops!)'); + }); + + + it('should allow registration of an errback without a success callback and resolve', + function() { + promise.then(null, error()); + syncResolve(deferred, 'done'); + 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', 'dVal'), error()); + + expect(logStr()).toBe(''); + syncResolve(deferred, 'yup'); + expect(logStr()).toBe('successA(yup); successB(yup); successC(yup); successD(yup)'); + }); + + + 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')); + + expect(logStr()).toBe(''); + syncReject(deferred, 'noo!'); + expect(logStr()).toBe('errorA(noo!); errorB(noo!); errorC(noo!); errorD(noo!)'); + }); + + + it('should propagate resolution and rejection between dependent promises', function() { + 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(''); + syncResolve(deferred, 'sweet!'); + expect(log).toEqual(['success1(sweet!)', + 'success2(x)', + 'error3(y)', + 'error4(z)', + 'success5(done)']); + }); + + + it('should reject a derived promise if an exception is thrown while resolving its parent', + function() { + promise.then(success(1, 'oops', true)). + then(success(2), error(2)); + syncResolve(deferred, 'done!'); + expect(logStr()).toBe('success1(done!); error2(oops)'); + }); + + + it('should reject a derived promise if an exception is thrown while rejecting its parent', + function() { + promise.then(null, error(1, 'oops', true)). + then(success(2), error(2)); + syncReject(deferred, 'timeout'); + expect(logStr()).toBe('error1(timeout); error2(oops)'); + }); + + + it('should call success callback in the next turn even if promise is already resolved', + function() { + deferred.resolve('done!'); + + promise.then(success()); + expect(logStr()).toBe(''); + + mockNextTick.flush(); + expect(log).toEqual(['success(done!)']); + }); + + + it('should call errpr callback in the next turn even if promise is already rejected', + function() { + deferred.reject('oops!'); + + promise.then(null, error()); + expect(logStr()).toBe(''); + + mockNextTick.flush(); + expect(log).toEqual(['error(oops!)']); + }); + }); + }); + }); + + + describe('reject', function() { + it('should package a string into a rejected promise', function() { + var rejectedPromise = q.reject('not gonna happen'); + promise.then(success(), error()); + syncResolve(deferred, rejectedPromise); + expect(log).toEqual(['error(not gonna happen)']); + }); + + + it('should package an exception into a rejected promise', function() { + var rejectedPromise = q.reject(Error('not gonna happen')); + promise.then(success(), error()); + syncResolve(deferred, rejectedPromise); + expect(log).toEqual(['error(Error: not gonna happen)']); + }); + }); + + + describe('when', function() { + describe('resolution', function() { + it('should call the success callback in the next turn when the value is a non-promise', + function() { + q.when('hello', success(), error()); + expect(logStr()).toBe(''); + mockNextTick.flush(); + expect(logStr()).toBe('success(hello)'); + }); + + + it('should call the success callback in the next turn when the value is a resolved promise', + function() { + deferred.resolve('hello'); + q.when(deferred.promise, success(), error()); + expect(logStr()).toBe(''); + mockNextTick.flush(); + expect(logStr()).toBe('success(hello)'); + }); + + + it('should call the errback in the next turn when the value is a rejected promise', function() { + deferred.reject('nope'); + q.when(deferred.promise, success(), error()); + expect(logStr()).toBe(''); + mockNextTick.flush(); + expect(logStr()).toBe('error(nope)'); + }); + + + it('should call the success callback after the original promise is resolved', + function() { + q.when(deferred.promise, success(), error()); + expect(logStr()).toBe(''); + mockNextTick.flush(); + expect(logStr()).toBe(''); + syncResolve(deferred, 'hello'); + expect(logStr()).toBe('success(hello)'); + }); + + + it('should call the errback after the orignal promise is rejected', + function() { + q.when(deferred.promise, success(), error()); + expect(logStr()).toBe(''); + mockNextTick.flush(); + expect(logStr()).toBe(''); + syncReject(deferred, 'nope'); + expect(logStr()).toBe('error(nope)'); + }); + }); + + + describe('optional callbacks', function() { + it('should not require success callback and propagate resolution', function() { + q.when('hi', null, error()).then(success(2), error()); + expect(logStr()).toBe(''); + mockNextTick.flush(); + expect(logStr()).toBe('success2(hi)'); + }); + + + it('should not require success callback and propagate rejection', function() { + q.when(q.reject('sorry'), null, error(1)).then(success(), error(2)); + expect(logStr()).toBe(''); + mockNextTick.flush(); + expect(logStr()).toBe('error1(sorry); error2(sorry)'); + }); + + + it('should not require errback and propagate resolution', function() { + q.when('hi', success(1, 'hello')).then(success(2), error()); + expect(logStr()).toBe(''); + mockNextTick.flush(); + expect(logStr()).toBe('success1(hi); success2(hello)'); + }); + + + it('should not require errback and propagate rejection', function() { + q.when(q.reject('sorry'), success()).then(success(2), error(2)); + expect(logStr()).toBe(''); + mockNextTick.flush(); + expect(logStr()).toBe('error2(sorry)'); + }); + }); + + + describe('returned promise', function() { + it('should return a promise that can be resolved with a value returned from the success ' + + 'callback', function() { + q.when('hello', success(1, 'hi'), error()).then(success(2), error()); + mockNextTick.flush(); + expect(logStr()).toBe('success1(hello); success2(hi)'); + }); + + + it('should return a promise that can be rejected with a rejected promise returned from the ' + + 'success callback', function() { + q.when('hello', success(1, q.reject('sorry')), error()).then(success(), error(2)); + mockNextTick.flush(); + expect(logStr()).toBe('success1(hello); error2(sorry)'); + }); + + + it('should return a promise that can be resolved with a value returned from the errback', + function() { + q.when(q.reject('sorry'), success(), error(1, 'hi')).then(success(2), error()); + mockNextTick.flush(); + expect(logStr()).toBe('error1(sorry); success2(hi)'); + }); + + + it('should return a promise that can be rejected with a rejected promise returned from the ' + + 'errback', function() { + q.when(q.reject('sorry'), success(), error(1, q.reject('sigh'))).then(success(), error(2)); + mockNextTick.flush(); + expect(logStr()).toBe('error1(sorry); error2(sigh)'); + }); + + + it('should return a promise that can be resolved with a promise returned from the success ' + + 'callback', function() { + var deferred2 = defer(); + q.when('hi', success(1, deferred2.promise), error()).then(success(2), error()); + mockNextTick.flush(); + expect(logStr()).toBe('success1(hi)'); + syncResolve(deferred2, 'finally!'); + expect(logStr()).toBe('success1(hi); success2(finally!)'); + }); + + + it('should return a promise that can be resolved with promise returned from the errback ' + + 'callback', function() { + var deferred2 = defer(); + q.when(q.reject('sorry'), success(), error(1, deferred2.promise)).then(success(2), error()); + mockNextTick.flush(); + expect(logStr()).toBe('error1(sorry)'); + syncResolve(deferred2, 'finally!'); + expect(logStr()).toBe('error1(sorry); success2(finally!)'); + }); + }); + + + describe('security', function() { + it('should call success callback only once even if the original promise gets fullfilled ' + + 'multiple times', function() { + var evilPromise = { + then: function(success, error) { + evilPromise.success = success; + evilPromise.error = error; + } + } + + q.when(evilPromise, success(), error()); + mockNextTick.flush(); + expect(logStr()).toBe(''); + evilPromise.success('done'); + mockNextTick.flush(); // TODO(i) wrong queue, evil promise would be resolved outside of the + // scope.$apply lifecycle and in that case we should have some kind + // of fallback queue for calling our callbacks from. Otherwise the + // application will get stuck until something triggers next $apply. + expect(logStr()).toBe('success(done)'); + + evilPromise.success('evil is me'); + evilPromise.error('burn burn'); + expect(logStr()).toBe('success(done)'); + }); + + + it('should call errback only once even if the original promise gets fullfilled multiple ' + + 'times', function() { + var evilPromise = { + then: function(success, error) { + evilPromise.success = success; + evilPromise.error = error; + } + } + + q.when(evilPromise, success(), error()); + mockNextTick.flush(); + expect(logStr()).toBe(''); + evilPromise.error('failed'); + expect(logStr()).toBe('error(failed)'); + + evilPromise.error('muhaha'); + evilPromise.success('take this'); + expect(logStr()).toBe('error(failed)'); + }); + }); + }); + + + describe('all', function() { + it('should take an array of promises and return a promise for an array of results', function() { + var deferred1 = defer(), + deferred2 = defer(); + + q.all([promise, deferred1.promise, deferred2.promise]).then(success(), error()); + expect(logStr()).toBe(''); + syncResolve(deferred, 'hi'); + expect(logStr()).toBe(''); + syncResolve(deferred2, 'cau'); + expect(logStr()).toBe(''); + syncResolve(deferred1, 'hola'); + expect(logStr()).toBe('success([hi,hola,cau])'); + }); + + + it('should reject the derived promise if at least one of the promises in the array is rejected', + function() { + var deferred1 = defer(), + deferred2 = defer(); + + q.all([promise, deferred1.promise, deferred2.promise]).then(success(), error()); + expect(logStr()).toBe(''); + syncResolve(deferred2, 'cau'); + expect(logStr()).toBe(''); + syncReject(deferred1, 'oops'); + expect(logStr()).toBe('error(oops)'); + }); + + + it('should ignore multiple resolutions of an (evil) array promise', function() { + var evilPromise = { + then: function(success, error) { + evilPromise.success = success; + evilPromise.error = error; + } + } + + q.all([promise, evilPromise]).then(success(), error()); + expect(logStr()).toBe(''); + + evilPromise.success('first'); + evilPromise.success('muhaha'); + evilPromise.error('arghhh'); + expect(logStr()).toBe(''); + + syncResolve(deferred, 'done'); + expect(logStr()).toBe('success([done,first])'); + }); + }); + + + describe('exception logging', function() { + var mockExceptionLogger = { + log: [], + logger: function(e) { + mockExceptionLogger.log.push(e); + } + } + + + beforeEach(function() { + q = qFactory(mockNextTick.nextTick, mockExceptionLogger.logger), + defer = q.defer; + deferred = defer() + promise = deferred.promise; + log = []; + mockExceptionLogger.log = []; + }); + + + describe('in then', function() { + it('should log exceptions thrown in a success callback and reject the derived promise', + function() { + var success1 = success(1, 'oops', true); + promise.then(success1).then(success(2), error(2)); + syncResolve(deferred, 'done'); + expect(logStr()).toBe('success1(done); error2(oops)'); + expect(mockExceptionLogger.log).toEqual(['oops']); + }); + + + it('should NOT log exceptions when a success callback returns rejected promise', function() { + promise.then(success(1, q.reject('rejected'))).then(success(2), error(2)); + syncResolve(deferred, 'done'); + expect(logStr()).toBe('success1(done); error2(rejected)'); + expect(mockExceptionLogger.log).toEqual([]); + }); + + + it('should log exceptions thrown in a errback and reject the derived promise', function() { + var error1 = error(1, 'oops', true); + promise.then(null, error1).then(success(2), error(2)); + syncReject(deferred, 'nope'); + expect(logStr()).toBe('error1(nope); error2(oops)'); + expect(mockExceptionLogger.log).toEqual(['oops']); + }); + + + it('should NOT log exceptions when an errback returns a rejected promise', function() { + promise.then(null, error(1, q.reject('rejected'))).then(success(2), error(2)); + syncReject(deferred, 'nope'); + expect(logStr()).toBe('error1(nope); error2(rejected)'); + expect(mockExceptionLogger.log).toEqual([]); + }); + }); + + + describe('in when', function() { + it('should log exceptions thrown in a success callback and reject the derived promise', + function() { + var success1 = success(1, 'oops', true); + q.when('hi', success1, error()).then(success(), error(2)); + mockNextTick.flush(); + expect(logStr()).toBe('success1(hi); error2(oops)'); + expect(mockExceptionLogger.log).toEqual(['oops']); + }); + + + it('should NOT log exceptions when a success callback returns rejected promise', function() { + q.when('hi', success(1, q.reject('rejected'))).then(success(2), error(2)); + mockNextTick.flush(); + expect(logStr()).toBe('success1(hi); error2(rejected)'); + expect(mockExceptionLogger.log).toEqual([]); + }); + + + it('should log exceptions thrown in a errback and reject the derived promise', function() { + var error1 = error(1, 'oops', true); + q.when(q.reject('sorry'), success(), error1).then(success(), error(2)); + mockNextTick.flush(); + expect(logStr()).toBe('error1(sorry); error2(oops)'); + expect(mockExceptionLogger.log).toEqual(['oops']); + }); + + + it('should NOT log exceptions when an errback returns a rejected promise', function() { + q.when(q.reject('sorry'), success(), error(1, q.reject('rejected'))). + then(success(2), error(2)); + mockNextTick.flush(); + expect(logStr()).toBe('error1(sorry); error2(rejected)'); + expect(mockExceptionLogger.log).toEqual([]); + }); + }); + }); +}); -- cgit v1.2.3