]> source.dussan.org Git - jquery.git/commitdiff
Deferred: Backwards-compatible standards interoperability
authorRichard Gibson <richard.gibson@gmail.com>
Mon, 29 Dec 2014 19:14:13 +0000 (14:14 -0500)
committerRichard Gibson <richard.gibson@gmail.com>
Fri, 20 Mar 2015 06:14:04 +0000 (02:14 -0400)
Fixes gh-1722
Closes gh-1996

(cherry picked from commit 555a50d340706e3e1e0de09231050493d0ad841e)

Gruntfile.js
build/tasks/promises-aplus-tests.js [new file with mode: 0644]
external/npo/npo.js [new file with mode: 0644]
external/requirejs/require.js
package.json
src/deferred.js
test/index.html
test/promises-aplus-adapter.js [new file with mode: 0644]
test/unit/deferred.js

index d1b4a7cb22a3537ad8ecd8c42009b14fee42fcbf..cb21a4fdfb799c3d6b02c46126ed0bda359028a3 100644 (file)
@@ -53,6 +53,8 @@ module.exports = function( grunt ) {
                                        "sizzle/dist": "sizzle/dist",
                                        "sizzle/LICENSE.txt": "sizzle/LICENSE.txt",
 
+                                       "npo/npo.js": "native-promise-only/npo.js",
+
                                        "qunit/qunit.js": "qunitjs/qunit/qunit.js",
                                        "qunit/qunit.css": "qunitjs/qunit/qunit.css",
                                        "qunit/LICENSE.txt": "qunitjs/LICENSE.txt",
@@ -158,7 +160,8 @@ module.exports = function( grunt ) {
        // Only defined for master at this time, but kept for cross-branch consistency
        grunt.registerTask( "test_fast", [] );
 
-       grunt.registerTask( "test", [ "test_fast" ] );
+       // gh-2133 TODO: cherry-pick 76df9e4e389d80bff410a9e5f08b848de1d21a2f for promises-aplus-tests
+       grunt.registerTask( "test", [ "test_fast"/*, "promises-aplus-tests"*/ ] );
 
        // Short list as a high frequency watch task
        grunt.registerTask( "dev", [ "build:*:*", "lint", "uglify", "remove_map_comment", "dist:*" ] );
diff --git a/build/tasks/promises-aplus-tests.js b/build/tasks/promises-aplus-tests.js
new file mode 100644 (file)
index 0000000..458ae1b
--- /dev/null
@@ -0,0 +1,20 @@
+module.exports = function( grunt ) {
+
+       "use strict";
+
+       var spawn = require( "child_process" ).spawn;
+
+       grunt.registerTask( "promises-aplus-tests", function() {
+               var done = this.async();
+               spawn(
+                       "node",
+                       [
+                               "./node_modules/.bin/promises-aplus-tests",
+                               "test/promises-aplus-adapter.js"
+                       ],
+                       { stdio: "inherit" }
+               ).on( "close", function( code ) {
+                       done( code === 0 );
+               });
+       });
+};
diff --git a/external/npo/npo.js b/external/npo/npo.js
new file mode 100644 (file)
index 0000000..bd07084
--- /dev/null
@@ -0,0 +1,5 @@
+/*! Native Promise Only
+    v0.7.6-a (c) Kyle Simpson
+    MIT License: http://getify.mit-license.org
+*/
+!function(t,n,e){n[t]=n[t]||e(),"undefined"!=typeof module&&module.exports?module.exports=n[t]:"function"==typeof define&&define.amd&&define(function(){return n[t]})}("Promise","undefined"!=typeof global?global:this,function(){"use strict";function t(t,n){l.add(t,n),h||(h=y(l.drain))}function n(t){var n,e=typeof t;return null==t||"object"!=e&&"function"!=e||(n=t.then),"function"==typeof n?n:!1}function e(){for(var t=0;t<this.chain.length;t++)o(this,1===this.state?this.chain[t].success:this.chain[t].failure,this.chain[t]);this.chain.length=0}function o(t,e,o){var r,i;try{e===!1?o.reject(t.msg):(r=e===!0?t.msg:e.call(void 0,t.msg),r===o.promise?o.reject(TypeError("Promise-chain cycle")):(i=n(r))?i.call(r,o.resolve,o.reject):o.resolve(r))}catch(c){o.reject(c)}}function r(o){var c,u,a=this;if(!a.triggered){a.triggered=!0,a.def&&(a=a.def);try{(c=n(o))?(u=new f(a),c.call(o,function(){r.apply(u,arguments)},function(){i.apply(u,arguments)})):(a.msg=o,a.state=1,a.chain.length>0&&t(e,a))}catch(s){i.call(u||new f(a),s)}}}function i(n){var o=this;o.triggered||(o.triggered=!0,o.def&&(o=o.def),o.msg=n,o.state=2,o.chain.length>0&&t(e,o))}function c(t,n,e,o){for(var r=0;r<n.length;r++)!function(r){t.resolve(n[r]).then(function(t){e(r,t)},o)}(r)}function f(t){this.def=t,this.triggered=!1}function u(t){this.promise=t,this.state=0,this.triggered=!1,this.chain=[],this.msg=void 0}function a(n){if("function"!=typeof n)throw TypeError("Not a function");if(0!==this.__NPO__)throw TypeError("Not a promise");this.__NPO__=1;var o=new u(this);this.then=function(n,r){var i={success:"function"==typeof n?n:!0,failure:"function"==typeof r?r:!1};return i.promise=new this.constructor(function(t,n){if("function"!=typeof t||"function"!=typeof n)throw TypeError("Not a function");i.resolve=t,i.reject=n}),o.chain.push(i),0!==o.state&&t(e,o),i.promise},this["catch"]=function(t){return this.then(void 0,t)};try{n.call(void 0,function(t){r.call(o,t)},function(t){i.call(o,t)})}catch(c){i.call(o,c)}}var s,h,l,p=Object.prototype.toString,y="undefined"!=typeof setImmediate?function(t){return setImmediate(t)}:setTimeout;try{Object.defineProperty({},"x",{}),s=function(t,n,e,o){return Object.defineProperty(t,n,{value:e,writable:!0,configurable:o!==!1})}}catch(d){s=function(t,n,e){return t[n]=e,t}}l=function(){function t(t,n){this.fn=t,this.self=n,this.next=void 0}var n,e,o;return{add:function(r,i){o=new t(r,i),e?e.next=o:n=o,e=o,o=void 0},drain:function(){var t=n;for(n=e=h=void 0;t;)t.fn.call(t.self),t=t.next}}}();var g=s({},"constructor",a,!1);return s(a,"prototype",g,!1),s(g,"__NPO__",0,!1),s(a,"resolve",function(t){var n=this;return t&&"object"==typeof t&&1===t.__NPO__?t:new n(function(n,e){if("function"!=typeof n||"function"!=typeof e)throw TypeError("Not a function");n(t)})}),s(a,"reject",function(t){return new this(function(n,e){if("function"!=typeof n||"function"!=typeof e)throw TypeError("Not a function");e(t)})}),s(a,"all",function(t){var n=this;return"[object Array]"!=p.call(t)?n.reject(TypeError("Not an array")):0===t.length?n.resolve([]):new n(function(e,o){if("function"!=typeof e||"function"!=typeof o)throw TypeError("Not a function");var r=t.length,i=Array(r),f=0;c(n,t,function(t,n){i[t]=n,++f===r&&e(i)},o)})}),s(a,"race",function(t){var n=this;return"[object Array]"!=p.call(t)?n.reject(TypeError("Not an array")):new n(function(e,o){if("function"!=typeof e||"function"!=typeof o)throw TypeError("Not a function");c(n,t,function(t,n){e(n)},o)})}),a});
index 7f31fa20417d5e7f9bd26dde74d8678c34ff20af..77a5bb1d3be75c2f6e0d7f8484d00b890319da0f 100644 (file)
@@ -1,5 +1,5 @@
 /** vim: et:ts=4:sw=4:sts=4
- * @license RequireJS 2.1.14 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved.
+ * @license RequireJS 2.1.15 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved.
  * Available via the MIT or new BSD license.
  * see: http://github.com/jrburke/requirejs for details
  */
@@ -12,7 +12,7 @@ var requirejs, require, define;
 (function (global) {
     var req, s, head, baseElement, dataMain, src,
         interactiveScript, currentlyAddingScript, mainScript, subPath,
-        version = '2.1.14',
+        version = '2.1.15',
         commentRegExp = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg,
         cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,
         jsSuffixRegExp = /\.js$/,
index b37effe05e5ee04025e6a404ad57eefb974337e8..a157ff9f2a5df09ddf6624d3cba9ef1fc3cf4249 100644 (file)
     "grunt-npmcopy": "0.1.0",
     "gzip-js": "0.3.2",
     "load-grunt-tasks": "1.0.0",
+    "native-promise-only": "0.7.6-a",
     "npm": "2.1.12",
+    "promises-aplus-tests": "2.1.0",
+    "q": "1.1.2",
     "qunitjs": "1.17.1",
     "requirejs": "2.1.15",
     "sinon": "1.12.2",
index 5d58ed2cf709ef793a64cd2d4c72b5322c75ab23..a45dad88bfc25a55f087363d0300bcbcbbf54dc5 100644 (file)
@@ -4,14 +4,24 @@ define([
        "./callbacks"
 ], function( jQuery, slice ) {
 
+function Identity( v ) {
+       return v;
+}
+function Thrower( ex ) {
+       throw ex;
+}
+
 jQuery.extend({
 
        Deferred: function( func ) {
                var tuples = [
-                               // action, add listener, listener list, final state
-                               [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ],
-                               [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ],
-                               [ "notify", "progress", jQuery.Callbacks("memory") ]
+                               // action, add listener, callbacks, .then handlers, final state
+                               [ "resolve", "done", jQuery.Callbacks("once memory"),
+                                       jQuery.Callbacks("once memory"), "resolved" ],
+                               [ "reject", "fail", jQuery.Callbacks("once memory"),
+                                       jQuery.Callbacks("once memory"), "rejected" ],
+                               [ "notify", "progress", jQuery.Callbacks("memory"),
+                                       jQuery.Callbacks("memory") ]
                        ],
                        state = "pending",
                        promise = {
@@ -22,12 +32,16 @@ jQuery.extend({
                                        deferred.done( arguments ).fail( arguments );
                                        return this;
                                },
-                               then: function( /* fnDone, fnFail, fnProgress */ ) {
+                               // Keep pipe for back-compat
+                               pipe: function( /* fnDone, fnFail, fnProgress */ ) {
                                        var fns = arguments;
+
                                        return jQuery.Deferred(function( newDefer ) {
                                                jQuery.each( tuples, function( i, tuple ) {
                                                        var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ];
-                                                       // deferred[ done | fail | progress ] for forwarding actions to newDefer
+                                                       // deferred.done(function() { bind to newDefer or newDefer.resolve })
+                                                       // deferred.fail(function() { bind to newDefer or newDefer.reject })
+                                                       // deferred.progress(function() { bind to newDefer or newDefer.notify })
                                                        deferred[ tuple[1] ](function() {
                                                                var returned = fn && fn.apply( this, arguments );
                                                                if ( returned && jQuery.isFunction( returned.promise ) ) {
@@ -46,6 +60,157 @@ jQuery.extend({
                                                fns = null;
                                        }).promise();
                                },
+                               then: function( onFulfilled, onRejected, onProgress ) {
+                                       var maxDepth = 0;
+                                       function resolve( depth, deferred, handler, special ) {
+                                               return function() {
+                                                       var that = this === promise ? undefined : this,
+                                                               args = arguments,
+                                                               mightThrow = function() {
+                                                                       var returned, then;
+
+                                                                       // Support: Promises/A+ section 2.3.3.3.3
+                                                                       // https://promisesaplus.com/#point-59
+                                                                       // Ignore double-resolution attempts
+                                                                       if ( depth < maxDepth ) {
+                                                                               return;
+                                                                       }
+
+                                                                       returned = handler.apply( that, args );
+
+                                                                       // Support: Promises/A+ section 2.3.1
+                                                                       // https://promisesaplus.com/#point-48
+                                                                       if ( returned === deferred.promise() ) {
+                                                                               throw new TypeError( "Thenable self-resolution" );
+                                                                       }
+
+                                                                       // Support: Promises/A+ sections 2.3.3.1, 3.5
+                                                                       // https://promisesaplus.com/#point-54
+                                                                       // https://promisesaplus.com/#point-75
+                                                                       // Retrieve `then` only once
+                                                                       then = returned &&
+
+                                                                               // Support: Promises/A+ section 2.3.4
+                                                                               // https://promisesaplus.com/#point-64
+                                                                               // Only check objects and functions for thenability
+                                                                               ( typeof returned === "object" ||
+                                                                                       typeof returned === "function" ) &&
+                                                                               returned.then;
+
+                                                                       // Handle a returned thenable
+                                                                       if ( jQuery.isFunction( then ) ) {
+                                                                               // Special processors (notify) just wait for resolution
+                                                                               if ( special ) {
+                                                                                       then.call(
+                                                                                               returned,
+                                                                                               resolve( maxDepth, deferred, Identity, special ),
+                                                                                               resolve( maxDepth, deferred, Thrower, special )
+                                                                                       );
+
+                                                                               // Normal processors (resolve) also hook into progress
+                                                                               } else {
+
+                                                                                       // ...and disregard older resolution values
+                                                                                       maxDepth++;
+
+                                                                                       then.call(
+                                                                                               returned,
+                                                                                               resolve( maxDepth, deferred, Identity, special ),
+                                                                                               resolve( maxDepth, deferred, Thrower, special ),
+                                                                                               resolve( maxDepth, deferred, Identity,
+                                                                                                       deferred.notify )
+                                                                                       );
+                                                                               }
+
+                                                                       // Handle all other returned values
+                                                                       } else {
+                                                                               // Only substitue handlers pass on context
+                                                                               // and multiple values (non-spec behavior)
+                                                                               if ( handler !== Identity ) {
+                                                                                       that = undefined;
+                                                                                       args = [ returned ];
+                                                                               }
+
+                                                                               // Process the value(s)
+                                                                               // Default process is resolve
+                                                                               ( special || deferred.resolveWith )(
+                                                                                       that || deferred.promise(), args );
+                                                                       }
+                                                               },
+
+                                                               // Only normal processors (resolve) catch and reject exceptions
+                                                               process = special ?
+                                                                       mightThrow :
+                                                                       function() {
+                                                                               try {
+                                                                                       mightThrow();
+                                                                               } catch ( e ) {
+
+                                                                                       // Support: Promises/A+ section 2.3.3.3.4.1
+                                                                                       // https://promisesaplus.com/#point-61
+                                                                                       // Ignore post-resolution exceptions
+                                                                                       if ( depth + 1 >= maxDepth ) {
+                                                                                               // Only substitue handlers pass on context
+                                                                                               // and multiple values (non-spec behavior)
+                                                                                               if ( handler !== Thrower ) {
+                                                                                                       that = undefined;
+                                                                                                       args = [ e ];
+                                                                                               }
+
+                                                                                               deferred.rejectWith( that || deferred.promise(),
+                                                                                                       args );
+                                                                                       }
+                                                                               }
+                                                                       };
+
+                                                       // Support: Promises/A+ section 2.3.3.3.1
+                                                       // https://promisesaplus.com/#point-57
+                                                       // Re-resolve promises immediately to dodge false rejection from
+                                                       // subsequent errors
+                                                       if ( depth ) {
+                                                               process();
+                                                       } else {
+                                                               setTimeout( process );
+                                                       }
+                                               };
+                                       }
+
+                                       return jQuery.Deferred(function( newDefer ) {
+                                               // fulfilled_handlers.add( ... )
+                                               tuples[ 0 ][ 3 ].add(
+                                                       resolve(
+                                                               0,
+                                                               newDefer,
+                                                               jQuery.isFunction( onFulfilled ) ?
+                                                                       onFulfilled :
+                                                                       Identity
+                                                       )
+                                               );
+
+                                               // rejected_handlers.add( ... )
+                                               tuples[ 1 ][ 3 ].add(
+                                                       resolve(
+                                                               0,
+                                                               newDefer,
+                                                               jQuery.isFunction( onRejected ) ?
+                                                                       onRejected :
+                                                                       Thrower
+                                                       )
+                                               );
+
+                                               // progress_handlers.add( ... )
+                                               tuples[ 2 ][ 3 ].add(
+                                                       resolve(
+                                                               0,
+                                                               newDefer,
+                                                               jQuery.isFunction( onProgress ) ?
+                                                                       onProgress :
+                                                                       Identity,
+                                                               newDefer.notifyWith
+                                                       )
+                                               );
+                                       }).promise();
+                               },
                                // Get a promise for this deferred
                                // If obj is provided, the promise aspect is added to the object
                                promise: function( obj ) {
@@ -54,32 +219,50 @@ jQuery.extend({
                        },
                        deferred = {};
 
-               // Keep pipe for back-compat
-               promise.pipe = promise.then;
-
                // Add list-specific methods
                jQuery.each( tuples, function( i, tuple ) {
                        var list = tuple[ 2 ],
-                               stateString = tuple[ 3 ];
+                               stateString = tuple[ 4 ];
 
-                       // promise[ done | fail | progress ] = list.add
+                       // promise.done = list.add
+                       // promise.fail = list.add
+                       // promise.progress = list.add
                        promise[ tuple[1] ] = list.add;
 
                        // Handle state
                        if ( stateString ) {
-                               list.add(function() {
-                                       // state = [ resolved | rejected ]
-                                       state = stateString;
+                               list.add(
+                                       function() {
+                                               // state = "resolved" (i.e., fulfilled)
+                                               // state = "rejected"
+                                               state = stateString;
+                                       },
+
+                                       // rejected_callbacks.disable
+                                       // fulfilled_callbacks.disable
+                                       tuples[ i ^ 1 ][ 2 ].disable,
 
-                               // [ reject_list | resolve_list ].disable; progress_list.lock
-                               }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
+                                       // progress_callbacks.lock
+                                       tuples[ 2 ][ 2 ].lock
+                               );
                        }
 
-                       // deferred[ resolve | reject | notify ]
+                       // fulfilled_handlers.fire
+                       // rejected_handlers.fire
+                       // progress_handlers.fire
+                       list.add( tuple[ 3 ].fire );
+
+                       // deferred.resolve = function() { deferred.resolveWith(...) }
+                       // deferred.reject = function() { deferred.rejectWith(...) }
+                       // deferred.notify = function() { deferred.notifyWith(...) }
                        deferred[ tuple[0] ] = function() {
                                deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments );
                                return this;
                        };
+
+                       // deferred.resolveWith = list.fireWith
+                       // deferred.rejectWith = list.fireWith
+                       // deferred.notifyWith = list.fireWith
                        deferred[ tuple[0] + "With" ] = list.fireWith;
                });
 
@@ -97,7 +280,8 @@ jQuery.extend({
 
        // Deferred helper
        when: function( subordinate /* , ..., subordinateN */ ) {
-               var i = 0,
+               var method,
+                       i = 0,
                        resolveValues = slice.call( arguments ),
                        length = resolveValues.length,
 
@@ -107,7 +291,7 @@ jQuery.extend({
 
                        // the master Deferred.
                        // If resolveValues consist of only a single Deferred, just use that.
-                       deferred = remaining === 1 ? subordinate : jQuery.Deferred(),
+                       master = remaining === 1 ? subordinate : jQuery.Deferred(),
 
                        // Update function for both resolve and progress values
                        updateFunc = function( i, contexts, values ) {
@@ -115,14 +299,12 @@ jQuery.extend({
                                        contexts[ i ] = this;
                                        values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;
                                        if ( values === progressValues ) {
-                                               deferred.notifyWith( contexts, values );
-
-                                       } else if ( !(--remaining) ) {
-                                               deferred.resolveWith( contexts, values );
+                                               master.notifyWith( contexts, values );
+                                       } else if ( !( --remaining ) ) {
+                                               master.resolveWith( contexts, values );
                                        }
                                };
                        },
-
                        progressValues, progressContexts, resolveContexts;
 
                // add listeners to Deferred subordinates; treat others as resolved
@@ -131,11 +313,22 @@ jQuery.extend({
                        progressContexts = new Array( length );
                        resolveContexts = new Array( length );
                        for ( ; i < length; i++ ) {
-                               if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) {
-                                       resolveValues[ i ].promise()
+                               if ( resolveValues[ i ] &&
+                                       jQuery.isFunction( (method = resolveValues[ i ].promise) ) ) {
+
+                                       method.call( resolveValues[ i ] )
                                                .progress( updateFunc( i, progressContexts, progressValues ) )
                                                .done( updateFunc( i, resolveContexts, resolveValues ) )
-                                               .fail( deferred.reject );
+                                               .fail( master.reject );
+                               } else if ( resolveValues[ i ] &&
+                                       jQuery.isFunction( (method = resolveValues[ i ].then) ) ) {
+
+                                       method.call(
+                                               resolveValues[ i ],
+                                               updateFunc( i, resolveContexts, resolveValues ),
+                                               master.reject,
+                                               updateFunc( i, progressContexts, progressValues )
+                                       );
                                } else {
                                        --remaining;
                                }
@@ -144,10 +337,10 @@ jQuery.extend({
 
                // if we're not waiting on anything, resolve the master
                if ( !remaining ) {
-                       deferred.resolveWith( resolveContexts, resolveValues );
+                       master.resolveWith( resolveContexts, resolveValues );
                }
 
-               return deferred.promise();
+               return master.promise();
        }
 });
 
index 184cea93a40dcb7e2c9a10e9117e9d225eb5898a..3e6fcea813109526b22745ad36936417f197bedb 100644 (file)
@@ -13,6 +13,7 @@
        <script src="data/jquery-1.9.1.js"></script>
 
        <script src="../external/qunit/qunit.js"></script>
+       <script src="../external/npo/npo.js"></script>
        <script src="../external/requirejs/require.js"></script>
        <script src="../external/sinon/fake_timers.js"></script>
        <script src="../external/sinon/timers_ie.js"></script>
diff --git a/test/promises-aplus-adapter.js b/test/promises-aplus-adapter.js
new file mode 100644 (file)
index 0000000..bb15873
--- /dev/null
@@ -0,0 +1,23 @@
+/*jshint es3:false, node:true */
+"use strict";
+
+require( "jsdom" ).env( "", function ( errors, window ) {
+       if ( errors ) {
+               console.error( errors );
+               return;
+       }
+
+       var jQuery = require( ".." )( window );
+
+       exports.deferred = function () {
+               var deferred = jQuery.Deferred();
+
+               return {
+                       get promise() {
+                               return deferred.promise();
+                       },
+                       resolve: deferred.resolve.bind( deferred ),
+                       reject: deferred.reject.bind( deferred )
+               };
+       };
+});
index 40f9362196306554fefb33ec09538a815b6c6031..c2dcf33d0b89f499feaf5cd6f3ffb2511c30c895 100644 (file)
@@ -14,7 +14,7 @@ jQuery.each( [ "", " - new operator" ], function( _, withNew ) {
 
                var defer = createDeferred();
 
-               strictEqual( defer.pipe, defer.then, "pipe is an alias of then" );
+               ok( jQuery.isFunction( defer.pipe ), "defer.pipe is a function" );
 
                createDeferred().resolve().done(function() {
                        ok( true, "Success on resolve" );
@@ -93,15 +93,16 @@ test( "jQuery.Deferred - chainability", function() {
        });
 });
 
-test( "jQuery.Deferred.then - filtering (done)", function() {
+test( "jQuery.Deferred.then - filtering (done)", function( assert ) {
 
-       expect( 4 );
+       assert.expect( 4 );
 
        var value1, value2, value3,
                defer = jQuery.Deferred(),
                piped = defer.then(function( a, b ) {
                        return a * b;
-               });
+               }),
+               done = jQuery.map( new Array( 3 ), function() { return assert.async(); } );
 
        piped.done(function( result ) {
                value3 = result;
@@ -112,32 +113,35 @@ test( "jQuery.Deferred.then - filtering (done)", function() {
                value2 = b;
        });
 
-       defer.resolve( 2, 3 );
-
-       strictEqual( value1, 2, "first resolve value ok" );
-       strictEqual( value2, 3, "second resolve value ok" );
-       strictEqual( value3, 6, "result of filter ok" );
+       defer.resolve( 2, 3 ).then(function() {
+               assert.strictEqual( value1, 2, "first resolve value ok" );
+               assert.strictEqual( value2, 3, "second resolve value ok" );
+               assert.strictEqual( value3, 6, "result of filter ok" );
+               done.pop().call();
+       });
 
        jQuery.Deferred().reject().then(function() {
-               ok( false, "then should not be called on reject" );
-       });
+               assert.ok( false, "then should not be called on reject" );
+       }).then( null, done.pop() );
 
        jQuery.Deferred().resolve().then( jQuery.noop ).done(function( value ) {
-               strictEqual( value, undefined, "then done callback can return undefined/null" );
+               assert.strictEqual( value, undefined, "then done callback can return undefined/null" );
+               done.pop().call();
        });
 });
 
-test( "jQuery.Deferred.then - filtering (fail)", function() {
+test( "jQuery.Deferred.then - filtering (fail)", function( assert ) {
 
-       expect( 4 );
+       assert.expect( 4 );
 
        var value1, value2, value3,
                defer = jQuery.Deferred(),
                piped = defer.then( null, function( a, b ) {
                        return a * b;
-               });
+               }),
+               done = jQuery.map( new Array( 3 ), function() { return assert.async(); } );
 
-       piped.fail(function( result ) {
+       piped.done(function( result ) {
                value3 = result;
        });
 
@@ -146,30 +150,70 @@ test( "jQuery.Deferred.then - filtering (fail)", function() {
                value2 = b;
        });
 
-       defer.reject( 2, 3 );
-
-       strictEqual( value1, 2, "first reject value ok" );
-       strictEqual( value2, 3, "second reject value ok" );
-       strictEqual( value3, 6, "result of filter ok" );
+       defer.reject( 2, 3 ).then( null, function() {
+               assert.strictEqual( value1, 2, "first reject value ok" );
+               assert.strictEqual( value2, 3, "second reject value ok" );
+               assert.strictEqual( value3, 6, "result of filter ok" );
+               done.pop().call();
+       });
 
        jQuery.Deferred().resolve().then( null, function() {
-               ok( false, "then should not be called on resolve" );
+               assert.ok( false, "then should not be called on resolve" );
+       }).then( done.pop() );
+
+       jQuery.Deferred().reject().then( null, jQuery.noop ).done(function( value ) {
+               assert.strictEqual( value, undefined, "then fail callback can return undefined/null" );
+               done.pop().call();
        });
+});
+
+test( "[PIPE ONLY] jQuery.Deferred.pipe - filtering (fail)", function( assert ) {
+
+       assert.expect( 4 );
+
+       var value1, value2, value3,
+               defer = jQuery.Deferred(),
+               piped = defer.pipe( null, function( a, b ) {
+                       return a * b;
+               }),
+               done = jQuery.map( new Array( 3 ), function() { return assert.async(); } );
 
-       jQuery.Deferred().reject().then( null, jQuery.noop ).fail(function( value ) {
-               strictEqual( value, undefined, "then fail callback can return undefined/null" );
+       piped.fail(function( result ) {
+               value3 = result;
+       });
+
+       defer.fail(function( a, b ) {
+               value1 = a;
+               value2 = b;
+       });
+
+       defer.reject( 2, 3 ).pipe( null, function() {
+               assert.strictEqual( value1, 2, "first reject value ok" );
+               assert.strictEqual( value2, 3, "second reject value ok" );
+               assert.strictEqual( value3, 6, "result of filter ok" );
+               done.pop().call();
+       });
+
+       jQuery.Deferred().resolve().pipe( null, function() {
+               assert.ok( false, "then should not be called on resolve" );
+       }).then( done.pop() );
+
+       jQuery.Deferred().reject().pipe( null, jQuery.noop ).fail(function( value ) {
+               assert.strictEqual( value, undefined, "then fail callback can return undefined/null" );
+               done.pop().call();
        });
 });
 
-test( "jQuery.Deferred.then - filtering (progress)", function() {
+test( "jQuery.Deferred.then - filtering (progress)", function( assert ) {
 
-       expect( 3 );
+       assert.expect( 3 );
 
        var value1, value2, value3,
                defer = jQuery.Deferred(),
                piped = defer.then( null, null, function( a, b ) {
                        return a * b;
-               });
+               }),
+               done = assert.async();
 
        piped.progress(function( result ) {
                value3 = result;
@@ -180,16 +224,17 @@ test( "jQuery.Deferred.then - filtering (progress)", function() {
                value2 = b;
        });
 
-       defer.notify( 2, 3 );
-
-       strictEqual( value1, 2, "first progress value ok" );
-       strictEqual( value2, 3, "second progress value ok" );
-       strictEqual( value3, 6, "result of filter ok" );
+       defer.notify( 2, 3 ).then( null, null, function() {
+               assert.strictEqual( value1, 2, "first progress value ok" );
+               assert.strictEqual( value2, 3, "second progress value ok" );
+               assert.strictEqual( value3, 6, "result of filter ok" );
+               done();
+       });
 });
 
-test( "jQuery.Deferred.then - deferred (done)", function() {
+test( "jQuery.Deferred.then - deferred (done)", function( assert ) {
 
-       expect( 3 );
+       assert.expect( 3 );
 
        var value1, value2, value3,
                defer = jQuery.Deferred(),
@@ -197,7 +242,8 @@ test( "jQuery.Deferred.then - deferred (done)", function() {
                        return jQuery.Deferred(function( defer ) {
                                defer.reject( a * b );
                        });
-               });
+               }),
+               done = assert.async();
 
        piped.fail(function( result ) {
                value3 = result;
@@ -210,14 +256,17 @@ test( "jQuery.Deferred.then - deferred (done)", function() {
 
        defer.resolve( 2, 3 );
 
-       strictEqual( value1, 2, "first resolve value ok" );
-       strictEqual( value2, 3, "second resolve value ok" );
-       strictEqual( value3, 6, "result of filter ok" );
+       piped.fail(function() {
+               assert.strictEqual( value1, 2, "first resolve value ok" );
+               assert.strictEqual( value2, 3, "second resolve value ok" );
+               assert.strictEqual( value3, 6, "result of filter ok" );
+               done();
+       });
 });
 
-test( "jQuery.Deferred.then - deferred (fail)", function() {
+test( "jQuery.Deferred.then - deferred (fail)", function( assert ) {
 
-       expect( 3 );
+       assert.expect( 3 );
 
        var value1, value2, value3,
                defer = jQuery.Deferred(),
@@ -225,7 +274,8 @@ test( "jQuery.Deferred.then - deferred (fail)", function() {
                        return jQuery.Deferred(function( defer ) {
                                defer.resolve( a * b );
                        });
-               });
+               }),
+               done = assert.async();
 
        piped.done(function( result ) {
                value3 = result;
@@ -238,14 +288,17 @@ test( "jQuery.Deferred.then - deferred (fail)", function() {
 
        defer.reject( 2, 3 );
 
-       strictEqual( value1, 2, "first reject value ok" );
-       strictEqual( value2, 3, "second reject value ok" );
-       strictEqual( value3, 6, "result of filter ok" );
+       piped.done(function() {
+               assert.strictEqual( value1, 2, "first reject value ok" );
+               assert.strictEqual( value2, 3, "second reject value ok" );
+               assert.strictEqual( value3, 6, "result of filter ok" );
+               done();
+       });
 });
 
-test( "jQuery.Deferred.then - deferred (progress)", function() {
+test( "jQuery.Deferred.then - deferred (progress)", function( assert ) {
 
-       expect( 3 );
+       assert.expect( 3 );
 
        var value1, value2, value3,
                defer = jQuery.Deferred(),
@@ -253,7 +306,48 @@ test( "jQuery.Deferred.then - deferred (progress)", function() {
                        return jQuery.Deferred(function( defer ) {
                                defer.resolve( a * b );
                        });
+               }),
+               done = assert.async();
+
+       piped.progress(function( result ) {
+               return jQuery.Deferred().resolve().then(function() {
+                       return result;
+               }).then(function( result ) {
+                       value3 = result;
                });
+       });
+
+       defer.progress(function( a, b ) {
+               value1 = a;
+               value2 = b;
+       });
+
+       defer.notify( 2, 3 );
+
+       piped.then( null, null, function( result ) {
+               return jQuery.Deferred().resolve().then(function() {
+                       return result;
+               }).then(function() {
+                       assert.strictEqual( value1, 2, "first progress value ok" );
+                       assert.strictEqual( value2, 3, "second progress value ok" );
+                       assert.strictEqual( value3, 6, "result of filter ok" );
+                       done();
+               });
+       });
+});
+
+test( "[PIPE ONLY] jQuery.Deferred.pipe - deferred (progress)", function( assert ) {
+
+       assert.expect( 3 );
+
+       var value1, value2, value3,
+               defer = jQuery.Deferred(),
+               piped = defer.pipe( null, null, function( a, b ) {
+                       return jQuery.Deferred(function( defer ) {
+                               defer.resolve( a * b );
+                       });
+               }),
+               done = assert.async();
 
        piped.done(function( result ) {
                value3 = result;
@@ -266,29 +360,36 @@ test( "jQuery.Deferred.then - deferred (progress)", function() {
 
        defer.notify( 2, 3 );
 
-       strictEqual( value1, 2, "first progress value ok" );
-       strictEqual( value2, 3, "second progress value ok" );
-       strictEqual( value3, 6, "result of filter ok" );
+       piped.done(function() {
+               assert.strictEqual( value1, 2, "first progress value ok" );
+               assert.strictEqual( value2, 3, "second progress value ok" );
+               assert.strictEqual( value3, 6, "result of filter ok" );
+               done();
+       });
 });
 
-test( "jQuery.Deferred.then - context", function() {
+test( "jQuery.Deferred.then - context", function( assert ) {
 
-       expect( 7 );
+       assert.expect( 7 );
 
        var defer, piped, defer2, piped2,
-               context = {};
+               context = {},
+               done = jQuery.map( new Array( 4 ), function() { return assert.async(); } );
 
        jQuery.Deferred().resolveWith( context, [ 2 ] ).then(function( value ) {
                return value * 3;
        }).done(function( value ) {
-               strictEqual( this, context, "custom context correctly propagated" );
-               strictEqual( value, 6, "proper value received" );
+               assert.notStrictEqual( this, context, "custom context not propagated through .then" );
+               assert.strictEqual( value, 6, "proper value received" );
+               done.pop().call();
        });
 
        jQuery.Deferred().resolve().then(function() {
-               return jQuery.Deferred().resolveWith(context);
+               return jQuery.Deferred().resolveWith( context );
        }).done(function() {
-               strictEqual( this, context, "custom context of returned deferred correctly propagated" );
+               assert.strictEqual( this, context,
+                       "custom context of returned deferred correctly propagated" );
+               done.pop().call();
        });
 
        defer = jQuery.Deferred();
@@ -299,8 +400,10 @@ test( "jQuery.Deferred.then - context", function() {
        defer.resolve( 2 );
 
        piped.done(function( value ) {
-               strictEqual( this, piped, "default context gets updated to latest promise in the chain" );
-               strictEqual( value, 6, "proper value received" );
+               assert.strictEqual( this, piped,
+                       "default context gets updated to latest promise in the chain" );
+               assert.strictEqual( value, 6, "proper value received" );
+               done.pop().call();
        });
 
        defer2 = jQuery.Deferred();
@@ -309,11 +412,159 @@ test( "jQuery.Deferred.then - context", function() {
        defer2.resolve( 2 );
 
        piped2.done(function( value ) {
-               strictEqual( this, piped2, "default context gets updated to latest promise in the chain (without passing function)" );
-               strictEqual( value, 2, "proper value received (without passing function)" );
+               assert.strictEqual( this, piped2,
+                       "default context updated to latest promise in the chain (without passing function)" );
+               assert.strictEqual( value, 2, "proper value received (without passing function)" );
+               done.pop().call();
        });
 });
 
+test( "[PIPE ONLY] jQuery.Deferred.pipe - context", function( assert ) {
+
+       assert.expect( 7 );
+
+       var defer, piped, defer2, piped2,
+               context = {},
+               done = jQuery.map( new Array( 4 ), function() { return assert.async(); } );
+
+       jQuery.Deferred().resolveWith( context, [ 2 ] ).pipe(function( value ) {
+               return value * 3;
+       }).done(function( value ) {
+               assert.strictEqual( this, context, "[PIPE ONLY] custom context correctly propagated" );
+               assert.strictEqual( value, 6, "proper value received" );
+               done.pop().call();
+       });
+
+       jQuery.Deferred().resolve().pipe(function() {
+               return jQuery.Deferred().resolveWith(context);
+       }).done(function() {
+               assert.strictEqual( this, context,
+                       "custom context of returned deferred correctly propagated" );
+               done.pop().call();
+       });
+
+       defer = jQuery.Deferred();
+       piped = defer.pipe(function( value ) {
+               return value * 3;
+       });
+
+       defer.resolve( 2 );
+
+       piped.done(function( value ) {
+               assert.strictEqual( this, piped,
+                       "default context gets updated to latest promise in the chain" );
+               assert.strictEqual( value, 6, "proper value received" );
+               done.pop().call();
+       });
+
+       defer2 = jQuery.Deferred();
+       piped2 = defer2.pipe();
+
+       defer2.resolve( 2 );
+
+       piped2.done(function( value ) {
+               assert.strictEqual( this, piped2,
+                       "default context updated to latest promise in the chain (without passing function)" );
+               assert.strictEqual( value, 2, "proper value received (without passing function)" );
+               done.pop().call();
+       });
+});
+
+asyncTest( "jQuery.Deferred.then - spec compatibility", function() {
+
+       expect( 1 );
+
+       var defer = jQuery.Deferred().done(function() {
+               setTimeout( start );
+               throw new Error();
+       });
+
+       defer.then(function() {
+               ok( true, "errors in .done callbacks don't stop .then handlers" );
+       });
+
+       try {
+               defer.resolve();
+       } catch ( _ ) {}
+});
+
+test( "jQuery.Deferred - 1.x/2.x compatibility", function( assert ) {
+
+       expect( 8 );
+
+       var context = { id: "callback context" },
+               thenable = jQuery.Deferred().resolve( "thenable fulfillment" ).promise(),
+               done = jQuery.map( new Array( 8 ), function() { return assert.async(); } );
+
+       thenable.unwrapped = false;
+
+       jQuery.Deferred().resolve( 1, 2 ).then(function() {
+               assert.deepEqual( [].slice.call( arguments ), [ 1, 2 ],
+                       ".then fulfillment callbacks receive all resolution values" );
+               done.pop().call();
+       });
+       jQuery.Deferred().reject( 1, 2 ).then( null, function() {
+               assert.deepEqual( [].slice.call( arguments ), [ 1, 2 ],
+                       ".then rejection callbacks receive all rejection values" );
+               done.pop().call();
+       });
+       jQuery.Deferred().notify( 1, 2 ).then( null, null, function() {
+               assert.deepEqual( [].slice.call( arguments ), [ 1, 2 ],
+                       ".then progress callbacks receive all progress values" );
+               done.pop().call();
+       });
+
+       jQuery.Deferred().resolveWith( context ).then(function() {
+               assert.deepEqual( this, context, ".then fulfillment callbacks receive context" );
+               done.pop().call();
+       });
+       jQuery.Deferred().rejectWith( context ).then( null, function() {
+               assert.deepEqual( this, context, ".then rejection callbacks receive context" );
+               done.pop().call();
+       });
+       jQuery.Deferred().notifyWith( context ).then( null, null, function() {
+               assert.deepEqual( this, context, ".then progress callbacks receive context" );
+               done.pop().call();
+       });
+
+       jQuery.Deferred().resolve( thenable ).done(function( value ) {
+               assert.strictEqual( value, thenable, ".done doesn't unwrap thenables" );
+               done.pop().call();
+       });
+
+       jQuery.Deferred().notify( thenable ).then().then( null, null, function( value ) {
+               assert.strictEqual( value, "thenable fulfillment",
+                       ".then implicit progress callbacks unwrap thenables" );
+               done.pop().call();
+       });
+});
+
+test( "jQuery.Deferred.then - progress and thenables", function( assert ) {
+
+       expect( 2 );
+
+       var trigger = jQuery.Deferred().notify(),
+               expectedProgress = [ "baz", "baz" ],
+               done = jQuery.map( new Array( 2 ), function() { return assert.async(); } ),
+               failer = function( evt ) {
+                       return function() {
+                               ok( false, "no unexpected " + evt );
+                       };
+               };
+
+       trigger.then( null, null, function() {
+               var notifier = jQuery.Deferred().notify( "foo" );
+               setTimeout(function() {
+                       notifier.notify( "bar" ).resolve( "baz" );
+               });
+               return notifier;
+       }).then( failer( "fulfill" ), failer( "reject" ), function( v ) {
+               assert.strictEqual( v, expectedProgress.shift(), "expected progress value" );
+               done.pop().call();
+       });
+       trigger.notify();
+});
+
 test( "jQuery.when", function() {
 
        expect( 37 );
@@ -376,30 +627,43 @@ test( "jQuery.when", function() {
 
 test( "jQuery.when - joined", function() {
 
-       expect( 119 );
+       expect( 195 );
 
        var deferreds = {
-                       value: 1,
-                       success: jQuery.Deferred().resolve( 1 ),
-                       error: jQuery.Deferred().reject( 0 ),
-                       futureSuccess: jQuery.Deferred().notify( true ),
-                       futureError: jQuery.Deferred().notify( true ),
-                       notify: jQuery.Deferred().notify( true )
+                       rawValue: 1,
+                       fulfilled: jQuery.Deferred().resolve( 1 ),
+                       rejected: jQuery.Deferred().reject( 0 ),
+                       notified: jQuery.Deferred().notify( true ),
+                       eventuallyFulfilled: jQuery.Deferred().notify( true ),
+                       eventuallyRejected: jQuery.Deferred().notify( true ),
+                       fulfilledStandardPromise: Promise.resolve( 1 ),
+                       rejectedStandardPromise: Promise.reject( 0 )
                },
                willSucceed = {
-                       value: true,
-                       success: true,
-                       futureSuccess: true
+                       rawValue: true,
+                       fulfilled: true,
+                       eventuallyFulfilled: true,
+                       fulfilledStandardPromise: true
                },
                willError = {
-                       error: true,
-                       futureError: true
+                       rejected: true,
+                       eventuallyRejected: true,
+                       rejectedStandardPromise: true
                },
                willNotify = {
-                       futureSuccess: true,
-                       futureError: true,
-                       notify: true
-               };
+                       notified: true,
+                       eventuallyFulfilled: true,
+                       eventuallyRejected: true
+               },
+               counter = 49;
+
+       stop();
+
+       function restart() {
+               if ( !--counter ) {
+                       start();
+               }
+       }
 
        jQuery.each( deferreds, function( id1, defer1 ) {
                jQuery.each( deferreds, function( id2, defer2 ) {
@@ -408,9 +672,11 @@ test( "jQuery.when - joined", function() {
                                shouldNotify = willNotify[ id1 ] || willNotify[ id2 ],
                                expected = shouldResolve ? [ 1, 1 ] : [ 0, undefined ],
                                expectedNotify = shouldNotify && [ willNotify[ id1 ], willNotify[ id2 ] ],
-                               code = id1 + "/" + id2,
-                               context1 = defer1 && jQuery.isFunction( defer1.promise ) ? defer1.promise() : undefined,
-                               context2 = defer2 && jQuery.isFunction( defer2.promise ) ? defer2.promise() : undefined;
+                               code = "jQuery.when( " + id1 + ", " + id2 + " )",
+                               context1 = defer1 && jQuery.isFunction( defer1.promise ) ? defer1.promise() :
+                                       ( defer1.then ? window : undefined ),
+                               context2 = defer2 && jQuery.isFunction( defer2.promise ) ? defer2.promise() :
+                                       ( defer2.then ? window : undefined );
 
                        jQuery.when( defer1, defer2 ).done(function( a, b ) {
                                if ( shouldResolve ) {
@@ -430,11 +696,11 @@ test( "jQuery.when - joined", function() {
                                deepEqual( [ a, b ], expectedNotify, code + " => progress" );
                                strictEqual( this[ 0 ], expectedNotify[ 0 ] ? context1 : undefined, code + " => first context OK" );
                                strictEqual( this[ 1 ], expectedNotify[ 1 ] ? context2 : undefined, code + " => second context OK" );
-                       });
+                       }).always( restart );
                });
        });
-       deferreds.futureSuccess.resolve( 1 );
-       deferreds.futureError.reject( 0 );
+       deferreds.eventuallyFulfilled.resolve( 1 );
+       deferreds.eventuallyRejected.reject( 0 );
 });
 
 test( "jQuery.when - resolved", function() {
@@ -456,5 +722,75 @@ test( "jQuery.when - resolved", function() {
        }).fail(function() {
                ok( false, "Error on resolve" );
        });
+});
+
+test( "jQuery.when - filtering", function() {
+
+       expect( 2 );
+
+       function increment( x ) {
+               return x + 1;
+       }
+
+       stop();
+
+       jQuery.when(
+               jQuery.Deferred().resolve( 3 ).then( increment ),
+               jQuery.Deferred().reject( 5 ).then( null, increment )
+       ).done(function( four, six ) {
+               strictEqual( four, 4, "resolved value incremented" );
+               strictEqual( six, 6, "rejected value incremented" );
+               start();
+       });
+});
+
+test( "jQuery.when - exceptions", function() {
+
+       expect( 2 );
+
+       function woops() {
+               throw "exception thrown";
+       }
+
+       stop();
+
+       jQuery.Deferred().resolve().then( woops ).fail(function( doneException ) {
+               strictEqual( doneException, "exception thrown", "throwing in done handler" );
+               jQuery.Deferred().reject().then( null, woops ).fail(function( failException ) {
+                       strictEqual( failException, "exception thrown", "throwing in fail handler" );
+                       start();
+               });
+       });
+});
+
+test( "jQuery.when - chaining", function() {
+
+       expect( 4 );
+
+       var defer = jQuery.Deferred();
+
+       function chain() {
+               return defer;
+       }
+
+       function chainStandard() {
+               return Promise.resolve( "std deferred" );
+       }
+
+       stop();
+
+       jQuery.when(
+               jQuery.Deferred().resolve( 3 ).then( chain ),
+               jQuery.Deferred().reject( 5 ).then( null, chain ),
+               jQuery.Deferred().resolve( 3 ).then( chainStandard ),
+               jQuery.Deferred().reject( 5 ).then( null, chainStandard )
+       ).done(function( v1, v2, s1, s2 ) {
+               strictEqual( v1, "other deferred", "chaining in done handler" );
+               strictEqual( v2, "other deferred", "chaining in fail handler" );
+               strictEqual( s1, "std deferred", "chaining thenable in done handler" );
+               strictEqual( s2, "std deferred", "chaining thenable in fail handler" );
+               start();
+       });
 
+       defer.resolve( "other deferred" );
 });