diff --git a/src/core.js b/src/core.js index 54b44b1ea..83d427c78 100644 --- a/src/core.js +++ b/src/core.js @@ -212,7 +212,12 @@ jQuery.extend( { noop: function() {}, isFunction: function( obj ) { - return jQuery.type( obj ) === "function"; + + // Support: Chrome <=57, Firefox <=52 + // In some browsers, typeof returns "function" for HTML elements + // (i.e., `typeof document.createElement( "object" ) === "function"`). + // We don't want to classify *any* DOM node as a function. + return typeof obj === "function" && typeof obj.nodeType !== "number"; }, isWindow: function( obj ) { @@ -464,7 +469,7 @@ function isArrayLike( obj ) { var length = !!obj && "length" in obj && obj.length, type = jQuery.type( obj ); - if ( type === "function" || jQuery.isWindow( obj ) ) { + if ( jQuery.isFunction( obj ) || jQuery.isWindow( obj ) ) { return false; } diff --git a/test/unit/core.js b/test/unit/core.js index adccfb58f..c717711da 100644 --- a/test/unit/core.js +++ b/test/unit/core.js @@ -406,7 +406,7 @@ QUnit[ "assign" in Object ? "test" : "skip" ]( "isPlainObject(Object.assign(...) QUnit.test( "isFunction", function( assert ) { - assert.expect( 19 ); + assert.expect( 20 ); var mystr, myarr, myfunction, fn, obj, nodes, first, input, a; @@ -439,9 +439,11 @@ QUnit.test( "isFunction", function( assert ) { fn = function() {}; assert.ok( jQuery.isFunction( fn ), "Normal Function" ); + assert.notOk( jQuery.isFunction( Object.create( fn ) ), "custom Function subclass" ); + obj = document.createElement( "object" ); - // Firefox says this is a function + // Some versions of Firefox and Chrome say this is a function assert.ok( !jQuery.isFunction( obj ), "Object Element" ); // Since 1.3, this isn't supported (#2968) @@ -491,6 +493,64 @@ QUnit.test( "isFunction", function( assert ) { } ); } ); +QUnit.test( "isFunction(cross-realm function)", function( assert ) { + assert.expect( 1 ); + + var iframe, doc, + done = assert.async(); + + // Functions from other windows should be matched + Globals.register( "iframeDone" ); + window.iframeDone = function( fn, detail ) { + window.iframeDone = undefined; + assert.ok( jQuery.isFunction( fn ), "cross-realm function" + + ( detail ? " - " + detail : "" ) ); + done(); + }; + + iframe = jQuery( "#qunit-fixture" )[ 0 ].appendChild( document.createElement( "iframe" ) ); + doc = iframe.contentDocument || iframe.contentWindow.document; + doc.open(); + doc.write( "" ); + doc.close(); +} ); + +supportjQuery.each( + { + GeneratorFunction: "function*() {}", + AsyncFunction: "async function() {}" + }, + function( subclass, source ) { + var fn; + try { + fn = Function( "return " + source )(); + } catch ( e ) {} + + QUnit[ fn ? "test" : "skip" ]( "isFunction(" + subclass + ")", + function( assert ) { + assert.expect( 1 ); + + assert.equal( jQuery.isFunction( fn ), true, source ); + } + ); + } +); + +QUnit[ typeof Symbol === "function" && Symbol.toStringTag ? "test" : "skip" ]( + "isFunction(custom @@toStringTag)", + function( assert ) { + assert.expect( 2 ); + + var obj = {}, + fn = function() {}; + obj[ Symbol.toStringTag ] = "Function"; + fn[ Symbol.toStringTag ] = "Object"; + + assert.equal( jQuery.isFunction( obj ), false, "function-mimicking object" ); + assert.equal( jQuery.isFunction( fn ), true, "object-mimicking function" ); + } +); + QUnit.test( "isNumeric", function( assert ) { assert.expect( 43 ); diff --git a/test/unit/deferred.js b/test/unit/deferred.js index 426af4b5f..f64d4fec8 100644 --- a/test/unit/deferred.js +++ b/test/unit/deferred.js @@ -526,9 +526,10 @@ QUnit.test( "jQuery.Deferred.then - spec compatibility", function( assert ) { assert.expect( 1 ); - var done = assert.async(); + var done = assert.async(), + defer = jQuery.Deferred(); - var defer = jQuery.Deferred().done( function() { + defer.done( function() { setTimeout( done ); throw new Error(); } ); @@ -542,6 +543,26 @@ QUnit.test( "jQuery.Deferred.then - spec compatibility", function( assert ) { } catch ( _ ) {} } ); +QUnit[ typeof Symbol === "function" && Symbol.toStringTag ? "test" : "skip" ]( + "jQuery.Deferred.then - IsCallable determination (gh-3596)", + function( assert ) { + + assert.expect( 1 ); + + var done = assert.async(), + defer = jQuery.Deferred(); + + function faker() { + assert.ok( true, "handler with non-'Function' @@toStringTag gets invoked" ); + } + faker[ Symbol.toStringTag ] = "String"; + + defer.then( faker ).then( done ); + + defer.resolve(); + } +); + // Test fails in IE9 but is skipped there because console is not active QUnit[ window.console ? "test" : "skip" ]( "jQuery.Deferred.exceptionHook", function( assert ) { @@ -861,8 +882,16 @@ QUnit.test( "jQuery.when(nonThenable) - like Promise.resolve", function( assert QUnit.test( "jQuery.when(thenable) - like Promise.resolve", function( assert ) { "use strict"; - var CASES = 16, - slice = [].slice, + var customToStringThen = { + then: function( onFulfilled ) { + onFulfilled(); + } + }; + if ( typeof Symbol === "function" ) { + customToStringThen.then[ Symbol.toStringTag ] = "String"; + } + + var slice = [].slice, sentinel = { context: "explicit" }, eventuallyFulfilled = jQuery.Deferred().notify( true ), eventuallyRejected = jQuery.Deferred().notify( true ), @@ -870,6 +899,7 @@ QUnit.test( "jQuery.when(thenable) - like Promise.resolve", function( assert ) { secondaryRejected = jQuery.Deferred().resolve( eventuallyRejected ), inputs = { promise: Promise.resolve( true ), + customToStringThen: customToStringThen, rejectedPromise: Promise.reject( false ), deferred: jQuery.Deferred().resolve( true ), eventuallyFulfilled: eventuallyFulfilled, @@ -894,6 +924,7 @@ QUnit.test( "jQuery.when(thenable) - like Promise.resolve", function( assert ) { }, willSucceed = { promise: [ true ], + customToStringThen: [], deferred: [ true ], eventuallyFulfilled: [ true ], secondaryFulfilled: [ true ], @@ -912,14 +943,15 @@ QUnit.test( "jQuery.when(thenable) - like Promise.resolve", function( assert ) { rejectedDeferredWith: [ false ], multiRejectedDeferredWith: [ "baz", "quux" ] }, + numCases = Object.keys( willSucceed ).length + Object.keys( willError ).length, // Support: Android 4.0 only // Strict mode functions invoked without .call/.apply get global-object context defaultContext = ( function getDefaultContext() { return this; } ).call(), - done = assert.async( CASES * 2 ); + done = assert.async( numCases * 2 ); - assert.expect( CASES * 4 ); + assert.expect( numCases * 4 ); jQuery.each( inputs, function( message, value ) { var code = "jQuery.when( " + message + " )",