import logger from '@wdio/logger' import type { Options, Capabilities } from '@wdio/types' import { getCacheDir, getDriverOptions, setupChromedriver, setupEdgedriver, setupGeckodriver, setupPuppeteerBrowser } from './utils.js' import { definesRemoteDriver, isSafari, isEdge, isFirefox, isChrome } from '../utils.js' const log = logger('@wdio/utils') const UNDEFINED_BROWSER_VERSION = null type SetupTaskFunction = (cap: WebdriverIO.Capabilities) => Promise enum BrowserDriverTaskLabel { BROWSER = 'browser binaries', DRIVER = 'browser driver' } const firefoxChannels: string[] = ['stable', 'latest'] as const function mapCapabilities ( options: Options.WebdriverIO, caps: Capabilities.TestrunnerCapabilities, task: SetupTaskFunction, taskItemLabel: string) { const capabilitiesToRequireSetup = ( Array.isArray(caps) ? caps.map((cap: Capabilities.RequestedStandaloneCapabilities | Capabilities.RequestedMultiremoteCapabilities) => { const w3cCaps = cap as Capabilities.W3CCapabilities const multiremoteCaps = cap as Capabilities.RequestedMultiremoteCapabilities const multiremoteInstanceNames = Object.keys(multiremoteCaps) if (typeof multiremoteCaps[multiremoteInstanceNames[0]] === 'object' && 'capabilities' in multiremoteCaps[multiremoteInstanceNames[0]]) { return Object.values(multiremoteCaps).map((c: Capabilities.WithRequestedCapabilities) => ( 'alwaysMatch' in c.capabilities ? c.capabilities.alwaysMatch : c.capabilities )) } if (w3cCaps.alwaysMatch) { return w3cCaps.alwaysMatch } return cap as WebdriverIO.Capabilities }).flat() : Object.values(caps as Capabilities.WithRequestedMultiremoteCapabilities['capabilities']).map((mrOpts) => { const w3cCaps = mrOpts.capabilities as Capabilities.W3CCapabilities if (w3cCaps.alwaysMatch) { return w3cCaps.alwaysMatch } return mrOpts.capabilities as WebdriverIO.Capabilities }) ).flat().filter((cap) => ( /** * only set up driver if */ // - capabilities are defined and not empty cap && // - browserName is defined so we know it is a browser session cap.browserName && // - we are not about to run a cloud session !definesRemoteDriver(options) && // - we are not running Safari (driver already installed on macOS) !isSafari(cap.browserName) && // - driver options don't define a binary path !getDriverOptions(cap).binary && // - environment does not define a binary path !(process.env.CHROMEDRIVER_PATH && isChrome(cap.browserName)) )) as WebdriverIO.Capabilities[] /** * 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()) } 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, caps: Capabilities.TestrunnerCapabilities) { return mapCapabilities(options, caps, async (cap: WebdriverIO.Capabilities) => { const cacheDir = getCacheDir(options, cap) if (isEdge(cap.browserName)) { return setupEdgedriver(cacheDir, cap.browserVersion) } else if (isFirefox(cap.browserName)) { /** * Some Firefox channels are allowed to be set for the browserVersion. * We unset these variables here to make `node-geckodriver` download * the latest driver version. */ const version = firefoxChannels.includes(cap.browserVersion ?? '') ? undefined : cap.browserVersion return setupGeckodriver(cacheDir, version) } else if (isChrome(cap.browserName)) { return setupChromedriver(cacheDir, cap.browserVersion) } return Promise.resolve() }, BrowserDriverTaskLabel.DRIVER) } export function setupBrowser (options: Omit, caps: Capabilities.TestrunnerCapabilities) { return mapCapabilities(options, caps, async (cap: WebdriverIO.Capabilities) => { const cacheDir = getCacheDir(options, cap) if (isEdge(cap.browserName)) { // not yet implemented return Promise.resolve() } else if (isChrome(cap.browserName) || isFirefox(cap.browserName)) { return setupPuppeteerBrowser(cacheDir, cap) } return Promise.resolve() }, BrowserDriverTaskLabel.BROWSER) }