├── test ├── data │ ├── empty.json │ ├── null.json │ ├── qunit-start.js │ ├── simple.json │ ├── 1x1.jpg │ ├── jsonpScript.js │ ├── compareVersions.js │ ├── core-jquery3.html │ ├── event-ready.html │ ├── iframeTest.js │ ├── event-lateload.html │ ├── test-utils.js │ ├── ajax-jsonp-callback-name.html │ └── testinit.js ├── bundler_smoke_tests │ ├── src-esm-commonjs │ │ ├── jquery-migrate-require.cjs │ │ └── main.js │ ├── webpack.config.cjs │ ├── test.html │ ├── src-pure-esm │ │ └── main.js │ ├── lib │ │ ├── utils.js │ │ ├── run-webpack.js │ │ └── run-rollup.js │ ├── rollup-pure-esm.config.js │ ├── rollup-commonjs.config.js │ └── run-jsdom-tests.js ├── node_smoke_tests │ └── node_smoke_tests.cjs ├── unit │ ├── jquery │ │ ├── data.js │ │ ├── effects.js │ │ ├── manipulation.js │ │ ├── event.js │ │ ├── css.js │ │ ├── ajax.js │ │ ├── selector.js │ │ ├── attributes.js │ │ ├── deferred.js │ │ └── core.js │ └── migrate.js └── index.html ├── .npmrc ├── .husky ├── pre-commit └── commit-msg ├── dist ├── package.json └── wrappers │ └── jquery-migrate.bundler-require-wrapper.js ├── src ├── version.js ├── migratemute.js ├── migrate.js ├── compareVersions.js ├── wrapper-esm.js ├── wrapper.js ├── jquery │ ├── effects.js │ ├── manipulation.js │ ├── data.js │ ├── deferred.js │ ├── event.js │ ├── selector.js │ ├── ajax.js │ ├── attributes.js │ ├── core.js │ └── css.js ├── disablePatches.js ├── utils.js └── main.js ├── .mailmap ├── .npmignore ├── .gitattributes ├── jtr-local.yml ├── dist-module └── wrappers │ └── jquery-migrate.node-module-wrapper.js ├── jtr-ci.yml ├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── node.js.yml │ ├── filestash.yml │ ├── browserstack.yml │ ├── codeql-analysis.yml │ └── browser-tests.yml ├── .gitignore ├── LICENSE.txt ├── package.json ├── eslint.config.js ├── README.md ├── CONTRIBUTING.md └── warnings.md /test/data/empty.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/data/null.json: -------------------------------------------------------------------------------- 1 | null -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run lint 2 | -------------------------------------------------------------------------------- /test/data/qunit-start.js: -------------------------------------------------------------------------------- 1 | QUnit.start(); 2 | -------------------------------------------------------------------------------- /test/data/simple.json: -------------------------------------------------------------------------------- 1 | {"foo":"bar","gibson":42} -------------------------------------------------------------------------------- /dist/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } 4 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx commitplease .git/COMMIT_EDITMSG 2 | -------------------------------------------------------------------------------- /src/version.js: -------------------------------------------------------------------------------- 1 | 2 | jQuery.migrateVersion = "4.0.0-pre"; 3 | -------------------------------------------------------------------------------- /test/data/1x1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jquery/jquery-migrate/HEAD/test/data/1x1.jpg -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Igor Kalashnikov 2 | Michał Gołębiowski-Owczarek 3 | -------------------------------------------------------------------------------- /test/data/jsonpScript.js: -------------------------------------------------------------------------------- 1 | /* global customJsonpCallback */ 2 | 3 | "use strict"; 4 | 5 | customJsonpCallback( { answer: 42 } ); 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | CDN 2 | coverage 3 | .project 4 | .settings 5 | *~ 6 | *.diff 7 | *.patch 8 | /*.html 9 | .DS_Store 10 | external 11 | node_modules 12 | -------------------------------------------------------------------------------- /test/bundler_smoke_tests/src-esm-commonjs/jquery-migrate-require.cjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const $ = require( "jquery-migrate" ); 4 | 5 | module.exports.$required = $; 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # JS files must always use LF for tools to work 5 | *.js eol=lf 6 | *.json eol=lf 7 | -------------------------------------------------------------------------------- /jtr-local.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | 3 | runs: 4 | jquery: 5 | - git 6 | - git.min 7 | - git.slim 8 | - git.slim.min 9 | - 4.0.0-rc.1 10 | - 4.0.0-rc.1.slim 11 | 12 | retries: 1 13 | -------------------------------------------------------------------------------- /src/migratemute.js: -------------------------------------------------------------------------------- 1 | // Included only in the minified build, via Uglify2 2 | // Only turn warnings off if not already overridden 3 | if ( typeof jQuery.migrateMute === "undefined" ) { 4 | jQuery.migrateMute = true; 5 | } 6 | -------------------------------------------------------------------------------- /dist-module/wrappers/jquery-migrate.node-module-wrapper.js: -------------------------------------------------------------------------------- 1 | // Node.js is able to import from a CommonJS module in an ESM one. 2 | import jQuery from "../../dist/jquery-migrate.js"; 3 | 4 | export { jQuery, jQuery as $ }; 5 | export default jQuery; 6 | -------------------------------------------------------------------------------- /test/bundler_smoke_tests/webpack.config.cjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | entry: `${ __dirname }/src-esm-commonjs/main.js`, 5 | output: { 6 | filename: "main.js", 7 | path: `${ __dirname }/tmp/webpack` 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /dist/wrappers/jquery-migrate.bundler-require-wrapper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Bundlers are able to synchronously require an ESM module from a CommonJS one. 4 | const { jQuery } = require( "../../dist-module/jquery-migrate.module.js" ); 5 | module.exports = jQuery; 6 | -------------------------------------------------------------------------------- /jtr-ci.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | 3 | runs: 4 | jquery: 5 | - git 6 | - git.min 7 | - git.slim 8 | - git.slim.min 9 | - 4.0.0-rc.1 10 | - 4.0.0-rc.1.slim 11 | plugin: 12 | - dev 13 | - min 14 | 15 | retries: 2 16 | hard-retries: 1 17 | -------------------------------------------------------------------------------- /test/bundler_smoke_tests/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Bundlers tests: @TITLE 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = tab 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.yml] 14 | indent_style = space 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | 8 | # Group all dependabot version update PRs into one 9 | groups: 10 | github-actions: 11 | applies-to: version-updates 12 | patterns: 13 | - "*" 14 | -------------------------------------------------------------------------------- /test/bundler_smoke_tests/src-pure-esm/main.js: -------------------------------------------------------------------------------- 1 | import { $ } from "jquery-migrate"; 2 | 3 | console.assert( /^jQuery/.test( $.expando ), 4 | "jQuery.expando should be detected on full jQuery" ); 5 | console.assert( typeof $.migrateVersion === "string" && $.migrateVersion.length > 0, 6 | "jQuery.migrateVersion was not detected, the jQuery Migrate bootstrap process has failed" ); 7 | -------------------------------------------------------------------------------- /src/migrate.js: -------------------------------------------------------------------------------- 1 | import "./version.js"; 2 | import "./compareVersions.js"; 3 | import "./main.js"; 4 | import "./jquery/core.js"; 5 | import "./jquery/selector.js"; 6 | import "./jquery/ajax.js"; 7 | import "./jquery/attributes.js"; 8 | import "./jquery/css.js"; 9 | import "./jquery/data.js"; 10 | import "./jquery/effects.js"; 11 | import "./jquery/event.js"; 12 | import "./jquery/manipulation.js"; 13 | import "./jquery/deferred.js"; 14 | -------------------------------------------------------------------------------- /test/bundler_smoke_tests/lib/utils.js: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import path from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | 5 | const dirname = path.dirname( fileURLToPath( import.meta.url ) ); 6 | const TMP_BUNDLERS_DIR = path.resolve( dirname, "..", "tmp" ); 7 | 8 | export async function cleanTmpBundlersDir() { 9 | await fs.rm( TMP_BUNDLERS_DIR, { 10 | force: true, 11 | recursive: true 12 | } ); 13 | } 14 | -------------------------------------------------------------------------------- /test/bundler_smoke_tests/rollup-pure-esm.config.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { fileURLToPath } from "node:url"; 3 | import resolve from "@rollup/plugin-node-resolve"; 4 | 5 | const dirname = path.dirname( fileURLToPath( import.meta.url ) ); 6 | 7 | export default { 8 | input: `${ dirname }/src-pure-esm/main.js`, 9 | output: { 10 | dir: `${ dirname }/tmp/rollup-pure-esm`, 11 | format: "iife", 12 | sourcemap: true 13 | }, 14 | plugins: [ 15 | resolve() 16 | ] 17 | }; 18 | -------------------------------------------------------------------------------- /test/bundler_smoke_tests/rollup-commonjs.config.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { fileURLToPath } from "node:url"; 3 | import resolve from "@rollup/plugin-node-resolve"; 4 | import commonjs from "@rollup/plugin-commonjs"; 5 | 6 | const dirname = path.dirname( fileURLToPath( import.meta.url ) ); 7 | 8 | export default { 9 | input: `${ dirname }/src-esm-commonjs/main.js`, 10 | output: { 11 | dir: `${ dirname }/tmp/rollup-commonjs`, 12 | format: "iife", 13 | sourcemap: true 14 | }, 15 | plugins: [ 16 | resolve(), 17 | commonjs() 18 | ] 19 | }; 20 | -------------------------------------------------------------------------------- /test/bundler_smoke_tests/src-esm-commonjs/main.js: -------------------------------------------------------------------------------- 1 | import { $ as $imported } from "jquery-migrate"; 2 | 3 | import { $required } from "./jquery-migrate-require.cjs"; 4 | 5 | console.assert( $required === $imported, 6 | "Only one copy of full jQuery should exist" ); 7 | console.assert( /^jQuery/.test( $imported.expando ), 8 | "jQuery.expando should be detected on full jQuery" ); 9 | console.assert( typeof $imported.migrateVersion === "string" && $imported.migrateVersion.length > 0, 10 | "jQuery.migrateVersion was not detected, the jQuery Migrate bootstrap process has failed" ); 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | CDN 3 | .project 4 | .settings 5 | *~ 6 | *.diff 7 | *.patch 8 | /*.html 9 | .DS_Store 10 | .sizecache.json 11 | .eslintcache 12 | 13 | # Ignore built files in `dist` folder 14 | # Leave the package.json and wrappers 15 | /dist/* 16 | !/dist/package.json 17 | !/dist/wrappers 18 | 19 | # Ignore built files in `dist-module` folder 20 | # Leave the package.json and wrappers 21 | /dist-module/* 22 | !/dist-module/package.json 23 | !/dist-module/wrappers 24 | 25 | /external 26 | /node_modules 27 | 28 | # Ignore BrowserStack files 29 | local.log 30 | browserstack.err 31 | -------------------------------------------------------------------------------- /src/compareVersions.js: -------------------------------------------------------------------------------- 1 | // Returns 0 if v1 == v2, -1 if v1 < v2, 1 if v1 > v2 2 | function compareVersions( v1, v2 ) { 3 | var i, 4 | rVersionParts = /^(\d+)\.(\d+)\.(\d+)/, 5 | v1p = rVersionParts.exec( v1 ) || [ ], 6 | v2p = rVersionParts.exec( v2 ) || [ ]; 7 | 8 | for ( i = 1; i <= 3; i++ ) { 9 | if ( +v1p[ i ] > +v2p[ i ] ) { 10 | return 1; 11 | } 12 | if ( +v1p[ i ] < +v2p[ i ] ) { 13 | return -1; 14 | } 15 | } 16 | return 0; 17 | } 18 | 19 | export function jQueryVersionSince( version ) { 20 | return compareVersions( jQuery.fn.jquery, version ) >= 0; 21 | } 22 | -------------------------------------------------------------------------------- /src/wrapper-esm.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Migrate - v@VERSION - @DATE 3 | * Copyright OpenJS Foundation and other contributors 4 | * Released under the MIT license 5 | * https://jquery.com/license/ 6 | */ 7 | import $ from "jquery"; 8 | 9 | // For ECMAScript module environments where a proper `window` 10 | // is present, execute the factory and get jQuery. 11 | function jQueryFactory( jQuery, window ) { 12 | 13 | // @CODE 14 | // build.js inserts compiled jQuery here 15 | 16 | return jQuery; 17 | } 18 | 19 | var jQuery = jQueryFactory( $, window ); 20 | 21 | export { jQuery, jQuery as $ }; 22 | 23 | export default jQuery; 24 | -------------------------------------------------------------------------------- /test/bundler_smoke_tests/lib/run-webpack.js: -------------------------------------------------------------------------------- 1 | import webpack from "webpack"; 2 | 3 | // See https://webpack.js.org/api/node/#webpack 4 | export async function runWebpack() { 5 | return new Promise( async( resolve, reject ) => { 6 | console.log( "Running Webpack" ); 7 | 8 | const { default: config } = await import( "../webpack.config.cjs" ); 9 | 10 | webpack( config, ( err, stats ) => { 11 | if ( err || stats.hasErrors() ) { 12 | console.error( "Errors detected during Webpack compilation" ); 13 | reject( err ); 14 | return; 15 | } 16 | 17 | console.log( "Build completed: Webpack" ); 18 | resolve(); 19 | } ); 20 | } ); 21 | } 22 | -------------------------------------------------------------------------------- /test/data/compareVersions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Returns 0 if v1 == v2, -1 if v1 < v2, 1 if v1 > v2 4 | window.compareVersions = function compareVersions( v1, v2 ) { 5 | var i, 6 | rVersionParts = /^(\d+)\.(\d+)\.(\d+)/, 7 | v1p = rVersionParts.exec( v1 ) || [ ], 8 | v2p = rVersionParts.exec( v2 ) || [ ]; 9 | 10 | for ( i = 1; i <= 3; i++ ) { 11 | if ( +v1p[ i ] > +v2p[ i ] ) { 12 | return 1; 13 | } 14 | if ( +v1p[ i ] < +v2p[ i ] ) { 15 | return -1; 16 | } 17 | } 18 | return 0; 19 | }; 20 | 21 | window.jQueryVersionSince = function jQueryVersionSince( version ) { 22 | return compareVersions( jQuery.fn.jquery, version ) >= 0; 23 | }; 24 | -------------------------------------------------------------------------------- /test/data/core-jquery3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | jQuery Migrate old-jQuery initialization test 6 | 7 | 8 | 9 | 10 | 11 | 15 | 19 | 20 | 21 |

jQuery Migrate old-jQuery initialization test

22 | 23 | 24 | -------------------------------------------------------------------------------- /test/node_smoke_tests/node_smoke_tests.cjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | console.log( "Running Node.js smoke tests..." ); 4 | 5 | const assert = require( "node:assert/strict" ); 6 | const { JSDOM } = require( "jsdom" ); 7 | 8 | const { window } = new JSDOM( "$" ); 9 | 10 | // Set the window global. 11 | globalThis.window = window; 12 | 13 | const $ = require( "jquery-migrate" ); 14 | 15 | assert( /^jQuery/.test( $.expando ), 16 | "jQuery.expando was not detected, the jQuery bootstrap process has failed" ); 17 | 18 | assert( typeof $.migrateVersion === "string" && $.migrateVersion.length > 0, 19 | "jQuery.migrateVersion was not detected, the jQuery Migrate bootstrap process has failed" ); 20 | 21 | console.log( "Node.js smoke tests passed." ); 22 | -------------------------------------------------------------------------------- /test/data/event-ready.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | jQuery Migrate `ready` event iframe test 6 | 7 | 8 | 9 | 12 | 13 | 17 | 23 | 24 | 25 |

jQuery Migrate `ready` event iframe test

