aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.eslintignore4
-rw-r--r--.eslintrc.json21
-rw-r--r--.github/workflows/codeql-analysis.yml6
-rw-r--r--.github/workflows/filestash.yml6
-rw-r--r--.github/workflows/node.js.yml61
-rw-r--r--Gruntfile.js16
-rw-r--r--bower.json2
-rw-r--r--build/tasks/minify.js53
-rw-r--r--demos/.eslintrc.json9
-rw-r--r--demos/effect/easing.html6
-rw-r--r--demos/spinner/currency.html2
-rw-r--r--demos/spinner/decimal.html2
-rw-r--r--demos/spinner/default.html2
-rw-r--r--demos/spinner/latlong.html2
-rw-r--r--demos/spinner/overflow.html2
-rw-r--r--demos/spinner/time.html2
-rw-r--r--eslint.config.mjs162
-rw-r--r--external/jquery-mousewheel/LICENSE.txt18
-rw-r--r--external/jquery-mousewheel/jquery.mousewheel.js181
-rw-r--r--jtr-git.yml38
-rw-r--r--jtr-stable.yml40
-rw-r--r--jtr.yml30
-rw-r--r--package.json22
-rw-r--r--tests/.eslintrc.json26
-rw-r--r--tests/lib/helper.js59
-rw-r--r--tests/lib/qunit.js25
-rw-r--r--tests/lib/testIframe.js7
-rw-r--r--tests/runner/.eslintrc.json38
-rw-r--r--tests/runner/browsers.js242
-rw-r--r--tests/runner/browserstack/api.js332
-rw-r--r--tests/runner/browserstack/buildBrowserFromString.js20
-rw-r--r--tests/runner/browserstack/createAuthHeader.js7
-rw-r--r--tests/runner/browserstack/local.js34
-rw-r--r--tests/runner/command.js140
-rw-r--r--tests/runner/createTestServer.js66
-rw-r--r--tests/runner/flags/browsers.js24
-rw-r--r--tests/runner/flags/jquery.js14
-rw-r--r--tests/runner/flags/suites.js27
-rw-r--r--tests/runner/lib/buildTestUrl.js24
-rw-r--r--tests/runner/lib/generateHash.js10
-rw-r--r--tests/runner/lib/getBrowserString.js48
-rw-r--r--tests/runner/lib/prettyMs.js18
-rw-r--r--tests/runner/listeners.js112
-rw-r--r--tests/runner/package.json3
-rw-r--r--tests/runner/queue.js119
-rw-r--r--tests/runner/reporter.js134
-rw-r--r--tests/runner/run.js338
-rw-r--r--tests/runner/selenium/createDriver.js84
-rw-r--r--tests/runner/server.js13
-rw-r--r--tests/unit/spinner/core.js56
-rw-r--r--tests/unit/spinner/mousewheel-wheel.html72
-rw-r--r--tests/unit/tabs/core.js77
-rw-r--r--tests/unit/tabs/tabs.html29
-rw-r--r--ui/.eslintrc.json42
-rw-r--r--ui/effect.js4
-rw-r--r--ui/effects/effect-explode.js2
-rw-r--r--ui/widgets/accordion.js2
-rw-r--r--ui/widgets/datepicker.js10
-rw-r--r--ui/widgets/progressbar.js2
-rw-r--r--ui/widgets/resizable.js2
-rw-r--r--ui/widgets/selectmenu.js2
-rw-r--r--ui/widgets/spinner.js26
-rw-r--r--ui/widgets/tabs.js69
63 files changed, 893 insertions, 2153 deletions
diff --git a/.eslintignore b/.eslintignore
deleted file mode 100644
index 5e992599f..000000000
--- a/.eslintignore
+++ /dev/null
@@ -1,4 +0,0 @@
-dist/**/*
-external/**/*
-tests/lib/vendor/**/*
-ui/vendor/**/*
diff --git a/.eslintrc.json b/.eslintrc.json
deleted file mode 100644
index e7d67eb0e..000000000
--- a/.eslintrc.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "root": true,
-
- "extends": "jquery",
-
- // Uncomment to find useless comment disable directives
- // "reportUnusedDisableDirectives": true,
-
- "parserOptions": {
- "ecmaVersion": 2018
- },
-
- "env": {
- "es6": true,
- "node": true
- },
-
- "rules": {
- "strict": [ "error", "global" ]
- }
-}
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index cb990610b..8c2f79b35 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -33,7 +33,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5
+ uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
# Override language selection by uncommenting this and choosing your languages
# with:
# languages: go, javascript, csharp, python, cpp, java
@@ -41,7 +41,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5
+ uses: github/codeql-action/autobuild@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
# â„šī¸ Command-line programs to run using the OS shell.
# 📚 https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -55,4 +55,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5
+ uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
diff --git a/.github/workflows/filestash.yml b/.github/workflows/filestash.yml
index b311ce899..9f0960a4a 100644
--- a/.github/workflows/filestash.yml
+++ b/.github/workflows/filestash.yml
@@ -13,19 +13,19 @@ jobs:
runs-on: ubuntu-latest
environment: filestash
env:
- NODE_VERSION: 20.x
+ NODE_VERSION: 22.x
name: Update Filestash
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Use Node.js ${{ env.NODE_VERSION }}
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache
- uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
+ uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ env.NODE_VERSION }}-npm-lock-${{ hashFiles('**/package-lock.json') }}
diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml
index caac8c047..cccd059d2 100644
--- a/.github/workflows/node.js.yml
+++ b/.github/workflows/node.js.yml
@@ -12,21 +12,21 @@ permissions:
contents: read
env:
- NODE_VERSION: 20.x
+ NODE_VERSION: 22.x
jobs:
build-and-test:
runs-on: ubuntu-latest
name: |
- ${{ matrix.BROWSER }} | ${{ matrix.JQUERYS.name }}
+ ${{ matrix.BROWSER }} | ${{ matrix.CONFIGS.name }}
strategy:
fail-fast: false
matrix:
BROWSER: [chrome, firefox]
- JQUERYS:
- - versions: --jquery git --jquery 3.x-git
+ CONFIGS:
+ - config: jtr-git.yml
name: jQuery git
- - versions: --jquery 3.7.1 --jquery 3.6.4 --jquery 2.2.4 --jquery 1.12.4
+ - config: jtr-stable.yml
name: jQuery stable
steps:
@@ -34,12 +34,12 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Use Node.js ${{ env.NODE_VERSION }}
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache
- uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
+ uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ env.NODE_VERSION }}-npm-lock-${{ hashFiles('**/package-lock.json') }}
@@ -49,41 +49,42 @@ jobs:
- name: Install npm dependencies
run: npm install
- - name: Lint
- run: npm run lint
-
- name: Build
run: npm run build
+ # Lint must happen after build as we lint generated files.
+ - name: Lint
+ run: npm run lint
+
- name: Test
run: |
- npm run test:unit -- -h -b ${{ matrix.BROWSER }} \
- ${{ matrix.JQUERYS.versions }} \
- --retries 3 --hard-retries 1
+ npm run test:unit -- \
+ --headless -b ${{ matrix.BROWSER }} \
+ -c ${{ matrix.CONFIGS.config }}
edge:
runs-on: windows-latest
name: |
- edge | ${{ matrix.JQUERYS.name }}
+ edge | ${{ matrix.CONFIGS.name }}
strategy:
fail-fast: false
matrix:
- JQUERYS:
- - versions: --jquery git --jquery 3.x-git
+ CONFIGS:
+ - config: jtr-git.yml
name: jQuery git
- - versions: --jquery 3.7.1 --jquery 3.6.4 --jquery 2.2.4 --jquery 1.12.4
+ - config: jtr-stable.yml
name: jQuery stable
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Use Node.js ${{ env.NODE_VERSION }}
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache
- uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
+ uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ env.NODE_VERSION }}-npm-lock-${{ hashFiles('**/package-lock.json') }}
@@ -97,34 +98,31 @@ jobs:
run: npm run build
- name: Test
- run: |
- npm run test:unit -- -h -b edge `
- ${{ matrix.JQUERYS.versions }} `
- --retries 3 --hard-retries 1
+ run: npm run test:unit -- -- --headless -b edge -c ${{ matrix.CONFIGS.config }}
safari:
runs-on: macos-latest
name: |
- safari | ${{ matrix.JQUERYS.name }}
+ safari | ${{ matrix.CONFIGS.name }}
strategy:
fail-fast: false
matrix:
- JQUERYS:
- - versions: --jquery git --jquery 3.x-git
+ CONFIGS:
+ - config: jtr-git.yml
name: jQuery git
- - versions: --jquery 3.7.1 --jquery 3.6.4 --jquery 2.2.4 --jquery 1.12.4
+ - config: jtr-stable.yml
name: jQuery stable
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Use Node.js ${{ env.NODE_VERSION }}
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache
- uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
+ uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ env.NODE_VERSION }}-npm-lock-${{ hashFiles('**/package-lock.json') }}
@@ -138,7 +136,4 @@ jobs:
run: npm run build
- name: Test
- run: |
- npm run test:unit -- -b safari \
- ${{ matrix.JQUERYS.versions }} \
- --retries 3 --hard-retries 1
+ run: npm run test:unit -- -b safari -c ${{ matrix.CONFIGS.config }}
diff --git a/Gruntfile.js b/Gruntfile.js
index 334e4bb1a..bbb71d33e 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -51,9 +51,6 @@ const cssFiles = [
// minified files
const minify = {
- options: {
- preserveComments: false
- },
main: {
options: {
banner: createBanner( uiFiles )
@@ -174,7 +171,7 @@ grunt.initConfig( {
}
},
- uglify: minify,
+ minify,
htmllint: {
good: {
options: {
@@ -207,9 +204,12 @@ grunt.initConfig( {
"ui/**/*.js",
"!ui/vendor/**/*.js",
"Gruntfile.js",
+ "dist/jquery-ui.js",
+ "dist/jquery-ui.min.js",
"build/**/*.js",
"tests/unit/**/*.js",
"tests/lib/**/*.js",
+ "!tests/lib/vendor/**/*.js",
"demos/**/*.js"
]
},
@@ -403,9 +403,9 @@ grunt.registerTask( "lint", [
"csslint",
"htmllint"
] );
-grunt.registerTask( "build", [ "requirejs", "concat" ] );
-grunt.registerTask( "default", [ "lint", "build" ] );
-grunt.registerTask( "sizer", [ "requirejs:js", "uglify:main", "compare_size:all" ] );
-grunt.registerTask( "sizer_all", [ "requirejs:js", "uglify", "compare_size" ] );
+grunt.registerTask( "build", [ "requirejs", "concat", "minify:main" ] );
+grunt.registerTask( "default", [ "build", "lint" ] );
+grunt.registerTask( "sizer", [ "requirejs:js", "minify:main", "compare_size:all" ] );
+grunt.registerTask( "sizer_all", [ "requirejs:js", "minify", "compare_size" ] );
};
diff --git a/bower.json b/bower.json
index eec454dea..eb3187e0c 100644
--- a/bower.json
+++ b/bower.json
@@ -13,7 +13,7 @@
},
"devDependencies": {
"jquery-color": "3.0.0",
- "jquery-mousewheel": "3.1.12",
+ "jquery-mousewheel": "3.2.2",
"jquery-simulate": "1.1.1",
"qunit": "2.19.4",
"requirejs": "2.1.14",
diff --git a/build/tasks/minify.js b/build/tasks/minify.js
new file mode 100644
index 000000000..6d83831ee
--- /dev/null
+++ b/build/tasks/minify.js
@@ -0,0 +1,53 @@
+"use strict";
+
+const swc = require( "@swc/core" );
+
+module.exports = function( grunt ) {
+
+grunt.registerMultiTask( "minify", async function() {
+ const done = this.async();
+ const options = this.options();
+
+ for ( const file of this.files ) {
+ if ( file.src.length === 0 ) {
+ grunt.log.writeln(
+ `No source file found, skipping minification to "${ file.dest }".` );
+ continue;
+ }
+ if ( file.src.length !== 1 ) {
+ grunt.fail.warn( "Minifying multiple source files into one " +
+ "destination file not supported" );
+ }
+
+ const contents = grunt.file.read( file.src[ 0 ] );
+
+ const { code } = await swc.minify(
+ contents,
+ {
+ compress: {
+ ecma: 5,
+ hoist_funs: false,
+ loops: false
+ },
+ format: {
+ ecma: 5,
+ asciiOnly: true,
+ comments: false,
+ preamble: options.banner
+ },
+ inlineSourcesContent: false,
+ mangle: true,
+ module: false,
+ sourceMap: false
+ }
+ );
+
+ grunt.file.write( file.dest, code );
+
+ grunt.log.writeln( `File ${ file.dest } created.` );
+ }
+
+ done();
+} );
+
+};
diff --git a/demos/.eslintrc.json b/demos/.eslintrc.json
deleted file mode 100644
index 805ec8eb2..000000000
--- a/demos/.eslintrc.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "root": true,
-
- "extends": "../ui/.eslintrc.json",
-
- "globals": {
- "require": true
- }
-}
diff --git a/demos/effect/easing.html b/demos/effect/easing.html
index 4bee1c41f..3014f7620 100644
--- a/demos/effect/easing.html
+++ b/demos/effect/easing.html
@@ -26,8 +26,12 @@
height = 100;
$.each( $.easing, function( name, impl ) {
+ // Skip _default property
+ if ( typeof impl !== "function" ) {
+ return;
+ }
var graph = $( "<div>" ).addClass( "graph" ).appendTo( "#graphs" ),
- text = $( "<div>" ).text( ++i + ". " + name ).appendTo( graph ),
+ text = $( "<div>" ).text( name ).css({ fontSize: "13px", textAlign: "center", whiteSpace: "nowrap" }).appendTo( graph ),
wrap = $( "<div>" ).appendTo( graph ).css( 'overflow', 'hidden' ),
canvas = $( "<canvas>" ).appendTo( wrap )[ 0 ];
diff --git a/demos/spinner/currency.html b/demos/spinner/currency.html
index 4180b12e1..fa3744ba5 100644
--- a/demos/spinner/currency.html
+++ b/demos/spinner/currency.html
@@ -7,7 +7,7 @@
<link rel="stylesheet" href="../../themes/base/all.css">
<link rel="stylesheet" href="../demos.css">
<script src="../../external/requirejs/require.js"></script>
- <script src="../bootstrap.js" data-modules="external/globalize/globalize external/globalize/globalize.culture.de-DE external/globalize/globalize.culture.ja-JP external/jquery-mousewheel/jquery.mousewheel">
+ <script src="../bootstrap.js" data-modules="external/globalize/globalize external/globalize/globalize.culture.de-DE external/globalize/globalize.culture.ja-JP">
$( "#currency" ).on( "change", function() {
$( "#spinner" ).spinner( "option", "culture", $( this ).val() );
});
diff --git a/demos/spinner/decimal.html b/demos/spinner/decimal.html
index 0b39f2bac..af86bbbe7 100644
--- a/demos/spinner/decimal.html
+++ b/demos/spinner/decimal.html
@@ -7,7 +7,7 @@
<link rel="stylesheet" href="../../themes/base/all.css">
<link rel="stylesheet" href="../demos.css">
<script src="../../external/requirejs/require.js"></script>
- <script src="../bootstrap.js" data-modules="external/globalize/globalize external/globalize/globalize.culture.de-DE external/globalize/globalize.culture.ja-JP external/jquery-mousewheel/jquery.mousewheel">
+ <script src="../bootstrap.js" data-modules="external/globalize/globalize external/globalize/globalize.culture.de-DE external/globalize/globalize.culture.ja-JP">
$( "#spinner" ).spinner({
step: 0.01,
numberFormat: "n"
diff --git a/demos/spinner/default.html b/demos/spinner/default.html
index 39684fb5e..2011b84ba 100644
--- a/demos/spinner/default.html
+++ b/demos/spinner/default.html
@@ -7,7 +7,7 @@
<link rel="stylesheet" href="../../themes/base/all.css">
<link rel="stylesheet" href="../demos.css">
<script src="../../external/requirejs/require.js"></script>
- <script src="../bootstrap.js" data-modules="external/jquery-mousewheel/jquery.mousewheel">
+ <script src="../bootstrap.js">
var spinner = $( "#spinner" ).spinner();
$( "#disable" ).on( "click", function() {
diff --git a/demos/spinner/latlong.html b/demos/spinner/latlong.html
index 0ee747eee..cdaf65f3c 100644
--- a/demos/spinner/latlong.html
+++ b/demos/spinner/latlong.html
@@ -8,7 +8,7 @@
<link rel="stylesheet" href="../demos.css">
<script src="https://maps.google.com/maps/api/js?sensor=false"></script>
<script src="../../external/requirejs/require.js"></script>
- <script src="../bootstrap.js" data-modules="external/jquery-mousewheel/jquery.mousewheel">
+ <script src="../bootstrap.js">
function latlong() {
return new google.maps.LatLng( $("#lat").val(), $("#lng").val() );
}
diff --git a/demos/spinner/overflow.html b/demos/spinner/overflow.html
index 1def62da0..76e107836 100644
--- a/demos/spinner/overflow.html
+++ b/demos/spinner/overflow.html
@@ -7,7 +7,7 @@
<link rel="stylesheet" href="../../themes/base/all.css">
<link rel="stylesheet" href="../demos.css">
<script src="../../external/requirejs/require.js"></script>
- <script src="../bootstrap.js" data-modules="external/jquery-mousewheel/jquery.mousewheel">
+ <script src="../bootstrap.js">
$( "#spinner" ).spinner({
spin: function( event, ui ) {
if ( ui.value > 10 ) {
diff --git a/demos/spinner/time.html b/demos/spinner/time.html
index 5836784d1..c7ceb7116 100644
--- a/demos/spinner/time.html
+++ b/demos/spinner/time.html
@@ -7,7 +7,7 @@
<link rel="stylesheet" href="../../themes/base/all.css">
<link rel="stylesheet" href="../demos.css">
<script src="../../external/requirejs/require.js"></script>
- <script src="../bootstrap.js" data-modules="external/globalize/globalize external/globalize/globalize.culture.de-DE external/jquery-mousewheel/jquery.mousewheel">
+ <script src="../bootstrap.js" data-modules="external/globalize/globalize external/globalize/globalize.culture.de-DE">
$.widget( "ui.timespinner", $.ui.spinner, {
options: {
// seconds
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 000000000..4fb03f6b3
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,162 @@
+import jqueryConfig from "eslint-config-jquery";
+import globals from "globals";
+
+export default [
+ {
+ ignores: [
+ "dist/**/*",
+ "!dist/jquery-ui.js",
+ "!dist/jquery-ui.min.js",
+ "external/**/*",
+ "tests/lib/vendor/**/*",
+ "ui/vendor/**/*"
+ ]
+ },
+
+ {
+ ignores: [ "dist/**/*" ],
+ rules: {
+ ...jqueryConfig.rules,
+ "no-unused-vars": [
+ "error",
+ {
+ argsIgnorePattern: "^_",
+ caughtErrorsIgnorePattern: "^_"
+ }
+ ]
+ }
+ },
+
+ {
+ files: [ "Gruntfile.js" ],
+ languageOptions: {
+ ecmaVersion: "latest",
+ sourceType: "commonjs",
+ globals: {
+ ...globals.node
+ }
+ },
+ rules: {
+ strict: [ "error", "global" ]
+ }
+ },
+
+ {
+ files: [ "eslint.config.mjs" ],
+ languageOptions: {
+ ecmaVersion: "latest",
+ sourceType: "module",
+ globals: {
+ ...globals.node
+ }
+ },
+ rules: {
+ strict: [ "error", "global" ]
+ }
+ },
+
+ // Source, demos
+ {
+ files: [ "ui/**/*.js", "demos/**/*.js" ],
+ languageOptions: {
+ ecmaVersion: 5,
+ sourceType: "script",
+ globals: {
+ ...globals.browser,
+ ...globals.jquery,
+ define: false,
+ Globalize: false
+ }
+ },
+ rules: {
+ strict: [ "error", "function" ],
+
+ // The following rule is relaxed due to too many violations:
+ "no-unused-vars": [
+ "error",
+ {
+ args: "after-used",
+ argsIgnorePattern: "^_",
+ caughtErrorsIgnorePattern: "^_"
+ }
+ ],
+
+ // Too many violations:
+ camelcase: "off",
+ "no-nested-ternary": "off"
+ }
+ },
+ {
+ files: [ "ui/i18n/**/*.js" ],
+ rules: {
+
+ // We want to keep all the strings in separate single lines
+ "max-len": "off"
+ }
+ },
+
+ // Dist files
+ // For dist files, we don't include any jQuery rules on purpose.
+ // We just want to make sure the files are correct ES5.
+ {
+ files: [ "dist/jquery-ui.js", "dist/jquery-ui.min.js" ],
+ languageOptions: {
+ ecmaVersion: 5,
+ sourceType: "script"
+ },
+ linterOptions: {
+ reportUnusedDisableDirectives: "off"
+ }
+ },
+
+ // Build
+ {
+ files: [ "build/**/*.js" ],
+ languageOptions: {
+ ecmaVersion: "latest",
+ sourceType: "commonjs",
+ globals: {
+ ...globals.node
+ }
+ },
+ rules: {
+ "no-implicit-globals": "error",
+ strict: [ "error", "global" ]
+ }
+ },
+
+ // Demos
+ {
+ files: [ "demos/**/*.js" ],
+ languageOptions: {
+ globals: {
+ require: true
+ }
+ }
+ },
+
+ // Tests
+ {
+ files: [ "tests/**/*.js" ],
+ languageOptions: {
+ ecmaVersion: 5,
+ sourceType: "script",
+ globals: {
+ ...globals.browser,
+ ...globals.jquery,
+ define: false,
+ Globalize: false,
+ QUnit: false,
+ require: true,
+ requirejs: true
+ }
+ },
+ "rules": {
+
+ // Too many violations:
+ "max-len": "off",
+ "no-unused-vars": "off",
+ strict: "off" // ideally, `[ "error", "function" ]`
+ }
+ }
+];
diff --git a/external/jquery-mousewheel/LICENSE.txt b/external/jquery-mousewheel/LICENSE.txt
index ad2020df4..f56b79ae0 100644
--- a/external/jquery-mousewheel/LICENSE.txt
+++ b/external/jquery-mousewheel/LICENSE.txt
@@ -1,4 +1,13 @@
-Copyright (c) 2013, Brandon Aaron (http://brandon.aaron.sh)
+Copyright OpenJS Foundation and other contributors, https://openjsf.org/
+
+This software consists of voluntary contributions made by many
+individuals. For exact contribution history, see the revision history
+available at https://github.com/jquery/jquery-mousewheel
+
+The following license applies to all parts of this software except as
+documented below:
+
+====
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
@@ -18,3 +27,10 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+====
+
+All files located in the node_modules and external directories are
+externally maintained libraries used by this software which have their
+own licenses; we recommend you read them, as their terms may differ from
+the terms above.
diff --git a/external/jquery-mousewheel/jquery.mousewheel.js b/external/jquery-mousewheel/jquery.mousewheel.js
index 6756fa610..aec55baf8 100644
--- a/external/jquery-mousewheel/jquery.mousewheel.js
+++ b/external/jquery-mousewheel/jquery.mousewheel.js
@@ -1,76 +1,83 @@
-/*! Copyright (c) 2013 Brandon Aaron (http://brandon.aaron.sh)
- * Licensed under the MIT License (LICENSE.txt).
- *
- * Version: 3.1.12
- *
- * Requires: jQuery 1.2.2+
+/*!
+ * jQuery Mousewheel 3.2.2
+ * Copyright OpenJS Foundation and other contributors
*/
-(function (factory) {
- if ( typeof define === 'function' && define.amd ) {
+( function( factory ) {
+ "use strict";
+
+ if ( typeof define === "function" && define.amd ) {
+
// AMD. Register as an anonymous module.
- define(['jquery'], factory);
- } else if (typeof exports === 'object') {
+ define( [ "jquery" ], factory );
+ } else if ( typeof exports === "object" ) {
+
// Node/CommonJS style for Browserify
module.exports = factory;
} else {
+
// Browser globals
- factory(jQuery);
+ factory( jQuery );
}
-}(function ($) {
+} )( function( $ ) {
+ "use strict";
- var toFix = ['wheel', 'mousewheel', 'DOMMouseScroll', 'MozMousePixelScroll'],
- toBind = ( 'onwheel' in document || document.documentMode >= 9 ) ?
- ['wheel'] : ['mousewheel', 'DomMouseScroll', 'MozMousePixelScroll'],
- slice = Array.prototype.slice,
- nullLowestDeltaTimeout, lowestDelta;
+ var nullLowestDeltaTimeout, lowestDelta,
+ modernEvents = !!$.fn.on,
+ toFix = [ "wheel", "mousewheel", "DOMMouseScroll", "MozMousePixelScroll" ],
+ toBind = ( "onwheel" in window.document || window.document.documentMode >= 9 ) ?
+ [ "wheel" ] : [ "mousewheel", "DomMouseScroll", "MozMousePixelScroll" ],
+ slice = Array.prototype.slice;
if ( $.event.fixHooks ) {
for ( var i = toFix.length; i; ) {
- $.event.fixHooks[ toFix[--i] ] = $.event.mouseHooks;
+ $.event.fixHooks[ toFix[ --i ] ] = $.event.mouseHooks;
}
}
var special = $.event.special.mousewheel = {
- version: '3.1.12',
+ version: "3.2.2",
setup: function() {
if ( this.addEventListener ) {
for ( var i = toBind.length; i; ) {
- this.addEventListener( toBind[--i], handler, false );
+ this.addEventListener( toBind[ --i ], handler, false );
}
} else {
this.onmousewheel = handler;
}
+
// Store the line height and page height for this particular element
- $.data(this, 'mousewheel-line-height', special.getLineHeight(this));
- $.data(this, 'mousewheel-page-height', special.getPageHeight(this));
+ $.data( this, "mousewheel-line-height", special.getLineHeight( this ) );
+ $.data( this, "mousewheel-page-height", special.getPageHeight( this ) );
},
teardown: function() {
if ( this.removeEventListener ) {
for ( var i = toBind.length; i; ) {
- this.removeEventListener( toBind[--i], handler, false );
+ this.removeEventListener( toBind[ --i ], handler, false );
}
} else {
this.onmousewheel = null;
}
+
// Clean up the data we added to the element
- $.removeData(this, 'mousewheel-line-height');
- $.removeData(this, 'mousewheel-page-height');
+ $.removeData( this, "mousewheel-line-height" );
+ $.removeData( this, "mousewheel-page-height" );
},
- getLineHeight: function(elem) {
- var $elem = $(elem),
- $parent = $elem['offsetParent' in $.fn ? 'offsetParent' : 'parent']();
- if (!$parent.length) {
- $parent = $('body');
+ getLineHeight: function( elem ) {
+ var $elem = $( elem ),
+ $parent = $elem[ "offsetParent" in $.fn ? "offsetParent" : "parent" ]();
+ if ( !$parent.length ) {
+ $parent = $( "body" );
}
- return parseInt($parent.css('fontSize'), 10) || parseInt($elem.css('fontSize'), 10) || 16;
+ return parseInt( $parent.css( "fontSize" ), 10 ) ||
+ parseInt( $elem.css( "fontSize" ), 10 ) || 16;
},
- getPageHeight: function(elem) {
- return $(elem).height();
+ getPageHeight: function( elem ) {
+ return $( elem ).height();
},
settings: {
@@ -79,56 +86,68 @@
}
};
- $.fn.extend({
- mousewheel: function(fn) {
- return fn ? this.bind('mousewheel', fn) : this.trigger('mousewheel');
+ $.fn.extend( {
+ mousewheel: function( fn ) {
+ return fn ?
+ this[ modernEvents ? "on" : "bind" ]( "mousewheel", fn ) :
+ this.trigger( "mousewheel" );
},
- unmousewheel: function(fn) {
- return this.unbind('mousewheel', fn);
+ unmousewheel: function( fn ) {
+ return this[ modernEvents ? "off" : "unbind" ]( "mousewheel", fn );
}
- });
+ } );
- function handler(event) {
+ function handler( event ) {
var orgEvent = event || window.event,
- args = slice.call(arguments, 1),
+ args = slice.call( arguments, 1 ),
delta = 0,
deltaX = 0,
deltaY = 0,
- absDelta = 0,
- offsetX = 0,
- offsetY = 0;
- event = $.event.fix(orgEvent);
- event.type = 'mousewheel';
+ absDelta = 0;
+ event = $.event.fix( orgEvent );
+ event.type = "mousewheel";
// Old school scrollwheel delta
- if ( 'detail' in orgEvent ) { deltaY = orgEvent.detail * -1; }
- if ( 'wheelDelta' in orgEvent ) { deltaY = orgEvent.wheelDelta; }
- if ( 'wheelDeltaY' in orgEvent ) { deltaY = orgEvent.wheelDeltaY; }
- if ( 'wheelDeltaX' in orgEvent ) { deltaX = orgEvent.wheelDeltaX * -1; }
+ if ( "detail" in orgEvent ) {
+ deltaY = orgEvent.detail * -1;
+ }
+ if ( "wheelDelta" in orgEvent ) {
+ deltaY = orgEvent.wheelDelta;
+ }
+ if ( "wheelDeltaY" in orgEvent ) {
+ deltaY = orgEvent.wheelDeltaY;
+ }
+ if ( "wheelDeltaX" in orgEvent ) {
+ deltaX = orgEvent.wheelDeltaX * -1;
+ }
// Firefox < 17 horizontal scrolling related to DOMMouseScroll event
- if ( 'axis' in orgEvent && orgEvent.axis === orgEvent.HORIZONTAL_AXIS ) {
+ if ( "axis" in orgEvent && orgEvent.axis === orgEvent.HORIZONTAL_AXIS ) {
deltaX = deltaY * -1;
deltaY = 0;
}
- // Set delta to be deltaY or deltaX if deltaY is 0 for backwards compatabilitiy
+ // Set delta to be deltaY or deltaX if deltaY is 0 for backwards compatability
delta = deltaY === 0 ? deltaX : deltaY;
// New school wheel delta (wheel event)
- if ( 'deltaY' in orgEvent ) {
+ if ( "deltaY" in orgEvent ) {
deltaY = orgEvent.deltaY * -1;
delta = deltaY;
}
- if ( 'deltaX' in orgEvent ) {
+ if ( "deltaX" in orgEvent ) {
deltaX = orgEvent.deltaX;
- if ( deltaY === 0 ) { delta = deltaX * -1; }
+ if ( deltaY === 0 ) {
+ delta = deltaX * -1;
+ }
}
// No change actually happened, no reason to go any further
- if ( deltaY === 0 && deltaX === 0 ) { return; }
+ if ( deltaY === 0 && deltaX === 0 ) {
+ return;
+ }
// Need to convert lines and pages to pixels if we aren't already in pixels
// There are three delta modes:
@@ -136,31 +155,32 @@
// * deltaMode 1 is by lines
// * deltaMode 2 is by pages
if ( orgEvent.deltaMode === 1 ) {
- var lineHeight = $.data(this, 'mousewheel-line-height');
+ var lineHeight = $.data( this, "mousewheel-line-height" );
delta *= lineHeight;
deltaY *= lineHeight;
deltaX *= lineHeight;
} else if ( orgEvent.deltaMode === 2 ) {
- var pageHeight = $.data(this, 'mousewheel-page-height');
+ var pageHeight = $.data( this, "mousewheel-page-height" );
delta *= pageHeight;
deltaY *= pageHeight;
deltaX *= pageHeight;
}
// Store lowest absolute delta to normalize the delta values
- absDelta = Math.max( Math.abs(deltaY), Math.abs(deltaX) );
+ absDelta = Math.max( Math.abs( deltaY ), Math.abs( deltaX ) );
if ( !lowestDelta || absDelta < lowestDelta ) {
lowestDelta = absDelta;
// Adjust older deltas if necessary
- if ( shouldAdjustOldDeltas(orgEvent, absDelta) ) {
+ if ( shouldAdjustOldDeltas( orgEvent, absDelta ) ) {
lowestDelta /= 40;
}
}
// Adjust older deltas if necessary
- if ( shouldAdjustOldDeltas(orgEvent, absDelta) ) {
+ if ( shouldAdjustOldDeltas( orgEvent, absDelta ) ) {
+
// Divide all the things by 40!
delta /= 40;
deltaX /= 40;
@@ -168,54 +188,55 @@
}
// Get a whole, normalized value for the deltas
- delta = Math[ delta >= 1 ? 'floor' : 'ceil' ](delta / lowestDelta);
- deltaX = Math[ deltaX >= 1 ? 'floor' : 'ceil' ](deltaX / lowestDelta);
- deltaY = Math[ deltaY >= 1 ? 'floor' : 'ceil' ](deltaY / lowestDelta);
+ delta = Math[ delta >= 1 ? "floor" : "ceil" ]( delta / lowestDelta );
+ deltaX = Math[ deltaX >= 1 ? "floor" : "ceil" ]( deltaX / lowestDelta );
+ deltaY = Math[ deltaY >= 1 ? "floor" : "ceil" ]( deltaY / lowestDelta );
// Normalise offsetX and offsetY properties
if ( special.settings.normalizeOffset && this.getBoundingClientRect ) {
var boundingRect = this.getBoundingClientRect();
- offsetX = event.clientX - boundingRect.left;
- offsetY = event.clientY - boundingRect.top;
+ event.offsetX = event.clientX - boundingRect.left;
+ event.offsetY = event.clientY - boundingRect.top;
}
// Add information to the event object
event.deltaX = deltaX;
event.deltaY = deltaY;
event.deltaFactor = lowestDelta;
- event.offsetX = offsetX;
- event.offsetY = offsetY;
+
// Go ahead and set deltaMode to 0 since we converted to pixels
// Although this is a little odd since we overwrite the deltaX/Y
// properties with normalized deltas.
event.deltaMode = 0;
// Add event and delta to the front of the arguments
- args.unshift(event, delta, deltaX, deltaY);
+ args.unshift( event, delta, deltaX, deltaY );
- // Clearout lowestDelta after sometime to better
+ // Clear out lowestDelta after sometime to better
// handle multiple device types that give different
// a different lowestDelta
// Ex: trackpad = 3 and mouse wheel = 120
- if (nullLowestDeltaTimeout) { clearTimeout(nullLowestDeltaTimeout); }
- nullLowestDeltaTimeout = setTimeout(nullLowestDelta, 200);
+ if ( nullLowestDeltaTimeout ) {
+ window.clearTimeout( nullLowestDeltaTimeout );
+ }
+ nullLowestDeltaTimeout = window.setTimeout( function() {
+ lowestDelta = null;
+ }, 200 );
- return ($.event.dispatch || $.event.handle).apply(this, args);
+ return ( $.event.dispatch || $.event.handle ).apply( this, args );
}
- function nullLowestDelta() {
- lowestDelta = null;
- }
+ function shouldAdjustOldDeltas( orgEvent, absDelta ) {
- function shouldAdjustOldDeltas(orgEvent, absDelta) {
- // If this is an older event and the delta is divisable by 120,
+ // If this is an older event and the delta is divisible by 120,
// then we are assuming that the browser is treating this as an
// older mouse wheel event and that we should divide the deltas
// by 40 to try and get a more usable deltaFactor.
// Side note, this actually impacts the reported scroll distance
// in older browsers and can cause scrolling to be slower than native.
// Turn this off by setting $.event.special.mousewheel.settings.adjustOldDeltas to false.
- return special.settings.adjustOldDeltas && orgEvent.type === 'mousewheel' && absDelta % 120 === 0;
+ return special.settings.adjustOldDeltas && orgEvent.type === "mousewheel" &&
+ absDelta % 120 === 0;
}
-}));
+} );
diff --git a/jtr-git.yml b/jtr-git.yml
new file mode 100644
index 000000000..0fc81ddd9
--- /dev/null
+++ b/jtr-git.yml
@@ -0,0 +1,38 @@
+version: 1
+
+base-url: /tests/unit/
+
+test-urls:
+ - accordion/accordion.html
+ - autocomplete/autocomplete.html
+ - button/button.html
+ - checkboxradio/checkboxradio.html
+ - controlgroup/controlgroup.html
+ - core/core.html
+ - datepicker/datepicker.html
+ - dialog/dialog.html
+ - draggable/draggable.html
+ - 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
+ - resizable/resizable.html
+ - selectable/selectable.html
+ - selectmenu/selectmenu.html
+ - slider/slider.html
+ - sortable/sortable.html
+ - spinner/spinner.html
+ - tabs/tabs.html
+ - tooltip/tooltip.html
+ - widget/widget.html
+
+runs:
+ jquery:
+ - git
+ - 3.x-git
+
+retries: 2
+hard-retries: 1
diff --git a/jtr-stable.yml b/jtr-stable.yml
new file mode 100644
index 000000000..1c6c27a10
--- /dev/null
+++ b/jtr-stable.yml
@@ -0,0 +1,40 @@
+version: 1
+
+base-url: /tests/unit/
+
+test-urls:
+ - accordion/accordion.html
+ - autocomplete/autocomplete.html
+ - button/button.html
+ - checkboxradio/checkboxradio.html
+ - controlgroup/controlgroup.html
+ - core/core.html
+ - datepicker/datepicker.html
+ - dialog/dialog.html
+ - draggable/draggable.html
+ - 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
+ - resizable/resizable.html
+ - selectable/selectable.html
+ - selectmenu/selectmenu.html
+ - slider/slider.html
+ - sortable/sortable.html
+ - spinner/spinner.html
+ - tabs/tabs.html
+ - tooltip/tooltip.html
+ - widget/widget.html
+
+runs:
+ jquery:
+ - 3.7.1
+ - 3.6.4
+ - 2.2.4
+ - 1.12.4
+
+retries: 2
+hard-retries: 1
diff --git a/jtr.yml b/jtr.yml
new file mode 100644
index 000000000..70d30ade8
--- /dev/null
+++ b/jtr.yml
@@ -0,0 +1,30 @@
+version: 1
+
+base-url: /tests/unit/
+
+test-urls:
+ - accordion/accordion.html
+ - autocomplete/autocomplete.html
+ - button/button.html
+ - checkboxradio/checkboxradio.html
+ - controlgroup/controlgroup.html
+ - core/core.html
+ - datepicker/datepicker.html
+ - dialog/dialog.html
+ - draggable/draggable.html
+ - 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
+ - resizable/resizable.html
+ - selectable/selectable.html
+ - selectmenu/selectmenu.html
+ - slider/slider.html
+ - sortable/sortable.html
+ - spinner/spinner.html
+ - tabs/tabs.html
+ - tooltip/tooltip.html
+ - widget/widget.html
diff --git a/package.json b/package.json
index f82758844..00f9e07fe 100644
--- a/package.json
+++ b/package.json
@@ -47,36 +47,30 @@
"scripts": {
"build": "grunt build",
"lint": "grunt lint",
- "test:server": "node tests/runner/server.js",
- "test:unit": "node tests/runner/command.js",
- "test": "grunt && npm run test:unit -- -h"
+ "test:server": "jtr serve",
+ "test:unit": "jtr",
+ "test": "grunt && npm run test:unit -- --headless"
},
"dependencies": {
"jquery": ">=1.12.0 <5.0.0"
},
"devDependencies": {
- "body-parser": "1.20.3",
- "browserstack-local": "1.5.5",
+ "@swc/core": "1.11.5",
"commitplease": "3.2.0",
- "diff": "5.2.0",
"eslint-config-jquery": "3.0.2",
- "exit-hook": "4.0.0",
- "express": "4.21.1",
- "express-body-parser-error-handler": "1.0.7",
+ "globals": "16.0.0",
"grunt": "1.6.1",
"grunt-bowercopy": "1.2.5",
"grunt-compare-size": "0.4.2",
"grunt-contrib-concat": "2.1.0",
"grunt-contrib-csslint": "2.0.0",
"grunt-contrib-requirejs": "1.0.0",
- "grunt-contrib-uglify": "5.2.2",
- "grunt-eslint": "24.0.1",
+ "grunt-eslint": "25.0.0",
"grunt-git-authors": "3.2.0",
"grunt-html": "17.1.0",
+ "jquery-test-runner": "0.2.5",
"load-grunt-tasks": "5.1.0",
- "rimraf": "6.0.1",
- "selenium-webdriver": "4.26.0",
- "yargs": "17.7.2"
+ "rimraf": "6.0.1"
},
"keywords": []
}
diff --git a/tests/.eslintrc.json b/tests/.eslintrc.json
deleted file mode 100644
index 714077182..000000000
--- a/tests/.eslintrc.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- "parserOptions": {
- "ecmaVersion": 5
- },
-
- "env": {
- "browser": true,
- "jquery": true,
- "node": false
- },
-
- "rules": {
- // Too many violations:
- "max-len": "off",
- "no-unused-vars": "off",
- "strict": "off" // ideally, `[ "error", "function" ]`
- },
-
- "globals": {
- "define": false,
- "Globalize": false,
- "QUnit": false,
- "require": true,
- "requirejs": true
- }
-}
diff --git a/tests/lib/helper.js b/tests/lib/helper.js
index 2315c5e19..2be4c48de 100644
--- a/tests/lib/helper.js
+++ b/tests/lib/helper.js
@@ -51,6 +51,65 @@ exports.moduleAfterEach = function( assert ) {
}
};
+exports.testIframe = function( title, fileName, func, wrapper, iframeStyles ) {
+ if ( !wrapper ) {
+ wrapper = QUnit.test;
+ }
+ wrapper.call( QUnit, title, function( assert ) {
+ var done = assert.async(),
+ $iframe = jQuery( "<iframe></iframe>" )
+ .css( {
+ position: "absolute",
+ top: "0",
+ left: "-600px",
+ width: "500px",
+ zIndex: 1,
+ background: "white"
+ } )
+ .attr( { id: "qunit-fixture-iframe", src: fileName } );
+
+ // Add other iframe styles
+ if ( iframeStyles ) {
+ $iframe.css( iframeStyles );
+ }
+
+ // Test iframes are expected to invoke this via startIframeTest
+ // (cf. iframeTest.js)
+ window.iframeCallback = function() {
+ var args = Array.prototype.slice.call( arguments );
+
+ args.unshift( assert );
+
+ setTimeout( function() {
+ var result;
+
+ this.iframeCallback = undefined;
+
+ result = func.apply( this, args );
+
+ function finish() {
+ func = function() {};
+ $iframe.remove();
+ done();
+ }
+
+ // Wait for promises returned by `func`.
+ if ( result && result.then ) {
+ result.then( finish );
+ } else {
+ finish();
+ }
+ } );
+ };
+
+ // Attach iframe to the body for visibility-dependent code.
+ // It will be removed by either the above code, or the testDone
+ // callback in qunit.js.
+ $iframe.prependTo( document.body );
+ } );
+};
+window.iframeCallback = undefined;
+
return exports;
} );
diff --git a/tests/lib/qunit.js b/tests/lib/qunit.js
index cc2f01d79..c4c96ef58 100644
--- a/tests/lib/qunit.js
+++ b/tests/lib/qunit.js
@@ -7,14 +7,14 @@ define( [
], function( QUnit, $ ) {
"use strict";
+var ajaxSettings = $.ajaxSettings;
+
QUnit.config.autostart = false;
QUnit.config.requireExpects = true;
QUnit.config.urlConfig.push( {
id: "jquery",
label: "jQuery version",
-
- // Keep in sync with tests/runner/jquery.js
value: [
"1.12.4",
"2.2.4",
@@ -36,16 +36,21 @@ QUnit.config.urlConfig.push( {
label: "Enable jquery-migrate"
} );
-QUnit.reset = ( function( reset ) {
- return function() {
+QUnit.testDone( function() {
- // Ensure jQuery events and data on the fixture are properly removed
- $( "#qunit-fixture" ).empty();
+ // Ensure jQuery events and data on the fixture are properly removed
+ $( "#qunit-fixture" ).empty();
- // Let QUnit reset the fixture
- reset.apply( this, arguments );
- };
-} )( QUnit.reset );
+ // Remove the iframe fixture
+ $( "#qunit-fixture-iframe" ).remove();
+
+ // Reset internal $ state
+ if ( ajaxSettings ) {
+ $.ajaxSettings = $.extend( true, {}, ajaxSettings );
+ } else {
+ delete $.ajaxSettings;
+ }
+} );
return QUnit;
diff --git a/tests/lib/testIframe.js b/tests/lib/testIframe.js
new file mode 100644
index 000000000..4db56833c
--- /dev/null
+++ b/tests/lib/testIframe.js
@@ -0,0 +1,7 @@
+window.startIframeTest = function() {
+ var args = Array.prototype.slice.call( arguments );
+
+ // Note: jQuery may be undefined if page did not load it
+ args.unshift( window.jQuery, window, document );
+ window.parent.iframeCallback.apply( null, args );
+};
diff --git a/tests/runner/.eslintrc.json b/tests/runner/.eslintrc.json
deleted file mode 100644
index 9ca2e75f6..000000000
--- a/tests/runner/.eslintrc.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
- "root": true,
-
- "extends": "jquery",
-
- "overrides": [
- {
- "files": ["**/*"],
- "env": {
- "es6": true,
- "node": true
- },
- "parserOptions": {
- "ecmaVersion": 2022,
- "sourceType": "module"
- }
- },
- {
- "files": ["./listeners.js"],
- "env": {
- "browser": true,
- "node": false
- },
- "globals": {
- "QUnit": false,
- "Symbol": false,
- "require": false
- },
- "parserOptions": {
- "ecmaVersion": 5,
- "sourceType": "script"
- },
- "rules": {
- "strict": ["error", "function"]
- }
- }
- ]
-}
diff --git a/tests/runner/browsers.js b/tests/runner/browsers.js
deleted file mode 100644
index 1ddccdf78..000000000
--- a/tests/runner/browsers.js
+++ /dev/null
@@ -1,242 +0,0 @@
-import chalk from "chalk";
-import { getBrowserString } from "./lib/getBrowserString.js";
-import {
- createWorker,
- deleteWorker,
- getAvailableSessions
-} from "./browserstack/api.js";
-import createDriver from "./selenium/createDriver.js";
-
-const workers = Object.create( null );
-
-/**
- * Keys are browser strings
- * Structure of a worker:
- * {
- * browser: object // The browser object
- * debug: boolean // Stops the worker from being cleaned up when finished
- * lastTouch: number // The last time a request was received
- * restarts: number // The number of times the worker has been restarted
- * options: object // The options to create the worker
- * url: string // The URL the worker is on
- * quit: function // A function to stop the worker
- * }
- */
-
-// Acknowledge the worker within the time limit.
-// BrowserStack can take much longer spinning up
-// some browsers, such as iOS 15 Safari.
-const ACKNOWLEDGE_INTERVAL = 1000;
-const ACKNOWLEDGE_TIMEOUT = 60 * 1000 * 5;
-
-const MAX_WORKER_RESTARTS = 5;
-
-// No report after the time limit
-// should refresh the worker
-const RUN_WORKER_TIMEOUT = 60 * 1000 * 2;
-
-const WORKER_WAIT_TIME = 30000;
-
-// Limit concurrency to 8 by default in selenium
-const MAX_SELENIUM_CONCURRENCY = 8;
-
-export async function createBrowserWorker( url, browser, options, restarts = 0 ) {
- if ( restarts > MAX_WORKER_RESTARTS ) {
- throw new Error(
- `Reached the maximum number of restarts for ${ chalk.yellow(
- getBrowserString( browser )
- ) }`
- );
- }
- const { browserstack, debug, headless, runId, tunnelId, verbose } = options;
- while ( await maxWorkersReached( options ) ) {
- if ( verbose ) {
- console.log( "\nWaiting for available sessions..." );
- }
- await new Promise( ( resolve ) => setTimeout( resolve, WORKER_WAIT_TIME ) );
- }
-
- const fullBrowser = getBrowserString( browser );
-
- let worker;
-
- if ( browserstack ) {
- worker = await createWorker( {
- ...browser,
- url: encodeURI( url ),
- project: "jquery",
- build: `Run ${ runId }`,
-
- // This is the maximum timeout allowed
- // by BrowserStack. We do this because
- // we control the timeout in the runner.
- // See https://github.com/browserstack/api/blob/b324a6a5bc1b6052510d74e286b8e1c758c308a7/README.md#timeout300
- timeout: 1800,
-
- // Not documented in the API docs,
- // but required to make local testing work.
- // See https://www.browserstack.com/docs/automate/selenium/manage-multiple-connections#nodejs
- "browserstack.local": true,
- "browserstack.localIdentifier": tunnelId
- } );
- worker.quit = () => deleteWorker( worker.id );
- } else {
- const driver = await createDriver( {
- browserName: browser.browser,
- headless,
- url,
- verbose
- } );
- worker = {
- quit: () => driver.quit()
- };
- }
-
- worker.debug = !!debug;
- worker.url = url;
- worker.browser = browser;
- worker.restarts = restarts;
- worker.options = options;
- touchBrowser( browser );
- workers[ fullBrowser ] = worker;
-
- // Wait for the worker to show up in the list
- // before returning it.
- return ensureAcknowledged( worker );
-}
-
-export function touchBrowser( browser ) {
- const fullBrowser = getBrowserString( browser );
- const worker = workers[ fullBrowser ];
- if ( worker ) {
- worker.lastTouch = Date.now();
- }
-}
-
-export async function setBrowserWorkerUrl( browser, url ) {
- const fullBrowser = getBrowserString( browser );
- const worker = workers[ fullBrowser ];
- if ( worker ) {
- worker.url = url;
- }
-}
-
-export async function restartBrowser( browser ) {
- const fullBrowser = getBrowserString( browser );
- const worker = workers[ fullBrowser ];
- if ( worker ) {
- await restartWorker( worker );
- }
-}
-
-/**
- * Checks that all browsers have received
- * a response in the given amount of time.
- * If not, the worker is restarted.
- */
-export async function checkLastTouches() {
- for ( const [ fullBrowser, worker ] of Object.entries( workers ) ) {
- if ( Date.now() - worker.lastTouch > RUN_WORKER_TIMEOUT ) {
- const options = worker.options;
- if ( options.verbose ) {
- console.log(
- `\nNo response from ${ chalk.yellow( fullBrowser ) } in ${
- RUN_WORKER_TIMEOUT / 1000 / 60
- }min.`
- );
- }
- await restartWorker( worker );
- }
- }
-}
-
-export async function cleanupAllBrowsers( { verbose } ) {
- const workersRemaining = Object.values( workers );
- const numRemaining = workersRemaining.length;
- if ( numRemaining ) {
- try {
- await Promise.all( workersRemaining.map( ( worker ) => worker.quit() ) );
- if ( verbose ) {
- console.log(
- `Stopped ${ numRemaining } browser${ numRemaining > 1 ? "s" : "" }.`
- );
- }
- } catch ( error ) {
-
- // Log the error, but do not consider the test run failed
- console.error( error );
- }
- }
-}
-
-async function maxWorkersReached( {
- browserstack,
- concurrency = MAX_SELENIUM_CONCURRENCY
-} ) {
- if ( browserstack ) {
- return ( await getAvailableSessions() ) <= 0;
- }
- return workers.length >= concurrency;
-}
-
-async function waitForAck( worker, { fullBrowser, verbose } ) {
- delete worker.lastTouch;
- return new Promise( ( resolve, reject ) => {
- const interval = setInterval( () => {
- if ( worker.lastTouch ) {
- if ( verbose ) {
- console.log( `\n${ fullBrowser } acknowledged.` );
- }
- clearTimeout( timeout );
- clearInterval( interval );
- resolve();
- }
- }, ACKNOWLEDGE_INTERVAL );
-
- const timeout = setTimeout( () => {
- clearInterval( interval );
- reject(
- new Error(
- `${ fullBrowser } not acknowledged after ${
- ACKNOWLEDGE_TIMEOUT / 1000 / 60
- }min.`
- )
- );
- }, ACKNOWLEDGE_TIMEOUT );
- } );
-}
-
-async function ensureAcknowledged( worker ) {
- const fullBrowser = getBrowserString( worker.browser );
- const verbose = worker.options.verbose;
- try {
- await waitForAck( worker, { fullBrowser, verbose } );
- return worker;
- } catch ( error ) {
- console.error( error.message );
- await restartWorker( worker );
- }
-}
-
-async function cleanupWorker( worker, { verbose } ) {
- for ( const [ fullBrowser, w ] of Object.entries( workers ) ) {
- if ( w === worker ) {
- delete workers[ fullBrowser ];
- await worker.quit();
- if ( verbose ) {
- console.log( `\nStopped ${ fullBrowser }.` );
- }
- return;
- }
- }
-}
-
-async function restartWorker( worker ) {
- await cleanupWorker( worker, worker.options );
- await createBrowserWorker(
- worker.url,
- worker.browser,
- worker.options,
- worker.restarts + 1
- );
-}
diff --git a/tests/runner/browserstack/api.js b/tests/runner/browserstack/api.js
deleted file mode 100644
index 632f90c3b..000000000
--- a/tests/runner/browserstack/api.js
+++ /dev/null
@@ -1,332 +0,0 @@
-/**
- * Browserstack API is documented at
- * https://github.com/browserstack/api
- */
-
-import { createAuthHeader } from "./createAuthHeader.js";
-
-const browserstackApi = "https://api.browserstack.com";
-const apiVersion = 5;
-
-const username = process.env.BROWSERSTACK_USERNAME;
-const accessKey = process.env.BROWSERSTACK_ACCESS_KEY;
-
-// iOS has null for version numbers,
-// and we do not need a similar check for OS versions.
-const rfinalVersion = /(?:^[0-9\.]+$)|(?:^null$)/;
-const rlatest = /^latest-(\d+)$/;
-
-const rnonDigits = /(?:[^\d\.]+)|(?:20\d{2})/g;
-
-async function fetchAPI( path, options = {}, versioned = true ) {
- if ( !username || !accessKey ) {
- throw new Error(
- "BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY environment variables must be set."
- );
- }
- const init = {
- method: "GET",
- ...options,
- headers: {
- authorization: createAuthHeader( username, accessKey ),
- accept: "application/json",
- "content-type": "application/json",
- ...options.headers
- }
- };
- const response = await fetch(
- `${ browserstackApi }/${ versioned ? `${ apiVersion }/` : "" }${ path }`,
- init
- );
- if ( !response.ok ) {
- console.log(
- `\n${ init.method } ${ path }`,
- response.status,
- response.statusText
- );
- throw new Error( `Error fetching ${ path }` );
- }
- return response.json();
-}
-
-/**
- * =============================
- * Browsers API
- * =============================
- */
-
-function compareVersionNumbers( a, b ) {
- if ( a != null && b == null ) {
- return -1;
- }
- if ( a == null && b != null ) {
- return 1;
- }
- if ( a == null && b == null ) {
- return 0;
- }
- const aParts = a.replace( rnonDigits, "" ).split( "." );
- const bParts = b.replace( rnonDigits, "" ).split( "." );
-
- if ( aParts.length > bParts.length ) {
- return -1;
- }
- if ( aParts.length < bParts.length ) {
- return 1;
- }
-
- for ( let i = 0; i < aParts.length; i++ ) {
- const aPart = Number( aParts[ i ] );
- const bPart = Number( bParts[ i ] );
- if ( aPart < bPart ) {
- return -1;
- }
- if ( aPart > bPart ) {
- return 1;
- }
- }
-
- if ( rnonDigits.test( a ) && !rnonDigits.test( b ) ) {
- return -1;
- }
- if ( !rnonDigits.test( a ) && rnonDigits.test( b ) ) {
- return 1;
- }
-
- return 0;
-}
-
-function sortBrowsers( a, b ) {
- if ( a.browser < b.browser ) {
- return -1;
- }
- if ( a.browser > b.browser ) {
- return 1;
- }
- const browserComparison = compareVersionNumbers(
- a.browser_version,
- b.browser_version
- );
- if ( browserComparison ) {
- return browserComparison;
- }
- if ( a.os < b.os ) {
- return -1;
- }
- if ( a.os > b.os ) {
- return 1;
- }
- const osComparison = compareVersionNumbers( a.os_version, b.os_version );
- if ( osComparison ) {
- return osComparison;
- }
- const deviceComparison = compareVersionNumbers( a.device, b.device );
- if ( deviceComparison ) {
- return deviceComparison;
- }
- return 0;
-}
-
-export async function getBrowsers( { flat = false } = {} ) {
- const query = new URLSearchParams();
- if ( flat ) {
- query.append( "flat", true );
- }
- const browsers = await fetchAPI( `/browsers?${ query }` );
- return browsers.sort( sortBrowsers );
-}
-
-function matchVersion( browserVersion, version ) {
- if ( !version ) {
- return false;
- }
- const regex = new RegExp(
- `^${ version.replace( /\\/g, "\\\\" ).replace( /\./g, "\\." ) }\\b`,
- "i"
- );
- return regex.test( browserVersion );
-}
-
-export async function filterBrowsers( filter ) {
- const browsers = await getBrowsers( { flat: true } );
- if ( !filter ) {
- return browsers;
- }
- const filterBrowser = ( filter.browser ?? "" ).toLowerCase();
- const filterVersion = ( filter.browser_version ?? "" ).toLowerCase();
- const filterOs = ( filter.os ?? "" ).toLowerCase();
- const filterOsVersion = ( filter.os_version ?? "" ).toLowerCase();
- const filterDevice = ( filter.device ?? "" ).toLowerCase();
-
- const filteredWithoutVersion = browsers.filter( ( browser ) => {
- return (
- ( !filterBrowser || filterBrowser === browser.browser.toLowerCase() ) &&
- ( !filterOs || filterOs === browser.os.toLowerCase() ) &&
- ( !filterOsVersion || matchVersion( browser.os_version, filterOsVersion ) ) &&
- ( !filterDevice || filterDevice === ( browser.device || "" ).toLowerCase() )
- );
- } );
-
- if ( !filterVersion ) {
- return filteredWithoutVersion;
- }
-
- if ( filterVersion.startsWith( "latest" ) ) {
- const groupedByName = filteredWithoutVersion
- .filter( ( b ) => rfinalVersion.test( b.browser_version ) )
- .reduce( ( acc, browser ) => {
- acc[ browser.browser ] = acc[ browser.browser ] ?? [];
- acc[ browser.browser ].push( browser );
- return acc;
- }, Object.create( null ) );
-
- const filtered = [];
- for ( const group of Object.values( groupedByName ) ) {
- const latest = group[ group.length - 1 ];
-
- // Mobile devices do not have browser version.
- // Skip the version check for these,
- // but include the latest in the list if it made it
- // through filtering.
- if ( !latest.browser_version ) {
-
- // Do not include in the list for latest-n.
- if ( filterVersion === "latest" ) {
- filtered.push( latest );
- }
- continue;
- }
-
- // Get the latest version and subtract the number from the filter,
- // ignoring any patch versions, which may differ between major versions.
- const num = rlatest.exec( filterVersion );
- const version = parseInt( latest.browser_version ) - ( num ? num[ 1 ] : 0 );
- const match = group.findLast( ( browser ) => {
- return matchVersion( browser.browser_version, version.toString() );
- } );
- if ( match ) {
- filtered.push( match );
- }
- }
- return filtered;
- }
-
- return filteredWithoutVersion.filter( ( browser ) => {
- return matchVersion( browser.browser_version, filterVersion );
- } );
-}
-
-export async function listBrowsers( filter ) {
- const browsers = await filterBrowsers( filter );
- console.log( "Available browsers:" );
- for ( const browser of browsers ) {
- let message = ` ${ browser.browser }_`;
- if ( browser.device ) {
- message += `:${ browser.device }_`;
- } else {
- message += `${ browser.browser_version }_`;
- }
- message += `${ browser.os }_${ browser.os_version }`;
- console.log( message );
- }
-}
-
-export async function getLatestBrowser( filter ) {
- if ( !filter.browser_version ) {
- filter.browser_version = "latest";
- }
- const browsers = await filterBrowsers( filter );
- return browsers[ browsers.length - 1 ];
-}
-
-/**
- * =============================
- * Workers API
- * =============================
- */
-
-/**
- * A browser object may only have one of `browser` or `device` set;
- * which property is set will depend on `os`.
- *
- * `options`: is an object with the following properties:
- * `os`: The operating system.
- * `os_version`: The operating system version.
- * `browser`: The browser name.
- * `browser_version`: The browser version.
- * `device`: The device name.
- * `url` (optional): Which URL to navigate to upon creation.
- * `timeout` (optional): Maximum life of the worker (in seconds). Maximum value of `1800`. Specifying `0` will use the default of `300`.
- * `name` (optional): Provide a name for the worker.
- * `build` (optional): Group workers into a build.
- * `project` (optional): Provide the project the worker belongs to.
- * `resolution` (optional): Specify the screen resolution (e.g. "1024x768").
- * `browserstack.local` (optional): Set to `true` to mark as local testing.
- * `browserstack.video` (optional): Set to `false` to disable video recording.
- * `browserstack.localIdentifier` (optional): ID of the local tunnel.
- */
-export function createWorker( options ) {
- return fetchAPI( "/worker", {
- method: "POST",
- body: JSON.stringify( options )
- } );
-}
-
-/**
- * Returns a worker object, if one exists, with the following properties:
- * `id`: The worker id.
- * `status`: A string representing the current status of the worker.
- * Possible statuses: `"running"`, `"queue"`.
- */
-export function getWorker( id ) {
- return fetchAPI( `/worker/${ id }` );
-}
-
-export async function deleteWorker( id ) {
- return fetchAPI( `/worker/${ id }`, { method: "DELETE" } );
-}
-
-export function getWorkers() {
- return fetchAPI( "/workers" );
-}
-
-/**
- * Stop all workers
- */
-export async function stopWorkers() {
- const workers = await getWorkers();
-
- // Run each request on its own
- // to avoid connect timeout errors.
- console.log( `${ workers.length } workers running...` );
- for ( const worker of workers ) {
- try {
- await deleteWorker( worker.id );
- } catch ( error ) {
-
- // Log the error, but continue trying to remove workers.
- console.error( error );
- }
- }
- console.log( "All workers stopped." );
-}
-
-/**
- * =============================
- * Plan API
- * =============================
- */
-
-export function getPlan() {
- return fetchAPI( "/automate/plan.json", {}, false );
-}
-
-export async function getAvailableSessions() {
- try {
- const [ plan, workers ] = await Promise.all( [ getPlan(), getWorkers() ] );
- return plan.parallel_sessions_max_allowed - workers.length;
- } catch ( error ) {
- console.error( error );
- return 0;
- }
-}
diff --git a/tests/runner/browserstack/buildBrowserFromString.js b/tests/runner/browserstack/buildBrowserFromString.js
deleted file mode 100644
index e0d99a039..000000000
--- a/tests/runner/browserstack/buildBrowserFromString.js
+++ /dev/null
@@ -1,20 +0,0 @@
-export function buildBrowserFromString( str ) {
- const [ browser, versionOrDevice, os, osVersion ] = str.split( "_" );
-
- // If the version starts with a colon, it's a device
- if ( versionOrDevice && versionOrDevice.startsWith( ":" ) ) {
- return {
- browser,
- device: versionOrDevice.slice( 1 ),
- os,
- os_version: osVersion
- };
- }
-
- return {
- browser,
- browser_version: versionOrDevice,
- os,
- os_version: osVersion
- };
-}
diff --git a/tests/runner/browserstack/createAuthHeader.js b/tests/runner/browserstack/createAuthHeader.js
deleted file mode 100644
index fe4831e9a..000000000
--- a/tests/runner/browserstack/createAuthHeader.js
+++ /dev/null
@@ -1,7 +0,0 @@
-const textEncoder = new TextEncoder();
-
-export function createAuthHeader( username, accessKey ) {
- const encoded = textEncoder.encode( `${ username }:${ accessKey }` );
- const base64 = btoa( String.fromCodePoint.apply( null, encoded ) );
- return `Basic ${ base64 }`;
-}
diff --git a/tests/runner/browserstack/local.js b/tests/runner/browserstack/local.js
deleted file mode 100644
index c84cf155c..000000000
--- a/tests/runner/browserstack/local.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import browserstackLocal from "browserstack-local";
-
-export async function localTunnel( localIdentifier, opts = {} ) {
- const tunnel = new browserstackLocal.Local();
-
- return new Promise( ( resolve, reject ) => {
-
- // https://www.browserstack.com/docs/local-testing/binary-params
- tunnel.start(
- {
- "enable-logging-for-api": "",
- localIdentifier,
- ...opts
- },
- async( error ) => {
- if ( error || !tunnel.isRunning() ) {
- return reject( error );
- }
- resolve( {
- stop: function stopTunnel() {
- return new Promise( ( resolve, reject ) => {
- tunnel.stop( ( error ) => {
- if ( error ) {
- return reject( error );
- }
- resolve();
- } );
- } );
- }
- } );
- }
- );
- } );
-}
diff --git a/tests/runner/command.js b/tests/runner/command.js
deleted file mode 100644
index cf5ddd8ee..000000000
--- a/tests/runner/command.js
+++ /dev/null
@@ -1,140 +0,0 @@
-import yargs from "yargs/yargs";
-import { browsers } from "./flags/browsers.js";
-import { getPlan, listBrowsers, stopWorkers } from "./browserstack/api.js";
-import { buildBrowserFromString } from "./browserstack/buildBrowserFromString.js";
-import { jquery } from "./flags/jquery.js";
-import { suites } from "./flags/suites.js";
-import { run } from "./run.js";
-
-const argv = yargs( process.argv.slice( 2 ) )
- .version( false )
- .strict()
- .command( {
- command: "[options]",
- describe: "Run jQuery tests in a browser"
- } )
- .option( "suite", {
- alias: "s",
- type: "array",
- choices: suites,
- description:
- "Run tests for a specific test suite.\n" +
- "Pass multiple test suites by repeating the option.\n" +
- "Defaults to all suites."
- } )
- .option( "jquery", {
- alias: "j",
- type: "array",
- choices: jquery,
- description:
- "Run tests against a specific jQuery version.\n" +
- "Pass multiple versions by repeating the option.",
- default: [ "3.7.1" ]
- } )
- .option( "migrate", {
- type: "boolean",
- description:
- "Run tests with jQuery Migrate enabled.",
- default: false
- } )
- .option( "browser", {
- alias: "b",
- type: "array",
- choices: browsers,
- description:
- "Run tests in a specific browser." +
- "Pass multiple browsers by repeating the option." +
- "If using BrowserStack, specify browsers using --browserstack.",
- default: [ "chrome" ]
- } )
- .option( "headless", {
- alias: "h",
- type: "boolean",
- description:
- "Run tests in headless mode. Cannot be used with --debug or --browserstack.",
- conflicts: [ "debug", "browserstack" ]
- } )
- .option( "concurrency", {
- alias: "c",
- type: "number",
- description:
- "Run tests in parallel in multiple browsers. " +
- "Defaults to 8 in normal mode. In browserstack mode, " +
- "defaults to the maximum available under your BrowserStack plan."
- } )
- .option( "debug", {
- alias: "d",
- type: "boolean",
- description:
- "Leave the browser open for debugging. Cannot be used with --headless.",
- conflicts: [ "headless" ]
- } )
- .option( "retries", {
- alias: "r",
- type: "number",
- description: "Number of times to retry failed tests by refreshing the URL."
- } )
- .option( "hard-retries", {
- type: "number",
- description:
- "Number of times to retry failed tests by restarting the worker. " +
- "This is in addition to the normal retries " +
- "and are only used when the normal retries are exhausted."
- } )
- .option( "verbose", {
- alias: "v",
- type: "boolean",
- description: "Log additional information."
- } )
- .option( "browserstack", {
- type: "array",
- description:
- "Run tests in BrowserStack.\n" +
- "Requires BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY environment variables.\n" +
- "The value can be empty for the default configuration, or a string in the format of\n" +
- "\"browser_[browserVersion | :device]_os_osVersion\" (see --list-browsers).\n" +
- "Pass multiple browsers by repeating the option.\n" +
- "The --browser option is ignored when --browserstack has a value.\n" +
- "Otherwise, the --browser option will be used, " +
- "with the latest version/device for that browser, on a matching OS."
- } )
- .option( "run-id", {
- type: "string",
- description: "A unique identifier for the run in BrowserStack."
- } )
- .option( "list-browsers", {
- type: "string",
- description:
- "List available BrowserStack browsers and exit.\n" +
- "Leave blank to view all browsers or pass " +
- "\"browser_[browserVersion | :device]_os_osVersion\" with each parameter " +
- "separated by an underscore to filter the list (any can be omitted).\n" +
- "\"latest\" can be used in place of \"browserVersion\" to find the latest version.\n" +
- "\"latest-n\" can be used to find the nth latest browser version.\n" +
- "Use a colon to indicate a device.\n" +
- "Examples: \"chrome__windows_10\", \"safari_latest\", " +
- "\"Mobile Safari\", \"Android Browser_:Google Pixel 8 Pro\".\n" +
- "Use quotes if spaces are necessary."
- } )
- .option( "stop-workers", {
- type: "boolean",
- description:
- "WARNING: This will stop all BrowserStack workers that may exist and exit," +
- "including any workers running from other projects.\n" +
- "This can be used as a failsafe when there are too many stray workers."
- } )
- .option( "browserstack-plan", {
- type: "boolean",
- description: "Show BrowserStack plan information and exit."
- } )
- .help().argv;
-
-if ( typeof argv.listBrowsers === "string" ) {
- listBrowsers( buildBrowserFromString( argv.listBrowsers ) );
-} else if ( argv.stopWorkers ) {
- stopWorkers();
-} else if ( argv.browserstackPlan ) {
- console.log( await getPlan() );
-} else {
- run( argv );
-}
diff --git a/tests/runner/createTestServer.js b/tests/runner/createTestServer.js
deleted file mode 100644
index 875e6d3b1..000000000
--- a/tests/runner/createTestServer.js
+++ /dev/null
@@ -1,66 +0,0 @@
-import { readFile } from "node:fs/promises";
-import bodyParser from "body-parser";
-import express from "express";
-import bodyParserErrorHandler from "express-body-parser-error-handler";
-
-export async function createTestServer( report ) {
- const app = express();
-
- // Redirect home to test page
- app.get( "/", ( _req, res ) => {
- res.redirect( "/tests/" );
- } );
-
- // Redirect to trailing slash
- app.use( ( req, res, next ) => {
- if ( req.path === "/tests" ) {
- const query = req.url.slice( req.path.length );
- res.redirect( 301, `${ req.path }/${ query }` );
- } else {
- next();
- }
- } );
-
- // Add a script tag to HTML pages to load the QUnit listeners
- app.use( /\/tests\/unit\/([a-zA-Z0-9_-]+)\/\1\.html$/, async( req, res ) => {
- const html = await readFile(
- `tests/unit/${ req.params[ 0 ] }/${ req.params[ 0 ] }.html`,
- "utf8"
- );
- res.send(
- html.replace(
- "</head>",
- "<script src=\"/tests/runner/listeners.js\"></script></head>"
- )
- );
- } );
-
- // Bind the reporter
- app.post(
- "/api/report",
- bodyParser.json( { limit: "50mb" } ),
- async( req, res ) => {
- if ( report ) {
- const response = await report( req.body );
- if ( response ) {
- res.json( response );
- return;
- }
- }
- res.sendStatus( 204 );
- }
- );
-
- // Handle errors from the body parser
- app.use( bodyParserErrorHandler() );
-
- // Serve static files
- app.use( "/dist", express.static( "dist" ) );
- app.use( "/src", express.static( "src" ) );
- app.use( "/tests", express.static( "tests" ) );
- app.use( "/ui", express.static( "ui" ) );
- app.use( "/themes", express.static( "themes" ) );
- app.use( "/external", express.static( "external" ) );
-
- return app;
-}
diff --git a/tests/runner/flags/browsers.js b/tests/runner/flags/browsers.js
deleted file mode 100644
index 5d2306afe..000000000
--- a/tests/runner/flags/browsers.js
+++ /dev/null
@@ -1,24 +0,0 @@
-// This list is static, so no requests are required
-// in the command help menu.
-
-import { getBrowsers } from "../browserstack/api.js";
-
-export const browsers = [
- "chrome",
- "ie",
- "firefox",
- "edge",
- "safari",
- "opera",
- "yandex",
- "IE Mobile",
- "Android Browser",
- "Mobile Safari"
-];
-
-// A function that can be used to update the above list.
-export async function getAvailableBrowsers() {
- const browsers = await getBrowsers( { flat: true } );
- const available = [ ...new Set( browsers.map( ( { browser } ) => browser ) ) ];
- return available;
-}
diff --git a/tests/runner/flags/jquery.js b/tests/runner/flags/jquery.js
deleted file mode 100644
index 0d4f21524..000000000
--- a/tests/runner/flags/jquery.js
+++ /dev/null
@@ -1,14 +0,0 @@
-// Keep in sync with tests/lib/qunit.js
-export const jquery = [
- "1.12.4",
- "2.2.4",
- "3.0.0",
- "3.1.0", "3.1.1",
- "3.2.0", "3.2.1",
- "3.3.0", "3.3.1",
- "3.4.0", "3.4.1",
- "3.5.0", "3.5.1",
- "3.6.0", "3.6.1", "3.6.2", "3.6.3", "3.6.4",
- "3.7.0", "3.7.1",
- "3.x-git", "git", "custom"
-];
diff --git a/tests/runner/flags/suites.js b/tests/runner/flags/suites.js
deleted file mode 100644
index a635ac4e5..000000000
--- a/tests/runner/flags/suites.js
+++ /dev/null
@@ -1,27 +0,0 @@
-export const suites = [
- "accordion",
- "autocomplete",
- "button",
- "checkboxradio",
- "controlgroup",
- "core",
- "datepicker",
- "dialog",
- "draggable",
- "droppable",
- "effects",
- "form-reset-mixin",
- "jquery-patch",
- "menu",
- "position",
- "progressbar",
- "resizable",
- "selectable",
- "selectmenu",
- "slider",
- "sortable",
- "spinner",
- "tabs",
- "tooltip",
- "widget"
-];
diff --git a/tests/runner/lib/buildTestUrl.js b/tests/runner/lib/buildTestUrl.js
deleted file mode 100644
index 5eb3b049b..000000000
--- a/tests/runner/lib/buildTestUrl.js
+++ /dev/null
@@ -1,24 +0,0 @@
-export function buildTestUrl( suite, { browserstack, jquery, migrate, port, reportId } ) {
- if ( !port ) {
- throw new Error( "No port specified." );
- }
-
- const query = new URLSearchParams();
-
- if ( jquery ) {
- query.append( "jquery", jquery );
- }
-
- if ( migrate ) {
- query.append( "migrate", "true" );
- }
-
- if ( reportId ) {
- query.append( "reportId", reportId );
- }
-
- // BrowserStack supplies a custom domain for local testing,
- // which is especially necessary for iOS testing.
- const host = browserstack ? "bs-local.com" : "localhost";
- return `http://${ host }:${ port }/tests/unit/${ suite }/${ suite }.html?${ query }`;
-}
diff --git a/tests/runner/lib/generateHash.js b/tests/runner/lib/generateHash.js
deleted file mode 100644
index 66f2161d5..000000000
--- a/tests/runner/lib/generateHash.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import crypto from "node:crypto";
-
-export function generateHash( string ) {
- const hash = crypto.createHash( "md5" );
- hash.update( string );
-
- // QUnit hashes are 8 characters long
- // We use 10 characters to be more visually distinct
- return hash.digest( "hex" ).slice( 0, 10 );
-}
diff --git a/tests/runner/lib/getBrowserString.js b/tests/runner/lib/getBrowserString.js
deleted file mode 100644
index 0d293074c..000000000
--- a/tests/runner/lib/getBrowserString.js
+++ /dev/null
@@ -1,48 +0,0 @@
-const browserMap = {
- chrome: "Chrome",
- edge: "Edge",
- firefox: "Firefox",
- ie: "IE",
- opera: "Opera",
- safari: "Safari"
-};
-
-export function browserSupportsHeadless( browser ) {
- browser = browser.toLowerCase();
- return (
- browser === "chrome" ||
- browser === "firefox" ||
- browser === "edge"
- );
-}
-
-export function getBrowserString(
- {
- browser,
- browser_version: browserVersion,
- device,
- os,
- os_version: osVersion
- },
- headless
-) {
- browser = browser.toLowerCase();
- browser = browserMap[ browser ] || browser;
- let str = browser;
- if ( browserVersion ) {
- str += ` ${ browserVersion }`;
- }
- if ( device ) {
- str += ` for ${ device }`;
- }
- if ( os ) {
- str += ` on ${ os }`;
- }
- if ( osVersion ) {
- str += ` ${ osVersion }`;
- }
- if ( headless && browserSupportsHeadless( browser ) ) {
- str += " (headless)";
- }
- return str;
-}
diff --git a/tests/runner/lib/prettyMs.js b/tests/runner/lib/prettyMs.js
deleted file mode 100644
index 99bae2b35..000000000
--- a/tests/runner/lib/prettyMs.js
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * Pretty print a time in milliseconds.
- */
-export function prettyMs( time ) {
- const minutes = Math.floor( time / 60000 );
- const seconds = Math.floor( time / 1000 );
- const ms = Math.floor( time % 1000 );
-
- let prettyTime = `${ ms }ms`;
- if ( seconds > 0 ) {
- prettyTime = `${ seconds }s ${ prettyTime }`;
- }
- if ( minutes > 0 ) {
- prettyTime = `${ minutes }m ${ prettyTime }`;
- }
-
- return prettyTime;
-}
diff --git a/tests/runner/listeners.js b/tests/runner/listeners.js
deleted file mode 100644
index ed6fb24e8..000000000
--- a/tests/runner/listeners.js
+++ /dev/null
@@ -1,112 +0,0 @@
-( function() {
- "use strict";
-
- // Get the report ID from the URL.
- var match = location.search.match( /reportId=([^&]+)/ );
- if ( !match ) {
- return;
- }
- var id = match[ 1 ];
-
- // Adopted from https://github.com/douglascrockford/JSON-js
- // Support: IE 11+
- // Using the replacer argument of JSON.stringify in IE has issues
- // TODO: Replace this with a circular replacer + JSON.stringify + WeakSet
- function decycle( object ) {
- var objects = [];
-
- // The derez function recurses through the object, producing the deep copy.
- function derez( value ) {
- if (
- typeof value === "object" &&
- value !== null &&
- !( value instanceof Boolean ) &&
- !( value instanceof Date ) &&
- !( value instanceof Number ) &&
- !( value instanceof RegExp ) &&
- !( value instanceof String )
- ) {
-
- // Return a string early for elements
- if ( value.nodeType ) {
- return value.toString();
- }
-
- if ( objects.indexOf( value ) > -1 ) {
- return;
- }
-
- objects.push( value );
-
- if ( Array.isArray( value ) ) {
-
- // If it is an array, replicate the array.
- return value.map( derez );
- } else {
-
- // If it is an object, replicate the object.
- var nu = Object.create( null );
- Object.keys( value ).forEach( function( name ) {
- nu[ name ] = derez( value[ name ] );
- } );
- return nu;
- }
- }
-
- // Serialize Symbols as string representations so they are
- // sent over the wire after being stringified.
- if ( typeof value === "symbol" ) {
-
- // We can *describe* unique symbols, but note that their identity
- // (e.g., `Symbol() !== Symbol()`) is lost
- var ctor = Symbol.keyFor( value ) !== undefined ? "Symbol.for" : "Symbol";
- return ctor + "(" + JSON.stringify( value.description ) + ")";
- }
-
- return value;
- }
- return derez( object );
- }
-
- function send( type, data ) {
- var json = JSON.stringify( {
- id: id,
- type: type,
- data: data ? decycle( data ) : undefined
- } );
- var request = new XMLHttpRequest();
- request.open( "POST", "/api/report", true );
- request.setRequestHeader( "Content-Type", "application/json" );
- request.send( json );
- return request;
- }
-
- require( [ "qunit" ], function( QUnit ) {
-
- // Send acknowledgement to the server.
- send( "ack" );
-
- QUnit.on( "testEnd", function( data ) {
- send( "testEnd", data );
- } );
-
- QUnit.on( "runEnd", function( data ) {
-
- // Reduce the payload size.
- // childSuites is large and unused.
- data.childSuites = undefined;
-
- var request = send( "runEnd", data );
- request.onload = function() {
- if ( request.status === 200 && request.responseText ) {
- try {
- var json = JSON.parse( request.responseText );
- window.location = json.url;
- } catch ( e ) {
- console.error( e );
- }
- }
- };
- } );
- } );
-} )();
diff --git a/tests/runner/package.json b/tests/runner/package.json
deleted file mode 100644
index bedb411a9..000000000
--- a/tests/runner/package.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "type": "module"
-}
diff --git a/tests/runner/queue.js b/tests/runner/queue.js
deleted file mode 100644
index 1c9ac1acb..000000000
--- a/tests/runner/queue.js
+++ /dev/null
@@ -1,119 +0,0 @@
-import chalk from "chalk";
-import { getBrowserString } from "./lib/getBrowserString.js";
-import {
- checkLastTouches,
- createBrowserWorker,
- restartBrowser,
- setBrowserWorkerUrl
-} from "./browsers.js";
-
-const TEST_POLL_TIMEOUT = 1000;
-
-const queue = [];
-
-export function getNextBrowserTest( reportId ) {
- const index = queue.findIndex( ( test ) => test.id === reportId );
- if ( index === -1 ) {
- return;
- }
-
- // Remove the completed test from the queue
- const previousTest = queue[ index ];
- queue.splice( index, 1 );
-
- // Find the next test for the same browser
- for ( const test of queue.slice( index ) ) {
- if ( test.fullBrowser === previousTest.fullBrowser ) {
-
- // Set the URL for our tracking
- setBrowserWorkerUrl( test.browser, test.url );
- test.running = true;
-
- // Return the URL for the next test.
- // listeners.js will use this to set the browser URL.
- return { url: test.url };
- }
- }
-}
-
-export function retryTest( reportId, maxRetries ) {
- if ( !maxRetries ) {
- return;
- }
- const test = queue.find( ( test ) => test.id === reportId );
- if ( test ) {
- test.retries++;
- if ( test.retries <= maxRetries ) {
- console.log(
- `\nRetrying test ${ reportId } for ${ chalk.yellow(
- test.options.suite
- ) }...${ test.retries }`
- );
- return test;
- }
- }
-}
-
-export async function hardRetryTest( reportId, maxHardRetries ) {
- if ( !maxHardRetries ) {
- return false;
- }
- const test = queue.find( ( test ) => test.id === reportId );
- if ( test ) {
- test.hardRetries++;
- if ( test.hardRetries <= maxHardRetries ) {
- console.log(
- `\nHard retrying test ${ reportId } for ${ chalk.yellow(
- test.options.suite
- ) }...${ test.hardRetries }`
- );
- await restartBrowser( test.browser );
- return true;
- }
- }
- return false;
-}
-
-export function addRun( url, browser, options ) {
- queue.push( {
- browser,
- fullBrowser: getBrowserString( browser ),
- hardRetries: 0,
- id: options.reportId,
- url,
- options,
- retries: 0,
- running: false
- } );
-}
-
-export async function runAll() {
- return new Promise( async( resolve, reject ) => {
- while ( queue.length ) {
- try {
- await checkLastTouches();
- } catch ( error ) {
- reject( error );
- }
-
- // Run one test URL per browser at a time
- const browsersTaken = [];
- for ( const test of queue ) {
- if ( browsersTaken.indexOf( test.fullBrowser ) > -1 ) {
- continue;
- }
- browsersTaken.push( test.fullBrowser );
- if ( !test.running ) {
- test.running = true;
- try {
- await createBrowserWorker( test.url, test.browser, test.options );
- } catch ( error ) {
- reject( error );
- }
- }
- }
- await new Promise( ( resolve ) => setTimeout( resolve, TEST_POLL_TIMEOUT ) );
- }
- resolve();
- } );
-}
diff --git a/tests/runner/reporter.js b/tests/runner/reporter.js
deleted file mode 100644
index 6e47a68e4..000000000
--- a/tests/runner/reporter.js
+++ /dev/null
@@ -1,134 +0,0 @@
-import chalk from "chalk";
-import * as Diff from "diff";
-import { getBrowserString } from "./lib/getBrowserString.js";
-import { prettyMs } from "./lib/prettyMs.js";
-
-function serializeForDiff( value ) {
-
- // Use naive serialization for everything except types with confusable values
- if ( typeof value === "string" ) {
- return JSON.stringify( value );
- }
- if ( typeof value === "bigint" ) {
- return `${ value }n`;
- }
- return `${ value }`;
-}
-
-export function reportTest( test, reportId, { browser, headless } ) {
- if ( test.status === "passed" ) {
-
- // Write to console without newlines
- process.stdout.write( "." );
- return;
- }
-
- let message = `${ chalk.bold( `${ test.suiteName }: ${ test.name }` ) }`;
- message += `\nTest ${ test.status } on ${ chalk.yellow(
- getBrowserString( browser, headless )
- ) } (${ chalk.bold( reportId ) }).`;
-
- // test.assertions only contains passed assertions;
- // test.errors contains all failed asssertions
- if ( test.errors.length ) {
- for ( const error of test.errors ) {
- message += "\n";
- if ( error.message ) {
- message += `\n${ error.message }`;
- }
- message += `\n${ chalk.gray( error.stack ) }`;
-
- // Show expected and actual values
- // if either is defined and non-null.
- // error.actual is set to null for failed
- // assert.expect() assertions, so skip those as well.
- // This should be fine because error.expected would
- // have to also be null for this to be skipped.
- if ( error.expected != null || error.actual != null ) {
- message += `\nexpected: ${ chalk.red( JSON.stringify( error.expected ) ) }`;
- message += `\nactual: ${ chalk.green( JSON.stringify( error.actual ) ) }`;
- let diff;
-
- if ( Array.isArray( error.expected ) && Array.isArray( error.actual ) ) {
-
- // Diff arrays
- diff = Diff.diffArrays( error.expected, error.actual );
- } else if (
- typeof error.expected === "object" &&
- typeof error.actual === "object"
- ) {
-
- // Diff objects
- diff = Diff.diffJson( error.expected, error.actual );
- } else if (
- typeof error.expected === "number" &&
- typeof error.actual === "number"
- ) {
-
- // Diff numbers directly
- const value = error.actual - error.expected;
- if ( value > 0 ) {
- diff = [ { added: true, value: `+${ value }` } ];
- } else {
- diff = [ { removed: true, value: `${ value }` } ];
- }
- } else if (
- typeof error.expected === "string" &&
- typeof error.actual === "string"
- ) {
-
- // Diff the characters of strings
- diff = Diff.diffChars( error.expected, error.actual );
- } else {
-
- // Diff everything else as words
- diff = Diff.diffWords(
- serializeForDiff( error.expected ),
- serializeForDiff( error.actual )
- );
- }
-
- if ( diff ) {
- message += "\n";
- message += diff
- .map( ( part ) => {
- if ( part.added ) {
- return chalk.green( part.value );
- }
- if ( part.removed ) {
- return chalk.red( part.value );
- }
- return chalk.gray( part.value );
- } )
- .join( "" );
- }
- }
- }
- }
-
- console.log( `\n\n${ message }` );
-
- // Only return failed messages
- if ( test.status === "failed" ) {
- return message;
- }
-}
-
-export function reportEnd( result, reportId, { browser, headless, jquery, migrate, suite } ) {
- const fullBrowser = getBrowserString( browser, headless );
- console.log(
- `\n\nTests finished in ${ prettyMs( result.runtime ) } ` +
- `for ${ chalk.yellow( suite ) } ` +
- `and jQuery ${ chalk.yellow( jquery ) } ` +
- ( migrate ? `with ${ chalk.yellow( "jQuery Migrate enabled " ) }` : "" ) +
- `in ${ chalk.yellow( fullBrowser ) } (${ chalk.bold( reportId ) })...`
- );
- console.log(
- ( result.status !== "passed" ?
- `${ chalk.red( result.testCounts.failed ) } failed. ` :
- "" ) +
- `${ chalk.green( result.testCounts.total ) } passed. ` +
- `${ chalk.gray( result.testCounts.skipped ) } skipped.`
- );
- return result.testCounts;
-}
diff --git a/tests/runner/run.js b/tests/runner/run.js
deleted file mode 100644
index 9c4f8d479..000000000
--- a/tests/runner/run.js
+++ /dev/null
@@ -1,338 +0,0 @@
-import chalk from "chalk";
-import { asyncExitHook, gracefulExit } from "exit-hook";
-import { getLatestBrowser } from "./browserstack/api.js";
-import { buildBrowserFromString } from "./browserstack/buildBrowserFromString.js";
-import { localTunnel } from "./browserstack/local.js";
-import { reportEnd, reportTest } from "./reporter.js";
-import { createTestServer } from "./createTestServer.js";
-import { buildTestUrl } from "./lib/buildTestUrl.js";
-import { generateHash } from "./lib/generateHash.js";
-import { getBrowserString } from "./lib/getBrowserString.js";
-import { suites as allSuites } from "./flags/suites.js";
-import { cleanupAllBrowsers, touchBrowser } from "./browsers.js";
-import {
- addRun,
- getNextBrowserTest,
- hardRetryTest,
- retryTest,
- runAll
-} from "./queue.js";
-
-const EXIT_HOOK_WAIT_TIMEOUT = 60 * 1000;
-
-/**
- * Run test suites in parallel in different browser instances.
- */
-export async function run( {
- browser: browserNames = [],
- browserstack,
- concurrency,
- debug,
- hardRetries,
- headless,
- jquery: jquerys = [],
- migrate,
- retries = 0,
- runId,
- suite: suites = [],
- verbose
-} ) {
- if ( !browserNames.length ) {
- browserNames = [ "chrome" ];
- }
- if ( !suites.length ) {
- suites = allSuites;
- }
- if ( !jquerys.length ) {
- jquerys = [ "3.7.1" ];
- }
- if ( headless && debug ) {
- throw new Error(
- "Cannot run in headless mode and debug mode at the same time."
- );
- }
-
- if ( verbose ) {
- console.log( browserstack ? "Running in BrowserStack." : "Running locally." );
- }
-
- const errorMessages = [];
- const pendingErrors = {};
-
- // Convert browser names to browser objects
- let browsers = browserNames.map( ( b ) => ( { browser: b } ) );
- const tunnelId = generateHash(
- `${ Date.now() }-${ suites.join( ":" ) }-${ ( browserstack || [] )
- .concat( browserNames )
- .join( ":" ) }`
- );
-
- // A unique identifier for this run
- if ( !runId ) {
- runId = tunnelId;
- }
-
- // Create the test app and
- // hook it up to the reporter
- const reports = Object.create( null );
- const app = await createTestServer( async( message ) => {
- switch ( message.type ) {
- case "testEnd": {
- const reportId = message.id;
- const report = reports[ reportId ];
- touchBrowser( report.browser );
- const errors = reportTest( message.data, reportId, report );
- pendingErrors[ reportId ] ??= Object.create( null );
- if ( errors ) {
- pendingErrors[ reportId ][ message.data.name ] = errors;
- } else {
- const existing = pendingErrors[ reportId ][ message.data.name ];
-
- // Show a message for flakey tests
- if ( existing ) {
- console.log();
- console.warn(
- chalk.italic(
- chalk.gray( existing.replace( "Test failed", "Test flakey" ) )
- )
- );
- console.log();
- delete pendingErrors[ reportId ][ message.data.name ];
- }
- }
- break;
- }
- case "runEnd": {
- const reportId = message.id;
- const report = reports[ reportId ];
- touchBrowser( report.browser );
- const { failed, total } = reportEnd(
- message.data,
- message.id,
- reports[ reportId ]
- );
- report.total = total;
-
- // Handle failure
- if ( failed ) {
- const retry = retryTest( reportId, retries );
-
- // Retry if retryTest returns a test
- if ( retry ) {
- return retry;
- }
-
- // Return early if hardRetryTest returns true
- if ( await hardRetryTest( reportId, hardRetries ) ) {
- return;
- }
- errorMessages.push( ...Object.values( pendingErrors[ reportId ] ) );
- }
-
- // Run the next test
- return getNextBrowserTest( reportId );
- }
- case "ack": {
- const report = reports[ message.id ];
- touchBrowser( report.browser );
- break;
- }
- default:
- console.warn( "Received unknown message type:", message.type );
- }
- } );
-
- // Start up local test server
- let server;
- let port;
- await new Promise( ( resolve ) => {
-
- // Pass 0 to choose a random, unused port
- server = app.listen( 0, () => {
- port = server.address().port;
- resolve();
- } );
- } );
-
- if ( !server || !port ) {
- throw new Error( "Server not started." );
- }
-
- if ( verbose ) {
- console.log( `Server started on port ${ port }.` );
- }
-
- function stopServer() {
- return new Promise( ( resolve ) => {
- server.close( () => {
- if ( verbose ) {
- console.log( "Server stopped." );
- }
- resolve();
- } );
- } );
- }
-
- async function cleanup() {
- console.log( "Cleaning up..." );
-
- await cleanupAllBrowsers( { verbose } );
-
- if ( tunnel ) {
- await tunnel.stop();
- if ( verbose ) {
- console.log( "Stopped BrowserStackLocal." );
- }
- }
- }
-
- asyncExitHook(
- async() => {
- await cleanup();
- await stopServer();
- },
- { wait: EXIT_HOOK_WAIT_TIMEOUT }
- );
-
- // Start up BrowserStackLocal
- let tunnel;
- if ( browserstack ) {
- if ( headless ) {
- console.warn(
- chalk.italic(
- "BrowserStack does not support headless mode. Running in normal mode."
- )
- );
- headless = false;
- }
-
- // Convert browserstack to browser objects.
- // If browserstack is an empty array, fall back
- // to the browsers array.
- if ( browserstack.length ) {
- browsers = browserstack.map( ( b ) => {
- if ( !b ) {
- return browsers[ 0 ];
- }
- return buildBrowserFromString( b );
- } );
- }
-
- // Fill out browser defaults
- browsers = await Promise.all(
- browsers.map( async( browser ) => {
-
- // Avoid undici connect timeout errors
- await new Promise( ( resolve ) => setTimeout( resolve, 100 ) );
-
- const latestMatch = await getLatestBrowser( browser );
- if ( !latestMatch ) {
- console.error(
- chalk.red( `Browser not found: ${ getBrowserString( browser ) }.` )
- );
- gracefulExit( 1 );
- }
- return latestMatch;
- } )
- );
-
- tunnel = await localTunnel( tunnelId );
- if ( verbose ) {
- console.log( "Started BrowserStackLocal." );
- }
- }
-
- function queueRuns( suite, browser ) {
- const fullBrowser = getBrowserString( browser, headless );
-
- for ( const jquery of jquerys ) {
- const reportId = generateHash( `${ suite } ${ jquery } ${ fullBrowser }` );
- reports[ reportId ] = { browser, headless, jquery, migrate, suite };
-
- const url = buildTestUrl( suite, {
- browserstack,
- jquery,
- migrate,
- port,
- reportId
- } );
-
- const options = {
- browserstack,
- concurrency,
- debug,
- headless,
- jquery,
- migrate,
- reportId,
- runId,
- suite,
- tunnelId,
- verbose
- };
-
- addRun( url, browser, options );
- }
- }
-
- for ( const browser of browsers ) {
- for ( const suite of suites ) {
- queueRuns( [ suite ], browser );
- }
- }
-
- try {
- console.log( `Starting Run ${ runId }...` );
- await runAll();
- } catch ( error ) {
- console.error( error );
- if ( !debug ) {
- gracefulExit( 1 );
- }
- } finally {
- console.log();
- if ( errorMessages.length === 0 ) {
- let stop = false;
- for ( const report of Object.values( reports ) ) {
- if ( !report.total ) {
- stop = true;
- console.error(
- chalk.red(
- `No tests were run for ${ report.suite } in ${ getBrowserString(
- report.browser
- ) }`
- )
- );
- }
- }
- if ( stop ) {
- return gracefulExit( 1 );
- }
- console.log( chalk.green( "All tests passed!" ) );
-
- if ( !debug || browserstack ) {
- gracefulExit( 0 );
- }
- } else {
- console.error( chalk.red( `${ errorMessages.length } tests failed.` ) );
- console.log(
- errorMessages.map( ( error, i ) => `\n${ i + 1 }. ${ error }` ).join( "\n" )
- );
-
- if ( debug ) {
- console.log();
- if ( browserstack ) {
- console.log( "Leaving browsers with failures open for debugging." );
- console.log(
- "View running sessions at https://automate.browserstack.com/dashboard/v2/"
- );
- } else {
- console.log( "Leaving browsers open for debugging." );
- }
- console.log( "Press Ctrl+C to exit." );
- } else {
- gracefulExit( 1 );
- }
- }
- }
-}
diff --git a/tests/runner/selenium/createDriver.js b/tests/runner/selenium/createDriver.js
deleted file mode 100644
index 095c12214..000000000
--- a/tests/runner/selenium/createDriver.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import { Builder, Capabilities, logging } from "selenium-webdriver";
-import Chrome from "selenium-webdriver/chrome.js";
-import Edge from "selenium-webdriver/edge.js";
-import Firefox from "selenium-webdriver/firefox.js";
-import { browserSupportsHeadless } from "../lib/getBrowserString.js";
-
-// Set script timeout to 10min
-const DRIVER_SCRIPT_TIMEOUT = 1000 * 60 * 10;
-
-export default async function createDriver( { browserName, headless, url, verbose } ) {
- const capabilities = Capabilities[ browserName ]();
- const prefs = new logging.Preferences();
- prefs.setLevel( logging.Type.BROWSER, logging.Level.ALL );
- capabilities.setLoggingPrefs( prefs );
-
- let driver = new Builder().withCapabilities( capabilities );
-
- const chromeOptions = new Chrome.Options();
- chromeOptions.addArguments( "--enable-chrome-browser-cloud-management" );
-
- // Alter the chrome binary path if
- // the CHROME_BIN environment variable is set
- if ( process.env.CHROME_BIN ) {
- if ( verbose ) {
- console.log( `Setting chrome binary to ${ process.env.CHROME_BIN }` );
- }
- chromeOptions.setChromeBinaryPath( process.env.CHROME_BIN );
- }
-
- const firefoxOptions = new Firefox.Options();
-
- if ( process.env.FIREFOX_BIN ) {
- if ( verbose ) {
- console.log( `Setting firefox binary to ${ process.env.FIREFOX_BIN }` );
- }
-
- firefoxOptions.setBinary( process.env.FIREFOX_BIN );
- }
-
- const edgeOptions = new Edge.Options();
- edgeOptions.addArguments( "--enable-chrome-browser-cloud-management" );
-
- // Alter the edge binary path if
- // the EDGE_BIN environment variable is set
- if ( process.env.EDGE_BIN ) {
- if ( verbose ) {
- console.log( `Setting edge binary to ${ process.env.EDGE_BIN }` );
- }
- edgeOptions.setEdgeChromiumBinaryPath( process.env.EDGE_BIN );
- }
-
- if ( headless ) {
- chromeOptions.addArguments( "--headless=new" );
- firefoxOptions.addArguments( "--headless" );
- edgeOptions.addArguments( "--headless=new" );
- if ( !browserSupportsHeadless( browserName ) ) {
- console.log(
- `Headless mode is not supported for ${ browserName }.` +
- "Running in normal mode instead."
- );
- }
- }
-
- driver = await driver
- .setChromeOptions( chromeOptions )
- .setFirefoxOptions( firefoxOptions )
- .setEdgeOptions( edgeOptions )
- .build();
-
- if ( verbose ) {
- const driverCapabilities = await driver.getCapabilities();
- const name = driverCapabilities.getBrowserName();
- const version = driverCapabilities.getBrowserVersion();
- console.log( `\nDriver created for ${ name } ${ version }` );
- }
-
- // Increase script timeout to 10min
- await driver.manage().setTimeouts( { script: DRIVER_SCRIPT_TIMEOUT } );
-
- // Set the first URL for the browser
- await driver.get( url );
-
- return driver;
-}
diff --git a/tests/runner/server.js b/tests/runner/server.js
deleted file mode 100644
index 10fbc220f..000000000
--- a/tests/runner/server.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import { createTestServer } from "./createTestServer.js";
-
-const port = process.env.PORT || 3000;
-
-async function runServer() {
- const app = await createTestServer();
-
- app.listen( { port, host: "0.0.0.0" }, function() {
- console.log( `Open tests at http://localhost:${ port }/tests/` );
- } );
-}
-
-runServer();
diff --git a/tests/unit/spinner/core.js b/tests/unit/spinner/core.js
index e9f7bc3d2..42bcc7bb5 100644
--- a/tests/unit/spinner/core.js
+++ b/tests/unit/spinner/core.js
@@ -163,7 +163,47 @@ QUnit.test( "mouse click on up button, increases value not greater than max", fu
assert.equal( element.val(), 0 );
} );
-QUnit.test( "mousewheel on input", function( assert ) {
+QUnit.test( "wheel on input", function( assert ) {
+ var ready = assert.async();
+ assert.expect( 5 );
+
+ var element = $( "#spin" ).val( 0 ).spinner( {
+ step: 2
+ } );
+
+ element.simulate( "focus" );
+ setTimeout( step1 );
+
+ function getWheelEvent( deltaY ) {
+ return jQuery.Event( new WheelEvent( "wheel", { deltaY: deltaY } ) );
+ }
+
+ function step1() {
+ element.trigger( getWheelEvent() );
+ assert.equal( element.val(), 0, "wheel event without delta does not change value" );
+
+ element.trigger( getWheelEvent( -1 ) );
+ assert.equal( element.val(), 2, "delta -1" );
+
+ element.trigger( getWheelEvent( 0.2 ) );
+ assert.equal( element.val(), 0, "delta 0.2" );
+
+ element.trigger( getWheelEvent( 15 ) );
+ assert.equal( element.val(), -2, "delta 15" );
+
+ element.simulate( "blur" );
+ setTimeout( step2 );
+ }
+
+ function step2() {
+ element.trigger( "wheel", -1 );
+ assert.equal( element.val(), -2, "wheel when not focused" );
+
+ ready();
+ }
+} );
+
+QUnit.test( "mousewheel on input (DEPRECATED)", function( assert ) {
var ready = assert.async();
assert.expect( 5 );
@@ -199,6 +239,20 @@ QUnit.test( "mousewheel on input", function( assert ) {
}
} );
+helper.testIframe(
+ "wheel & mousewheel conflicts",
+ "mousewheel-wheel.html",
+ function( assert, jQuery, window, document, values ) {
+ assert.expect( 5 );
+
+ assert.equal( values[ 0 ], 0, "wheel event without delta does not change value" );
+ assert.equal( values[ 1 ], 2, "delta -1" );
+ assert.equal( values[ 2 ], 0, "delta 0.2" );
+ assert.equal( values[ 3 ], -2, "delta 15" );
+ assert.equal( values[ 4 ], -2, "wheel when not focused" );
+ }
+);
+
QUnit.test( "reading HTML5 attributes", function( assert ) {
assert.expect( 6 );
var markup = "<input type='number' min='-100' max='100' value='5' step='2'>",
diff --git a/tests/unit/spinner/mousewheel-wheel.html b/tests/unit/spinner/mousewheel-wheel.html
new file mode 100644
index 000000000..e512a36cc
--- /dev/null
+++ b/tests/unit/spinner/mousewheel-wheel.html
@@ -0,0 +1,72 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>jQuery UI Spinner Test Suite</title>
+
+ <script src="../../../external/requirejs/require.js"></script>
+ <script src="../../../external/jquery/jquery.js"></script>
+ <script src="../../lib/css.js" data-modules="core button spinner theme"></script>
+ <script src="../../lib/testIframe.js"></script>
+</head>
+<body>
+
+<input id="spin" class="foo">
+
+<script>
+ function runTest() {
+ var values = [],
+ element = $( "#spin" ).val( 0 ).spinner( {
+ step: 2
+ } );
+
+ element.focus();
+ setTimeout( step1 );
+
+ function dispatchWheelEvent( elem, deltaY ) {
+ elem[ 0 ].dispatchEvent( new WheelEvent( "wheel", {
+ deltaY: deltaY
+ } ) );
+ }
+
+ function step1() {
+ dispatchWheelEvent( element );
+ values.push( element.val() );
+
+ dispatchWheelEvent( element, -1 );
+ values.push( element.val() );
+
+ dispatchWheelEvent( element, 0.2 );
+ values.push( element.val() );
+
+ dispatchWheelEvent( element, 15 );
+ values.push( element.val() );
+
+ element.blur();
+ setTimeout( step2 );
+ }
+
+ function step2() {
+ dispatchWheelEvent( element, -1 );
+ values.push( element.val() );
+
+ startIframeTest( values );
+ }
+ }
+
+ requirejs.config( {
+ paths: {
+ "jquery-mousewheel": "../../../external/jquery-mousewheel/jquery.mousewheel",
+ "ui": "../../../ui"
+ },
+ } );
+
+ require( [
+ "jquery-mousewheel",
+ "ui/widgets/spinner"
+ ], function() {
+ runTest();
+ } );
+</script>
+</body>
+</html>
diff --git a/tests/unit/tabs/core.js b/tests/unit/tabs/core.js
index c2fd89048..1eac3c268 100644
--- a/tests/unit/tabs/core.js
+++ b/tests/unit/tabs/core.js
@@ -747,4 +747,81 @@ QUnit.test( "extra listeners created when tabs are added/removed (trac-15136)",
"No extra listeners after removing all the extra tabs" );
} );
+QUnit.test( "URL-based auth with local tabs (gh-2213)", function( assert ) {
+ assert.expect( 1 );
+
+ var origAjax = $.ajax,
+ element = $( "#tabs1" ),
+ anchor = element.find( "a[href='#fragment-3']" ),
+ url = new URL( anchor.prop( "href" ) );
+
+ try {
+ $.ajax = function() {
+ throw new Error( "Unexpected AJAX call; all tabs are local!" );
+ };
+
+ anchor.attr( "href", url.protocol + "//username:password@" + url.host +
+ url.pathname + url.search + url.hash );
+
+ element.tabs();
+ anchor.trigger( "click" );
+
+ assert.strictEqual( element.tabs( "option", "active" ), 2,
+ "should set the active option" );
+ } finally {
+ $.ajax = origAjax;
+ }
+} );
+
+( function() {
+ function getVerifyTab( assert, element ) {
+ return function verifyTab( index ) {
+ assert.strictEqual(
+ element.tabs( "option", "active" ),
+ index,
+ "should set the active option to " + index );
+ assert.strictEqual(
+ element.find( "[role='tabpanel']:visible" ).text().trim(),
+ "Tab " + ( index + 1 ),
+ "should set the panel to 'Tab " + ( index + 1 ) + "'" );
+ };
+ }
+
+ QUnit.test( "href encoding/decoding (gh-2344)", function( assert ) {
+ assert.expect( 12 );
+
+ location.hash = "#tabs-2";
+
+ var i,
+ element = $( "#tabs10" ).tabs(),
+ tabLinks = element.find( "> ul a" ),
+ verifyTab = getVerifyTab( assert, element );
+
+ for ( i = 0; i < tabLinks.length; i++ ) {
+ tabLinks.eq( i ).trigger( "click" );
+ verifyTab( i );
+ }
+
+ location.hash = "";
+ } );
+
+ QUnit.test( "href encoding/decoding on init (gh-2344)", function( assert ) {
+ assert.expect( 12 );
+
+ var i,
+ element = $( "#tabs10" ),
+ tabLinks = element.find( "> ul a" ),
+ verifyTab = getVerifyTab( assert, element );
+
+ for ( i = 0; i < tabLinks.length; i++ ) {
+ location.hash = tabLinks.eq( i ).attr( "href" );
+ element.tabs();
+ verifyTab( i );
+ element.tabs( "destroy" );
+ }
+
+ location.hash = "";
+ } );
+} )();
+
} );
diff --git a/tests/unit/tabs/tabs.html b/tests/unit/tabs/tabs.html
index cb4e5389f..3f18fa015 100644
--- a/tests/unit/tabs/tabs.html
+++ b/tests/unit/tabs/tabs.html
@@ -125,6 +125,35 @@
<div id="tabs9-1"></div>
</div>
+<div id="tabs10">
+ <ul>
+ <li><a href="#tabs-1">1</a></li>
+ <li><a href="#tabs-2">2</a></li>
+ <li><a href="#%EF%B8%8F">3</a></li>
+ <li><a href="#🤗">4</a></li>
+ <li><a href="#😅">5</a></li>
+ <li><a href="#%25F0%259F%25A4%25AD">6</a></li>
+ </ul>
+ <div id="tabs-1">
+ <p>Tab 1</p>
+ </div>
+ <div id="tabs-2">
+ <p>Tab 2</p>
+ </div>
+ <div id="%EF%B8%8F">
+ <p>Tab 3</p>
+ </div>
+ <div id="🤗">
+ <p>Tab 4</p>
+ </div>
+ <div id="%F0%9F%98%85">
+ <p>Tab 5</p>
+ </div>
+ <div id="%F0%9F%A4%AD">
+ <p>Tab 6</p>
+ </div>
+</div>
+
</div>
</body>
</html>
diff --git a/ui/.eslintrc.json b/ui/.eslintrc.json
deleted file mode 100644
index eaba81af2..000000000
--- a/ui/.eslintrc.json
+++ /dev/null
@@ -1,42 +0,0 @@
-{
- "root": true,
-
- "extends": "jquery",
-
- "parserOptions": {
- "ecmaVersion": 5
- },
-
- "env": {
- "browser": true,
- "jquery": true,
- "node": false
- },
-
- "rules": {
- "strict": [ "error", "function" ],
-
- // The following rule is relaxed due to too many violations:
- "no-unused-vars": [ "error", { "vars": "all", "args": "after-used" } ],
-
- // Too many violations:
- "camelcase": "off",
- "no-nested-ternary": "off"
- },
-
- "globals": {
- "define": false,
- "Globalize": false
- },
-
- "overrides": [
- {
- "files": [ "i18n/**/*.js" ],
- "rules": {
-
- // We want to keep all the strings in separate single lines
- "max-len": "off"
- }
- }
- ]
-}
diff --git a/ui/effect.js b/ui/effect.js
index bbbb733c3..cb9ab8043 100644
--- a/ui/effect.js
+++ b/ui/effect.js
@@ -9,9 +9,7 @@
//>>label: Effects Core
//>>group: Effects
-/* eslint-disable max-len */
//>>description: Extends the internal jQuery effects. Includes morphing and easing. Required by all other effects.
-/* eslint-enable max-len */
//>>docs: https://api.jqueryui.com/category/effects-core/
//>>demos: https://jqueryui.com/effect/
@@ -320,7 +318,7 @@ if ( $.uiBackCompat === true ) {
try {
// eslint-disable-next-line no-unused-expressions
active.id;
- } catch ( e ) {
+ } catch ( _e ) {
active = document.body;
}
diff --git a/ui/effects/effect-explode.js b/ui/effects/effect-explode.js
index ed40833a8..da38b4d55 100644
--- a/ui/effects/effect-explode.js
+++ b/ui/effects/effect-explode.js
@@ -9,9 +9,7 @@
//>>label: Explode Effect
//>>group: Effects
-/* eslint-disable max-len */
//>>description: Explodes an element in all directions into n pieces. Implodes an element to its original wholeness.
-/* eslint-enable max-len */
//>>docs: https://api.jqueryui.com/explode-effect/
//>>demos: https://jqueryui.com/effect/
diff --git a/ui/widgets/accordion.js b/ui/widgets/accordion.js
index ff6e4631d..43a50db83 100644
--- a/ui/widgets/accordion.js
+++ b/ui/widgets/accordion.js
@@ -9,9 +9,7 @@
//>>label: Accordion
//>>group: Widgets
-/* eslint-disable max-len */
//>>description: Displays collapsible content panels for presenting information in a limited amount of space.
-/* eslint-enable max-len */
//>>docs: https://api.jqueryui.com/accordion/
//>>demos: https://jqueryui.com/accordion/
//>>css.structure: ../../themes/base/core.css
diff --git a/ui/widgets/datepicker.js b/ui/widgets/datepicker.js
index 323723b89..029f255e8 100644
--- a/ui/widgets/datepicker.js
+++ b/ui/widgets/datepicker.js
@@ -1,4 +1,4 @@
-/* eslint-disable max-len, camelcase */
+/* eslint-disable max-len */
/*!
* jQuery UI Datepicker @VERSION
* https://jqueryui.com
@@ -535,7 +535,7 @@ $.extend( Datepicker.prototype, {
_getInst: function( target ) {
try {
return $.data( target, "datepicker" );
- } catch ( err ) {
+ } catch ( _err ) {
throw "Missing instance data for this datepicker";
}
},
@@ -768,7 +768,7 @@ $.extend( Datepicker.prototype, {
$.datepicker._updateAlternate( inst );
$.datepicker._updateDatepicker( inst );
}
- } catch ( err ) {
+ } catch ( _err ) {
}
}
return true;
@@ -1540,7 +1540,7 @@ $.extend( Datepicker.prototype, {
try {
date = this.parseDate( dateFormat, dates, settings ) || defaultDate;
- } catch ( event ) {
+ } catch ( _err ) {
dates = ( noDefault ? "" : dates );
}
inst.selectedDay = date.getDate();
@@ -1569,7 +1569,7 @@ $.extend( Datepicker.prototype, {
try {
return $.datepicker.parseDate( $.datepicker._get( inst, "dateFormat" ),
offset, $.datepicker._getFormatConfig( inst ) );
- } catch ( e ) {
+ } catch ( _e ) {
// Ignore
}
diff --git a/ui/widgets/progressbar.js b/ui/widgets/progressbar.js
index 20e96440a..ad5366ade 100644
--- a/ui/widgets/progressbar.js
+++ b/ui/widgets/progressbar.js
@@ -9,9 +9,7 @@
//>>label: Progressbar
//>>group: Widgets
-/* eslint-disable max-len */
//>>description: Displays a status indicator for loading state, standard percentage, and other progress indicators.
-/* eslint-enable max-len */
//>>docs: https://api.jqueryui.com/progressbar/
//>>demos: https://jqueryui.com/progressbar/
//>>css.structure: ../../themes/base/core.css
diff --git a/ui/widgets/resizable.js b/ui/widgets/resizable.js
index 6f1e0ebde..012315283 100644
--- a/ui/widgets/resizable.js
+++ b/ui/widgets/resizable.js
@@ -104,7 +104,7 @@ $.widget( "ui.resizable", $.ui.mouse, {
el[ scroll ] = 1;
has = ( el[ scroll ] > 0 );
el[ scroll ] = 0;
- } catch ( e ) {
+ } catch ( _e ) {
// `el` might be a string, then setting `scroll` will throw
// an error in strict mode; ignore it.
diff --git a/ui/widgets/selectmenu.js b/ui/widgets/selectmenu.js
index f1b48fa60..749e33491 100644
--- a/ui/widgets/selectmenu.js
+++ b/ui/widgets/selectmenu.js
@@ -9,9 +9,7 @@
//>>label: Selectmenu
//>>group: Widgets
-/* eslint-disable max-len */
//>>description: Duplicates and extends the functionality of a native HTML select element, allowing it to be customizable in behavior and appearance far beyond the limitations of a native select.
-/* eslint-enable max-len */
//>>docs: https://api.jqueryui.com/selectmenu/
//>>demos: https://jqueryui.com/selectmenu/
//>>css.structure: ../../themes/base/core.css
diff --git a/ui/widgets/spinner.js b/ui/widgets/spinner.js
index d999d85d7..d4034b458 100644
--- a/ui/widgets/spinner.js
+++ b/ui/widgets/spinner.js
@@ -136,9 +136,10 @@ $.widget( "ui.spinner", {
this._trigger( "change", event );
}
},
- mousewheel: function( event, delta ) {
+ wheel: function( event ) {
var activeElement = this.document[ 0 ].activeElement;
var isActive = this.element[ 0 ] === activeElement;
+ var delta = event.deltaY || event.originalEvent && event.originalEvent.deltaY;
if ( !isActive || !delta ) {
return;
@@ -148,7 +149,7 @@ $.widget( "ui.spinner", {
return false;
}
- this._spin( ( delta > 0 ? 1 : -1 ) * this.options.step, event );
+ this._spin( ( delta > 0 ? -1 : 1 ) * this.options.step, event );
clearTimeout( this.mousewheelTimer );
this.mousewheelTimer = this._delay( function() {
if ( this.spinning ) {
@@ -157,6 +158,27 @@ $.widget( "ui.spinner", {
}, 100 );
event.preventDefault();
},
+
+ // DEPRECATED
+ // Kept for backwards compatibility. Please use the modern `wheel`
+ // event. The `delta` parameter is provided by the jQuery Mousewheel
+ // plugin if one is loaded.
+ mousewheel: function( event, delta ) {
+ if ( !event.isTrigger ) {
+
+ // If this is not a trigger call, the `wheel` handler will
+ // fire as well, let's not duplicate it.
+ return;
+ }
+
+ var wheelEvent = $.Event( event );
+ wheelEvent.type = "wheel";
+ if ( delta ) {
+ wheelEvent.deltaY = -delta;
+ }
+ return this._events.wheel.call( this, wheelEvent );
+ },
+
"mousedown .ui-spinner-button": function( event ) {
var previous;
diff --git a/ui/widgets/tabs.js b/ui/widgets/tabs.js
index 7b7907c32..494e54f22 100644
--- a/ui/widgets/tabs.js
+++ b/ui/widgets/tabs.js
@@ -61,26 +61,19 @@ $.widget( "ui.tabs", {
load: null
},
- _isLocal: ( function() {
- var rhash = /#.*$/;
-
- return function( anchor ) {
- var anchorUrl, locationUrl;
-
- anchorUrl = anchor.href.replace( rhash, "" );
- locationUrl = location.href.replace( rhash, "" );
-
- // Decoding may throw an error if the URL isn't UTF-8 (#9518)
- try {
- anchorUrl = decodeURIComponent( anchorUrl );
- } catch ( error ) {}
- try {
- locationUrl = decodeURIComponent( locationUrl );
- } catch ( error ) {}
-
- return anchor.hash.length > 1 && anchorUrl === locationUrl;
- };
- } )(),
+ _isLocal: function( anchor ) {
+ var anchorUrl = new URL( anchor.href ),
+ locationUrl = new URL( location.href );
+
+ return anchor.hash.length > 1 &&
+
+ // `href` may contain a hash but also username & password;
+ // we want to ignore them, so we check the three fields
+ // below instead.
+ anchorUrl.origin === locationUrl.origin &&
+ anchorUrl.pathname === locationUrl.pathname &&
+ anchorUrl.search === locationUrl.search;
+ },
_create: function() {
var that = this,
@@ -121,18 +114,31 @@ $.widget( "ui.tabs", {
_initialActive: function() {
var active = this.options.active,
collapsible = this.options.collapsible,
- locationHashDecoded = decodeURIComponent( location.hash.substring( 1 ) );
+ locationHash = location.hash.substring( 1 ),
+ locationHashDecoded = decodeURIComponent( locationHash );
if ( active === null ) {
// check the fragment identifier in the URL
- if ( locationHashDecoded ) {
+ if ( locationHash ) {
this.tabs.each( function( i, tab ) {
- if ( $( tab ).attr( "aria-controls" ) === locationHashDecoded ) {
+ if ( $( tab ).attr( "aria-controls" ) === locationHash ) {
active = i;
return false;
}
} );
+
+ // If not found, decode the hash & try again.
+ // See the comment in `_processTabs` under the `_isLocal` check
+ // for more information.
+ if ( active === null ) {
+ this.tabs.each( function( i, tab ) {
+ if ( $( tab ).attr( "aria-controls" ) === locationHashDecoded ) {
+ active = i;
+ return false;
+ }
+ } );
+ }
}
// Check for a tab marked active via a class
@@ -430,9 +436,24 @@ $.widget( "ui.tabs", {
// Inline tab
if ( that._isLocal( anchor ) ) {
- selector = decodeURIComponent( anchor.hash );
+
+ // The "scrolling to a fragment" section of the HTML spec:
+ // https://html.spec.whatwg.org/#scrolling-to-a-fragment
+ // uses a concept of document's indicated part:
+ // https://html.spec.whatwg.org/#the-indicated-part-of-the-document
+ // Slightly below there's an algorithm to compute the indicated
+ // part:
+ // https://html.spec.whatwg.org/#the-indicated-part-of-the-document
+ // First, the algorithm tries the hash as-is, without decoding.
+ // Then, if one is not found, the same is attempted with a decoded
+ // hash. Replicate this logic.
+ selector = anchor.hash;
panelId = selector.substring( 1 );
panel = that.element.find( "#" + CSS.escape( panelId ) );
+ if ( !panel.length ) {
+ panelId = decodeURIComponent( panelId );
+ panel = that.element.find( "#" + CSS.escape( panelId ) );
+ }
// remote tab
} else {