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 split2 from 'split2'
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, findEdgePath, type EdgedriverParameters } from 'edgedriver'
import type { InstallOptions } from '@puppeteer/browsers'
import type { Capabilities } from '@wdio/types'
import { parseParams, setupPuppeteerBrowser, setupChromedriver, getCacheDir, generateDefaultPrefs } from './utils.js'
import { isChrome, isFirefox, isEdge, isSafari, isAppiumCapability } from '../utils.js'
import { SUPPORTED_BROWSERNAMES } from '../constants.js'
export type ChromedriverParameters = (
Partial<InstallOptions> &
Omit<EdgedriverParameters, 'port' | 'edgeDriverVersion' | 'customEdgeDriverPath'> &
Pick<GeckodriverParameters, 'spawnOpts'>
)
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')
const DRIVER_WAIT_TIMEOUT = 10 * 1000
const DRIVER_RETRY_INTERVAL = 100
export async function startWebDriver (options: Capabilities.RemoteConfig) {
* in case we are running unit tests, just return
*/
if (process.env.WDIO_SKIP_DRIVER_SETUP) {
options.hostname = 'localhost'
options.port = 4321
return
}
let driverProcess: ChildProcess
let driver = ''
const start = Date.now()
const caps = (options.capabilities as Capabilities.W3CCapabilities).alwaysMatch || options.capabilities as WebdriverIO.Capabilities
* session might be a mobile session so don't do anything
*/
if (isAppiumCapability(caps)) {
return
}
if (!caps.browserName) {
throw new Error(
'No "browserName" defined in capabilities nor hostname or port found!\n' +
'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)
* support for custom chromedriver path via environment variable like
* other drivers do as well
*/
const chromedriverBinary = chromedriverOptions.binary || process.env.CHROMEDRIVER_PATH
const { executablePath: chromeExecuteablePath, browserVersion } = await setupPuppeteerBrowser(cacheDir, caps)
const { executablePath: chromedriverExcecuteablePath } = chromedriverBinary
? { executablePath: chromedriverBinary }
: await setupChromedriver(cacheDir, browserVersion)
const prefs = generateDefaultPrefs(caps)
caps['goog:chromeOptions'] = deepmerge(
{ binary: chromeExecuteablePath },
prefs,
caps['goog:chromeOptions'] || {}
)
chromedriverOptions.allowedOrigins = chromedriverOptions.allowedOrigins || ['*']
chromedriverOptions.allowedIps = chromedriverOptions.allowedIps || ['0.0.0.0']
const driverParams = parseParams({ port, ...chromedriverOptions })
* Set NODE_OPTIONS empty to avoid passing it to the chromedriver process so that Electron doesn't crash
*/
driverProcess = cp.spawn(chromedriverExcecuteablePath, driverParams, {
env: { ...process.env, NODE_OPTIONS: '' },
...(chromedriverOptions.spawnOpts || {})
})
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: /preview/i.test(caps.browserName),
...safaridriverOptions,
port
})
} else if (isFirefox(caps.browserName)) {
* Firefox
*/
const { executablePath } = await setupPuppeteerBrowser(cacheDir, caps)
caps['moz:firefoxOptions'] = deepmerge(
{ binary: executablePath },
caps['moz:firefoxOptions'] || {}
)
* the "binary" parameter refers to the driver binary in the WebdriverIO.GeckodriverOptions and
* to the Firefox binary in the driver option
*/
delete caps.browserVersion
const { binary, ...geckodriverOptions } = caps['wdio:geckodriverOptions'] || ({} as WebdriverIO.GeckodriverOptions)
if (binary) {
geckodriverOptions.customGeckoDriverPath = binary
}
driver = 'GeckoDriver'
driverProcess = await startGeckodriver({ ...geckodriverOptions, cacheDir, port, allowHosts: ['localhost'] })
} else if (isEdge(caps.browserName)) {
* Microsoft Edge
*/
const { binary, ...edgedriverOptions } = caps['wdio:edgedriverOptions'] || ({} as WebdriverIO.EdgedriverOptions)
if (binary) {
edgedriverOptions.customEdgeDriverPath = binary
}
driver = 'EdgeDriver'
driverProcess = await startEdgedriver({ ...edgedriverOptions, cacheDir, port, allowedIps: ['0.0.0.0'] }).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'
* on Linux set the path to the Edge binary if not already set
*/
if (!caps['ms:edgeOptions']?.binary) {
caps['ms:edgeOptions'] = caps['ms:edgeOptions'] || {}
caps['ms:edgeOptions'].binary = findEdgePath()
log.info(`Found Edge binary at ${caps['ms:edgeOptions'].binary}`)
}
} else {
throw new Error(
`Unknown browser name "${caps.browserName}". Make sure to pick from one of the following ` +
Object.values(SUPPORTED_BROWSERNAMES).flat(Infinity)
)
}
const logIdentifier = driver.split(' ').shift()?.toLowerCase() || 'driver'
if (options.outputDir) {
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)
} else {
const driverLog = logger(logIdentifier)
driverProcess.stdout?.pipe(split2()).on('data', driverLog.info.bind(driverLog))
driverProcess.stderr?.pipe(split2()).on('data', driverLog.warn.bind(driverLog))
}
await waitPort({ port, output: 'silent', timeout: DRIVER_WAIT_TIMEOUT, interval: DRIVER_RETRY_INTERVAL })
.catch((e) => { throw new Error(`Timed out to connect to ${driver}: ${e.message}`) })
options.hostname = 'localhost'
options.port = port
log.info(`Started ${driver} in ${Date.now() - start}ms on port ${port}`)
return driverProcess
}