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 & Omit & Pick ) declare global { namespace WebdriverIO { interface ChromedriverOptions extends ChromedriverParameters {} interface GeckodriverOptions extends Omit {} interface EdgedriverOptions extends Omit {} interface SafaridriverOptions extends Omit {} } } const log = logger('@wdio/utils') const DRIVER_WAIT_TIMEOUT = 10 * 1000 // 10s 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 }