aboutsummaryrefslogtreecommitdiffstats
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/data/mock.php4
-rw-r--r--test/runner/browsers.js248
-rw-r--r--test/runner/browserstack/api.js332
-rw-r--r--test/runner/browserstack/buildBrowserFromString.js20
-rw-r--r--test/runner/browserstack/createAuthHeader.js7
-rw-r--r--test/runner/browserstack/local.js34
-rw-r--r--test/runner/command.js134
-rw-r--r--test/runner/createTestServer.js192
-rw-r--r--test/runner/flags/browsers.js25
-rw-r--r--test/runner/flags/modules.js24
-rw-r--r--test/runner/jsdom/createWindow.js21
-rw-r--r--test/runner/lib/buildTestUrl.js29
-rw-r--r--test/runner/lib/generateHash.js50
-rw-r--r--test/runner/lib/getBrowserString.js49
-rw-r--r--test/runner/lib/prettyMs.js18
-rw-r--r--test/runner/listeners.js110
-rw-r--r--test/runner/queue.js119
-rw-r--r--test/runner/reporter.js132
-rw-r--r--test/runner/run.js338
-rw-r--r--test/runner/selenium/createDriver.js97
-rw-r--r--test/runner/server.js13
-rw-r--r--test/unit/ajax.js33
-rw-r--r--test/unit/attributes.js18
-rw-r--r--test/unit/css.js23
-rw-r--r--test/unit/dimensions.js100
-rw-r--r--test/unit/event.js4
-rw-r--r--test/unit/manipulation.js10
-rw-r--r--test/unit/offset.js24
-rw-r--r--test/unit/selector.js37
-rw-r--r--test/unit/support.js32
30 files changed, 194 insertions, 2083 deletions
diff --git a/test/data/mock.php b/test/data/mock.php
index 105f9867b..013189521 100644
--- a/test/data/mock.php
+++ b/test/data/mock.php
@@ -95,9 +95,9 @@ QUnit.assert.ok( true, "mock executed");';
}
if ( isset( $req->query['array'] ) ) {
- echo '[ {"name": "John", "age": 21}, {"name": "Peter", "age": 25 } ]';
+ echo '[{"name":"John","age":21},{"name":"Peter","age":25}]';
} else {
- echo '{ "data": {"lang": "en", "length": 25} }';
+ echo '{"data":{"lang":"en","length":25}}';
}
}
diff --git a/test/runner/browsers.js b/test/runner/browsers.js
deleted file mode 100644
index 5962ea160..000000000
--- a/test/runner/browsers.js
+++ /dev/null
@@ -1,248 +0,0 @@
-import chalk from "chalk";
-import { getBrowserString } from "./lib/getBrowserString.js";
-import {
- createWorker,
- deleteWorker,
- getAvailableSessions
-} from "./browserstack/api.js";
-import createDriver from "./selenium/createDriver.js";
-import createWindow from "./jsdom/createWindow.js";
-
-const workers = Object.create( null );
-
-/**
- * Keys are browser strings
- * Structure of a worker:
- * {
- * browser: object // The browser object
- * debug: boolean // Stops the worker from being cleaned up when finished
- * lastTouch: number // The last time a request was received
- * restarts: number // The number of times the worker has been restarted
- * options: object // The options to create the worker
- * url: string // The URL the worker is on
- * quit: function // A function to stop the worker
- * }
- */
-
-// Acknowledge the worker within the time limit.
-// BrowserStack can take much longer spinning up
-// some browsers, such as iOS 15 Safari.
-const ACKNOWLEDGE_INTERVAL = 1000;
-const ACKNOWLEDGE_TIMEOUT = 60 * 1000 * 5;
-
-const MAX_WORKER_RESTARTS = 5;
-
-// No report after the time limit
-// should refresh the worker
-const RUN_WORKER_TIMEOUT = 60 * 1000 * 2;
-
-const WORKER_WAIT_TIME = 30000;
-
-// Limit concurrency to 8 by default in selenium
-const MAX_SELENIUM_CONCURRENCY = 8;
-
-export async function createBrowserWorker( url, browser, options, restarts = 0 ) {
- if ( restarts > MAX_WORKER_RESTARTS ) {
- throw new Error(
- `Reached the maximum number of restarts for ${ chalk.yellow(
- getBrowserString( browser )
- ) }`
- );
- }
- const { browserstack, debug, headless, reportId, runId, tunnelId, verbose } = options;
- while ( await maxWorkersReached( options ) ) {
- if ( verbose ) {
- console.log( "\nWaiting for available sessions..." );
- }
- await new Promise( ( resolve ) => setTimeout( resolve, WORKER_WAIT_TIME ) );
- }
-
- const fullBrowser = getBrowserString( browser );
-
- let worker;
-
- if ( browserstack ) {
- worker = await createWorker( {
- ...browser,
- url: encodeURI( url ),
- project: "jquery",
- build: `Run ${ runId }`,
-
- // This is the maximum timeout allowed
- // by BrowserStack. We do this because
- // we control the timeout in the runner.
- // See https://github.com/browserstack/api/blob/b324a6a5bc1b6052510d74e286b8e1c758c308a7/README.md#timeout300
- timeout: 1800,
-
- // Not documented in the API docs,
- // but required to make local testing work.
- // See https://www.browserstack.com/docs/automate/selenium/manage-multiple-connections#nodejs
- "browserstack.local": true,
- "browserstack.localIdentifier": tunnelId
- } );
- worker.quit = () => deleteWorker( worker.id );
- } else if ( browser.browser === "jsdom" ) {
- const window = await createWindow( { reportId, url, verbose } );
- worker = {
- quit: () => window.close()
- };
- } else {
- const driver = await createDriver( {
- browserName: browser.browser,
- headless,
- url,
- verbose
- } );
- worker = {
- quit: () => driver.quit()
- };
- }
-
- worker.debug = !!debug;
- worker.url = url;
- worker.browser = browser;
- worker.restarts = restarts;
- worker.options = options;
- touchBrowser( browser );
- workers[ fullBrowser ] = worker;
-
- // Wait for the worker to show up in the list
- // before returning it.
- return ensureAcknowledged( worker );
-}
-
-export function touchBrowser( browser ) {
- const fullBrowser = getBrowserString( browser );
- const worker = workers[ fullBrowser ];
- if ( worker ) {
- worker.lastTouch = Date.now();
- }
-}
-
-export async function setBrowserWorkerUrl( browser, url ) {
- const fullBrowser = getBrowserString( browser );
- const worker = workers[ fullBrowser ];
- if ( worker ) {
- worker.url = url;
- }
-}
-
-export async function restartBrowser( browser ) {
- const fullBrowser = getBrowserString( browser );
- const worker = workers[ fullBrowser ];
- if ( worker ) {
- await restartWorker( worker );
- }
-}
-
-/**
- * Checks that all browsers have received
- * a response in the given amount of time.
- * If not, the worker is restarted.
- */
-export async function checkLastTouches() {
- for ( const [ fullBrowser, worker ] of Object.entries( workers ) ) {
- if ( Date.now() - worker.lastTouch > RUN_WORKER_TIMEOUT ) {
- const options = worker.options;
- if ( options.verbose ) {
- console.log(
- `\nNo response from ${ chalk.yellow( fullBrowser ) } in ${
- RUN_WORKER_TIMEOUT / 1000 / 60
- }min.`
- );
- }
- await restartWorker( worker );
- }
- }
-}
-
-export async function cleanupAllBrowsers( { verbose } ) {
- const workersRemaining = Object.values( workers );
- const numRemaining = workersRemaining.length;
- if ( numRemaining ) {
- try {
- await Promise.all( workersRemaining.map( ( worker ) => worker.quit() ) );
- if ( verbose ) {
- console.log(
- `Stopped ${ numRemaining } browser${ numRemaining > 1 ? "s" : "" }.`
- );
- }
- } catch ( error ) {
-
- // Log the error, but do not consider the test run failed
- console.error( error );
- }
- }
-}
-
-async function maxWorkersReached( {
- browserstack,
- concurrency = MAX_SELENIUM_CONCURRENCY
-} ) {
- if ( browserstack ) {
- return ( await getAvailableSessions() ) <= 0;
- }
- return workers.length >= concurrency;
-}
-
-async function waitForAck( worker, { fullBrowser, verbose } ) {
- delete worker.lastTouch;
- return new Promise( ( resolve, reject ) => {
- const interval = setInterval( () => {
- if ( worker.lastTouch ) {
- if ( verbose ) {
- console.log( `\n${ fullBrowser } acknowledged.` );
- }
- clearTimeout( timeout );
- clearInterval( interval );
- resolve();
- }
- }, ACKNOWLEDGE_INTERVAL );
-
- const timeout = setTimeout( () => {
- clearInterval( interval );
- reject(
- new Error(
- `${ fullBrowser } not acknowledged after ${
- ACKNOWLEDGE_TIMEOUT / 1000 / 60
- }min.`
- )
- );
- }, ACKNOWLEDGE_TIMEOUT );
- } );
-}
-
-async function ensureAcknowledged( worker ) {
- const fullBrowser = getBrowserString( worker.browser );
- const verbose = worker.options.verbose;
- try {
- await waitForAck( worker, { fullBrowser, verbose } );
- return worker;
- } catch ( error ) {
- console.error( error.message );
- await restartWorker( worker );
- }
-}
-
-async function cleanupWorker( worker, { verbose } ) {
- for ( const [ fullBrowser, w ] of Object.entries( workers ) ) {
- if ( w === worker ) {
- delete workers[ fullBrowser ];
- await worker.quit();
- if ( verbose ) {
- console.log( `\nStopped ${ fullBrowser }.` );
- }
- return;
- }
- }
-}
-
-async function restartWorker( worker ) {
- await cleanupWorker( worker, worker.options );
- await createBrowserWorker(
- worker.url,
- worker.browser,
- worker.options,
- worker.restarts + 1
- );
-}
diff --git a/test/runner/browserstack/api.js b/test/runner/browserstack/api.js
deleted file mode 100644
index 632f90c3b..000000000
--- a/test/runner/browserstack/api.js
+++ /dev/null
@@ -1,332 +0,0 @@
-/**
- * Browserstack API is documented at
- * https://github.com/browserstack/api
- */
-
-import { createAuthHeader } from "./createAuthHeader.js";
-
-const browserstackApi = "https://api.browserstack.com";
-const apiVersion = 5;
-
-const username = process.env.BROWSERSTACK_USERNAME;
-const accessKey = process.env.BROWSERSTACK_ACCESS_KEY;
-
-// iOS has null for version numbers,
-// and we do not need a similar check for OS versions.
-const rfinalVersion = /(?:^[0-9\.]+$)|(?:^null$)/;
-const rlatest = /^latest-(\d+)$/;
-
-const rnonDigits = /(?:[^\d\.]+)|(?:20\d{2})/g;
-
-async function fetchAPI( path, options = {}, versioned = true ) {
- if ( !username || !accessKey ) {
- throw new Error(
- "BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY environment variables must be set."
- );
- }
- const init = {
- method: "GET",
- ...options,
- headers: {
- authorization: createAuthHeader( username, accessKey ),
- accept: "application/json",
- "content-type": "application/json",
- ...options.headers
- }
- };
- const response = await fetch(
- `${ browserstackApi }/${ versioned ? `${ apiVersion }/` : "" }${ path }`,
- init
- );
- if ( !response.ok ) {
- console.log(
- `\n${ init.method } ${ path }`,
- response.status,
- response.statusText
- );
- throw new Error( `Error fetching ${ path }` );
- }
- return response.json();
-}
-
-/**
- * =============================
- * Browsers API
- * =============================
- */
-
-function compareVersionNumbers( a, b ) {
- if ( a != null && b == null ) {
- return -1;
- }
- if ( a == null && b != null ) {
- return 1;
- }
- if ( a == null && b == null ) {
- return 0;
- }
- const aParts = a.replace( rnonDigits, "" ).split( "." );
- const bParts = b.replace( rnonDigits, "" ).split( "." );
-
- if ( aParts.length > bParts.length ) {
- return -1;
- }
- if ( aParts.length < bParts.length ) {
- return 1;
- }
-
- for ( let i = 0; i < aParts.length; i++ ) {
- const aPart = Number( aParts[ i ] );
- const bPart = Number( bParts[ i ] );
- if ( aPart < bPart ) {
- return -1;
- }
- if ( aPart > bPart ) {
- return 1;
- }
- }
-
- if ( rnonDigits.test( a ) && !rnonDigits.test( b ) ) {
- return -1;
- }
- if ( !rnonDigits.test( a ) && rnonDigits.test( b ) ) {
- return 1;
- }
-
- return 0;
-}
-
-function sortBrowsers( a, b ) {
- if ( a.browser < b.browser ) {
- return -1;
- }
- if ( a.browser > b.browser ) {
- return 1;
- }
- const browserComparison = compareVersionNumbers(
- a.browser_version,
- b.browser_version
- );
- if ( browserComparison ) {
- return browserComparison;
- }
- if ( a.os < b.os ) {
- return -1;
- }
- if ( a.os > b.os ) {
- return 1;
- }
- const osComparison = compareVersionNumbers( a.os_version, b.os_version );
- if ( osComparison ) {
- return osComparison;
- }
- const deviceComparison = compareVersionNumbers( a.device, b.device );
- if ( deviceComparison ) {
- return deviceComparison;
- }
- return 0;
-}
-
-export async function getBrowsers( { flat = false } = {} ) {
- const query = new URLSearchParams();
- if ( flat ) {
- query.append( "flat", true );
- }
- const browsers = await fetchAPI( `/browsers?${ query }` );
- return browsers.sort( sortBrowsers );
-}
-
-function matchVersion( browserVersion, version ) {
- if ( !version ) {
- return false;
- }
- const regex = new RegExp(
- `^${ version.replace( /\\/g, "\\\\" ).replace( /\./g, "\\." ) }\\b`,
- "i"
- );
- return regex.test( browserVersion );
-}
-
-export async function filterBrowsers( filter ) {
- const browsers = await getBrowsers( { flat: true } );
- if ( !filter ) {
- return browsers;
- }
- const filterBrowser = ( filter.browser ?? "" ).toLowerCase();
- const filterVersion = ( filter.browser_version ?? "" ).toLowerCase();
- const filterOs = ( filter.os ?? "" ).toLowerCase();
- const filterOsVersion = ( filter.os_version ?? "" ).toLowerCase();
- const filterDevice = ( filter.device ?? "" ).toLowerCase();
-
- const filteredWithoutVersion = browsers.filter( ( browser ) => {
- return (
- ( !filterBrowser || filterBrowser === browser.browser.toLowerCase() ) &&
- ( !filterOs || filterOs === browser.os.toLowerCase() ) &&
- ( !filterOsVersion || matchVersion( browser.os_version, filterOsVersion ) ) &&
- ( !filterDevice || filterDevice === ( browser.device || "" ).toLowerCase() )
- );
- } );
-
- if ( !filterVersion ) {
- return filteredWithoutVersion;
- }
-
- if ( filterVersion.startsWith( "latest" ) ) {
- const groupedByName = filteredWithoutVersion
- .filter( ( b ) => rfinalVersion.test( b.browser_version ) )
- .reduce( ( acc, browser ) => {
- acc[ browser.browser ] = acc[ browser.browser ] ?? [];
- acc[ browser.browser ].push( browser );
- return acc;
- }, Object.create( null ) );
-
- const filtered = [];
- for ( const group of Object.values( groupedByName ) ) {
- const latest = group[ group.length - 1 ];
-
- // Mobile devices do not have browser version.
- // Skip the version check for these,
- // but include the latest in the list if it made it
- // through filtering.
- if ( !latest.browser_version ) {
-
- // Do not include in the list for latest-n.
- if ( filterVersion === "latest" ) {
- filtered.push( latest );
- }
- continue;
- }
-
- // Get the latest version and subtract the number from the filter,
- // ignoring any patch versions, which may differ between major versions.
- const num = rlatest.exec( filterVersion );
- const version = parseInt( latest.browser_version ) - ( num ? num[ 1 ] : 0 );
- const match = group.findLast( ( browser ) => {
- return matchVersion( browser.browser_version, version.toString() );
- } );
- if ( match ) {
- filtered.push( match );
- }
- }
- return filtered;
- }
-
- return filteredWithoutVersion.filter( ( browser ) => {
- return matchVersion( browser.browser_version, filterVersion );
- } );
-}
-
-export async function listBrowsers( filter ) {
- const browsers = await filterBrowsers( filter );
- console.log( "Available browsers:" );
- for ( const browser of browsers ) {
- let message = ` ${ browser.browser }_`;
- if ( browser.device ) {
- message += `:${ browser.device }_`;
- } else {
- message += `${ browser.browser_version }_`;
- }
- message += `${ browser.os }_${ browser.os_version }`;
- console.log( message );
- }
-}
-
-export async function getLatestBrowser( filter ) {
- if ( !filter.browser_version ) {
- filter.browser_version = "latest";
- }
- const browsers = await filterBrowsers( filter );
- return browsers[ browsers.length - 1 ];
-}
-
-/**
- * =============================
- * Workers API
- * =============================
- */
-
-/**
- * A browser object may only have one of `browser` or `device` set;
- * which property is set will depend on `os`.
- *
- * `options`: is an object with the following properties:
- * `os`: The operating system.
- * `os_version`: The operating system version.
- * `browser`: The browser name.
- * `browser_version`: The browser version.
- * `device`: The device name.
- * `url` (optional): Which URL to navigate to upon creation.
- * `timeout` (optional): Maximum life of the worker (in seconds). Maximum value of `1800`. Specifying `0` will use the default of `300`.
- * `name` (optional): Provide a name for the worker.
- * `build` (optional): Group workers into a build.
- * `project` (optional): Provide the project the worker belongs to.
- * `resolution` (optional): Specify the screen resolution (e.g. "1024x768").
- * `browserstack.local` (optional): Set to `true` to mark as local testing.
- * `browserstack.video` (optional): Set to `false` to disable video recording.
- * `browserstack.localIdentifier` (optional): ID of the local tunnel.
- */
-export function createWorker( options ) {
- return fetchAPI( "/worker", {
- method: "POST",
- body: JSON.stringify( options )
- } );
-}
-
-/**
- * Returns a worker object, if one exists, with the following properties:
- * `id`: The worker id.
- * `status`: A string representing the current status of the worker.
- * Possible statuses: `"running"`, `"queue"`.
- */
-export function getWorker( id ) {
- return fetchAPI( `/worker/${ id }` );
-}
-
-export async function deleteWorker( id ) {
- return fetchAPI( `/worker/${ id }`, { method: "DELETE" } );
-}
-
-export function getWorkers() {
- return fetchAPI( "/workers" );
-}
-
-/**
- * Stop all workers
- */
-export async function stopWorkers() {
- const workers = await getWorkers();
-
- // Run each request on its own
- // to avoid connect timeout errors.
- console.log( `${ workers.length } workers running...` );
- for ( const worker of workers ) {
- try {
- await deleteWorker( worker.id );
- } catch ( error ) {
-
- // Log the error, but continue trying to remove workers.
- console.error( error );
- }
- }
- console.log( "All workers stopped." );
-}
-
-/**
- * =============================
- * Plan API
- * =============================
- */
-
-export function getPlan() {
- return fetchAPI( "/automate/plan.json", {}, false );
-}
-
-export async function getAvailableSessions() {
- try {
- const [ plan, workers ] = await Promise.all( [ getPlan(), getWorkers() ] );
- return plan.parallel_sessions_max_allowed - workers.length;
- } catch ( error ) {
- console.error( error );
- return 0;
- }
-}
diff --git a/test/runner/browserstack/buildBrowserFromString.js b/test/runner/browserstack/buildBrowserFromString.js
deleted file mode 100644
index e0d99a039..000000000
--- a/test/runner/browserstack/buildBrowserFromString.js
+++ /dev/null
@@ -1,20 +0,0 @@
-export function buildBrowserFromString( str ) {
- const [ browser, versionOrDevice, os, osVersion ] = str.split( "_" );
-
- // If the version starts with a colon, it's a device
- if ( versionOrDevice && versionOrDevice.startsWith( ":" ) ) {
- return {
- browser,
- device: versionOrDevice.slice( 1 ),
- os,
- os_version: osVersion
- };
- }
-
- return {
- browser,
- browser_version: versionOrDevice,
- os,
- os_version: osVersion
- };
-}
diff --git a/test/runner/browserstack/createAuthHeader.js b/test/runner/browserstack/createAuthHeader.js
deleted file mode 100644
index fe4831e9a..000000000
--- a/test/runner/browserstack/createAuthHeader.js
+++ /dev/null
@@ -1,7 +0,0 @@
-const textEncoder = new TextEncoder();
-
-export function createAuthHeader( username, accessKey ) {
- const encoded = textEncoder.encode( `${ username }:${ accessKey }` );
- const base64 = btoa( String.fromCodePoint.apply( null, encoded ) );
- return `Basic ${ base64 }`;
-}
diff --git a/test/runner/browserstack/local.js b/test/runner/browserstack/local.js
deleted file mode 100644
index c84cf155c..000000000
--- a/test/runner/browserstack/local.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import browserstackLocal from "browserstack-local";
-
-export async function localTunnel( localIdentifier, opts = {} ) {
- const tunnel = new browserstackLocal.Local();
-
- return new Promise( ( resolve, reject ) => {
-
- // https://www.browserstack.com/docs/local-testing/binary-params
- tunnel.start(
- {
- "enable-logging-for-api": "",
- localIdentifier,
- ...opts
- },
- async( error ) => {
- if ( error || !tunnel.isRunning() ) {
- return reject( error );
- }
- resolve( {
- stop: function stopTunnel() {
- return new Promise( ( resolve, reject ) => {
- tunnel.stop( ( error ) => {
- if ( error ) {
- return reject( error );
- }
- resolve();
- } );
- } );
- }
- } );
- }
- );
- } );
-}
diff --git a/test/runner/command.js b/test/runner/command.js
deleted file mode 100644
index 4bb7a9c6a..000000000
--- a/test/runner/command.js
+++ /dev/null
@@ -1,134 +0,0 @@
-import yargs from "yargs/yargs";
-import { browsers } from "./flags/browsers.js";
-import { getPlan, listBrowsers, stopWorkers } from "./browserstack/api.js";
-import { buildBrowserFromString } from "./browserstack/buildBrowserFromString.js";
-import { modules } from "./flags/modules.js";
-import { run } from "./run.js";
-
-const argv = yargs( process.argv.slice( 2 ) )
- .version( false )
- .strict()
- .command( {
- command: "[options]",
- describe: "Run jQuery tests in a browser"
- } )
- .option( "module", {
- alias: "m",
- type: "array",
- choices: modules,
- description:
- "Run tests for a specific module. " +
- "Pass multiple modules by repeating the option. " +
- "Defaults to all modules."
- } )
- .option( "browser", {
- alias: "b",
- type: "array",
- choices: browsers,
- description:
- "Run tests in a specific browser." +
- "Pass multiple browsers by repeating the option." +
- "If using BrowserStack, specify browsers using --browserstack." +
- "Only the basic module is supported on jsdom.",
- default: [ "chrome" ]
- } )
- .option( "headless", {
- alias: "h",
- type: "boolean",
- description:
- "Run tests in headless mode. Cannot be used with --debug or --browserstack.",
- conflicts: [ "debug", "browserstack" ]
- } )
- .option( "esm", {
- alias: "esmodules",
- type: "boolean",
- description: "Run tests using jQuery's source, which is written with ECMAScript Modules."
- } )
- .option( "concurrency", {
- alias: "c",
- type: "number",
- description:
- "Run tests in parallel in multiple browsers. " +
- "Defaults to 8 in normal mode. In browserstack mode, " +
- "defaults to the maximum available under your BrowserStack plan."
- } )
- .option( "debug", {
- alias: "d",
- type: "boolean",
- description:
- "Leave the browser open for debugging. Cannot be used with --headless.",
- conflicts: [ "headless" ]
- } )
- .option( "retries", {
- alias: "r",
- type: "number",
- description: "Number of times to retry failed tests by refreshing the URL."
- } )
- .option( "hard-retries", {
- type: "number",
- description:
- "Number of times to retry failed tests by restarting the worker. " +
- "This is in addition to the normal retries " +
- "and are only used when the normal retries are exhausted."
- } )
- .option( "verbose", {
- alias: "v",
- type: "boolean",
- description: "Log additional information."
- } )
- .option( "isolate", {
- type: "boolean",
- description: "Run each module by itself in the test page. This can extend testing time."
- } )
- .option( "browserstack", {
- type: "array",
- description:
- "Run tests in BrowserStack.\n" +
- "Requires BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY environment variables.\n" +
- "The value can be empty for the default configuration, or a string in the format of\n" +
- "\"browser_[browserVersion | :device]_os_osVersion\" (see --list-browsers).\n" +
- "Pass multiple browsers by repeating the option.\n" +
- "The --browser option is ignored when --browserstack has a value.\n" +
- "Otherwise, the --browser option will be used, " +
- "with the latest version/device for that browser, on a matching OS."
- } )
- .option( "run-id", {
- type: "string",
- description: "A unique identifier for the run in BrowserStack."
- } )
- .option( "list-browsers", {
- type: "string",
- description:
- "List available BrowserStack browsers and exit.\n" +
- "Leave blank to view all browsers or pass " +
- "\"browser_[browserVersion | :device]_os_osVersion\" with each parameter " +
- "separated by an underscore to filter the list (any can be omitted).\n" +
- "\"latest\" can be used in place of \"browserVersion\" to find the latest version.\n" +
- "\"latest-n\" can be used to find the nth latest browser version.\n" +
- "Use a colon to indicate a device.\n" +
- "Examples: \"chrome__windows_10\", \"safari_latest\", " +
- "\"Mobile Safari\", \"Android Browser_:Google Pixel 8 Pro\".\n" +
- "Use quotes if spaces are necessary."
- } )
- .option( "stop-workers", {
- type: "boolean",
- description:
- "WARNING: This will stop all BrowserStack workers that may exist and exit," +
- "including any workers running from other projects.\n" +
- "This can be used as a failsafe when there are too many stray workers."
- } )
- .option( "browserstack-plan", {
- type: "boolean",
- description: "Show BrowserStack plan information and exit."
- } )
- .help().argv;
-
-if ( typeof argv.listBrowsers === "string" ) {
- listBrowsers( buildBrowserFromString( argv.listBrowsers ) );
-} else if ( argv.stopWorkers ) {
- stopWorkers();
-} else if ( argv.browserstackPlan ) {
- console.log( await getPlan() );
-} else {
- run( argv );
-}
diff --git a/test/runner/createTestServer.js b/test/runner/createTestServer.js
deleted file mode 100644
index 9b9810bcb..000000000
--- a/test/runner/createTestServer.js
+++ /dev/null
@@ -1,192 +0,0 @@
-import http from "node:http";
-import { readFile, stat } from "node:fs/promises";
-import { createReadStream } from "node:fs";
-import mockServer from "../middleware-mockserver.cjs";
-import getRawBody from "raw-body";
-
-export async function createTestServer( report, { quiet } = {} ) {
- const indexHTML = await readFile( "./test/index.html", "utf8" );
-
- // Support connect-style middleware
- const middlewares = [];
- function use( middleware ) {
- middlewares.push( middleware );
- }
-
- function run( req, res ) {
- let i = 0;
-
- // Log responses unless quiet is set
- if ( !quiet ) {
- const originalEnd = res.end;
- res.end = function( ...args ) {
- console.log( `${ req.method } ${ req.url } ${ this.statusCode }` );
- originalEnd.call( this, ...args );
- };
- }
-
- // Add a parsed URL object to the request object
- req.parsedUrl = new URL(
- `http://${ process.env.HOST ?? "localhost" }${ req.url }`
- );
-
- // Add a simplified redirect helper to the response object
- res.redirect = ( status, location ) => {
- if ( !location ) {
- location = status;
- status = 303;
- }
-
- res.writeHead( status, { Location: location } );
- res.end();
- };
-
- const next = () => {
- const middleware = middlewares[ i++ ];
- if ( middleware ) {
- try {
- middleware( req, res, next );
- } catch ( error ) {
- console.error( error );
- res.writeHead( 500, { "Content-Type": "application/json" } );
- res.end( "Internal Server Error" );
- }
- } else {
- res.writeHead( 404 );
- res.end();
- }
- };
-
- next();
- }
-
- // Redirect home to test page
- use( ( req, res, next ) => {
- if ( req.parsedUrl.pathname === "/" ) {
- res.redirect( "/test/" );
- } else {
- next();
- }
- } );
-
- // Redirect to trailing slash
- use( ( req, res, next ) => {
- if ( req.parsedUrl.pathname === "/test" ) {
- res.redirect( 308, `${ req.parsedUrl.pathname }/${ req.parsedUrl.search }` );
- } else {
- next();
- }
- } );
-
- // Add a script tag to the index.html to load the QUnit listeners
- use( ( req, res, next ) => {
- if (
- ( req.method === "GET" || req.method === "HEAD" ) &&
- ( req.parsedUrl.pathname === "/test/" ||
- req.parsedUrl.pathname === "/test/index.html" )
- ) {
- res.writeHead( 200, { "Content-Type": "text/html" } );
- res.end(
- indexHTML.replace(
- "</head>",
- "<script src=\"/test/runner/listeners.js\"></script></head>"
- )
- );
- } else {
- next();
- }
- } );
-
- // Bind the reporter
- use( async( req, res, next ) => {
- if ( req.url !== "/api/report" || req.method !== "POST" ) {
- return next();
- }
- let body;
- try {
- body = JSON.parse( await getRawBody( req ) );
- } catch ( error ) {
- if ( error.code === "ECONNABORTED" ) {
- return;
- }
- console.error( error );
- res.writeHead( 400, { "Content-Type": "application/json" } );
- res.end( JSON.stringify( { error: "Invalid JSON" } ) );
- return;
- }
- const response = await report( body );
- if ( response ) {
- res.writeHead( 200, { "Content-Type": "application/json" } );
- res.end( JSON.stringify( response ) );
- } else {
- res.writeHead( 204 );
- res.end();
- }
- } );
-
- // Hook up mock server
- use( mockServer() );
-
- // Serve static files
- const validMimeTypes = {
-
- // No .mjs or .cjs files are used in tests
- ".js": "application/javascript",
- ".css": "text/css",
- ".html": "text/html",
- ".xml": "application/xml",
- ".xhtml": "application/xhtml+xml",
- ".jpg": "image/jpeg",
- ".png": "image/png",
- ".svg": "image/svg+xml",
- ".ico": "image/x-icon",
- ".map": "application/json",
- ".txt": "text/plain",
- ".log": "text/plain"
- };
- use( async( req, res, next ) => {
- if (
- !req.url.startsWith( "/dist/" ) &&
- !req.url.startsWith( "/src/" ) &&
- !req.url.startsWith( "/test/" ) &&
- !req.url.startsWith( "/external/" )
- ) {
- return next();
- }
- const file = req.parsedUrl.pathname.slice( 1 );
- const ext = file.slice( file.lastIndexOf( "." ) );
-
- // Allow POST to .html files in tests
- if (
- req.method !== "GET" &&
- req.method !== "HEAD" &&
- ( ext !== ".html" || req.method !== "POST" )
- ) {
- return next();
- }
- const mimeType = validMimeTypes[ ext ];
- if ( mimeType ) {
- try {
- await stat( file );
- } catch ( error ) {
- res.writeHead( 404 );
- res.end();
- return;
- }
- res.writeHead( 200, { "Content-Type": mimeType } );
- createReadStream( file )
- .pipe( res )
- .on( "error", ( error ) => {
- console.error( error );
- res.writeHead( 500 );
- res.end();
- } );
- } else {
- console.error( `Invalid file extension: ${ ext }` );
- res.writeHead( 404 );
- res.end();
- }
- } );
-
- return http.createServer( run );
-}
diff --git a/test/runner/flags/browsers.js b/test/runner/flags/browsers.js
deleted file mode 100644
index c15d7085e..000000000
--- a/test/runner/flags/browsers.js
+++ /dev/null
@@ -1,25 +0,0 @@
-// This list is static, so no requests are required
-// in the command help menu.
-
-import { getBrowsers } from "../browserstack/api.js";
-
-export const browsers = [
- "chrome",
- "ie",
- "firefox",
- "edge",
- "safari",
- "opera",
- "yandex",
- "IE Mobile",
- "Android Browser",
- "Mobile Safari",
- "jsdom"
-];
-
-// A function that can be used to update the above list.
-export async function getAvailableBrowsers() {
- const browsers = await getBrowsers( { flat: true } );
- const available = [ ...new Set( browsers.map( ( { browser } ) => browser ) ) ];
- return available.concat( "jsdom" );
-}
diff --git a/test/runner/flags/modules.js b/test/runner/flags/modules.js
deleted file mode 100644
index 53f9a933a..000000000
--- a/test/runner/flags/modules.js
+++ /dev/null
@@ -1,24 +0,0 @@
-export const modules = [
- "basic",
-
- "ajax",
- "animation",
- "attributes",
- "callbacks",
- "core",
- "css",
- "data",
- "deferred",
- "deprecated",
- "dimensions",
- "effects",
- "event",
- "manipulation",
- "offset",
- "queue",
- "selector",
- "serialize",
- "support",
- "traversing",
- "tween"
-];
diff --git a/test/runner/jsdom/createWindow.js b/test/runner/jsdom/createWindow.js
deleted file mode 100644
index de6c63ffa..000000000
--- a/test/runner/jsdom/createWindow.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import jsdom from "jsdom";
-
-const { JSDOM } = jsdom;
-
-export default async function createWindow( { reportId, url, verbose } ) {
- const virtualConsole = new jsdom.VirtualConsole();
- virtualConsole.sendTo( console );
- virtualConsole.removeAllListeners( "clear" );
-
- const { window } = await JSDOM.fromURL( url, {
- resources: "usable",
- runScripts: "dangerously",
- virtualConsole
- } );
-
- if ( verbose ) {
- console.log( `JSDOM window created (${ reportId })` );
- }
-
- return window;
-}
diff --git a/test/runner/lib/buildTestUrl.js b/test/runner/lib/buildTestUrl.js
deleted file mode 100644
index 6e0f1a9b0..000000000
--- a/test/runner/lib/buildTestUrl.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import { generateModuleId } from "./generateHash.js";
-
-export function buildTestUrl( modules, { browserstack, esm, jsdom, port, reportId } ) {
- if ( !port ) {
- throw new Error( "No port specified." );
- }
-
- const query = new URLSearchParams();
- for ( const module of modules ) {
- query.append( "moduleId", generateModuleId( module ) );
- }
-
- if ( esm ) {
- query.append( "esmodules", "true" );
- }
-
- if ( jsdom ) {
- query.append( "jsdom", "true" );
- }
-
- if ( reportId ) {
- query.append( "reportId", reportId );
- }
-
- // BrowserStack supplies a custom domain for local testing,
- // which is especially necessary for iOS testing.
- const host = browserstack ? "bs-local.com" : "localhost";
- return `http://${ host }:${ port }/test/?${ query }`;
-}
diff --git a/test/runner/lib/generateHash.js b/test/runner/lib/generateHash.js
deleted file mode 100644
index dbbd4b476..000000000
--- a/test/runner/lib/generateHash.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import crypto from "node:crypto";
-
-export function generateHash( string ) {
- const hash = crypto.createHash( "md5" );
- hash.update( string );
-
- // QUnit hashes are 8 characters long
- // We use 10 characters to be more visually distinct
- return hash.digest( "hex" ).slice( 0, 10 );
-}
-
-/**
- * A copy of the generate hash function from QUnit,
- * used to generate a hash for the module name.
- *
- * QUnit errors on passing multiple modules to the
- * module query parameter. We need to know
- * the hash for each module before loading QUnit
- * in order to pass multiple moduleId parameters instead.
- */
-export function generateModuleId( module, browser ) {
-
- // QUnit normally hashes the test name, but
- // we've repurposed this function to generate
- // report IDs for module/browser combinations.
- // We still use it without the browser parameter
- // to get the same module IDs as QUnit to pass
- // multiple ahead-of-time in the query string.
- const str = module + "\x1C" + browser;
- let hash = 0;
-
- for ( let i = 0; i < str.length; i++ ) {
- hash = ( hash << 5 ) - hash + str.charCodeAt( i );
- hash |= 0;
- }
-
- let hex = ( 0x100000000 + hash ).toString( 16 );
- if ( hex.length < 8 ) {
- hex = "0000000" + hex;
- }
-
- return hex.slice( -8 );
-}
-
-export function printModuleHashes( modules ) {
- console.log( "Module hashes:" );
- modules.forEach( ( module ) => {
- console.log( ` ${ module }: ${ generateModuleId( module ) }` );
- } );
-}
diff --git a/test/runner/lib/getBrowserString.js b/test/runner/lib/getBrowserString.js
deleted file mode 100644
index 413a60500..000000000
--- a/test/runner/lib/getBrowserString.js
+++ /dev/null
@@ -1,49 +0,0 @@
-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;
-}
diff --git a/test/runner/lib/prettyMs.js b/test/runner/lib/prettyMs.js
deleted file mode 100644
index 99bae2b35..000000000
--- a/test/runner/lib/prettyMs.js
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * Pretty print a time in milliseconds.
- */
-export function prettyMs( time ) {
- const minutes = Math.floor( time / 60000 );
- const seconds = Math.floor( time / 1000 );
- const ms = Math.floor( time % 1000 );
-
- let prettyTime = `${ ms }ms`;
- if ( seconds > 0 ) {
- prettyTime = `${ seconds }s ${ prettyTime }`;
- }
- if ( minutes > 0 ) {
- prettyTime = `${ minutes }m ${ prettyTime }`;
- }
-
- return prettyTime;
-}
diff --git a/test/runner/listeners.js b/test/runner/listeners.js
deleted file mode 100644
index 61a98e7ce..000000000
--- a/test/runner/listeners.js
+++ /dev/null
@@ -1,110 +0,0 @@
-( function() {
-
- "use strict";
-
- // Get the report ID from the URL.
- var match = location.search.match( /reportId=([^&]+)/ );
- if ( !match ) {
- return;
- }
- var id = match[ 1 ];
-
- // Adopted from https://github.com/douglascrockford/JSON-js
- // Support: IE 11+
- // Using the replacer argument of JSON.stringify in IE has issues
- // TODO: Replace this with a circular replacer + JSON.stringify + WeakSet
- function decycle( object ) {
- var objects = [];
-
- // The derez function recurses through the object, producing the deep copy.
- function derez( value ) {
- if (
- typeof value === "object" &&
- value !== null &&
- !( value instanceof Boolean ) &&
- !( value instanceof Date ) &&
- !( value instanceof Number ) &&
- !( value instanceof RegExp ) &&
- !( value instanceof String )
- ) {
-
- // Return a string early for elements
- if ( value.nodeType ) {
- return value.toString();
- }
-
- if ( objects.indexOf( value ) > -1 ) {
- return;
- }
-
- objects.push( value );
-
- if ( Array.isArray( value ) ) {
-
- // If it is an array, replicate the array.
- return value.map( derez );
- } else {
-
- // If it is an object, replicate the object.
- var nu = Object.create( null );
- Object.keys( value ).forEach( function( name ) {
- nu[ name ] = derez( value[ name ] );
- } );
- return nu;
- }
- }
-
- // Serialize Symbols as string representations so they are
- // sent over the wire after being stringified.
- if ( typeof value === "symbol" ) {
-
- // We can *describe* unique symbols, but note that their identity
- // (e.g., `Symbol() !== Symbol()`) is lost
- var ctor = Symbol.keyFor( value ) !== undefined ? "Symbol.for" : "Symbol";
- return ctor + "(" + JSON.stringify( value.description ) + ")";
- }
-
- return value;
- }
- return derez( object );
- }
-
- function send( type, data ) {
- var json = JSON.stringify( {
- id: id,
- type: type,
- data: data ? decycle( data ) : undefined
- } );
- var request = new XMLHttpRequest();
- request.open( "POST", "/api/report", true );
- request.setRequestHeader( "Content-Type", "application/json" );
- request.send( json );
- return request;
- }
-
- // Send acknowledgement to the server.
- send( "ack" );
-
- QUnit.on( "testEnd", function( data ) {
- send( "testEnd", data );
- } );
-
- QUnit.on( "runEnd", function( data ) {
-
- // Reduce the payload size.
- // childSuites is large and unused.
- data.childSuites = undefined;
-
- var request = send( "runEnd", data );
- request.onload = function() {
- if ( request.status === 200 && request.responseText ) {
- try {
- var json = JSON.parse( request.responseText );
- window.location = json.url;
- } catch ( e ) {
- console.error( e );
- }
- }
- };
- } );
-} )();
diff --git a/test/runner/queue.js b/test/runner/queue.js
deleted file mode 100644
index 843d5672f..000000000
--- a/test/runner/queue.js
+++ /dev/null
@@ -1,119 +0,0 @@
-import chalk from "chalk";
-import { getBrowserString } from "./lib/getBrowserString.js";
-import {
- checkLastTouches,
- createBrowserWorker,
- restartBrowser,
- setBrowserWorkerUrl
-} from "./browsers.js";
-
-const TEST_POLL_TIMEOUT = 1000;
-
-const queue = [];
-
-export function getNextBrowserTest( reportId ) {
- const index = queue.findIndex( ( test ) => test.id === reportId );
- if ( index === -1 ) {
- return;
- }
-
- // Remove the completed test from the queue
- const previousTest = queue[ index ];
- queue.splice( index, 1 );
-
- // Find the next test for the same browser
- for ( const test of queue.slice( index ) ) {
- if ( test.fullBrowser === previousTest.fullBrowser ) {
-
- // Set the URL for our tracking
- setBrowserWorkerUrl( test.browser, test.url );
- test.running = true;
-
- // Return the URL for the next test.
- // listeners.js will use this to set the browser URL.
- return { url: test.url };
- }
- }
-}
-
-export function retryTest( reportId, maxRetries ) {
- if ( !maxRetries ) {
- return;
- }
- const test = queue.find( ( test ) => test.id === reportId );
- if ( test ) {
- test.retries++;
- if ( test.retries <= maxRetries ) {
- console.log(
- `\nRetrying test ${ reportId } for ${ chalk.yellow(
- test.options.modules.join( ", " )
- ) }...${ test.retries }`
- );
- return test;
- }
- }
-}
-
-export async function hardRetryTest( reportId, maxHardRetries ) {
- if ( !maxHardRetries ) {
- return false;
- }
- const test = queue.find( ( test ) => test.id === reportId );
- if ( test ) {
- test.hardRetries++;
- if ( test.hardRetries <= maxHardRetries ) {
- console.log(
- `\nHard retrying test ${ reportId } for ${ chalk.yellow(
- test.options.modules.join( ", " )
- ) }...${ test.hardRetries }`
- );
- await restartBrowser( test.browser );
- return true;
- }
- }
- return false;
-}
-
-export function addRun( url, browser, options ) {
- queue.push( {
- browser,
- fullBrowser: getBrowserString( browser ),
- hardRetries: 0,
- id: options.reportId,
- url,
- options,
- retries: 0,
- running: false
- } );
-}
-
-export async function runAll() {
- return new Promise( async( resolve, reject ) => {
- while ( queue.length ) {
- try {
- await checkLastTouches();
- } catch ( error ) {
- reject( error );
- }
-
- // Run one test URL per browser at a time
- const browsersTaken = [];
- for ( const test of queue ) {
- if ( browsersTaken.indexOf( test.fullBrowser ) > -1 ) {
- continue;
- }
- browsersTaken.push( test.fullBrowser );
- if ( !test.running ) {
- test.running = true;
- try {
- await createBrowserWorker( test.url, test.browser, test.options );
- } catch ( error ) {
- reject( error );
- }
- }
- }
- await new Promise( ( resolve ) => setTimeout( resolve, TEST_POLL_TIMEOUT ) );
- }
- resolve();
- } );
-}
diff --git a/test/runner/reporter.js b/test/runner/reporter.js
deleted file mode 100644
index e79059648..000000000
--- a/test/runner/reporter.js
+++ /dev/null
@@ -1,132 +0,0 @@
-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, modules } ) {
- const fullBrowser = getBrowserString( browser, headless );
- console.log(
- `\n\nTests finished in ${ prettyMs( result.runtime ) } ` +
- `for ${ chalk.yellow( modules.join( "," ) ) } ` +
- `in ${ chalk.yellow( fullBrowser ) } (${ chalk.bold( reportId ) })...`
- );
- console.log(
- ( result.status !== "passed" ?
- `${ chalk.red( result.testCounts.failed ) } failed. ` :
- "" ) +
- `${ chalk.green( result.testCounts.total ) } passed. ` +
- `${ chalk.gray( result.testCounts.skipped ) } skipped.`
- );
- return result.testCounts;
-}
diff --git a/test/runner/run.js b/test/runner/run.js
deleted file mode 100644
index 4ee0bac2a..000000000
--- a/test/runner/run.js
+++ /dev/null
@@ -1,338 +0,0 @@
-import chalk from "chalk";
-import { asyncExitHook, gracefulExit } from "exit-hook";
-import { getLatestBrowser } from "./browserstack/api.js";
-import { buildBrowserFromString } from "./browserstack/buildBrowserFromString.js";
-import { localTunnel } from "./browserstack/local.js";
-import { reportEnd, reportTest } from "./reporter.js";
-import { createTestServer } from "./createTestServer.js";
-import { buildTestUrl } from "./lib/buildTestUrl.js";
-import { generateHash, printModuleHashes } from "./lib/generateHash.js";
-import { getBrowserString } from "./lib/getBrowserString.js";
-import { modules as allModules } from "./flags/modules.js";
-import { cleanupAllBrowsers, touchBrowser } from "./browsers.js";
-import {
- addRun,
- getNextBrowserTest,
- hardRetryTest,
- retryTest,
- runAll
-} from "./queue.js";
-
-const EXIT_HOOK_WAIT_TIMEOUT = 60 * 1000;
-
-/**
- * Run modules in parallel in different browser instances.
- */
-export async function run( {
- browser: browserNames = [],
- browserstack,
- concurrency,
- debug,
- esm,
- hardRetries,
- headless,
- isolate,
- module: modules = [],
- retries = 0,
- runId,
- verbose
-} ) {
- if ( !browserNames.length ) {
- browserNames = [ "chrome" ];
- }
- if ( !modules.length ) {
- modules = allModules;
- }
- if ( headless && debug ) {
- throw new Error(
- "Cannot run in headless mode and debug mode at the same time."
- );
- }
-
- if ( verbose ) {
- console.log( browserstack ? "Running in BrowserStack." : "Running locally." );
- }
-
- const errorMessages = [];
- const pendingErrors = {};
-
- // Convert browser names to browser objects
- let browsers = browserNames.map( ( b ) => ( { browser: b } ) );
- const tunnelId = generateHash(
- `${ Date.now() }-${ modules.join( ":" ) }-${ ( browserstack || [] )
- .concat( browserNames )
- .join( ":" ) }`
- );
-
- // A unique identifier for this run
- if ( !runId ) {
- runId = tunnelId;
- }
-
- // Create the test app and
- // hook it up to the reporter
- const reports = Object.create( null );
- const app = await createTestServer( async( message ) => {
- switch ( message.type ) {
- case "testEnd": {
- const reportId = message.id;
- const report = reports[ reportId ];
- touchBrowser( report.browser );
- const errors = reportTest( message.data, reportId, report );
- pendingErrors[ reportId ] ??= Object.create( null );
- if ( errors ) {
- pendingErrors[ reportId ][ message.data.name ] = errors;
- } else {
- const existing = pendingErrors[ reportId ][ message.data.name ];
-
- // Show a message for flakey tests
- if ( existing ) {
- console.log();
- console.warn(
- chalk.italic(
- chalk.gray( existing.replace( "Test failed", "Test flakey" ) )
- )
- );
- console.log();
- delete pendingErrors[ reportId ][ message.data.name ];
- }
- }
- break;
- }
- case "runEnd": {
- const reportId = message.id;
- const report = reports[ reportId ];
- touchBrowser( report.browser );
- const { failed, total } = reportEnd(
- message.data,
- message.id,
- reports[ reportId ]
- );
- report.total = total;
-
- // Handle failure
- if ( failed ) {
- const retry = retryTest( reportId, retries );
-
- // Retry if retryTest returns a test
- if ( retry ) {
- return retry;
- }
-
- // Return early if hardRetryTest returns true
- if ( await hardRetryTest( reportId, hardRetries ) ) {
- return;
- }
- errorMessages.push( ...Object.values( pendingErrors[ reportId ] ) );
- }
-
- // Run the next test
- return getNextBrowserTest( reportId );
- }
- case "ack": {
- const report = reports[ message.id ];
- touchBrowser( report.browser );
- break;
- }
- default:
- console.warn( "Received unknown message type:", message.type );
- }
-
- // Hide test server request logs in CLI output
- }, { quiet: true } );
-
- // Start up local test server
- let server;
- let port;
- await new Promise( ( resolve ) => {
-
- // Pass 0 to choose a random, unused port
- server = app.listen( 0, () => {
- port = server.address().port;
- resolve();
- } );
- } );
-
- if ( !server || !port ) {
- throw new Error( "Server not started." );
- }
-
- if ( verbose ) {
- console.log( `Server started on port ${ port }.` );
- }
-
- function stopServer() {
- return new Promise( ( resolve ) => {
- server.close( () => {
- if ( verbose ) {
- console.log( "Server stopped." );
- }
- resolve();
- } );
- } );
- }
-
- async function cleanup() {
- console.log( "Cleaning up..." );
-
- await cleanupAllBrowsers( { verbose } );
-
- if ( tunnel ) {
- await tunnel.stop();
- if ( verbose ) {
- console.log( "Stopped BrowserStackLocal." );
- }
- }
- }
-
- asyncExitHook(
- async() => {
- await cleanup();
- await stopServer();
- },
- { wait: EXIT_HOOK_WAIT_TIMEOUT }
- );
-
- // Start up BrowserStackLocal
- let tunnel;
- if ( browserstack ) {
- if ( headless ) {
- console.warn(
- chalk.italic(
- "BrowserStack does not support headless mode. Running in normal mode."
- )
- );
- headless = false;
- }
-
- // Convert browserstack to browser objects.
- // If browserstack is an empty array, fall back
- // to the browsers array.
- if ( browserstack.length ) {
- browsers = browserstack.map( ( b ) => {
- if ( !b ) {
- return browsers[ 0 ];
- }
- return buildBrowserFromString( b );
- } );
- }
-
- // Fill out browser defaults
- browsers = await Promise.all(
- browsers.map( async( browser ) => {
-
- // Avoid undici connect timeout errors
- await new Promise( ( resolve ) => setTimeout( resolve, 100 ) );
-
- const latestMatch = await getLatestBrowser( browser );
- if ( !latestMatch ) {
- console.error(
- chalk.red( `Browser not found: ${ getBrowserString( browser ) }.` )
- );
- gracefulExit( 1 );
- }
- return latestMatch;
- } )
- );
-
- tunnel = await localTunnel( tunnelId );
- if ( verbose ) {
- console.log( "Started BrowserStackLocal." );
-
- printModuleHashes( modules );
- }
- }
-
- function queueRun( modules, browser ) {
- const fullBrowser = getBrowserString( browser, headless );
- const reportId = generateHash( `${ modules.join( ":" ) } ${ fullBrowser }` );
- reports[ reportId ] = { browser, headless, modules };
-
- const url = buildTestUrl( modules, {
- browserstack,
- esm,
- jsdom: browser.browser === "jsdom",
- port,
- reportId
- } );
-
- const options = {
- browserstack,
- concurrency,
- debug,
- headless,
- modules,
- reportId,
- runId,
- tunnelId,
- verbose
- };
-
- addRun( url, browser, options );
- }
-
- for ( const browser of browsers ) {
- if ( isolate ) {
- for ( const module of modules ) {
- queueRun( [ module ], browser );
- }
- } else {
- queueRun( modules, browser );
- }
- }
-
- try {
- console.log( `Starting Run ${ runId }...` );
- await runAll();
- } catch ( error ) {
- console.error( error );
- if ( !debug ) {
- gracefulExit( 1 );
- }
- } finally {
- console.log();
- if ( errorMessages.length === 0 ) {
- let stop = false;
- for ( const report of Object.values( reports ) ) {
- if ( !report.total ) {
- stop = true;
- console.error(
- chalk.red(
- `No tests were run for ${ report.modules.join(
- ", "
- ) } in ${ getBrowserString( report.browser ) }`
- )
- );
- }
- }
- if ( stop ) {
- return gracefulExit( 1 );
- }
- console.log( chalk.green( "All tests passed!" ) );
-
- if ( !debug || browserstack ) {
- gracefulExit( 0 );
- }
- } else {
- console.error( chalk.red( `${ errorMessages.length } tests failed.` ) );
- console.log(
- errorMessages.map( ( error, i ) => `\n${ i + 1 }. ${ error }` ).join( "\n" )
- );
-
- if ( debug ) {
- console.log();
- if ( browserstack ) {
- console.log( "Leaving browsers with failures open for debugging." );
- console.log(
- "View running sessions at https://automate.browserstack.com/dashboard/v2/"
- );
- } else {
- console.log( "Leaving browsers open for debugging." );
- }
- console.log( "Press Ctrl+C to exit." );
- } else {
- gracefulExit( 1 );
- }
- }
- }
-}
diff --git a/test/runner/selenium/createDriver.js b/test/runner/selenium/createDriver.js
deleted file mode 100644
index df1204763..000000000
--- a/test/runner/selenium/createDriver.js
+++ /dev/null
@@ -1,97 +0,0 @@
-import { Builder, Capabilities, logging } from "selenium-webdriver";
-import Chrome from "selenium-webdriver/chrome.js";
-import Edge from "selenium-webdriver/edge.js";
-import Firefox from "selenium-webdriver/firefox.js";
-import IE from "selenium-webdriver/ie.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 ]();
-
- // Support: IE 11+
- // When those are set for IE, the process crashes with an error:
- // "Unable to match capability set 0: goog:loggingPrefs is an unknown
- // extension capability for IE".
- if ( browserName !== "ie" ) {
- 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 );
- }
-
- const ieOptions = new IE.Options();
- ieOptions.setEdgeChromium( true );
- ieOptions.setEdgePath( "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe" );
-
- 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 )
- .setIeOptions( ieOptions )
- .build();
-
- if ( verbose ) {
- const driverCapabilities = await driver.getCapabilities();
- const name = driverCapabilities.getBrowserName();
- const version = driverCapabilities.getBrowserVersion();
- console.log( `\nDriver created for ${ name } ${ version }` );
- }
-
- // Increase script timeout to 10min
- await driver.manage().setTimeouts( { script: DRIVER_SCRIPT_TIMEOUT } );
-
- // Set the first URL for the browser
- await driver.get( url );
-
- return driver;
-}
diff --git a/test/runner/server.js b/test/runner/server.js
deleted file mode 100644
index 09fe0da4c..000000000
--- a/test/runner/server.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import { createTestServer } from "./createTestServer.js";
-
-const port = process.env.PORT || 3000;
-
-async function runServer() {
- const app = await createTestServer();
-
- app.listen( { port, host: "0.0.0.0" }, function() {
- console.log( `Open tests at http://localhost:${ port }/test/` );
- } );
-}
-
-runServer();
diff --git a/test/unit/ajax.js b/test/unit/ajax.js
index 53dc9c5d2..4f843f2e5 100644
--- a/test/unit/ajax.js
+++ b/test/unit/ajax.js
@@ -2782,7 +2782,7 @@ if ( typeof window.ArrayBuffer === "undefined" || typeof new XMLHttpRequest().re
} );
} );
- QUnit.test( "jQuery.get( String, null-ish, String ) - dataType with null callback (gh-4989)",
+ QUnit.test( "jQuery.get( String, null, String ) - dataType with null callback (gh-4989)",
function( assert ) {
assert.expect( 2 );
var done = assert.async( 2 );
@@ -2802,6 +2802,37 @@ if ( typeof window.ArrayBuffer === "undefined" || typeof new XMLHttpRequest().re
} );
} );
+ QUnit.test( "jQuery.get( String, null-ish, null-ish, String ) - dataType with null/undefined data & callback",
+ function( assert ) {
+ assert.expect( 8 );
+ var done = assert.async( 8 );
+
+ [
+ { data: null, success: null },
+ { data: null, success: undefined },
+ { data: undefined, success: null },
+ { data: undefined, success: undefined }
+ ].forEach( function( options ) {
+ var data = options.data,
+ success = options.success;
+ jQuery.get( url( "mock.php?action=json&header" ), data, success, "json" )
+ .then( function( json ) {
+ assert.deepEqual( json, { data: { lang: "en", length: 25 } },
+ "`dataType: \"json\"` applied with `" + data + "` data & `" +
+ success + "` success callback" );
+ done();
+ } );
+
+ jQuery.get( url( "mock.php?action=json&header" ), data, success, "text" )
+ .then( function( text ) {
+ assert.strictEqual( text, "{\"data\":{\"lang\":\"en\",\"length\":25}}",
+ "`dataType: \"text\"` applied with `" + data + "` data & `" +
+ success + "` success callback" );
+ done();
+ } );
+ } );
+ } );
+
//----------- jQuery.getJSON()
QUnit.test( "jQuery.getJSON( String, Hash, Function ) - JSON array", function( assert ) {
diff --git a/test/unit/attributes.js b/test/unit/attributes.js
index a54f2d65d..5ace087b9 100644
--- a/test/unit/attributes.js
+++ b/test/unit/attributes.js
@@ -479,6 +479,24 @@ QUnit.test( "attr(String, Object)", function( assert ) {
assert.equal( jQuery( "#name" ).attr( "nonexisting", undefined ).attr( "nonexisting" ), undefined, ".attr('attribute', undefined) does not create attribute (trac-5571)" );
} );
+QUnit.test( "attr( previously-boolean-attr, non-boolean-value)", function( assert ) {
+ assert.expect( 3 );
+
+ var div = jQuery( "<div></div>" ).appendTo( "#qunit-fixture" );
+
+ div.attr( "hidden", "foo" );
+ assert.strictEqual( div.attr( "hidden" ), "foo",
+ "Values not normalized for previously-boolean hidden attribute" );
+
+ div.attr( "hidden", "until-found" );
+ assert.strictEqual( div.attr( "hidden" ), "until-found",
+ "`until-found` value preserved for hidden attribute" );
+
+ div.attr( "hiDdeN", "uNtil-fOund" );
+ assert.strictEqual( div.attr( "hidden" ), "uNtil-fOund",
+ "`uNtil-fOund` different casing preserved" );
+} );
+
QUnit.test( "attr(non-ASCII)", function( assert ) {
assert.expect( 2 );
diff --git a/test/unit/css.js b/test/unit/css.js
index e232f5db0..8a96fb357 100644
--- a/test/unit/css.js
+++ b/test/unit/css.js
@@ -1379,7 +1379,7 @@ testIframe(
function( assert, jQuery, window, document, widthBeforeSet, widthAfterSet ) {
assert.expect( 2 );
- // Support: Firefox 126+
+ // Support: Firefox 126 - 135+
// Newer Firefox implements CSS zoom in a way it affects
// those values slightly.
assert.ok( /^100(?:|\.0\d*)px$/.test( widthBeforeSet ), "elem.css('width') works correctly with browser zoom" );
@@ -1812,14 +1812,9 @@ QUnit.testUnlessIE( "css(--customProperty)", function( assert ) {
var div = jQuery( "<div>" ).appendTo( "#qunit-fixture" ),
$elem = jQuery( "<div>" ).addClass( "test__customProperties" )
- .appendTo( "#qunit-fixture" ),
- webkitOrBlink = /webkit\b/i.test( navigator.userAgent ),
- expected = 20;
+ .appendTo( "#qunit-fixture" );
- if ( webkitOrBlink ) {
- expected -= 2;
- }
- assert.expect( expected );
+ assert.expect( 20 );
div.css( "--color", "blue" );
assert.equal( div.css( "--color" ), "blue", "Modified CSS custom property using string" );
@@ -1848,13 +1843,15 @@ QUnit.testUnlessIE( "css(--customProperty)", function( assert ) {
assert.equal( $elem.css( "--prop5" ), "val5", "Multiple Following whitespace trimmed" );
assert.equal( $elem.css( "--prop6" ), "val6", "Preceding and Following whitespace trimmed" );
assert.equal( $elem.css( "--prop7" ), "val7", "Multiple preceding and following whitespace trimmed" );
+ assert.equal( $elem.css( "--prop8" ), "\"val8\"", "Works with double quotes" );
- // Support: Chrome <=49 - 73+, Safari <=9.1 - 12.1+
- // Chrome treats single quotes as double ones.
- // Safari treats double quotes as single ones.
- if ( !webkitOrBlink ) {
- assert.equal( $elem.css( "--prop8" ), "\"val8\"", "Works with double quotes" );
+ // Support: Safari <=9.1 - 18.1+
+ // Safari converts single quotes to double ones.
+ if ( !/\bapplewebkit\/605\.1\.15\b/i.test( navigator.userAgent ) ) {
assert.equal( $elem.css( "--prop9" ), "'val9'", "Works with single quotes" );
+ } else {
+ assert.equal( $elem.css( "--prop9" ).replace( /"/g, "'" ), "'val9'",
+ "Works with single quotes, but they may be changed to double ones" );
}
assert.equal( $elem.css( "--prop10" ), "val10", "Multiple preceding and following escaped unicode whitespace trimmed" );
diff --git a/test/unit/dimensions.js b/test/unit/dimensions.js
index 6b0c9c798..3a8b988f1 100644
--- a/test/unit/dimensions.js
+++ b/test/unit/dimensions.js
@@ -345,40 +345,94 @@ QUnit.test( "child of a hidden elem (or unconnected node) has accurate inner/out
$divNormal.remove();
} );
-QUnit.test( "getting dimensions shouldn't modify runtimeStyle see trac-9233", function( assert ) {
- assert.expect( 1 );
+QUnit.test( "hidden element with dimensions from a stylesheet", function( assert ) {
+ assert.expect( 2 );
- var $div = jQuery( "<div>" ).appendTo( "#qunit-fixture" ),
- div = $div.get( 0 ),
- runtimeStyle = div.runtimeStyle;
+ var div = jQuery( "" +
+ "<div class='display-none-style'>" +
+ " <style>" +
+ " .display-none-style {" +
+ " display: none;" +
+ " width: 111px;" +
+ " height: 123px;" +
+ " }" +
+ " </style>" +
+ "</div>" +
+ "" )
+ .appendTo( "#qunit-fixture" );
- if ( runtimeStyle ) {
- div.runtimeStyle.marginLeft = "12em";
- div.runtimeStyle.left = "11em";
- }
+ assert.strictEqual( div.width(), 111, "width of a hidden element" );
+ assert.strictEqual( div.height(), 123, "height of a hidden element" );
+} );
+
+QUnit.test( "hidden element with implicit content-based dimensions", function( assert ) {
+ assert.expect( 2 );
- $div.outerWidth( true );
+ var container = jQuery( "" +
- if ( runtimeStyle ) {
- assert.equal( div.runtimeStyle.left, "11em", "getting dimensions modifies runtimeStyle, see trac-9233" );
- } else {
- assert.ok( true, "this browser doesn't support runtimeStyle, see trac-9233" );
- }
+ // font-size affects the child dimensions implicitly
+ "<div style='font-size: 20px'>" +
+ " <div style='padding: 10px; display: none'>" +
+ " <div style='width: 3em; height: 2em'></div>" +
+ " </div>" +
+ "</div>" +
+ "" ),
+ div = container.children().first();
- $div.remove();
+ container.appendTo( "#qunit-fixture" );
+
+ assert.strictEqual( div.width(), 60, "width of a hidden element" );
+ assert.strictEqual( div.height(), 40, "height of a hidden element" );
} );
QUnit.test( "table dimensions", function( assert ) {
- assert.expect( 2 );
-
- var table = jQuery( "<table><colgroup><col></col><col></col></colgroup><tbody><tr><td></td><td>a</td></tr><tr><td></td><td>a</td></tr></tbody></table>" ).appendTo( "#qunit-fixture" ),
+ assert.expect( 3 );
+
+ var table = jQuery( "" +
+ "<table style='border-spacing: 0'>" +
+ " <colgroup>" +
+ " <col />" +
+ " <col span='2' class='col-double' />" +
+ " </colgroup>" +
+ " <tbody>" +
+ " <tr>" +
+ " <td></td>" +
+ " <td class='td-a-1'>a</td>" +
+ " <td class='td-b-1'>b</td>" +
+ " </tr>" +
+ " <tr>" +
+ " <td></td>" +
+ " <td>a</td>" +
+ " <td>b</td>" +
+ " </tr>" +
+ " </tbody>" +
+ "</table>"
+ ).appendTo( "#qunit-fixture" ),
tdElem = table.find( "td" ).first(),
- colElem = table.find( "col" ).first().width( 300 );
+ colElem = table.find( "col" ).first(),
+ doubleColElem = table.find( ".col-double" );
+
+ table.find( "td" ).css( { margin: 0, padding: 0, border: 0 } );
- table.find( "td" ).css( { "margin": 0, "padding": 0 } );
+ colElem.width( 300 );
+
+ table.find( ".td-a-1" ).width( 200 );
+ table.find( ".td-b-1" ).width( 400 );
assert.equal( tdElem.width(), tdElem.width(), "width() doesn't alter dimension values of empty cells, see trac-11293" );
- assert.equal( colElem.width(), 300, "col elements have width(), see trac-12243" );
+ assert.equal( colElem.width(), 300, "col elements have width(), (trac-12243)" );
+
+ // Support: IE 11+
+ // In IE, `<col>` computed width is `"auto"` unless `width` is set
+ // explicitly via CSS so measurements there remain incorrect. Because of
+ // the lack of a proper workaround, we accept this limitation.
+ // To make IE pass the test, set the width explicitly.
+ if ( QUnit.isIE ) {
+ doubleColElem.width( 600 );
+ }
+
+ assert.equal( doubleColElem.width(), 600,
+ "col with span measured correctly (gh-5628)" );
} );
QUnit.test( "SVG dimensions (basic content-box)", function( assert ) {
@@ -691,7 +745,7 @@ QUnit.test( "interaction with scrollbars (gh-3589)", function( assert ) {
.appendTo( "#qunit-fixture" ),
// Workarounds for IE kill fractional output here.
- fraction = document.documentMode ? 0 : 0.5,
+ fraction = QUnit.isIE ? 0 : 0.5,
borderWidth = 1,
padding = 2,
size = 100 + fraction,
diff --git a/test/unit/event.js b/test/unit/event.js
index 89d48c420..dac53ed93 100644
--- a/test/unit/event.js
+++ b/test/unit/event.js
@@ -1438,7 +1438,7 @@ QUnit.test( "Submit event can be stopped (trac-11049)", function( assert ) {
form.remove();
} );
-// Support: iOS <=7 - 12+
+// Support: iOS <=7 - 18+
// iOS has the window.onbeforeunload field but doesn't support the beforeunload
// handler making it impossible to feature-detect the support.
QUnit[ /(ipad|iphone|ipod)/i.test( navigator.userAgent ) ? "skip" : "test" ](
@@ -2707,7 +2707,7 @@ testIframe(
// IE does propagate the event to the parent document. In this test
// we mainly care about the inner element so we'll just skip this one
// assertion in IE.
- if ( !document.documentMode ) {
+ if ( !QUnit.isIE ) {
assert.ok( false, "fired a focusin event in the parent document" );
}
} );
diff --git a/test/unit/manipulation.js b/test/unit/manipulation.js
index 8300b4b9c..f4941b890 100644
--- a/test/unit/manipulation.js
+++ b/test/unit/manipulation.js
@@ -3020,8 +3020,7 @@ QUnit.test( "Sanitized HTML doesn't get unsanitized", function( assert ) {
var container,
counter = 0,
- oldIos = /iphone os (?:8|9|10|11|12)_/i.test( navigator.userAgent ),
- assertCount = oldIos ? 12 : 13,
+ assertCount = 13,
done = assert.async( assertCount );
assert.expect( assertCount );
@@ -3065,12 +3064,7 @@ QUnit.test( "Sanitized HTML doesn't get unsanitized", function( assert ) {
test( "<option><style></option></select><img src=url404 onerror=xss(11)></style>" );
- // Support: iOS 8 - 12 only.
- // Old iOS parses `<noembed>` tags differently, executing this code. This is no
- // different to native behavior on that OS, though, so just accept it.
- if ( !oldIos ) {
- test( "<noembed><noembed/><img src=url404 onerror=xss(12)>" );
- }
+ test( "<noembed><noembed/><img src=url404 onerror=xss(12)>" );
} );
QUnit.test( "Works with invalid attempts to close the table wrapper", function( assert ) {
diff --git a/test/unit/offset.js b/test/unit/offset.js
index 5cece84f4..bf16f7ddb 100644
--- a/test/unit/offset.js
+++ b/test/unit/offset.js
@@ -752,25 +752,15 @@ QUnit.test( "iframe scrollTop/Left (see gh-1945)", function( assert ) {
var ifDoc = jQuery( "#iframe" )[ 0 ].contentDocument;
- // Support: iOS <=8 - 12+
- // Mobile Safari resizes the iframe by its content meaning it's not possible to scroll
- // the iframe but only its parent element.
- if ( /iphone os|ipad/i.test( navigator.userAgent ) ) {
- assert.equal( true, true, "Can't scroll iframes in this environment" );
- assert.equal( true, true, "Can't scroll iframes in this environment" );
+ // Tests scrollTop/Left with iframes
+ jQuery( "#iframe" ).css( "width", "50px" ).css( "height", "50px" );
+ ifDoc.write( "<div style='width: 1000px; height: 1000px;'></div>" );
- } else {
+ jQuery( ifDoc ).scrollTop( 200 );
+ jQuery( ifDoc ).scrollLeft( 500 );
- // Tests scrollTop/Left with iframes
- jQuery( "#iframe" ).css( "width", "50px" ).css( "height", "50px" );
- ifDoc.write( "<div style='width: 1000px; height: 1000px;'></div>" );
-
- jQuery( ifDoc ).scrollTop( 200 );
- jQuery( ifDoc ).scrollLeft( 500 );
-
- assert.equal( jQuery( ifDoc ).scrollTop(), 200, "$($('#iframe')[0].contentDocument).scrollTop()" );
- assert.equal( jQuery( ifDoc ).scrollLeft(), 500, "$($('#iframe')[0].contentDocument).scrollLeft()" );
- }
+ assert.equal( jQuery( ifDoc ).scrollTop(), 200, "$($('#iframe')[0].contentDocument).scrollTop()" );
+ assert.equal( jQuery( ifDoc ).scrollLeft(), 500, "$($('#iframe')[0].contentDocument).scrollLeft()" );
} );
} )();
diff --git a/test/unit/selector.js b/test/unit/selector.js
index 1c3358254..062bad02b 100644
--- a/test/unit/selector.js
+++ b/test/unit/selector.js
@@ -858,7 +858,7 @@ QUnit.test( "pseudo - nth-child", function( assert ) {
);
} else {
- // Support: Chrome 75+, Firefox 67+
+ // Support: Chrome 75 - 133+, Firefox 67 - 135+
// Some browsers mark disconnected elements as matching `:nth-child(n)`
// so let's skip the test.
assert.ok( "skip", "disconnected elements match ':nth-child(n)' in Chrome/Firefox" );
@@ -912,7 +912,7 @@ QUnit.test( "pseudo - nth-last-child", function( assert ) {
);
} else {
- // Support: Chrome 75+, Firefox 67+
+ // Support: Chrome 75 - 133+, Firefox 67 - 135+
// Some browsers mark disconnected elements as matching `:nth-last-child(n)`
// so let's skip the test.
assert.ok( "skip", "disconnected elements match ':nth-last-child(n)' in Chrome/Firefox" );
@@ -954,7 +954,7 @@ QUnit[ QUnit.jQuerySelectors ? "test" : "skip" ]( "pseudo - has", function( asse
"div:has(div:has(div:not([id])))",
[ "moretests", "t2037", "fx-test-group", "fx-queue" ] );
- // Support: Safari 15.4+, Chrome 105+
+ // Support: Chrome 105 - 111 only, Safari 15.4 - 16.3 only
// `qSA` in Safari/Chrome throws for `:has()` with only unsupported arguments
// but if you add a supported arg to the list, it will run and just potentially
// return no results. Make sure this is accounted for. (gh-5098)
@@ -2237,6 +2237,37 @@ QUnit[ QUnit.jQuerySelectors ? "test" : "skip" ]( "custom pseudos", function( as
}
} );
+QUnit[ QUnit.jQuerySelectors ? "test" : "skip" ]( "custom attribute getters", function( assert ) {
+ assert.expect( 2 );
+
+ var original = jQuery.attrHooks.hreflang,
+ selector = "a:contains('mozilla')[hreflang='https://mozilla.org/en']";
+
+ try {
+ jQuery.attrHooks.hreflang = {
+ get: function( elem, name ) {
+ var href = elem.getAttribute( "href" ),
+ lang = elem.getAttribute( name );
+ return lang && ( href + lang );
+ }
+ };
+
+ assert.deepEqual(
+ jQuery.find( selector, createWithFriesXML() ),
+ [],
+ "Custom attrHooks (preferred document)"
+ );
+ assert.t( "Custom attrHooks (preferred document)", selector, [ "mozilla" ] );
+ } finally {
+ jQuery.attrHooks.hreflang = original;
+ }
+} );
+QUnit[ QUnit.jQuerySelectors ? "test" : "skip" ]( "Ensure no 'undefined' handler is added", function( assert ) {
+ assert.expect( 1 );
+ assert.ok( !jQuery.attrHooks.hasOwnProperty( "undefined" ),
+ "Extra attr handlers are not added to jQuery.attrHooks (https://github.com/jquery/sizzle/issues/353)" );
+} );
+
QUnit.test( "jQuery.find.matchesSelector", function( assert ) {
assert.expect( 15 );
diff --git a/test/unit/support.js b/test/unit/support.js
index bf3f4d26d..8e6fc83a7 100644
--- a/test/unit/support.js
+++ b/test/unit/support.js
@@ -82,38 +82,32 @@ testIframe(
expectedMap = {
ie_11: {
cssHas: true,
+ reliableColDimensions: 11,
reliableTrDimensions: false
},
- chrome_111: {
- cssHas: false,
- reliableTrDimensions: true
- },
chrome: {
cssHas: true,
- reliableTrDimensions: true
- },
- safari_16_3: {
- cssHas: false,
+ reliableColDimensions: true,
reliableTrDimensions: true
},
safari: {
cssHas: true,
+ reliableColDimensions: false,
reliableTrDimensions: true
},
firefox: {
cssHas: true,
+ reliableColDimensions: false,
reliableTrDimensions: false
},
- ios_14_15_3: {
- cssHas: true,
- reliableTrDimensions: true
- },
- ios_15_4_16_3: {
+ ios_16_3: {
cssHas: false,
+ reliableColDimensions: false,
reliableTrDimensions: true
},
ios: {
cssHas: true,
+ reliableColDimensions: false,
reliableTrDimensions: true
}
};
@@ -125,24 +119,18 @@ testIframe(
}
}
- if ( document.documentMode ) {
+ if ( QUnit.isIE ) {
expected = expectedMap.ie_11;
- } else if ( /\b(?:headless)?chrome\/(?:10\d|11[01])\b/i.test( userAgent ) ) {
- expected = expectedMap.chrome_111;
} else if ( /\b(?:headless)?chrome\//i.test( userAgent ) ) {
// Catches Edge, Chrome on Android & Opera as well.
expected = expectedMap.chrome;
} else if ( /\bfirefox\//i.test( userAgent ) ) {
expected = expectedMap.firefox;
- } else if ( /\biphone os (?:14_|15_[0123])/i.test( userAgent ) ) {
- expected = expectedMap.ios_14_15_3;
- } else if ( /\biphone os (?:15_|16_[0123])/i.test( userAgent ) ) {
- expected = expectedMap.ios_15_4_16_3;
+ } else if ( /\biphone os 16_[0123]/i.test( userAgent ) ) {
+ expected = expectedMap.ios_16_3;
} else if ( /\b(?:iphone|ipad);.*(?:iphone)? os \d+_/i.test( userAgent ) ) {
expected = expectedMap.ios;
- } else if ( /\bversion\/(?:15|16\.[0123])(?:\.\d+)* safari/i.test( userAgent ) ) {
- expected = expectedMap.safari_16_3;
} else if ( /\bversion\/\d+(?:\.\d+)+ safari/i.test( userAgent ) ) {
expected = expectedMap.safari;
}