Implements Phases 1-8 of the TFTSR implementation plan. Rust backend (Tauri 2.x, src-tauri/): - Multi-provider AI: OpenAI-compatible, Anthropic, Gemini, Mistral, Ollama - PII detection engine: 11 regex patterns with overlap resolution - SQLCipher AES-256 encrypted database with 10 versioned migrations - 28 Tauri IPC commands for triage, analysis, document, and system ops - Ollama: hardware probe, model recommendations, pull/delete with events - RCA and blameless post-mortem Markdown document generators - PDF export via printpdf - Audit log: SHA-256 hash of every external data send - Integration stubs for Confluence, ServiceNow, Azure DevOps (v0.2) Frontend (React 18 + TypeScript + Vite, src/): - 9 pages: full triage workflow NewIssue→LogUpload→Triage→Resolution→RCA→Postmortem→History+Settings - 7 components: ChatWindow, TriageProgress, PiiDiffViewer, DocEditor, HardwareReport, ModelSelector, UI primitives - 3 Zustand stores: session, settings (persisted), history - Type-safe tauriCommands.ts matching Rust backend types exactly - 8 IT domain system prompts (Linux, Windows, Network, K8s, DB, Virt, HW, Obs) DevOps: - .woodpecker/test.yml: rustfmt, clippy, cargo test, tsc, vitest on every push - .woodpecker/release.yml: linux/amd64 + linux/arm64 builds, Gogs release upload Verified: - cargo check: zero errors - tsc --noEmit: zero errors - vitest run: 13/13 unit tests passing Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2266 lines
85 KiB
JavaScript
2266 lines
85 KiB
JavaScript
// 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 logger3 from "@wdio/logger";
|
|
import { commandCallStructure, isValidParameter, getArgumentType, transformCommandLogResult } from "@wdio/utils";
|
|
import { WebDriverBidiProtocol as WebDriverBidiProtocol2 } 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 killDriverProcess() {
|
|
throw new Error("killDriverProcess is not available in this environment");
|
|
},
|
|
get variables() {
|
|
return {};
|
|
}
|
|
}
|
|
};
|
|
|
|
// src/types.ts
|
|
var CommandRuntimeOptions = class {
|
|
// mask the text parameter value of the command
|
|
mask;
|
|
constructor(options) {
|
|
this.mask = options.mask;
|
|
}
|
|
};
|
|
|
|
// src/utils.ts
|
|
import { deepmergeCustom } from "deepmerge-ts";
|
|
import logger2, { SENSITIVE_DATA_REPLACER } from "@wdio/logger";
|
|
import {
|
|
WebDriverProtocol,
|
|
MJsonWProtocol,
|
|
AppiumProtocol,
|
|
ChromiumProtocol,
|
|
SauceLabsProtocol,
|
|
SeleniumProtocol,
|
|
GeckoProtocol,
|
|
WebDriverBidiProtocol
|
|
} from "@wdio/protocols";
|
|
import { CAPABILITY_KEYS } from "@wdio/protocols";
|
|
|
|
// src/bidi/core.ts
|
|
import logger from "@wdio/logger";
|
|
|
|
// 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
|
|
},
|
|
/**
|
|
* Mask sensitive data in logs by replacing matching string or all captured groups for the provided regular expressions as string
|
|
*/
|
|
maskingPatterns: {
|
|
type: "string",
|
|
default: void 0
|
|
}
|
|
};
|
|
var ELEMENT_KEY = "element-6066-11e4-a52e-4f735466cecf";
|
|
var SHADOW_ELEMENT_KEY = "shadow-6066-11e4-a52e-4f735466cecf";
|
|
var BASE_64_REGEX = /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$/;
|
|
var BASE_64_SAFE_STRING_TO_PROCESS_LENGTH = 2e5;
|
|
var APPIUM_MASKING_HEADER = { "x-appium-is-sensitive": "true" };
|
|
|
|
// src/bidi/utils.ts
|
|
function isBase64Safe(str) {
|
|
if (typeof str !== "string") {
|
|
return false;
|
|
}
|
|
if (str.length === 0) {
|
|
return true;
|
|
}
|
|
if (str.length % 4 !== 0) {
|
|
return false;
|
|
}
|
|
const length = str.length;
|
|
const digitCount = length.toString().length;
|
|
if (length > BASE_64_SAFE_STRING_TO_PROCESS_LENGTH) {
|
|
const chunkSize = Math.floor(length / digitCount / 4) * 4;
|
|
for (let i = 0; i < length; i += chunkSize) {
|
|
const chunk = str.slice(i, i + chunkSize);
|
|
if (!BASE_64_REGEX.test(chunk)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
return BASE_64_REGEX.test(str);
|
|
}
|
|
|
|
// src/bidi/core.ts
|
|
var SCRIPT_PREFIX = "/* __wdio script__ */";
|
|
var SCRIPT_SUFFIX = "/* __wdio script end__ */";
|
|
var log = logger("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() {
|
|
log.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;
|
|
}
|
|
log.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) {
|
|
log.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" && isBase64Safe(payload.result.data)) {
|
|
resultLog = JSON.stringify({
|
|
...payload.result,
|
|
data: `Base64 string [${payload.result.data.length} chars]`
|
|
});
|
|
}
|
|
log.info("BIDI RESULT", resultLog);
|
|
this.client?.emit("bidiResult", payload);
|
|
const resolve = this.#pendingCommands.get(payload.id);
|
|
if (!resolve) {
|
|
log.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)}`);
|
|
log.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) {
|
|
const error = payload;
|
|
failError.message += ` with error: ${payload.error} - ${error.message}`;
|
|
if (error.stacktrace && typeof error.stacktrace === "string") {
|
|
const driverStack = error.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");
|
|
}
|
|
log.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.SessionSubscribeResult>`
|
|
**/
|
|
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.SessionUnsubscribeParameters` {@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.BrowserCreateUserContextParameters` {@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.getClientWindows" with parameters.
|
|
* @url https://w3c.github.io/webdriver-bidi/#command-browser-getClientWindows
|
|
* @param params `remote.EmptyParams` {@link https://w3c.github.io/webdriver-bidi/#command-browser-getClientWindows | command parameter}
|
|
* @returns `Promise<local.BrowserGetClientWindowsResult>`
|
|
**/
|
|
async browserGetClientWindows(params) {
|
|
const result = await this.send({
|
|
method: "browser.getClientWindows",
|
|
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 "browser.setClientWindowState" with parameters.
|
|
* @url https://w3c.github.io/webdriver-bidi/#command-browser-setClientWindowState
|
|
* @param params `remote.BrowserSetClientWindowStateParameters` {@link https://w3c.github.io/webdriver-bidi/#command-browser-setClientWindowState | command parameter}
|
|
* @returns `Promise<local.EmptyResult>`
|
|
**/
|
|
async browserSetClientWindowState(params) {
|
|
const result = await this.send({
|
|
method: "browser.setClientWindowState",
|
|
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 "emulation.setGeolocationOverride" with parameters.
|
|
* @url https://w3c.github.io/webdriver-bidi/#command-emulation-setGeolocationOverride
|
|
* @param params `remote.EmulationSetGeolocationOverrideParameters` {@link https://w3c.github.io/webdriver-bidi/#command-emulation-setGeolocationOverride | command parameter}
|
|
* @returns `Promise<local.EmptyResult>`
|
|
**/
|
|
async emulationSetGeolocationOverride(params) {
|
|
const result = await this.send({
|
|
method: "emulation.setGeolocationOverride",
|
|
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 "network.setCacheBehavior" with parameters.
|
|
* @url https://w3c.github.io/webdriver-bidi/#command-network-setCacheBehavior
|
|
* @param params `remote.NetworkSetCacheBehaviorParameters` {@link https://w3c.github.io/webdriver-bidi/#command-network-setCacheBehavior | command parameter}
|
|
* @returns `Promise<local.EmptyResult>`
|
|
**/
|
|
async networkSetCacheBehavior(params) {
|
|
const result = await this.send({
|
|
method: "network.setCacheBehavior",
|
|
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;
|
|
}
|
|
/**
|
|
* WebDriver Bidi command to send command method "webExtension.install" with parameters.
|
|
* @url https://w3c.github.io/webdriver-bidi/#command-webExtension-install
|
|
* @param params `remote.WebExtensionInstallParameters` {@link https://w3c.github.io/webdriver-bidi/#command-webExtension-install | command parameter}
|
|
* @returns `Promise<local.WebExtensionInstallResult>`
|
|
**/
|
|
async webExtensionInstall(params) {
|
|
const result = await this.send({
|
|
method: "webExtension.install",
|
|
params
|
|
});
|
|
return result.result;
|
|
}
|
|
/**
|
|
* WebDriver Bidi command to send command method "webExtension.uninstall" with parameters.
|
|
* @url https://w3c.github.io/webdriver-bidi/#command-webExtension-uninstall
|
|
* @param params `remote.WebExtensionUninstallParameters` {@link https://w3c.github.io/webdriver-bidi/#command-webExtension-uninstall | command parameter}
|
|
* @returns `Promise<local.EmptyResult>`
|
|
**/
|
|
async webExtensionUninstall(params) {
|
|
const result = await this.send({
|
|
method: "webExtension.uninstall",
|
|
params
|
|
});
|
|
return result.result;
|
|
}
|
|
};
|
|
|
|
// src/utils.ts
|
|
var log2 = logger2("webdriver");
|
|
var deepmerge = deepmergeCustom({ mergeArrays: false });
|
|
function deepEqual(a, b) {
|
|
if (a === b) {
|
|
return true;
|
|
}
|
|
if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) {
|
|
return false;
|
|
}
|
|
const keysA = Object.keys(a);
|
|
const keysB = Object.keys(b);
|
|
if (keysA.length !== keysB.length) {
|
|
return false;
|
|
}
|
|
for (const key of keysA) {
|
|
if (!keysB.includes(key) || !deepEqual(a[key], b[key])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
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 keysToNormalize = new Set(Object.keys(capabilities.alwaysMatch));
|
|
if (capabilities.firstMatch) {
|
|
capabilities.firstMatch.forEach((match) => {
|
|
Object.keys(match).forEach((key) => keysToNormalize.add(key));
|
|
});
|
|
for (const key of keysToNormalize) {
|
|
const alwaysVal = capabilities.alwaysMatch[key];
|
|
if (alwaysVal === void 0) {
|
|
continue;
|
|
}
|
|
const hasConflict = capabilities.firstMatch.some(
|
|
(match) => key in match && !deepEqual(match[key], alwaysVal)
|
|
);
|
|
if (hasConflict) {
|
|
delete capabilities.alwaysMatch[key];
|
|
capabilities.firstMatch.forEach((match) => {
|
|
if (!(key in match)) {
|
|
match[key] = alwaysVal;
|
|
}
|
|
});
|
|
} else {
|
|
capabilities.firstMatch.forEach((match) => {
|
|
if (key in match) {
|
|
delete match[key];
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
const sessionRequest = new environment.value.Request(
|
|
"POST",
|
|
"/session",
|
|
{ capabilities }
|
|
);
|
|
let response;
|
|
try {
|
|
response = await sessionRequest.makeRequest(params);
|
|
} catch (err) {
|
|
log2.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") {
|
|
log2.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) {
|
|
log2.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;
|
|
log2.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 ? WebDriverBidiProtocol : {},
|
|
/**
|
|
* 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 === "")) {
|
|
log2.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 === "saucelabs.com" || 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 = environment.value.variables.WDIO_UNIT_TESTS;
|
|
if (isUnitTesting) {
|
|
log2.info("Skip connecting to WebDriver Bidi interface due to unit tests");
|
|
return {
|
|
_bidiHandler: {
|
|
value: {
|
|
isConnected: true,
|
|
waitForConnected: () => Promise.resolve(),
|
|
socket: { on: () => {
|
|
}, off: () => {
|
|
} }
|
|
}
|
|
}
|
|
};
|
|
}
|
|
socketUrl = socketUrl.replace("localhost", "127.0.0.1");
|
|
const bidiReqOpts = strictSSL ? {} : { rejectUnauthorized: false };
|
|
if (userHeaders) {
|
|
bidiReqOpts.headers = userHeaders;
|
|
}
|
|
const handler = new BidiHandler(socketUrl, bidiReqOpts);
|
|
handler.connect().then((isConnected) => isConnected && log2.info(`Connected to WebDriver Bidi interface at ${socketUrl}`));
|
|
return {
|
|
_bidiHandler: { value: handler },
|
|
...Object.values(WebDriverBidiProtocol).map((def) => def.socket).reduce((acc, cur) => {
|
|
acc[cur.command] = {
|
|
value: function(...args) {
|
|
const bidiFn = handler[cur.command];
|
|
handler.attachClient(this);
|
|
this.emit(cur.command, args);
|
|
return bidiFn?.apply(handler, args);
|
|
}
|
|
};
|
|
return acc;
|
|
}, {})
|
|
};
|
|
}
|
|
function parseBidiMessage(data) {
|
|
try {
|
|
const payload = JSON.parse(data.toString());
|
|
if (payload.type !== "event") {
|
|
return;
|
|
}
|
|
this.emit(payload.method, payload.params);
|
|
} catch (err) {
|
|
log2.error(`Failed parse WebDriver Bidi message: ${err.message}`);
|
|
}
|
|
}
|
|
function mask(commandInfo, options, body, args) {
|
|
const unmaskedResult = { maskedBody: body, maskedArgs: args, isMasked: false };
|
|
if (!options.mask) {
|
|
return unmaskedResult;
|
|
}
|
|
const textValueParamIndex = commandInfo.parameters.findIndex((param) => param.name === "text");
|
|
if (textValueParamIndex === -1) {
|
|
return unmaskedResult;
|
|
}
|
|
const textValueIndexInArgs = (commandInfo.variables?.length ?? 0) + textValueParamIndex;
|
|
const text = args[textValueIndexInArgs];
|
|
if (typeof text !== "string" || !text) {
|
|
return unmaskedResult;
|
|
}
|
|
const maskedBody = {
|
|
...body,
|
|
text: SENSITIVE_DATA_REPLACER
|
|
};
|
|
const textValueArgsIndex = textValueParamIndex + (commandInfo.variables?.length ?? 0);
|
|
const maskedArgs = args.slice(0, textValueArgsIndex).concat(SENSITIVE_DATA_REPLACER).concat(args.slice(textValueArgsIndex + 1));
|
|
return {
|
|
maskedBody,
|
|
maskedArgs,
|
|
isMasked: true
|
|
};
|
|
}
|
|
|
|
// src/command.ts
|
|
var log3 = logger3("webdriver");
|
|
var BIDI_COMMANDS = Object.values(WebDriverBidiProtocol2).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(...unmaskedArgs) {
|
|
let runtimeOptions = {};
|
|
if (unmaskedArgs.length > 0 && unmaskedArgs[unmaskedArgs.length - 1] instanceof CommandRuntimeOptions) {
|
|
runtimeOptions = unmaskedArgs.pop();
|
|
}
|
|
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 DISABLE_WEBDRIVERIO_DEPRECATION_WARNINGS = globalThis.process && globalThis.process.env ? globalThis.process.env.DISABLE_WEBDRIVERIO_DEPRECATION_WARNINGS : void 0;
|
|
if (typeof deprecated === "string" && !DISABLE_WEBDRIVERIO_DEPRECATION_WARNINGS) {
|
|
const warning = deprecated.replace("This command", `The "${command}" command`);
|
|
log3.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 (unmaskedArgs.length < minAllowedParams || unmaskedArgs.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
|
|
);
|
|
}
|
|
const unmaskedBody = {};
|
|
for (const [it, arg] of Object.entries(unmaskedArgs)) {
|
|
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;
|
|
}
|
|
unmaskedBody[commandParams[i].name] = arg;
|
|
}
|
|
const { maskedBody, maskedArgs, isMasked } = mask(commandInfo, runtimeOptions, unmaskedBody, unmaskedArgs);
|
|
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, maskedArgs)}" after session has been deleted, aborting request without executing it`);
|
|
}
|
|
const request = new environment.value.Request(method, endpoint, unmaskedBody, abortSignal, isHubCommand, {
|
|
onPerformance: (data) => this.emit("request.performance", { ...data, request: {
|
|
...data.request,
|
|
body: isMasked ? maskedBody : data.request.body
|
|
} }),
|
|
onRequest: (data) => this.emit("request.start", { ...data, body: isMasked ? maskedBody : data.body }),
|
|
onResponse: (data) => this.emit("request.end", data),
|
|
onRetry: (data) => this.emit("request.retry", data),
|
|
onLogData: (data) => log3.info("DATA", transformCommandLogResult(isMasked ? maskedBody : data))
|
|
});
|
|
this.emit("command", { command, method, endpoint, body: maskedBody });
|
|
log3.info("COMMAND", commandCallStructure(command, maskedArgs));
|
|
const options = isMasked ? { ...this.options, headers: { ...this.options.headers, ...APPIUM_MASKING_HEADER } } : this.options;
|
|
return request.makeRequest(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 maskedBody.script === "string" && maskedBody.script.includes("(() => window.__wdioEvents__)")) {
|
|
resultLog = `[${result.value.length} framework events captured]`;
|
|
}
|
|
log3.info("RESULT", resultLog);
|
|
}
|
|
this.emit("result", { command, method, endpoint, body: maskedBody, result });
|
|
if (command === "deleteSession") {
|
|
const browser = this;
|
|
browser._bidiHandler?.close();
|
|
const shutdownDriver = maskedBody.deleteSessionOpts?.shutdownDriver !== false;
|
|
environment.value.killDriverProcess(this.capabilities, shutdownDriver);
|
|
if (!environment.value.variables.WDIO_WORKER_ID) {
|
|
logger3.clearLogger();
|
|
}
|
|
}
|
|
return result.value;
|
|
}).catch((error) => {
|
|
this.emit("result", { command, method, endpoint, body: maskedBody, 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/bidi/localTypes.ts
|
|
var localTypes_exports = {};
|
|
|
|
// src/bidi/remoteTypes.ts
|
|
var remoteTypes_exports = {};
|
|
|
|
// src/index.ts
|
|
var log4 = logger4("webdriver");
|
|
var WebDriver = class _WebDriver {
|
|
static async newSession(options, modifier, userPrototype = {}, customCommandWrapper, implicitWaitExclusionList = []) {
|
|
const envLogLevel = environment.value.variables.WDIO_LOG_LEVEL;
|
|
options.logLevel = envLogLevel ?? options.logLevel;
|
|
const params = validateConfig(DEFAULTS, options);
|
|
if (params.logLevel && (!options.logLevels || !options.logLevels.webdriver)) {
|
|
logger4.setLevel("webdriver", params.logLevel);
|
|
}
|
|
log4.info("Initiate new session using the WebDriver protocol");
|
|
const driverProcess = await startWebDriver(params);
|
|
const requestedCapabilities = { ...params.capabilities };
|
|
const { sessionId, capabilities } = await startWebDriverSession(params);
|
|
const environment2 = sessionEnvironmentDetector({ capabilities, requestedCapabilities });
|
|
const environmentPrototype = getEnvironmentVars(environment2);
|
|
const protocolCommands = getPrototype(environment2);
|
|
if (driverProcess?.pid) {
|
|
capabilities["wdio:driverPID"] = driverProcess.pid;
|
|
}
|
|
const bidiPrototype = {};
|
|
if (isBidi(capabilities)) {
|
|
log4.info(`Register BiDi handler for session with id ${sessionId}`);
|
|
Object.assign(bidiPrototype, initiateBidi(
|
|
capabilities.webSocketUrl,
|
|
options.strictSSL,
|
|
options.headers
|
|
));
|
|
}
|
|
const monad = webdriverMonad(
|
|
{ ...params, requestedCapabilities },
|
|
modifier,
|
|
{
|
|
...protocolCommands,
|
|
...environmentPrototype,
|
|
...userPrototype,
|
|
...bidiPrototype
|
|
}
|
|
);
|
|
const client = monad(sessionId, customCommandWrapper, implicitWaitExclusionList);
|
|
if (isBidi(capabilities)) {
|
|
if (await client._bidiHandler.waitForConnected()) {
|
|
client._bidiHandler.socket?.on("message", parseBidiMessage.bind(client));
|
|
}
|
|
}
|
|
if (params.enableDirectConnect) {
|
|
setupDirectConnect(client);
|
|
}
|
|
return client;
|
|
}
|
|
/**
|
|
* allows user to attach to existing sessions
|
|
*/
|
|
static attachToSession(options, modifier, userPrototype = {}, commandWrapper) {
|
|
if (!options || typeof options.sessionId !== "string") {
|
|
throw new Error("sessionId is required to attach to existing session");
|
|
}
|
|
if (options.logLevel) {
|
|
logger4.setLevel("webdriver", options.logLevel);
|
|
}
|
|
options.capabilities = options.capabilities || {};
|
|
options.isW3C = options.isW3C === false ? false : true;
|
|
options.protocol = options.protocol || DEFAULTS.protocol.default;
|
|
options.hostname = options.hostname || DEFAULTS.hostname.default;
|
|
options.port = options.port || DEFAULTS.port.default;
|
|
options.path = options.path || DEFAULTS.path.default;
|
|
const environment2 = sessionEnvironmentDetector({ capabilities: options.capabilities, requestedCapabilities: options.capabilities });
|
|
options = Object.assign(environment2, options);
|
|
const environmentPrototype = getEnvironmentVars(options);
|
|
const protocolCommands = getPrototype(options);
|
|
const bidiPrototype = {};
|
|
if (isBidi(options.capabilities || {})) {
|
|
const webSocketUrl = options.capabilities?.webSocketUrl;
|
|
log4.info(`Register BiDi handler for session with id ${options.sessionId}`);
|
|
Object.assign(bidiPrototype, initiateBidi(
|
|
webSocketUrl,
|
|
options.strictSSL,
|
|
options.headers
|
|
));
|
|
}
|
|
const prototype = { ...protocolCommands, ...environmentPrototype, ...userPrototype, ...bidiPrototype };
|
|
const monad = webdriverMonad(options, modifier, prototype);
|
|
const client = monad(options.sessionId, commandWrapper);
|
|
if (isBidi(options.capabilities || {})) {
|
|
client._bidiHandler?.waitForConnected().then(() => {
|
|
client._bidiHandler?.socket.on("message", parseBidiMessage.bind(client));
|
|
});
|
|
}
|
|
return client;
|
|
}
|
|
/**
|
|
* Changes The instance session id and browser capabilities for the new session
|
|
* directly into the passed in browser object
|
|
*
|
|
* @param {object} instance the object we get from a new browser session.
|
|
* @returns {string} the new session id of the browser
|
|
*/
|
|
static async reloadSession(instance, newCapabilities) {
|
|
const capabilities = newCapabilities ? newCapabilities : Object.assign({}, instance.requestedCapabilities);
|
|
let params = { ...instance.options, capabilities };
|
|
for (const prop of ["protocol", "hostname", "port", "path", "queryParams", "user", "key"]) {
|
|
if (prop in capabilities) {
|
|
params = { ...params, [prop]: capabilities[prop] };
|
|
delete capabilities[prop];
|
|
}
|
|
}
|
|
let driverProcess;
|
|
if (params.hostname === "localhost" && newCapabilities?.browserName) {
|
|
delete params.port;
|
|
delete params.hostname;
|
|
driverProcess = await startWebDriver(params);
|
|
}
|
|
const { sessionId, capabilities: newSessionCapabilities } = await startWebDriverSession(params);
|
|
if (driverProcess?.pid) {
|
|
newSessionCapabilities["wdio:driverPID"] = driverProcess.pid;
|
|
}
|
|
for (const prop of ["protocol", "hostname", "port", "path", "queryParams", "user", "key"]) {
|
|
if (prop in params) {
|
|
instance.options[prop] = params[prop];
|
|
}
|
|
}
|
|
for (const prop in instance.requestedCapabilities) {
|
|
delete instance.requestedCapabilities[prop];
|
|
}
|
|
const driverPid = instance.capabilities["wdio:driverPID"];
|
|
instance.sessionId = sessionId;
|
|
instance.capabilities = newSessionCapabilities;
|
|
instance.capabilities["wdio:driverPID"] = driverPid;
|
|
Object.assign(instance.requestedCapabilities, capabilities);
|
|
if (isBidi(instance.capabilities || {})) {
|
|
const bidiReqOpts = instance.options.strictSSL ? {} : { rejectUnauthorized: false };
|
|
await instance._bidiHandler?.reconnect(newSessionCapabilities.webSocketUrl, bidiReqOpts);
|
|
instance._bidiHandler?.socket?.on("message", parseBidiMessage.bind(instance));
|
|
}
|
|
return sessionId;
|
|
}
|
|
static get WebDriver() {
|
|
return _WebDriver;
|
|
}
|
|
};
|
|
|
|
// src/request/node.ts
|
|
import dns from "node:dns";
|
|
import { fetch as fetch2, Agent, ProxyAgent, getGlobalDispatcher } from "undici";
|
|
|
|
// src/request/request.ts
|
|
import logger5 from "@wdio/logger";
|
|
import { sleep } from "@wdio/utils";
|
|
|
|
// src/request/error.ts
|
|
import { transformCommandLogResult as transformCommandLogResult2 } from "@wdio/utils";
|
|
|
|
// src/request/constants.ts
|
|
var RETRYABLE_STATUS_CODES = [408, 413, 429, 500, 502, 503, 504];
|
|
var RETRYABLE_ERROR_CODES = [
|
|
"ETIMEDOUT",
|
|
"ECONNRESET",
|
|
"EADDRINUSE",
|
|
"ECONNREFUSED",
|
|
"EPIPE",
|
|
"ENOTFOUND",
|
|
"ENETUNREACH",
|
|
"EAI_AGAIN",
|
|
// additional error codes we like to retry
|
|
"UND_ERR_CONNECT_TIMEOUT",
|
|
"UND_ERR_SOCKET"
|
|
];
|
|
var REG_EXPS = {
|
|
commandName: /.*\/session\/[0-9a-f-]+\/(.*)/,
|
|
execFn: /return \(([\s\S]*)\)\.apply\(null, arguments\)/
|
|
};
|
|
|
|
// src/request/error.ts
|
|
var WebDriverError = class extends Error {
|
|
/**
|
|
* return timeout error with information about the executing command on which the test hangs
|
|
*/
|
|
computeErrorMessage() {
|
|
const cmdName = this.#getExecCmdName();
|
|
const cmdArgs = this.#getExecCmdArgs(this.opts);
|
|
const cmdInfoMsg = `when running "${cmdName}" with method "${this.opts.method}"`;
|
|
const cmdArgsMsg = cmdArgs ? ` and args ${cmdArgs}` : "";
|
|
return `WebDriverError: ${this.message} ${cmdInfoMsg}${cmdArgsMsg}`;
|
|
}
|
|
#getExecCmdName() {
|
|
const { href } = this.url;
|
|
const res = href.match(REG_EXPS.commandName) || [];
|
|
return res[1] || href;
|
|
}
|
|
#getExecCmdArgs(requestOptions) {
|
|
const { body: cmdJson } = requestOptions;
|
|
if (typeof cmdJson !== "object") {
|
|
return "";
|
|
}
|
|
const transformedRes = transformCommandLogResult2(cmdJson);
|
|
if (typeof transformedRes === "string") {
|
|
return transformedRes;
|
|
}
|
|
if (typeof cmdJson.script === "string") {
|
|
const scriptRes = cmdJson.script.match(REG_EXPS.execFn) || [];
|
|
return `"${scriptRes[1] || cmdJson.script}"`;
|
|
}
|
|
return Object.keys(cmdJson).length ? `"${JSON.stringify(cmdJson)}"` : "";
|
|
}
|
|
};
|
|
var WebDriverRequestError = class extends WebDriverError {
|
|
url;
|
|
opts;
|
|
statusCode;
|
|
body;
|
|
code;
|
|
constructor(err, url, opts) {
|
|
let message = err.message;
|
|
if (err.message === "fetch failed") {
|
|
message = `Failed to fetch [${opts.method}] ${url.href}: please make sure you have a WebDriver compatible server running on ${url.origin}`;
|
|
}
|
|
super(message);
|
|
this.url = url;
|
|
this.opts = opts;
|
|
const errorCode = typeof err.cause === "object" && err.cause && "code" in err.cause && typeof err.cause.code === "string" ? err.cause.code : "code" in err && typeof err.code === "string" ? err.code : void 0;
|
|
if (errorCode) {
|
|
this.code = errorCode;
|
|
this.message = errorCode === "UND_ERR_CONNECT_TIMEOUT" ? 'Request timed out! Consider increasing the "connectionRetryTimeout" option.' : "Request failed with error code " + errorCode;
|
|
}
|
|
if (typeof err.cause === "object" && err.cause) {
|
|
this.statusCode = "statusCode" in err.cause && typeof err.cause.statusCode === "number" ? err.cause.statusCode : void 0;
|
|
this.body = "body" in err.cause ? err.cause.body : void 0;
|
|
}
|
|
this.message = this.computeErrorMessage();
|
|
}
|
|
};
|
|
var WebDriverResponseError = class _WebDriverResponseError extends WebDriverError {
|
|
url;
|
|
opts;
|
|
constructor(response, url, opts) {
|
|
const errorObj = !response || typeof response !== "object" || !("body" in response) || !response.body ? new Error("Response has empty body") : typeof response.body === "string" && response.body.length ? new Error(response.body) : typeof response.body !== "object" ? new Error("Unknown error") : "value" in response.body && response.body.value ? response.body.value : response.body;
|
|
let errorMessage = errorObj.message || errorObj.error || errorObj.class || "unknown error";
|
|
if (typeof errorMessage === "string" && errorMessage.includes("invalid locator")) {
|
|
const requestOptions = opts.body;
|
|
errorMessage = `The selector "${requestOptions.value}" used with strategy "${requestOptions.using}" is invalid!`;
|
|
}
|
|
super(errorMessage);
|
|
if (errorObj.error) {
|
|
this.name = errorObj.error;
|
|
} else if (errorMessage && errorMessage.includes("stale element reference")) {
|
|
this.name = "stale element reference";
|
|
} else {
|
|
this.name = errorObj.name || "WebDriver Error";
|
|
}
|
|
Error.captureStackTrace(this, _WebDriverResponseError);
|
|
this.url = url;
|
|
this.opts = opts;
|
|
this.message = this.computeErrorMessage();
|
|
}
|
|
};
|
|
|
|
// package.json
|
|
var package_default = {
|
|
name: "webdriver",
|
|
version: "9.24.0",
|
|
description: "A Node.js bindings implementation for the W3C WebDriver and Mobile JSONWire Protocol",
|
|
author: "Christian Bromann <mail@bromann.dev>",
|
|
homepage: "https://github.com/webdriverio/webdriverio/tree/main/packages/webdriver",
|
|
license: "MIT",
|
|
type: "module",
|
|
main: "./build/index.cjs",
|
|
module: "./build/index.js",
|
|
exports: {
|
|
".": {
|
|
types: "./build/index.d.ts",
|
|
browserSource: "./src/browser.js",
|
|
browser: "./build/index.js",
|
|
importSource: "./src/node.ts",
|
|
import: "./build/node.js",
|
|
requireSource: "./src/index.cts",
|
|
require: "./build/index.cjs"
|
|
}
|
|
},
|
|
types: "./build/index.d.ts",
|
|
typeScriptVersion: "3.8.3",
|
|
engines: {
|
|
node: ">=18.20.0"
|
|
},
|
|
repository: {
|
|
type: "git",
|
|
url: "git+https://github.com/webdriverio/webdriverio.git",
|
|
directory: "packages/webdriver"
|
|
},
|
|
keywords: [
|
|
"webdriver"
|
|
],
|
|
bugs: {
|
|
url: "https://github.com/webdriverio/webdriverio/issues"
|
|
},
|
|
dependencies: {
|
|
"@types/node": "^20.1.0",
|
|
"@types/ws": "^8.5.3",
|
|
"@wdio/config": "workspace:*",
|
|
"@wdio/logger": "workspace:*",
|
|
"@wdio/protocols": "workspace:*",
|
|
"@wdio/types": "workspace:*",
|
|
"@wdio/utils": "workspace:*",
|
|
"deepmerge-ts": "^7.0.3",
|
|
"https-proxy-agent": "^7.0.6",
|
|
undici: "^6.21.3",
|
|
ws: "^8.8.0"
|
|
}
|
|
};
|
|
|
|
// src/request/polyfill.ts
|
|
if (!AbortSignal.any) {
|
|
AbortSignal.any = function(signals) {
|
|
if (!signals || !Array.isArray(signals)) {
|
|
throw new TypeError("AbortSignal.any requires an array of AbortSignal objects");
|
|
}
|
|
const controller = new AbortController();
|
|
if (signals.some((signal) => signal.aborted)) {
|
|
controller.abort();
|
|
return controller.signal;
|
|
}
|
|
const listeners = signals.map((signal) => {
|
|
const listener = () => {
|
|
if ("reason" in signal && signal.reason !== void 0) {
|
|
controller.abort(signal.reason);
|
|
} else {
|
|
controller.abort();
|
|
}
|
|
cleanup();
|
|
};
|
|
signal.addEventListener("abort", listener);
|
|
return { signal, listener };
|
|
});
|
|
const cleanup = () => {
|
|
listeners.forEach(({ signal, listener }) => {
|
|
signal.removeEventListener("abort", listener);
|
|
});
|
|
};
|
|
controller.signal.addEventListener("abort", cleanup);
|
|
return controller.signal;
|
|
};
|
|
}
|
|
|
|
// src/request/request.ts
|
|
var ERRORS_TO_EXCLUDE_FROM_RETRY = [
|
|
"detached shadow root",
|
|
"move target out of bounds"
|
|
];
|
|
var DEFAULT_HEADERS = {
|
|
"Content-Type": "application/json; charset=utf-8",
|
|
"Connection": "keep-alive",
|
|
"Accept": "application/json",
|
|
"User-Agent": "webdriver/" + package_default.version
|
|
};
|
|
var log5 = logger5("webdriver");
|
|
var WebDriverRequest = class {
|
|
body;
|
|
method;
|
|
endpoint;
|
|
isHubCommand;
|
|
requiresSessionId;
|
|
eventHandler;
|
|
abortSignal;
|
|
constructor(method, endpoint, body, abortSignal, isHubCommand = false, eventHandler = {}) {
|
|
this.body = body;
|
|
this.method = method;
|
|
this.endpoint = endpoint;
|
|
this.isHubCommand = isHubCommand;
|
|
this.requiresSessionId = Boolean(this.endpoint.match(/:sessionId/));
|
|
this.eventHandler = eventHandler;
|
|
this.abortSignal = abortSignal;
|
|
}
|
|
async makeRequest(options, sessionId) {
|
|
const { url, requestOptions } = await this.createOptions(options, sessionId);
|
|
this.eventHandler.onRequest?.(requestOptions);
|
|
return this._request(url, requestOptions, options.transformResponse, options.connectionRetryCount, 0);
|
|
}
|
|
async createOptions(options, sessionId, isBrowser = false) {
|
|
const timeout = options.connectionRetryTimeout || DEFAULTS.connectionRetryTimeout.default;
|
|
const requestOptions = {
|
|
method: this.method,
|
|
redirect: "follow",
|
|
signal: AbortSignal.any([
|
|
AbortSignal.timeout(timeout),
|
|
...this.abortSignal ? [this.abortSignal] : []
|
|
])
|
|
};
|
|
const requestHeaders = new Headers({
|
|
...DEFAULT_HEADERS,
|
|
...typeof options.headers === "object" ? options.headers : {}
|
|
});
|
|
const searchParams = isBrowser ? void 0 : typeof options.queryParams === "object" ? options.queryParams : void 0;
|
|
if (this.body && (Object.keys(this.body).length || this.method === "POST")) {
|
|
requestOptions.body = JSON.stringify(this.body);
|
|
const contentLength = new TextEncoder().encode(requestOptions.body).length;
|
|
requestHeaders.set("Content-Length", `${contentLength}`);
|
|
}
|
|
let endpoint = this.endpoint;
|
|
if (this.requiresSessionId) {
|
|
if (!sessionId) {
|
|
throw new Error("A sessionId is required for this command");
|
|
}
|
|
endpoint = endpoint.replace(":sessionId", sessionId);
|
|
}
|
|
const url = new URL(`${options.protocol}://${options.hostname}:${options.port}${this.isHubCommand ? this.endpoint : `${options.path || ""}/${endpoint}`.replace(/(\/){2,}/g, "/")}`);
|
|
if (searchParams) {
|
|
url.search = new URLSearchParams(searchParams).toString();
|
|
}
|
|
if (this.endpoint === "/session" && options.user && options.key) {
|
|
requestHeaders.set("Authorization", "Basic " + btoa(options.user + ":" + options.key));
|
|
}
|
|
requestOptions.headers = requestHeaders;
|
|
return {
|
|
url,
|
|
requestOptions: typeof options.transformRequest === "function" ? options.transformRequest(requestOptions) : requestOptions
|
|
};
|
|
}
|
|
async _libRequest(url, opts) {
|
|
try {
|
|
const response = await this.fetch(url, opts);
|
|
return await this.parseResponse(response);
|
|
} catch (err) {
|
|
if (!(err instanceof Error)) {
|
|
throw new WebDriverRequestError(
|
|
new Error(`Failed to fetch ${url.href}: ${err.message || err || "Unknown error"}`),
|
|
url,
|
|
opts
|
|
);
|
|
}
|
|
throw new WebDriverRequestError(err, url, opts);
|
|
}
|
|
}
|
|
async parseResponse(response) {
|
|
const rawBody = await response.text();
|
|
try {
|
|
return {
|
|
statusCode: response.status,
|
|
body: rawBody ? JSON.parse(rawBody) : {}
|
|
};
|
|
} catch {
|
|
throw new Error(`Could not parse response body: "${rawBody}"`, {
|
|
cause: {
|
|
statusCode: response.status,
|
|
body: rawBody
|
|
}
|
|
});
|
|
}
|
|
}
|
|
async _request(url, fullRequestOptions, transformResponse, totalRetryCount = 0, retryCount = 0) {
|
|
log5.info(`[${fullRequestOptions.method}] ${url.href}`);
|
|
if (fullRequestOptions.body && Object.keys(fullRequestOptions.body).length) {
|
|
this.eventHandler.onLogData?.(fullRequestOptions.body);
|
|
}
|
|
const { ...requestLibOptions } = fullRequestOptions;
|
|
const startTime = performance.now();
|
|
let response = await this._libRequest(url, requestLibOptions).catch((err) => err);
|
|
const durationMillisecond = performance.now() - startTime;
|
|
const retry = async (error2) => {
|
|
if (retryCount >= totalRetryCount || error2.message.includes("invalid session id")) {
|
|
log5.error(error2.message);
|
|
this.eventHandler.onResponse?.({ error: error2 });
|
|
this.eventHandler.onPerformance?.({ request: fullRequestOptions, durationMillisecond, success: false, error: error2, retryCount });
|
|
throw error2;
|
|
}
|
|
if (retryCount > 0) {
|
|
await sleep(Math.min(1e4, 250 * Math.pow(2, retryCount)));
|
|
}
|
|
++retryCount;
|
|
this.eventHandler.onRetry?.({ error: error2, retryCount });
|
|
this.eventHandler.onPerformance?.({ request: fullRequestOptions, durationMillisecond, success: false, error: error2, retryCount });
|
|
log5.warn(error2.message);
|
|
log5.info(`Retrying ${retryCount}/${totalRetryCount}`);
|
|
return this._request(url, fullRequestOptions, transformResponse, totalRetryCount, retryCount);
|
|
};
|
|
if (response instanceof Error) {
|
|
const resError = response;
|
|
if (!(this.abortSignal && this.abortSignal.aborted) && (resError.code && RETRYABLE_ERROR_CODES.includes(resError.code) || resError.statusCode && RETRYABLE_STATUS_CODES.includes(resError.statusCode))) {
|
|
return retry(resError);
|
|
}
|
|
this.eventHandler.onPerformance?.({ request: fullRequestOptions, durationMillisecond, success: false, error: response, retryCount });
|
|
throw response;
|
|
}
|
|
if (typeof transformResponse === "function") {
|
|
response = transformResponse(response, fullRequestOptions);
|
|
}
|
|
if (isSuccessfulResponse(response.statusCode, response.body)) {
|
|
this.eventHandler.onResponse?.({ result: response.body });
|
|
this.eventHandler.onPerformance?.({ request: fullRequestOptions, durationMillisecond, success: true, retryCount });
|
|
return response.body;
|
|
}
|
|
const error = new WebDriverResponseError(response, url, fullRequestOptions);
|
|
if (this.isHubCommand) {
|
|
if (typeof response.body === "string" && response.body.startsWith("<!DOCTYPE html>")) {
|
|
this.eventHandler.onPerformance?.({ request: fullRequestOptions, durationMillisecond, success: false, error, retryCount });
|
|
return Promise.reject(new Error("Command can only be called to a Selenium Hub"));
|
|
}
|
|
return { value: response.body || null };
|
|
}
|
|
if (error.name === "stale element reference") {
|
|
log5.warn("Request encountered a stale element - terminating request");
|
|
this.eventHandler.onResponse?.({ error });
|
|
this.eventHandler.onPerformance?.({ request: fullRequestOptions, durationMillisecond, success: false, error, retryCount });
|
|
throw error;
|
|
}
|
|
if (ERRORS_TO_EXCLUDE_FROM_RETRY.includes(error.name)) {
|
|
throw error;
|
|
}
|
|
return retry(error);
|
|
}
|
|
};
|
|
|
|
// src/request/node.ts
|
|
dns.setDefaultResultOrder("ipv4first");
|
|
var SESSION_DISPATCHERS = /* @__PURE__ */ new Map();
|
|
var FetchRequest = class extends WebDriverRequest {
|
|
async fetch(url, opts) {
|
|
const response = await fetch2(url, opts);
|
|
if (opts.method === "DELETE") {
|
|
const match = url.pathname.match(/\/session\/([^/]+)$/);
|
|
const sessionId = match?.[1];
|
|
if (sessionId) {
|
|
this.cleanupSessionDispatcher(sessionId);
|
|
}
|
|
}
|
|
return response;
|
|
}
|
|
getDispatcher(url, options, sessionId) {
|
|
if (sessionId && SESSION_DISPATCHERS.has(sessionId)) {
|
|
return SESSION_DISPATCHERS.get(sessionId);
|
|
}
|
|
try {
|
|
const globalDispatcher = getGlobalDispatcher();
|
|
if (globalDispatcher && (globalDispatcher.constructor.name === "ProxyAgent" || "proxy" in globalDispatcher || // Also check for other custom dispatcher types
|
|
globalDispatcher.constructor.name !== "Agent" && globalDispatcher.constructor.name !== "MockAgent")) {
|
|
return globalDispatcher;
|
|
}
|
|
} catch {
|
|
}
|
|
const { PROXY_URL, NO_PROXY } = environment.value.variables;
|
|
const shouldUseProxy = PROXY_URL && !NO_PROXY?.some((str) => url.hostname.endsWith(str));
|
|
const dispatcher = shouldUseProxy ? new ProxyAgent({
|
|
uri: PROXY_URL,
|
|
connectTimeout: options.connectionRetryTimeout,
|
|
headersTimeout: options.connectionRetryTimeout,
|
|
bodyTimeout: options.connectionRetryTimeout
|
|
}) : new Agent({
|
|
connectTimeout: options.connectionRetryTimeout,
|
|
headersTimeout: options.connectionRetryTimeout,
|
|
bodyTimeout: options.connectionRetryTimeout
|
|
});
|
|
if (sessionId) {
|
|
SESSION_DISPATCHERS.set(sessionId, dispatcher);
|
|
}
|
|
return dispatcher;
|
|
}
|
|
cleanupSessionDispatcher(sessionId) {
|
|
const dispatcher = SESSION_DISPATCHERS.get(sessionId);
|
|
if (dispatcher && typeof dispatcher.close === "function") {
|
|
dispatcher.close();
|
|
}
|
|
SESSION_DISPATCHERS.delete(sessionId);
|
|
}
|
|
async createOptions(options, sessionId, isBrowser = false) {
|
|
const { url, requestOptions } = await super.createOptions(options, sessionId, isBrowser);
|
|
requestOptions.dispatcher = this.getDispatcher(url, options, sessionId);
|
|
return { url, requestOptions };
|
|
}
|
|
};
|
|
|
|
// src/request/web.ts
|
|
var FetchRequest2 = class extends WebDriverRequest {
|
|
fetch(url, opts) {
|
|
return fetch(url, opts);
|
|
}
|
|
};
|
|
|
|
// src/node/bidi.ts
|
|
import { isIP } from "node:net";
|
|
import dns2 from "node:dns/promises";
|
|
import logger6 from "@wdio/logger";
|
|
import WebSocket from "ws";
|
|
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
var log6 = logger6("webdriver");
|
|
var CONNECTION_TIMEOUT = 1e4;
|
|
async function createBidiConnection(webSocketUrl, options) {
|
|
const candidateUrls = await listWebsocketCandidateUrls(webSocketUrl);
|
|
return connectWebsocket(candidateUrls, options);
|
|
}
|
|
async function listWebsocketCandidateUrls(webSocketUrl) {
|
|
const parsedUrl = new URL(webSocketUrl);
|
|
const candidateUrls = [webSocketUrl];
|
|
if (isIP(parsedUrl.hostname)) {
|
|
return candidateUrls;
|
|
}
|
|
try {
|
|
const candidateIps = await dns2.lookup(parsedUrl.hostname, { family: 0, all: true });
|
|
if (candidateIps.length > 1) {
|
|
const hostnameMapper = (result) => webSocketUrl.replace(parsedUrl.hostname, result.address);
|
|
candidateUrls.push(...candidateIps.map(hostnameMapper));
|
|
}
|
|
} catch (error) {
|
|
log6.error(`Could not resolve hostname ${parsedUrl.hostname}: ${error}`);
|
|
}
|
|
return candidateUrls;
|
|
}
|
|
async function connectWebsocket(candidateUrls, options) {
|
|
const websockets = candidateUrls.map((candidateUrl) => {
|
|
log6.debug(`Attempt to connect to webSocketUrl ${candidateUrl}`);
|
|
try {
|
|
const finalizedOptions = { ...options };
|
|
const { PROXY_URL, NO_PROXY } = environment.value.variables;
|
|
const shouldUseProxy = PROXY_URL && !NO_PROXY?.some((str) => {
|
|
try {
|
|
return new URL(candidateUrl).hostname.endsWith(str);
|
|
} catch {
|
|
return false;
|
|
}
|
|
});
|
|
if (shouldUseProxy) {
|
|
log6.debug(`Adding proxy ${PROXY_URL} for webSocketUrl ${candidateUrl}`);
|
|
finalizedOptions.agent = new HttpsProxyAgent(PROXY_URL);
|
|
}
|
|
const ws2 = new WebSocket(candidateUrl, finalizedOptions);
|
|
return ws2;
|
|
} catch {
|
|
return void 0;
|
|
}
|
|
}).filter(Boolean);
|
|
const wsConnectPromises = websockets.map((ws2, index) => {
|
|
const promise = new Promise((resolve) => {
|
|
ws2.once("open", () => resolve({ ws: ws2, isConnected: true, index }));
|
|
ws2.once("error", (err) => {
|
|
log6.debug(`Could not connect to Bidi protocol at ${candidateUrls[index]}: ${err.message}`);
|
|
resolve({ ws: ws2, isConnected: false, errorMessage: err.message, index });
|
|
});
|
|
});
|
|
return { promise, index };
|
|
});
|
|
let timeoutId;
|
|
const connectionTimeoutPromise = new Promise((resolve) => {
|
|
timeoutId = setTimeout(() => {
|
|
log6.error(`Could not connect to Bidi protocol of any candidate url in time: "${candidateUrls.join('", "')}"`);
|
|
return resolve(void 0);
|
|
}, CONNECTION_TIMEOUT);
|
|
});
|
|
const wsInfo = await Promise.race([
|
|
firstResolved(wsConnectPromises),
|
|
connectionTimeoutPromise
|
|
]);
|
|
clearTimeout(timeoutId);
|
|
const socketsToCleanup = wsInfo ? websockets.filter((_, index) => wsInfo.index !== index) : websockets;
|
|
for (const socket of socketsToCleanup) {
|
|
socket.terminate();
|
|
}
|
|
if (wsInfo?.isConnected) {
|
|
log6.info(`Connected to Bidi protocol at ${candidateUrls[wsInfo.index]}`);
|
|
return wsInfo.ws;
|
|
}
|
|
return void 0;
|
|
}
|
|
function firstResolved(promises, errorMessages = []) {
|
|
if (promises.length === 0) {
|
|
const sep = "\n - ";
|
|
const errorMessage = errorMessages.length > 0 ? sep + errorMessages.join(sep) : "";
|
|
log6.error("Could not connect to Bidi protocol" + errorMessage);
|
|
return Promise.resolve(void 0);
|
|
}
|
|
return Promise.race(promises.map(({ promise }) => promise)).then((result) => {
|
|
if (result.isConnected) {
|
|
return result;
|
|
}
|
|
return firstResolved(
|
|
promises.filter(({ index }) => index !== result.index),
|
|
[...errorMessages, result.errorMessage || "unknown error"]
|
|
);
|
|
});
|
|
}
|
|
|
|
// src/node/utils.ts
|
|
import logger7 from "@wdio/logger";
|
|
var log7 = logger7("webdriver");
|
|
function killDriverProcess(capabilities, shutdownDriver) {
|
|
if (shutdownDriver && "wdio:driverPID" in capabilities && capabilities["wdio:driverPID"]) {
|
|
log7.info(`Kill driver process with PID ${capabilities["wdio:driverPID"]}`);
|
|
try {
|
|
const killedSuccessfully = process.kill(capabilities["wdio:driverPID"], "SIGKILL");
|
|
if (!killedSuccessfully) {
|
|
log7.warn("Failed to kill driver process, manually clean-up might be required");
|
|
}
|
|
} catch (err) {
|
|
log7.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);
|
|
}
|
|
}
|
|
|
|
// src/node.ts
|
|
var node_default = WebDriver;
|
|
environment.value = {
|
|
Request: (
|
|
/**
|
|
* Currently Nock doesn't support the mocking of undici requests, therefore for all
|
|
* Smoke test we use the native fetch implementation.
|
|
*
|
|
* @see https://github.com/nock/nock/issues/2183#issuecomment-2252525890
|
|
*/
|
|
process.env.WDIO_USE_NATIVE_FETCH || /**
|
|
* For unit tests we use the WebFetchRequest implementation as we can better mock the
|
|
* requests in the unit tests.
|
|
*/
|
|
process.env.WDIO_UNIT_TESTS ? FetchRequest2 : FetchRequest
|
|
),
|
|
Socket: ws,
|
|
createBidiConnection,
|
|
killDriverProcess,
|
|
variables: {
|
|
WDIO_LOG_LEVEL: process.env.WDIO_LOG_LEVEL,
|
|
WDIO_UNIT_TESTS: process.env.WDIO_UNIT_TESTS,
|
|
WEBDRIVER_CACHE_DIR: process.env.WEBDRIVER_CACHE_DIR || os.tmpdir(),
|
|
PROXY_URL: process.env.HTTP_PROXY || process.env.HTTPS_PROXY,
|
|
NO_PROXY: process.env.NO_PROXY && process.env.NO_PROXY.trim() ? process.env.NO_PROXY.trim().split(/[\s,;]+/) : []
|
|
}
|
|
};
|
|
export {
|
|
APPIUM_MASKING_HEADER,
|
|
BASE_64_REGEX,
|
|
BASE_64_SAFE_STRING_TO_PROCESS_LENGTH,
|
|
BidiHandler,
|
|
CommandRuntimeOptions,
|
|
DEFAULTS,
|
|
ELEMENT_KEY,
|
|
SHADOW_ELEMENT_KEY,
|
|
WebDriver,
|
|
command_default as command,
|
|
node_default as default,
|
|
getEnvironmentVars,
|
|
getPrototype,
|
|
initiateBidi,
|
|
localTypes_exports as local,
|
|
parseBidiMessage,
|
|
remoteTypes_exports as remote
|
|
};
|