(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:
Christian Bromann 2023-08-15 00:45:24 -07:00 committed by GitHub
parent 2a3178cc95
commit 34661c5f45
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 995 additions and 396 deletions

View file

@ -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()

View file

@ -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, {}]],
/**

View file

@ -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
View file

@ -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",

View file

@ -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 = () => {}

View file

@ -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)}`)
}
/**

View file

@ -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)
/**

View file

@ -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

View file

@ -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 {}
}
}

View file

@ -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"

View file

@ -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 = '/'

View 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
}

View 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')
}

View 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()))
}

View file

@ -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

View file

@ -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

View file

@ -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 {

View 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'
})
})
})

View file

@ -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')
})

View file

@ -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"
}
}

View file

@ -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']
}

View file

@ -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
}

View file

@ -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)
)
}

View file

@ -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'

View file

@ -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',

View file

@ -4,6 +4,7 @@
"noImplicitAny": true,
"lib": ["es2018", "dom"],
"types": [
"@wdio/utils",
"@wdio/globals/types",
"@wdio/allure-reporter",
"@wdio/appium-service",

View file

@ -2,6 +2,7 @@
"extends": "@tsconfig/node18/tsconfig.json",
"compilerOptions": {
"strict": true,
"outDir": "./build",
"allowJs": true,
"declaration": true,