26 | 27 | 28 | -------------------------------------------------------------------------------- /test/data/iframeTest.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* 4 | * Iframe-based unit tests support 5 | * Load this file immediately after jQuery, before jQuery Migrate 6 | */ 7 | 8 | // Capture output so the test in the parent window can inspect it, and so the 9 | // initialization for the iframe doesn't show another Migrate startup header. 10 | var logOutput = ""; 11 | 12 | window.console.log = function() { 13 | logOutput += Array.prototype.join.call( arguments, " " ) + "\n"; 14 | }; 15 | 16 | // Callback gets ( jQuery, window, document, log [, startIframe args ] ) 17 | window.startIframeTest = function() { 18 | var args = Array.prototype.slice.call( arguments ); 19 | 20 | // Note: jQuery may be undefined if page did not load it 21 | args.unshift( window.jQuery, window, document, logOutput ); 22 | window.parent.TestManager.iframeCallback.apply( null, args ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/wrapper.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Migrate - v@VERSION - @DATE 3 | * Copyright OpenJS Foundation and other contributors 4 | * Released under the MIT license 5 | * https://jquery.com/license/ 6 | */ 7 | ( function( factory ) { 8 | "use strict"; 9 | 10 | if ( typeof define === "function" && define.amd ) { 11 | 12 | // AMD. Register as an anonymous module. 13 | define( [ "jquery" ], function( jQuery ) { 14 | return factory( jQuery, window ); 15 | } ); 16 | } else if ( typeof module === "object" && module.exports ) { 17 | 18 | // Node/CommonJS 19 | // eslint-disable-next-line no-undef 20 | module.exports = factory( require( "jquery" ), window ); 21 | } else { 22 | 23 | // Browser globals 24 | factory( jQuery, window ); 25 | } 26 | } )( function( jQuery, window ) { 27 | "use strict"; 28 | 29 | // @CODE 30 | // build.js inserts compiled jQuery here 31 | 32 | return jQuery; 33 | } ); 34 | -------------------------------------------------------------------------------- /src/jquery/effects.js: -------------------------------------------------------------------------------- 1 | import { migrateWarn } from "../main.js"; 2 | import "../disablePatches.js"; 3 | 4 | // Support jQuery slim which excludes the effects module 5 | if ( jQuery.fx ) { 6 | 7 | var intervalValue = jQuery.fx.interval, 8 | intervalMsg = "jQuery.fx.interval is removed"; 9 | 10 | // Don't warn if document is hidden, jQuery uses setTimeout (gh-292) 11 | Object.defineProperty( jQuery.fx, "interval", { 12 | configurable: true, 13 | enumerable: true, 14 | get: function() { 15 | if ( !window.document.hidden ) { 16 | migrateWarn( "fx-interval", intervalMsg ); 17 | } 18 | 19 | // Only fallback to the default if patch is enabled 20 | if ( !jQuery.migrateIsPatchEnabled( "fx-interval" ) ) { 21 | return intervalValue; 22 | } 23 | return intervalValue === undefined ? 13 : intervalValue; 24 | }, 25 | set: function( newValue ) { 26 | migrateWarn( "fx-interval", intervalMsg ); 27 | intervalValue = newValue; 28 | } 29 | } ); 30 | 31 | } 32 | -------------------------------------------------------------------------------- /test/unit/jquery/data.js: -------------------------------------------------------------------------------- 1 | QUnit.module( "data" ); 2 | 3 | QUnit.test( "properties from Object.prototype", function( assert ) { 4 | assert.expect( 8 ); 5 | 6 | var div = jQuery( "
" ).appendTo( "#qunit-fixture" ); 7 | 8 | div.data( "foo", "bar" ); 9 | jQuery._data( div[ 0 ], "baz", "qaz" ); 10 | 11 | expectNoMessage( assert, "Regular properties", function() { 12 | assert.strictEqual( div.data( "foo" ), "bar", "data access" ); 13 | assert.strictEqual( jQuery.data( div[ 0 ], "foo" ), "bar", "data access (static method)" ); 14 | assert.strictEqual( jQuery._data( div[ 0 ], "baz" ), "qaz", "private data access" ); 15 | } ); 16 | 17 | expectMessage( assert, "Properties from Object.prototype", 3, function() { 18 | assert.ok( div.data().hasOwnProperty( "foo" ), 19 | "hasOwnProperty works" ); 20 | assert.ok( jQuery.data( div[ 0 ] ).hasOwnProperty( "foo" ), 21 | "hasOwnProperty works (static method)" ); 22 | assert.ok( jQuery._data( div[ 0 ] ).hasOwnProperty( "baz" ), 23 | "hasOwnProperty works (private data)" ); 24 | } ); 25 | } ); 26 | -------------------------------------------------------------------------------- /src/jquery/manipulation.js: -------------------------------------------------------------------------------- 1 | import { migratePatchFunc, migrateWarn } from "../main.js"; 2 | import "../disablePatches.js"; 3 | 4 | var rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi, 5 | makeMarkup = function( html ) { 6 | var doc = window.document.implementation.createHTMLDocument( "" ); 7 | doc.body.innerHTML = html; 8 | return doc.body && doc.body.innerHTML; 9 | }, 10 | warnIfChanged = function( html ) { 11 | var changed = html.replace( rxhtmlTag, "<$1>" ); 12 | if ( changed !== html && makeMarkup( html ) !== makeMarkup( changed ) ) { 13 | migrateWarn( "self-closed-tags", 14 | "HTML tags must be properly nested and closed: " + html ); 15 | } 16 | }; 17 | 18 | migratePatchFunc( jQuery, "htmlPrefilter", function( html ) { 19 | warnIfChanged( html ); 20 | return html.replace( rxhtmlTag, "<$1>" ); 21 | }, "self-closed-tags" ); 22 | 23 | // This patch needs to be disabled by default as it re-introduces 24 | // security issues (CVE-2020-11022, CVE-2020-11023). 25 | jQuery.migrateDisablePatches( "self-closed-tags" ); 26 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | jQuery Migrate Test Suite 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 28 | 29 | 30 | 33 | 34 | 35 |
36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /test/data/event-lateload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | jQuery Migrate late load event binding test 6 | 7 | 8 | 9 | 10 | 13 | 14 | 18 | 37 | 38 | 39 |

jQuery Migrate late load event binding test

40 | 41 | 42 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Node 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches-ignore: "dependabot/**" 7 | 8 | jobs: 9 | build-and-test: 10 | runs-on: ubuntu-latest 11 | name: ${{ matrix.NPM_SCRIPT }} - ${{ matrix.NAME }} (${{ matrix.NODE_VERSION }}) 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | NAME: ["Node"] 16 | NODE_VERSION: [20.x, 22.x, 24.x] 17 | NPM_SCRIPT: ["test:browserless"] 18 | include: 19 | - NAME: "Node" 20 | NODE_VERSION: "22.x" 21 | NPM_SCRIPT: "lint" 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 25 | 26 | - name: Use Node.js ${{ env.NODE_VERSION }} 27 | uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 28 | with: 29 | node-version: ${{ matrix.NODE_VERSION }} 30 | cache: npm 31 | cache-dependency-path: '**/package-lock.json' 32 | 33 | - name: Install dependencies 34 | run: npm install 35 | 36 | - name: Run tests 37 | run: npm run ${{ matrix.NPM_SCRIPT }} 38 | -------------------------------------------------------------------------------- /src/disablePatches.js: -------------------------------------------------------------------------------- 1 | // A map from disabled patch codes to `true`. This should really 2 | // be a `Set` but those are unsupported in IE. 3 | var disabledPatches = Object.create( null ); 4 | 5 | // Don't apply patches for specified codes. Helpful for code bases 6 | // where some Migrate warnings have been addressed, and it's desirable 7 | // to avoid needless patches or false positives. 8 | jQuery.migrateDisablePatches = function() { 9 | var i; 10 | for ( i = 0; i < arguments.length; i++ ) { 11 | disabledPatches[ arguments[ i ] ] = true; 12 | } 13 | }; 14 | 15 | // Allow enabling patches disabled via `jQuery.migrateDisablePatches`. 16 | // Helpful if you want to disable a patch only for some code that won't 17 | // be updated soon to be able to focus on other warnings - and enable it 18 | // immediately after such a call: 19 | // ```js 20 | // jQuery.migrateDisablePatches( "workaroundA" ); 21 | // elem.pluginViolatingWarningA( "pluginMethod" ); 22 | // jQuery.migrateEnablePatches( "workaroundA" ); 23 | // ``` 24 | jQuery.migrateEnablePatches = function() { 25 | var i; 26 | for ( i = 0; i < arguments.length; i++ ) { 27 | delete disabledPatches[ arguments[ i ] ]; 28 | } 29 | }; 30 | 31 | jQuery.migrateIsPatchEnabled = function( patchCode ) { 32 | return !disabledPatches[ patchCode ]; 33 | }; 34 | -------------------------------------------------------------------------------- /src/jquery/data.js: -------------------------------------------------------------------------------- 1 | import { migratePatchFunc } from "../main.js"; 2 | import { patchProto } from "../utils.js"; 3 | 4 | function patchDataProto( original, options ) { 5 | var warningId = options.warningId, 6 | apiName = options.apiName, 7 | isInstanceMethod = options.isInstanceMethod; 8 | 9 | return function apiWithProtoPatched() { 10 | var result = original.apply( this, arguments ); 11 | 12 | if ( arguments.length !== ( isInstanceMethod ? 0 : 1 ) || result === undefined ) { 13 | return result; 14 | } 15 | 16 | patchProto( result, { 17 | warningId: warningId, 18 | apiName: apiName 19 | } ); 20 | 21 | return result; 22 | }; 23 | } 24 | 25 | migratePatchFunc( jQuery, "data", 26 | patchDataProto( jQuery.data, { 27 | warningId: "data-null-proto", 28 | apiName: "jQuery.data()", 29 | isInstanceMethod: false 30 | } ), 31 | "data-null-proto" ); 32 | migratePatchFunc( jQuery, "_data", 33 | patchDataProto( jQuery._data, { 34 | warningId: "data-null-proto", 35 | apiName: "jQuery._data()", 36 | isInstanceMethod: false 37 | } ), 38 | "data-null-proto" ); 39 | migratePatchFunc( jQuery.fn, "data", 40 | patchDataProto( jQuery.fn.data, { 41 | warningId: "data-null-proto", 42 | apiName: "jQuery.fn.data()", 43 | isInstanceMethod: true 44 | } ), 45 | "data-null-proto" ); 46 | -------------------------------------------------------------------------------- /test/unit/jquery/effects.js: -------------------------------------------------------------------------------- 1 | // Support jQuery slim which excludes the effects module 2 | if ( jQuery.fx ) { 3 | 4 | QUnit.module( "effects" ); 5 | 6 | // If the browser has requestAnimationFrame, jQuery won't touch fx.interval 7 | QUnit.test( "jQuery.fx.interval - no warning on animations", function( assert ) { 8 | assert.expect( 1 ); 9 | 10 | var start = assert.async(); 11 | 12 | // Can't use expectNoMessage since this is async 13 | jQuery.migrateReset(); 14 | jQuery( "
" ) 15 | .appendTo( "#qunit-fixture" ) 16 | .animate( { opacity: 0.5 }, 50, function() { 17 | assert.equal( jQuery.migrateMessages.length, 0, "no warning" ); 18 | start(); 19 | } ); 20 | } ); 21 | 22 | // Only rAF browsers implement the interval warning 23 | QUnit.test( "jQuery.fx.interval - user change", function( assert ) { 24 | assert.expect( 3 ); 25 | 26 | var oldInterval, 27 | warner = window.requestAnimationFrame ? expectMessage : expectNoMessage; 28 | 29 | assert.ok( true, "requestAnimationFrame is " + 30 | ( window.requestAnimationFrame ? "present" : "absent" ) ); 31 | warner( assert, "read fx.interval", function() { 32 | oldInterval = jQuery.fx.interval; 33 | } ); 34 | warner( assert, "write fx.interval", function() { 35 | jQuery.fx.interval = 13; 36 | } ); 37 | 38 | jQuery.fx.interval = oldInterval; 39 | } ); 40 | 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/filestash.yml: -------------------------------------------------------------------------------- 1 | name: Filestash 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: read # to fetch code (actions/checkout) 10 | 11 | jobs: 12 | update: 13 | runs-on: ubuntu-latest 14 | environment: filestash 15 | env: 16 | NODE_VERSION: 22.x 17 | name: Update Filestash 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 21 | 22 | - name: Use Node.js ${{ env.NODE_VERSION }} 23 | uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 24 | with: 25 | node-version: ${{ env.NODE_VERSION }} 26 | cache: npm 27 | cache-dependency-path: '**/package-lock.json' 28 | 29 | - name: Install dependencies 30 | run: npm install 31 | 32 | - name: Build 33 | run: npm run build 34 | 35 | - name: Set up SSH 36 | run: | 37 | install --directory ~/.ssh --mode 700 38 | base64 --decode <<< "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519 39 | chmod 600 ~/.ssh/id_ed25519 40 | ssh-keyscan -t ed25519 -H "${{ secrets.FILESTASH_SERVER }}" >> ~/.ssh/known_hosts 41 | 42 | - name: Upload to Filestash 43 | run: | 44 | rsync dist/jquery-migrate.js filestash@"${{ secrets.FILESTASH_SERVER }}":jquery-migrate-git.js 45 | rsync dist/jquery-migrate.min.js filestash@"${{ secrets.FILESTASH_SERVER }}":jquery-migrate-git.min.js 46 | -------------------------------------------------------------------------------- /test/unit/jquery/manipulation.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | QUnit.module( "manipulation" ); 4 | 5 | QUnit.test( "Improperly closed elements", function( assert ) { 6 | assert.expect( 4 ); 7 | 8 | var oldWindowAlert = window.alert; 9 | window.alert = function() { 10 | assert.notOk( "Called alert with " + arguments ); 11 | }; 12 | 13 | jQuery.migrateEnablePatches( "self-closed-tags" ); 14 | 15 | expectMessage( assert, "Elements not self-closable nested wrong", 4, function() { 16 | jQuery( "

" ); 17 | jQuery( "

" ); 18 | jQuery( "
" ).append( "

" ); 19 | jQuery( "" ); 24 | jQuery( "
" ).append( "" ); 25 | jQuery( "" ); 26 | } ); 27 | 28 | expectNoMessage( assert, "Elements not self-closable but tolerable", function() { 29 | jQuery( "
" ); 30 | jQuery( "

" ); 31 | jQuery( "

" ).append( "" ); 32 | } ); 33 | 34 | expectNoMessage( assert, "Bare elements", function() { 35 | jQuery( "

