From 9887579b61972647f1478e64c5d7987f9d9cb039 Mon Sep 17 00:00:00 2001 From: Michał Gołębiowski-Owczarek Date: Fri, 10 May 2024 14:45:59 +0200 Subject: All: Stop relying on jquery-patch.js internally, add tests Avoid relying on jQuery patches. Instead: * use `CSS.escape` instead of `jQuery.escapeSelector` * use `.filter()` with a proper handler instead of `.even()` Keep `jquery-patch.js` for backwards compatibility, though. Also, add tests for jquery-patch. Ref gh-2249 --- build/tasks/testswarm.js | 1 + tests/lib/bootstrap.js | 8 -- tests/runner/flags/suites.js | 1 + tests/unit/accordion/common.js | 12 ++- tests/unit/all.html | 1 + tests/unit/index.html | 1 + tests/unit/jquery-patch/all.html | 26 ++++++ tests/unit/jquery-patch/core.js | 141 ++++++++++++++++++++++++++++++ tests/unit/jquery-patch/jquery-patch.html | 26 ++++++ ui/core.js | 1 - ui/jquery-patch.js | 31 ++----- ui/labels.js | 2 +- ui/widgets/accordion.js | 12 ++- ui/widgets/checkboxradio.js | 2 +- ui/widgets/selectmenu.js | 2 +- ui/widgets/tabs.js | 2 +- 16 files changed, 228 insertions(+), 41 deletions(-) create mode 100644 tests/unit/jquery-patch/all.html create mode 100644 tests/unit/jquery-patch/core.js create mode 100644 tests/unit/jquery-patch/jquery-patch.html diff --git a/build/tasks/testswarm.js b/build/tasks/testswarm.js index 1f5bb81b2..8ab2b7643 100644 --- a/build/tasks/testswarm.js +++ b/build/tasks/testswarm.js @@ -35,6 +35,7 @@ var versions = { "Droppable": "droppable/droppable.html", "Effects": "effects/effects.html", "Form Reset Mixin": "form-reset-mixin/form-reset-mixin.html", + "jQuery Patch": "jquery-patch/jquery-patch.html", "Menu": "menu/menu.html", "Position": "position/position.html", "Progressbar": "progressbar/progressbar.html", diff --git a/tests/lib/bootstrap.js b/tests/lib/bootstrap.js index e0df9ebf5..b86444139 100644 --- a/tests/lib/bootstrap.js +++ b/tests/lib/bootstrap.js @@ -171,14 +171,6 @@ function migrateUrl() { } } - var jQueryVersion = parseUrl().jquery; - - // Load the jQuery fixes, if necessary - if ( !jQueryVersion || - ( jQueryVersion.indexOf( "git" ) === -1 && parseFloat( jQueryVersion ) < 4 ) ) { - modules.unshift( "ui/jquery-patch" ); - } - requireTests( modules, { backCompat: backCompat } ); } )(); diff --git a/tests/runner/flags/suites.js b/tests/runner/flags/suites.js index aa7732bf1..a635ac4e5 100644 --- a/tests/runner/flags/suites.js +++ b/tests/runner/flags/suites.js @@ -11,6 +11,7 @@ export const suites = [ "droppable", "effects", "form-reset-mixin", + "jquery-patch", "menu", "position", "progressbar", diff --git a/tests/unit/accordion/common.js b/tests/unit/accordion/common.js index 926d5d9c3..4f1ba7e5d 100644 --- a/tests/unit/accordion/common.js +++ b/tests/unit/accordion/common.js @@ -16,7 +16,17 @@ common.testWidget( "accordion", { disabled: false, event: "click", header: function( elem ) { - return elem.find( "> li > :first-child" ).add( elem.find( "> :not(li)" ).even() ); + return elem + .find( "> li > :first-child" ) + .add( + elem.find( "> :not(li)" ) + + // Support: jQuery <3.5 only + // We could use `.even()` but that's unavailable in older jQuery. + .filter( function( i ) { + return i % 2 === 0; + } ) + ); }, heightStyle: "auto", icons: { diff --git a/tests/unit/all.html b/tests/unit/all.html index 3a39956f0..67d714130 100644 --- a/tests/unit/all.html +++ b/tests/unit/all.html @@ -28,6 +28,7 @@ "droppable/droppable.html", "effects/effects.html", "form-reset-mixin/form-reset-mixin.html", + "jquery-patch/jquery-patch.html", "menu/menu.html", "position/position.html", "progressbar/progressbar.html", diff --git a/tests/unit/index.html b/tests/unit/index.html index 091e39c41..80224682a 100644 --- a/tests/unit/index.html +++ b/tests/unit/index.html @@ -55,6 +55,7 @@

