From efac12aced0a0a3cbcee61643f8d43846df08789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82e=CC=A8biowski-Owczarek?= Date: Sat, 14 Sep 2024 23:48:08 +0200 Subject: [PATCH] Manipulation: Fill in & warn `jQuery.Deferred.getStackHook` The API has been replaced by `jQuery.Deferred.getErrorHook`; the legacy name will be removed in jQuery 4.0.0. Fixes gh-483 --- build/tasks/npmcopy.js | 5 +- eslint.config.js | 1 + package-lock.json | 142 +++++++++++++++++++- package.json | 1 + src/jquery/deferred.js | 45 ++++++- test/index.html | 1 + test/unit/jquery/deferred.js | 242 ++++++++++++++++++++++++++++++++++- 7 files changed, 431 insertions(+), 6 deletions(-) diff --git a/build/tasks/npmcopy.js b/build/tasks/npmcopy.js index 9bd3eea4..47c943ab 100644 --- a/build/tasks/npmcopy.js +++ b/build/tasks/npmcopy.js @@ -8,7 +8,10 @@ const files = { "qunit/qunit.js": "qunit/qunit/qunit.js", "qunit/qunit.css": "qunit/qunit/qunit.css", - "qunit/LICENSE.txt": "qunit/LICENSE.txt" + "qunit/LICENSE.txt": "qunit/LICENSE.txt", + + "sinon/sinon.js": "sinon/pkg/sinon.js", + "sinon/LICENSE.txt": "sinon/LICENSE" }; async function npmcopy() { diff --git a/eslint.config.js b/eslint.config.js index a83d79b0..6faa17df 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -95,6 +95,7 @@ export default [ ...globals.browser, jQuery: false, QUnit: false, + sinon: false, url: false, expectWarning: false, expectNoWarning: false, diff --git a/package-lock.json b/package-lock.json index 82ef8940..67deac94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "jquery-migrate", - "version": "3.5.1-pre", + "version": "3.5.3-pre", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "jquery-migrate", - "version": "3.5.1-pre", + "version": "3.5.3-pre", "license": "MIT", "devDependencies": { "@types/selenium-webdriver": "4.1.22", @@ -30,6 +30,7 @@ "qunit": "2.21.0", "rollup": "4.18.0", "selenium-webdriver": "4.21.0", + "sinon": "9.2.4", "uglify-js": "3.9.4", "yargs": "17.7.2" }, @@ -430,6 +431,45 @@ "win32" ] }, + "node_modules/@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", + "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.3.1.tgz", + "integrity": "sha512-1Hc0b1TtyfBu8ixF/tpfSHTVWKwCBLY4QJbkgnE7HcwyvT2xArDxb4K7dMgqRm3szI+LJbzmW/s4xxEhv6hwDg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "dev": true, + "license": "(Unlicense OR Apache-2.0)" + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -3032,6 +3072,13 @@ "setimmediate": "^1.0.5" } }, + "node_modules/just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true, + "license": "MIT" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3078,6 +3125,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3195,6 +3249,37 @@ "node": ">= 0.6" } }, + "node_modules/nise": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz", + "integrity": "sha512-eQMEmGN/8arp0xsvGoQ+B1qvSkR73B1nWSCh7nOt5neMCtwcQVYQGdzQMhcNscktTsWB54xnlSQFzOAPJD8nXA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^6.0.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node_modules/nise/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nise/node_modules/path-to-regexp": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "0.0.1" + } + }, "node_modules/node-watch": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/node-watch/-/node-watch-0.7.3.tgz", @@ -3999,6 +4084,49 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sinon": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.4.tgz", + "integrity": "sha512-zljcULZQsJxVra28qIAL6ow1Z9tpattkCTEJR4RBP3TGc00FcttsP5pK284Nas5WjMZU5Yzy3kAIp3B3KRf5Yg==", + "deprecated": "16.1.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^1.8.1", + "@sinonjs/fake-timers": "^6.0.1", + "@sinonjs/samsam": "^5.3.1", + "diff": "^4.0.2", + "nise": "^4.0.4", + "supports-color": "^7.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/spawnback": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/spawnback/-/spawnback-1.0.1.tgz", @@ -4289,6 +4417,16 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", diff --git a/package.json b/package.json index 2e1b73e7..a4cca443 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "qunit": "2.21.0", "rollup": "4.18.0", "selenium-webdriver": "4.21.0", + "sinon": "9.2.4", "uglify-js": "3.9.4", "yargs": "17.7.2" }, diff --git a/src/jquery/deferred.js b/src/jquery/deferred.js index 30268adb..7f036efe 100644 --- a/src/jquery/deferred.js +++ b/src/jquery/deferred.js @@ -1,9 +1,15 @@ -import { migratePatchFunc, migratePatchAndWarnFunc } from "../main.js"; +import { + migratePatchFunc, + migratePatchAndWarnFunc, + migrateWarn +} from "../main.js"; +import { jQueryVersionSince } from "../compareVersions.js"; // Support jQuery slim which excludes the deferred module in jQuery 4.0+ if ( jQuery.Deferred ) { -var oldDeferred = jQuery.Deferred, +var unpatchedGetStackHookValue, + oldDeferred = jQuery.Deferred, tuples = [ // Action, add listener, callbacks, .then handlers, final state @@ -63,4 +69,39 @@ migratePatchFunc( jQuery, "Deferred", function( func ) { // Preserve handler of uncaught exceptions in promise chains jQuery.Deferred.exceptionHook = oldDeferred.exceptionHook; +// Preserve the optional hook to record the error, if defined +jQuery.Deferred.getErrorHook = oldDeferred.getErrorHook; + +// We want to mirror jQuery.Deferred.getErrorHook here, so we cannot use +// existing Migrate utils. +Object.defineProperty( jQuery.Deferred, "getStackHook", { + configurable: true, + enumerable: true, + get: function() { + if ( jQuery.migrateIsPatchEnabled( "deferred-getStackHook" ) ) { + + // jQuery 3.x checks `getStackHook` if `getErrorHook` missing; + // don't warn on the getter there. + if ( jQueryVersionSince( "4.0.0" ) ) { + migrateWarn( "deferred-getStackHook", + "jQuery.Deferred.getStackHook is deprecated; " + + "use jQuery.Deferred.getErrorHook" ); + } + return jQuery.Deferred.getErrorHook; + } else { + return unpatchedGetStackHookValue; + } + }, + set: function( newValue ) { + if ( jQuery.migrateIsPatchEnabled( "deferred-getStackHook" ) ) { + migrateWarn( "deferred-getStackHook", + "jQuery.Deferred.getStackHook is deprecated; " + + "use jQuery.Deferred.getErrorHook" ); + jQuery.Deferred.getErrorHook = newValue; + } else { + unpatchedGetStackHookValue = newValue; + } + } +} ); + } diff --git a/test/index.html b/test/index.html index a6aa9cfe..22f840c0 100644 --- a/test/index.html +++ b/test/index.html @@ -10,6 +10,7 @@ + diff --git a/test/unit/jquery/deferred.js b/test/unit/jquery/deferred.js index e602e075..0c91dfd0 100644 --- a/test/unit/jquery/deferred.js +++ b/test/unit/jquery/deferred.js @@ -1,7 +1,15 @@ // Support jQuery slim which excludes the deferred module in jQuery 4.0+ if ( jQuery.Deferred ) { -QUnit.module( "deferred" ); +QUnit.module( "deferred", { + beforeEach: function() { + this.sandbox = sinon.createSandbox(); + }, + afterEach: function() { + this.sandbox.restore(); + jQuery.Deferred.getErrorHook = jQuery.Deferred.getStackHook = undefined; + } +} ); QUnit.test( "jQuery.Deferred.exceptionHook", function( assert ) { assert.expect( 1 ); @@ -10,6 +18,238 @@ QUnit.test( "jQuery.Deferred.exceptionHook", function( assert ) { assert.ok( typeof jQuery.Deferred.exceptionHook === "function", "hook is present" ); } ); +QUnit.test( "jQuery.Deferred.getStackHook - getter", function( assert ) { + assert.expect( 5 ); + + var exceptionHookSpy, + done = assert.async(); + + // Source: https://github.com/dmethvin/jquery-deferred-reporter + function getErrorHook() { + + // Throw an error as IE doesn't capture `stack` of non-thrown ones. + try { + throw new Error( "Test exception in jQuery.Deferred" ); + } catch ( err ) { + return err; + } + } + + jQuery.Deferred.getErrorHook = getErrorHook; + + exceptionHookSpy = this.sandbox.spy( jQuery.Deferred, "exceptionHook" ); + + expectWarning( assert, "jQuery.Deferred.getStackHook - getter", + + // The getter only warns in jQuery 4+ as jQuery 3.x reads it internally. + jQueryVersionSince( "4.0.0" ) ? 1 : 0, + function() { + assert.strictEqual( jQuery.Deferred.getStackHook, jQuery.Deferred.getErrorHook, + "getStackHook mirrors getErrorHook (getter)" ); + } ); + + expectNoWarning( assert, "asyncHook reported in jQuery.Deferred.exceptionHook", function() { + jQuery + .when() + .then( function() { + throw new ReferenceError( "Test ReferenceError" ); + } ) + .catch( function() { + var asyncError = exceptionHookSpy.lastCall.args[ 1 ]; + assert.ok( asyncError instanceof Error, + "Error passed to exceptionHook (instance)" ); + assert.strictEqual( asyncError.message, "Test exception in jQuery.Deferred", + "Error passed to exceptionHook (message)" ); + done(); + } ); + } ); +} ); + +QUnit.test( "jQuery.Deferred.getStackHook - getter, no getErrorHook", function( assert ) { + assert.expect( 1 ); + + var done = assert.async(); + + expectNoWarning( assert, "No Migrate warning in a regular `then`", function() { + jQuery + .when() + .then( function() { + done(); + } ); + } ); +} ); + +QUnit.test( "jQuery.Deferred.getStackHook - setter", function( assert ) { + assert.expect( 5 ); + + var exceptionHookSpy, + done = assert.async(); + + exceptionHookSpy = this.sandbox.spy( jQuery.Deferred, "exceptionHook" ); + + expectWarning( assert, "jQuery.Deferred.getStackHook - setter", 1, function() { + var mockFn = function() {}; + jQuery.Deferred.getStackHook = mockFn; + assert.strictEqual( jQuery.Deferred.getErrorHook, mockFn, + "getStackHook mirrors getErrorHook (setter)" ); + } ); + + expectWarning( assert, "asyncHook from jQuery.Deferred.getStackHook reported", + 1, function() { + jQuery.Deferred.getStackHook = function() { + + // Throw an error as IE doesn't capture `stack` of non-thrown ones. + try { + throw new SyntaxError( "Different exception in jQuery.Deferred" ); + } catch ( err ) { + return err; + } + }; + + jQuery + .when() + .then( function() { + throw new ReferenceError( "Test ReferenceError" ); + } ) + .catch( function() { + var asyncError = exceptionHookSpy.lastCall.args[ 1 ]; + assert.ok( asyncError instanceof SyntaxError, + "Error passed to exceptionHook (instance)" ); + assert.strictEqual( asyncError.message, "Different exception in jQuery.Deferred", + "Error passed to exceptionHook (message)" ); + + done(); + } ); + } ); +} ); + +QUnit.test( "jQuery.Deferred.getStackHook - disabled patch, getter", function( assert ) { + assert.expect( 5 ); + + var exceptionHookSpy, + done = assert.async(); + + // Source: https://github.com/dmethvin/jquery-deferred-reporter + function getErrorHook() { + + // Throw an error as IE doesn't capture `stack` of non-thrown ones. + try { + throw new Error( "Test exception in jQuery.Deferred" ); + } catch ( err ) { + return err; + } + } + + jQuery.migrateDisablePatches( "deferred-getStackHook" ); + + jQuery.Deferred.getErrorHook = getErrorHook; + + exceptionHookSpy = this.sandbox.spy( jQuery.Deferred, "exceptionHook" ); + + expectNoWarning( assert, "jQuery.Deferred.getStackHook - getter", function() { + assert.strictEqual( jQuery.Deferred.getStackHook, undefined, + "getStackHook does not mirror getErrorHook (getter)" ); + } ); + + expectNoWarning( assert, "asyncHook reported in jQuery.Deferred.exceptionHook", function() { + jQuery + .when() + .then( function() { + throw new ReferenceError( "Test ReferenceError" ); + } ) + .catch( function() { + var asyncError = exceptionHookSpy.lastCall.args[ 1 ]; + assert.ok( asyncError instanceof Error, + "Error passed to exceptionHook (instance)" ); + assert.strictEqual( asyncError.message, "Test exception in jQuery.Deferred", + "Error passed to exceptionHook (message)" ); + done(); + } ); + } ); +} ); + +QUnit.test( "jQuery.Deferred.getStackHook - disabled patch, setter", function( assert ) { + assert.expect( jQueryVersionSince( "4.0.0" ) ? 4 : 5 ); + + var exceptionHookSpy, + done = assert.async(); + + // Source: https://github.com/dmethvin/jquery-deferred-reporter + function getErrorHook() { + + // Throw an error as IE doesn't capture `stack` of non-thrown ones. + try { + throw new Error( "Test exception in jQuery.Deferred" ); + } catch ( err ) { + return err; + } + } + + jQuery.migrateDisablePatches( "deferred-getStackHook" ); + + jQuery.Deferred.getErrorHook = getErrorHook; + + exceptionHookSpy = this.sandbox.spy( jQuery.Deferred, "exceptionHook" ); + + expectNoWarning( assert, "jQuery.Deferred.getStackHook - setter", function() { + var mockFn = function() {}; + jQuery.Deferred.getStackHook = mockFn; + assert.strictEqual( jQuery.Deferred.getErrorHook, getErrorHook, + "getStackHook does not mirror getErrorHook (setter)" ); + } ); + + expectNoWarning( assert, "asyncHook from jQuery.Deferred.getStackHook reported", function() { + jQuery.Deferred.getErrorHook = undefined; + jQuery.Deferred.getStackHook = function() { + + // Throw an error as IE doesn't capture `stack` of non-thrown ones. + try { + throw new SyntaxError( "Different exception in jQuery.Deferred" ); + } catch ( err ) { + return err; + } + }; + + jQuery + .when() + .then( function() { + throw new ReferenceError( "Test ReferenceError" ); + } ) + .catch( function() { + var asyncError = exceptionHookSpy.lastCall.args[ 1 ]; + + if ( jQueryVersionSince( "4.0.0" ) ) { + assert.strictEqual( asyncError, undefined, + "Error not passed to exceptionHook" ); + } else { + assert.ok( asyncError instanceof Error, + "Error passed to exceptionHook (instance)" ); + assert.strictEqual( asyncError.message, + "Different exception in jQuery.Deferred", + "Error passed to exceptionHook (message)" ); + } + + done(); + } ); + } ); +} ); + +QUnit.test( "jQuery.Deferred.getStackHook - disabled patch, getter + setter interaction", + function( assert ) { + assert.expect( 3 ); + + jQuery.migrateDisablePatches( "deferred-getStackHook" ); + + expectNoWarning( assert, "jQuery.Deferred.getStackHook - setter & getter", function() { + var mockFn = function() {}; + assert.strictEqual( jQuery.Deferred.getStackHook, undefined, + "getStackHook is `undefined` by default" ); + jQuery.Deferred.getStackHook = mockFn; + assert.strictEqual( jQuery.Deferred.getStackHook, mockFn, + "getStackHook getter reports what the setter set" ); + } ); +} ); + QUnit.test( ".pipe() warnings", function( assert ) { assert.expect( 4 );