mirror of
https://github.com/jquery/jquery-ui.git
synced 2024-11-21 11:04:24 +00:00
4af5caed7a
Close gh-2234
333 lines
8.8 KiB
JavaScript
333 lines
8.8 KiB
JavaScript
/**
|
|
* 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;
|
|
}
|
|
}
|