(webdriver): improvements to driver management (#10952)
* (webdriver): improvements to driver management * fix unit tests * fix deps * fix deps * revert test changes * fix test * fix component tests * disable edge e2e tests * better error logging
This commit is contained in:
parent
2a3178cc95
commit
34661c5f45
29 changed files with 995 additions and 396 deletions
|
|
@ -111,3 +111,11 @@ export const transformCommandLogResult = vi.fn().mockImplementation((data) => da
|
|||
export const canAccess = vi.fn()
|
||||
export const sleep = vi.fn().mockImplementation(sleepOrig)
|
||||
export const UNICODE_CHARACTERS = UNICODE_CHARACTERS_ORIG
|
||||
|
||||
export const startWebDriver = vi.fn((params) => {
|
||||
params.hostname = '0.0.0.0'
|
||||
params.port = 4444
|
||||
params.path = '/'
|
||||
})
|
||||
export const setupBrowser = vi.fn()
|
||||
export const setupDriver = vi.fn()
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import url from 'node:url'
|
||||
import path from 'node:path'
|
||||
import type { Options } from '@wdio/types'
|
||||
|
||||
import DevtoolsService from '../../packages/wdio-devtools-service/build/index.js'
|
||||
|
||||
const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
|
||||
|
||||
export const config = {
|
||||
export const config: Options.Testrunner = {
|
||||
/**
|
||||
* specify test files
|
||||
*/
|
||||
|
|
@ -20,7 +21,7 @@ export const config = {
|
|||
browserVersion: 'stable',
|
||||
'wdio:devtoolsOptions': { headless: true, dumpio: true }
|
||||
}],
|
||||
|
||||
bail: 1,
|
||||
services: [[DevtoolsService, {}]],
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -50,11 +50,14 @@ if (os.platform() === 'darwin') {
|
|||
})
|
||||
}
|
||||
|
||||
if (['darwin', 'win32'].includes(os.platform())) {
|
||||
(config.capabilities as Capabilities.Capabilities[]).push({
|
||||
browserName: 'edge',
|
||||
'ms:edgeOptions': {
|
||||
args: ['--headless', '--disable-gpu']
|
||||
}
|
||||
})
|
||||
}
|
||||
/**
|
||||
* ToDo(Christian): fix https://github.com/webdriverio-community/node-edgedriver/issues/93
|
||||
*/
|
||||
// if (['darwin', 'win32'].includes(os.platform())) {
|
||||
// (config.capabilities as Capabilities.Capabilities[]).push({
|
||||
// browserName: 'edge',
|
||||
// 'ms:edgeOptions': {
|
||||
// args: ['--headless', '--disable-gpu']
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
|
|
|||
66
package-lock.json
generated
66
package-lock.json
generated
|
|
@ -28666,14 +28666,59 @@
|
|||
"version": "8.14.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "^1.6.0",
|
||||
"@wdio/logger": "8.11.0",
|
||||
"@wdio/types": "8.14.0",
|
||||
"import-meta-resolve": "^3.0.0"
|
||||
"chrome-launcher": "^1.0.0",
|
||||
"decamelize": "^6.0.0",
|
||||
"deepmerge-ts": "^5.1.0",
|
||||
"edgedriver": "^5.3.3",
|
||||
"geckodriver": "^4.2.0",
|
||||
"get-port": "^7.0.0",
|
||||
"got": "^13.0.0",
|
||||
"import-meta-resolve": "^3.0.0",
|
||||
"safaridriver": "^0.1.0",
|
||||
"wait-port": "^1.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.13 || >=18"
|
||||
}
|
||||
},
|
||||
"packages/wdio-utils/node_modules/get-port": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-port/-/get-port-7.0.0.tgz",
|
||||
"integrity": "sha512-mDHFgApoQd+azgMdwylJrv2DX47ywGq1i5VFJE7fZ0dttNq3iQMfsU4IvEgBHojA3KqEudyu7Vq+oN8kNaNkWw==",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"packages/wdio-utils/node_modules/got": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz",
|
||||
"integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==",
|
||||
"dependencies": {
|
||||
"@sindresorhus/is": "^5.2.0",
|
||||
"@szmarczak/http-timer": "^5.0.1",
|
||||
"cacheable-lookup": "^7.0.0",
|
||||
"cacheable-request": "^10.2.8",
|
||||
"decompress-response": "^6.0.0",
|
||||
"form-data-encoder": "^2.1.2",
|
||||
"get-stream": "^6.0.1",
|
||||
"http2-wrapper": "^2.1.10",
|
||||
"lowercase-keys": "^3.0.0",
|
||||
"p-cancelable": "^3.0.0",
|
||||
"responselike": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/got?sponsor=1"
|
||||
}
|
||||
},
|
||||
"packages/wdio-webdriver-mock-service": {
|
||||
"name": "@wdio/webdriver-mock-service",
|
||||
"version": "8.14.6",
|
||||
|
|
@ -28694,7 +28739,6 @@
|
|||
"version": "8.14.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "^1.4.6",
|
||||
"@types/node": "^20.1.0",
|
||||
"@types/ws": "^8.5.3",
|
||||
"@wdio/config": "8.14.6",
|
||||
|
|
@ -28702,33 +28746,15 @@
|
|||
"@wdio/protocols": "8.14.6",
|
||||
"@wdio/types": "8.14.0",
|
||||
"@wdio/utils": "8.14.0",
|
||||
"chrome-launcher": "^1.0.0",
|
||||
"decamelize": "^6.0.0",
|
||||
"deepmerge-ts": "^5.1.0",
|
||||
"edgedriver": "^5.3.2",
|
||||
"geckodriver": "^4.2.0",
|
||||
"get-port": "^7.0.0",
|
||||
"got": "^ 12.6.1",
|
||||
"ky": "^0.33.0",
|
||||
"safaridriver": "^0.1.0",
|
||||
"wait-port": "^1.0.4",
|
||||
"ws": "^8.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.13 || >=18"
|
||||
}
|
||||
},
|
||||
"packages/webdriver/node_modules/get-port": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-port/-/get-port-7.0.0.tgz",
|
||||
"integrity": "sha512-mDHFgApoQd+azgMdwylJrv2DX47ywGq1i5VFJE7fZ0dttNq3iQMfsU4IvEgBHojA3KqEudyu7Vq+oN8kNaNkWw==",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"packages/webdriverio": {
|
||||
"version": "8.14.6",
|
||||
"license": "MIT",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export const pathToFileURL = () => ''
|
|||
export const fileURLToPath = () => ''
|
||||
export const dirname = () => ''
|
||||
export const resolve = () => ''
|
||||
export const download = () => ''
|
||||
export const sep = '/'
|
||||
export const start = () => {}
|
||||
export const install = () => {}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import chalk, { supportsColor } from 'chalk'
|
|||
import logger from '@wdio/logger'
|
||||
import type { Options, Capabilities, Workers } from '@wdio/types'
|
||||
|
||||
import type { HookError } from './utils.js'
|
||||
import { HookError } from './utils.js'
|
||||
import { getRunnerName } from './utils.js'
|
||||
|
||||
const log = logger('@wdio/cli')
|
||||
|
|
@ -211,8 +211,11 @@ export default class WDIOCLInterface extends EventEmitter {
|
|||
return args
|
||||
}
|
||||
|
||||
logHookError (error: HookError) {
|
||||
return this.log(`${chalk.red(error.name)} in "${error.origin}"\n${chalk.red(error.stack || error.message)}`)
|
||||
logHookError (error: Error | HookError) {
|
||||
if (error instanceof HookError) {
|
||||
return this.log(`${chalk.red(error.name)} in "${error.origin}"\n${chalk.red(error.stack || error.message)}`)
|
||||
}
|
||||
return this.log(`${chalk.red(error.name)}: ${chalk.red(error.stack || error.message)}`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import exitHook from 'async-exit-hook'
|
|||
|
||||
import logger from '@wdio/logger'
|
||||
import { ConfigParser } from '@wdio/config'
|
||||
import { initialisePlugin, initialiseLauncherService, sleep } from '@wdio/utils'
|
||||
import { initialisePlugin, initialiseLauncherService, sleep, setupDriver, setupBrowser } from '@wdio/utils'
|
||||
import type { Options, Capabilities, Services } from '@wdio/types'
|
||||
|
||||
import CLInterface from './interface.js'
|
||||
|
|
@ -135,6 +135,14 @@ class Launcher {
|
|||
await runLauncherHook(config.onPrepare, config, caps)
|
||||
await runServiceHook(this._launcher, 'onPrepare', config, caps)
|
||||
|
||||
/**
|
||||
* pre-configure necessary driver for worker threads
|
||||
*/
|
||||
await Promise.all([
|
||||
setupDriver(config, caps),
|
||||
setupBrowser(config, caps)
|
||||
])
|
||||
|
||||
exitCode = await this._runMode(config, caps)
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ export interface DesiredCapabilities extends Capabilities, SauceLabsCapabilities
|
|||
excludeDriverLogs?: string[];
|
||||
}
|
||||
|
||||
export interface VendorExtensions extends EdgeCapabilities, AppiumCapabilities, WebdriverIO.WDIODevtoolsOptions, WebdriverIO.WDIODriverOptions, WebdriverIO.WDIOVSCodeServiceOptions {
|
||||
export interface VendorExtensions extends EdgeCapabilities, AppiumCapabilities, WebdriverIO.WDIODevtoolsOptions, WebdriverIOCapabilities, WebdriverIO.WDIOVSCodeServiceOptions {
|
||||
// Aerokube Selenoid specific
|
||||
'selenoid:options'?: SelenoidOptions
|
||||
// Aerokube Moon specific
|
||||
|
|
@ -201,6 +201,13 @@ export interface VendorExtensions extends EdgeCapabilities, AppiumCapabilities,
|
|||
testobject_api_key?: string
|
||||
}
|
||||
|
||||
export interface WebdriverIOCapabilities {
|
||||
'wdio:chromedriverOptions'?: WebdriverIO.ChromedriverOptions
|
||||
'wdio:safaridriverOptions'?: WebdriverIO.SafaridriverOptions
|
||||
'wdio:geckodriverOptions'?: WebdriverIO.GeckodriverOptions
|
||||
'wdio:edgedriverOptions'?: WebdriverIO.EdgedriverOptions
|
||||
}
|
||||
|
||||
export interface ChromeOptions {
|
||||
/**
|
||||
* List of command-line arguments to use when starting Chrome. Arguments with an
|
||||
|
|
|
|||
|
|
@ -17,6 +17,13 @@ export type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ?
|
|||
export type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>
|
||||
export type ThenArg<T> = T extends PromiseLike<infer U> ? U : T
|
||||
|
||||
interface DriverOptions {
|
||||
/**
|
||||
* directory where browser and driver should be stored
|
||||
*/
|
||||
cacheDir?: string
|
||||
}
|
||||
|
||||
declare global {
|
||||
namespace WebdriverIO {
|
||||
interface MochaOpts { [key: string]: any }
|
||||
|
|
@ -27,13 +34,16 @@ declare global {
|
|||
interface Config extends Options.Testrunner {}
|
||||
interface HookFunctionExtension {}
|
||||
interface WDIODevtoolsOptions {}
|
||||
interface WDIODriverOptions {}
|
||||
interface WDIOVSCodeServiceOptions {}
|
||||
interface BrowserRunnerOptions {}
|
||||
interface ChromedriverOptions extends DriverOptions {}
|
||||
interface GeckodriverOptions extends DriverOptions {}
|
||||
interface EdgedriverOptions extends DriverOptions {}
|
||||
interface SafaridriverOptions {}
|
||||
}
|
||||
|
||||
namespace WebDriver {
|
||||
interface Capabilities extends Capabilities.Capabilities {}
|
||||
interface DesiredCapabilities extends Capabilities.DesiredCapabilities {}
|
||||
interface Capabilities extends Capabilities.Capabilities, Capabilities.WebdriverIOCapabilities {}
|
||||
interface DesiredCapabilities extends Capabilities.DesiredCapabilities, Capabilities.WebdriverIOCapabilities {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,9 +30,19 @@
|
|||
"url": "https://github.com/webdriverio/webdriverio/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "^1.6.0",
|
||||
"@wdio/logger": "8.11.0",
|
||||
"@wdio/types": "8.14.0",
|
||||
"import-meta-resolve": "^3.0.0"
|
||||
"chrome-launcher": "^1.0.0",
|
||||
"decamelize": "^6.0.0",
|
||||
"deepmerge-ts": "^5.1.0",
|
||||
"edgedriver": "^5.3.3",
|
||||
"geckodriver": "^4.2.0",
|
||||
"get-port": "^7.0.0",
|
||||
"got": "^13.0.0",
|
||||
"import-meta-resolve": "^3.0.0",
|
||||
"safaridriver": "^0.1.0",
|
||||
"wait-port": "^1.0.4"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
|
|
|||
|
|
@ -79,3 +79,14 @@ export const UNICODE_CHARACTERS = {
|
|||
'ZenkakuHankaku': '\uE040',
|
||||
'Zenkaku_Hankaku': '\uE040'
|
||||
} as const
|
||||
|
||||
export const SUPPORTED_BROWSERNAMES = {
|
||||
chrome: ['chrome', 'googlechrome', 'chromium', 'chromium-browser'],
|
||||
firefox: ['firefox', 'ff', 'mozilla', 'mozilla firefox'],
|
||||
edge: ['edge', 'microsoftedge', 'msedge'],
|
||||
safari: ['safari', 'safari technology preview']
|
||||
}
|
||||
|
||||
export const DEFAULT_HOSTNAME = '0.0.0.0'
|
||||
export const DEFAULT_PROTOCOL = 'http'
|
||||
export const DEFAULT_PATH = '/'
|
||||
|
|
|
|||
151
packages/wdio-utils/src/driver/index.ts
Normal file
151
packages/wdio-utils/src/driver/index.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import cp, { type ChildProcess } from 'node:child_process'
|
||||
|
||||
import getPort from 'get-port'
|
||||
import waitPort from 'wait-port'
|
||||
import logger from '@wdio/logger'
|
||||
import { deepmerge } from 'deepmerge-ts'
|
||||
|
||||
import { start as startSafaridriver, type SafaridriverOptions as SafaridriverParameters } from 'safaridriver'
|
||||
import { start as startGeckodriver, type GeckodriverParameters } from 'geckodriver'
|
||||
import { start as startEdgedriver, type EdgedriverParameters } from 'edgedriver'
|
||||
import type { InstallOptions } from '@puppeteer/browsers'
|
||||
|
||||
import type { Capabilities, Options } from '@wdio/types'
|
||||
|
||||
import {
|
||||
parseParams, setupChrome, definesRemoteDriver, setupChromedriver,
|
||||
isChrome, isFirefox, isEdge, isSafari, getCacheDir
|
||||
} from './utils.js'
|
||||
import { SUPPORTED_BROWSERNAMES } from '../constants.js'
|
||||
|
||||
export type ChromedriverParameters = InstallOptions & Omit<EdgedriverParameters, 'port' | 'edgeDriverVersion' | 'customEdgeDriverPath'>
|
||||
declare global {
|
||||
namespace WebdriverIO {
|
||||
interface ChromedriverOptions extends ChromedriverParameters {}
|
||||
interface GeckodriverOptions extends Omit<GeckodriverParameters, 'port'> {}
|
||||
interface EdgedriverOptions extends Omit<EdgedriverParameters, 'port'> {}
|
||||
interface SafaridriverOptions extends Omit<SafaridriverParameters, 'port'> {}
|
||||
}
|
||||
}
|
||||
|
||||
const log = logger('@wdio/utils')
|
||||
|
||||
export async function startWebDriver (options: Options.WebDriver) {
|
||||
/**
|
||||
* in case we are running unit tests, just return
|
||||
*/
|
||||
if (process.env.WDIO_SKIP_DRIVER_SETUP) {
|
||||
options.hostname = '0.0.0.0'
|
||||
options.port = 4321
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* if any of the connection parameter are set, don't start any driver
|
||||
*/
|
||||
if (definesRemoteDriver(options)) {
|
||||
log.info(`Connecting to existing driver at ${options.protocol}://${options.hostname}:${options.port}${options.path}`)
|
||||
return
|
||||
}
|
||||
|
||||
let driverProcess: ChildProcess
|
||||
let driver = ''
|
||||
const start = Date.now()
|
||||
const caps = (options.capabilities as Capabilities.W3CCapabilities).alwaysMatch || options.capabilities as Capabilities.Capabilities
|
||||
|
||||
/**
|
||||
* session might be a mobile session so don't do anything
|
||||
*/
|
||||
if (!caps.browserName) {
|
||||
throw new Error(
|
||||
'No "browserName" defined in capabilities nor hostname or port found!\n' +
|
||||
'If you like to run a mobile session with Appium, make sure to set "hostname" and "port" in your ' +
|
||||
'WebdriverIO options. If you like to run a local browser session make sure to pick from one of ' +
|
||||
`the following browser names: ${Object.values(SUPPORTED_BROWSERNAMES).flat(Infinity)}`
|
||||
)
|
||||
}
|
||||
|
||||
const port = await getPort()
|
||||
const cacheDir = getCacheDir(options, caps)
|
||||
if (isChrome(caps.browserName)) {
|
||||
/**
|
||||
* Chrome
|
||||
*/
|
||||
const chromedriverOptions = caps['wdio:chromedriverOptions'] || ({} as WebdriverIO.ChromedriverOptions)
|
||||
const { executablePath: chromeExecuteablePath, browserVersion } = await setupChrome(cacheDir, caps)
|
||||
const { executablePath: chromedriverExcecuteablePath } = await setupChromedriver(cacheDir, browserVersion)
|
||||
|
||||
caps['goog:chromeOptions'] = deepmerge(
|
||||
{ binary: chromeExecuteablePath },
|
||||
caps['goog:chromeOptions'] || {}
|
||||
)
|
||||
chromedriverOptions.allowedOrigins = chromedriverOptions.allowedOrigins || ['*']
|
||||
chromedriverOptions.allowedIps = chromedriverOptions.allowedIps || ['']
|
||||
const driverParams = parseParams({ port, ...chromedriverOptions })
|
||||
driverProcess = cp.spawn(chromedriverExcecuteablePath, driverParams)
|
||||
driver = `Chromedriver v${browserVersion} with params ${driverParams.join(' ')}`
|
||||
} else if (isSafari(caps.browserName)) {
|
||||
const safaridriverOptions = caps['wdio:safaridriverOptions'] || ({} as WebdriverIO.SafaridriverOptions)
|
||||
/**
|
||||
* Safari
|
||||
*/
|
||||
driver = 'SafariDriver'
|
||||
driverProcess = startSafaridriver({
|
||||
useTechnologyPreview: Boolean(caps.browserName.match(/preview/i)),
|
||||
...safaridriverOptions,
|
||||
port
|
||||
})
|
||||
|
||||
/**
|
||||
* set "Host" header as it is required by Safaridriver
|
||||
*/
|
||||
options.headers = deepmerge({ Host: 'localhost' }, (options.headers || {}))
|
||||
} else if (isFirefox(caps.browserName)) {
|
||||
/**
|
||||
* Firefox
|
||||
*/
|
||||
const geckodriverOptions = caps['wdio:geckodriverOptions'] || ({} as GeckodriverParameters)
|
||||
driver = 'GeckoDriver'
|
||||
driverProcess = await startGeckodriver({ ...geckodriverOptions, cacheDir, port })
|
||||
} else if (isEdge(caps.browserName)) {
|
||||
/**
|
||||
* Microsoft Edge
|
||||
*/
|
||||
const edgedriverOptions = caps['wdio:edgedriverOptions'] || ({} as EdgedriverParameters)
|
||||
driver = 'EdgeDriver'
|
||||
driverProcess = await startEdgedriver({ ...edgedriverOptions, cacheDir, port }).catch((err) => {
|
||||
log.warn(`Couldn't start EdgeDriver: ${err.message}, retry ...`)
|
||||
return startEdgedriver({ ...edgedriverOptions, cacheDir, port })
|
||||
})
|
||||
|
||||
/**
|
||||
* Microsoft Edge is very particular when it comes to browser names
|
||||
*/
|
||||
caps.browserName = 'MicrosoftEdge'
|
||||
} else {
|
||||
throw new Error(
|
||||
`Unknown browser name "${caps.browserName}". Make sure to pick from one of the following ` +
|
||||
Object.values(SUPPORTED_BROWSERNAMES).flat(Infinity)
|
||||
)
|
||||
}
|
||||
|
||||
if (options.outputDir) {
|
||||
const logIdentifier = driver.split(' ').shift()?.toLowerCase()
|
||||
const logFileName = process.env.WDIO_WORKER_ID
|
||||
? `wdio-${process.env.WDIO_WORKER_ID}-${logIdentifier}.log`
|
||||
: `wdio-${logIdentifier}-${port}.log`
|
||||
const logFile = path.resolve(options.outputDir, logFileName)
|
||||
const logStream = fs.createWriteStream(logFile, { flags: 'w' })
|
||||
driverProcess.stdout?.pipe(logStream)
|
||||
driverProcess.stderr?.pipe(logStream)
|
||||
}
|
||||
|
||||
await waitPort({ port, output: 'silent' })
|
||||
|
||||
options.hostname = '0.0.0.0'
|
||||
options.port = port
|
||||
log.info(`Started ${driver} in ${Date.now() - start}ms on port ${port}`)
|
||||
return driverProcess
|
||||
}
|
||||
121
packages/wdio-utils/src/driver/manager.ts
Normal file
121
packages/wdio-utils/src/driver/manager.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import logger from '@wdio/logger'
|
||||
import type { Options, Capabilities } from '@wdio/types'
|
||||
|
||||
import {
|
||||
getCacheDir, definesRemoteDriver,
|
||||
isSafari, isEdge, isFirefox, isChrome,
|
||||
setupChromedriver, setupEdgedriver, setupGeckodriver, setupChrome
|
||||
} from './utils.js'
|
||||
|
||||
const log = logger('@wdio/utils')
|
||||
const UNDEFINED_BROWSER_VERSION = null
|
||||
|
||||
type SetupTaskFunction = (cap: Capabilities.Capabilities) => Promise<unknown>
|
||||
|
||||
function mapCapabilities (
|
||||
options: Omit<Options.WebDriver, 'capabilities'>,
|
||||
caps: Capabilities.RemoteCapabilities,
|
||||
task: SetupTaskFunction,
|
||||
taskItemLabel: string) {
|
||||
const capabilitiesToRequireSetup = (
|
||||
Array.isArray(caps)
|
||||
? caps.map((cap) => {
|
||||
const w3cCaps = cap as Capabilities.W3CCapabilities
|
||||
const multiremoteCaps = cap as Capabilities.MultiRemoteCapabilities
|
||||
const isMultiremote = Boolean(multiremoteCaps[Object.keys(cap)[0]].capabilities)
|
||||
|
||||
if (isMultiremote) {
|
||||
return Object.values(multiremoteCaps).map((c) => c.capabilities)as Capabilities.Capabilities[]
|
||||
} else if (w3cCaps.alwaysMatch) {
|
||||
return w3cCaps.alwaysMatch
|
||||
}
|
||||
return cap as Capabilities.Capabilities
|
||||
}).flat()
|
||||
: Object.values(caps as Capabilities.MultiRemoteCapabilities).map((mrOpts) => {
|
||||
const w3cCaps = mrOpts.capabilities as Capabilities.W3CCapabilities
|
||||
if (w3cCaps.alwaysMatch) {
|
||||
return w3cCaps.alwaysMatch
|
||||
}
|
||||
return mrOpts.capabilities as Capabilities.Capabilities
|
||||
})
|
||||
).flat().filter((cap) => (
|
||||
/**
|
||||
* only set up driver if
|
||||
* - browserName is defined so we know it is a browser session
|
||||
* - we are not about to run a cloud session
|
||||
* - we are not running Safari (driver already installed on macOS)
|
||||
*/
|
||||
cap.browserName &&
|
||||
!definesRemoteDriver(options) &&
|
||||
!isSafari(cap.browserName)
|
||||
))
|
||||
|
||||
/**
|
||||
* nothing to setup
|
||||
*/
|
||||
if (capabilitiesToRequireSetup.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* get all browser names and versions that need driver setup
|
||||
*/
|
||||
const queueByBrowserName = capabilitiesToRequireSetup.reduce((queue, cap) => {
|
||||
if (!cap.browserName) {
|
||||
return queue
|
||||
}
|
||||
if (!queue.has(cap.browserName)) {
|
||||
queue.set(cap.browserName, new Map<string, Capabilities.Capabilities>())
|
||||
}
|
||||
const browserVersion = cap.browserVersion || UNDEFINED_BROWSER_VERSION
|
||||
queue.get(cap.browserName)!.set(browserVersion, cap)
|
||||
return queue
|
||||
}, new Map())
|
||||
|
||||
const driverToSetupString = Array.from(queueByBrowserName.entries())
|
||||
.map(([browserName, versions]) => `${browserName}@${Array.from(versions.keys()).map((bv) => bv || 'stable').join(', ')}`)
|
||||
.join(' - ')
|
||||
|
||||
log.info(`Setting up ${taskItemLabel} for: ${driverToSetupString}`)
|
||||
return Promise.all(
|
||||
Array.from(queueByBrowserName.entries()).map(([browserName, queueByBrowserVersion]) => {
|
||||
return Array.from(queueByBrowserVersion).map(([browserVersion, cap]) => task({
|
||||
...cap,
|
||||
browserName,
|
||||
...(browserVersion !== UNDEFINED_BROWSER_VERSION ? { browserVersion } : {})
|
||||
}))
|
||||
}).flat()
|
||||
)
|
||||
}
|
||||
|
||||
export async function setupDriver (options: Omit<Options.WebDriver, 'capabilities'>, caps: Capabilities.RemoteCapabilities) {
|
||||
return mapCapabilities(options, caps, async (cap: Capabilities.Capabilities) => {
|
||||
const cacheDir = getCacheDir(options, cap)
|
||||
if (isEdge(cap.browserName)) {
|
||||
return setupEdgedriver(cacheDir, cap.browserVersion)
|
||||
} else if (isFirefox(cap.browserName)) {
|
||||
return setupGeckodriver(cacheDir, cap.browserVersion)
|
||||
} else if (isChrome(cap.browserName)) {
|
||||
return setupChromedriver(cacheDir, cap.browserVersion)
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}, 'browser driver')
|
||||
}
|
||||
|
||||
export function setupBrowser (options: Omit<Options.WebDriver, 'capabilities'>, caps: Capabilities.RemoteCapabilities) {
|
||||
return mapCapabilities(options, caps, async (cap: Capabilities.Capabilities) => {
|
||||
const cacheDir = getCacheDir(options, cap)
|
||||
if (isEdge(cap.browserName)) {
|
||||
// not yet implemented
|
||||
return Promise.resolve()
|
||||
} else if (isFirefox(cap.browserName)) {
|
||||
// not yet implemented
|
||||
return Promise.resolve()
|
||||
} else if (isChrome(cap.browserName)) {
|
||||
return setupChrome(cacheDir, cap)
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}, 'browser binaries')
|
||||
}
|
||||
236
packages/wdio-utils/src/driver/utils.ts
Normal file
236
packages/wdio-utils/src/driver/utils.ts
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
import os from 'node:os'
|
||||
import fs from 'node:fs'
|
||||
import fsp from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import cp from 'node:child_process'
|
||||
|
||||
import got from 'got'
|
||||
import decamelize from 'decamelize'
|
||||
import logger from '@wdio/logger'
|
||||
import { getChromePath } from 'chrome-launcher'
|
||||
import {
|
||||
install, canDownload, resolveBuildId, detectBrowserPlatform, Browser, ChromeReleaseChannel,
|
||||
computeExecutablePath, type InstallOptions
|
||||
} from '@puppeteer/browsers'
|
||||
import { download as downloadGeckodriver } from 'geckodriver'
|
||||
import { download as downloadEdgedriver } from 'edgedriver'
|
||||
import type { EdgedriverParameters } from 'edgedriver'
|
||||
import type { Options, Capabilities } from '@wdio/types'
|
||||
|
||||
import { DEFAULT_HOSTNAME, DEFAULT_PROTOCOL, DEFAULT_PATH, SUPPORTED_BROWSERNAMES } from '../constants.js'
|
||||
|
||||
const log = logger('webdriver')
|
||||
const EXCLUDED_PARAMS = ['version', 'help']
|
||||
|
||||
export function parseParams(params: EdgedriverParameters) {
|
||||
return Object.entries(params)
|
||||
.filter(([key,]) => !EXCLUDED_PARAMS.includes(key))
|
||||
.map(([key, val]) => {
|
||||
if (typeof val === 'boolean' && !val) {
|
||||
return ''
|
||||
}
|
||||
const vals = Array.isArray(val) ? val : [val]
|
||||
return vals.map((v) => `--${decamelize(key, { separator: '-' })}${typeof v === 'boolean' ? '' : `=${v}`}`)
|
||||
})
|
||||
.flat()
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
export function getLocalChromePath() {
|
||||
try {
|
||||
const chromePath = getChromePath()
|
||||
return chromePath
|
||||
} catch (err: unknown) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
export function getBuildIdByPath(chromePath?: string) {
|
||||
if (!chromePath) {
|
||||
return
|
||||
} else if (os.platform() === 'win32') {
|
||||
const versionPath = path.dirname(chromePath)
|
||||
const contents = fs.readdirSync(versionPath)
|
||||
const versions = contents.filter(a => /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/g.test(a))
|
||||
|
||||
// returning oldest in case there is an updated version and chrome still hasn't relaunched
|
||||
const oldest = versions.sort((a: string, b: string) => a > b ? -1 : 1)[0]
|
||||
return oldest
|
||||
}
|
||||
|
||||
const versionString = cp.execSync(`"${chromePath}" --version`).toString()
|
||||
return versionString.split(' ').pop()?.trim()
|
||||
}
|
||||
|
||||
let lastTimeCalled = Date.now()
|
||||
export const downloadProgressCallback = (artifact: string, downloadedBytes: number, totalBytes: number) => {
|
||||
if (Date.now() - lastTimeCalled < 1000) {
|
||||
return
|
||||
}
|
||||
const percentage = ((downloadedBytes / totalBytes) * 100).toFixed(2)
|
||||
log.info(`Downloading ${artifact} ${percentage}%`)
|
||||
lastTimeCalled = Date.now()
|
||||
}
|
||||
|
||||
export async function setupChrome(cacheDir: string, caps: Capabilities.Capabilities) {
|
||||
caps.browserName = caps.browserName?.toLowerCase()
|
||||
const exist = await fsp.access(cacheDir).then(() => true, () => false)
|
||||
if (!exist) {
|
||||
await fsp.mkdir(cacheDir, { recursive: true })
|
||||
}
|
||||
|
||||
const platform = detectBrowserPlatform()
|
||||
if (!platform) {
|
||||
throw new Error('The current platform is not supported.')
|
||||
}
|
||||
|
||||
if (!caps.browserVersion) {
|
||||
const executablePath = getLocalChromePath()!
|
||||
const tag = getBuildIdByPath(executablePath)
|
||||
|
||||
/**
|
||||
* verify that we have a valid Chrome browser installed
|
||||
*/
|
||||
if (tag) {
|
||||
return {
|
||||
cacheDir,
|
||||
platform,
|
||||
executablePath,
|
||||
buildId: await resolveBuildId(Browser.CHROME, platform, tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* otherwise download provided Chrome browser version or "stable"
|
||||
*/
|
||||
const tag = caps.browserVersion || ChromeReleaseChannel.STABLE
|
||||
const buildId = await resolveBuildId(Browser.CHROME, platform, tag)
|
||||
const installOptions: InstallOptions & { unpack?: true } = {
|
||||
unpack: true,
|
||||
cacheDir,
|
||||
platform,
|
||||
buildId,
|
||||
browser: Browser.CHROME,
|
||||
downloadProgressCallback: (downloadedBytes, totalBytes) => downloadProgressCallback('Chrome', downloadedBytes, totalBytes)
|
||||
}
|
||||
const isCombinationAvailable = await canDownload(installOptions)
|
||||
if (!isCombinationAvailable) {
|
||||
throw new Error(`Couldn't find a matching Chrome browser for tag "${buildId}" on platform "${platform}"`)
|
||||
}
|
||||
|
||||
log.info(`Setting up Chrome v${buildId}`)
|
||||
await install(installOptions)
|
||||
const executablePath = computeExecutablePath(installOptions)
|
||||
return { executablePath, browserVersion: buildId }
|
||||
}
|
||||
|
||||
export function getCacheDir (options: Pick<Options.WebDriver, 'cacheDir'>, caps: Capabilities.Capabilities) {
|
||||
const driverOptions: { cacheDir?: string } = (
|
||||
caps['wdio:chromedriverOptions'] ||
|
||||
caps['wdio:chromedriverOptions'] ||
|
||||
caps['wdio:chromedriverOptions'] ||
|
||||
caps['wdio:chromedriverOptions'] ||
|
||||
{}
|
||||
)
|
||||
return driverOptions.cacheDir || options.cacheDir || os.tmpdir()
|
||||
}
|
||||
|
||||
export async function setupChromedriver (cacheDir: string, driverVersion?: string) {
|
||||
const platform = detectBrowserPlatform()
|
||||
if (!platform) {
|
||||
throw new Error('The current platform is not supported.')
|
||||
}
|
||||
|
||||
const version = driverVersion || getBuildIdByPath(getLocalChromePath()) || ChromeReleaseChannel.STABLE
|
||||
const buildId = await resolveBuildId(Browser.CHROMEDRIVER, platform, version)
|
||||
let executablePath = computeExecutablePath({
|
||||
browser: Browser.CHROMEDRIVER,
|
||||
buildId,
|
||||
platform,
|
||||
cacheDir
|
||||
})
|
||||
let loggedBuildId = buildId
|
||||
const hasChromedriverInstalled = await fsp.access(executablePath).then(() => true, () => false)
|
||||
if (!hasChromedriverInstalled) {
|
||||
log.info(`Downloading Chromedriver v${buildId}`)
|
||||
const chromedriverInstallOpts: InstallOptions & {unpack?: true} = {
|
||||
cacheDir,
|
||||
buildId,
|
||||
platform,
|
||||
browser: Browser.CHROMEDRIVER,
|
||||
unpack: true,
|
||||
downloadProgressCallback: (downloadedBytes, totalBytes) => downloadProgressCallback('Chromedriver', downloadedBytes, totalBytes)
|
||||
}
|
||||
|
||||
try {
|
||||
await install({ ...chromedriverInstallOpts, buildId })
|
||||
} catch (err) {
|
||||
/**
|
||||
* in case we detect a Chrome browser installed for which there is no Chromedriver available
|
||||
* we are falling back to the latest known good version
|
||||
*/
|
||||
log.warn(`Couldn't download Chromedriver v${buildId}: ${(err as Error).message}, trying to find known good version...`)
|
||||
let knownGoodVersion: string
|
||||
if (buildId.includes('.')) {
|
||||
const majorVersion = buildId.split('.')[0]
|
||||
const knownGoodVersions: any = await got('https://googlechromelabs.github.io/chrome-for-testing/known-good-versions.json').json()
|
||||
const versionMatch = knownGoodVersions.versions.filter(({ version }: { version: string }) => version.startsWith(majorVersion)).pop()
|
||||
if (!versionMatch) {
|
||||
throw new Error(`Couldn't find known good version for Chromedriver v${majorVersion}`)
|
||||
}
|
||||
knownGoodVersion = versionMatch.version
|
||||
} else {
|
||||
knownGoodVersion = await resolveBuildId(Browser.CHROMEDRIVER, platform, buildId)
|
||||
}
|
||||
|
||||
loggedBuildId = knownGoodVersion
|
||||
await install({ ...chromedriverInstallOpts, buildId: loggedBuildId })
|
||||
executablePath = computeExecutablePath({
|
||||
browser: Browser.CHROMEDRIVER,
|
||||
buildId: loggedBuildId,
|
||||
platform,
|
||||
cacheDir
|
||||
})
|
||||
}
|
||||
} else {
|
||||
log.info(`Using Chromedriver v${buildId} from cache directory ${cacheDir}`)
|
||||
}
|
||||
|
||||
return { executablePath }
|
||||
}
|
||||
|
||||
export function setupGeckodriver (cacheDir: string, driverVersion?: string) {
|
||||
return downloadGeckodriver(driverVersion, cacheDir)
|
||||
}
|
||||
|
||||
export function setupEdgedriver (cacheDir: string, driverVersion?: string) {
|
||||
return downloadEdgedriver(driverVersion, cacheDir)
|
||||
}
|
||||
|
||||
/**
|
||||
* helper method to determine if we need to start a browser driver
|
||||
* which is: whenever the user has set connection options that differ from
|
||||
* the default, or a port is set
|
||||
*/
|
||||
export function definesRemoteDriver(options: Pick<Options.WebDriver, 'protocol' | 'hostname' | 'port' | 'path'>) {
|
||||
return Boolean(
|
||||
(options.protocol && options.protocol !== DEFAULT_PROTOCOL) ||
|
||||
(options.hostname && options.hostname !== DEFAULT_HOSTNAME) ||
|
||||
Boolean(options.port) ||
|
||||
(options.path && options.path !== DEFAULT_PATH)
|
||||
)
|
||||
}
|
||||
|
||||
export function isChrome (browserName?: string) {
|
||||
return Boolean(browserName && SUPPORTED_BROWSERNAMES.chrome.includes(browserName.toLowerCase()))
|
||||
}
|
||||
export function isSafari (browserName?: string) {
|
||||
return Boolean(browserName && SUPPORTED_BROWSERNAMES.safari.includes(browserName.toLowerCase()))
|
||||
}
|
||||
export function isFirefox (browserName?: string) {
|
||||
return Boolean(browserName && SUPPORTED_BROWSERNAMES.firefox.includes(browserName.toLowerCase()))
|
||||
}
|
||||
export function isEdge (browserName?: string) {
|
||||
return Boolean(browserName && SUPPORTED_BROWSERNAMES.edge.includes(browserName.toLowerCase()))
|
||||
}
|
||||
|
|
@ -13,6 +13,8 @@ import {
|
|||
isW3C, capabilitiesEnvironmentDetector,
|
||||
sessionEnvironmentDetector, devtoolsEnvironmentDetector
|
||||
} from './envDetector.js'
|
||||
import { startWebDriver } from './driver/index.js'
|
||||
import { setupDriver, setupBrowser } from './driver/manager.js'
|
||||
import { UNICODE_CHARACTERS } from './constants.js'
|
||||
|
||||
export {
|
||||
|
|
@ -28,6 +30,9 @@ export {
|
|||
safeImport,
|
||||
canAccess,
|
||||
sleep,
|
||||
startWebDriver,
|
||||
setupBrowser,
|
||||
setupDriver,
|
||||
|
||||
/**
|
||||
* runner shim
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import * as iterators from './pIteration.js'
|
||||
import logger from '@wdio/logger'
|
||||
import type { Clients } from '@wdio/types'
|
||||
|
||||
import * as iterators from './pIteration.js'
|
||||
|
||||
const log = logger('@wdio/utils:shim')
|
||||
|
||||
let inCommandHook = false
|
||||
|
|
|
|||
|
|
@ -183,7 +183,6 @@ export async function safeImport (name: string): Promise<Services.ServicePlugin
|
|||
*
|
||||
* Note that import-meta-resolve will resolve to CJS no ESM export is found
|
||||
*/
|
||||
|
||||
const localNodeModules = path.join(process.cwd(), 'node_modules')
|
||||
|
||||
try {
|
||||
|
|
|
|||
334
packages/wdio-utils/tests/driver/manager.test.ts
Normal file
334
packages/wdio-utils/tests/driver/manager.test.ts
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
import path from 'node:path'
|
||||
import { describe, test, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
import type * as utils from '../../src/driver/utils.js'
|
||||
import { setupChromedriver, setupEdgedriver, setupGeckodriver, setupChrome } from '../../src/driver/utils.js'
|
||||
import { setupDriver, setupBrowser } from '../../src/driver/manager.js'
|
||||
|
||||
vi.mock('../../src/driver/utils.js', async (orig) => {
|
||||
const origMod = await orig<typeof utils>()
|
||||
return {
|
||||
...origMod,
|
||||
getCacheDir: vi.fn().mockReturnValue('/foo/bar'),
|
||||
setupChromedriver: vi.fn(),
|
||||
setupEdgedriver: vi.fn(),
|
||||
setupGeckodriver: vi.fn(),
|
||||
setupChrome: vi.fn()
|
||||
}
|
||||
})
|
||||
vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger')))
|
||||
|
||||
describe('setupDriver', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(setupChromedriver).mockClear()
|
||||
vi.mocked(setupEdgedriver).mockClear()
|
||||
vi.mocked(setupGeckodriver).mockClear()
|
||||
})
|
||||
|
||||
test('with testrunner capabilities', async () => {
|
||||
await setupDriver({}, [{
|
||||
browserName: 'chrome',
|
||||
browserVersion: '1'
|
||||
}, {
|
||||
browserName: 'chrome',
|
||||
browserVersion: '2'
|
||||
}, {
|
||||
browserName: 'edge',
|
||||
browserVersion: '3'
|
||||
}, {
|
||||
browserName: 'edge',
|
||||
browserVersion: '3'
|
||||
}, {
|
||||
browserName: 'edge',
|
||||
browserVersion: '4'
|
||||
}, {
|
||||
browserName: 'edge'
|
||||
}, {
|
||||
browserName: 'edge'
|
||||
}, {
|
||||
browserName: 'firefox',
|
||||
browserVersion: '5'
|
||||
}, {
|
||||
browserName: 'firefox',
|
||||
browserVersion: '5'
|
||||
}, {
|
||||
browserName: 'firefox',
|
||||
browserVersion: '6'
|
||||
}])
|
||||
expect(setupChromedriver).toBeCalledTimes(2)
|
||||
expect(setupChromedriver).toBeCalledWith('/foo/bar', '1')
|
||||
expect(setupChromedriver).toBeCalledWith('/foo/bar', '2')
|
||||
expect(setupEdgedriver).toBeCalledTimes(3)
|
||||
expect(setupEdgedriver).toBeCalledWith('/foo/bar', undefined)
|
||||
expect(setupEdgedriver).toBeCalledWith('/foo/bar', '3')
|
||||
expect(setupEdgedriver).toBeCalledWith('/foo/bar', '4')
|
||||
expect(setupGeckodriver).toBeCalledTimes(2)
|
||||
expect(setupGeckodriver).toBeCalledWith('/foo/bar', '5')
|
||||
expect(setupGeckodriver).toBeCalledWith('/foo/bar', '6')
|
||||
})
|
||||
|
||||
test('with multiremote capabilities', async () => {
|
||||
await setupDriver({}, {
|
||||
browserA: {
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
browserVersion: '1'
|
||||
}
|
||||
},
|
||||
browserB: {
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
browserVersion: '2'
|
||||
}
|
||||
},
|
||||
browserBRepeat: {
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
browserVersion: '2'
|
||||
}
|
||||
},
|
||||
browserC: {
|
||||
capabilities: {
|
||||
browserName: 'edge',
|
||||
browserVersion: '3'
|
||||
}
|
||||
},
|
||||
browserD: {
|
||||
capabilities: {
|
||||
browserName: 'edge',
|
||||
browserVersion: '4'
|
||||
}
|
||||
},
|
||||
browserE: {
|
||||
capabilities: {
|
||||
browserName: 'firefox',
|
||||
browserVersion: '5'
|
||||
}
|
||||
},
|
||||
browserERepeat: {
|
||||
capabilities: {
|
||||
browserName: 'firefox',
|
||||
browserVersion: '5'
|
||||
}
|
||||
},
|
||||
browserF: {
|
||||
capabilities: {
|
||||
browserName: 'firefox',
|
||||
browserVersion: '6'
|
||||
}
|
||||
}
|
||||
})
|
||||
expect(setupChromedriver).toBeCalledTimes(2)
|
||||
expect(setupChromedriver).toBeCalledWith('/foo/bar', '1')
|
||||
expect(setupChromedriver).toBeCalledWith('/foo/bar', '2')
|
||||
expect(setupEdgedriver).toBeCalledTimes(2)
|
||||
expect(setupEdgedriver).toBeCalledWith('/foo/bar', '3')
|
||||
expect(setupEdgedriver).toBeCalledWith('/foo/bar', '4')
|
||||
expect(setupGeckodriver).toBeCalledTimes(2)
|
||||
expect(setupGeckodriver).toBeCalledWith('/foo/bar', '5')
|
||||
expect(setupGeckodriver).toBeCalledWith('/foo/bar', '6')
|
||||
})
|
||||
|
||||
test('with multiremote capabilities series', async () => {
|
||||
await setupDriver({}, [{
|
||||
browserA: {
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
browserVersion: '1'
|
||||
}
|
||||
},
|
||||
browserB: {
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
browserVersion: '2'
|
||||
}
|
||||
},
|
||||
}, {
|
||||
browserC: {
|
||||
capabilities: {
|
||||
browserName: 'edge',
|
||||
browserVersion: '3'
|
||||
}
|
||||
},
|
||||
browserD: {
|
||||
capabilities: {
|
||||
browserName: 'edge',
|
||||
browserVersion: '4'
|
||||
}
|
||||
},
|
||||
browserDRepeat: {
|
||||
capabilities: {
|
||||
browserName: 'edge',
|
||||
browserVersion: '4'
|
||||
}
|
||||
},
|
||||
}, {
|
||||
browserA: {
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
browserVersion: '1'
|
||||
}
|
||||
},
|
||||
browserE: {
|
||||
capabilities: {
|
||||
browserName: 'firefox',
|
||||
browserVersion: '5'
|
||||
}
|
||||
},
|
||||
browserF: {
|
||||
capabilities: {
|
||||
browserName: 'firefox',
|
||||
browserVersion: '6'
|
||||
}
|
||||
}
|
||||
}])
|
||||
expect(setupChromedriver).toBeCalledTimes(2)
|
||||
expect(setupChromedriver).toBeCalledWith('/foo/bar', '1')
|
||||
expect(setupChromedriver).toBeCalledWith('/foo/bar', '2')
|
||||
expect(setupEdgedriver).toBeCalledTimes(2)
|
||||
expect(setupEdgedriver).toBeCalledWith('/foo/bar', '3')
|
||||
expect(setupEdgedriver).toBeCalledWith('/foo/bar', '4')
|
||||
expect(setupGeckodriver).toBeCalledTimes(2)
|
||||
expect(setupGeckodriver).toBeCalledWith('/foo/bar', '5')
|
||||
expect(setupGeckodriver).toBeCalledWith('/foo/bar', '6')
|
||||
})
|
||||
})
|
||||
|
||||
describe('setupBrowser', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(setupChrome).mockClear()
|
||||
})
|
||||
|
||||
test('with testrunner capabilities', async () => {
|
||||
await setupBrowser({}, [{
|
||||
browserName: 'chrome',
|
||||
browserVersion: '1'
|
||||
}, {
|
||||
browserName: 'chrome',
|
||||
browserVersion: '2'
|
||||
}, {
|
||||
browserName: 'edge',
|
||||
browserVersion: '3'
|
||||
}, {
|
||||
browserName: 'edge',
|
||||
browserVersion: '4'
|
||||
}, {
|
||||
browserName: 'firefox',
|
||||
browserVersion: '5'
|
||||
}, {
|
||||
browserName: 'firefox',
|
||||
browserVersion: '6'
|
||||
}])
|
||||
expect(setupChrome).toBeCalledTimes(2)
|
||||
expect(setupChrome).toBeCalledWith('/foo/bar', {
|
||||
browserName: 'chrome',
|
||||
browserVersion: '1'
|
||||
})
|
||||
expect(setupChrome).toBeCalledWith('/foo/bar', {
|
||||
browserName: 'chrome',
|
||||
browserVersion: '2'
|
||||
})
|
||||
})
|
||||
|
||||
test('with multiremote capabilities', async () => {
|
||||
await setupBrowser({}, {
|
||||
browserA: {
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
browserVersion: '1'
|
||||
}
|
||||
},
|
||||
browserB: {
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
browserVersion: '2'
|
||||
}
|
||||
},
|
||||
browserC: {
|
||||
capabilities: {
|
||||
browserName: 'edge',
|
||||
browserVersion: '3'
|
||||
}
|
||||
},
|
||||
browserD: {
|
||||
capabilities: {
|
||||
browserName: 'edge',
|
||||
browserVersion: '4'
|
||||
}
|
||||
},
|
||||
browserE: {
|
||||
capabilities: {
|
||||
browserName: 'firefox',
|
||||
browserVersion: '5'
|
||||
}
|
||||
},
|
||||
browserF: {
|
||||
capabilities: {
|
||||
browserName: 'firefox',
|
||||
browserVersion: '6'
|
||||
}
|
||||
}
|
||||
})
|
||||
expect(setupChrome).toBeCalledTimes(2)
|
||||
expect(setupChrome).toBeCalledWith('/foo/bar', {
|
||||
browserName: 'chrome',
|
||||
browserVersion: '1'
|
||||
})
|
||||
expect(setupChrome).toBeCalledWith('/foo/bar', {
|
||||
browserName: 'chrome',
|
||||
browserVersion: '2'
|
||||
})
|
||||
})
|
||||
|
||||
test('with multiremote capabilities series', async () => {
|
||||
await setupBrowser({}, [{
|
||||
browserA: {
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
browserVersion: '1'
|
||||
}
|
||||
},
|
||||
browserB: {
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
browserVersion: '2'
|
||||
}
|
||||
},
|
||||
}, {
|
||||
browserC: {
|
||||
capabilities: {
|
||||
browserName: 'edge',
|
||||
browserVersion: '3'
|
||||
}
|
||||
},
|
||||
browserD: {
|
||||
capabilities: {
|
||||
browserName: 'edge',
|
||||
browserVersion: '4'
|
||||
}
|
||||
},
|
||||
}, {
|
||||
browserE: {
|
||||
capabilities: {
|
||||
browserName: 'firefox',
|
||||
browserVersion: '5'
|
||||
}
|
||||
},
|
||||
browserF: {
|
||||
capabilities: {
|
||||
browserName: 'firefox',
|
||||
browserVersion: '6'
|
||||
}
|
||||
}
|
||||
}])
|
||||
expect(setupChrome).toBeCalledTimes(2)
|
||||
expect(setupChrome).toBeCalledWith('/foo/bar', {
|
||||
browserName: 'chrome',
|
||||
browserVersion: '1'
|
||||
})
|
||||
expect(setupChrome).toBeCalledWith('/foo/bar', {
|
||||
browserName: 'chrome',
|
||||
browserVersion: '2'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -32,6 +32,13 @@ vi.mock('node:fs', () => ({
|
|||
}
|
||||
}))
|
||||
|
||||
vi.mock('node:fs/promises', () => ({
|
||||
default: {
|
||||
mkdir: vi.fn().mockResolvedValue({}),
|
||||
access: vi.fn().mockResolvedValue({})
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('node:child_process', () => ({
|
||||
default: {
|
||||
execSync: vi.fn().mockReturnValue(Buffer.from('Google Chrome 115.0.5790.98\n'))
|
||||
|
|
@ -75,13 +82,14 @@ describe('driver utils', () => {
|
|||
|
||||
it('should throw if platform is not supported', async () => {
|
||||
vi.mocked(detectBrowserPlatform).mockReturnValueOnce(undefined)
|
||||
await expect(setupChrome({}, '/foo/bar')).rejects.toThrow('The current platform is not supported.')
|
||||
await expect(setupChrome('/foo/bar', {})).rejects.toThrow('The current platform is not supported.')
|
||||
})
|
||||
|
||||
it('should run setup for local chrome if browser version is omitted', async () => {
|
||||
vi.mocked(detectBrowserPlatform).mockReturnValueOnce('mac' as any)
|
||||
await expect(setupChrome({}, '/foo/bar')).resolves.toEqual({
|
||||
await expect(setupChrome('/foo/bar', {})).resolves.toEqual({
|
||||
buildId: '115.0.5790.98',
|
||||
cacheDir: '/foo/bar',
|
||||
executablePath: '/foo/bar',
|
||||
platform: 'mac'
|
||||
})
|
||||
|
|
@ -90,8 +98,9 @@ describe('driver utils', () => {
|
|||
it('should install chrome stable if browser is not found', async () => {
|
||||
vi.mocked(detectBrowserPlatform).mockReturnValueOnce('windows' as any)
|
||||
vi.mocked(getChromePath).mockReturnValue('/path/to/stable')
|
||||
await expect(setupChrome({}, '/foo/bar')).resolves.toEqual( {
|
||||
await expect(setupChrome('/foo/bar', {})).resolves.toEqual( {
|
||||
buildId: '115.0.5790.98',
|
||||
cacheDir: '/foo/bar',
|
||||
executablePath: '/path/to/stable',
|
||||
platform: 'windows'
|
||||
})
|
||||
|
|
@ -101,15 +110,14 @@ describe('driver utils', () => {
|
|||
vi.mocked(detectBrowserPlatform).mockReturnValueOnce('windows' as any)
|
||||
vi.mocked(canDownload).mockResolvedValueOnce(false)
|
||||
vi.mocked(getChromePath).mockImplementationOnce(() => { throw new Error('boom') })
|
||||
await expect(setupChrome({}, '/foo/bar')).rejects.toThrow(/Couldn't find a matching Chrome browser /)
|
||||
await expect(setupChrome('/foo/bar', {})).rejects.toThrow(/Couldn't find a matching Chrome browser /)
|
||||
})
|
||||
|
||||
it('should install chrome browser with specific version provided', async () => {
|
||||
vi.mocked(detectBrowserPlatform).mockReturnValueOnce('windows' as any)
|
||||
await expect(setupChrome({ browserVersion: '1.2.3' }, '/foo/bar')).resolves.toEqual({
|
||||
buildId: '115.0.5790.98',
|
||||
await expect(setupChrome('/foo/bar', { browserVersion: '1.2.3' })).resolves.toEqual({
|
||||
browserVersion: '115.0.5790.98',
|
||||
executablePath: '/foo/bar/executable',
|
||||
platform: 'windows'
|
||||
})
|
||||
expect(resolveBuildId).toBeCalledWith('chrome', 'windows', '1.2.3')
|
||||
})
|
||||
|
|
@ -36,7 +36,6 @@
|
|||
"url": "https://github.com/webdriverio/webdriverio/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "^1.4.6",
|
||||
"@types/node": "^20.1.0",
|
||||
"@types/ws": "^8.5.3",
|
||||
"@wdio/config": "8.14.6",
|
||||
|
|
@ -44,16 +43,9 @@
|
|||
"@wdio/protocols": "8.14.6",
|
||||
"@wdio/types": "8.14.0",
|
||||
"@wdio/utils": "8.14.0",
|
||||
"chrome-launcher": "^1.0.0",
|
||||
"decamelize": "^6.0.0",
|
||||
"deepmerge-ts": "^5.1.0",
|
||||
"edgedriver": "^5.3.2",
|
||||
"geckodriver": "^4.2.0",
|
||||
"get-port": "^7.0.0",
|
||||
"got": "^ 12.6.1",
|
||||
"ky": "^0.33.0",
|
||||
"safaridriver": "^0.1.0",
|
||||
"wait-port": "^1.0.4",
|
||||
"ws": "^8.8.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,10 +152,3 @@ export const REG_EXPS = {
|
|||
commandName: /.*\/session\/[0-9a-f-]+\/(.*)/,
|
||||
execFn: /return \(([\s\S]*)\)\.apply\(null, arguments\)/
|
||||
}
|
||||
|
||||
export const SUPPORTED_BROWSERNAMES = {
|
||||
chrome: ['chrome', 'googlechrome', 'chromium', 'chromium-browser'],
|
||||
firefox: ['firefox', 'ff', 'mozilla', 'mozilla firefox'],
|
||||
edge: ['edge', 'microsoftedge', 'msedge'],
|
||||
safari: ['safari', 'safari technology preview']
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,202 +0,0 @@
|
|||
import fs from 'node:fs'
|
||||
import fsp from 'node:fs/promises'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import cp, { type ChildProcess } from 'node:child_process'
|
||||
|
||||
import got from 'got'
|
||||
import getPort from 'get-port'
|
||||
import waitPort from 'wait-port'
|
||||
import logger from '@wdio/logger'
|
||||
import { deepmerge } from 'deepmerge-ts'
|
||||
import type { Options } from '@wdio/types'
|
||||
|
||||
import { start as startSafaridriver, type SafaridriverOptions } from 'safaridriver'
|
||||
import { start as startGeckodriver, type GeckodriverParameters } from 'geckodriver'
|
||||
import { start as startEdgedriver, type EdgedriverParameters } from 'edgedriver'
|
||||
import { install, computeExecutablePath, Browser, type InstallOptions } from '@puppeteer/browsers'
|
||||
|
||||
import type { Capabilities } from '@wdio/types'
|
||||
|
||||
import { parseParams, setupChrome, definesRemoteDriver, downloadProgressCallback } from './utils.js'
|
||||
import { SUPPORTED_BROWSERNAMES } from '../constants.js'
|
||||
|
||||
const log = logger('webdriver')
|
||||
|
||||
export interface ExtendedCapabilities extends Capabilities.Capabilities, WDIODriverOptions {}
|
||||
export type ChromedriverOptions = InstallOptions & Omit<EdgedriverParameters, 'port' | 'edgeDriverVersion' | 'customEdgeDriverPath'>
|
||||
|
||||
export interface WDIODriverOptions {
|
||||
'wdio:chromedriverOptions'?: ChromedriverOptions
|
||||
'wdio:safaridriverOptions'?: Omit<SafaridriverOptions, 'port'>
|
||||
'wdio:geckodriverOptions'?: Omit<GeckodriverParameters, 'port'>
|
||||
'wdio:edgedriverOptions'?: Omit<EdgedriverParameters, 'port'>
|
||||
}
|
||||
|
||||
export async function startWebDriver (options: Options.WebDriver) {
|
||||
/**
|
||||
* in case we are running unit tests, just return
|
||||
*/
|
||||
if (process.env.WDIO_SKIP_DRIVER_SETUP) {
|
||||
options.hostname = '0.0.0.0'
|
||||
options.port = 4321
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* if any of the connection parameter are set, don't start any driver
|
||||
*/
|
||||
if (definesRemoteDriver(options)) {
|
||||
log.info(`Connecting to existing driver at ${options.protocol}://${options.hostname}:${options.port}${options.path}`)
|
||||
return
|
||||
}
|
||||
|
||||
let driverProcess: ChildProcess
|
||||
let driver = ''
|
||||
const start = Date.now()
|
||||
const caps: ExtendedCapabilities = (options.capabilities as Capabilities.W3CCapabilities).alwaysMatch || options.capabilities as Capabilities.Capabilities
|
||||
|
||||
/**
|
||||
* session might be a mobile session so don't do anything
|
||||
*/
|
||||
if (!caps.browserName) {
|
||||
throw new Error(
|
||||
'No "browserName" defined in capabilities nor hostname or port found!\n' +
|
||||
'If you like to run a mobile session with Appium, make sure to set "hostname" and "port" in your ' +
|
||||
'WebdriverIO options. If you like to run a local browser session make sure to pick from one of ' +
|
||||
`the following browser names: ${Object.values(SUPPORTED_BROWSERNAMES).flat(Infinity)}`
|
||||
)
|
||||
}
|
||||
|
||||
const port = await getPort()
|
||||
if (SUPPORTED_BROWSERNAMES.chrome.includes(caps.browserName.toLowerCase())) {
|
||||
/**
|
||||
* Chrome
|
||||
*/
|
||||
caps.browserName = caps.browserName.toLowerCase()
|
||||
const chromedriverOptions = caps['wdio:chromedriverOptions'] || ({} as ChromedriverOptions)
|
||||
const cacheDir = chromedriverOptions.cacheDir || options.cacheDir || os.tmpdir()
|
||||
const exist = await fsp.access(cacheDir).then(() => true, () => false)
|
||||
if (!exist) {
|
||||
await fsp.mkdir(cacheDir, { recursive: true })
|
||||
}
|
||||
|
||||
const { executablePath, buildId, platform } = await setupChrome(caps, cacheDir)
|
||||
let chromedriverBinaryPath = computeExecutablePath({
|
||||
browser: Browser.CHROMEDRIVER,
|
||||
buildId,
|
||||
cacheDir
|
||||
})
|
||||
let loggedBuildId = buildId
|
||||
const hasChromedriverInstalled = await fsp.access(chromedriverBinaryPath).then(() => true, () => false)
|
||||
if (!hasChromedriverInstalled) {
|
||||
log.info(`Downloading Chromedriver v${buildId}`)
|
||||
const chromedriverInstallOpts: InstallOptions & {unpack?: true} = {
|
||||
...chromedriverOptions,
|
||||
cacheDir,
|
||||
platform,
|
||||
buildId,
|
||||
browser: Browser.CHROMEDRIVER,
|
||||
unpack: true,
|
||||
downloadProgressCallback: (downloadedBytes, totalBytes) => downloadProgressCallback('Chromedriver', downloadedBytes, totalBytes)
|
||||
}
|
||||
|
||||
try {
|
||||
await install({ ...chromedriverInstallOpts, buildId })
|
||||
} catch (err) {
|
||||
/**
|
||||
* in case we detect a Chrome browser installed for which there is no Chromedriver available
|
||||
* we are falling back to the latest known good version
|
||||
*/
|
||||
log.warn(`Couldn't download Chromedriver v${buildId}: ${(err as Error).message}, trying to find known good version...`)
|
||||
const majorVersion = buildId.split('.')[0]
|
||||
const knownGoodVersions: any = await got('https://googlechromelabs.github.io/chrome-for-testing/known-good-versions.json').json()
|
||||
const knownGoodVersion = knownGoodVersions.versions.filter(({ version }: { version: string }) => version.startsWith(majorVersion)).pop()
|
||||
if (!knownGoodVersion) {
|
||||
throw new Error(`Couldn't find known good version for Chromedriver v${majorVersion}`)
|
||||
}
|
||||
loggedBuildId = knownGoodVersion.version
|
||||
await install({ ...chromedriverInstallOpts, buildId: loggedBuildId })
|
||||
chromedriverBinaryPath = computeExecutablePath({
|
||||
browser: Browser.CHROMEDRIVER,
|
||||
buildId: loggedBuildId,
|
||||
cacheDir
|
||||
})
|
||||
}
|
||||
} else {
|
||||
log.info(`Using Chromedriver v${buildId} from cache directory ${cacheDir}`)
|
||||
}
|
||||
caps['goog:chromeOptions'] = deepmerge(
|
||||
{ binary: executablePath },
|
||||
caps['goog:chromeOptions'] || {}
|
||||
)
|
||||
chromedriverOptions.allowedOrigins = chromedriverOptions.allowedOrigins || ['*']
|
||||
chromedriverOptions.allowedIps = chromedriverOptions.allowedIps || ['']
|
||||
const driverParams = parseParams({ port, ...chromedriverOptions })
|
||||
driverProcess = cp.spawn(chromedriverBinaryPath, driverParams)
|
||||
driver = `Chromedriver v${loggedBuildId} with params ${driverParams.join(' ')}`
|
||||
} else if (SUPPORTED_BROWSERNAMES.safari.includes(caps.browserName.toLowerCase())) {
|
||||
const safaridriverOptions = caps['wdio:safaridriverOptions'] || ({} as SafaridriverOptions)
|
||||
/**
|
||||
* Safari
|
||||
*/
|
||||
driver = 'SafariDriver'
|
||||
driverProcess = startSafaridriver({
|
||||
useTechnologyPreview: Boolean(caps.browserName.match(/preview/i)),
|
||||
...safaridriverOptions,
|
||||
port
|
||||
})
|
||||
|
||||
/**
|
||||
* set "Host" header as it is required by Safaridriver
|
||||
*/
|
||||
options.headers = deepmerge({ Host: 'localhost' }, (options.headers || {}))
|
||||
} else if (SUPPORTED_BROWSERNAMES.firefox.includes(caps.browserName.toLowerCase())) {
|
||||
/**
|
||||
* Firefox
|
||||
*/
|
||||
const geckodriverOptions = caps['wdio:geckodriverOptions'] || ({} as GeckodriverParameters)
|
||||
const cacheDir = geckodriverOptions.cacheDir || options.cacheDir || os.tmpdir()
|
||||
driver = 'GeckoDriver'
|
||||
driverProcess = await startGeckodriver({ ...geckodriverOptions, cacheDir, port })
|
||||
} else if (SUPPORTED_BROWSERNAMES.edge.includes(caps.browserName.toLowerCase())) {
|
||||
/**
|
||||
* Microsoft Edge
|
||||
*/
|
||||
const edgedriverOptions = caps['wdio:edgedriverOptions'] || ({} as EdgedriverParameters)
|
||||
const cacheDir = edgedriverOptions.cacheDir || options.cacheDir || os.tmpdir()
|
||||
driver = 'EdgeDriver'
|
||||
driverProcess = await startEdgedriver({ ...edgedriverOptions, cacheDir, port }).catch((err) => {
|
||||
log.warn(`Couldn't start EdgeDriver: ${err.message}, retry ...`)
|
||||
return startEdgedriver({ ...edgedriverOptions, cacheDir, port })
|
||||
})
|
||||
|
||||
/**
|
||||
* Microsoft Edge is very particular when it comes to browser names
|
||||
*/
|
||||
caps.browserName = 'MicrosoftEdge'
|
||||
} else {
|
||||
throw new Error(
|
||||
`Unknown browser name "${caps.browserName}". Make sure to pick from one of the following ` +
|
||||
Object.values(SUPPORTED_BROWSERNAMES).flat(Infinity)
|
||||
)
|
||||
}
|
||||
|
||||
if (options.outputDir) {
|
||||
const logIdentifier = driver.split(' ').shift()?.toLowerCase()
|
||||
const logFileName = process.env.WDIO_WORKER_ID
|
||||
? `wdio-${process.env.WDIO_WORKER_ID}-${logIdentifier}.log`
|
||||
: `wdio-${logIdentifier}-${port}.log`
|
||||
const logFile = path.resolve(options.outputDir, logFileName)
|
||||
const logStream = fs.createWriteStream(logFile, { flags: 'w' })
|
||||
driverProcess.stdout?.pipe(logStream)
|
||||
driverProcess.stderr?.pipe(logStream)
|
||||
}
|
||||
|
||||
await waitPort({ port, output: 'silent' })
|
||||
|
||||
options.hostname = '0.0.0.0'
|
||||
options.port = port
|
||||
log.info(`Started ${driver} in ${Date.now() - start}ms on port ${port}`)
|
||||
return driverProcess
|
||||
}
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
import os from 'node:os'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import cp from 'node:child_process'
|
||||
|
||||
import decamelize from 'decamelize'
|
||||
import logger from '@wdio/logger'
|
||||
import { getChromePath } from 'chrome-launcher'
|
||||
import {
|
||||
install, canDownload, resolveBuildId, detectBrowserPlatform, Browser, ChromeReleaseChannel,
|
||||
computeExecutablePath, type InstallOptions
|
||||
} from '@puppeteer/browsers'
|
||||
import type { EdgedriverParameters } from 'edgedriver'
|
||||
import type { Capabilities, Options } from '@wdio/types'
|
||||
|
||||
import { DEFAULTS } from '../constants.js'
|
||||
|
||||
const log = logger('webdriver')
|
||||
const EXCLUDED_PARAMS = ['version', 'help']
|
||||
|
||||
export function parseParams(params: EdgedriverParameters) {
|
||||
return Object.entries(params)
|
||||
.filter(([key,]) => !EXCLUDED_PARAMS.includes(key))
|
||||
.map(([key, val]) => {
|
||||
if (typeof val === 'boolean' && !val) {
|
||||
return ''
|
||||
}
|
||||
const vals = Array.isArray(val) ? val : [val]
|
||||
return vals.map((v) => `--${decamelize(key, { separator: '-' })}${typeof v === 'boolean' ? '' : `=${v}`}`)
|
||||
})
|
||||
.flat()
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
export function getLocalChromePath() {
|
||||
try {
|
||||
const chromePath = getChromePath()
|
||||
return chromePath
|
||||
} catch (err: unknown) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
export function getBuildIdByPath(chromePath?: string) {
|
||||
if (!chromePath) {
|
||||
return
|
||||
} else if (os.platform() === 'win32') {
|
||||
const versionPath = path.dirname(chromePath)
|
||||
const contents = fs.readdirSync(versionPath)
|
||||
const versions = contents.filter(a => /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/g.test(a))
|
||||
|
||||
// returning oldest in case there is an updated version and chrome still hasn't relaunched
|
||||
const oldest = versions.sort((a: string, b: string) => a > b ? -1 : 1)[0]
|
||||
return oldest
|
||||
}
|
||||
|
||||
const versionString = cp.execSync(`"${chromePath}" --version`).toString()
|
||||
return versionString.split(' ').pop()?.trim()
|
||||
}
|
||||
|
||||
let lastTimeCalled = Date.now()
|
||||
export const downloadProgressCallback = (artifact: string, downloadedBytes: number, totalBytes: number) => {
|
||||
if (Date.now() - lastTimeCalled < 1000) {
|
||||
return
|
||||
}
|
||||
const percentage = ((downloadedBytes / totalBytes) * 100).toFixed(2)
|
||||
log.info(`Downloading ${artifact} ${percentage}%`)
|
||||
lastTimeCalled = Date.now()
|
||||
}
|
||||
|
||||
export async function setupChrome(caps: Capabilities.Capabilities, cacheDir: string) {
|
||||
const platform = detectBrowserPlatform()
|
||||
if (!platform) {
|
||||
throw new Error('The current platform is not supported.')
|
||||
}
|
||||
|
||||
if (!caps.browserVersion) {
|
||||
const executablePath = getLocalChromePath()!
|
||||
const tag = getBuildIdByPath(executablePath)
|
||||
|
||||
/**
|
||||
* verify that we have a valid Chrome browser installed
|
||||
*/
|
||||
if (tag) {
|
||||
return {
|
||||
platform,
|
||||
executablePath,
|
||||
buildId: await resolveBuildId(Browser.CHROME, platform, tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* otherwise download provided Chrome browser version or "stable"
|
||||
*/
|
||||
const tag = caps.browserVersion || ChromeReleaseChannel.STABLE
|
||||
const buildId = await resolveBuildId(Browser.CHROME, platform, tag)
|
||||
const installOptions: InstallOptions & { unpack?: true } = {
|
||||
unpack: true,
|
||||
cacheDir,
|
||||
platform,
|
||||
buildId,
|
||||
browser: Browser.CHROME,
|
||||
downloadProgressCallback: (downloadedBytes, totalBytes) => downloadProgressCallback('Chrome', downloadedBytes, totalBytes)
|
||||
}
|
||||
const isCombinationAvailable = await canDownload(installOptions)
|
||||
if (!isCombinationAvailable) {
|
||||
throw new Error(`Couldn't find a matching Chrome browser for tag "${buildId}" on platform "${platform}"`)
|
||||
}
|
||||
|
||||
log.info(`Setting up Chrome v${buildId}`)
|
||||
await install(installOptions)
|
||||
const executablePath = computeExecutablePath(installOptions)
|
||||
return { executablePath, buildId, platform }
|
||||
}
|
||||
|
||||
/**
|
||||
* helper method to determine if we need to start a browser driver
|
||||
* which is: whenever the user has set connection options that differ from
|
||||
* the default, or a port is set
|
||||
*/
|
||||
export function definesRemoteDriver(options: Options.WebDriver) {
|
||||
return Boolean(
|
||||
(options.protocol && options.protocol !== DEFAULTS.protocol.default) ||
|
||||
(options.hostname && options.hostname !== DEFAULTS.hostname.default) ||
|
||||
Boolean(options.port) ||
|
||||
(options.path && options.path !== DEFAULTS.path.default)
|
||||
)
|
||||
}
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
import path from 'node:path'
|
||||
import logger from '@wdio/logger'
|
||||
|
||||
import { webdriverMonad, sessionEnvironmentDetector } from '@wdio/utils'
|
||||
import { webdriverMonad, sessionEnvironmentDetector, startWebDriver } from '@wdio/utils'
|
||||
import { validateConfig } from '@wdio/config'
|
||||
import type { Options, Capabilities } from '@wdio/types'
|
||||
|
||||
import command from './command.js'
|
||||
import { startWebDriver } from './driver/index.js'
|
||||
import { BidiHandler } from './bidi/handler.js'
|
||||
import { DEFAULTS } from './constants.js'
|
||||
import { startWebDriverSession, getPrototype, getEnvironmentVars, setupDirectConnect } from './utils.js'
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ const packages = {
|
|||
'webdriver': 'packages/webdriver',
|
||||
'webdriverio': 'packages/webdriverio',
|
||||
'@wdio/globals': 'packages/wdio-globals',
|
||||
'@wdio/utils': 'packages/wdio-utils',
|
||||
'@wdio/browser-runner': 'packages/wdio-browser-runner',
|
||||
'@wdio/reporter': 'packages/wdio-reporter',
|
||||
'@wdio/allure-reporter': 'packages/wdio-allure-reporter',
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
"noImplicitAny": true,
|
||||
"lib": ["es2018", "dom"],
|
||||
"types": [
|
||||
"@wdio/utils",
|
||||
"@wdio/globals/types",
|
||||
"@wdio/allure-reporter",
|
||||
"@wdio/appium-service",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"extends": "@tsconfig/node18/tsconfig.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"outDir": "./build",
|
||||
"allowJs": true,
|
||||
"declaration": true,
|
||||
|
|
|
|||
Loading…
Reference in a new issue