UNPKG

@wdio/utils

Version:

A WDIO helper utility to provide several utility functions used across the project.

498 lines (491 loc) 20.8 kB
// src/node/startWebDriver.ts import fs3 from "node:fs"; import path3 from "node:path"; import cp2 from "node:child_process"; import getPort from "get-port"; import waitPort from "wait-port"; import logger2 from "@wdio/logger"; import split2 from "split2"; import { deepmerge } from "deepmerge-ts"; import { start as startSafaridriver } from "safaridriver"; import { start as startGeckodriver } from "geckodriver"; import { start as startEdgedriver, findEdgePath } from "edgedriver"; // src/node/utils.ts 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 decamelize from "decamelize"; import logger from "@wdio/logger"; import { install, canDownload, resolveBuildId, detectBrowserPlatform, Browser, ChromeReleaseChannel, computeExecutablePath } from "@puppeteer/browsers"; import { download as downloadGeckodriver } from "geckodriver"; import { download as downloadEdgedriver } from "edgedriver"; import { locateChrome, locateFirefox, locateApp } from "locate-app"; var log = logger("webdriver"); var EXCLUDED_PARAMS = ["version", "help"]; var canAccess = (file) => { if (!file) { return false; } try { fs.accessSync(file); return true; } catch { return false; } }; function parseParams(params) { 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); } function getBuildIdByChromePath(chromePath) { if (!chromePath) { return; } 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)); const oldest = versions.sort((a, b) => a > b ? -1 : 1)[0]; return oldest; } const result = cp.spawnSync(chromePath, ["--version", "--no-sandbox"], { encoding: "utf8", env: process.env }); if (result.error) { throw result.error; } const versionSanitized = result.stdout.trim().split(" ").find((s) => s.split(".").length === 4); if (!versionSanitized) { throw new Error(`Couldn't find valid Chrome version from "${result.stdout}", please raise an issue in the WebdriverIO project (https://github.com/webdriverio/webdriverio/issues/new/choose)`); } return versionSanitized; } async function getBuildIdByFirefoxPath(firefoxPath) { if (!firefoxPath) { return; } if (os.platform() === "win32") { const appPath = path.dirname(firefoxPath); const contents = (await fsp.readFile(path.join(appPath, "application.ini"))).toString("utf-8"); return contents.split("\n").filter((line) => line.startsWith("Version=")).map((line) => line.replace(/Version=/g, "").replace(/\r/g, "")).pop(); } const result = cp.spawnSync(firefoxPath, ["--version"], { encoding: "utf8", env: process.env }); if (result.error) { throw result.error; } return result.stdout.trim().split(" ").pop()?.trim(); } var lastTimeCalled = Date.now(); var downloadProgressCallback = (artifact, downloadedBytes, totalBytes) => { if (Date.now() - lastTimeCalled < 1e3) { return; } const percentage = (downloadedBytes / totalBytes * 100).toFixed(2); log.progress(`Downloading ${artifact} ${percentage}%`); lastTimeCalled = Date.now(); }; var _install = async (args, retry = false) => { await install(args).catch((err) => { const error = `Failed downloading ${args.browser} v${args.buildId} using ${JSON.stringify(args)}: ${err.message}, retrying ...`; if (retry) { err.message += "\n" + error.replace(", retrying ...", ""); throw new Error(err); } log.error(error); return _install(args, true); }); log.progress(""); }; function locateChromeSafely() { return locateChrome().catch(() => void 0); } async function setupPuppeteerBrowser(cacheDir, caps) { caps.browserName = caps.browserName?.toLowerCase(); const browserName = caps.browserName === Browser.FIREFOX ? Browser.FIREFOX : caps.browserName === Browser.CHROMIUM ? Browser.CHROMIUM : Browser.CHROME; const exist = await fsp.access(cacheDir).then(() => true, () => false); const isChromeOrChromium = browserName === Browser.CHROME || caps.browserName === Browser.CHROMIUM; if (!exist) { await fsp.mkdir(cacheDir, { recursive: true }); } if (browserName === Browser.CHROMIUM) { caps.browserName = Browser.CHROME; } const browserOptions = (isChromeOrChromium ? caps["goog:chromeOptions"] : caps["moz:firefoxOptions"]) || {}; if (typeof browserOptions.binary === "string") { return { executablePath: browserOptions.binary, browserVersion: caps.browserVersion || (isChromeOrChromium ? getBuildIdByChromePath(browserOptions.binary) : await getBuildIdByFirefoxPath(browserOptions.binary)) }; } const platform = detectBrowserPlatform(); if (!platform) { throw new Error("The current platform is not supported."); } if (!caps.browserVersion) { const executablePath2 = browserName === Browser.CHROME ? await locateChromeSafely() : browserName === Browser.CHROMIUM ? await locateApp({ appName: Browser.CHROMIUM, macOsName: Browser.CHROMIUM, linuxWhich: "chromium-browser" }).catch(() => void 0) : await locateFirefox().catch(() => void 0); const browserVersion2 = isChromeOrChromium ? getBuildIdByChromePath(executablePath2) : await getBuildIdByFirefoxPath(executablePath2); if (browserVersion2) { return { executablePath: executablePath2, browserVersion: browserVersion2 }; } } const tag = browserName === Browser.CHROME ? caps.browserVersion || ChromeReleaseChannel.STABLE : caps.browserVersion || "latest"; const buildId = await resolveBuildId(browserName, platform, tag); const installOptions = { unpack: true, cacheDir, platform, buildId, browser: browserName, downloadProgressCallback: (downloadedBytes, totalBytes) => downloadProgressCallback(`${browserName} (${buildId})`, downloadedBytes, totalBytes) }; const isCombinationAvailable = await canDownload(installOptions); if (!isCombinationAvailable) { throw new Error(`Couldn't find a matching ${browserName} browser for tag "${buildId}" on platform "${platform}"`); } log.info(`Setting up ${browserName} v${buildId}`); await _install(installOptions); const executablePath = computeExecutablePath(installOptions); let browserVersion = buildId; if (browserName === Browser.CHROMIUM) { browserVersion = await resolveBuildId(Browser.CHROME, platform, tag); } return { executablePath, browserVersion }; } function getDriverOptions(caps) { return caps["wdio:chromedriverOptions"] || caps["wdio:geckodriverOptions"] || caps["wdio:edgedriverOptions"] || // Safaridriver does not have any options as it already // is installed on macOS {}; } function getCacheDir(options, caps) { const driverOptions = getDriverOptions(caps); return driverOptions.cacheDir || options.cacheDir || os.tmpdir(); } function getMajorVersionFromString(fullVersion) { let prefix; if (fullVersion) { prefix = fullVersion.match(/^[+-]?([0-9]+)/); } return prefix && prefix.length > 0 ? prefix[0] : ""; } async function setupChromedriver(cacheDir, driverVersion) { const platform = detectBrowserPlatform(); if (!platform) { throw new Error("The current platform is not supported."); } const version = driverVersion || getBuildIdByChromePath(await locateChromeSafely()) || ChromeReleaseChannel.STABLE; const buildId = await resolveBuildId(Browser.CHROMEDRIVER, platform, version); let executablePath = computeExecutablePath({ browser: Browser.CHROMEDRIVER, buildId, platform, cacheDir }); const hasChromedriverInstalled = await fsp.access(executablePath).then(() => true, () => false); if (!hasChromedriverInstalled) { log.info(`Downloading Chromedriver v${buildId}`); const chromedriverInstallOpts = { cacheDir, buildId, platform, browser: Browser.CHROMEDRIVER, unpack: true, downloadProgressCallback: (downloadedBytes, totalBytes) => downloadProgressCallback("Chromedriver", downloadedBytes, totalBytes) }; let knownBuild = buildId; if (await canDownload(chromedriverInstallOpts)) { await _install({ ...chromedriverInstallOpts, buildId }); log.info(`Download of Chromedriver v${buildId} was successful`); } else { log.warn(`Chromedriver v${buildId} don't exist, trying to find known good version...`); knownBuild = await resolveBuildId(Browser.CHROMEDRIVER, platform, getMajorVersionFromString(version)); if (knownBuild) { await _install({ ...chromedriverInstallOpts, buildId: knownBuild }); log.info(`Download of Chromedriver v${knownBuild} was successful`); } else { throw new Error(`Couldn't download any known good version from Chromedriver major v${getMajorVersionFromString(version)}, requested full version - v${version}`); } } executablePath = computeExecutablePath({ browser: Browser.CHROMEDRIVER, buildId: knownBuild, platform, cacheDir }); } else { log.info(`Using Chromedriver v${buildId} from cache directory ${cacheDir}`); } return { executablePath }; } function setupGeckodriver(cacheDir, driverVersion) { return downloadGeckodriver(driverVersion, cacheDir); } function setupEdgedriver(cacheDir, driverVersion) { return downloadEdgedriver(driverVersion, cacheDir); } function generateDefaultPrefs(caps) { return caps["goog:chromeOptions"]?.debuggerAddress ? {} : { prefs: { "profile.password_manager_leak_detection": false } }; } // src/utils.ts import fs2 from "node:fs/promises"; import url from "node:url"; import path2 from "node:path"; // src/constants.ts var SUPPORTED_BROWSERNAMES = { chrome: ["chrome", "googlechrome", "chromium", "chromium-browser"], firefox: ["firefox", "ff", "mozilla", "mozilla firefox"], edge: ["edge", "microsoftedge", "msedge"], safari: ["safari", "safari technology preview"] }; var DEFAULT_HOSTNAME = "localhost"; var DEFAULT_PROTOCOL = "http"; var DEFAULT_PATH = "/"; // src/utils.ts function isAppiumCapability(caps) { return Boolean( caps && // @ts-expect-error outdated jsonwp cap (caps.automationName || caps["appium:automationName"] || "appium:options" in caps && caps["appium:options"]?.automationName || // @ts-expect-error outdated jsonwp cap caps.deviceName || caps["appium:deviceName"] || "appium:options" in caps && caps["appium:options"]?.deviceName || "lt:options" in caps && caps["lt:options"]?.deviceName || // @ts-expect-error outdated jsonwp cap caps.appiumVersion || caps["appium:appiumVersion"] || "appium:options" in caps && caps["appium:options"]?.appiumVersion || "lt:options" in caps && caps["lt:options"]?.appiumVersion) ); } function definesRemoteDriver(options) { return Boolean( options.protocol && options.protocol !== DEFAULT_PROTOCOL || options.hostname && options.hostname !== DEFAULT_HOSTNAME || Boolean(options.port) || options.path && options.path !== DEFAULT_PATH || Boolean(options.user && options.key) ); } function isChrome(browserName) { return Boolean(browserName && SUPPORTED_BROWSERNAMES.chrome.includes(browserName.toLowerCase())); } function isSafari(browserName) { return Boolean(browserName && SUPPORTED_BROWSERNAMES.safari.includes(browserName.toLowerCase())); } function isFirefox(browserName) { return Boolean(browserName && SUPPORTED_BROWSERNAMES.firefox.includes(browserName.toLowerCase())); } function isEdge(browserName) { return Boolean(browserName && SUPPORTED_BROWSERNAMES.edge.includes(browserName.toLowerCase())); } // src/node/startWebDriver.ts var log2 = logger2("@wdio/utils"); var DRIVER_WAIT_TIMEOUT = 10 * 1e3; var DRIVER_RETRY_INTERVAL = 100; async function startWebDriver(options) { if (process.env.WDIO_SKIP_DRIVER_SETUP) { options.hostname = "localhost"; options.port = 4321; return; } let driverProcess; let driver = ""; const start = Date.now(); const caps = options.capabilities.alwaysMatch || options.capabilities; if (isAppiumCapability(caps)) { return; } if (!caps.browserName) { throw new Error( `No "browserName" defined in capabilities nor hostname or port found! 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)) { const chromedriverOptions = caps["wdio:chromedriverOptions"] || {}; 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 }); driverProcess = cp2.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"] || {}; driver = "SafariDriver"; driverProcess = startSafaridriver({ useTechnologyPreview: /preview/i.test(caps.browserName), ...safaridriverOptions, port }); } else if (isFirefox(caps.browserName)) { const { executablePath } = await setupPuppeteerBrowser(cacheDir, caps); caps["moz:firefoxOptions"] = deepmerge( { binary: executablePath }, caps["moz:firefoxOptions"] || {} ); delete caps.browserVersion; const { binary, ...geckodriverOptions } = caps["wdio:geckodriverOptions"] || {}; if (binary) { geckodriverOptions.customGeckoDriverPath = binary; } driver = "GeckoDriver"; driverProcess = await startGeckodriver({ ...geckodriverOptions, cacheDir, port, allowHosts: ["localhost"] }); } else if (isEdge(caps.browserName)) { const { binary, ...edgedriverOptions } = caps["wdio:edgedriverOptions"] || {}; if (binary) { edgedriverOptions.customEdgeDriverPath = binary; } driver = "EdgeDriver"; driverProcess = await startEdgedriver({ ...edgedriverOptions, cacheDir, port, allowedIps: ["0.0.0.0"] }).catch((err) => { log2.warn(`Couldn't start EdgeDriver: ${err.message}, retry ...`); return startEdgedriver({ ...edgedriverOptions, cacheDir, port }); }); caps.browserName = "MicrosoftEdge"; if (!caps["ms:edgeOptions"]?.binary) { caps["ms:edgeOptions"] = caps["ms:edgeOptions"] || {}; caps["ms:edgeOptions"].binary = findEdgePath(); log2.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 = path3.resolve(options.outputDir, logFileName); const logStream = fs3.createWriteStream(logFile, { flags: "w" }); driverProcess.stdout?.pipe(logStream); driverProcess.stderr?.pipe(logStream); } else { const driverLog = logger2(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; log2.info(`Started ${driver} in ${Date.now() - start}ms on port ${port}`); return driverProcess; } // src/node/manager.ts import logger3 from "@wdio/logger"; var log3 = logger3("@wdio/utils"); var UNDEFINED_BROWSER_VERSION = null; var firefoxChannels = ["stable", "latest"]; function mapCapabilities(options, caps, task, taskItemLabel) { const capabilitiesToRequireSetup = (Array.isArray(caps) ? caps.map((cap) => { const w3cCaps = cap; const multiremoteCaps = cap; const multiremoteInstanceNames = Object.keys(multiremoteCaps); if (typeof multiremoteCaps[multiremoteInstanceNames[0]] === "object" && "capabilities" in multiremoteCaps[multiremoteInstanceNames[0]]) { return Object.values(multiremoteCaps).map((c) => "alwaysMatch" in c.capabilities ? c.capabilities.alwaysMatch : c.capabilities); } if (w3cCaps.alwaysMatch) { return w3cCaps.alwaysMatch; } return cap; }).flat() : Object.values(caps).map((mrOpts) => { const w3cCaps = mrOpts.capabilities; if (w3cCaps.alwaysMatch) { return w3cCaps.alwaysMatch; } return mrOpts.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)) )); if (capabilitiesToRequireSetup.length === 0) { return; } const queueByBrowserName = capabilitiesToRequireSetup.reduce((queue, cap) => { if (!cap.browserName) { return queue; } if (!queue.has(cap.browserName)) { queue.set(cap.browserName, /* @__PURE__ */ new Map()); } const browserVersion = cap.browserVersion || UNDEFINED_BROWSER_VERSION; queue.get(cap.browserName).set(browserVersion, cap); return queue; }, /* @__PURE__ */ new Map()); const driverToSetupString = Array.from(queueByBrowserName.entries()).map(([browserName, versions]) => `${browserName}@${Array.from(versions.keys()).map((bv) => bv || "stable").join(", ")}`).join(" - "); log3.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() ); } async function setupDriver(options, caps) { return mapCapabilities(options, caps, async (cap) => { const cacheDir = getCacheDir(options, cap); if (isEdge(cap.browserName)) { return setupEdgedriver(cacheDir, cap.browserVersion); } else if (isFirefox(cap.browserName)) { const version = firefoxChannels.includes(cap.browserVersion ?? "") ? void 0 : cap.browserVersion; return setupGeckodriver(cacheDir, version); } else if (isChrome(cap.browserName)) { return setupChromedriver(cacheDir, cap.browserVersion); } return Promise.resolve(); }, "browser driver" /* DRIVER */); } function setupBrowser(options, caps) { return mapCapabilities(options, caps, async (cap) => { const cacheDir = getCacheDir(options, cap); if (isEdge(cap.browserName)) { return Promise.resolve(); } else if (isChrome(cap.browserName) || isFirefox(cap.browserName)) { return setupPuppeteerBrowser(cacheDir, cap); } return Promise.resolve(); }, "browser binaries" /* BROWSER */); } export { canAccess, setupBrowser, setupDriver, startWebDriver };