Tests: replace grunt-contrib-qunit with jQuery test runner

- add filestash workflow

Close gh-2221
This commit is contained in:
Timmy Willison 2024-03-29 09:13:46 -04:00 committed by GitHub
parent 802642c373
commit 91df20be6b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 12196 additions and 113 deletions

View File

@ -10,6 +10,10 @@ charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.yml]
indent_style = space
indent_size = 2
[external/**]
trim_trailing_whitespace = false
insert_final_newline = varies

51
.github/workflows/filestash.yml vendored Normal file
View File

@ -0,0 +1,51 @@
name: Filestash
on:
push:
branches:
- main
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
update:
runs-on: ubuntu-latest
environment: filestash
env:
NODE_VERSION: 20.x
name: Update Filestash
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ env.NODE_VERSION }}-npm-lock-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ env.NODE_VERSION }}-npm-lock-
- name: Install dependencies
run: npm install
- name: Build
run: npm run build
- name: Set up SSH
run: |
install --directory ~/.ssh --mode 700
base64 --decode <<< "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -t ed25519 -H "${{ secrets.FILESTASH_SERVER }}" >> ~/.ssh/known_hosts
- name: Upload to Filestash
run: |
rsync dist/jquery-ui.js filestash@"${{ secrets.FILESTASH_SERVER }}":ui/jquery-ui-git.js
rsync dist/jquery-ui.css filestash@"${{ secrets.FILESTASH_SERVER }}":ui/jquery-ui-git.css

163
.github/workflows/node.js.yml vendored Normal file
View File

@ -0,0 +1,163 @@
name: Node
on:
pull_request:
push:
branches-ignore: "dependabot/**"
# Once a week every Monday
schedule:
- cron: "42 1 * * 1"
permissions:
contents: read
env:
NODE_VERSION: 20.x
jobs:
build-and-test:
runs-on: ubuntu-latest
name: ${{ matrix.BROWSER }} - jQuery ${{ matrix.JQUERY }}
strategy:
fail-fast: false
matrix:
BROWSER: [chrome, firefox]
JQUERY:
- "git"
- "3.x-git"
- "3.7.1"
- "2.2.4"
- "1.12.4"
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ env.NODE_VERSION }}-npm-lock-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ env.NODE_VERSION }}-npm-lock-
- name: Install npm dependencies
run: npm install
- name: Lint
run: npm run lint
- name: Build
run: npm run build
- name: Test
run: npm run test:unit -- -h -b ${{ matrix.BROWSER }} --jquery ${{ matrix.JQUERY }} --retries 3
edge:
runs-on: windows-latest
name: edge - jQuery ${{ matrix.JQUERY }}
strategy:
fail-fast: false
matrix:
JQUERY:
- "git"
- "3.x-git"
- "3.7.1"
- "2.2.4"
- "1.12.4"
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ env.NODE_VERSION }}-npm-lock-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ env.NODE_VERSION }}-npm-lock-
- name: Install dependencies
run: npm install
- name: Build
run: npm run build
- name: Test
run: npm run test:unit -- -h -b edge --jquery ${{ matrix.JQUERY }} --retries 3
safari:
runs-on: macos-latest
name: safari - jQuery ${{ matrix.JQUERY }}
strategy:
fail-fast: false
matrix:
JQUERY:
- "git"
- "3.x-git"
- "3.7.1"
- "2.2.4"
- "1.12.4"
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ env.NODE_VERSION }}-npm-lock-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ env.NODE_VERSION }}-npm-lock-
- name: Install dependencies
run: npm install
- name: Build
run: npm run build
- name: Test
run: npm run test:unit -- -b safari --jquery ${{ matrix.JQUERY }} --retries 3
legacy-build:
runs-on: ubuntu-latest
name: Build on Node 10.x
env:
NODE_VERSION: 10.x
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ env.NODE_VERSION }}-npm-lock-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ env.NODE_VERSION }}-npm-lock-
- name: Install npm dependencies
run: npm install
- name: Build
run: npm run build

View File

@ -1,63 +0,0 @@
name: Grunt tests
on: [push, pull_request]
permissions:
contents: read
jobs:
grunt:
name: Grunt based tests with Node.js ${{ matrix.node-version }}
runs-on: ubuntu-latest
strategy:
matrix:
# Node.js 10 is required by jQuery infra
node-version: [10.x, 18.x, 20.x]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Get npm cache directory
id: npm-cache-dir
run: |
echo "dir=\"$(npm config get cache)\"" >> $GITHUB_OUTPUT
- name: Cache npm dependencies
uses: actions/cache@v4
with:
path: ${{ steps.npm-cache-dir.outputs.dir }}
key: ${{ runner.os }}-node-${{ matrix.node-version }}-npm-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-node-${{ matrix.node-version }}-npm-
${{ runner.os }}-node-${{ matrix.node-version }}-
${{ runner.os }}-node-
${{ runner.os }}-
- name: Install npm dependencies
run: npm install
# Keep these steps in sync with the default command tasks in our Gruntfile!
- name: Run lint
run: node_modules/.bin/grunt lint
- name: Run RequireJS
run: node_modules/.bin/grunt requirejs
- name: Run Qunit
run: node_modules/.bin/grunt test
valid:
name: Build & tests
needs: grunt
runs-on: ubuntu-latest
steps:
- name: Grunt based tests passed
run: echo "✅"

1
.npmrc Normal file
View File

@ -0,0 +1 @@
save-exact=true

View File

@ -77,7 +77,7 @@ The tests require a local web server and the samples contain some PHP, so a PHP
### Running the Tests
To lint the JavaScript, HTML, and CSS, as well as run a smoke test in PhantomJS, run the full test suite through npm:
To lint the JavaScript, HTML, and CSS, as well as run the full test suite in Headless Chrome:
```bash
npm test

View File

@ -77,7 +77,6 @@ const compareFiles = {
"dist/jquery-ui.min.js"
]
};
const component = grunt.option( "component" ) || "**";
const htmllintBad = [
"demos/tabs/ajax/content*.html",
@ -205,30 +204,6 @@ grunt.initConfig( {
src: htmllintBad
}
},
qunit: {
files: expandFiles( "tests/unit/" + component + "/*.html" ).filter( function( file ) {
return !( /(all|index|test)\.html$/ ).test( file );
} ),
options: {
puppeteer: {
args: [
"--allow-file-access-from-files"
]
},
inject: [
require.resolve(
"./tests/lib/grunt-contrib-qunit-bridges/bridge-wrapper.js.intro"
),
require.resolve( "grunt-contrib-qunit/chrome/bridge" ),
require.resolve(
"./tests/lib/grunt-contrib-qunit-bridges/bridge-wrapper.js.outro"
)
],
page: {
viewportSize: { width: 700, height: 500 }
}
}
},
eslint: {
all: [
"ui/**/*.js",
@ -430,6 +405,9 @@ grunt.initConfig( {
"jquery-3.7.0/jquery.js": "jquery-3.7.0/dist/jquery.js",
"jquery-3.7.0/LICENSE.txt": "jquery-3.7.0/LICENSE.txt",
"jquery-3.7.1/jquery.js": "jquery-3.7.1/dist/jquery.js",
"jquery-3.7.1/LICENSE.txt": "jquery-3.7.1/LICENSE.txt",
"jquery-migrate-1.4.1/jquery-migrate.js":
"jquery-migrate-1.4.1/dist/jquery-migrate.js",
"jquery-migrate-1.4.1/LICENSE.txt": "jquery-migrate-1.4.1/LICENSE.txt",
@ -473,13 +451,12 @@ grunt.initConfig( {
require( "load-grunt-tasks" )( grunt, {
pattern: nodeV16OrNewer ? [ "grunt-*" ] : [
"grunt-*",
"!grunt-contrib-qunit",
"!grunt-eslint",
"!grunt-html"
]
} );
// local testswarm and build tasks
// local tasks
grunt.loadTasks( "build/tasks" );
grunt.registerTask( "update-authors", function() {
@ -518,15 +495,15 @@ grunt.registerTask( "print_old_node_message", ( ...args ) => {
} );
// Keep this task list in sync with the testing steps in our GitHub action test workflow file!
grunt.registerTask( "default", [ "lint", "requirejs", "test" ] );
grunt.registerTask( "jenkins", [ "default", "concat" ] );
grunt.registerTask( "lint", [
"asciilint",
runIfNewNode( "eslint" ),
"csslint",
runIfNewNode( "htmllint" )
] );
grunt.registerTask( "test", [ runIfNewNode( "qunit" ) ] );
grunt.registerTask( "build", [ "requirejs", "concat" ] );
grunt.registerTask( "default", [ "lint", "build" ] );
grunt.registerTask( "jenkins", [ "build" ] );
grunt.registerTask( "sizer", [ "requirejs:js", "uglify:main", "compare_size:all" ] );
grunt.registerTask( "sizer_all", [ "requirejs:js", "uglify", "compare_size" ] );

View File

@ -29,4 +29,4 @@ For more information, see the [contributing page](CONTRIBUTING.md).
Run the unit tests manually with appropriate browsers and any local web server. See our [environment setup](CONTRIBUTING.md#environment-minimum-required) and [information on running tests](CONTRIBUTING.md#running-the-tests).
You can also run the unit tests inside phantomjs by [setting up your environment](CONTRIBUTING.md#user-content-environment-recommended-setup).
You can also run the unit tests `npm run test:unit -- --help`.

View File

@ -17,7 +17,6 @@
"jquery-simulate": "1.1.1",
"qunit": "2.19.4",
"requirejs": "2.1.14",
"jquery-1.8.0": "jquery#1.8.0",
"jquery-1.8.1": "jquery#1.8.1",
"jquery-1.8.2": "jquery#1.8.2",
@ -68,6 +67,7 @@
"jquery-3.6.3": "jquery#3.6.3",
"jquery-3.6.4": "jquery#3.6.4",
"jquery-3.7.0": "jquery#3.7.0",
"jquery-3.7.1": "jquery#3.7.1",
"jquery-migrate-1.4.1": "https://registry.npmjs.org/jquery-migrate/-/jquery-migrate-1.4.1.tgz",
"jquery-migrate-3.4.1": "https://registry.npmjs.org/jquery-migrate/-/jquery-migrate-3.4.1.tgz"
}

20
external/jquery-3.7.1/LICENSE.txt vendored Normal file
View File

@ -0,0 +1,20 @@
Copyright OpenJS Foundation and other contributors, https://openjsf.org/
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
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.

10716
external/jquery-3.7.1/jquery.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -45,21 +45,29 @@
},
"license": "MIT",
"scripts": {
"test": "grunt"
"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"
},
"dependencies": {
"jquery": ">=1.8.0 <4.0.0"
},
"devDependencies": {
"body-parser": "1.20.2",
"commitplease": "3.2.0",
"eslint-config-jquery": "3.0.0",
"diff": "5.2.0",
"eslint-config-jquery": "3.0.2",
"exit-hook": "4.0.0",
"express": "4.19.1",
"express-body-parser-error-handler": "1.0.7",
"grunt": "1.6.1",
"grunt-bowercopy": "1.2.5",
"grunt-cli": "1.4.3",
"grunt-compare-size": "0.4.2",
"grunt-contrib-concat": "2.1.0",
"grunt-contrib-csslint": "2.0.0",
"grunt-contrib-qunit": "7.0.0",
"grunt-contrib-requirejs": "1.0.0",
"grunt-contrib-uglify": "5.2.2",
"grunt-eslint": "24.0.1",
@ -67,7 +75,9 @@
"grunt-html": "16.0.0",
"load-grunt-tasks": "5.1.0",
"rimraf": "4.4.1",
"testswarm": "1.1.2"
"selenium-webdriver": "4.18.1",
"testswarm": "1.1.2",
"yargs": "17.7.2"
},
"keywords": []
}

View File

@ -18,8 +18,7 @@
<p><a href="unit/index.html">Unit tests</a> exist for all functionality in jQuery UI.
The unit tests can be run locally (some tests require a web server with PHP)
to ensure proper functionality before committing changes.
The unit tests are also run on <a href="https://swarm.jquery.org/project/jqueryui">TestSwarm</a>
for every commit.</p>
The unit tests are also run in Chrome, Firefox, Edge, and Safari on every commit.</p>
<h2>Visual Tests</h2>
<p><a href="visual/index.html">Visual tests</a> only exist in cases where we can't verify proper functionality

View File

@ -1,7 +1,7 @@
( function() {
"use strict";
var DEFAULT_JQUERY_VERSION = "3.7.0";
var DEFAULT_JQUERY_VERSION = "3.7.1";
requirejs.config( {
paths: {
@ -11,7 +11,6 @@ requirejs.config( {
"jquery-migrate": migrateUrl(),
"jquery-simulate": "../../../external/jquery-simulate/jquery.simulate",
"lib": "../../lib",
"phantom-bridge": "../../../node_modules/grunt-contrib-qunit/phantomjs/bridge",
"qunit-assert-classes": "../../lib/vendor/qunit-assert-classes/qunit-assert-classes",
"qunit-assert-close": "../../lib/vendor/qunit-assert-close/qunit-assert-close",
"qunit": "../../../external/qunit/qunit",
@ -33,11 +32,6 @@ define( "jquery-no-back-compat", [ "jquery" ], function( $ ) {
return $;
} );
// Create a dummy bridge if we're not actually testing in PhantomJS
if ( !/PhantomJS/.test( navigator.userAgent ) ) {
define( "phantom-bridge", function() {} );
}
// Load all modules in series
function requireModules( dependencies, callback, modules ) {
if ( !dependencies.length ) {

View File

@ -3,8 +3,7 @@ define( [
"jquery",
"qunit-assert-classes",
"qunit-assert-close",
"lib/qunit-assert-domequal",
"phantom-bridge"
"lib/qunit-assert-domequal"
], function( QUnit, $ ) {
"use strict";
@ -14,6 +13,8 @@ QUnit.config.requireExpects = true;
QUnit.config.urlConfig.push( {
id: "jquery",
label: "jQuery version",
// Keep in sync with tests/runner/jquery.js
value: [
"1.8.0", "1.8.1", "1.8.2", "1.8.3",
"1.9.0", "1.9.1",
@ -30,7 +31,7 @@ QUnit.config.urlConfig.push( {
"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.0", "3.7.1",
"3.x-git", "git", "custom"
],
tooltip: "Which jQuery Core version to test against"

View File

@ -0,0 +1,41 @@
{
"root": true,
"extends": "jquery",
"overrides": [
{
"files": ["**/*"],
"env": {
"node": true
},
"globals": {
"fetch": false,
"Promise": false,
"require": false
},
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module"
}
},
{
"files": ["./listeners.js"],
"env": {
"browser": true,
"node": false
},
"globals": {
"QUnit": false,
"Symbol": false
},
"parserOptions": {
"ecmaVersion": 5,
"sourceType": "script"
},
"rules": {
"strict": ["error", "function"]
}
}
]
}

4
tests/runner/browsers.js Normal file
View File

@ -0,0 +1,4 @@
// This list is static, so no requests are required
// in the command help menu.
export const browsers = [ "chrome", "ie", "firefox", "edge", "safari", "opera" ];

78
tests/runner/command.js Normal file
View File

@ -0,0 +1,78 @@
import yargs from "yargs/yargs";
import { browsers } from "./browsers.js";
import { suites } from "./suites.js";
import { run } from "./run.js";
import { jquery } from "./jquery.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.\n" +
"Pass multiple browsers by repeating the option.",
default: [ "chrome" ]
} )
.option( "headless", {
alias: "h",
type: "boolean",
description:
"Run tests in headless mode. Cannot be used with --debug.",
conflicts: [ "debug" ]
} )
.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."
} )
.option( "concurrency", {
alias: "c",
type: "number",
description: "Run tests in parallel in multiple browsers. Defaults to 8."
} )
.option( "verbose", {
alias: "v",
type: "boolean",
description: "Log additional information."
} )
.help().argv;
run( argv );

View File

@ -0,0 +1,66 @@
import bodyParser from "body-parser";
import express from "express";
import bodyParserErrorHandler from "express-body-parser-error-handler";
import { readFile } from "node:fs/promises";
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\/([^/]+)\/\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;
}

20
tests/runner/jquery.js vendored Normal file
View File

@ -0,0 +1,20 @@
// Keep in sync with tests/lib/qunit.js
export const jquery = [
"1.8.0", "1.8.1", "1.8.2", "1.8.3",
"1.9.0", "1.9.1",
"1.10.0", "1.10.1", "1.10.2",
"1.11.0", "1.11.1", "1.11.2", "1.11.3",
"1.12.0", "1.12.1", "1.12.2", "1.12.3", "1.12.4",
"2.0.0", "2.0.1", "2.0.2", "2.0.3",
"2.1.0", "2.1.1", "2.1.2", "2.1.3", "2.1.4",
"2.2.0", "2.2.1", "2.2.2", "2.2.3", "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"
];

View File

@ -0,0 +1,21 @@
export function buildTestUrl( suite, { 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 );
}
return `http://localhost:${ port }/tests/unit/${ suite }/${ suite }.html?${ query }`;
}

View File

@ -0,0 +1,10 @@
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 );
}

View File

@ -0,0 +1,49 @@
const browserMap = {
chrome: "Chrome",
edge: "Edge",
firefox: "Firefox",
ie: "IE",
jsdom: "JSDOM",
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;
}

View File

@ -0,0 +1,18 @@
/**
* 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;
}

112
tests/runner/listeners.js Normal file
View File

@ -0,0 +1,112 @@
( 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 );
}
}
};
} );
} );
} )();

View File

@ -0,0 +1,3 @@
{
"type": "module"
}

134
tests/runner/reporter.js Normal file
View File

@ -0,0 +1,134 @@
import chalk from "chalk";
import { getBrowserString } from "./lib/getBrowserString.js";
import { prettyMs } from "./lib/prettyMs.js";
import * as Diff from "diff";
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;
}

234
tests/runner/run.js Normal file
View File

@ -0,0 +1,234 @@
import chalk from "chalk";
import { asyncExitHook, gracefulExit } from "exit-hook";
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 "./suites.js";
import { cleanupAllBrowsers, touchBrowser } from "./selenium/browsers.js";
import { addRun, getNextBrowserTest, retryTest, runAll } from "./selenium/queue.js";
const EXIT_HOOK_WAIT_TIMEOUT = 60 * 1000;
/**
* Run test suites in parallel in different browser instances.
*/
export async function run( {
browser: browserNames = [],
concurrency,
debug,
headless,
jquery: jquerys = [],
migrate,
retries = 0,
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."
);
}
const errorMessages = [];
const pendingErrors = {};
// Convert browser names to browser objects
let browsers = browserNames.map( ( b ) => ( { browser: b } ) );
// 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;
}
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();
} );
} );
}
asyncExitHook(
async() => {
await cleanupAllBrowsers( { verbose } );
await stopServer();
},
{ wait: EXIT_HOOK_WAIT_TIMEOUT }
);
function queueRuns( suite, browser ) {
const fullBrowser = getBrowserString( browser, headless );
for ( const jquery of jquerys ) {
const reportId = generateHash( `${ suite } ${ fullBrowser }` );
reports[ reportId ] = { browser, headless, jquery, migrate, suite };
const url = buildTestUrl( suite, {
jquery,
migrate,
port,
reportId
} );
const options = {
debug,
headless,
jquery,
migrate,
reportId,
suite,
verbose
};
addRun( url, browser, options );
}
}
for ( const browser of browsers ) {
for ( const suite of suites ) {
queueRuns( suite, browser );
}
}
try {
await runAll( { concurrency, verbose } );
} 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 ) {
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();
console.log( "Leaving browsers open for debugging." );
console.log( "Press Ctrl+C to exit." );
} else {
gracefulExit( 1 );
}
}
}
}

View File

@ -0,0 +1,200 @@
import chalk from "chalk";
import { getBrowserString } from "../lib/getBrowserString.js";
import createDriver from "./createDriver.js";
const workers = Object.create( null );
/**
* Keys are browser strings
* Structure of a worker:
* {
* debug: boolean, // Stops the worker from being cleaned up when finished
* id: string,
* lastTouch: number, // The last time a request was received
* url: string,
* browser: object, // The browser object
* options: object // The options to create the worker
* }
*/
// Acknowledge the worker within the time limit.
const ACKNOWLEDGE_INTERVAL = 1000;
const ACKNOWLEDGE_TIMEOUT = 60 * 1000 * 1;
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_CONCURRENCY = 8;
export function touchBrowser( browser ) {
const fullBrowser = getBrowserString( browser );
const worker = workers[ fullBrowser ];
if ( worker ) {
worker.lastTouch = Date.now();
}
}
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 restartWorker( worker ) {
await cleanupWorker( worker, worker.options );
await createBrowserWorker(
worker.url,
worker.browser,
worker.options,
worker.restarts + 1
);
}
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 );
}
}
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 { concurrency = MAX_CONCURRENCY, debug, headless, verbose } = options;
while ( workers.length >= concurrency ) {
if ( verbose ) {
console.log( "\nWaiting for available sessions..." );
}
await new Promise( ( resolve ) => setTimeout( resolve, WORKER_WAIT_TIME ) );
}
const fullBrowser = getBrowserString( browser );
const driver = await createDriver( {
browserName: browser.browser,
headless,
url,
verbose
} );
const worker = {
debug: !!debug,
driver,
url,
browser,
restarts,
options
};
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 async function setBrowserWorkerUrl( browser, url ) {
const fullBrowser = getBrowserString( browser );
const worker = workers[ fullBrowser ];
if ( worker ) {
worker.url = url;
}
}
/**
* 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 cleanupWorker( worker, { verbose } ) {
for ( const [ fullBrowser, w ] of Object.entries( workers ) ) {
if ( w === worker ) {
delete workers[ fullBrowser ];
await w.driver.quit();
if ( verbose ) {
console.log( `\nStopped ${ fullBrowser }.` );
}
return;
}
}
}
export async function cleanupAllBrowsers( { verbose } ) {
const workersRemaining = Object.values( workers );
const numRemaining = workersRemaining.length;
if ( numRemaining ) {
try {
await Promise.all(
workersRemaining.map( ( worker ) => worker.driver.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 );
}
}
}

View File

@ -0,0 +1,84 @@
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;
}

View File

@ -0,0 +1,97 @@
import chalk from "chalk";
import { getBrowserString } from "../lib/getBrowserString.js";
import {
checkLastTouches,
createBrowserWorker,
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 function addRun( url, browser, options ) {
queue.push( {
browser,
fullBrowser: getBrowserString( browser ),
id: options.reportId,
retries: 0,
url,
options,
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();
} );
}

13
tests/runner/server.js Normal file
View File

@ -0,0 +1,13 @@
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();

26
tests/runner/suites.js Normal file
View File

@ -0,0 +1,26 @@
export const suites = [
"accordion",
"autocomplete",
"button",
"checkboxradio",
"controlgroup",
"core",
"datepicker",
"dialog",
"draggable",
"droppable",
"effects",
"form-reset-mixin",
"menu",
"position",
"progressbar",
"resizable",
"selectable",
"selectmenu",
"slider",
"sortable",
"spinner",
"tabs",
"tooltip",
"widget"
];

View File

@ -17,7 +17,7 @@ var versions = [
"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.0", "3.7.1",
"3.x-git", "git", "custom"
],
additionalTests = {