@wdio/utils
Version:
A WDIO helper utility to provide several utility functions used across the project.
498 lines (491 loc) • 20.8 kB
JavaScript
// 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
};