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
This commit is contained in:
Michał Gołębiowski-Owczarek 2024-05-10 14:45:59 +02:00
parent f90eab84b5
commit 9887579b61
No known key found for this signature in database
16 changed files with 228 additions and 41 deletions

View File

@ -35,6 +35,7 @@ var versions = {
"Droppable": "droppable/droppable.html", "Droppable": "droppable/droppable.html",
"Effects": "effects/effects.html", "Effects": "effects/effects.html",
"Form Reset Mixin": "form-reset-mixin/form-reset-mixin.html", "Form Reset Mixin": "form-reset-mixin/form-reset-mixin.html",
"jQuery Patch": "jquery-patch/jquery-patch.html",
"Menu": "menu/menu.html", "Menu": "menu/menu.html",
"Position": "position/position.html", "Position": "position/position.html",
"Progressbar": "progressbar/progressbar.html", "Progressbar": "progressbar/progressbar.html",

View File

@ -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 } ); requireTests( modules, { backCompat: backCompat } );
} )(); } )();

View File

@ -11,6 +11,7 @@ export const suites = [
"droppable", "droppable",
"effects", "effects",
"form-reset-mixin", "form-reset-mixin",
"jquery-patch",
"menu", "menu",
"position", "position",
"progressbar", "progressbar",

View File

@ -16,7 +16,17 @@ common.testWidget( "accordion", {
disabled: false, disabled: false,
event: "click", event: "click",
header: function( elem ) { 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", heightStyle: "auto",
icons: { icons: {

View File

@ -28,6 +28,7 @@
"droppable/droppable.html", "droppable/droppable.html",
"effects/effects.html", "effects/effects.html",
"form-reset-mixin/form-reset-mixin.html", "form-reset-mixin/form-reset-mixin.html",
"jquery-patch/jquery-patch.html",
"menu/menu.html", "menu/menu.html",
"position/position.html", "position/position.html",
"progressbar/progressbar.html", "progressbar/progressbar.html",

View File

@ -55,6 +55,7 @@
<h2>Utilities</h2> <h2>Utilities</h2>
<ul> <ul>
<li><a href="jquery-patch/jquery-patch.html">jQuery Patch</a></li>
<li><a href="position/position.html">Position</a></li> <li><a href="position/position.html">Position</a></li>
</ul> </ul>

View File

@ -0,0 +1,26 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>jQuery UI Form Reset Mixin Test Suite</title>
<script src="../../../external/jquery/jquery.js"></script>
<link rel="stylesheet" href="../../../external/qunit/qunit.css">
<link rel="stylesheet" href="../../lib/vendor/qunit-composite/qunit-composite.css">
<script src="../../../external/qunit/qunit.js"></script>
<script src="../../lib/vendor/qunit-composite/qunit-composite.js"></script>
<script src="../subsuite.js"></script>
<script>
testAllVersions( "jquery-patch" );
</script>
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture">
</div>
</body>
</html>

141
tests/unit/jquery-patch/core.js vendored Normal file
View File

@ -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( "<ul><li>One</li><li>Two</li><li>Three</li><li>Four</li></ul>" ),
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" );
} );
} );

View File

@ -0,0 +1,26 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>jQuery UI Form Reset Mixin Test Suite</title>
<script src="../../../external/requirejs/require.js"></script>
<script src="../../lib/css.js"></script>
<script src="../../lib/bootstrap.js" data-modules="core">
</script>
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture">
<form id="main">
<input id="input1">
<input id="input2">
<input id="input3">
<input id="input4">
</form>
</div>
</body>
</html>

View File

@ -9,7 +9,6 @@ define( [
"./focusable", "./focusable",
"./keycode", "./keycode",
"./labels", "./labels",
"./jquery-patch",
"./plugin", "./plugin",
"./scroll-parent", "./scroll-parent",
"./tabbable", "./tabbable",

31
ui/jquery-patch.js vendored
View File

@ -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 * https://jqueryui.com
* *
* Copyright OpenJS Foundation and other contributors * Copyright OpenJS Foundation and other contributors
@ -8,9 +8,9 @@
* *
*/ */
//>>label: jQuery 1.8+ Support //>>label: Legacy jQuery Core patches
//>>group: Core //>>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 ) { ( function( factory ) {
"use strict"; "use strict";
@ -31,29 +31,8 @@
// This method has been defined in jQuery 3.0.0. // This method has been defined in jQuery 3.0.0.
// Code from https://github.com/jquery/jquery/blob/e539bac79e666bba95bba86d690b4e609dca2286/src/selector/escapeSelector.js // Code from https://github.com/jquery/jquery/blob/e539bac79e666bba95bba86d690b4e609dca2286/src/selector/escapeSelector.js
if ( !$.escapeSelector ) { if ( !$.escapeSelector ) {
$.escapeSelector = function( id ) {
// CSS string/identifier serialization return CSS.escape( id + "" );
// 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 );
}; };
} }

View File

@ -55,7 +55,7 @@ return $.fn.labels = function() {
ancestors = ancestor.add( ancestor.length ? ancestor.siblings() : this.siblings() ); ancestors = ancestor.add( ancestor.length ? ancestor.siblings() : this.siblings() );
// Create a selector for the label based on the id // 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 ) ); labels = labels.add( ancestors.find( selector ).addBack( selector ) );

View File

@ -52,7 +52,17 @@ return $.widget( "ui.accordion", {
collapsible: false, collapsible: false,
event: "click", event: "click",
header: function( elem ) { 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", heightStyle: "auto",
icons: { icons: {

View File

@ -156,7 +156,7 @@ $.widget( "ui.checkboxradio", [ $.ui.formResetMixin, {
_getRadioGroup: function() { _getRadioGroup: function() {
var group; var group;
var name = this.element[ 0 ].name; var name = this.element[ 0 ].name;
var nameSelector = "input[name='" + $.escapeSelector( name ) + "']"; var nameSelector = "input[name='" + CSS.escape( name ) + "']";
if ( !name ) { if ( !name ) {
return $( [] ); return $( [] );

View File

@ -415,7 +415,7 @@ return $.widget( "ui.selectmenu", [ $.ui.formResetMixin, {
} }
if ( !$( event.target ).closest( ".ui-selectmenu-menu, #" + if ( !$( event.target ).closest( ".ui-selectmenu-menu, #" +
$.escapeSelector( this.ids.button ) ).length ) { CSS.escape( this.ids.button ) ).length ) {
this.close( event ); this.close( event );
} }
} }

View File

@ -722,7 +722,7 @@ $.widget( "ui.tabs", {
// meta-function to give users option to provide a href string instead of a numerical index. // meta-function to give users option to provide a href string instead of a numerical index.
if ( typeof index === "string" ) { if ( typeof index === "string" ) {
index = this.anchors.index( this.anchors.filter( "[href$='" + index = this.anchors.index( this.anchors.filter( "[href$='" +
$.escapeSelector( index ) + "']" ) ); CSS.escape( index ) + "']" ) );
} }
return index; return index;