Utilities

diff --git a/tests/unit/jquery-patch/all.html b/tests/unit/jquery-patch/all.html new file mode 100644 index 000000000..d26b50373 --- /dev/null +++ b/tests/unit/jquery-patch/all.html @@ -0,0 +1,26 @@ + + + + + jQuery UI Form Reset Mixin Test Suite + + + + + + + + + + + + + +
+
+ +
+ + diff --git a/tests/unit/jquery-patch/core.js b/tests/unit/jquery-patch/core.js new file mode 100644 index 000000000..5ce7771f8 --- /dev/null +++ b/tests/unit/jquery-patch/core.js @@ -0,0 +1,141 @@ +define( [ + "qunit", + "jquery", + "lib/helper", + "ui/jquery-patch" +], function( QUnit, $, helper ) { +"use strict"; + +QUnit.module( "jquery-patch: core", { afterEach: helper.moduleAfterEach } ); + +QUnit.test( "jQuery.escapeSelector", function( assert ) { + assert.expect( 58 ); + + // Edge cases + assert.equal( jQuery.escapeSelector(), "undefined", "Converts undefined to string" ); + assert.equal( jQuery.escapeSelector( "-" ), "\\-", "Escapes standalone dash" ); + assert.equal( jQuery.escapeSelector( "-a" ), "-a", "Doesn't escape leading dash followed by non-number" ); + assert.equal( jQuery.escapeSelector( "--" ), "--", "Doesn't escape standalone double dash" ); + assert.equal( jQuery.escapeSelector( "\uFFFD" ), "\uFFFD", + "Doesn't escape standalone replacement character" ); + assert.equal( jQuery.escapeSelector( "a\uFFFD" ), "a\uFFFD", + "Doesn't escape trailing replacement character" ); + assert.equal( jQuery.escapeSelector( "\uFFFDb" ), "\uFFFDb", + "Doesn't escape leading replacement character" ); + assert.equal( jQuery.escapeSelector( "a\uFFFDb" ), "a\uFFFDb", + "Doesn't escape embedded replacement character" ); + + // Derived from CSSOM tests + // https://test.csswg.org/harness/test/cssom-1_dev/section/7.1/ + + // String conversion + assert.equal( jQuery.escapeSelector( true ), "true", "Converts boolean true to string" ); + assert.equal( jQuery.escapeSelector( false ), "false", "Converts boolean true to string" ); + assert.equal( jQuery.escapeSelector( null ), "null", "Converts null to string" ); + assert.equal( jQuery.escapeSelector( "" ), "", "Doesn't modify empty string" ); + + // Null bytes + assert.equal( jQuery.escapeSelector( "\0" ), "\uFFFD", + "Escapes null-character input as replacement character" ); + assert.equal( jQuery.escapeSelector( "a\0" ), "a\uFFFD", + "Escapes trailing-null input as replacement character" ); + assert.equal( jQuery.escapeSelector( "\0b" ), "\uFFFDb", + "Escapes leading-null input as replacement character" ); + assert.equal( jQuery.escapeSelector( "a\0b" ), "a\uFFFDb", + "Escapes embedded-null input as replacement character" ); + + // Number prefix + assert.equal( jQuery.escapeSelector( "0a" ), "\\30 a", "Escapes leading 0" ); + assert.equal( jQuery.escapeSelector( "1a" ), "\\31 a", "Escapes leading 1" ); + assert.equal( jQuery.escapeSelector( "2a" ), "\\32 a", "Escapes leading 2" ); + assert.equal( jQuery.escapeSelector( "3a" ), "\\33 a", "Escapes leading 3" ); + assert.equal( jQuery.escapeSelector( "4a" ), "\\34 a", "Escapes leading 4" ); + assert.equal( jQuery.escapeSelector( "5a" ), "\\35 a", "Escapes leading 5" ); + assert.equal( jQuery.escapeSelector( "6a" ), "\\36 a", "Escapes leading 6" ); + assert.equal( jQuery.escapeSelector( "7a" ), "\\37 a", "Escapes leading 7" ); + assert.equal( jQuery.escapeSelector( "8a" ), "\\38 a", "Escapes leading 8" ); + assert.equal( jQuery.escapeSelector( "9a" ), "\\39 a", "Escapes leading 9" ); + + // Letter-number prefix + assert.equal( jQuery.escapeSelector( "a0b" ), "a0b", "Doesn't escape embedded 0" ); + assert.equal( jQuery.escapeSelector( "a1b" ), "a1b", "Doesn't escape embedded 1" ); + assert.equal( jQuery.escapeSelector( "a2b" ), "a2b", "Doesn't escape embedded 2" ); + assert.equal( jQuery.escapeSelector( "a3b" ), "a3b", "Doesn't escape embedded 3" ); + assert.equal( jQuery.escapeSelector( "a4b" ), "a4b", "Doesn't escape embedded 4" ); + assert.equal( jQuery.escapeSelector( "a5b" ), "a5b", "Doesn't escape embedded 5" ); + assert.equal( jQuery.escapeSelector( "a6b" ), "a6b", "Doesn't escape embedded 6" ); + assert.equal( jQuery.escapeSelector( "a7b" ), "a7b", "Doesn't escape embedded 7" ); + assert.equal( jQuery.escapeSelector( "a8b" ), "a8b", "Doesn't escape embedded 8" ); + assert.equal( jQuery.escapeSelector( "a9b" ), "a9b", "Doesn't escape embedded 9" ); + + // Dash-number prefix + assert.equal( jQuery.escapeSelector( "-0a" ), "-\\30 a", "Escapes 0 after leading dash" ); + assert.equal( jQuery.escapeSelector( "-1a" ), "-\\31 a", "Escapes 1 after leading dash" ); + assert.equal( jQuery.escapeSelector( "-2a" ), "-\\32 a", "Escapes 2 after leading dash" ); + assert.equal( jQuery.escapeSelector( "-3a" ), "-\\33 a", "Escapes 3 after leading dash" ); + assert.equal( jQuery.escapeSelector( "-4a" ), "-\\34 a", "Escapes 4 after leading dash" ); + assert.equal( jQuery.escapeSelector( "-5a" ), "-\\35 a", "Escapes 5 after leading dash" ); + assert.equal( jQuery.escapeSelector( "-6a" ), "-\\36 a", "Escapes 6 after leading dash" ); + assert.equal( jQuery.escapeSelector( "-7a" ), "-\\37 a", "Escapes 7 after leading dash" ); + assert.equal( jQuery.escapeSelector( "-8a" ), "-\\38 a", "Escapes 8 after leading dash" ); + assert.equal( jQuery.escapeSelector( "-9a" ), "-\\39 a", "Escapes 9 after leading dash" ); + + // Double dash prefix + assert.equal( jQuery.escapeSelector( "--a" ), "--a", "Doesn't escape leading double dash" ); + + // Miscellany + assert.equal( jQuery.escapeSelector( "\x01\x02\x1E\x1F" ), "\\1 \\2 \\1e \\1f ", + "Escapes C0 control characters" ); + assert.equal( jQuery.escapeSelector( "\x80\x2D\x5F\xA9" ), "\x80\x2D\x5F\xA9", + "Doesn't escape general punctuation or non-ASCII ISO-8859-1 characters" ); + assert.equal( + jQuery.escapeSelector( "\x7F\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F\x90" + + "\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F" ), + "\\7f \x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F\x90" + + "\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F", + "Escapes DEL control character" + ); + assert.equal( jQuery.escapeSelector( "\xA0\xA1\xA2" ), "\xA0\xA1\xA2", + "Doesn't escape non-ASCII ISO-8859-1 characters" ); + assert.equal( jQuery.escapeSelector( "a0123456789b" ), "a0123456789b", + "Doesn't escape embedded numbers" ); + assert.equal( jQuery.escapeSelector( "abcdefghijklmnopqrstuvwxyz" ), "abcdefghijklmnopqrstuvwxyz", + "Doesn't escape lowercase ASCII letters" ); + assert.equal( jQuery.escapeSelector( "ABCDEFGHIJKLMNOPQRSTUVWXYZ" ), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "Doesn't escape uppercase ASCII letters" ); + assert.equal( jQuery.escapeSelector( "\x20\x21\x78\x79" ), "\\ \\!xy", + "Escapes non-word ASCII characters" ); + + // Astral symbol (U+1D306 TETRAGRAM FOR CENTRE) + assert.equal( jQuery.escapeSelector( "\uD834\uDF06" ), "\uD834\uDF06", + "Doesn't escape astral characters" ); + + // Lone surrogates + assert.equal( jQuery.escapeSelector( "\uDF06" ), "\uDF06", "Doesn't escape lone low surrogate" ); + assert.equal( jQuery.escapeSelector( "\uD834" ), "\uD834", "Doesn't escape lone high surrogate" ); +} ); + +QUnit.test( "even()/odd()", function( assert ) { + assert.expect( 8 ); + + var lis, + ul = jQuery( "" ), + none = jQuery(); + + ul.appendTo( "#qunit-fixture" ); + + lis = ul.find( "li" ); + + assert.strictEqual( lis.even().length, 2, "even() length" ); + assert.strictEqual( lis.even().eq( 0 ).text(), "One", "even(): 1st" ); + assert.strictEqual( lis.even().eq( 1 ).text(), "Three", "even(): 2nd" ); + + assert.strictEqual( lis.odd().length, 2, "odd() length" ); + assert.strictEqual( lis.odd().eq( 0 ).text(), "Two", "odd(): 1st" ); + assert.strictEqual( lis.odd().eq( 1 ).text(), "Four", "odd(): 2nd" ); + + assert.deepEqual( none.even().get(), [], "even() none" ); + assert.deepEqual( none.odd().get(), [], "odd() none" ); +} ); + +} ); diff --git a/tests/unit/jquery-patch/jquery-patch.html b/tests/unit/jquery-patch/jquery-patch.html new file mode 100644 index 000000000..a20e25d4a --- /dev/null +++ b/tests/unit/jquery-patch/jquery-patch.html @@ -0,0 +1,26 @@ + + + + + jQuery UI Form Reset Mixin Test Suite + + + + + + + +
+
+ +
+ + + + +
+ +
+ + diff --git a/ui/core.js b/ui/core.js index 33d7974e2..d4651c689 100644 --- a/ui/core.js +++ b/ui/core.js @@ -9,7 +9,6 @@ define( [ "./focusable", "./keycode", "./labels", - "./jquery-patch", "./plugin", "./scroll-parent", "./tabbable", diff --git a/ui/jquery-patch.js b/ui/jquery-patch.js index e03b32edf..a4001048d 100644 --- a/ui/jquery-patch.js +++ b/ui/jquery-patch.js @@ -1,5 +1,5 @@ /*! - * jQuery UI Support for jQuery core 1.8.x and newer @VERSION + * jQuery UI Legacy jQuery Core patches @VERSION * https://jqueryui.com * * Copyright OpenJS Foundation and other contributors @@ -8,9 +8,9 @@ * */ -//>>label: jQuery 1.8+ Support +//>>label: Legacy jQuery Core patches //>>group: Core -//>>description: Support version 1.8.x and newer of jQuery core +//>>description: Backport `.even()`, `.odd()` and `$.escapeSelector` to older jQuery Core versions (deprecated) ( function( factory ) { "use strict"; @@ -31,29 +31,8 @@ // This method has been defined in jQuery 3.0.0. // Code from https://github.com/jquery/jquery/blob/e539bac79e666bba95bba86d690b4e609dca2286/src/selector/escapeSelector.js if ( !$.escapeSelector ) { - - // CSS string/identifier serialization - // https://drafts.csswg.org/cssom/#common-serializing-idioms - var rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g; - - var fcssescape = function( ch, asCodePoint ) { - if ( asCodePoint ) { - - // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER - if ( ch === "\0" ) { - return "\uFFFD"; - } - - // Control characters and (dependent upon position) numbers get escaped as code points - return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; - } - - // Other potentially-special ASCII characters get backslash-escaped - return "\\" + ch; - }; - - $.escapeSelector = function( sel ) { - return ( sel + "" ).replace( rcssescape, fcssescape ); + $.escapeSelector = function( id ) { + return CSS.escape( id + "" ); }; } diff --git a/ui/labels.js b/ui/labels.js index 4f6533451..5ff44c28b 100644 --- a/ui/labels.js +++ b/ui/labels.js @@ -55,7 +55,7 @@ return $.fn.labels = function() { ancestors = ancestor.add( ancestor.length ? ancestor.siblings() : this.siblings() ); // Create a selector for the label based on the id - selector = "label[for='" + $.escapeSelector( id ) + "']"; + selector = "label[for='" + CSS.escape( id ) + "']"; labels = labels.add( ancestors.find( selector ).addBack( selector ) ); diff --git a/ui/widgets/accordion.js b/ui/widgets/accordion.js index b6a7a7eee..ff6e4631d 100644 --- a/ui/widgets/accordion.js +++ b/ui/widgets/accordion.js @@ -52,7 +52,17 @@ return $.widget( "ui.accordion", { collapsible: false, event: "click", header: function( elem ) { - return elem.find( "> li > :first-child" ).add( elem.find( "> :not(li)" ).even() ); + return elem + .find( "> li > :first-child" ) + .add( + elem.find( "> :not(li)" ) + + // Support: jQuery <3.5 only + // We could use `.even()` but that's unavailable in older jQuery. + .filter( function( i ) { + return i % 2 === 0; + } ) + ); }, heightStyle: "auto", icons: { diff --git a/ui/widgets/checkboxradio.js b/ui/widgets/checkboxradio.js index fc812116e..379252758 100644 --- a/ui/widgets/checkboxradio.js +++ b/ui/widgets/checkboxradio.js @@ -156,7 +156,7 @@ $.widget( "ui.checkboxradio", [ $.ui.formResetMixin, { _getRadioGroup: function() { var group; var name = this.element[ 0 ].name; - var nameSelector = "input[name='" + $.escapeSelector( name ) + "']"; + var nameSelector = "input[name='" + CSS.escape( name ) + "']"; if ( !name ) { return $( [] ); diff --git a/ui/widgets/selectmenu.js b/ui/widgets/selectmenu.js index eecd368f5..f1b48fa60 100644 --- a/ui/widgets/selectmenu.js +++ b/ui/widgets/selectmenu.js @@ -415,7 +415,7 @@ return $.widget( "ui.selectmenu", [ $.ui.formResetMixin, { } if ( !$( event.target ).closest( ".ui-selectmenu-menu, #" + - $.escapeSelector( this.ids.button ) ).length ) { + CSS.escape( this.ids.button ) ).length ) { this.close( event ); } } diff --git a/ui/widgets/tabs.js b/ui/widgets/tabs.js index e191dfbb4..72b868e4f 100644 --- a/ui/widgets/tabs.js +++ b/ui/widgets/tabs.js @@ -722,7 +722,7 @@ $.widget( "ui.tabs", { // meta-function to give users option to provide a href string instead of a numerical index. if ( typeof index === "string" ) { index = this.anchors.index( this.anchors.filter( "[href$='" + - $.escapeSelector( index ) + "']" ) ); + CSS.escape( index ) + "']" ) ); } return index; -- cgit v1.2.3