" ).append( "" ); 36 | jQuery( "" ); 37 | jQuery( "" ); 38 | } ); 39 | 40 | window.alert = oldWindowAlert; 41 | } ); 42 | -------------------------------------------------------------------------------- /test/bundler_smoke_tests/lib/run-rollup.js: -------------------------------------------------------------------------------- 1 | import { rollup } from "rollup"; 2 | 3 | import { loadConfigFile } from "rollup/loadConfigFile"; 4 | import path from "node:path"; 5 | import { fileURLToPath } from "node:url"; 6 | 7 | const dirname = path.dirname( fileURLToPath( import.meta.url ) ); 8 | const pureEsmConfigPath = path.resolve( 9 | dirname, "..", "rollup-pure-esm.config.js" ); 10 | const commonJSConfigPath = path.resolve( 11 | dirname, "..", "rollup-commonjs.config.js" ); 12 | 13 | // See https://rollupjs.org/javascript-api/#programmatically-loading-a-config-file 14 | async function runRollup( name, configPath ) { 15 | 16 | console.log( `Running Rollup, version: ${ name }` ); 17 | 18 | // options is an array of "inputOptions" objects with an additional 19 | // "output" property that contains an array of "outputOptions". 20 | // We generate a single output so the array only has one element. 21 | const { 22 | options: [ optionsObj ], 23 | warnings 24 | } = await loadConfigFile( configPath, {} ); 25 | 26 | // "warnings" wraps the default `onwarn` handler passed by the CLI. 27 | // This prints all warnings up to this point: 28 | warnings.flush(); 29 | 30 | const bundle = await rollup( optionsObj ); 31 | await Promise.all( optionsObj.output.map( bundle.write ) ); 32 | 33 | console.log( `Build completed: Rollup, version: ${ name }` ); 34 | } 35 | 36 | export async function runRollupPureEsm() { 37 | await runRollup( "pure ESM", pureEsmConfigPath ); 38 | } 39 | 40 | export async function runRollupEsmAndCommonJs() { 41 | await runRollup( "ESM + CommonJS", commonJSConfigPath ); 42 | } 43 | -------------------------------------------------------------------------------- /test/data/test-utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* exported expectMessage, expectNoMessage */ 4 | 5 | window.expectMessage = function expectMessage( assert, name, expected, fn ) { 6 | var result; 7 | if ( !fn ) { 8 | fn = expected; 9 | expected = null; 10 | } 11 | jQuery.migrateReset(); 12 | result = fn(); 13 | 14 | function check() { 15 | 16 | // Special-case for 0 messages expected 17 | if ( expected === 0 ) { 18 | assert.deepEqual( jQuery.migrateMessages, [], name + ": did not message" ); 19 | 20 | // Simple numeric equality assertion for messages matching an explicit 21 | // count 22 | } else if ( expected && jQuery.migrateMessages.length === expected ) { 23 | assert.equal( jQuery.migrateMessages.length, expected, name + ": messaged" ); 24 | 25 | // Simple ok assertion when we saw at least one message and weren't 26 | // looking for an explicit count 27 | } else if ( !expected && jQuery.migrateMessages.length ) { 28 | assert.ok( true, name + ": messaged" ); 29 | 30 | // Failure; use deepEqual to show the messages that *were* generated 31 | // and the expectation 32 | } else { 33 | assert.deepEqual( jQuery.migrateMessages, 34 | "", name + ": messaged" 35 | ); 36 | } 37 | } 38 | 39 | if ( result && result.then ) { 40 | return Promise.resolve( 41 | result.then( function() { 42 | check(); 43 | } ) 44 | ); 45 | } else { 46 | check(); 47 | return Promise.resolve(); 48 | } 49 | }; 50 | 51 | window.expectNoMessage = function expectNoMessage( assert, name, expected, fn ) { 52 | 53 | // Expected is present only for signature compatibility with expectMessage 54 | return expectMessage( assert, name, 0, fn || expected ); 55 | }; 56 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 2 | 3 | This software consists of voluntary contributions made by many 4 | individuals. For exact contribution history, see the revision history 5 | available at https://github.com/jquery/jquery-migrate 6 | 7 | The following license applies to all parts of this software except as 8 | documented below: 9 | 10 | ==== 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | ==== 32 | 33 | All files located in the node_modules and external directories are 34 | externally maintained libraries used by this software which have their 35 | own licenses; we recommend you read them, as their terms may differ from 36 | the terms above. 37 | -------------------------------------------------------------------------------- /test/bundler_smoke_tests/run-jsdom-tests.js: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import jsdom, { JSDOM } from "jsdom"; 3 | import path from "node:path"; 4 | import { fileURLToPath } from "node:url"; 5 | import { runRollupEsmAndCommonJs, runRollupPureEsm } from "./lib/run-rollup.js"; 6 | import { runWebpack } from "./lib/run-webpack.js"; 7 | import { cleanTmpBundlersDir } from "./lib/utils.js"; 8 | 9 | const dirname = path.dirname( fileURLToPath( import.meta.url ) ); 10 | 11 | async function runJSDOMTest( { title, folder } ) { 12 | console.log( "Running bundlers tests:", title ); 13 | 14 | const template = await fs.readFile( `${ dirname }/test.html`, "utf-8" ); 15 | const scriptSource = await fs.readFile( 16 | `${ dirname }/tmp/${ folder }/main.js`, "utf-8" ); 17 | 18 | const html = template 19 | .replace( /@TITLE\b/, () => title ) 20 | .replace( /@SCRIPT\b/, () => scriptSource ); 21 | 22 | const virtualConsole = new jsdom.VirtualConsole(); 23 | virtualConsole.forwardTo( console ); 24 | virtualConsole.on( "assert", ( success ) => { 25 | if ( !success ) { 26 | process.exitCode = 1; 27 | } 28 | } ); 29 | 30 | new JSDOM( html, { 31 | resources: "usable", 32 | runScripts: "dangerously", 33 | virtualConsole 34 | } ); 35 | 36 | if ( process.exitCode === 0 || process.exitCode == null ) { 37 | console.log( "Bundlers tests passed for:", title ); 38 | } else { 39 | console.error( "Bundlers tests failed for:", title ); 40 | } 41 | } 42 | 43 | async function buildAndTest() { 44 | await cleanTmpBundlersDir(); 45 | 46 | await Promise.all( [ 47 | runRollupPureEsm(), 48 | runRollupEsmAndCommonJs(), 49 | runWebpack() 50 | ] ); 51 | 52 | await Promise.all( [ 53 | runJSDOMTest( { 54 | title: "Rollup with pure ESM setup", 55 | folder: "rollup-pure-esm" 56 | } ), 57 | 58 | runJSDOMTest( { 59 | title: "Rollup with ESM + CommonJS", 60 | folder: "rollup-commonjs" 61 | } ), 62 | 63 | runJSDOMTest( { 64 | title: "Webpack", 65 | folder: "webpack" 66 | } ) 67 | ] ); 68 | 69 | // The directory won't be cleaned in case of failures; this may aid debugging. 70 | await cleanTmpBundlersDir(); 71 | } 72 | 73 | await buildAndTest(); 74 | -------------------------------------------------------------------------------- /src/jquery/deferred.js: -------------------------------------------------------------------------------- 1 | import { 2 | migratePatchFunc, 3 | migratePatchAndInfoFunc, 4 | migrateWarn 5 | } from "../main.js"; 6 | 7 | // Support jQuery slim which excludes the deferred module in jQuery 4.0+ 8 | if ( jQuery.Deferred ) { 9 | 10 | var unpatchedGetStackHookValue, 11 | oldDeferred = jQuery.Deferred; 12 | 13 | migratePatchFunc( jQuery, "Deferred", function( func ) { 14 | var deferred = oldDeferred(), 15 | promise = deferred.promise(); 16 | 17 | migratePatchAndInfoFunc( deferred, "pipe", deferred.pipe, "deferred-pipe", 18 | "deferred.pipe() is deprecated" ); 19 | migratePatchAndInfoFunc( promise, "pipe", promise.pipe, "deferred-pipe", 20 | "deferred.pipe() is deprecated" ); 21 | 22 | if ( func ) { 23 | func.call( deferred, deferred ); 24 | } 25 | 26 | return deferred; 27 | }, "deferred-pipe" ); 28 | 29 | // Preserve handler of uncaught exceptions in promise chains 30 | jQuery.Deferred.exceptionHook = oldDeferred.exceptionHook; 31 | 32 | // Preserve the optional hook to record the error, if defined 33 | jQuery.Deferred.getErrorHook = oldDeferred.getErrorHook; 34 | 35 | // We want to mirror jQuery.Deferred.getErrorHook here, so we cannot use 36 | // existing Migrate utils. 37 | Object.defineProperty( jQuery.Deferred, "getStackHook", { 38 | configurable: true, 39 | enumerable: true, 40 | get: function() { 41 | if ( jQuery.migrateIsPatchEnabled( "deferred-getStackHook" ) ) { 42 | migrateWarn( "deferred-getStackHook", 43 | "jQuery.Deferred.getStackHook is removed; use jQuery.Deferred.getErrorHook" ); 44 | return jQuery.Deferred.getErrorHook; 45 | } else { 46 | return unpatchedGetStackHookValue; 47 | } 48 | }, 49 | set: function( newValue ) { 50 | if ( jQuery.migrateIsPatchEnabled( "deferred-getStackHook" ) ) { 51 | 52 | // Only warn if `getErrorHook` wasn't set to the same value first. 53 | if ( jQuery.Deferred.getErrorHook !== newValue ) { 54 | migrateWarn( "deferred-getStackHook", 55 | "jQuery.Deferred.getStackHook is removed; use jQuery.Deferred.getErrorHook" ); 56 | jQuery.Deferred.getErrorHook = newValue; 57 | } 58 | } else { 59 | unpatchedGetStackHookValue = newValue; 60 | } 61 | } 62 | } ); 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import { migrateWarn } from "./main.js"; 2 | 3 | export function camelCase( string ) { 4 | return string.replace( /-([a-z])/g, function( _, letter ) { 5 | return letter.toUpperCase(); 6 | } ); 7 | } 8 | 9 | // Make `object` inherit from `Object.prototype` via an additional object 10 | // in between; that intermediate object proxies properties 11 | // to `Object.prototype`, warning about their usage first. 12 | export function patchProto( object, options ) { 13 | var i, 14 | warningId = options.warningId, 15 | apiName = options.apiName, 16 | 17 | // `Object.prototype` keys are not enumerable so list the 18 | // official ones here. An alternative would be wrapping 19 | // objects with a Proxy but that creates additional issues 20 | // like breaking object identity on subsequent calls. 21 | objProtoKeys = [ 22 | "__proto__", 23 | "__defineGetter__", 24 | "__defineSetter__", 25 | "__lookupGetter__", 26 | "__lookupSetter__", 27 | "hasOwnProperty", 28 | "isPrototypeOf", 29 | "propertyIsEnumerable", 30 | "toLocaleString", 31 | "toString", 32 | "valueOf" 33 | ], 34 | 35 | // Use a null prototype at the beginning so that we can define our 36 | // `__proto__` getter & setter. We'll reset the prototype afterward. 37 | intermediateObj = Object.create( null ); 38 | 39 | for ( i = 0; i < objProtoKeys.length; i++ ) { 40 | ( function( key ) { 41 | Object.defineProperty( intermediateObj, key, { 42 | get: function() { 43 | migrateWarn( warningId, 44 | "Accessing properties from " + apiName + 45 | " inherited from Object.prototype is removed" ); 46 | return ( key + "__cache" ) in intermediateObj ? 47 | intermediateObj[ key + "__cache" ] : 48 | Object.prototype[ key ]; 49 | }, 50 | set: function( value ) { 51 | migrateWarn( warningId, 52 | "Setting properties from " + apiName + 53 | " inherited from Object.prototype is removed" ); 54 | intermediateObj[ key + "__cache" ] = value; 55 | } 56 | } ); 57 | } )( objProtoKeys[ i ] ); 58 | } 59 | 60 | Object.setPrototypeOf( intermediateObj, Object.prototype ); 61 | Object.setPrototypeOf( object, intermediateObj ); 62 | 63 | return object; 64 | } 65 | -------------------------------------------------------------------------------- /.github/workflows/browserstack.yml: -------------------------------------------------------------------------------- 1 | name: Browserstack 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | # Once a week every Tuesday 8 | schedule: 9 | - cron: "42 1 * * 2" 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | environment: browserstack 15 | env: 16 | BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} 17 | BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} 18 | NODE_VERSION: 22.x 19 | name: ${{ matrix.BROWSER }} 20 | concurrency: 21 | group: ${{ matrix.BROWSER }} - ${{ github.sha }} 22 | timeout-minutes: 30 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | BROWSER: 27 | - 'IE_11' 28 | - 'Safari_latest' 29 | # JTR doesn't take into account the jump from Safari 18 to 26, 30 | # so we need to specify versions explicitly. 31 | # See https://github.com/jquery/jquery-test-runner/issues/17 32 | - 'Safari_18' 33 | - 'Chrome_latest' 34 | - 'Chrome_latest-1' 35 | - 'Opera_latest' 36 | - 'Edge_latest' 37 | - 'Edge_latest-1' 38 | - 'Firefox_latest' 39 | - 'Firefox_latest-1' 40 | - '_:iPhone 17_iOS_26' 41 | - '_:iPhone 16_iOS_18' 42 | - '_:iPhone 15 Pro_iOS_17' 43 | - '_:iPad Air 13 2025_iOS_26' 44 | - '_:iPad Air 13 2025_iOS_18' 45 | steps: 46 | - name: Checkout 47 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 48 | 49 | - name: Use Node.js ${{ env.NODE_VERSION }} 50 | uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 51 | with: 52 | node-version: ${{ env.NODE_VERSION }} 53 | cache: npm 54 | cache-dependency-path: '**/package-lock.json' 55 | 56 | - name: Install dependencies 57 | run: npm install 58 | 59 | - name: Build jQuery 60 | run: npm run build:all 61 | 62 | - name: Pretest script 63 | run: npm run pretest 64 | 65 | - name: Test 66 | run: | 67 | npm run test:unit -- -v -c jtr-ci.yml \ 68 | --browserstack "${{ matrix.BROWSER }}" \ 69 | --run-id ${{ github.run_id }} \ 70 | -------------------------------------------------------------------------------- /src/jquery/event.js: -------------------------------------------------------------------------------- 1 | import { 2 | migrateWarn, 3 | migrateWarnProp, 4 | migratePatchAndInfoFunc, 5 | migratePatchFunc, 6 | migratePatchProp 7 | } from "../main.js"; 8 | import "../disablePatches.js"; 9 | import { patchProto } from "../utils.js"; 10 | 11 | var oldEventAdd = jQuery.event.add; 12 | 13 | jQuery.event.props = []; 14 | jQuery.event.fixHooks = {}; 15 | 16 | migratePatchFunc( jQuery.event, "add", function( elem, types ) { 17 | 18 | // This misses the multiple-types case but that seems awfully rare 19 | if ( elem === window && types === "load" && window.document.readyState === "complete" ) { 20 | migrateWarn( "load-after-event", 21 | "jQuery(window).on('load'...) called after load event occurred" ); 22 | } 23 | return oldEventAdd.apply( this, arguments ); 24 | }, "load-after-event" ); 25 | 26 | jQuery.each( ( "blur focus focusin focusout resize scroll click dblclick " + 27 | "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + 28 | "change select submit keydown keypress keyup contextmenu" ).split( " " ), 29 | function( _i, name ) { 30 | 31 | // Handle event binding 32 | migratePatchAndInfoFunc( jQuery.fn, name, jQuery.fn[ name ], "shorthand-deprecated-v3", 33 | "jQuery.fn." + name + "() event shorthand is deprecated" ); 34 | } ); 35 | 36 | migratePatchAndInfoFunc( jQuery.fn, "bind", jQuery.fn.bind, 37 | "pre-on-methods", "jQuery.fn.bind() is deprecated" ); 38 | migratePatchAndInfoFunc( jQuery.fn, "unbind", jQuery.fn.unbind, 39 | "pre-on-methods", "jQuery.fn.unbind() is deprecated" ); 40 | migratePatchAndInfoFunc( jQuery.fn, "delegate", jQuery.fn.delegate, 41 | "pre-on-methods", "jQuery.fn.delegate() is deprecated" ); 42 | migratePatchAndInfoFunc( jQuery.fn, "undelegate", jQuery.fn.undelegate, 43 | "pre-on-methods", "jQuery.fn.undelegate() is deprecated" ); 44 | 45 | migratePatchAndInfoFunc( jQuery.fn, "hover", jQuery.fn.hover, 46 | "hover", "jQuery.fn.hover() is deprecated" ); 47 | 48 | migrateWarnProp( jQuery.event, "global", {}, "event-global", 49 | "jQuery.event.global is removed" ); 50 | 51 | migratePatchProp( jQuery.event, "special", 52 | patchProto( jQuery.extend( Object.create( null ), jQuery.event.special ), { 53 | warningId: "event-special-null-proto", 54 | apiName: "jQuery.event.special" 55 | } ), 56 | "event-special-null-proto" ); 57 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "Code scanning - action" 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches-ignore: "dependabot/**" 7 | schedule: 8 | - cron: "0 4 * * 6" 9 | 10 | permissions: 11 | contents: read # to fetch code (actions/checkout) 12 | 13 | jobs: 14 | CodeQL-Build: 15 | permissions: 16 | contents: read # to fetch code (actions/checkout) 17 | security-events: write # (github/codeql-action/autobuild) 18 | 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 24 | with: 25 | # We must fetch at least the immediate parents so that if this is 26 | # a pull request then we can checkout the head. 27 | fetch-depth: 2 28 | 29 | # If this run was triggered by a pull request event, then checkout 30 | # the head of the pull request instead of the merge commit. 31 | - run: git checkout HEAD^2 32 | if: ${{ github.event_name == 'pull_request' }} 33 | 34 | # Initializes the CodeQL tools for scanning. 35 | - name: Initialize CodeQL 36 | uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6 37 | # Override language selection by uncommenting this and choosing your languages 38 | # with: 39 | # languages: go, javascript, csharp, python, cpp, java 40 | 41 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 42 | # If this step fails, then you should remove it and run the build manually (see below) 43 | - name: Autobuild 44 | uses: github/codeql-action/autobuild@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6 45 | 46 | # ℹ️ Command-line programs to run using the OS shell. 47 | # 📚 https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 48 | 49 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 50 | # and modify them (or add more) to build your code if your project 51 | # uses a compiled language 52 | 53 | #- run: | 54 | # make bootstrap 55 | # make release 56 | 57 | - name: Perform CodeQL Analysis 58 | uses: github/codeql-action/analyze@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6 59 | -------------------------------------------------------------------------------- /test/data/ajax-jsonp-callback-name.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | jQuery Migrate test for re-using JSONP callback name with `dataType: "json"` 6 | 7 | 8 | 9 | 12 | 13 | 17 | 83 | 84 | 85 |

jQuery Migrate test for re-using JSONP callback name

86 | 87 | 88 | -------------------------------------------------------------------------------- /src/jquery/selector.js: -------------------------------------------------------------------------------- 1 | import { migratePatchFunc, migrateInfoProp, migrateInfo } from "../main.js"; 2 | 3 | // Now jQuery.expr.pseudos is the standard incantation 4 | migrateInfoProp( jQuery.expr, "filters", jQuery.expr.pseudos, "expr-pre-pseudos", 5 | "jQuery.expr.filters is deprecated; use jQuery.expr.pseudos" ); 6 | migrateInfoProp( jQuery.expr, ":", jQuery.expr.pseudos, "expr-pre-pseudos", 7 | "jQuery.expr[':'] is deprecated; use jQuery.expr.pseudos" ); 8 | 9 | function markFunction( fn ) { 10 | fn[ jQuery.expando ] = true; 11 | return fn; 12 | } 13 | 14 | migratePatchFunc( jQuery.expr.filter, "PSEUDO", function( pseudo, argument ) { 15 | 16 | // pseudo-class names are case-insensitive 17 | // https://www.w3.org/TR/selectors/#pseudo-classes 18 | // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters 19 | // Remember that setFilters inherits from pseudos 20 | var args, 21 | fn = jQuery.expr.pseudos[ pseudo ] || 22 | jQuery.expr.setFilters[ pseudo.toLowerCase() ] || 23 | jQuery.error( 24 | "Syntax error, unrecognized expression: unsupported pseudo: " + 25 | pseudo ); 26 | 27 | // The user may use createPseudo to indicate that 28 | // arguments are needed to create the filter function 29 | // just as jQuery does 30 | if ( fn[ jQuery.expando ] ) { 31 | return fn( argument ); 32 | } 33 | 34 | // But maintain support for old signatures 35 | if ( fn.length > 1 ) { 36 | migrateInfo( "legacy-custom-pseudos", 37 | "Pseudos with multiple arguments are deprecated; " + 38 | "use jQuery.expr.createPseudo()" ); 39 | args = [ pseudo, pseudo, "", argument ]; 40 | return jQuery.expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? 41 | markFunction( function( seed, matches ) { 42 | var idx, 43 | matched = fn( seed, argument ), 44 | i = matched.length; 45 | while ( i-- ) { 46 | idx = Array.prototype.indexOf.call( seed, matched[ i ] ); 47 | seed[ idx ] = !( matches[ idx ] = matched[ i ] ); 48 | } 49 | } ) : 50 | function( elem ) { 51 | return fn( elem, 0, args ); 52 | }; 53 | } 54 | 55 | return fn; 56 | }, "legacy-custom-pseudos" ); 57 | 58 | if ( typeof Proxy !== "undefined" ) { 59 | jQuery.each( [ "pseudos", "setFilters" ], function( _, api ) { 60 | jQuery.expr[ api ] = new Proxy( jQuery.expr[ api ], { 61 | set: function( _target, _prop, fn ) { 62 | if ( typeof fn === "function" && !fn[ jQuery.expando ] && fn.length > 1 ) { 63 | migrateInfo( "legacy-custom-pseudos", 64 | "Pseudos with multiple arguments are deprecated; " + 65 | "use jQuery.expr.createPseudo()" ); 66 | } 67 | return Reflect.set.apply( this, arguments ); 68 | } 69 | } ); 70 | } ); 71 | } 72 | -------------------------------------------------------------------------------- /test/unit/jquery/event.js: -------------------------------------------------------------------------------- 1 | 2 | QUnit.module( "event" ); 3 | 4 | QUnit.test( ".bind() and .unbind()", function( assert ) { 5 | assert.expect( 3 ); 6 | 7 | var $elem = jQuery( "
" ).appendTo( "#qunit-fixture" ); 8 | 9 | expectMessage( assert, ".bind()", 1, function() { 10 | $elem 11 | .bind( "click", function() { 12 | assert.ok( true, "click fired" ); 13 | } ) 14 | .trigger( "click" ); 15 | } ); 16 | 17 | expectMessage( assert, ".unbind()", 1, function() { 18 | $elem 19 | .unbind( "click" ) 20 | .trigger( "click" ); 21 | } ); 22 | } ); 23 | 24 | QUnit.test( ".delegate() and .undelegate()", function( assert ) { 25 | assert.expect( 3 ); 26 | 27 | var $div = jQuery( "
" ).appendTo( "#qunit-fixture" ); 28 | 29 | jQuery( "

" ).appendTo( $div ); 30 | 31 | expectMessage( assert, ".delegate()", 1, function() { 32 | $div 33 | .delegate( "p", "click", function() { 34 | assert.ok( true, "delegated click fired" ); 35 | } ) 36 | .find( "p" ).trigger( "click" ); 37 | } ); 38 | 39 | expectMessage( assert, ".undelegate()", 1, function() { 40 | $div 41 | .undelegate( "p", "click" ) 42 | .find( "p" ).trigger( "click" ); 43 | } ); 44 | } ); 45 | 46 | QUnit.test( "Event aliases", function( assert ) { 47 | assert.expect( 14 ); 48 | 49 | var $div = jQuery( "

" ); 50 | 51 | "scroll click submit keydown".split( " " ).forEach( function( name ) { 52 | expectMessage( assert, "." + name + "()", 2, function() { 53 | $div[ name ]( function( event ) { 54 | assert.equal( event.type, name, name ); 55 | $div.off( event ); 56 | } )[ name ](); 57 | } ); 58 | } ); 59 | 60 | expectMessage( assert, ".hover() one-arg", 1, function() { 61 | $div.hover( function( event ) { 62 | assert.ok( /mouseenter|mouseleave/.test( event.type ), event.type ); 63 | $div.off( event ); 64 | } ).trigger( "mouseenter" ).trigger( "mouseleave" ); 65 | } ); 66 | 67 | expectMessage( assert, ".hover() two-arg", 1, function() { 68 | $div.hover( 69 | function( event ) { 70 | assert.equal( "mouseenter", event.type, event.type ); 71 | }, 72 | function( event ) { 73 | assert.equal( "mouseleave", event.type, event.type ); 74 | } 75 | ).trigger( "mouseenter" ).trigger( "mouseleave" ); 76 | } ); 77 | } ); 78 | 79 | TestManager.runIframeTest( "Load within a ready handler", "event-lateload.html", 80 | function( assert, jQuery ) { 81 | assert.expect( 2 ); 82 | 83 | assert.equal( jQuery.migrateMessages.length, 1, "warnings: " + 84 | JSON.stringify( jQuery.migrateMessages ) ); 85 | assert.ok( /load/.test( jQuery.migrateMessages[ 0 ] ), "message ok" ); 86 | } ); 87 | 88 | QUnit.test( "jQuery.event.global", function( assert ) { 89 | assert.expect( 3 ); 90 | 91 | expectMessage( assert, "jQuery.event.global", 2, function() { 92 | assert.ok( jQuery.isPlainObject( jQuery.event.global ), "is a plain object" ); 93 | assert.deepEqual( jQuery.event.global, {}, "is an empty object" ); 94 | } ); 95 | } ); 96 | 97 | QUnit.test( "jQuery.event.special: properties from Object.prototype", function( assert ) { 98 | assert.expect( 4 ); 99 | 100 | try { 101 | expectNoMessage( assert, "Regular properties", function() { 102 | jQuery.event.special.fakeevent = {}; 103 | 104 | // eslint-disable-next-line no-unused-expressions 105 | jQuery.event.special.fakeevent; 106 | } ); 107 | 108 | expectMessage( assert, "Properties from Object.prototype", 2, function() { 109 | assert.ok( jQuery.event.special.hasOwnProperty( "fakeevent" ), 110 | "hasOwnProperty works (property present)" ); 111 | assert.ok( !jQuery.event.special.hasOwnProperty( "fakeevent2" ), 112 | "hasOwnProperty works (property missing)" ); 113 | } ); 114 | } finally { 115 | delete jQuery.event.special.fakeevent; 116 | } 117 | } ); 118 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-migrate", 3 | "title": "jQuery Migrate", 4 | "description": "Migrate older jQuery code to jQuery 4.x", 5 | "version": "4.0.0-pre", 6 | "type": "module", 7 | "exports": { 8 | "node": { 9 | "import": "./dist-module/wrappers/jquery-migrate.node-module-wrapper.js", 10 | "default": "./dist/jquery-migrate.js" 11 | }, 12 | "module": { 13 | "import": "./dist-module/jquery-migrate.module.js", 14 | "default": "./dist/wrappers/jquery-migrate.bundler-require-wrapper.js" 15 | }, 16 | "import": "./dist-module/jquery-migrate.module.js", 17 | "default": "./dist/jquery-migrate.js" 18 | }, 19 | "main": "dist/jquery-migrate.js", 20 | "homepage": "https://github.com/jquery/jquery-migrate", 21 | "author": { 22 | "name": "OpenJS Foundation and other contributors", 23 | "url": "https://openjsf.org" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git://github.com/jquery/jquery-migrate.git" 28 | }, 29 | "bugs": { 30 | "url": "http://bugs.jquery.com/" 31 | }, 32 | "license": "MIT", 33 | "scripts": { 34 | "build": "node ./build/command.js", 35 | "build:all": "node --input-type=module -e \"import { buildDefaultFiles } from './build/tasks/build.js'; buildDefaultFiles()\"", 36 | "build:clean": "rimraf --glob dist/*.{js,map} --glob dist-module/*.{js,map}", 37 | "build:main": "node --input-type=module -e \"import { build } from './build/tasks/build.js'; build()\"", 38 | "lint": "eslint --cache .", 39 | "npmcopy": "node build/tasks/npmcopy.js", 40 | "prepare": "husky", 41 | "pretest": "npm run npmcopy", 42 | "start": "node --input-type=module -e \"import { buildDefaultFiles } from './build/tasks/build.js'; buildDefaultFiles({ watch: true })\"", 43 | "test:browser": "npm run pretest && npm run build:all && npm run test:unit -- -b chrome -b firefox --headless", 44 | "test:browserless": "npm run pretest && npm run build:all && node test/bundler_smoke_tests/run-jsdom-tests.js && node test/node_smoke_tests/node_smoke_tests.cjs", 45 | "test:ie": "npm run pretest && npm run build:all && npm run test:unit -- -v -b ie", 46 | "test:bundlers": "npm run pretest && npm run build:all && node test/bundler_smoke_tests/run-jsdom-tests.js", 47 | "test:node_smoke_tests": "npm run pretest && npm run build:all && node test/node_smoke_tests/node_smoke_tests.cjs", 48 | "test:safari": "npm run pretest && npm run build:all && npm run test:unit -- -v -b safari", 49 | "test:server": "jtr serve", 50 | "test:esm": "npm run pretest && npm run build:main && npm run test:unit -- -f plugin=esmodules --headless ", 51 | "test:unit": "jtr", 52 | "test": "npm run build:all && npm run lint && npm run test:browserless && npm run test:browser && npm run test:esm" 53 | }, 54 | "peerDependencies": { 55 | "jquery": ">=4.0.0-beta.2 <5" 56 | }, 57 | "devDependencies": { 58 | "@rollup/plugin-commonjs": "29.0.0", 59 | "@rollup/plugin-node-resolve": "16.0.3", 60 | "chalk": "5.6.2", 61 | "commitplease": "3.2.0", 62 | "enquirer": "2.4.1", 63 | "eslint": "8.57.1", 64 | "eslint-config-jquery": "3.0.2", 65 | "eslint-plugin-import": "2.32.0", 66 | "globals": "16.5.0", 67 | "husky": "9.1.7", 68 | "jquery": "4.0.0-rc.1", 69 | "jquery-test-runner": "0.2.8", 70 | "jsdom": "27.2.0", 71 | "native-promise-only": "0.8.1", 72 | "qunit": "2.24.2", 73 | "rollup": "4.53.2", 74 | "sinon": "9.2.4", 75 | "uglify-js": "3.19.3", 76 | "webpack": "5.103.0", 77 | "yargs": "18.0.0" 78 | }, 79 | "keywords": [ 80 | "jquery", 81 | "javascript", 82 | "browser", 83 | "plugin", 84 | "migrate" 85 | ], 86 | "commitplease": { 87 | "nohook": true, 88 | "components": [ 89 | "Docs", 90 | "Tests", 91 | "Build", 92 | "Release", 93 | "Core", 94 | "Ajax", 95 | "Attributes", 96 | "Callbacks", 97 | "CSS", 98 | "Data", 99 | "Deferred", 100 | "Deprecated", 101 | "Dimensions", 102 | "Effects", 103 | "Event", 104 | "Manipulation", 105 | "Offset", 106 | "Queue", 107 | "Selector", 108 | "Serialize", 109 | "Traversing", 110 | "Wrap" 111 | ] 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /test/unit/jquery/css.js: -------------------------------------------------------------------------------- 1 | 2 | QUnit.module( "css" ); 3 | 4 | QUnit[ 5 | typeof Proxy !== "undefined" ? "test" : "skip" 6 | ]( "jQuery.cssProps", function( assert ) { 7 | assert.expect( 2 ); 8 | 9 | expectMessage( assert, "Write to cssProps", function() { 10 | jQuery.cssProps.devoHat = "awesomeHat"; 11 | } ); 12 | 13 | expectNoMessage( assert, "Read from cssProps", function() { 14 | // eslint-disable-next-line no-unused-expressions 15 | jQuery.cssProps.devoHat; 16 | // eslint-disable-next-line no-unused-expressions 17 | jQuery.cssProps.unknownProp; 18 | } ); 19 | 20 | delete jQuery.cssProps.devoHat; 21 | } ); 22 | 23 | QUnit.test( "jQuery.css with arrays", function( assert ) { 24 | assert.expect( 2 ); 25 | 26 | expectNoMessage( assert, "String value direct", function() { 27 | var cssValues = jQuery( "
" ) 28 | .css( { 29 | "z-index": "2", 30 | fontSize: "16px" 31 | } ) 32 | .css( [ "font-size", "zIndex" ] ); 33 | 34 | assert.deepEqual( cssValues, { "font-size": "16px", zIndex: "2" }, 35 | ".css( array ) works" ); 36 | } ); 37 | } ); 38 | 39 | QUnit[ 40 | typeof Proxy !== "undefined" ? "test" : "skip" 41 | ]( "jQuery.css with numbers", function( assert ) { 42 | var allowlist = [ 43 | "margin", 44 | "marginTop", 45 | "marginRight", 46 | "marginBottom", 47 | "marginLeft", 48 | "padding", 49 | "paddingTop", 50 | "paddingRight", 51 | "paddingBottom", 52 | "paddingLeft", 53 | "top", 54 | "right", 55 | "bottom", 56 | "left", 57 | "width", 58 | "height", 59 | "minWidth", 60 | "minHeight", 61 | "maxWidth", 62 | "maxHeight", 63 | "border", 64 | "borderWidth", 65 | "borderTop", 66 | "borderTopWidth", 67 | "borderRight", 68 | "borderRightWidth", 69 | "borderBottom", 70 | "borderBottomWidth", 71 | "borderLeft", 72 | "borderLeftWidth" 73 | ]; 74 | 75 | assert.expect( 7 ); 76 | 77 | function kebabCase( string ) { 78 | return string.replace( /[A-Z]/g, function( match ) { 79 | return "-" + match.toLowerCase(); 80 | } ); 81 | } 82 | 83 | expectMessage( assert, "Number value direct", function() { 84 | jQuery( "
" ).css( "fake-property", 10 ); 85 | } ); 86 | 87 | expectMessage( assert, "Number in an object", 1, function() { 88 | jQuery( "
" ).css( { 89 | "width": 14, 90 | "height": "10px", 91 | "fake-property": 2 92 | } ); 93 | } ); 94 | 95 | expectNoMessage( assert, "String value direct", function() { 96 | jQuery( "
" ).css( "fake-property", "10px" ); 97 | } ); 98 | 99 | expectNoMessage( assert, "String in an object", function() { 100 | jQuery( "
" ).css( { 101 | "width": "14em", 102 | "height": "10px", 103 | "fake-property": "2" 104 | } ); 105 | } ); 106 | 107 | expectNoMessage( assert, "Number value (allowlisted props)", function() { 108 | allowlist.forEach( function( prop ) { 109 | jQuery( "
" ).css( prop, 1 ); 110 | jQuery( "
" ).css( kebabCase( prop ), 1 ); 111 | } ); 112 | } ); 113 | 114 | expectNoMessage( assert, "Props from jQuery.cssNumber", function() { 115 | var prop; 116 | for ( prop in jQuery.cssNumber ) { 117 | jQuery( "
" ).css( prop, 1 ); 118 | jQuery( "
" ).css( kebabCase( prop ), 1 ); 119 | } 120 | } ); 121 | 122 | // z-index is tested explicitly as raw jQuery 4.0 will not have `jQuery.cssNumber` 123 | // so iterating over it won't find anything, and we'd like to ensure number values 124 | // are not warned against for safe CSS props like z-index (gh-438). 125 | expectNoMessage( assert, "z-index", function() { 126 | jQuery( "
" ).css( "z-index", 1 ); 127 | jQuery( "
" ).css( kebabCase( "zIndex" ), 1 ); 128 | } ); 129 | 130 | } ); 131 | 132 | QUnit.test( "jQuery.cssNumber", function( assert ) { 133 | assert.expect( 1 ); 134 | 135 | assert.ok( jQuery.cssNumber, "jQuery.cssNumber exists" ); 136 | } ); 137 | 138 | QUnit.test( "An unsupported jQuery.fn.css(Object,Number) signature", function( assert ) { 139 | assert.expect( 1 ); 140 | assert.ok( ( jQuery( "
" ).css( { left: "100%" }, 300 ), "No crash" ) ); 141 | } ); 142 | -------------------------------------------------------------------------------- /src/jquery/ajax.js: -------------------------------------------------------------------------------- 1 | import { migrateWarn } from "../main.js"; 2 | 3 | // Support jQuery slim which excludes the ajax module 4 | if ( jQuery.ajax ) { 5 | 6 | var oldCallbacks = [], 7 | guid = "migrate-" + Date.now(), 8 | origJsonpCallback = jQuery.ajaxSettings.jsonpCallback, 9 | rjsonp = /(=)\?(?=&|$)|\?\?/, 10 | rquery = /\?/; 11 | 12 | jQuery.ajaxSetup( { 13 | jsonpCallback: function() { 14 | 15 | // Source is virtually the same as in Core, but we need to duplicate 16 | // to maintain a proper `oldCallbacks` reference. 17 | if ( jQuery.migrateIsPatchEnabled( "jsonp-promotion" ) ) { 18 | var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( guid++ ) ); 19 | this[ callback ] = true; 20 | return callback; 21 | } else { 22 | return origJsonpCallback.apply( this, arguments ); 23 | } 24 | } 25 | } ); 26 | 27 | // Register this prefilter before the jQuery one. Otherwise, a promoted 28 | // request is transformed into one with the script dataType, and we can't 29 | // catch it anymore. 30 | // 31 | // Code mostly from: 32 | // https://github.com/jquery/jquery/blob/fa0058af426c4e482059214c29c29f004254d9a1/src/ajax/jsonp.js#L20-L97 33 | jQuery.ajaxPrefilter( "+json", function( s, originalSettings, jqXHR ) { 34 | 35 | if ( !jQuery.migrateIsPatchEnabled( "jsonp-promotion" ) ) { 36 | return; 37 | } 38 | 39 | var callbackName, overwritten, responseContainer, 40 | jsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ? 41 | "url" : 42 | typeof s.data === "string" && 43 | ( s.contentType || "" ) 44 | .indexOf( "application/x-www-form-urlencoded" ) === 0 && 45 | rjsonp.test( s.data ) && "data" 46 | ); 47 | 48 | // Handle iff the expected data type is "jsonp" or we have a parameter to set 49 | if ( jsonProp || s.dataTypes[ 0 ] === "jsonp" ) { 50 | migrateWarn( "jsonp-promotion", "JSON-to-JSONP auto-promotion is removed" ); 51 | 52 | // Get callback name, remembering preexisting value associated with it 53 | callbackName = s.jsonpCallback = typeof s.jsonpCallback === "function" ? 54 | s.jsonpCallback() : 55 | s.jsonpCallback; 56 | 57 | // Insert callback into url or form data 58 | if ( jsonProp ) { 59 | s[ jsonProp ] = s[ jsonProp ].replace( rjsonp, "$1" + callbackName ); 60 | } else if ( s.jsonp !== false ) { 61 | s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName; 62 | } 63 | 64 | // Use data converter to retrieve json after script execution 65 | s.converters[ "script json" ] = function() { 66 | if ( !responseContainer ) { 67 | jQuery.error( callbackName + " was not called" ); 68 | } 69 | return responseContainer[ 0 ]; 70 | }; 71 | 72 | // Force json dataType 73 | s.dataTypes[ 0 ] = "json"; 74 | 75 | // Install callback 76 | overwritten = window[ callbackName ]; 77 | window[ callbackName ] = function() { 78 | responseContainer = arguments; 79 | }; 80 | 81 | // Clean-up function (fires after converters) 82 | jqXHR.always( function() { 83 | 84 | // If previous value didn't exist - remove it 85 | if ( overwritten === undefined ) { 86 | jQuery( window ).removeProp( callbackName ); 87 | 88 | // Otherwise restore preexisting value 89 | } else { 90 | window[ callbackName ] = overwritten; 91 | } 92 | 93 | // Save back as free 94 | if ( s[ callbackName ] ) { 95 | 96 | // Make sure that re-using the options doesn't screw things around 97 | s.jsonpCallback = originalSettings.jsonpCallback; 98 | 99 | // Save the callback name for future use 100 | oldCallbacks.push( callbackName ); 101 | } 102 | 103 | // Call if it was a function and we have a response 104 | if ( responseContainer && typeof overwritten === "function" ) { 105 | overwritten( responseContainer[ 0 ] ); 106 | } 107 | 108 | responseContainer = overwritten = undefined; 109 | } ); 110 | 111 | // Delegate to script 112 | return "script"; 113 | } 114 | } ); 115 | 116 | // Don't trigger the above logic by default as the JSON-to-JSONP auto-promotion 117 | // behavior is gone in jQuery 4.0 and as it has security implications, we don't 118 | // want to restore the legacy behavior by default. 119 | jQuery.migrateDisablePatches( "jsonp-promotion" ); 120 | 121 | } 122 | -------------------------------------------------------------------------------- /src/jquery/attributes.js: -------------------------------------------------------------------------------- 1 | import { migratePatchFunc, migrateWarn } from "../main.js"; 2 | 3 | var oldJQueryAttr = jQuery.attr, 4 | oldToggleClass = jQuery.fn.toggleClass, 5 | booleans = "checked|selected|async|autofocus|autoplay|controls|defer|" + 6 | "disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", 7 | rbooleans = new RegExp( "^(?:" + booleans + ")$", "i" ), 8 | 9 | // Some formerly boolean attributes gained new values with special meaning. 10 | // Skip the old boolean attr logic for those values. 11 | extraBoolAttrValues = { 12 | hidden: [ "until-found" ] 13 | }; 14 | 15 | // HTML boolean attributes have special behavior: 16 | // we consider the lowercase name to be the only valid value, so 17 | // getting (if the attribute is present) normalizes to that, as does 18 | // setting to any non-`false` value (and setting to `false` removes the attribute). 19 | // See https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#boolean-attributes 20 | jQuery.each( booleans.split( "|" ), function( _i, name ) { 21 | var origAttrHooks = jQuery.attrHooks[ name ] || {}; 22 | jQuery.attrHooks[ name ] = { 23 | get: origAttrHooks.get || function( elem ) { 24 | var attrValue; 25 | 26 | if ( jQuery.migrateIsPatchEnabled( "boolean-attributes" ) ) { 27 | attrValue = elem.getAttribute( name ); 28 | 29 | if ( attrValue !== name && attrValue != null && 30 | ( extraBoolAttrValues[ name ] || [] ) 31 | .indexOf( String( attrValue ).toLowerCase() ) === -1 32 | ) { 33 | migrateWarn( "boolean-attributes", 34 | "Boolean attribute '" + name + 35 | "' value is different from its lowercased name" ); 36 | 37 | return name.toLowerCase(); 38 | } 39 | } 40 | 41 | return null; 42 | }, 43 | 44 | set: origAttrHooks.set || function( elem, value, name ) { 45 | if ( jQuery.migrateIsPatchEnabled( "boolean-attributes" ) ) { 46 | if ( value !== name && 47 | ( extraBoolAttrValues[ name ] || [] ) 48 | .indexOf( String( value ).toLowerCase() ) === -1 49 | ) { 50 | if ( value !== false ) { 51 | migrateWarn( "boolean-attributes", 52 | "Boolean attribute '" + name + 53 | "' is not set to its lowercased name" ); 54 | } 55 | 56 | if ( value === false ) { 57 | 58 | // Remove boolean attributes when set to false 59 | jQuery.removeAttr( elem, name ); 60 | } else { 61 | elem.setAttribute( name, name ); 62 | } 63 | return name; 64 | } 65 | } 66 | } 67 | }; 68 | } ); 69 | 70 | migratePatchFunc( jQuery, "attr", function( elem, name, value ) { 71 | var nType = elem.nodeType; 72 | 73 | // Fallback to the original method on text, comment and attribute nodes 74 | // and when attributes are not supported. 75 | if ( nType === 3 || nType === 8 || nType === 2 || 76 | typeof elem.getAttribute === "undefined" ) { 77 | return oldJQueryAttr.apply( this, arguments ); 78 | } 79 | 80 | if ( value === false && name.toLowerCase().indexOf( "aria-" ) !== 0 && 81 | !rbooleans.test( name ) ) { 82 | migrateWarn( "attr-false", 83 | "Setting the non-ARIA non-boolean attribute '" + name + 84 | "' to false" ); 85 | 86 | jQuery.attr( elem, name, "false" ); 87 | return; 88 | } 89 | 90 | return oldJQueryAttr.apply( this, arguments ); 91 | }, "attr-false" ); 92 | 93 | migratePatchFunc( jQuery.fn, "toggleClass", function( state ) { 94 | 95 | // Only deprecating no-args or single boolean arg 96 | if ( state !== undefined && typeof state !== "boolean" ) { 97 | 98 | return oldToggleClass.apply( this, arguments ); 99 | } 100 | 101 | migrateWarn( "toggleClass-bool", "jQuery.fn.toggleClass( [ boolean ] ) is removed" ); 102 | 103 | // Toggle entire class name of each element 104 | return this.each( function() { 105 | var className = this.getAttribute && this.getAttribute( "class" ) || ""; 106 | 107 | if ( className ) { 108 | jQuery.data( this, "__className__", className ); 109 | } 110 | 111 | // If the element has a class name or if we're passed `false`, 112 | // then remove the whole classname (if there was one, the above saved it). 113 | // Otherwise, bring back whatever was previously saved (if anything), 114 | // falling back to the empty string if nothing was stored. 115 | if ( this.setAttribute ) { 116 | this.setAttribute( "class", 117 | className || state === false ? 118 | "" : 119 | jQuery.data( this, "__className__" ) || "" 120 | ); 121 | } 122 | } ); 123 | }, "toggleClass-bool" ); 124 | -------------------------------------------------------------------------------- /.github/workflows/browser-tests.yml: -------------------------------------------------------------------------------- 1 | name: Browser Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | env: 10 | NODE_VERSION: 22.x 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | name: ${{ matrix.NAME || matrix.BROWSER }} (${{ matrix.MIGRATE_VERSION }} Migrate) 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | NAME: [""] 20 | BROWSER: ["chrome", "firefox"] 21 | MIGRATE_VERSION: ["min"] 22 | include: 23 | - NAME: "" 24 | BROWSER: "chrome" 25 | MIGRATE_VERSION: "esmodules" 26 | - NAME: "Firefox ESR (new)" 27 | BROWSER: "firefox" 28 | MIGRATE_VERSION: "min" 29 | - NAME: "Firefox ESR (old)" 30 | BROWSER: "firefox" 31 | MIGRATE_VERSION: "min" 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 35 | 36 | - name: Use Node.js ${{ env.NODE_VERSION }} 37 | uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 38 | with: 39 | node-version: ${{ env.NODE_VERSION }} 40 | cache: npm 41 | cache-dependency-path: '**/package-lock.json' 42 | 43 | - name: Set download URL for Firefox ESR (old) 44 | run: | 45 | echo "FIREFOX_SOURCE_URL=https://download.mozilla.org/?product=firefox-esr-latest-ssl&lang=en-US&os=linux64" >> "$GITHUB_ENV" 46 | if: contains(matrix.NAME, 'Firefox ESR (old)') 47 | 48 | - name: Set download URL for Firefox ESR (new) 49 | run: | 50 | echo "FIREFOX_SOURCE_URL=https://download.mozilla.org/?product=firefox-esr-next-latest-ssl&lang=en-US&os=linux64" >> "$GITHUB_ENV" 51 | if: contains(matrix.NAME, 'Firefox ESR (new)') 52 | 53 | - name: Install Firefox ESR 54 | run: | 55 | # Support: Firefox <135 only 56 | # Older Firefox used to be compressed using bzip2, newer using xz. Try 57 | # to uncompress using xz, fall back to bzip2 if that fails. 58 | # Note: this will download the old Firefox ESR twice, but it will still work 59 | # when the current ESR version starts to use xz with no changes to the code. 60 | wget --no-verbose "$FIREFOX_SOURCE_URL" -O - | tar -Jx -C "$HOME" || \ 61 | wget --no-verbose "$FIREFOX_SOURCE_URL" -O - | tar -jx -C "$HOME" 62 | echo "PATH=${HOME}/firefox:$PATH" >> "$GITHUB_ENV" 63 | echo "FIREFOX_BIN=${HOME}/firefox/firefox" >> "$GITHUB_ENV" 64 | if: contains(matrix.NAME, 'Firefox ESR') 65 | 66 | - name: Install dependencies 67 | run: npm install 68 | 69 | - name: Run tests 70 | run: | 71 | npm run pretest 72 | npm run build:all 73 | npm run test:unit -- -c jtr-local.yml \ 74 | --headless -b ${{ matrix.BROWSER }} -f plugin=${{ matrix.MIGRATE_VERSION }} 75 | 76 | ie: 77 | runs-on: windows-latest 78 | name: Edge in IE mode (min Migrate) 79 | env: 80 | MIGRATE_VERSION: "min" 81 | steps: 82 | - name: Checkout 83 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 84 | 85 | - name: Use Node.js ${{ env.NODE_VERSION }} 86 | uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 87 | with: 88 | node-version: ${{ env.NODE_VERSION }} 89 | cache: npm 90 | cache-dependency-path: '**/package-lock.json' 91 | 92 | - name: Install dependencies 93 | run: npm install 94 | 95 | - name: Run tests 96 | shell: cmd 97 | run: npm run test:ie -- -c jtr-local.yml -f plugin=${{ env.MIGRATE_VERSION }} 98 | 99 | safari: 100 | runs-on: macos-latest 101 | name: Safari (min Migrate) 102 | env: 103 | MIGRATE_VERSION: "min" 104 | steps: 105 | - name: Checkout 106 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 107 | 108 | - name: Use Node.js ${{ env.NODE_VERSION }} 109 | uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 110 | with: 111 | node-version: ${{ env.NODE_VERSION }} 112 | cache: npm 113 | cache-dependency-path: '**/package-lock.json' 114 | 115 | - name: Install dependencies 116 | run: npm install 117 | 118 | - name: Run tests 119 | run: npm run test:safari -- -c jtr-local.yml -f plugin=${{ env.MIGRATE_VERSION }} 120 | -------------------------------------------------------------------------------- /src/jquery/core.js: -------------------------------------------------------------------------------- 1 | import { migratePatchAndWarnFunc, migratePatchAndInfoFunc } from "../main.js"; 2 | import "../disablePatches.js"; 3 | 4 | var arr = [], 5 | push = arr.push, 6 | sort = arr.sort, 7 | splice = arr.splice, 8 | class2type = {}, 9 | 10 | // Matches dashed string for camelizing 11 | rmsPrefix = /^-ms-/, 12 | rdashAlpha = /-([a-z])/g, 13 | 14 | // Require that the "whitespace run" starts from a non-whitespace 15 | // to avoid O(N^2) behavior when the engine would try matching "\s+$" at each space position. 16 | rtrim = /^[\s\uFEFF\xA0]+|([^\s\uFEFF\xA0])[\s\uFEFF\xA0]+$/g; 17 | 18 | migratePatchAndWarnFunc( jQuery, "parseJSON", function() { 19 | return JSON.parse.apply( null, arguments ); 20 | }, "parseJSON", 21 | "jQuery.parseJSON is removed; use JSON.parse" ); 22 | 23 | migratePatchAndInfoFunc( jQuery, "holdReady", jQuery.holdReady, 24 | "holdReady", "jQuery.holdReady() is deprecated" ); 25 | 26 | migratePatchAndWarnFunc( jQuery, "unique", jQuery.uniqueSort, 27 | "unique", "jQuery.unique() is removed; use jQuery.uniqueSort()" ); 28 | 29 | migratePatchAndWarnFunc( jQuery, "trim", function( text ) { 30 | return text == null ? 31 | "" : 32 | ( text + "" ).replace( rtrim, "$1" ); 33 | }, "trim", 34 | "jQuery.trim() is removed; use String.prototype.trim" ); 35 | 36 | migratePatchAndWarnFunc( jQuery, "nodeName", function( elem, name ) { 37 | return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); 38 | }, "nodeName", 39 | "jQuery.nodeName() is removed" ); 40 | 41 | migratePatchAndWarnFunc( jQuery, "isArray", Array.isArray, "isArray", 42 | "jQuery.isArray() is removed; use Array.isArray()" 43 | ); 44 | 45 | migratePatchAndWarnFunc( jQuery, "isNumeric", 46 | function( obj ) { 47 | 48 | // As of jQuery 3.0, isNumeric is limited to 49 | // strings and numbers (primitives or objects) 50 | // that can be coerced to finite numbers (gh-2662) 51 | var type = typeof obj; 52 | return ( type === "number" || type === "string" ) && 53 | 54 | // parseFloat NaNs numeric-cast false positives ("") 55 | // ...but misinterprets leading-number strings, e.g. hex literals ("0x...") 56 | // subtraction forces infinities to NaN 57 | !isNaN( obj - parseFloat( obj ) ); 58 | }, "isNumeric", 59 | "jQuery.isNumeric() is removed" 60 | ); 61 | 62 | // Populate the class2type map 63 | jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol". 64 | split( " " ), 65 | function( _, name ) { 66 | class2type[ "[object " + name + "]" ] = name.toLowerCase(); 67 | } ); 68 | 69 | migratePatchAndWarnFunc( jQuery, "type", function( obj ) { 70 | if ( obj == null ) { 71 | return obj + ""; 72 | } 73 | 74 | return typeof obj === "object" ? 75 | class2type[ Object.prototype.toString.call( obj ) ] || "object" : 76 | typeof obj; 77 | }, "type", 78 | "jQuery.type() is removed" ); 79 | 80 | migratePatchAndWarnFunc( jQuery, "isFunction", function( obj ) { 81 | return typeof obj === "function"; 82 | }, "isFunction", 83 | "jQuery.isFunction() is removed" ); 84 | 85 | migratePatchAndWarnFunc( jQuery, "isWindow", 86 | function( obj ) { 87 | return obj != null && obj === obj.window; 88 | }, "isWindow", 89 | "jQuery.isWindow() is removed" 90 | ); 91 | 92 | migratePatchAndWarnFunc( jQuery, "now", Date.now, "now", 93 | "jQuery.now() is removed; use Date.now()" 94 | ); 95 | 96 | // Used by camelCase as callback to replace() 97 | function fcamelCase( _all, letter ) { 98 | return letter.toUpperCase(); 99 | } 100 | 101 | migratePatchAndWarnFunc( jQuery, "camelCase", 102 | function( string ) { 103 | 104 | // Convert dashed to camelCase; used by the css and data modules 105 | // Support: IE <=9 - 11, Edge 12 - 15 106 | // Microsoft forgot to hump their vendor prefix (trac-9572) 107 | return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); 108 | }, "camelCase", 109 | "jQuery.camelCase() is removed" 110 | ); 111 | 112 | // Bind a function to a context, optionally partially applying any 113 | // arguments. 114 | // jQuery.proxy is deprecated to promote standards (specifically Function#bind) 115 | // However, it is not slated for removal any time soon 116 | migratePatchAndInfoFunc( jQuery, "proxy", jQuery.proxy, 117 | "proxy", "jQuery.proxy() is deprecated" ); 118 | 119 | migratePatchAndWarnFunc( jQuery.fn, "push", push, "push", 120 | "jQuery.fn.push() is removed; use .add() or convert to an array" ); 121 | migratePatchAndWarnFunc( jQuery.fn, "sort", sort, "sort", 122 | "jQuery.fn.sort() is removed; convert to an array before sorting" ); 123 | migratePatchAndWarnFunc( jQuery.fn, "splice", splice, "splice", 124 | "jQuery.fn.splice() is removed; use .slice() or .not() with .eq()" ); 125 | -------------------------------------------------------------------------------- /src/jquery/css.js: -------------------------------------------------------------------------------- 1 | import { migrateWarn, migratePatchFunc } from "../main.js"; 2 | import { camelCase } from "../utils.js"; 3 | 4 | var origFnCss, internalCssNumber, 5 | ralphaStart = /^[a-z]/, 6 | 7 | // The regex visualized: 8 | // 9 | // /----------\ 10 | // | | /-------\ 11 | // | / Top \ | | | 12 | // /--- Border ---+-| Right |-+---+- Width -+---\ 13 | // | | Bottom | | 14 | // | \ Left / | 15 | // | | 16 | // | /----------\ | 17 | // | /-------------\ | | |- END 18 | // | | | | / Top \ | | 19 | // | | / Margin \ | | | Right | | | 20 | // |---------+-| |-+---+-| Bottom |-+----| 21 | // | \ Padding / \ Left / | 22 | // BEGIN -| | 23 | // | /---------\ | 24 | // | | | | 25 | // | | / Min \ | / Width \ | 26 | // \--------------+-| |-+---| |---/ 27 | // \ Max / \ Height / 28 | rautoPx = /^(?:Border(?:Top|Right|Bottom|Left)?(?:Width|)|(?:Margin|Padding)?(?:Top|Right|Bottom|Left)?|(?:Min|Max)?(?:Width|Height))$/; 29 | 30 | if ( typeof Proxy !== "undefined" ) { 31 | jQuery.cssProps = new Proxy( jQuery.cssProps || {}, { 32 | set: function() { 33 | migrateWarn( "cssProps", "jQuery.cssProps is removed" ); 34 | return Reflect.set.apply( this, arguments ); 35 | } 36 | } ); 37 | } 38 | 39 | // `jQuery.cssNumber` is missing in jQuery >=4; fill it with the latest 3.x version: 40 | // https://github.com/jquery/jquery/blob/3.7.1/src/css.js#L216-L246 41 | // This way, number values for the CSS properties below won't start triggering 42 | // Migrate warnings when jQuery gets updated to >=4.0.0 (gh-438). 43 | // 44 | // We need to keep this as a local variable as we need it internally 45 | // in a `jQuery.fn.css` patch and this usage shouldn't warn. 46 | internalCssNumber = { 47 | animationIterationCount: true, 48 | aspectRatio: true, 49 | borderImageSlice: true, 50 | columnCount: true, 51 | flexGrow: true, 52 | flexShrink: true, 53 | fontWeight: true, 54 | gridArea: true, 55 | gridColumn: true, 56 | gridColumnEnd: true, 57 | gridColumnStart: true, 58 | gridRow: true, 59 | gridRowEnd: true, 60 | gridRowStart: true, 61 | lineHeight: true, 62 | opacity: true, 63 | order: true, 64 | orphans: true, 65 | scale: true, 66 | widows: true, 67 | zIndex: true, 68 | zoom: true, 69 | 70 | // SVG-related 71 | fillOpacity: true, 72 | floodOpacity: true, 73 | stopOpacity: true, 74 | strokeMiterlimit: true, 75 | strokeOpacity: true 76 | }; 77 | 78 | if ( typeof Proxy !== "undefined" ) { 79 | jQuery.cssNumber = new Proxy( internalCssNumber, { 80 | get: function() { 81 | migrateWarn( "css-number", "jQuery.cssNumber is deprecated" ); 82 | return Reflect.get.apply( this, arguments ); 83 | }, 84 | set: function() { 85 | migrateWarn( "css-number", "jQuery.cssNumber is deprecated" ); 86 | return Reflect.set.apply( this, arguments ); 87 | } 88 | } ); 89 | } else { 90 | 91 | // Support: IE 9-11+ 92 | // IE doesn't support proxies, but we still want to restore the legacy 93 | // jQuery.cssNumber there. 94 | jQuery.cssNumber = internalCssNumber; 95 | } 96 | 97 | function isAutoPx( prop ) { 98 | 99 | // The first test is used to ensure that: 100 | // 1. The prop starts with a lowercase letter (as we uppercase it for the second regex). 101 | // 2. The prop is not empty. 102 | return ralphaStart.test( prop ) && 103 | rautoPx.test( prop[ 0 ].toUpperCase() + prop.slice( 1 ) ); 104 | } 105 | 106 | origFnCss = jQuery.fn.css; 107 | 108 | migratePatchFunc( jQuery.fn, "css", function( name, value ) { 109 | var camelName, 110 | origThis = this; 111 | 112 | if ( name && typeof name === "object" && !Array.isArray( name ) ) { 113 | jQuery.each( name, function( n, v ) { 114 | jQuery.fn.css.call( origThis, n, v ); 115 | } ); 116 | return this; 117 | } 118 | 119 | if ( typeof value === "number" ) { 120 | camelName = camelCase( name ); 121 | 122 | // Use `internalCssNumber` to avoid triggering our warnings in this 123 | // internal check. 124 | if ( !isAutoPx( camelName ) && !internalCssNumber[ camelName ] ) { 125 | migrateWarn( "css-number", 126 | "Auto-appending 'px' to number-typed values " + 127 | "for jQuery.fn.css( \"" + name + "\", value ) is removed" ); 128 | } 129 | } 130 | 131 | return origFnCss.apply( this, arguments ); 132 | }, "css-number" ); 133 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { jQueryVersionSince } from "./compareVersions.js"; 2 | import "./disablePatches.js"; 3 | 4 | ( function() { 5 | 6 | // Need jQuery 4.x and no older Migrate loaded 7 | if ( !jQuery || !jQueryVersionSince( "4.0.0" ) || 8 | jQueryVersionSince( "5.0.0" ) ) { 9 | window.console.log( "JQMIGRATE: jQuery 4.x REQUIRED" ); 10 | } 11 | if ( jQuery.migrateMessages ) { 12 | window.console.log( "JQMIGRATE: Migrate plugin loaded multiple times" ); 13 | } 14 | 15 | // Show a message on the console so devs know we're active 16 | window.console.log( "JQMIGRATE: Migrate is installed" + 17 | ( jQuery.migrateMute ? "" : " with logging active" ) + 18 | ", version " + jQuery.migrateVersion ); 19 | 20 | } )(); 21 | 22 | var messagesLogged = Object.create( null ); 23 | 24 | // List of warnings already given; public read only 25 | jQuery.migrateMessages = []; 26 | 27 | // By default, each warning is only reported once. 28 | if ( jQuery.migrateDeduplicateMessages === undefined ) { 29 | jQuery.migrateDeduplicateMessages = true; 30 | } 31 | 32 | // Set to `false` to disable traces that appear with warnings 33 | if ( jQuery.migrateTrace === undefined ) { 34 | jQuery.migrateTrace = true; 35 | } 36 | 37 | // Forget any warnings we've already given; public 38 | jQuery.migrateReset = function() { 39 | messagesLogged = Object.create( null ); 40 | jQuery.migrateMessages.length = 0; 41 | }; 42 | 43 | function migrateMessageInternal( code, msg, consoleMethod ) { 44 | var console = window.console; 45 | 46 | if ( jQuery.migrateIsPatchEnabled( code ) && 47 | ( !jQuery.migrateDeduplicateMessages || !messagesLogged[ msg ] ) ) { 48 | messagesLogged[ msg ] = true; 49 | jQuery.migrateMessages.push( consoleMethod.toUpperCase() + ": " + 50 | msg + " [" + code + "]" ); 51 | 52 | if ( console[ consoleMethod ] && !jQuery.migrateMute ) { 53 | console[ consoleMethod ]( "JQMIGRATE: " + msg ); 54 | 55 | if ( jQuery.migrateTrace ) { 56 | 57 | // Label the trace so that filtering messages in DevTools 58 | // doesn't hide traces. Note that IE ignores the label. 59 | console.trace( "JQMIGRATE: " + msg ); 60 | } 61 | } 62 | } 63 | } 64 | 65 | export function migrateWarn( code, msg ) { 66 | migrateMessageInternal( code, msg, "warn" ); 67 | } 68 | 69 | export function migrateInfo( code, msg ) { 70 | migrateMessageInternal( code, msg, "info" ); 71 | } 72 | 73 | function migratePatchPropInternal( 74 | obj, prop, value, code, msg, migrateMessageFn 75 | ) { 76 | var orig = obj[ prop ]; 77 | Object.defineProperty( obj, prop, { 78 | configurable: true, 79 | enumerable: true, 80 | 81 | get: function() { 82 | if ( jQuery.migrateIsPatchEnabled( code ) ) { 83 | 84 | // If `msg` not provided, do not message; more sophisticated 85 | // messaging logic is most likely embedded in `value`. 86 | if ( msg ) { 87 | migrateMessageFn( code, msg ); 88 | } 89 | 90 | return value; 91 | } 92 | 93 | return orig; 94 | }, 95 | 96 | set: function( newValue ) { 97 | if ( jQuery.migrateIsPatchEnabled( code ) ) { 98 | 99 | // See the comment in the getter. 100 | if ( msg ) { 101 | migrateMessageFn( code, msg ); 102 | } 103 | } 104 | 105 | // If a new value was set, apply it regardless if 106 | // the patch is later disabled or not. 107 | orig = value = newValue; 108 | } 109 | } ); 110 | } 111 | 112 | export function migrateWarnProp( obj, prop, value, code, msg ) { 113 | if ( !msg ) { 114 | throw new Error( "No warning message provided" ); 115 | } 116 | migratePatchPropInternal( obj, prop, value, code, msg, migrateWarn ); 117 | } 118 | 119 | export function migrateInfoProp( obj, prop, value, code, msg ) { 120 | if ( !msg ) { 121 | throw new Error( "No warning message provided" ); 122 | } 123 | migratePatchPropInternal( obj, prop, value, code, msg, migrateInfo ); 124 | } 125 | 126 | export function migratePatchProp( obj, prop, value, code ) { 127 | migratePatchPropInternal( obj, prop, value, code ); 128 | } 129 | 130 | // The value of the "Func" APIs is that for method we want to allow 131 | // checking for the method existence without triggering a warning. 132 | // For other deprecated properties, we do need to warn on access. 133 | function migratePatchFuncInternal( 134 | obj, prop, newFunc, code, msg, migrateMessageFn 135 | ) { 136 | 137 | function wrappedNewFunc() { 138 | 139 | // If `msg` not provided, do not warn; more sophisticated warnings 140 | // logic is most likely embedded in `newFunc`, in that case here 141 | // we just care about the logic choosing the proper implementation 142 | // based on whether the patch is disabled or not. 143 | if ( msg ) { 144 | migrateMessageFn( code, msg ); 145 | } 146 | 147 | return newFunc.apply( this, arguments ); 148 | } 149 | 150 | migratePatchPropInternal( obj, prop, wrappedNewFunc, code ); 151 | } 152 | 153 | export function migratePatchAndWarnFunc( obj, prop, newFunc, code, msg ) { 154 | if ( !msg ) { 155 | throw new Error( "No warning message provided" ); 156 | } 157 | return migratePatchFuncInternal( obj, prop, newFunc, code, msg, migrateWarn ); 158 | } 159 | 160 | export function migratePatchAndInfoFunc( obj, prop, newFunc, code, msg ) { 161 | if ( !msg ) { 162 | throw new Error( "No info message provided" ); 163 | } 164 | return migratePatchFuncInternal( obj, prop, newFunc, code, msg, migrateInfo ); 165 | } 166 | 167 | export function migratePatchFunc( obj, prop, newFunc, code ) { 168 | return migratePatchFuncInternal( obj, prop, newFunc, code ); 169 | } 170 | 171 | if ( window.document.compatMode === "BackCompat" ) { 172 | 173 | // jQuery has never supported or tested Quirks Mode 174 | migrateWarn( "quirks", "jQuery is not compatible with Quirks Mode" ); 175 | } 176 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import jqueryConfig from "eslint-config-jquery"; 2 | import importPlugin from "eslint-plugin-import"; 3 | import globals from "globals"; 4 | 5 | export default [ 6 | { 7 | 8 | // Only global ignores will bypass the parser 9 | // and avoid JS parsing errors 10 | // See https://github.com/eslint/eslint/discussions/17412 11 | ignores: [ "external", "null.json", "simple.json" ] 12 | }, 13 | 14 | { 15 | files: [ 16 | "eslint.config.js", 17 | ".release-it.cjs", 18 | "build/**", 19 | "test/node_smoke_tests/**", 20 | "test/bundler_smoke_tests/**/*" 21 | ], 22 | languageOptions: { 23 | ecmaVersion: "latest", 24 | globals: { 25 | ...globals.node 26 | } 27 | }, 28 | rules: jqueryConfig.rules 29 | }, 30 | 31 | { 32 | files: [ "src/**" ], 33 | plugins: { 34 | import: importPlugin 35 | }, 36 | languageOptions: { 37 | ecmaVersion: 2015, 38 | 39 | // The browser env is not enabled on purpose so that code takes 40 | // all browser-only globals from window instead of assuming 41 | // they're available as globals. This makes it possible to use 42 | // jQuery with tools like jsdom which provide a custom window 43 | // implementation. 44 | globals: { 45 | jQuery: false, 46 | window: false 47 | } 48 | }, 49 | rules: { 50 | ...jqueryConfig.rules, 51 | "import/extensions": [ "error", "always" ], 52 | "import/no-cycle": "error", 53 | indent: [ 54 | "error", 55 | "tab", 56 | { 57 | outerIIFEBody: 0, 58 | 59 | // This makes it so code within if statements checking 60 | // for jQuery features is not indented. 61 | ignoredNodes: [ "Program > IfStatement > *" ] 62 | } 63 | ], 64 | "one-var": [ "error", { var: "always" } ], 65 | strict: [ "error", "function" ] 66 | } 67 | }, 68 | 69 | { 70 | files: [ 71 | "src/wrapper.js", 72 | "src/wrapper-esm.js", 73 | "src/wrapper-factory.js", 74 | "src/wrapper-factory-esm.js" 75 | ], 76 | languageOptions: { 77 | globals: { 78 | jQuery: false 79 | } 80 | }, 81 | rules: { 82 | "no-unused-vars": "off", 83 | indent: [ 84 | "error", 85 | "tab", 86 | { 87 | 88 | // This makes it so code within the wrapper is not indented. 89 | ignoredNodes: [ 90 | "Program > FunctionDeclaration > *" 91 | ] 92 | } 93 | ] 94 | } 95 | }, 96 | 97 | { 98 | files: [ "src/wrapper.js" ], 99 | languageOptions: { 100 | sourceType: "script", 101 | globals: { 102 | define: false, 103 | module: false 104 | } 105 | }, 106 | rules: { 107 | indent: [ 108 | "error", 109 | "tab", 110 | { 111 | 112 | // This makes it so code within the wrapper is not indented. 113 | ignoredNodes: [ 114 | "Program > ExpressionStatement > CallExpression > :last-child > *" 115 | ] 116 | } 117 | ] 118 | } 119 | }, 120 | 121 | { 122 | files: [ "test/unit/**" ], 123 | languageOptions: { 124 | ecmaVersion: 5, 125 | sourceType: "script", 126 | globals: { 127 | ...globals.browser, 128 | Promise: false, 129 | Symbol: false, 130 | jQuery: false, 131 | QUnit: false, 132 | sinon: false, 133 | url: false, 134 | expectMessage: false, 135 | expectNoMessage: false, 136 | compareVersions: false, 137 | jQueryVersionSince: false, 138 | startIframeTest: false, 139 | TestManager: false 140 | } 141 | }, 142 | rules: { 143 | ...jqueryConfig.rules, 144 | "no-unused-vars": [ 145 | "error", 146 | { args: "after-used", argsIgnorePattern: "^_" } 147 | ] 148 | } 149 | }, 150 | 151 | { 152 | files: [ "test/data/**" ], 153 | ignores: [ "test/data/jquery-*.js", "test/data/qunit-start.js" ], 154 | languageOptions: { 155 | ecmaVersion: 5, 156 | sourceType: "script", 157 | globals: { 158 | ...globals.browser, 159 | Promise: false, 160 | Symbol: false, 161 | global: false, 162 | jQuery: false, 163 | QUnit: false, 164 | url: true, 165 | compareVersions: true, 166 | jQueryVersionSince: false, 167 | expectMessage: true, 168 | expectNoMessage: true, 169 | startIframeTest: true, 170 | TestManager: true 171 | } 172 | }, 173 | rules: { 174 | ...jqueryConfig.rules, 175 | strict: [ "error", "global" ] 176 | } 177 | }, 178 | 179 | { 180 | files: [ "test/runner/**" ], 181 | languageOptions: { 182 | ecmaVersion: "latest", 183 | globals: { 184 | ...globals.node 185 | }, 186 | sourceType: "module" 187 | }, 188 | rules: { 189 | ...jqueryConfig.rules 190 | } 191 | }, 192 | 193 | { 194 | files: [ "test/runner/listeners.js" ], 195 | languageOptions: { 196 | ecmaVersion: 5, 197 | globals: { 198 | ...globals.browser, 199 | QUnit: false, 200 | Symbol: false 201 | }, 202 | sourceType: "script" 203 | }, 204 | rules: { 205 | ...jqueryConfig.rules, 206 | strict: [ "error", "function" ] 207 | } 208 | }, 209 | 210 | { 211 | files: [ "dist/jquery-migrate.js" ], 212 | languageOptions: { 213 | globals: { 214 | define: false, 215 | jQuery: false, 216 | module: false, 217 | Proxy: false, 218 | Reflect: false, 219 | window: false 220 | } 221 | }, 222 | rules: { 223 | ...jqueryConfig.rules, 224 | strict: [ "error", "function" ], 225 | 226 | // These are fine for the built version 227 | "no-multiple-empty-lines": "off", 228 | "one-var": "off" 229 | } 230 | }, 231 | 232 | { 233 | files: [ "dist/**" ], 234 | languageOptions: { 235 | ecmaVersion: 5, 236 | sourceType: "script" 237 | } 238 | }, 239 | 240 | { 241 | files: [ "dist-module/**" ], 242 | languageOptions: { 243 | ecmaVersion: 2015, 244 | sourceType: "module" 245 | } 246 | }, 247 | 248 | { 249 | files: [ "dist/wrappers/*.js" ], 250 | languageOptions: { 251 | ecmaVersion: 2015, 252 | sourceType: "commonjs" 253 | } 254 | } 255 | ]; 256 | -------------------------------------------------------------------------------- /test/unit/jquery/ajax.js: -------------------------------------------------------------------------------- 1 | // Support jQuery slim which excludes the ajax module 2 | if ( jQuery.ajax ) { 3 | 4 | QUnit.module( "ajax" ); 5 | 6 | [ " - Same Domain", " - Cross Domain" ].forEach( function( label, crossDomain ) { 7 | function runTests( options ) { 8 | var forceEnablePatch = ( options || {} ).forceEnablePatch || false; 9 | 10 | QUnit.test( "jQuery.ajax() JSON-to-JSONP auto-promotion" + label + ( 11 | forceEnablePatch ? ", patch force-enabled" : "" 12 | ), function( assert ) { 13 | 14 | assert.expect( 10 ); 15 | 16 | if ( forceEnablePatch ) { 17 | jQuery.migrateEnablePatches( "jsonp-promotion" ); 18 | } 19 | 20 | var done = assert.async(), 21 | tests = [ 22 | function() { 23 | var testName = "dataType: \"json\""; 24 | return expectNoMessage( assert, testName, function() { 25 | return jQuery.ajax( { 26 | url: url( "null.json" ), 27 | context: { testName: testName }, 28 | crossDomain: crossDomain, 29 | dataType: "json", 30 | jsonpCallback: "customJsonpCallback" 31 | } ).then( function() { 32 | assert.ok( true, this.testName + " (success)" ); 33 | } ).catch( function() { 34 | assert.ok( false, this.testName + " (failure)" ); 35 | } ); 36 | } ); 37 | }, 38 | 39 | function() { 40 | var testName = "dataType: \"json\", URL callback"; 41 | return expectMessage( assert, testName, forceEnablePatch ? 1 : 0, 42 | function() { 43 | return jQuery.ajax( { 44 | url: url( "jsonpScript.js?callback=?" ), 45 | context: { testName: testName }, 46 | crossDomain: crossDomain, 47 | dataType: "json", 48 | jsonpCallback: "customJsonpCallback" 49 | } ).then( function() { 50 | assert.ok( forceEnablePatch, this.testName + " (success)" ); 51 | } ).catch( function() { 52 | assert.ok( !forceEnablePatch, this.testName + " (failure)" ); 53 | } ); 54 | } ); 55 | }, 56 | 57 | function() { 58 | var testName = "dataType: \"json\", data callback"; 59 | return expectMessage( assert, testName, forceEnablePatch ? 1 : 0, 60 | function() { 61 | return jQuery.ajax( { 62 | url: url( "jsonpScript.js" ), 63 | context: { testName: testName }, 64 | crossDomain: crossDomain, 65 | data: "callback=?", 66 | dataType: "json", 67 | jsonpCallback: "customJsonpCallback" 68 | } ).then( function() { 69 | assert.ok( forceEnablePatch, this.testName + " (success)" ); 70 | } ).catch( function() { 71 | assert.ok( !forceEnablePatch, this.testName + " (failure)" ); 72 | } ); 73 | } ); 74 | }, 75 | 76 | function() { 77 | var testName = "dataType: \"jsonp\", URL callback"; 78 | return expectNoMessage( assert, testName, function() { 79 | return jQuery.ajax( { 80 | url: url( "jsonpScript.js?callback=?" ), 81 | context: { testName: testName }, 82 | crossDomain: crossDomain, 83 | dataType: "jsonp", 84 | jsonpCallback: "customJsonpCallback" 85 | } ).then( function() { 86 | assert.ok( true, this.testName + " (success)" ); 87 | } ).catch( function() { 88 | assert.ok( false, this.testName + " (failure)" ); 89 | } ); 90 | } ); 91 | }, 92 | 93 | function() { 94 | var testName = "dataType: \"jsonp\", data callback"; 95 | return expectNoMessage( assert, testName, function() { 96 | return jQuery.ajax( { 97 | url: url( "jsonpScript.js" ), 98 | context: { testName: testName }, 99 | crossDomain: crossDomain, 100 | data: "callback=?", 101 | dataType: "jsonp", 102 | jsonpCallback: "customJsonpCallback" 103 | } ).then( function() { 104 | assert.ok( true, this.testName + " (success)" ); 105 | } ).catch( function() { 106 | assert.ok( false, this.testName + " (failure)" ); 107 | } ); 108 | } ); 109 | } 110 | ]; 111 | 112 | // Invoke tests sequentially as they're async and early tests could get warnings 113 | // from later ones. 114 | function run( tests ) { 115 | var test = tests[ 0 ]; 116 | return test().then( function() { 117 | if ( tests.length > 1 ) { 118 | return run( tests.slice( 1 ) ); 119 | } 120 | } ); 121 | } 122 | 123 | run( tests ) 124 | .then( function() { 125 | done(); 126 | } ); 127 | } ); 128 | } 129 | 130 | // In jQuery 4+, this behavior is disabled by default for security 131 | // reasons, re-enable for this test, but test default behavior as well. 132 | runTests( { forceEnablePatch: true } ); 133 | runTests( { forceEnablePatch: false } ); 134 | } ); 135 | 136 | TestManager.runIframeTest( 137 | "Re-use JSONP callback name (jQuery trac-8205)", 138 | "ajax-jsonp-callback-name.html", 139 | function( assert, jQuery, window, document, log, 140 | thisCallbackInWindow1, thisCallbackInWindow2, 141 | previousJsonpCallback, previousCallback, 142 | nextJsonpCallback, requestSucceeded, error ) { 143 | assert.expect( 7 ); 144 | 145 | assert.ok( requestSucceeded, 146 | "JSONP shouldn't fail" + 147 | ( error ? "; error: " + ( error && error.message || error ) : "" ) ); 148 | assert.ok( !!previousCallback, 149 | "Previous `callback` passed from the iframe" ); 150 | assert.ok( 151 | !!nextJsonpCallback, 152 | "Next `jsonpCallback` passed from the iframe" 153 | ); 154 | 155 | assert.ok( thisCallbackInWindow1, 156 | "JSONP callback name is in the window" ); 157 | assert.strictEqual( 158 | previousJsonpCallback, 159 | undefined, 160 | "jsonpCallback option is set back to default in callbacks" 161 | ); 162 | assert.ok( 163 | !thisCallbackInWindow2, 164 | "JSONP callback name was removed from the window" 165 | ); 166 | assert.strictEqual( 167 | nextJsonpCallback, 168 | previousCallback, 169 | "JSONP callback name is re-used" 170 | ); 171 | } ); 172 | 173 | } 174 | -------------------------------------------------------------------------------- /test/unit/migrate.js: -------------------------------------------------------------------------------- 1 | 2 | QUnit.module( "migrate" ); 3 | 4 | QUnit.test( "jQuery.migrateVersion", function( assert ) { 5 | assert.expect( 1 ); 6 | 7 | assert.ok( /^\d+\.\d+\.[\w\-]+/.test( jQuery.migrateVersion ), "Version property" ); 8 | } ); 9 | 10 | QUnit.test( "compareVersions and jQueryVersionSince", function( assert ) { 11 | assert.expect( 9 ); 12 | 13 | assert.equal( compareVersions( "3.0.1", "3.0.0" ), 1, "greater than 1" ); 14 | assert.equal( compareVersions( "3.0.1", "2.10.0" ), 1, "greater than 2" ); 15 | assert.equal( compareVersions( "3.2.1", "3.3.0" ), -1, "less than 1" ); 16 | assert.equal( compareVersions( "3.2.1", "4.1.3" ), -1, "less than 2" ); 17 | assert.equal( compareVersions( "3.2.2", "3.11.1" ), -1, "less than 3" ); 18 | assert.equal( compareVersions( "3.4.1", "3.4.1" ), 0, "equal" ); 19 | 20 | 21 | // Test against live jQuery version with suitably generous comparisons 22 | assert.equal( jQueryVersionSince( "1.4.2" ), true, "since - past version" ); 23 | assert.equal( jQueryVersionSince( "8.0.3" ), false, "since - future version" ); 24 | assert.equal( jQueryVersionSince( jQuery.fn.jquery ), true, "since - equal" ); 25 | } ); 26 | 27 | QUnit.test( "jQuery.migrateDeduplicateMessages", function( assert ) { 28 | assert.expect( 3 ); 29 | 30 | var origValue = jQuery.migrateDeduplicateMessages; 31 | assert.strictEqual( origValue, true, "true by default" ); 32 | 33 | jQuery.migrateDeduplicateMessages = true; 34 | expectMessage( assert, "jQuery.migrateDeduplicateMessages === true", 1, function() { 35 | jQuery.trim( " a " ); 36 | jQuery.trim( "a" ); 37 | } ); 38 | 39 | jQuery.migrateDeduplicateMessages = false; 40 | expectMessage( assert, "jQuery.migrateDeduplicateMessages === false", 2, function() { 41 | jQuery.trim( " a " ); 42 | jQuery.trim( "a" ); 43 | } ); 44 | 45 | jQuery.migrateDeduplicateMessages = origValue; 46 | } ); 47 | 48 | QUnit.test( "disabling/enabling patches", function( assert ) { 49 | assert.expect( 32 ); 50 | 51 | var elem = jQuery( "
" ); 52 | 53 | elem.appendTo( "#qunit-fixture" ); 54 | 55 | // We can't register new warnings in tests so we need to use 56 | // existing warnings. If the ones we rely on here get removed, 57 | // replace them with ones that still exist. 58 | 59 | // A few APIs that are not slated for removal to make these tests more stable: 60 | assert.strictEqual( jQuery.migrateIsPatchEnabled( "pre-on-methods" ), 61 | true, "patch enabled by default (pre-on-methods)" ); 62 | assert.strictEqual( jQuery.migrateIsPatchEnabled( "proxy" ), 63 | true, "patch enabled by default (proxy)" ); 64 | assert.strictEqual( jQuery.migrateIsPatchEnabled( "shorthand-deprecated-v3" ), 65 | true, "patch enabled by default (shorthand-deprecated-v3)" ); 66 | 67 | // APIs patched via `migratePatchAndWarnFunc` or `migratePatchAndInfoFunc`; 68 | // we're testing that: 69 | // * they don't warn on access but only when called 70 | // * they don't exist (access evaluates to `undefined`) if patch is disabled 71 | assert.strictEqual( jQuery.migrateIsPatchEnabled( "push" ), 72 | true, "patch enabled by default (push)" ); 73 | assert.strictEqual( jQuery.migrateIsPatchEnabled( "isArray" ), 74 | true, "patch enabled by default (isArray)" ); 75 | 76 | // APIs patched via `migrateWarnProp` or `migrateInfoProp`; we're testing that: 77 | // * they don't exist (access evaluates to `undefined`) if patch is disabled 78 | assert.strictEqual( jQuery.migrateIsPatchEnabled( "event-global" ), 79 | true, "patch enabled by default (event-global)" ); 80 | 81 | expectMessage( assert, "pre-on-methods (default)", function() { 82 | jQuery().bind(); 83 | } ); 84 | expectMessage( assert, "proxy (default)", function() { 85 | jQuery.proxy( jQuery.noop ); 86 | } ); 87 | expectMessage( assert, "shorthand-deprecated-v3 (default)", function() { 88 | jQuery().click(); 89 | } ); 90 | expectMessage( assert, "push (default)", function() { 91 | jQuery().push(); 92 | } ); 93 | expectMessage( assert, "isArray (default)", function() { 94 | jQuery.isArray(); 95 | } ); 96 | expectMessage( assert, "event-global (default)", function() { 97 | 98 | // eslint-disable-next-line no-unused-expressions 99 | jQuery.event.global; 100 | } ); 101 | 102 | expectNoMessage( assert, "push access without calling (default)", function() { 103 | assert.strictEqual( typeof jQuery().push, "function", 104 | "access check doesn't trigger a message (push)" ); 105 | } ); 106 | expectNoMessage( assert, "isArray access without calling (default)", function() { 107 | assert.strictEqual( typeof jQuery.isArray, "function", 108 | "access check doesn't trigger a message (isArray)" ); 109 | } ); 110 | 111 | jQuery.migrateDisablePatches( "pre-on-methods", "proxy", "push", "isArray", "event-global" ); 112 | assert.strictEqual( jQuery.migrateIsPatchEnabled( "pre-on-methods" ), 113 | false, "patch disabled (pre-on-methods)" ); 114 | assert.strictEqual( jQuery.migrateIsPatchEnabled( "proxy" ), 115 | false, "patch disabled (proxy)" ); 116 | assert.strictEqual( jQuery.migrateIsPatchEnabled( "shorthand-deprecated-v3" ), 117 | true, "patch still enabled (shorthand-deprecated-v3)" ); 118 | assert.strictEqual( jQuery.migrateIsPatchEnabled( "push" ), 119 | false, "patch disabled (push)" ); 120 | assert.strictEqual( jQuery.migrateIsPatchEnabled( "event-global" ), 121 | false, "patch disabled (event-global)" ); 122 | 123 | expectNoMessage( assert, "pre-on-methods (disabled)", function() { 124 | jQuery().bind(); 125 | } ); 126 | expectNoMessage( assert, "proxy (disabled)", function() { 127 | jQuery.proxy( jQuery.noop ); 128 | } ); 129 | expectMessage( assert, "shorthand-deprecated-v3 (not disabled)", function() { 130 | jQuery().click(); 131 | } ); 132 | expectNoMessage( assert, "push (disabled)", function() { 133 | assert.strictEqual( jQuery().push, undefined, "`jQuery.fn.push` no longer defined" ); 134 | } ); 135 | expectNoMessage( assert, "isArray (disabled)", function() { 136 | assert.strictEqual( jQuery.isArray, undefined, "`jQuery.isArray` no longer defined" ); 137 | } ); 138 | expectNoMessage( assert, "event-global (disabled)", function() { 139 | assert.strictEqual( jQuery.event.global, undefined, 140 | "`jQuery.event.global` no longer defined" ); 141 | } ); 142 | 143 | jQuery.migrateDisablePatches( "shorthand-deprecated-v3" ); 144 | assert.strictEqual( jQuery.migrateIsPatchEnabled( "shorthand-deprecated-v3" ), 145 | false, "patch disabled (shorthand-deprecated-v3)" ); 146 | 147 | expectNoMessage( assert, "shorthand-deprecated-v3 (disabled)", function() { 148 | jQuery().click(); 149 | } ); 150 | } ); 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![CI Status](https://github.com/jquery/jquery-migrate/actions/workflows/node.js.yml/badge.svg?branch=main) 2 | 3 | #### NOTE: To upgrade to jQuery 4.x, you first need version 3.x. If you're using an older version, first upgrade to jQuery 3.x using [jQuery Migrate 3.x](https://github.com/jquery/jquery-migrate/tree/3.x-stable#readme), to resolve any compatibility issues. For more information about the changes made in jQuery 4.0, see the [upgrade guide](https://jquery.com/upgrade-guide/4.0/) and [blog post](https://blog.jquery.com/2025/08/11/jquery-4-0-0-release-candidate-1/). 4 | 5 | # jQuery Migrate 6 | Upgrading libraries such as jQuery can be a lot of work, when breaking changes have been introduced. jQuery Migrate makes this easier, by restoring the APIs that were removed, and additionally shows warnings in the browser console (development version of jQuery Migrate only) when removed and/or deprecated APIs are used. 7 | 8 | That way you can spot and fix what otherwise would have been errors, until you no longer need jQuery Migrate and can remove it. 9 | 10 | ## Version compatibility 11 | 12 | The following table indicates which jQuery Migrate versions can be used with which jQuery versions: 13 | 14 | | jQuery version | jQuery Migrate version | 15 | |----------------|------------------------| 16 | | 1.x | 1.x | 17 | | 2.x | 1.x | 18 | | 3.x | 3.x | 19 | | 4.x | 4.x | 20 | 21 | Each jQuery Migrate version supports the same browsers that the jQuery version used with it. See the [jQuery Browser Support page](https://jquery.com/browser-support/) for more information. 22 | 23 | ## Usage 24 | 25 | In your web page, load this plugin *after* the script tag for jQuery, for example: 26 | 27 | ```html 28 | 29 | 30 | ``` 31 | 32 | ## Download 33 | 34 | ### Development vs. Production versions 35 | 36 | The production build is minified and does not generate console warnings. It will only generate a console log message upon loading, or if it detects an error such as an outdated version of jQuery that it does not support. Do not use this file for development or debugging, it will make your life miserable. 37 | 38 | | | Development | Production | 39 | |--|-------------|------------| 40 | | Debugging enabled |

| | 41 | | Minified | |

| 42 | | Latest release (*may be hotlinked if desired*) | [jquery-migrate-3.5.2.js](https://code.jquery.com/jquery-migrate-3.5.2.js) | [jquery-migrate-3.5.2.min.js](https://code.jquery.com/jquery-migrate-3.5.2.min.js) | 43 | | \* Latest work-in-progress build | [jquery-migrate-git.js](https://releases.jquery.com/git/jquery-migrate-git.js) | [jquery-migrate-git.min.js](https://releases.jquery.com/git/jquery-migrate-git.min.js) | 44 | 45 | 46 | \* **Work-in-progress build:** Although this file represents the most recent updates to the plugin, it may not have been thoroughly tested. We do not recommend using this file on production sites since it may be unstable; use the released production version instead. 47 | 48 | 49 | ## Debugging 50 | 51 | The development version of the plugin displays warnings in the browser console. Developers can also inspect the `jQuery.migrateMessages` array to see what error messages have been generated. 52 | 53 | All warnings generated by this plugin start with the string "JQMIGRATE". A list of the warnings you may see are in [warnings.md](https://github.com/jquery/jquery-migrate/blob/main/warnings.md). 54 | 55 | 56 | ## Migrate Plugin API 57 | 58 | This plugin adds some properties to the `jQuery` object that can be used to programmatically control and examine its behavior: 59 | 60 | `jQuery.migrateMessages`: This property is an array of string warning messages that have been generated by the code on the page, in the order they were generated. Messages appear in the array only once, even if the condition has occurred multiple times, unless `jQuery.migrateReset()` is called. 61 | 62 | `jQuery.migrateMute`: Set this property to `true` to prevent console warnings from being generated in the development version. The `jQuery.migrateMessages` array is still maintained when this property is set, which allows programmatic inspection without console output. 63 | 64 | `jQuery.migrateTrace`: Set this property to `false` if you want warnings but do not want stack traces to appear on the console. 65 | 66 | `jQuery.migrateReset()`: This method clears the `jQuery.migrateMessages` array and "forgets" the list of messages that have been seen already. 67 | 68 | `jQuery.migrateVersion`: This string property indicates the version of Migrate in use. 69 | 70 | `jQuery.migrateDeduplicateMessages`: By default, Migrate only gives a specific warning once. If you set this property to `false` it will give a warning for every occurrence each time it happens. Note that this can generate a lot of output, for example when a warning occurs in a loop. 71 | 72 | `jQuery.migrateDisablePatches`: Disables patches by their codes. You can find a code for each patch in square brackets in [warnings.md](https://github.com/jquery/jquery-migrate/blob/main/warnings.md). A limited number of warnings doesn't have codes defined and cannot be disabled. These are mostly setup issues like using an incorrect version of jQuery or loading Migrate multiple times. 73 | 74 | `jQuery.migrateDisablePatches`: Disables patches by their codes. 75 | 76 | `jQuery.migrateIsPatchEnabled`: Returns `true` if a patch of a provided code is enabled and `false` otherwise. 77 | 78 | `jQuery.UNSAFE_restoreLegacyHtmlPrefilter`: A deprecated alias of `jQuery.migrateEnablePatches( "self-closed-tags" )` 79 | 80 | ## Reporting problems 81 | 82 | Bugs that only occur when the jQuery Migrate plugin is used should be reported in the [jQuery Migrate Issue Tracker](https://github.com/jquery/jquery-migrate/issues) and should be accompanied by an executable test case that demonstrates the bug. The easiest way to do this is via an online test tool such as [jsFiddle.net](https://jsFiddle.net/) or [jsbin.com](https://jsbin.com). Use the development version when you are reporting bugs. 83 | 84 | Bugs in jQuery itself should be reported on the [jQuery Core bug tracker](https://bugs.jquery.com/) and again should be accompanied by a test case from [jsFiddle.net](https://jsFiddle.net/) or [jsbin.com](http://jsbin.com) so that we can reproduce the issue. 85 | 86 | For other questions about the plugin that aren't bugs, ask on the [jQuery Forum](http://forum.jquery.com). 87 | 88 | Build and run tests: 89 | ==================================================== 90 | 91 | ## Build with `npm` commands 92 | 93 | ```sh 94 | $ git clone git://github.com/jquery/jquery-migrate.git 95 | $ cd jquery-migrate 96 | $ npm install 97 | $ npm run build 98 | ``` 99 | 100 | ### Run tests 101 | 102 | ```sh 103 | $ npm test 104 | ``` 105 | 106 | ### Or 107 | 108 | ```sh 109 | $ npm run test:server 110 | ``` 111 | 112 | and open http://localhost:3000/test/ in your browser. 113 | -------------------------------------------------------------------------------- /test/unit/jquery/selector.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | QUnit.module( "selector", { 4 | beforeEach: function() { 5 | 6 | /* eslint-disable max-len */ 7 | var template = "" + 8 | "

See this blog entry for more information.

" + 9 | "\n" + 10 | "

" + 11 | " Here are some [links] in a normal paragraph: Google," + 12 | " Google Groups (Link)." + 13 | " This link has class='blog':" + 14 | " mozilla" + 15 | "\n" + 16 | "

" + 17 | "
" + 18 | "

Everything inside the red border is inside a div with id='foo'.

" + 19 | "

This is a normal link: Yahoo

" + 20 | "

This link has class='blog': Timmy Willison's Weblog

" + 21 | "
" + 22 | "\n" + 23 | "
" + 24 | " " + 25 | " " + 26 | " " + 27 | " " + 28 | " " + 29 | "
"; 30 | /* eslint-enable */ 31 | 32 | jQuery( "#qunit-fixture" ).append( template ); 33 | } 34 | } ); 35 | 36 | ( function() { 37 | 38 | /** 39 | * Asserts that a select matches the given IDs. 40 | * The select is not bound by a context. 41 | * @param {Function} assert - The assert function passed from the test 42 | * @param {String} message - Assertion name 43 | * @param {String} selector - jQuery selector 44 | * @param {String} expectedIds - Array of ids to construct what is expected 45 | * @example testSelector("Check for something", "p", ["foo", "bar"]); 46 | */ 47 | function testSelector( assert, message, selector, expectedIds ) { 48 | var r = [], 49 | i = 0; 50 | 51 | var elems = jQuery( selector ).get(); 52 | 53 | for ( ; i < expectedIds.length; i++ ) { 54 | r.push( document.getElementById( expectedIds[ i ] ) ); 55 | } 56 | 57 | assert.deepEqual( elems, r, message + " (" + selector + ")" ); 58 | } 59 | 60 | QUnit.test( "jQuery.expr.pseudos aliases", function( assert ) { 61 | assert.expect( 7 ); 62 | 63 | expectMessage( assert, "jQuery.expr.filters", function() { 64 | jQuery.expr.filters.mazda = function( elem ) { 65 | return elem.style.zoom === "3"; 66 | }; 67 | } ); 68 | 69 | expectMessage( assert, "jQuery.expr[':']", function() { 70 | jQuery.expr[ ":" ].marginal = function( elem ) { 71 | return parseInt( elem.style.marginLeftWidth ) > 20; 72 | }; 73 | } ); 74 | 75 | expectNoMessage( assert, "jQuery.expr.pseudos", function() { 76 | var fixture = jQuery( "#qunit-fixture" ).prepend( "

hello

" ); 77 | 78 | assert.ok( jQuery.expr.pseudos.mazda, "filters assigned" ); 79 | assert.ok( jQuery.expr.pseudos.marginal, "[':'] assigned" ); 80 | fixture.find( "p" ).first().css( "marginLeftWidth", "40px" ); 81 | assert.equal( fixture.find( "p:marginal" ).length, 1, "One marginal element" ); 82 | assert.equal( fixture.find( "div:mazda" ).length, 0, "No mazda elements" ); 83 | delete jQuery.expr.pseudos.mazda; 84 | delete jQuery.expr.pseudos.marginal; 85 | } ); 86 | } ); 87 | 88 | QUnit.test( "custom pseudos", function( assert ) { 89 | assert.expect( 7 ); 90 | 91 | expectNoMessage( assert, "custom pseudos", function() { 92 | try { 93 | jQuery.expr.pseudos.foundation = jQuery.expr.pseudos.root; 94 | assert.deepEqual( 95 | jQuery.find( ":foundation" ), 96 | [ document.documentElement ], 97 | "Copy element filter with new name" 98 | ); 99 | } finally { 100 | delete jQuery.expr.pseudos.foundation; 101 | } 102 | 103 | try { 104 | jQuery.expr.setFilters.primary = jQuery.expr.setFilters.first; 105 | testSelector( 106 | assert, 107 | "Copy set filter with new name", 108 | "div#qunit-fixture :primary", 109 | [ "firstp" ] 110 | ); 111 | } finally { 112 | delete jQuery.expr.setFilters.primary; 113 | } 114 | 115 | try { 116 | jQuery.expr.pseudos.aristotlean = jQuery.expr.createPseudo( function() { 117 | return function( elem ) { 118 | return !!elem.id; 119 | }; 120 | } ); 121 | testSelector( 122 | assert, 123 | "Custom element filter", 124 | "#foo :aristotlean", 125 | [ "sndp", "en", "yahoo", "sap", "anchor2", "timmy" ] 126 | ); 127 | } finally { 128 | delete jQuery.expr.pseudos.aristotlean; 129 | } 130 | 131 | try { 132 | jQuery.expr.pseudos.endswith = jQuery.expr.createPseudo( function( text ) { 133 | return function( elem ) { 134 | return jQuery.text( elem ).slice( -text.length ) === text; 135 | }; 136 | } ); 137 | testSelector( 138 | assert, 139 | "Custom element filter with argument", 140 | "a:endswith(ogle)", 141 | [ "google" ] 142 | ); 143 | } finally { 144 | delete jQuery.expr.pseudos.endswith; 145 | } 146 | 147 | try { 148 | jQuery.expr.setFilters.second = jQuery.expr.createPseudo( function() { 149 | return jQuery.expr.createPseudo( function( seed, matches ) { 150 | if ( seed[ 1 ] ) { 151 | matches[ 1 ] = seed[ 1 ]; 152 | seed[ 1 ] = false; 153 | } 154 | } ); 155 | } ); 156 | testSelector( assert, 157 | "Custom set filter", 158 | "#qunit-fixture p:second", 159 | [ "ap" ] 160 | ); 161 | } finally { 162 | delete jQuery.expr.pseudos.second; 163 | } 164 | 165 | try { 166 | jQuery.expr.setFilters.slice = jQuery.expr.createPseudo( function( argument ) { 167 | var bounds = argument.split( ":" ); 168 | return jQuery.expr.createPseudo( function( seed, matches ) { 169 | var i = bounds[ 1 ]; 170 | 171 | // Match elements found at the specified indexes 172 | while ( --i >= bounds[ 0 ] ) { 173 | if ( seed[ i ] ) { 174 | matches[ i ] = seed[ i ]; 175 | seed[ i ] = false; 176 | } 177 | } 178 | } ); 179 | } ); 180 | testSelector( 181 | assert, 182 | "Custom set filter with argument", 183 | "#qunit-fixture p:slice(1:3)", 184 | [ "ap", "sndp" ] 185 | ); 186 | } finally { 187 | delete jQuery.expr.pseudos.slice; 188 | } 189 | } ); 190 | } ); 191 | 192 | QUnit.test( "backwards-compatible custom pseudos", function( assert ) { 193 | assert.expect( 7 ); 194 | 195 | var expectWarningWithProxy = typeof Proxy !== "undefined" ? 196 | expectMessage : 197 | function( _assert, _title, fn ) { 198 | fn(); 199 | assert.ok( true, "No Proxy => warnings not expected" ); 200 | }; 201 | 202 | try { 203 | expectWarningWithProxy( assert, "Custom element filter with argument - setter", function() { 204 | jQuery.expr.pseudos.icontains = function( elem, i, match ) { 205 | return jQuery 206 | .text( elem ) 207 | .toLowerCase() 208 | .indexOf( ( match[ 3 ] || "" ).toLowerCase() ) > -1; 209 | }; 210 | } ); 211 | expectMessage( assert, "Custom element filter with argument - getter", function() { 212 | testSelector( 213 | assert, 214 | "Custom element filter with argument", 215 | "a:icontains(THIS BLOG ENTRY)", 216 | [ "john1" ] 217 | ); 218 | } ); 219 | } finally { 220 | delete jQuery.expr.pseudos.icontains; 221 | } 222 | 223 | try { 224 | expectWarningWithProxy( assert, "Custom setFilter pseudo - setter", function() { 225 | jQuery.expr.setFilters.podium = function( elements, argument ) { 226 | var count = argument == null || argument === "" ? 3 : +argument; 227 | return elements.slice( 0, count ); 228 | }; 229 | } ); 230 | expectMessage( assert, "Custom setFilter pseudo - getter", function() { 231 | 232 | // Using TAG as the first token here forces this setMatcher into a fail state 233 | // Where the descendent combinator was lost 234 | testSelector( 235 | assert, 236 | "Custom setFilter", 237 | "form#form :PODIUM", 238 | [ "label-for", "text1", "text2" ] 239 | ); 240 | testSelector( 241 | assert, 242 | "Custom setFilter with argument", 243 | "#form input:Podium(1)", 244 | [ "text1" ] 245 | ); 246 | } ); 247 | } finally { 248 | delete jQuery.expr.setFilters.podium; 249 | } 250 | } ); 251 | 252 | } )(); 253 | -------------------------------------------------------------------------------- /test/data/testinit.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | ( typeof global != "undefined" ? global : window ).TestManager = { 4 | 5 | /** 6 | * Load a version of a file based on URL parameters. 7 | * 8 | * dev Uncompressed development version: source files in the project /dist dir 9 | * esmodules Non-combined dev version: source files from the project /src dir 10 | * min Minified version in the project /dist dir 11 | * VER Version from code.jquery.com, e.g.: git, 4.0.0.min or 4.0.0-rc.1 12 | * else Full or relative path to be used for script src 13 | */ 14 | loadProject: function( projectName, defaultVersion, isSelf ) { 15 | var file, 16 | urlTag = this.projects[ projectName ].urlTag, 17 | matcher = new RegExp( "\\b" + urlTag + "=([^&]+)" ), 18 | projectRoot = this.baseURL + ( isSelf ? "../.." : "../../../" + projectName ), 19 | version = ( matcher.exec( document.location.search ) || {} )[ 1 ] || defaultVersion; 20 | 21 | // The esmodules mode requires the browser to support ES modules 22 | // so it won't run in IE. 23 | if ( version === "esmodules" ) { 24 | 25 | // This is the main source file that imports all the others. 26 | file = projectRoot + "/src/migrate.js"; 27 | } else if ( version === "dev" ) { 28 | file = projectRoot + "/dist/" + projectName + ".js"; 29 | } else if ( version === "min" ) { 30 | file = projectRoot + "/dist/" + projectName + ".min.js"; 31 | } else if ( version.indexOf( "git" ) === 0 ) { 32 | file = "https://releases.jquery.com/git/" + projectName + "-" + version + ".js"; 33 | } else if ( /^[\w\.\-]+$/.test( version ) ) { 34 | file = "https://code.jquery.com/" + projectName + "-" + version + ".js"; 35 | } else { 36 | file = version; 37 | } 38 | this.loaded.push( { 39 | projectName: projectName, 40 | tag: version, 41 | file: file 42 | } ); 43 | 44 | if ( version === "esmodules" ) { 45 | document.write( "" ); 46 | } else { 47 | document.write( "" ); 48 | } 49 | }, 50 | 51 | /** 52 | * Load jQuery Migrate tests. In esmodules mode it loads all tests as 53 | * ES modules so that they get executed in the correct order. 54 | */ 55 | loadTests: function() { 56 | var esmodules = QUnit.config.plugin === "esmodules" || 57 | QUnit.urlParams.plugin === "esmodules", 58 | testFiles = [ 59 | "data/test-utils.js", 60 | "unit/migrate.js", 61 | "unit/jquery/core.js", 62 | "unit/jquery/ajax.js", 63 | "unit/jquery/attributes.js", 64 | "unit/jquery/css.js", 65 | "unit/jquery/data.js", 66 | "unit/jquery/deferred.js", 67 | "unit/jquery/effects.js", 68 | "unit/jquery/event.js", 69 | "unit/jquery/manipulation.js", 70 | "unit/jquery/selector.js" 71 | ]; 72 | 73 | testFiles.forEach( function( testFile ) { 74 | document.write( "" ); 76 | } ); 77 | 78 | document.write( "" ); 81 | 82 | }, 83 | 84 | /** 85 | * Iframe tests that require setup not covered in the standard unit test 86 | * 87 | * Note that options passed into the standard unit tests will also be passed to 88 | * the iframe, but the iframe html page is responsible for processing them 89 | * as appropriate (for example by calling TestManager.loadProject) 90 | */ 91 | runIframeTest: function( title, url, func ) { 92 | var that = this, 93 | esmodules = QUnit.config.plugin === "esmodules" || 94 | QUnit.urlParams.plugin === "esmodules"; 95 | 96 | // Skip iframe tests in esmodules mode as that mode is not compatible with how 97 | // they are written. 98 | if ( esmodules ) { 99 | QUnit.skip( title ); 100 | return; 101 | } 102 | 103 | QUnit.test( title, function( assert ) { 104 | var iframe, 105 | query = window.location.search.slice( 1 ), 106 | done = assert.async(); 107 | 108 | that.iframeCallback = function() { 109 | var args = Array.prototype.slice.call( arguments ); 110 | 111 | args.unshift( assert ); 112 | 113 | setTimeout( function() { 114 | that.iframeCallback = undefined; 115 | 116 | func.apply( this, args ); 117 | func = function() {}; 118 | iframe.remove(); 119 | 120 | done(); 121 | } ); 122 | }; 123 | iframe = jQuery( "
" ) 124 | .css( { position: "absolute", width: "500px", left: "-600px" } ) 125 | .append( jQuery( "