mirror of
https://github.com/jquery/jquery-ui.git
synced 2024-11-21 11:04:24 +00:00
Tests: replace grunt-contrib-qunit with jQuery test runner
- add filestash workflow Close gh-2221
This commit is contained in:
parent
802642c373
commit
91df20be6b
@ -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
51
.github/workflows/filestash.yml
vendored
Normal 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
163
.github/workflows/node.js.yml
vendored
Normal 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
|
63
.github/workflows/test.yml
vendored
63
.github/workflows/test.yml
vendored
@ -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 "✅"
|
@ -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
|
||||
|
37
Gruntfile.js
37
Gruntfile.js
@ -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" ] );
|
||||
|
||||
|
@ -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`.
|
||||
|
@ -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
20
external/jquery-3.7.1/LICENSE.txt
vendored
Normal 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
10716
external/jquery-3.7.1/jquery.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@ -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": []
|
||||
}
|
||||
|
@ -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
|
||||
|
8
tests/lib/bootstrap.js
vendored
8
tests/lib/bootstrap.js
vendored
@ -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 ) {
|
||||
|
@ -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"
|
||||
|
41
tests/runner/.eslintrc.json
Normal file
41
tests/runner/.eslintrc.json
Normal 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
4
tests/runner/browsers.js
Normal 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
78
tests/runner/command.js
Normal 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 );
|
66
tests/runner/createTestServer.js
Normal file
66
tests/runner/createTestServer.js
Normal 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
20
tests/runner/jquery.js
vendored
Normal 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"
|
||||
];
|
21
tests/runner/lib/buildTestUrl.js
Normal file
21
tests/runner/lib/buildTestUrl.js
Normal 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 }`;
|
||||
}
|
10
tests/runner/lib/generateHash.js
Normal file
10
tests/runner/lib/generateHash.js
Normal 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 );
|
||||
}
|
49
tests/runner/lib/getBrowserString.js
Normal file
49
tests/runner/lib/getBrowserString.js
Normal 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;
|
||||
}
|
18
tests/runner/lib/prettyMs.js
Normal file
18
tests/runner/lib/prettyMs.js
Normal 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
112
tests/runner/listeners.js
Normal 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 );
|
||||
}
|
||||
}
|
||||
};
|
||||
} );
|
||||
} );
|
||||
} )();
|
3
tests/runner/package.json
Normal file
3
tests/runner/package.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
134
tests/runner/reporter.js
Normal file
134
tests/runner/reporter.js
Normal 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
234
tests/runner/run.js
Normal 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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
200
tests/runner/selenium/browsers.js
Normal file
200
tests/runner/selenium/browsers.js
Normal 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 );
|
||||
}
|
||||
}
|
||||
}
|
84
tests/runner/selenium/createDriver.js
Normal file
84
tests/runner/selenium/createDriver.js
Normal 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;
|
||||
}
|
97
tests/runner/selenium/queue.js
Normal file
97
tests/runner/selenium/queue.js
Normal 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
13
tests/runner/server.js
Normal 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
26
tests/runner/suites.js
Normal 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"
|
||||
];
|
@ -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 = {
|
||||
|
Loading…
Reference in New Issue
Block a user