UNPKG

webdriver

Version:

A Node.js bindings implementation for the W3C WebDriver and Mobile JSONWire Protocol

1,275 lines (1,263 loc) 76.1 kB
// src/node.ts import os from "node:os"; import ws from "ws"; // src/index.ts import logger4 from "@wdio/logger"; import { webdriverMonad, sessionEnvironmentDetector, startWebDriver, isBidi } from "@wdio/utils"; import { validateConfig } from "@wdio/config"; // src/command.ts import logger from "@wdio/logger"; import { commandCallStructure, isValidParameter, getArgumentType } from "@wdio/utils"; import { WebDriverBidiProtocol } from "@wdio/protocols"; // src/environment.ts var isNode = !!(typeof process !== "undefined" && process.version); var environment = { value: { get Request() { throw new Error("Request is not available in this environment"); }, get Socket() { throw new Error("Socket is not available in this environment"); }, get createBidiConnection() { throw new Error("createBidiConnection is not available in this environment"); }, get variables() { return {}; } } }; // src/command.ts var log = logger("webdriver"); var BIDI_COMMANDS = Object.values(WebDriverBidiProtocol).map((def) => def.socket.command); var sessionAbortListeners = /* @__PURE__ */ new Map(); function command_default(method, endpointUri, commandInfo, doubleEncodeVariables = false) { const { command, deprecated, ref, parameters, variables = [], isHubCommand = false } = commandInfo; return async function protocolCommand(...args) { const isBidiCommand = BIDI_COMMANDS.includes(command); let endpoint = endpointUri; const commandParams = [...variables.map((v) => Object.assign(v, { /** * url variables are: */ required: true, // always required as they are part of the endpoint type: "string" // have to be always type of string })), ...parameters]; const commandUsage = `${command}(${commandParams.map((p) => p.name).join(", ")})`; const moreInfo = ` For more info see ${ref} `; const body = {}; if (typeof deprecated === "string" && !process.env.DISABLE_WEBDRIVERIO_DEPRECATION_WARNINGS) { const warning = deprecated.replace("This command", `The "${command}" command`); log.warn(warning); console.warn(`\u26A0\uFE0F [WEBDRIVERIO DEPRECATION NOTICE] ${warning}`); } if (isBidiCommand) { throw new Error( `Failed to execute WebDriver Bidi command "${command}" as no Bidi session was established. Make sure you enable it by setting "webSocketUrl: true" in your capabilities and verify that your environment and browser supports it.` ); } const minAllowedParams = commandParams.filter((param) => param.required).length; if (args.length < minAllowedParams || args.length > commandParams.length) { const parameterDescription = commandParams.length ? ` Property Description: ${commandParams.map((p) => ` "${p.name}" (${p.type}): ${p.description}`).join("\n")}` : ""; throw new Error( `Wrong parameters applied for ${command} Usage: ${commandUsage}` + parameterDescription + moreInfo ); } for (const [it, arg] of Object.entries(args)) { if (isBidiCommand) { break; } const i = parseInt(it, 10); const commandParam = commandParams[i]; if (!isValidParameter(arg, commandParam.type)) { if (typeof arg === "undefined" && !commandParam.required) { continue; } const actual = commandParam.type.endsWith("[]") ? `(${(Array.isArray(arg) ? arg : [arg]).map((a) => getArgumentType(a))})[]` : getArgumentType(arg); throw new Error( `Malformed type for "${commandParam.name}" parameter of command ${command} Expected: ${commandParam.type} Actual: ${actual}` + moreInfo ); } if (i < variables.length) { const encodedArg = doubleEncodeVariables ? encodeURIComponent(encodeURIComponent(arg)) : encodeURIComponent(arg); endpoint = endpoint.replace(`:${commandParams[i].name}`, encodedArg); continue; } body[commandParams[i].name] = arg; } const { isAborted, abortSignal, cleanup } = manageSessionAbortions.call(this); const requiresSession = endpointUri.includes("/:sessionId/"); if (isAborted && command !== "deleteSession" && requiresSession) { throw new Error(`Trying to run command "${commandCallStructure(command, args)}" after session has been deleted, aborting request without executing it`); } const request = new environment.value.Request(method, endpoint, body, abortSignal, isHubCommand, { onPerformance: (data) => this.emit("request.performance", data), onRequest: (data) => this.emit("request.start", data), onResponse: (data) => this.emit("request.end", data), onRetry: (data) => this.emit("request.retry", data) }); this.emit("command", { command, method, endpoint, body }); log.info("COMMAND", commandCallStructure(command, args)); return request.makeRequest(this.options, this.sessionId).then((result) => { if (typeof result.value !== "undefined") { let resultLog = result.value; if (/screenshot|recording/i.test(command) && typeof result.value === "string" && result.value.length > 64) { resultLog = `${result.value.slice(0, 61)}...`; } else if (command === "executeScript" && typeof body.script === "string" && body.script.includes("(() => window.__wdioEvents__)")) { resultLog = `[${result.value.length} framework events captured]`; } log.info("RESULT", resultLog); } this.emit("result", { command, method, endpoint, body, result }); if (command === "deleteSession") { const browser = this; browser._bidiHandler?.close(); const shutdownDriver = body.deleteSessionOpts?.shutdownDriver !== false; if (shutdownDriver && "wdio:driverPID" in this.capabilities && this.capabilities["wdio:driverPID"]) { log.info(`Kill driver process with PID ${this.capabilities["wdio:driverPID"]}`); try { const killedSuccessfully = process.kill(this.capabilities["wdio:driverPID"], "SIGKILL"); if (!killedSuccessfully) { log.warn("Failed to kill driver process, manually clean-up might be required"); } } catch (err) { log.warn("Failed to kill driver process", err); } setTimeout(() => { for (const handle of process._getActiveHandles()) { if (handle.servername && handle.servername.includes("edgedl.me")) { handle.destroy(); } } }, 10); } if (!process.env.WDIO_WORKER_ID) { logger.clearLogger(); } } return result.value; }).catch((error) => { this.emit("result", { command, method, endpoint, body, result: { error } }); throw error; }).finally(() => { cleanup(); }); }; } function manageSessionAbortions() { const abort = new AbortController(); const abortOnSessionEnd = (result) => { if (result.command !== "deleteSession") { return; } const abortListeners = sessionAbortListeners.get(this.sessionId); if (abortListeners) { for (const abortListener of abortListeners) { abortListener.abort(); } abortListeners.clear(); sessionAbortListeners.set(this.sessionId, null); } }; let abortListenerForCurrentSession = sessionAbortListeners.get(this.sessionId); if (typeof abortListenerForCurrentSession === "undefined") { abortListenerForCurrentSession = /* @__PURE__ */ new Set(); sessionAbortListeners.set(this.sessionId, abortListenerForCurrentSession); this.on("result", abortOnSessionEnd); } if (abortListenerForCurrentSession === null) { return { isAborted: true, abortSignal: void 0, cleanup: () => { } }; } abortListenerForCurrentSession.add(abort); return { isAborted: false, abortSignal: abort.signal, cleanup: () => { this.off("result", abortOnSessionEnd); abortListenerForCurrentSession?.delete(abort); } }; } // src/constants.ts var DEFAULTS = { /** * protocol of automation driver */ protocol: { type: "string", default: "http", match: /(http|https)/ }, /** * hostname of automation driver */ hostname: { type: "string", default: "localhost" }, /** * port of automation driver */ port: { type: "number" }, /** * path to WebDriver endpoints */ path: { type: "string", validate: (path) => { if (!path.startsWith("/")) { throw new TypeError('The option "path" needs to start with a "/"'); } return true; }, default: "/" }, /** * A key-value store of query parameters to be added to every selenium request */ queryParams: { type: "object" }, /** * cloud user if applicable */ user: { type: "string" }, /** * access key to user */ key: { type: "string" }, /** * capability of WebDriver session */ capabilities: { type: "object", required: true }, /** * Level of logging verbosity */ logLevel: { type: "string", default: "info", match: /(trace|debug|info|warn|error|silent)/ }, /** * directory for log files */ outputDir: { type: "string" }, /** * Timeout for any WebDriver request to a driver or grid */ connectionRetryTimeout: { type: "number", default: 12e4 }, /** * Count of request retries to the Selenium server */ connectionRetryCount: { type: "number", default: 3 }, /** * Override default agent */ logLevels: { type: "object" }, /** * Pass custom headers */ headers: { type: "object" }, /** * Function transforming the request options before the request is made */ transformRequest: { type: "function", default: (requestOptions) => requestOptions }, /** * Function transforming the response object after it is received */ transformResponse: { type: "function", default: (response) => response }, /** * Appium direct connect options server (https://appiumpro.com/editions/86-connecting-directly-to-appium-hosts-in-distributed-environments) * Whether to allow direct connect caps to adjust endpoint details (Appium only) */ enableDirectConnect: { type: "boolean", default: true }, /** * Whether it requires SSL certificates to be valid in HTTP/s requests * for an environment which cannot get process environment well. */ strictSSL: { type: "boolean", default: true }, /** * The path to the root of the cache directory. This directory is used to store all drivers that are downloaded * when attempting to start a session. */ cacheDir: { type: "string", default: environment.value.variables.WEBDRIVER_CACHE_DIR } }; var ELEMENT_KEY = "element-6066-11e4-a52e-4f735466cecf"; var SHADOW_ELEMENT_KEY = "shadow-6066-11e4-a52e-4f735466cecf"; // src/utils.ts import { deepmergeCustom } from "deepmerge-ts"; import logger3 from "@wdio/logger"; import { WebDriverProtocol, MJsonWProtocol, AppiumProtocol, ChromiumProtocol, SauceLabsProtocol, SeleniumProtocol, GeckoProtocol, WebDriverBidiProtocol as WebDriverBidiProtocol2 } from "@wdio/protocols"; import { CAPABILITY_KEYS } from "@wdio/protocols"; // src/bidi/core.ts import logger2 from "@wdio/logger"; var SCRIPT_PREFIX = "/* __wdio script__ */"; var SCRIPT_SUFFIX = "/* __wdio script end__ */"; var base64Regex = /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$/; var log2 = logger2("webdriver"); var RESPONSE_TIMEOUT = 1e3 * 60; var BidiCore = class { #id = 0; #ws; #waitForConnected; #resolveWaitForConnected; #webSocketUrl; #clientOptions; #pendingCommands = /* @__PURE__ */ new Map(); client; /** * @private */ _isConnected = false; constructor(webSocketUrl, opts) { this.#webSocketUrl = webSocketUrl; this.#clientOptions = opts; this.#resolveWaitForConnected = () => { }; this.#waitForConnected = new Promise((resolve) => { this.#resolveWaitForConnected = resolve; }); } /** * We initiate the Bidi instance before a WebdriverIO instance is created. * In order to emit Bidi events we have to attach the WebdriverIO instance * to the Bidi instance afterwards. */ attachClient(client) { this.client = client; } async connect() { log2.info(`Connecting to webSocketUrl ${this.#webSocketUrl}`); this.#ws = await environment.value.createBidiConnection(this.#webSocketUrl, this.#clientOptions); this._isConnected = Boolean(this.#ws); this.#resolveWaitForConnected(this._isConnected); if (this.#ws) { this.#ws.on("message", this.#handleResponse.bind(this)); } return this._isConnected; } close() { if (!this._isConnected) { return; } log2.info(`Close Bidi connection to ${this.#webSocketUrl}`); this._isConnected = false; if (this.#ws) { this.#ws.off("message", this.#handleResponse.bind(this)); this.#ws.close(); this.#ws.terminate(); this.#ws = void 0; } } reconnect(webSocketUrl, opts) { log2.info(`Reconnect to new Bidi session at ${webSocketUrl}`); this.close(); this.#webSocketUrl = webSocketUrl; this.#clientOptions = opts; return this.connect(); } /** * Helper function that allows to wait until Bidi connection establishes * @returns a promise that resolves once the connection to WebDriver Bidi protocol was established */ waitForConnected() { return this.#waitForConnected; } get socket() { return this.#ws; } get isConnected() { return this._isConnected; } /** * for testing purposes only * @internal */ get __handleResponse() { return this.#handleResponse.bind(this); } #handleResponse(data) { try { const payload = JSON.parse(data.toString()); if (!payload.id) { return; } let resultLog = data.toString(); if (typeof payload.result === "object" && payload.result && "data" in payload.result && typeof payload.result.data === "string" && base64Regex.test(payload.result.data)) { resultLog = JSON.stringify({ ...payload.result, data: `Base64 string [${payload.result.data.length} chars]` }); } log2.info("BIDI RESULT", resultLog); this.client?.emit("bidiResult", payload); const resolve = this.#pendingCommands.get(payload.id); if (!resolve) { log2.error(`Couldn't resolve command with id ${payload.id}`); return; } this.#pendingCommands.delete(payload.id); resolve(payload); } catch (err) { const error = err instanceof Error ? err : new Error(`Failed parse message: ${String(err)}`); log2.error(`Failed parse message: ${error.message}`); } } async send(params) { const id = this.sendAsync(params); const failError = new Error(`WebDriver Bidi command "${params.method}" failed`); const payload = await new Promise((resolve, reject) => { const t = setTimeout(() => { reject(new Error(`Command ${params.method} with id ${id} (with the following parameter: ${JSON.stringify(params.params)}) timed out`)); this.#pendingCommands.delete(id); }, RESPONSE_TIMEOUT); this.#pendingCommands.set(id, (payload2) => { clearTimeout(t); resolve(payload2); }); }); if (payload.type === "error" || "error" in payload) { failError.message += ` with error: ${payload.error} - ${payload.message}`; if (payload.stacktrace && typeof payload.stacktrace === "string") { const driverStack = payload.stacktrace.split("\n").filter(Boolean).map((line) => ` at ${line}`).join("\n"); failError.stack += ` Driver Stack: ${driverStack}`; } throw failError; } return payload; } sendAsync(params) { if (!this.#ws || !this._isConnected) { throw new Error("No connection to WebDriver Bidi was established"); } log2.info("BIDI COMMAND", ...parseBidiCommand(params)); const id = ++this.#id; this.client?.emit("bidiCommand", params); this.#ws.send(JSON.stringify({ id, ...params })); return id; } }; function parseBidiCommand(params) { const commandName = params.method; if (commandName === "script.addPreloadScript") { const param = params.params; const logString = `{ functionDeclaration: <PreloadScript[${new TextEncoder().encode(param.functionDeclaration).length} bytes]>, contexts: ${JSON.stringify(param.contexts)} }`; return [commandName, logString]; } else if (commandName === "script.callFunction") { const param = params.params; const fn = param.functionDeclaration; let fnName = ""; if (fn.includes(SCRIPT_PREFIX)) { const internalFn = fn.slice( fn.indexOf(SCRIPT_PREFIX) + SCRIPT_PREFIX.length, fn.indexOf(SCRIPT_SUFFIX) ); const functionPrefix = "function "; if (internalFn.startsWith(functionPrefix)) { fnName = internalFn.slice( internalFn.indexOf(functionPrefix) + functionPrefix.length, internalFn.indexOf("(") ); } } const logString = JSON.stringify({ ...param, functionDeclaration: `<Function[${new TextEncoder().encode(param.functionDeclaration).length} bytes] ${fnName || "anonymous"}>` }); return [commandName, logString]; } return [commandName, JSON.stringify(params.params)]; } // src/bidi/handler.ts var BidiHandler = class extends BidiCore { /** * WebDriver Bidi command to send command method "session.status" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-session-status * @param params `remote.EmptyParams` {@link https://w3c.github.io/webdriver-bidi/#command-session-status | command parameter} * @returns `Promise<local.SessionStatusResult>` **/ async sessionStatus(params) { const result = await this.send({ method: "session.status", params }); return result.result; } /** * WebDriver Bidi command to send command method "session.new" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-session-new * @param params `remote.SessionNewParameters` {@link https://w3c.github.io/webdriver-bidi/#command-session-new | command parameter} * @returns `Promise<local.SessionNewResult>` **/ async sessionNew(params) { const result = await this.send({ method: "session.new", params }); return result.result; } /** * WebDriver Bidi command to send command method "session.end" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-session-end * @param params `remote.EmptyParams` {@link https://w3c.github.io/webdriver-bidi/#command-session-end | command parameter} * @returns `Promise<local.EmptyResult>` **/ async sessionEnd(params) { const result = await this.send({ method: "session.end", params }); return result.result; } /** * WebDriver Bidi command to send command method "session.subscribe" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-session-subscribe * @param params `remote.SessionSubscriptionRequest` {@link https://w3c.github.io/webdriver-bidi/#command-session-subscribe | command parameter} * @returns `Promise<local.EmptyResult>` **/ async sessionSubscribe(params) { const result = await this.send({ method: "session.subscribe", params }); return result.result; } /** * WebDriver Bidi command to send command method "session.unsubscribe" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-session-unsubscribe * @param params `remote.SessionSubscriptionRequest` {@link https://w3c.github.io/webdriver-bidi/#command-session-unsubscribe | command parameter} * @returns `Promise<local.EmptyResult>` **/ async sessionUnsubscribe(params) { const result = await this.send({ method: "session.unsubscribe", params }); return result.result; } /** * WebDriver Bidi command to send command method "browser.close" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-browser-close * @param params `remote.EmptyParams` {@link https://w3c.github.io/webdriver-bidi/#command-browser-close | command parameter} * @returns `Promise<local.EmptyResult>` **/ async browserClose(params) { const result = await this.send({ method: "browser.close", params }); return result.result; } /** * WebDriver Bidi command to send command method "browser.createUserContext" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-browser-createUserContext * @param params `remote.EmptyParams` {@link https://w3c.github.io/webdriver-bidi/#command-browser-createUserContext | command parameter} * @returns `Promise<local.BrowserCreateUserContextResult>` **/ async browserCreateUserContext(params) { const result = await this.send({ method: "browser.createUserContext", params }); return result.result; } /** * WebDriver Bidi command to send command method "browser.getUserContexts" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-browser-getUserContexts * @param params `remote.EmptyParams` {@link https://w3c.github.io/webdriver-bidi/#command-browser-getUserContexts | command parameter} * @returns `Promise<local.BrowserGetUserContextsResult>` **/ async browserGetUserContexts(params) { const result = await this.send({ method: "browser.getUserContexts", params }); return result.result; } /** * WebDriver Bidi command to send command method "browser.removeUserContext" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-browser-removeUserContext * @param params `remote.BrowserRemoveUserContextParameters` {@link https://w3c.github.io/webdriver-bidi/#command-browser-removeUserContext | command parameter} * @returns `Promise<local.EmptyResult>` **/ async browserRemoveUserContext(params) { const result = await this.send({ method: "browser.removeUserContext", params }); return result.result; } /** * WebDriver Bidi command to send command method "browsingContext.activate" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-browsingContext-activate * @param params `remote.BrowsingContextActivateParameters` {@link https://w3c.github.io/webdriver-bidi/#command-browsingContext-activate | command parameter} * @returns `Promise<local.EmptyResult>` **/ async browsingContextActivate(params) { const result = await this.send({ method: "browsingContext.activate", params }); return result.result; } /** * WebDriver Bidi command to send command method "browsingContext.captureScreenshot" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-browsingContext-captureScreenshot * @param params `remote.BrowsingContextCaptureScreenshotParameters` {@link https://w3c.github.io/webdriver-bidi/#command-browsingContext-captureScreenshot | command parameter} * @returns `Promise<local.BrowsingContextCaptureScreenshotResult>` **/ async browsingContextCaptureScreenshot(params) { const result = await this.send({ method: "browsingContext.captureScreenshot", params }); return result.result; } /** * WebDriver Bidi command to send command method "browsingContext.close" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-browsingContext-close * @param params `remote.BrowsingContextCloseParameters` {@link https://w3c.github.io/webdriver-bidi/#command-browsingContext-close | command parameter} * @returns `Promise<local.EmptyResult>` **/ async browsingContextClose(params) { const result = await this.send({ method: "browsingContext.close", params }); return result.result; } /** * WebDriver Bidi command to send command method "browsingContext.create" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-browsingContext-create * @param params `remote.BrowsingContextCreateParameters` {@link https://w3c.github.io/webdriver-bidi/#command-browsingContext-create | command parameter} * @returns `Promise<local.BrowsingContextCreateResult>` **/ async browsingContextCreate(params) { const result = await this.send({ method: "browsingContext.create", params }); return result.result; } /** * WebDriver Bidi command to send command method "browsingContext.getTree" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-browsingContext-getTree * @param params `remote.BrowsingContextGetTreeParameters` {@link https://w3c.github.io/webdriver-bidi/#command-browsingContext-getTree | command parameter} * @returns `Promise<local.BrowsingContextGetTreeResult>` **/ async browsingContextGetTree(params) { const result = await this.send({ method: "browsingContext.getTree", params }); return result.result; } /** * WebDriver Bidi command to send command method "browsingContext.handleUserPrompt" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-browsingContext-handleUserPrompt * @param params `remote.BrowsingContextHandleUserPromptParameters` {@link https://w3c.github.io/webdriver-bidi/#command-browsingContext-handleUserPrompt | command parameter} * @returns `Promise<local.EmptyResult>` **/ async browsingContextHandleUserPrompt(params) { const result = await this.send({ method: "browsingContext.handleUserPrompt", params }); return result.result; } /** * WebDriver Bidi command to send command method "browsingContext.locateNodes" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-browsingContext-locateNodes * @param params `remote.BrowsingContextLocateNodesParameters` {@link https://w3c.github.io/webdriver-bidi/#command-browsingContext-locateNodes | command parameter} * @returns `Promise<local.BrowsingContextLocateNodesResult>` **/ async browsingContextLocateNodes(params) { const result = await this.send({ method: "browsingContext.locateNodes", params }); return result.result; } /** * WebDriver Bidi command to send command method "browsingContext.navigate" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-browsingContext-navigate * @param params `remote.BrowsingContextNavigateParameters` {@link https://w3c.github.io/webdriver-bidi/#command-browsingContext-navigate | command parameter} * @returns `Promise<local.BrowsingContextNavigateResult>` **/ async browsingContextNavigate(params) { const result = await this.send({ method: "browsingContext.navigate", params }); return result.result; } /** * WebDriver Bidi command to send command method "browsingContext.print" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-browsingContext-print * @param params `remote.BrowsingContextPrintParameters` {@link https://w3c.github.io/webdriver-bidi/#command-browsingContext-print | command parameter} * @returns `Promise<local.BrowsingContextPrintResult>` **/ async browsingContextPrint(params) { const result = await this.send({ method: "browsingContext.print", params }); return result.result; } /** * WebDriver Bidi command to send command method "browsingContext.reload" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-browsingContext-reload * @param params `remote.BrowsingContextReloadParameters` {@link https://w3c.github.io/webdriver-bidi/#command-browsingContext-reload | command parameter} * @returns `Promise<local.EmptyResult>` **/ async browsingContextReload(params) { const result = await this.send({ method: "browsingContext.reload", params }); return result.result; } /** * WebDriver Bidi command to send command method "browsingContext.setViewport" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-browsingContext-setViewport * @param params `remote.BrowsingContextSetViewportParameters` {@link https://w3c.github.io/webdriver-bidi/#command-browsingContext-setViewport | command parameter} * @returns `Promise<local.EmptyResult>` **/ async browsingContextSetViewport(params) { const result = await this.send({ method: "browsingContext.setViewport", params }); return result.result; } /** * WebDriver Bidi command to send command method "browsingContext.traverseHistory" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-browsingContext-traverseHistory * @param params `remote.BrowsingContextTraverseHistoryParameters` {@link https://w3c.github.io/webdriver-bidi/#command-browsingContext-traverseHistory | command parameter} * @returns `Promise<local.BrowsingContextTraverseHistoryResult>` **/ async browsingContextTraverseHistory(params) { const result = await this.send({ method: "browsingContext.traverseHistory", params }); return result.result; } /** * WebDriver Bidi command to send command method "network.addIntercept" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-network-addIntercept * @param params `remote.NetworkAddInterceptParameters` {@link https://w3c.github.io/webdriver-bidi/#command-network-addIntercept | command parameter} * @returns `Promise<local.NetworkAddInterceptResult>` **/ async networkAddIntercept(params) { const result = await this.send({ method: "network.addIntercept", params }); return result.result; } /** * WebDriver Bidi command to send command method "network.continueRequest" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-network-continueRequest * @param params `remote.NetworkContinueRequestParameters` {@link https://w3c.github.io/webdriver-bidi/#command-network-continueRequest | command parameter} * @returns `Promise<local.EmptyResult>` **/ async networkContinueRequest(params) { const result = await this.send({ method: "network.continueRequest", params }); return result.result; } /** * WebDriver Bidi command to send command method "network.continueResponse" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-network-continueResponse * @param params `remote.NetworkContinueResponseParameters` {@link https://w3c.github.io/webdriver-bidi/#command-network-continueResponse | command parameter} * @returns `Promise<local.EmptyResult>` **/ async networkContinueResponse(params) { const result = await this.send({ method: "network.continueResponse", params }); return result.result; } /** * WebDriver Bidi command to send command method "network.continueWithAuth" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-network-continueWithAuth * @param params `remote.NetworkContinueWithAuthParameters` {@link https://w3c.github.io/webdriver-bidi/#command-network-continueWithAuth | command parameter} * @returns `Promise<local.EmptyResult>` **/ async networkContinueWithAuth(params) { const result = await this.send({ method: "network.continueWithAuth", params }); return result.result; } /** * WebDriver Bidi command to send command method "network.failRequest" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-network-failRequest * @param params `remote.NetworkFailRequestParameters` {@link https://w3c.github.io/webdriver-bidi/#command-network-failRequest | command parameter} * @returns `Promise<local.EmptyResult>` **/ async networkFailRequest(params) { const result = await this.send({ method: "network.failRequest", params }); return result.result; } /** * WebDriver Bidi command to send command method "network.provideResponse" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-network-provideResponse * @param params `remote.NetworkProvideResponseParameters` {@link https://w3c.github.io/webdriver-bidi/#command-network-provideResponse | command parameter} * @returns `Promise<local.EmptyResult>` **/ async networkProvideResponse(params) { const result = await this.send({ method: "network.provideResponse", params }); return result.result; } /** * WebDriver Bidi command to send command method "network.removeIntercept" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-network-removeIntercept * @param params `remote.NetworkRemoveInterceptParameters` {@link https://w3c.github.io/webdriver-bidi/#command-network-removeIntercept | command parameter} * @returns `Promise<local.EmptyResult>` **/ async networkRemoveIntercept(params) { const result = await this.send({ method: "network.removeIntercept", params }); return result.result; } /** * WebDriver Bidi command to send command method "script.addPreloadScript" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-script-addPreloadScript * @param params `remote.ScriptAddPreloadScriptParameters` {@link https://w3c.github.io/webdriver-bidi/#command-script-addPreloadScript | command parameter} * @returns `Promise<local.ScriptAddPreloadScriptResult>` **/ async scriptAddPreloadScript(params) { const result = await this.send({ method: "script.addPreloadScript", params }); return result.result; } /** * WebDriver Bidi command to send command method "script.disown" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-script-disown * @param params `remote.ScriptDisownParameters` {@link https://w3c.github.io/webdriver-bidi/#command-script-disown | command parameter} * @returns `Promise<local.EmptyResult>` **/ async scriptDisown(params) { const result = await this.send({ method: "script.disown", params }); return result.result; } /** * WebDriver Bidi command to send command method "script.callFunction" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-script-callFunction * @param params `remote.ScriptCallFunctionParameters` {@link https://w3c.github.io/webdriver-bidi/#command-script-callFunction | command parameter} * @returns `Promise<local.EmptyResult>` **/ async scriptCallFunction(params) { const result = await this.send({ method: "script.callFunction", params }); return result.result; } /** * WebDriver Bidi command to send command method "script.evaluate" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-script-evaluate * @param params `remote.ScriptEvaluateParameters` {@link https://w3c.github.io/webdriver-bidi/#command-script-evaluate | command parameter} * @returns `Promise<local.ScriptEvaluateResult>` **/ async scriptEvaluate(params) { const result = await this.send({ method: "script.evaluate", params }); return result.result; } /** * WebDriver Bidi command to send command method "script.getRealms" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-script-getRealms * @param params `remote.ScriptGetRealmsParameters` {@link https://w3c.github.io/webdriver-bidi/#command-script-getRealms | command parameter} * @returns `Promise<local.ScriptGetRealmsResult>` **/ async scriptGetRealms(params) { const result = await this.send({ method: "script.getRealms", params }); return result.result; } /** * WebDriver Bidi command to send command method "script.removePreloadScript" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-script-removePreloadScript * @param params `remote.ScriptRemovePreloadScriptParameters` {@link https://w3c.github.io/webdriver-bidi/#command-script-removePreloadScript | command parameter} * @returns `Promise<local.EmptyResult>` **/ async scriptRemovePreloadScript(params) { const result = await this.send({ method: "script.removePreloadScript", params }); return result.result; } /** * WebDriver Bidi command to send command method "storage.getCookies" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-storage-getCookies * @param params `remote.StorageGetCookiesParameters` {@link https://w3c.github.io/webdriver-bidi/#command-storage-getCookies | command parameter} * @returns `Promise<local.StorageGetCookiesResult>` **/ async storageGetCookies(params) { const result = await this.send({ method: "storage.getCookies", params }); return result.result; } /** * WebDriver Bidi command to send command method "storage.setCookie" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-storage-setCookie * @param params `remote.StorageSetCookieParameters` {@link https://w3c.github.io/webdriver-bidi/#command-storage-setCookie | command parameter} * @returns `Promise<local.StorageSetCookieResult>` **/ async storageSetCookie(params) { const result = await this.send({ method: "storage.setCookie", params }); return result.result; } /** * WebDriver Bidi command to send command method "storage.deleteCookies" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-storage-deleteCookies * @param params `remote.StorageDeleteCookiesParameters` {@link https://w3c.github.io/webdriver-bidi/#command-storage-deleteCookies | command parameter} * @returns `Promise<local.StorageDeleteCookiesResult>` **/ async storageDeleteCookies(params) { const result = await this.send({ method: "storage.deleteCookies", params }); return result.result; } /** * WebDriver Bidi command to send command method "input.performActions" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-input-performActions * @param params `remote.InputPerformActionsParameters` {@link https://w3c.github.io/webdriver-bidi/#command-input-performActions | command parameter} * @returns `Promise<local.EmptyResult>` **/ async inputPerformActions(params) { const result = await this.send({ method: "input.performActions", params }); return result.result; } /** * WebDriver Bidi command to send command method "input.releaseActions" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-input-releaseActions * @param params `remote.InputReleaseActionsParameters` {@link https://w3c.github.io/webdriver-bidi/#command-input-releaseActions | command parameter} * @returns `Promise<local.EmptyResult>` **/ async inputReleaseActions(params) { const result = await this.send({ method: "input.releaseActions", params }); return result.result; } /** * WebDriver Bidi command to send command method "input.setFiles" with parameters. * @url https://w3c.github.io/webdriver-bidi/#command-input-setFiles * @param params `remote.InputSetFilesParameters` {@link https://w3c.github.io/webdriver-bidi/#command-input-setFiles | command parameter} * @returns `Promise<local.EmptyResult>` **/ async inputSetFiles(params) { const result = await this.send({ method: "input.setFiles", params }); return result.result; } }; // src/utils.ts var log3 = logger3("webdriver"); var deepmerge = deepmergeCustom({ mergeArrays: false }); var BROWSER_DRIVER_ERRORS = [ "unknown command: wd/hub/session", // chromedriver "HTTP method not allowed", // geckodriver "'POST /wd/hub/session' was not found.", // safaridriver "Command not found" // iedriver ]; async function startWebDriverSession(params) { const capabilities = params.capabilities && "alwaysMatch" in params.capabilities ? params.capabilities : { alwaysMatch: params.capabilities, firstMatch: [{}] }; if ( /** * except, if user does not want to opt-in */ !capabilities.alwaysMatch["wdio:enforceWebDriverClassic"] && /** * or user requests a Safari session which does not support Bidi */ typeof capabilities.alwaysMatch.browserName === "string" && capabilities.alwaysMatch.browserName.toLowerCase() !== "safari" ) { capabilities.alwaysMatch.webSocketUrl = true; capabilities.alwaysMatch.unhandledPromptBehavior = "ignore"; } validateCapabilities(capabilities.alwaysMatch); const sessionRequest = new environment.value.Request( "POST", "/session", { capabilities } ); let response; try { response = await sessionRequest.makeRequest(params); } catch (err) { log3.error(err); const message = getSessionError(err, params); throw new Error(message); } const sessionId = response.value.sessionId || response.sessionId; params.capabilities = response.value.capabilities || response.value; return { sessionId, capabilities: params.capabilities }; } function validateCapabilities(capabilities) { const chromeArgs = capabilities["goog:chromeOptions"]?.args || []; if (chromeArgs.includes("incognito") || chromeArgs.includes("--incognito")) { throw new Error( 'Please remove "incognito" from `"goog:chromeOptions".args` as it is not supported running Chrome with WebDriver. WebDriver sessions are always incognito mode and do not persist across browser sessions.' ); } if (capabilities) { const extensionCaps = Object.keys(capabilities).filter((cap) => cap.includes(":")); const invalidWebDriverCaps = Object.keys(capabilities).filter((cap) => !CAPABILITY_KEYS.includes(cap) && !cap.includes(":")); if (extensionCaps.length && invalidWebDriverCaps.length) { throw new Error( `Invalid or unsupported WebDriver capabilities found ("${invalidWebDriverCaps.join('", "')}"). Ensure to only use valid W3C WebDriver capabilities (see https://w3c.github.io/webdriver/#capabilities).If you run your tests on a remote vendor, like Sauce Labs or BrowserStack, make sure that you put them into vendor specific capabilities, e.g. "sauce:options" or "bstack:options". Please reach out to your vendor support team if you have further questions.` ); } } } function isSuccessfulResponse(statusCode, body) { if (!body || typeof body !== "object" || !("value" in body) || typeof body.value === "undefined") { log3.debug("request failed due to missing body"); return false; } if ("status" in body && body.status === 7 && body.value && typeof body.value === "object" && "message" in body.value && body.value.message && typeof body.value.message === "string" && (body.value.message.toLowerCase().startsWith("no such element") || // Appium body.value.message === "An element could not be located on the page using the given search parameters." || // Internet Explorer body.value.message.toLowerCase().startsWith("unable to find element"))) { return true; } if ("status" in body && body.status && body.status !== 0) { log3.debug(`request failed due to status ${body.status}`); return false; } const hasErrorResponse = body.value && (typeof body.value === "object" && "error" in body.value && body.value.error || typeof body.value === "object" && "stackTrace" in body.value && body.value.stackTrace || typeof body.value === "object" && "stacktrace" in body.value && body.value.stacktrace); if (statusCode === 200 && !hasErrorResponse) { return true; } if (statusCode === 404 && typeof body.value === "object" && body.value && "error" in body.value && body.value.error === "no such element") { return true; } if (hasErrorResponse) { const errMsg = typeof body.value === "object" && body.value && "error" in body.value ? body.value.error : body.value; log3.debug("request failed due to response error:", errMsg); return false; } return true; } function getPrototype({ isW3C, isChromium, isFirefox, isMobile, isSauce, isSeleniumStandalone }) { const prototype = {}; const ProtocolCommands = deepmerge( /** * if mobile apply JSONWire and WebDriver protocol because * some legacy JSONWire commands are still used in Appium * (e.g. set/get geolocation) */ isMobile ? deepmerge(AppiumProtocol, WebDriverProtocol) : WebDriverProtocol, /** * enable Bidi protocol for W3C sessions */ isW3C ? WebDriverBidiProtocol2 : {}, /** * only apply mobile protocol if session is actually for mobile */ // eslint-disable-next-line @typescript-eslint/no-explicit-any isMobile ? deepmerge(MJsonWProtocol, AppiumProtocol) : {}, /** * only apply special Chromium commands if session is using Chrome or Edge */ isChromium ? ChromiumProtocol : {}, /** * only apply special Firefox commands if session is using Firefox */ isFirefox ? GeckoProtocol : {}, /** * only Sauce Labs specific vendor commands */ isSauce ? SauceLabsProtocol : {}, /** * only apply special commands when running tests using * Selenium Grid or Selenium Standalone server */ isSeleniumStandalone ? SeleniumProtocol : {}, {} ); for (const [endpoint, methods] of Object.entries(ProtocolCommands)) { for (const [method, commandData] of Object.entries(methods)) { prototype[commandData.command] = { value: command_default(method, endpoint, commandData, isSeleniumStandalone) }; } } return prototype; } function getEnvironmentVars({ isW3C, isMobile, isIOS, isAndroid, isFirefox, isSauce, isSeleniumStandalone, isChromium, isWindowsApp, isMacApp }) { return { isW3C: { value: isW3C }, isMobile: { value: isMobile }, isIOS: { value: isIOS }, isAndroid: { value: isAndroid }, isFirefox: { value: isFirefox }, isSauce: { value: isSauce }, isSeleniumStandalone: { value: isSeleniumStandalone }, isBidi: { /** * Return the value of this flag dynamically based on whether the * BidiHandler was able to connect to the `webSocketUrl` url provided * by the session response. */ get: function() { return Boolean(this._bidiHandler?.isConnected); } }, isChromium: { value: isChromium }, isWindowsApp: { value: isWindowsApp }, isMacApp: { value: isMacApp } }; } function setupDirectConnect(client) { const capabilities = client.capabilities; const directConnectProtocol = capabilities["appium:directConnectProtocol"]; const directConnectHost = capabilities["appium:directConnectHost"]; const directConnectPath = capabilities["appium:directConnectPath"]; const directConnectPort = capabilities["appium:directConnectPort"]; if (directConnectProtocol && directConnectHost && directConnectPort && (directConnectPath || directConnectPath === "")) { log3.info(`Found direct connect information in new session response. Will connect to server at ${directConnectProtocol}://${directConnectHost}:${directConnectPort}${directConnectPath}`); client.options.protocol = directConnectProtocol; client.options.hostname = directConnectHost; client.options.port = directConnectPort; client.options.path = directConnectPath; } } var getSessionError = (err, params = {}) => { if (err.code === "ECONNREFUSED") { return `Unable to connect to "${params.protocol}://${params.hostname}:${params.port}${params.path}", make sure browser driver is running on that address. It seems like the service failed to start or is rejecting any connections.`; } if (err.message === "unhandled request") { return `The browser driver couldn't start the session. Make sure you have set the "path" correctly!`; } if (!err.message) { return "See wdio.* logs for more information."; } if (err.message.includes("Whoops! The URL specified routes to this help page.")) { return "It seems you are running a Selenium Standalone server and point to a wrong path. Please set `path: '/wd/hub'` in your wdio.conf.js!"; } if (BROWSER_DRIVER_ERRORS.some((m) => err && err.message && err.message.includes(m))) { return "Make sure to set `path: '/'` in your wdio.conf.js!"; } if (err.message.includes("Bad Request - Invalid Hostname") && err.message.includes("HTTP Error 400")) { return "Run edge driver on 127.0.0.1 instead of localhost, ex: --host=127.0.0.1, or set `hostname: 'localhost'` in your wdio.conf.js"; } const w3cCapMessage = '\nMake sure to add vendor prefix like "goog:", "appium:", "moz:", etc to non W3C capabilities.\nSee more https://www.w3.org/TR/webdriver/#capabilities'; if (err.message.includes("Illegal key values seen in w3c capabilities")) { return err.message + w3cCapMessage; } if (err.message === "Response has empty body") { return "Make sure to connect to valid hostname:port or the port is not in use.\nIf you use a grid server " + w3cCapMessage; } if (err.message.includes("failed serving request POST /wd/hub/session: Unauthorized") && params.hostname?.endsWith("saucelabs.com")) { return "Session request was not authorized because you either did provide a wrong access key or tried to run in a region that has not been enabled for your user. If have registered a free trial account it is connected to a specific region. Ensure this region is set in your configuration (https://webdriver.io/docs/options.html#region)."; } return err.message; }; function initiateBidi(socketUrl, strictSSL = true, userHeaders) { const isUnitTesting = process.env.WDIO_UNIT_TESTS; if (isUnitTesting) { log3.info("Skip connecting to WebDriver Bidi interface due to unit tests"); return { _bidiHandler: { value: { isConnected: true, waitForConnec