diff options
Diffstat (limited to 'test')
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; } |