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>
9175 lines
389 KiB
JavaScript
9175 lines
389 KiB
JavaScript
var __defProp = Object.defineProperty;
|
|
var __export = (target, all) => {
|
|
for (var name in all)
|
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
};
|
|
|
|
// src/node.ts
|
|
import os from "node:os";
|
|
import fs8 from "node:fs";
|
|
import process2 from "node:process";
|
|
|
|
// src/node/downloadFile.ts
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import JSZip from "jszip";
|
|
import logger from "@wdio/logger";
|
|
var log = logger("webdriverio");
|
|
async function downloadFile(fileName, targetDirectory) {
|
|
if (typeof fileName !== "string" || typeof targetDirectory !== "string") {
|
|
throw new Error("number or type of arguments don't agree with downloadFile command");
|
|
}
|
|
if (typeof this.download !== "function") {
|
|
throw new Error(`The downloadFile command is not available in ${this.capabilities.browserName} and only available when using Selenium Grid`);
|
|
}
|
|
const response = await this.download(fileName);
|
|
const base64Content = response.contents;
|
|
if (!targetDirectory.endsWith("/")) {
|
|
targetDirectory += "/";
|
|
}
|
|
fs.mkdirSync(targetDirectory, { recursive: true });
|
|
const zipFilePath = path.join(targetDirectory, `${fileName}.zip`);
|
|
const binaryString = atob(base64Content);
|
|
const bytes = Uint8Array.from(binaryString, (char) => char.charCodeAt(0));
|
|
fs.writeFileSync(zipFilePath, bytes);
|
|
const zipData = fs.readFileSync(zipFilePath);
|
|
const filesData = [];
|
|
try {
|
|
const zip2 = await JSZip.loadAsync(zipData);
|
|
const keys2 = Object.keys(zip2.files);
|
|
for (let i = 0; i < keys2.length; i++) {
|
|
const fileData = await zip2.files[keys2[i]].async("nodebuffer");
|
|
const dir = path.resolve(targetDirectory, keys2[i]);
|
|
fs.writeFileSync(dir, fileData);
|
|
log.info(`File extracted: ${keys2[i]}`);
|
|
filesData.push(dir);
|
|
}
|
|
} catch (error) {
|
|
log.error("Error unzipping file:", error);
|
|
}
|
|
return Promise.resolve({
|
|
files: filesData
|
|
});
|
|
}
|
|
|
|
// src/node/savePDF.ts
|
|
import fs3 from "node:fs";
|
|
import path3 from "node:path";
|
|
|
|
// src/node/utils.ts
|
|
import path2 from "node:path";
|
|
import fs2 from "node:fs/promises";
|
|
async function assertDirectoryExists(filepath) {
|
|
const exist = await fs2.access(path2.dirname(filepath)).then(() => true, () => false);
|
|
if (!exist) {
|
|
throw new Error(`directory (${path2.dirname(filepath)}) doesn't exist`);
|
|
}
|
|
}
|
|
|
|
// src/node/savePDF.ts
|
|
async function savePDF(filepath, options) {
|
|
if (typeof filepath !== "string" || !filepath.endsWith(".pdf")) {
|
|
throw new Error('savePDF expects a filepath of type string and ".pdf" file ending');
|
|
}
|
|
const absoluteFilepath = path3.resolve(filepath);
|
|
await assertDirectoryExists(absoluteFilepath);
|
|
const pdf = await this.printPage(
|
|
options?.orientation,
|
|
options?.scale,
|
|
options?.background,
|
|
options?.width,
|
|
options?.height,
|
|
options?.top,
|
|
options?.bottom,
|
|
options?.left,
|
|
options?.right,
|
|
options?.shrinkToFit,
|
|
options?.pageRanges
|
|
);
|
|
const page = Buffer.from(pdf, "base64");
|
|
fs3.writeFileSync(absoluteFilepath, page);
|
|
return page;
|
|
}
|
|
|
|
// src/node/saveRecordingScreen.ts
|
|
import fs4 from "node:fs";
|
|
import path4 from "node:path";
|
|
async function saveRecordingScreen(filepath) {
|
|
if (typeof filepath !== "string") {
|
|
throw new Error("saveRecordingScreen expects a filepath");
|
|
}
|
|
const absoluteFilepath = path4.resolve(filepath);
|
|
await assertDirectoryExists(absoluteFilepath);
|
|
const videoBuffer = await this.stopRecordingScreen();
|
|
const video = Buffer.from(videoBuffer, "base64");
|
|
fs4.writeFileSync(absoluteFilepath, video);
|
|
return video;
|
|
}
|
|
|
|
// src/node/uploadFile.ts
|
|
import fs5 from "node:fs";
|
|
import path5 from "node:path";
|
|
import archiver from "archiver";
|
|
async function uploadFile(localPath) {
|
|
if (typeof localPath !== "string") {
|
|
throw new Error("number or type of arguments don't agree with uploadFile command");
|
|
}
|
|
if (typeof this.file !== "function") {
|
|
throw new Error(`The uploadFile command is not available in ${this.capabilities.browserName}`);
|
|
}
|
|
const zipData = [];
|
|
const source = fs5.createReadStream(localPath);
|
|
return new Promise((resolve, reject) => {
|
|
archiver("zip").on("error", (err) => reject(err)).on("data", (data) => zipData.push(data)).on("end", () => this.file(Buffer.concat(zipData).toString("base64")).then((localPath2) => resolve(localPath2), reject)).append(source, { name: path5.basename(localPath) }).finalize();
|
|
});
|
|
}
|
|
|
|
// src/node/saveScreenshot.ts
|
|
import fs6 from "node:fs/promises";
|
|
import path6 from "node:path";
|
|
import { getBrowserObject } from "@wdio/utils";
|
|
|
|
// src/session/context.ts
|
|
import logger2 from "@wdio/logger";
|
|
|
|
// src/environment.ts
|
|
var isNode = !!(typeof process !== "undefined" && process.version);
|
|
var environment = {
|
|
value: {
|
|
get readFileSync() {
|
|
throw new Error("Can't read files form file system in this environment");
|
|
},
|
|
get downloadFile() {
|
|
throw new Error("The `downloadFile` command is not available in this environment");
|
|
},
|
|
get savePDF() {
|
|
throw new Error("The `savePDF` command is not available in this environment");
|
|
},
|
|
get saveRecordingScreen() {
|
|
throw new Error("The `saveRecordingScreen` command is not available in this environment");
|
|
},
|
|
get uploadFile() {
|
|
throw new Error("The `uploadFile` command is not available in this environment");
|
|
},
|
|
get saveScreenshot() {
|
|
throw new Error("The `saveScreenshot` command for WebdriverIO.Browser is not available in this environment");
|
|
},
|
|
get saveElementScreenshot() {
|
|
throw new Error("The `saveScreenshot` command for WebdriverIO.Element is not available in this environment");
|
|
},
|
|
get osType() {
|
|
return () => "browser";
|
|
},
|
|
get variables() {
|
|
return isNode ? process.env : {};
|
|
}
|
|
}
|
|
};
|
|
|
|
// src/session/session.ts
|
|
var sessionManager = /* @__PURE__ */ new Map();
|
|
var listenerRegisteredSession = /* @__PURE__ */ new Set();
|
|
var SessionManager = class {
|
|
#browser;
|
|
#scope;
|
|
/**
|
|
* SessionManager constructor
|
|
* Logic in here should be executed for all session singletons, e.g. remove instance
|
|
* of itself when a session was deleted.
|
|
* @param browser WebdriverIO.Browser
|
|
* @param scope scope of the session manager, e.g. context, network etc.
|
|
*/
|
|
constructor(browser, scope) {
|
|
this.#browser = browser;
|
|
this.#scope = scope;
|
|
const registrationId = `${this.#browser.sessionId}-${this.#scope}`;
|
|
if (!listenerRegisteredSession.has(registrationId)) {
|
|
this.#browser.on("command", this.#onCommandListener);
|
|
listenerRegisteredSession.add(registrationId);
|
|
}
|
|
}
|
|
#onCommandListener = this.#onCommand.bind(this);
|
|
#onCommand(ev) {
|
|
if (ev.command === "deleteSession") {
|
|
const sessionManagerInstances = sessionManager.get(this.#scope);
|
|
const sessionManagerInstance = sessionManagerInstances?.get(this.#browser);
|
|
if (sessionManagerInstance && sessionManagerInstances) {
|
|
sessionManagerInstance.removeListeners();
|
|
sessionManagerInstances.delete(this.#browser);
|
|
}
|
|
}
|
|
}
|
|
removeListeners() {
|
|
this.#browser.off("command", this.#onCommandListener);
|
|
}
|
|
initialize() {
|
|
return void 0;
|
|
}
|
|
/**
|
|
* check if session manager should be enabled, if
|
|
*/
|
|
isEnabled() {
|
|
return (
|
|
// we are in a Bidi session
|
|
this.#browser.isBidi && // we are not running unit tests
|
|
!environment.value.variables.WDIO_UNIT_TESTS
|
|
);
|
|
}
|
|
static getSessionManager(browser, Manager) {
|
|
const scope = Manager.name;
|
|
let sessionManagerInstances = sessionManager.get(scope);
|
|
if (!sessionManagerInstances) {
|
|
sessionManagerInstances = /* @__PURE__ */ new Map();
|
|
sessionManager.set(scope, sessionManagerInstances);
|
|
}
|
|
let sessionManagerInstance = sessionManagerInstances.get(browser);
|
|
if (!sessionManagerInstance) {
|
|
sessionManagerInstance = new Manager(browser);
|
|
sessionManagerInstances.set(browser, sessionManagerInstance);
|
|
}
|
|
return sessionManagerInstance;
|
|
}
|
|
};
|
|
|
|
// src/utils/mobile.ts
|
|
function getNativeContext({ capabilities, isMobile }) {
|
|
if (!capabilities || typeof capabilities !== "object" || !isMobile) {
|
|
return false;
|
|
}
|
|
const isBrowserNameFalse = !!capabilities?.browserName === false;
|
|
const isAutoWebviewFalse = !// @ts-expect-error
|
|
(capabilities?.autoWebview === true || capabilities["appium:autoWebview"] === true || capabilities["appium:options"]?.autoWebview === true || capabilities["lt:options"]?.autoWebview === true);
|
|
return isBrowserNameFalse && isMobile && isAutoWebviewFalse;
|
|
}
|
|
function getMobileContext({ capabilities, isAndroid, isNativeContext }) {
|
|
return isNativeContext ? "NATIVE_APP" : (
|
|
// Android webviews are always WEBVIEW_<package_name>, Chrome will always be CHROMIUM
|
|
// We can only determine it for Android and Chrome, for all other, including iOS, we return undefined
|
|
isAndroid && capabilities?.browserName?.toLowerCase() === "chrome" ? "CHROMIUM" : void 0
|
|
);
|
|
}
|
|
function calculateAndroidPinchAndZoomSpeed({ browser, duration, scale }) {
|
|
const deviceScreenSize = (browser.capabilities?.deviceScreenSize || "1080x2400").split("x").reduce((a, b) => a * b);
|
|
const baseDistance = Math.sqrt(deviceScreenSize);
|
|
const gestureDistance = Math.max(baseDistance * Math.abs(scale), baseDistance * 0.1);
|
|
const durationSeconds = duration / 1e3;
|
|
return Math.floor(gestureDistance / durationSeconds);
|
|
}
|
|
function validatePinchAndZoomOptions({ browser, gesture, options }) {
|
|
if (typeof options !== "undefined" && (typeof options !== "object" || Array.isArray(options))) {
|
|
throw new TypeError("Options must be an object");
|
|
}
|
|
const DEFAULT_SCALE = 0.5;
|
|
const DEFAULT_DURATION = browser.isIOS ? 1.5 : 1500;
|
|
const MIN_SCALE = 0;
|
|
const MAX_SCALE = 1;
|
|
const MIN_DURATION_MS = 500;
|
|
const MAX_DURATION_MS = 1e4;
|
|
const { scale: scaleOption, duration: durationOption } = options;
|
|
const scale = typeof scaleOption === "number" ? scaleOption >= MIN_SCALE && scaleOption <= MAX_SCALE ? scaleOption : (() => {
|
|
throw new Error(`The 'scale' option must be a number between ${MIN_SCALE} and ${MAX_SCALE}`);
|
|
})() : DEFAULT_SCALE;
|
|
const duration = typeof durationOption === "number" ? durationOption >= MIN_DURATION_MS && durationOption <= MAX_DURATION_MS ? browser.isIOS ? durationOption / 1e3 : durationOption : (() => {
|
|
throw new Error(`The 'duration' option must be between ${MIN_DURATION_MS} and ${MAX_DURATION_MS} ms (${MIN_DURATION_MS / 1e3} and ${MAX_DURATION_MS / 1e3} seconds)`);
|
|
})() : DEFAULT_DURATION;
|
|
return {
|
|
duration,
|
|
scale: browser.isIOS && gesture === "zoom" ? scale * 10 : scale
|
|
};
|
|
}
|
|
|
|
// src/session/context.ts
|
|
var log2 = logger2("webdriverio:context");
|
|
var COMMANDS_REQUIRING_RESET = ["deleteSession", "refresh", "switchToParentFrame"];
|
|
function getContextManager(browser) {
|
|
return SessionManager.getSessionManager(browser, ContextManager);
|
|
}
|
|
var ContextManager = class _ContextManager extends SessionManager {
|
|
#browser;
|
|
#currentContext;
|
|
#mobileContext;
|
|
#isNativeContext;
|
|
#getContextSupport = true;
|
|
#currentWindowHandle;
|
|
#onCommandResultBidiAndClassicListener;
|
|
#onCommandListener;
|
|
#onCommandResultMobileListener;
|
|
#navigationStartedListener;
|
|
constructor(browser) {
|
|
super(browser, _ContextManager.name);
|
|
this.#browser = browser;
|
|
const capabilities = this.#browser.capabilities;
|
|
this.#isNativeContext = getNativeContext({ capabilities, isMobile: this.#browser.isMobile });
|
|
this.#mobileContext = getMobileContext({
|
|
capabilities,
|
|
isAndroid: this.#browser.isAndroid,
|
|
isNativeContext: this.#isNativeContext
|
|
});
|
|
this.#onCommandResultBidiAndClassicListener = this.#onCommandResultBidiAndClassic.bind(this);
|
|
this.#onCommandListener = this.#onCommand.bind(this);
|
|
this.#onCommandResultMobileListener = this.#onCommandResultMobile.bind(this);
|
|
this.#navigationStartedListener = this.#navigationStarted.bind(this);
|
|
this.#browser.on("result", this.#onCommandResultBidiAndClassicListener);
|
|
if (!this.isEnabled() && !this.#browser.isMobile) {
|
|
return;
|
|
}
|
|
this.#browser.on("command", this.#onCommandListener);
|
|
if (this.#browser.isMobile) {
|
|
this.#browser.on("result", this.#onCommandResultMobileListener);
|
|
} else {
|
|
this.#browser.sessionSubscribe({
|
|
events: ["browsingContext.navigationStarted"]
|
|
});
|
|
this.#browser.on("browsingContext.navigationStarted", this.#navigationStartedListener);
|
|
}
|
|
}
|
|
removeListeners() {
|
|
super.removeListeners();
|
|
this.#browser.off("result", this.#onCommandResultBidiAndClassicListener);
|
|
this.#browser.off("command", this.#onCommandListener);
|
|
if (this.#browser.isMobile) {
|
|
this.#browser.off("result", this.#onCommandResultMobileListener);
|
|
} else {
|
|
this.#browser.off("browsingContext.navigationStarted", this.#navigationStartedListener);
|
|
}
|
|
}
|
|
async #navigationStarted(nav) {
|
|
if (!this.#currentContext || nav.context === this.#currentContext) {
|
|
return;
|
|
}
|
|
const { contexts } = await this.#browser.browsingContextGetTree({});
|
|
const hasContext = this.findContext(this.#currentContext, contexts, "byContextId");
|
|
const newContext = contexts.find((context) => context.context === nav.context);
|
|
if (!hasContext && newContext) {
|
|
this.setCurrentContext(newContext.context);
|
|
await this.#browser.switchToWindow(this.#currentContext);
|
|
return;
|
|
}
|
|
}
|
|
#onCommandResultBidiAndClassic(event) {
|
|
if (event.command === "closeWindow") {
|
|
this.#currentWindowHandle = void 0;
|
|
const windowHandles = event.result.value || [];
|
|
if (windowHandles.length === 0) {
|
|
throw new Error("All window handles were removed, causing WebdriverIO to close the session.");
|
|
}
|
|
this.#currentContext = windowHandles[0];
|
|
return this.#browser.switchToWindow(this.#currentContext);
|
|
}
|
|
if (event.command === "getWindowHandle") {
|
|
const windowHandle = event.result.value || void 0;
|
|
this.#currentWindowHandle = windowHandle;
|
|
}
|
|
if (event.command === "switchToWindow") {
|
|
const err = event.result.error || void 0;
|
|
if (!err) {
|
|
const windowHandle = event.body.handle || void 0;
|
|
this.#currentWindowHandle = windowHandle;
|
|
}
|
|
}
|
|
}
|
|
#onCommand(event) {
|
|
if (event.command === "switchToParentFrame") {
|
|
if (!this.#currentContext) {
|
|
return;
|
|
}
|
|
return this.#browser.browsingContextGetTree({}).then(({ contexts }) => {
|
|
const parentContext = this.findParentContext(this.#currentContext, contexts);
|
|
if (!parentContext) {
|
|
return;
|
|
}
|
|
this.setCurrentContext(parentContext.context);
|
|
});
|
|
}
|
|
if (event.command === "switchToWindow") {
|
|
this.setCurrentContext(event.body.handle);
|
|
}
|
|
if (COMMANDS_REQUIRING_RESET.includes(event.command)) {
|
|
this.#currentContext = void 0;
|
|
}
|
|
if (this.#browser.isMobile && event.command === "switchAppiumContext") {
|
|
this.#mobileContext = event.body.name;
|
|
}
|
|
}
|
|
#onCommandResultMobile(event) {
|
|
if (event.command === "getAppiumContext") {
|
|
this.setCurrentContext(event.result.value);
|
|
}
|
|
if (event.command === "switchAppiumContext" && event.result.value === null && this.#mobileContext) {
|
|
this.setCurrentContext(this.#mobileContext);
|
|
}
|
|
}
|
|
/**
|
|
* set context at the start of the session
|
|
*/
|
|
async initialize() {
|
|
if (environment.value.variables.WDIO_UNIT_TESTS) {
|
|
return "";
|
|
}
|
|
if (this.#browser.isMobile && !this.#isNativeContext && !this.#mobileContext && this.#getContextSupport) {
|
|
const context = await this.#browser.getContext().catch((err) => {
|
|
log2.warn(
|
|
`Error getting context: ${err}
|
|
|
|
WebDriver capabilities: ${JSON.stringify(this.#browser.capabilities)}
|
|
Requested WebDriver capabilities: ${JSON.stringify(this.#browser.requestedCapabilities)}`
|
|
);
|
|
if (err.message.includes("Request failed with status code 405")) {
|
|
this.#getContextSupport = false;
|
|
}
|
|
return void 0;
|
|
});
|
|
this.#mobileContext = typeof context === "string" ? context : typeof context === "object" ? context.id : void 0;
|
|
}
|
|
const windowHandle = this.#mobileContext || await this.#browser.getWindowHandle();
|
|
this.setCurrentContext(windowHandle);
|
|
return windowHandle;
|
|
}
|
|
setCurrentContext(context) {
|
|
this.#currentContext = context;
|
|
if (this.#browser.isMobile) {
|
|
this.#isNativeContext = context ? context === "NATIVE_APP" : this.#isNativeContext;
|
|
this.#mobileContext = context || void 0;
|
|
}
|
|
}
|
|
async getCurrentContext() {
|
|
if (!this.#currentContext) {
|
|
return this.initialize();
|
|
}
|
|
return this.#currentContext;
|
|
}
|
|
/**
|
|
* Sets the cached current window handle value.
|
|
* @param handle current window handle to set
|
|
*/
|
|
setCurrentWindowHandle(handle) {
|
|
this.#currentWindowHandle = handle;
|
|
}
|
|
/**
|
|
* Returns the cached window handle.
|
|
*
|
|
* @returns the current window handle, or undefined if the current window is closed.
|
|
*/
|
|
getCurrentWindowHandle() {
|
|
return this.#currentWindowHandle;
|
|
}
|
|
get isNativeContext() {
|
|
return this.#isNativeContext;
|
|
}
|
|
get mobileContext() {
|
|
return this.#mobileContext;
|
|
}
|
|
/**
|
|
* Get the flat context tree for the current session
|
|
* @returns a flat list of all contexts in the current session
|
|
*/
|
|
async getFlatContextTree() {
|
|
const tree = await this.#browser.browsingContextGetTree({});
|
|
const mapContext = (context) => [
|
|
context.context,
|
|
...(context.children || []).map(mapContext).flat(Infinity)
|
|
];
|
|
const allContexts = tree.contexts.map(mapContext).flat(Infinity).reduce((acc, ctx) => {
|
|
const context = this.findContext(ctx, tree.contexts, "byContextId");
|
|
acc[ctx] = context;
|
|
return acc;
|
|
}, {});
|
|
return allContexts;
|
|
}
|
|
/**
|
|
* Find the parent context of a given context id
|
|
* @param contextId the context id you want to find the parent of
|
|
* @param contexts the list of contexts to search through returned from `browsingContextGetTree`
|
|
* @returns the parent context of the context with the given id
|
|
*/
|
|
findParentContext(contextId, contexts) {
|
|
for (const context of contexts) {
|
|
if (context.children?.some((child) => child.context === contextId)) {
|
|
return context;
|
|
}
|
|
if (Array.isArray(context.children) && context.children.length > 0) {
|
|
const result = this.findParentContext(contextId, context.children);
|
|
if (result) {
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
return void 0;
|
|
}
|
|
/**
|
|
* Find a context by URL or ID
|
|
* @param urlOrId The URL or ID of the context to find
|
|
* @param contexts The list of contexts to search through returned from `browsingContextGetTree`
|
|
* @param matcherType The type of matcher to use to find the context
|
|
* @returns The context with the given URL or ID
|
|
*/
|
|
findContext(urlOrId, contexts, matcherType) {
|
|
const matcher = {
|
|
byUrl,
|
|
byUrlContaining,
|
|
byContextId
|
|
}[matcherType];
|
|
for (const context of contexts || []) {
|
|
if (matcher(context, urlOrId)) {
|
|
return context;
|
|
}
|
|
if (Array.isArray(context.children) && context.children.length > 0) {
|
|
const result = this.findContext(urlOrId, context.children, matcherType);
|
|
if (result) {
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
return void 0;
|
|
}
|
|
};
|
|
function byUrl(context, url2) {
|
|
return context.url === url2;
|
|
}
|
|
function byUrlContaining(context, url2) {
|
|
return context.url.includes(url2);
|
|
}
|
|
function byContextId(context, contextId) {
|
|
return context.context === contextId;
|
|
}
|
|
|
|
// src/node/saveScreenshot.ts
|
|
async function saveScreenshot(filepath, options) {
|
|
if (typeof filepath !== "string") {
|
|
throw new Error('saveScreenshot expects a filepath of type string and ".png" file ending');
|
|
}
|
|
const absoluteFilepath = path6.resolve(filepath);
|
|
await assertDirectoryExists(absoluteFilepath);
|
|
const screenBuffer = this.isBidi ? await takeScreenshotBidi.call(this, filepath, options) : await takeScreenshotClassic.call(this, filepath, options);
|
|
const screenshot = Buffer.from(screenBuffer, "base64");
|
|
await fs6.writeFile(absoluteFilepath, screenshot);
|
|
return screenshot;
|
|
}
|
|
function takeScreenshotClassic(filepath, options) {
|
|
if (options) {
|
|
throw new Error("saveScreenshot does not support options in WebDriver Classic mode");
|
|
}
|
|
const fileExtension = path6.extname(filepath).slice(1);
|
|
if (fileExtension !== "png") {
|
|
throw new Error('Invalid file extension, use ".png" for PNG format');
|
|
}
|
|
return this.takeScreenshot();
|
|
}
|
|
async function takeScreenshotBidi(filepath, options) {
|
|
const browser = getBrowserObject(this);
|
|
const contextManager = getContextManager(browser);
|
|
const context = await contextManager.getCurrentContext();
|
|
const tree = await this.browsingContextGetTree({});
|
|
const origin = options?.fullPage ? "document" : "viewport";
|
|
const givenFormat = options?.format || path6.extname(filepath).slice(1);
|
|
const imageFormat = givenFormat === "png" ? "image/png" : givenFormat === "jpeg" || givenFormat === "jpg" ? "image/jpeg" : void 0;
|
|
if (!imageFormat) {
|
|
throw new Error(`Invalid image format, use 'png', 'jpg' or 'jpeg', got '${options?.format}'`);
|
|
}
|
|
if (imageFormat === "image/jpeg" && path6.extname(filepath) !== ".jpeg" && path6.extname(filepath) !== ".jpg") {
|
|
throw new Error('Invalid file extension, use ".jpeg" or ".jpg" for JPEG format');
|
|
} else if (imageFormat === "image/png" && path6.extname(filepath) !== ".png") {
|
|
throw new Error('Invalid file extension, use ".png" for PNG format');
|
|
}
|
|
const quality = typeof options?.quality === "number" ? options.quality / 100 : void 0;
|
|
if (typeof options?.quality === "number" && (options?.quality < 0 || options?.quality > 100)) {
|
|
throw new Error(`Invalid quality, use a number between 0 and 100, got '${options?.quality}'`);
|
|
}
|
|
if (typeof options?.quality === "number" && imageFormat !== "image/jpeg") {
|
|
throw new Error('Invalid option "quality" for PNG format');
|
|
}
|
|
const format = {
|
|
type: imageFormat,
|
|
quality
|
|
};
|
|
const clip = options?.clip ? {
|
|
type: "box",
|
|
x: options.clip.x,
|
|
y: options.clip.y,
|
|
width: options.clip.width,
|
|
height: options.clip.height
|
|
} : void 0;
|
|
if (clip) {
|
|
if (typeof clip.x !== "number" || typeof clip.y !== "number" || typeof clip.width !== "number" || typeof clip.height !== "number") {
|
|
throw new Error("Invalid clip, use an object with x, y, width and height properties");
|
|
}
|
|
}
|
|
const { data } = contextManager.findParentContext(context, tree.contexts) ? await browser.$("html").getElement().then(
|
|
(el) => this.takeElementScreenshot(el.elementId).then((data2) => ({ data: data2 }))
|
|
) : await this.browsingContextCaptureScreenshot({ context, origin, format, clip });
|
|
return data;
|
|
}
|
|
|
|
// src/node/saveElementScreenshot.ts
|
|
import fs7 from "node:fs/promises";
|
|
import path7 from "node:path";
|
|
async function saveElementScreenshot(filepath) {
|
|
if (typeof filepath !== "string" || !filepath.endsWith(".png")) {
|
|
throw new Error('saveScreenshot expects a filepath of type string and ".png" file ending');
|
|
}
|
|
const absoluteFilepath = path7.resolve(filepath);
|
|
await assertDirectoryExists(absoluteFilepath);
|
|
const screenBuffer = await this.takeElementScreenshot(this.elementId);
|
|
const screenshot = Buffer.from(screenBuffer, "base64");
|
|
await fs7.writeFile(absoluteFilepath, screenshot);
|
|
return screenshot;
|
|
}
|
|
|
|
// src/index.ts
|
|
import logger30 from "@wdio/logger";
|
|
import WebDriver, { DEFAULTS } from "webdriver";
|
|
import { validateConfig } from "@wdio/config";
|
|
import { enableFileLogging, wrapCommand as wrapCommand3, isBidi } from "@wdio/utils";
|
|
|
|
// src/multiremote.ts
|
|
import zip from "lodash.zip";
|
|
import clone2 from "lodash.clonedeep";
|
|
import { webdriverMonad as webdriverMonad2, wrapCommand as wrapCommand2 } from "@wdio/utils";
|
|
|
|
// src/middlewares.ts
|
|
import { ELEMENT_KEY as ELEMENT_KEY21 } from "webdriver";
|
|
import { getBrowserObject as getBrowserObject39 } from "@wdio/utils";
|
|
|
|
// src/utils/implicitWait.ts
|
|
import logger3 from "@wdio/logger";
|
|
import { getBrowserObject as getBrowserObject2 } from "@wdio/utils";
|
|
var log3 = logger3("webdriverio");
|
|
async function implicitWait(currentElement, commandName) {
|
|
const browser = getBrowserObject2(currentElement);
|
|
const skipForMobileScroll = browser.isMobile && await browser.isNativeContext && (commandName === "scrollIntoView" || commandName === "tap");
|
|
if (!currentElement.elementId && !/(waitUntil|waitFor|isExisting|is?\w+Displayed|is?\w+Clickable)/.test(commandName) && !skipForMobileScroll) {
|
|
log3.debug(
|
|
`command ${commandName} was called on an element ("${currentElement.selector}") that wasn't found, waiting for it...`
|
|
);
|
|
try {
|
|
await currentElement.waitForExist();
|
|
return currentElement.parent.$(currentElement.selector).getElement();
|
|
} catch {
|
|
if (currentElement.selector.toString().includes("this.previousElementSibling")) {
|
|
throw new Error(
|
|
`Can't call ${commandName} on previous element of element with selector "${currentElement.parent.selector}" because sibling wasn't found`
|
|
);
|
|
}
|
|
if (currentElement.selector.toString().includes("this.nextElementSibling")) {
|
|
throw new Error(
|
|
`Can't call ${commandName} on next element of element with selector "${currentElement.parent.selector}" because sibling wasn't found`
|
|
);
|
|
}
|
|
if (currentElement.selector.toString().includes("this.parentElement")) {
|
|
throw new Error(
|
|
`Can't call ${commandName} on parent element of element with selector "${currentElement.parent.selector}" because it wasn't found`
|
|
);
|
|
}
|
|
throw new Error(
|
|
`Can't call ${commandName} on element with selector "${currentElement.selector}" because element wasn't found`
|
|
);
|
|
}
|
|
}
|
|
return currentElement;
|
|
}
|
|
|
|
// src/utils/refetchElement.ts
|
|
async function refetchElement(currentElement, commandName) {
|
|
const selectors = [];
|
|
while (currentElement.elementId && currentElement.parent) {
|
|
selectors.push({ selector: currentElement.selector, index: currentElement.index || 0 });
|
|
currentElement = currentElement.parent;
|
|
}
|
|
selectors.reverse();
|
|
const length = selectors.length;
|
|
return selectors.reduce(async (elementPromise, { selector, index }, currentIndex) => {
|
|
const resolvedElement = await elementPromise;
|
|
let nextElement2 = index > 0 ? await resolvedElement.$$(selector)[index]?.getElement() : null;
|
|
nextElement2 = nextElement2 || await resolvedElement.$(selector).getElement();
|
|
return await implicitWait(nextElement2, currentIndex + 1 < length ? "$" : commandName);
|
|
}, Promise.resolve(currentElement));
|
|
}
|
|
|
|
// src/utils/index.ts
|
|
import cssValue from "css-value";
|
|
import rgb2hex from "rgb2hex";
|
|
import GraphemeSplitter from "grapheme-splitter";
|
|
import logger29 from "@wdio/logger";
|
|
import isPlainObject from "is-plain-obj";
|
|
import { ELEMENT_KEY as ELEMENT_KEY20 } from "webdriver";
|
|
import { UNICODE_CHARACTERS as UNICODE_CHARACTERS2, asyncIterators, getBrowserObject as getBrowserObject38 } from "@wdio/utils";
|
|
|
|
// src/commands/browser.ts
|
|
var browser_exports = {};
|
|
__export(browser_exports, {
|
|
$: () => $,
|
|
$$: () => $$,
|
|
SESSION_MOCKS: () => SESSION_MOCKS,
|
|
action: () => action,
|
|
actions: () => actions,
|
|
addInitScript: () => addInitScript,
|
|
call: () => call,
|
|
custom$: () => custom$,
|
|
custom$$: () => custom$$,
|
|
debug: () => debug,
|
|
deepLink: () => deepLink,
|
|
deleteCookies: () => deleteCookies,
|
|
downloadFile: () => downloadFile2,
|
|
emulate: () => emulate,
|
|
execute: () => execute,
|
|
executeAsync: () => executeAsync,
|
|
getContext: () => getContext,
|
|
getContexts: () => getContexts,
|
|
getCookies: () => getCookies,
|
|
getPuppeteer: () => getPuppeteer,
|
|
getWindowSize: () => getWindowSize,
|
|
keys: () => keys,
|
|
mock: () => mock,
|
|
mockClearAll: () => mockClearAll,
|
|
mockRestoreAll: () => mockRestoreAll,
|
|
newWindow: () => newWindow,
|
|
pause: () => pause,
|
|
react$: () => react$,
|
|
react$$: () => react$$,
|
|
relaunchActiveApp: () => relaunchActiveApp,
|
|
reloadSession: () => reloadSession,
|
|
restore: () => restore,
|
|
savePDF: () => savePDF2,
|
|
saveRecordingScreen: () => saveRecordingScreen2,
|
|
saveScreenshot: () => saveScreenshot2,
|
|
scroll: () => scroll,
|
|
setCookies: () => setCookies,
|
|
setTimeout: () => setTimeout2,
|
|
setViewport: () => setViewport,
|
|
setWindowSize: () => setWindowSize,
|
|
swipe: () => swipe,
|
|
switchContext: () => switchContext,
|
|
switchFrame: () => switchFrame,
|
|
switchWindow: () => switchWindow,
|
|
tap: () => tap,
|
|
throttle: () => throttle,
|
|
throttleCPU: () => throttleCPU,
|
|
throttleNetwork: () => throttleNetwork,
|
|
touchAction: () => touchAction2,
|
|
uploadFile: () => uploadFile2,
|
|
url: () => url,
|
|
waitUntil: () => waitUntil
|
|
});
|
|
|
|
// src/utils/getElementObject.ts
|
|
import { webdriverMonad, wrapCommand } from "@wdio/utils";
|
|
import clone from "lodash.clonedeep";
|
|
import { ELEMENT_KEY } from "webdriver";
|
|
import { getBrowserObject as getBrowserObject3 } from "@wdio/utils";
|
|
var WebDriverError = class extends Error {
|
|
constructor(obj) {
|
|
const { name, stack } = obj;
|
|
const { error, stacktrace } = obj;
|
|
super(error || name || "");
|
|
Object.assign(this, {
|
|
message: obj.message,
|
|
stack: stacktrace || stack
|
|
});
|
|
}
|
|
};
|
|
function getElement(selector, res, props = { isReactElement: false, isShadowElement: false }) {
|
|
const browser = getBrowserObject3(this);
|
|
const browserCommandKeys = Object.keys(browser_exports);
|
|
const propertiesObject = {
|
|
/**
|
|
* filter out browser commands from object
|
|
*/
|
|
...Object.entries(clone(browser.__propertiesObject__)).reduce((commands, [name, descriptor]) => {
|
|
if (!browserCommandKeys.includes(name)) {
|
|
commands[name] = descriptor;
|
|
}
|
|
return commands;
|
|
}, {}),
|
|
...getPrototype("element"),
|
|
scope: { value: "element" }
|
|
};
|
|
propertiesObject.emit = { value: this.emit.bind(this) };
|
|
const element = webdriverMonad(this.options, (client) => {
|
|
const elementId = getElementFromResponse(res);
|
|
if (elementId) {
|
|
client.elementId = elementId;
|
|
client[ELEMENT_KEY] = elementId;
|
|
if (res && this.isBidi && "locator" in res) {
|
|
client.locator = res.locator;
|
|
}
|
|
} else {
|
|
client.error = res;
|
|
}
|
|
if (selector) {
|
|
client.selector = selector;
|
|
}
|
|
client.parent = this;
|
|
client.isReactElement = props.isReactElement;
|
|
client.isShadowElement = props.isShadowElement;
|
|
return client;
|
|
}, propertiesObject);
|
|
const elementInstance = element(this.sessionId, elementErrorHandler(wrapCommand));
|
|
const origAddCommand = elementInstance.addCommand.bind(elementInstance);
|
|
elementInstance.addCommand = (name, fn) => {
|
|
browser.__propertiesObject__[name] = { value: fn };
|
|
origAddCommand(name, fn);
|
|
};
|
|
return elementInstance;
|
|
}
|
|
var getElements = function getElements2(selector, elemResponse, props = { isReactElement: false, isShadowElement: false }) {
|
|
const browser = getBrowserObject3(this);
|
|
const browserCommandKeys = Object.keys(browser_exports);
|
|
const propertiesObject = {
|
|
/**
|
|
* filter out browser commands from object
|
|
*/
|
|
...Object.entries(clone(browser.__propertiesObject__)).reduce((commands, [name, descriptor]) => {
|
|
if (!browserCommandKeys.includes(name)) {
|
|
commands[name] = descriptor;
|
|
}
|
|
return commands;
|
|
}, {}),
|
|
...getPrototype("element")
|
|
};
|
|
if (elemResponse.length === 0) {
|
|
return [];
|
|
}
|
|
const elements = [elemResponse].flat(1).map((res, i) => {
|
|
if (res.selector && "$$" in res) {
|
|
return res;
|
|
}
|
|
propertiesObject.scope = { value: "element" };
|
|
propertiesObject.emit = { value: this.emit.bind(this) };
|
|
const element = webdriverMonad(this.options, (client) => {
|
|
const elementId = getElementFromResponse(res);
|
|
if (elementId) {
|
|
client.elementId = elementId;
|
|
client[ELEMENT_KEY] = elementId;
|
|
if (res && this.isBidi && "locator" in res) {
|
|
client.locator = res.locator;
|
|
}
|
|
} else {
|
|
res = res;
|
|
client.error = res instanceof Error ? res : new WebDriverError(res);
|
|
}
|
|
client.selector = Array.isArray(selector) ? selector[i].selector : selector;
|
|
client.parent = this;
|
|
client.index = i;
|
|
client.isReactElement = props.isReactElement;
|
|
client.isShadowElement = props.isShadowElement;
|
|
return client;
|
|
}, propertiesObject);
|
|
const elementInstance = element(this.sessionId, elementErrorHandler(wrapCommand));
|
|
const origAddCommand = elementInstance.addCommand.bind(elementInstance);
|
|
elementInstance.addCommand = (name, fn) => {
|
|
browser.__propertiesObject__[name] = { value: fn };
|
|
origAddCommand(name, fn);
|
|
};
|
|
return elementInstance;
|
|
});
|
|
return elements;
|
|
};
|
|
|
|
// src/constants.ts
|
|
import { UNICODE_CHARACTERS, HOOK_DEFINITION } from "@wdio/utils";
|
|
var WDIO_DEFAULTS = {
|
|
/**
|
|
* allows to specify automation protocol
|
|
*/
|
|
automationProtocol: {
|
|
type: "string",
|
|
default: "webdriver",
|
|
validate: (param) => {
|
|
if (typeof param !== "string") {
|
|
throw new Error("automationProtocol should be a string");
|
|
}
|
|
if (typeof import.meta.resolve !== "function") {
|
|
return;
|
|
}
|
|
try {
|
|
import.meta.resolve(param);
|
|
} catch (err) {
|
|
const error = err instanceof Error ? err : new Error("unknown error");
|
|
throw new Error(`Couldn't find automation protocol "${param}": ${error.message}`);
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* capabilities of WebDriver sessions
|
|
*/
|
|
capabilities: {
|
|
type: "object",
|
|
validate: (param) => {
|
|
if (typeof param === "object") {
|
|
return true;
|
|
}
|
|
throw new Error('the "capabilities" options needs to be an object or a list of objects');
|
|
},
|
|
required: true
|
|
},
|
|
/**
|
|
* Shorten navigateTo command calls by setting a base url
|
|
*/
|
|
baseUrl: {
|
|
type: "string"
|
|
},
|
|
/**
|
|
* Default interval for all waitFor* commands
|
|
*/
|
|
waitforInterval: {
|
|
type: "number",
|
|
default: 100
|
|
},
|
|
/**
|
|
* Default timeout for all waitFor* commands
|
|
*/
|
|
waitforTimeout: {
|
|
type: "number",
|
|
default: 5e3
|
|
},
|
|
/**
|
|
* Hooks
|
|
*/
|
|
onReload: HOOK_DEFINITION,
|
|
beforeCommand: HOOK_DEFINITION,
|
|
afterCommand: HOOK_DEFINITION
|
|
};
|
|
var FF_REMOTE_DEBUG_ARG = "-remote-debugging-port";
|
|
var DEEP_SELECTOR = ">>>";
|
|
var ARIA_SELECTOR = "aria/";
|
|
var restoreFunctions = /* @__PURE__ */ new Map();
|
|
var Key = {
|
|
/**
|
|
* Special control key that works cross browser for Mac, where it's the command key, and for
|
|
* Windows or Linux, where it is the control key.
|
|
*/
|
|
Ctrl: "WDIO_CONTROL",
|
|
NULL: UNICODE_CHARACTERS.NULL,
|
|
Cancel: UNICODE_CHARACTERS.Cancel,
|
|
Help: UNICODE_CHARACTERS.Help,
|
|
Backspace: UNICODE_CHARACTERS.Backspace,
|
|
Tab: UNICODE_CHARACTERS.Tab,
|
|
Clear: UNICODE_CHARACTERS.Clear,
|
|
Return: UNICODE_CHARACTERS.Return,
|
|
Enter: UNICODE_CHARACTERS.Enter,
|
|
Shift: UNICODE_CHARACTERS.Shift,
|
|
Control: UNICODE_CHARACTERS.Control,
|
|
Alt: UNICODE_CHARACTERS.Alt,
|
|
Pause: UNICODE_CHARACTERS.Pause,
|
|
Escape: UNICODE_CHARACTERS.Escape,
|
|
Space: UNICODE_CHARACTERS.Space,
|
|
PageUp: UNICODE_CHARACTERS.PageUp,
|
|
PageDown: UNICODE_CHARACTERS.PageDown,
|
|
End: UNICODE_CHARACTERS.End,
|
|
Home: UNICODE_CHARACTERS.Home,
|
|
ArrowLeft: UNICODE_CHARACTERS.ArrowLeft,
|
|
ArrowUp: UNICODE_CHARACTERS.ArrowUp,
|
|
ArrowRight: UNICODE_CHARACTERS.ArrowRight,
|
|
ArrowDown: UNICODE_CHARACTERS.ArrowDown,
|
|
Insert: UNICODE_CHARACTERS.Insert,
|
|
Delete: UNICODE_CHARACTERS.Delete,
|
|
Semicolon: UNICODE_CHARACTERS.Semicolon,
|
|
Equals: UNICODE_CHARACTERS.Equals,
|
|
Numpad0: UNICODE_CHARACTERS["Numpad 0"],
|
|
Numpad1: UNICODE_CHARACTERS["Numpad 1"],
|
|
Numpad2: UNICODE_CHARACTERS["Numpad 2"],
|
|
Numpad3: UNICODE_CHARACTERS["Numpad 3"],
|
|
Numpad4: UNICODE_CHARACTERS["Numpad 4"],
|
|
Numpad5: UNICODE_CHARACTERS["Numpad 5"],
|
|
Numpad6: UNICODE_CHARACTERS["Numpad 6"],
|
|
Numpad7: UNICODE_CHARACTERS["Numpad 7"],
|
|
Numpad8: UNICODE_CHARACTERS["Numpad 8"],
|
|
Numpad9: UNICODE_CHARACTERS["Numpad 9"],
|
|
Multiply: UNICODE_CHARACTERS.Multiply,
|
|
Add: UNICODE_CHARACTERS.Add,
|
|
Separator: UNICODE_CHARACTERS.Separator,
|
|
Subtract: UNICODE_CHARACTERS.Subtract,
|
|
Decimal: UNICODE_CHARACTERS.Decimal,
|
|
Divide: UNICODE_CHARACTERS.Divide,
|
|
F1: UNICODE_CHARACTERS.F1,
|
|
F2: UNICODE_CHARACTERS.F2,
|
|
F3: UNICODE_CHARACTERS.F3,
|
|
F4: UNICODE_CHARACTERS.F4,
|
|
F5: UNICODE_CHARACTERS.F5,
|
|
F6: UNICODE_CHARACTERS.F6,
|
|
F7: UNICODE_CHARACTERS.F7,
|
|
F8: UNICODE_CHARACTERS.F8,
|
|
F9: UNICODE_CHARACTERS.F9,
|
|
F10: UNICODE_CHARACTERS.F10,
|
|
F11: UNICODE_CHARACTERS.F11,
|
|
F12: UNICODE_CHARACTERS.F12,
|
|
Command: UNICODE_CHARACTERS.Command,
|
|
ZenkakuHankaku: UNICODE_CHARACTERS.ZenkakuHankaku
|
|
};
|
|
|
|
// src/commands/browser/$$.ts
|
|
async function $$(selector) {
|
|
if (this.isBidi && typeof selector === "string" && !selector.startsWith(DEEP_SELECTOR)) {
|
|
if (globalThis.wdio?.execute) {
|
|
const command = "$$";
|
|
const res3 = "elementId" in this ? await globalThis.wdio.executeWithScope(command, this.elementId, selector) : await globalThis.wdio.execute(command, selector);
|
|
const elements3 = await getElements.call(this, selector, res3);
|
|
return enhanceElementsArray(elements3, this, selector);
|
|
}
|
|
const res2 = await findDeepElements.call(this, selector);
|
|
const elements2 = await getElements.call(this, selector, res2);
|
|
return enhanceElementsArray(elements2, getParent.call(this, res2), selector);
|
|
}
|
|
let res = Array.isArray(selector) ? selector : await findElements.call(this, selector);
|
|
if (Array.isArray(selector) && isElement(selector[0])) {
|
|
res = [];
|
|
for (const el of selector) {
|
|
const $el = await findElement.call(this, el);
|
|
if ($el) {
|
|
res.push($el);
|
|
}
|
|
}
|
|
}
|
|
const elements = await getElements.call(this, selector, res);
|
|
return enhanceElementsArray(elements, getParent.call(this, res), selector);
|
|
}
|
|
function getParent(res) {
|
|
let parent = res.length > 0 ? res[0].parent || this : this;
|
|
if (typeof parent.$ === "undefined") {
|
|
parent = "selector" in parent ? getElement.call(this, parent.selector, parent) : this;
|
|
}
|
|
return parent;
|
|
}
|
|
|
|
// src/commands/browser/$.ts
|
|
import { ELEMENT_KEY as ELEMENT_KEY2 } from "webdriver";
|
|
async function $(selector) {
|
|
if (globalThis.wdio && typeof selector === "string" && !selector.startsWith(DEEP_SELECTOR)) {
|
|
const res2 = "elementId" in this ? await globalThis.wdio.executeWithScope("$", this.elementId, selector) : await globalThis.wdio.execute("$", selector);
|
|
return getElement.call(this, selector, res2);
|
|
}
|
|
if (typeof selector === "object") {
|
|
const elementRef = selector;
|
|
if (typeof elementRef[ELEMENT_KEY2] === "string") {
|
|
return getElement.call(this, void 0, elementRef);
|
|
}
|
|
}
|
|
const res = await findElement.call(this, selector);
|
|
return getElement.call(this, selector, res);
|
|
}
|
|
|
|
// src/utils/actions/base.ts
|
|
import { ELEMENT_KEY as ELEMENT_KEY3 } from "webdriver";
|
|
var keyActionIds = 0;
|
|
var pointerActionIds = 0;
|
|
var wheelActionIds = 0;
|
|
var BaseAction = class {
|
|
constructor(instance, type, params) {
|
|
this.instance = instance;
|
|
this.#instance = instance;
|
|
if (params?.id) {
|
|
this.#id = params.id;
|
|
} else {
|
|
switch (type) {
|
|
case "key":
|
|
this.#id = `key${++keyActionIds}`;
|
|
break;
|
|
case "pointer":
|
|
this.#id = `pointer${++pointerActionIds}`;
|
|
break;
|
|
case "wheel":
|
|
this.#id = `wheel${++wheelActionIds}`;
|
|
break;
|
|
default:
|
|
this.#id = `action${type}`;
|
|
}
|
|
}
|
|
this.#type = type;
|
|
this.#parameters = params?.parameters || {};
|
|
}
|
|
#id;
|
|
#type;
|
|
#parameters;
|
|
#instance;
|
|
sequence = [];
|
|
toJSON() {
|
|
return {
|
|
id: this.#id,
|
|
type: this.#type,
|
|
parameters: this.#parameters,
|
|
actions: this.sequence
|
|
};
|
|
}
|
|
/**
|
|
* Inserts a pause action for the specified device, ensuring it idles for a tick.
|
|
* @param duration idle time of tick
|
|
*/
|
|
pause(duration) {
|
|
this.sequence.push({ type: "pause", duration });
|
|
return this;
|
|
}
|
|
/**
|
|
* Perform action sequence
|
|
* @param skipRelease set to true if `releaseActions` command should not be invoked
|
|
*/
|
|
async perform(skipRelease = false) {
|
|
for (const seq of this.sequence) {
|
|
if (!seq.origin || typeof seq.origin === "string") {
|
|
continue;
|
|
}
|
|
if (typeof seq.origin.then === "function") {
|
|
await seq.origin.waitForExist();
|
|
seq.origin = await seq.origin;
|
|
}
|
|
if (!seq.origin[ELEMENT_KEY3]) {
|
|
throw new Error(`Couldn't find element for "${seq.type}" action sequence`);
|
|
}
|
|
seq.origin = { [ELEMENT_KEY3]: seq.origin[ELEMENT_KEY3] };
|
|
}
|
|
await this.#instance.performActions([this.toJSON()]);
|
|
if (!skipRelease) {
|
|
await this.#instance.releaseActions();
|
|
}
|
|
}
|
|
};
|
|
|
|
// src/utils/actions/key.ts
|
|
var KeyAction = class extends BaseAction {
|
|
constructor(instance, params) {
|
|
super(instance, "key", params);
|
|
}
|
|
#sanitizeKey(value) {
|
|
if (typeof value !== "string") {
|
|
throw new Error(`Invalid type for key input: "${typeof value}", expected a string!`);
|
|
}
|
|
const platformName = this.instance.capabilities.platformName;
|
|
const isMac = (
|
|
// check capabilities first
|
|
platformName && platformName.match(/mac(\s)*os/i) || // if not set, expect we run locally
|
|
this.instance.options.hostname?.match(/0\.0\.0\.0|127\.0\.0\.1|local/i) && environment.value.osType().match(/darwin/i)
|
|
);
|
|
if (value === Key.Ctrl) {
|
|
return isMac ? Key.Command : Key.Control;
|
|
}
|
|
if (value.length > 1) {
|
|
throw new Error(`Your key input contains more than one character: "${value}", only one is allowed though!`);
|
|
}
|
|
return value;
|
|
}
|
|
/**
|
|
* Generates a key up action.
|
|
* @param value key value
|
|
*/
|
|
up(value) {
|
|
this.sequence.push({ type: "keyUp", value: this.#sanitizeKey(value) });
|
|
return this;
|
|
}
|
|
/**
|
|
* Generates a key down action.
|
|
* @param value key value
|
|
*/
|
|
down(value) {
|
|
this.sequence.push({ type: "keyDown", value: this.#sanitizeKey(value) });
|
|
return this;
|
|
}
|
|
};
|
|
|
|
// src/utils/actions/pointer.ts
|
|
var buttonNumbers = [0, 1, 2];
|
|
var buttonNames = ["left", "middle", "right"];
|
|
var buttonValue = [...buttonNumbers, ...buttonNames];
|
|
var ORIGIN_DEFAULT = "viewport";
|
|
var BUTTON_DEFAULT = 0;
|
|
var POINTER_TYPE_DEFAULT = "mouse";
|
|
var UP_PARAM_DEFAULTS = {
|
|
button: BUTTON_DEFAULT
|
|
};
|
|
var PARAM_DEFAULTS = {
|
|
...UP_PARAM_DEFAULTS,
|
|
width: 0,
|
|
height: 0,
|
|
pressure: 0,
|
|
tangentialPressure: 0,
|
|
tiltX: 0,
|
|
tiltY: 0,
|
|
twist: 0,
|
|
altitudeAngle: 0,
|
|
azimuthAngle: 0
|
|
};
|
|
var MOVE_PARAM_DEFAULTS = {
|
|
x: 0,
|
|
y: 0,
|
|
duration: 100,
|
|
origin: ORIGIN_DEFAULT
|
|
};
|
|
function removeDefaultParams(seq) {
|
|
for (const [key, value] of Object.entries(seq)) {
|
|
if (value === 0 && !["x", "y", "button", "duration"].includes(key)) {
|
|
delete seq[key];
|
|
}
|
|
}
|
|
}
|
|
function mapButton(params) {
|
|
const buttons = {
|
|
left: 0,
|
|
middle: 1,
|
|
right: 2
|
|
};
|
|
if (typeof params === "number") {
|
|
return { button: params };
|
|
}
|
|
if (typeof params === "string") {
|
|
return { button: buttons[params] };
|
|
}
|
|
if (typeof params === "object" && typeof params.button === "string") {
|
|
return { ...params, button: buttons[params.button] };
|
|
}
|
|
return params;
|
|
}
|
|
var PointerAction = class extends BaseAction {
|
|
constructor(instance, params = {}) {
|
|
if (!params.parameters) {
|
|
params.parameters = { pointerType: POINTER_TYPE_DEFAULT };
|
|
}
|
|
super(instance, "pointer", params);
|
|
}
|
|
move(params = {}, y) {
|
|
const seq = {
|
|
type: "pointerMove",
|
|
// default params
|
|
...PARAM_DEFAULTS,
|
|
...UP_PARAM_DEFAULTS,
|
|
...MOVE_PARAM_DEFAULTS
|
|
};
|
|
if (typeof params === "number") {
|
|
Object.assign(seq, { x: params, y });
|
|
} else if (params) {
|
|
Object.assign(seq, params);
|
|
}
|
|
removeDefaultParams(seq);
|
|
this.sequence.push(seq);
|
|
return this;
|
|
}
|
|
up(params = UP_PARAM_DEFAULTS) {
|
|
this.sequence.push({
|
|
type: "pointerUp",
|
|
...mapButton(params)
|
|
});
|
|
return this;
|
|
}
|
|
down(params = {}) {
|
|
const seq = {
|
|
type: "pointerDown",
|
|
...PARAM_DEFAULTS,
|
|
...mapButton(params)
|
|
};
|
|
removeDefaultParams(seq);
|
|
this.sequence.push(seq);
|
|
return this;
|
|
}
|
|
/**
|
|
* An action that cancels this pointer's current input.
|
|
*/
|
|
cancel() {
|
|
this.sequence.push({ type: "pointerCancel" });
|
|
return this;
|
|
}
|
|
};
|
|
|
|
// src/utils/actions/wheel.ts
|
|
var DEFAULT_SCROLL_PARAMS = {
|
|
x: 0,
|
|
y: 0,
|
|
deltaX: 0,
|
|
deltaY: 0,
|
|
duration: 0
|
|
};
|
|
var WheelAction = class extends BaseAction {
|
|
constructor(instance, params) {
|
|
super(instance, "wheel", params);
|
|
}
|
|
/**
|
|
* Scrolls a page to given coordinates or origin.
|
|
*/
|
|
scroll(params) {
|
|
this.sequence.push({ type: "scroll", ...DEFAULT_SCROLL_PARAMS, ...params });
|
|
return this;
|
|
}
|
|
};
|
|
|
|
// src/commands/browser/action.ts
|
|
function action(type, opts) {
|
|
if (type === "key") {
|
|
return new KeyAction(this, opts);
|
|
}
|
|
if (type === "pointer") {
|
|
return new PointerAction(this, opts);
|
|
}
|
|
if (type === "wheel") {
|
|
return new WheelAction(this, opts);
|
|
}
|
|
throw new Error(`Unsupported action type "${type}", supported are "key", "pointer", "wheel"`);
|
|
}
|
|
|
|
// src/commands/browser/actions.ts
|
|
async function actions(actions2) {
|
|
await this.performActions(actions2.map((action2) => action2.toJSON()));
|
|
await this.releaseActions();
|
|
}
|
|
|
|
// src/utils/bidi/index.ts
|
|
import { ELEMENT_KEY as ELEMENT_KEY4 } from "webdriver";
|
|
|
|
// src/commands/constant.ts
|
|
var TOUCH_ACTIONS = ["press", "longPress", "tap", "moveTo", "wait", "release"];
|
|
var POS_ACTIONS = TOUCH_ACTIONS.slice(0, 4);
|
|
var ACCEPTED_OPTIONS = ["x", "y", "element"];
|
|
var SCRIPT_PREFIX = "/* __wdio script__ */";
|
|
var SCRIPT_SUFFIX = "/* __wdio script end__ */";
|
|
var resqScript = `!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.resq=e():(t.window=t.window||{},t.window.resq=e())}(window,(function(){return function(t){var e={};function r(n){if(e[n])return e[n].exports;var o=e[n]={i:n,l:!1,exports:{}};return t[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=t,r.c=e,r.d=function(t,e,n){r.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:n})},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.t=function(t,e){if(1&e&&(t=r(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)r.d(n,o,function(e){return t[e]}.bind(null,o));return n},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,"a",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p="",r(r.s=16)}([function(t,e,r){"use strict";r.d(e,"a",(function(){return m})),r.d(e,"d",(function(){return j})),r.d(e,"b",(function(){return M})),r.d(e,"c",(function(){return P}));var n=r(1),o=r.n(n),u=r(14),i=r.n(u),c=r(2),f=r.n(c),s=r(15),a=r.n(s);function l(t,e){var r=Object.keys(t);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(t);e&&(n=n.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),r.push.apply(r,n)}return r}var p=Array.isArray,d=Object.keys;function x(t){return"function"==typeof t}function y(t){return t instanceof HTMLElement||t instanceof Text}function h(t){return"object"===f()(t)&&!p(t)}function b(t){if(!t||"string"==typeof t)return t;var e=function(t){for(var e=1;e<arguments.length;e++){var r=null!=arguments[e]?arguments[e]:{};e%2?l(Object(r),!0).forEach((function(e){i()(t,e,r[e])})):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(r)):l(Object(r)).forEach((function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(r,e))}))}return t}({},t);return delete e.children,e}function v(t,e){var r=arguments.length>2&&void 0!==arguments[2]&&arguments[2];return!(!p(t)||!p(e))&&(r?t.length===e.length&&!t.find((function(t){return!e.includes(t)})):t.some((function(t){return e.includes(t)})))}function _(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},r=arguments.length>2&&void 0!==arguments[2]&&arguments[2],n=[];if(!d(t).length)return!0;if(null===e||!d(e).length)return!1;if(r)return a()(t,e);var o=d(t).filter((function(t){return d(e).includes(t)}));return o.forEach((function(r){h(t[r])&&h(e[r])&&(n=n.concat(_(t[r],e[r]))),(t[r]===e[r]||v(t[r],e[r]))&&n.push(e)})),n.length>0&&n.filter((function(t){return t})).length===o.length}function m(t){var e,r={children:[]};if(!t)return r;r.name=x(e=t.type)?e.displayName||e.name:e,r.props=b(t.memoizedProps),r.state=function(t){if(t){var e=t.baseState;return e||t}}(t.memoizedState);var n=t.child;if(n)for(r.children.push(n);n.sibling;)r.children.push(n.sibling),n=n.sibling;return r.children=r.children.map((function(t){return m(t)})),x(t.type)&&function(t){return t.children.length>1}(r)?(r.node=function(t){return t.children.map((function(t){return t.node})).filter((function(t){return!!t}))}(r),r.isFragment=!0):r.node=function(t){return y(t.stateNode)?t.stateNode:t.child&&y(t.child.stateNode)?t.child.stateNode:null}(t),r}function g(t){for(;t.length;){var e=t.shift();if(e.node)return e.node;e.children&&Array.isArray(e.children)&&t.push.apply(t,o()(e.children))}}function O(t,e){for(var r=[];t.length;){var n=t.shift().children;n&&Array.isArray(n)&&n.forEach((function(n){e(n)&&(!n.node&&Array.isArray(n.children)&&(n.node=g(n.children.concat([]))),r.push(n)),t.push(n)}))}return r}function w(t,e){var r=function(t){if(t){var e=t.split("(");return 1===e.length?t:e.find((function(t){return t.includes(")")})).replace(/\\)*/g,"")}}(e);return new RegExp("^"+t.split("*").map((function(t){return t.replace(/([.*+?^=!:\${}()|[\\]/\\\\])/g,"\\\\$1")})).join(".+")+"$").test(r)}function j(t,e){var r=arguments.length>3?arguments[3]:void 0;return t.reduce((function(t,e){return t.concat(O(t,r&&"function"==typeof r?r:function(t){return"string"==typeof t.name?w(e,t.name):null!==t.name&&"object"===f()(t.name)&&w(e,t.name.displayName)}))}),[e])}function M(t,e,r){var n=arguments.length>3&&void 0!==arguments[3]&&arguments[3];return x(r)?(console.warn("Functions are not supported as filter matchers"),[]):t.filter((function(t){return h(r)&&_(r,t[e],n)||p(r)&&v(r,t[e],n)||t[e]===r}))}function P(t){if(t.hasOwnProperty("_reactRootContainer"))return t._reactRootContainer._internalRoot.current;var e=Object.keys(t).find((function(t){return t.startsWith("__reactInternalInstance")||t.startsWith("__reactFiber")||t.startsWith("__reactContainer")}));return e?t[e]:void 0}},function(t,e,r){var n=r(17),o=r(18),u=r(19),i=r(20);t.exports=function(t){return n(t)||o(t)||u(t)||i()},t.exports.default=t.exports,t.exports.__esModule=!0},function(t,e){function r(e){return"function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?(t.exports=r=function(t){return typeof t},t.exports.default=t.exports,t.exports.__esModule=!0):(t.exports=r=function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},t.exports.default=t.exports,t.exports.__esModule=!0),r(e)}t.exports=r,t.exports.default=t.exports,t.exports.__esModule=!0},function(t,e){function r(e){return t.exports=r=Object.setPrototypeOf?Object.getPrototypeOf:function(t){return t.__proto__||Object.getPrototypeOf(t)},t.exports.default=t.exports,t.exports.__esModule=!0,r(e)}t.exports=r,t.exports.default=t.exports,t.exports.__esModule=!0},function(t,e){t.exports=function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")},t.exports.default=t.exports,t.exports.__esModule=!0},function(t,e){function r(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}t.exports=function(t,e,n){return e&&r(t.prototype,e),n&&r(t,n),t},t.exports.default=t.exports,t.exports.__esModule=!0},function(t,e){function r(e,n){return t.exports=r=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t},t.exports.default=t.exports,t.exports.__esModule=!0,r(e,n)}t.exports=r,t.exports.default=t.exports,t.exports.__esModule=!0},function(t,e,r){var n=r(6);t.exports=function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),e&&n(t,e)},t.exports.default=t.exports,t.exports.__esModule=!0},function(t,e,r){var n=r(3),o=r(6),u=r(22),i=r(23);function c(e){var r="function"==typeof Map?new Map:void 0;return t.exports=c=function(t){if(null===t||!u(t))return t;if("function"!=typeof t)throw new TypeError("Super expression must either be null or a function");if(void 0!==r){if(r.has(t))return r.get(t);r.set(t,e)}function e(){return i(t,arguments,n(this).constructor)}return e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),o(e,t)},t.exports.default=t.exports,t.exports.__esModule=!0,c(e)}t.exports=c,t.exports.default=t.exports,t.exports.__esModule=!0},function(t,e){var r;r=function(){return this}();try{r=r||new Function("return this")()}catch(t){"object"==typeof window&&(r=window)}t.exports=r},function(t,e){t.exports=function(t,e){(null==e||e>t.length)&&(e=t.length);for(var r=0,n=new Array(e);r<e;r++)n[r]=t[r];return n},t.exports.default=t.exports,t.exports.__esModule=!0},function(t,e,r){"use strict";(function(t){r.d(e,"a",(function(){return o}));var n=r(0);function o(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:5e3,r=arguments.length>1?arguments[1]:void 0;if(t.isReactLoaded)return Promise.resolve("React already loaded");var o=function(){var t=document.createTreeWalker(document);if(r)return document.querySelector(r);for(;t.nextNode();)if(t.currentNode.hasOwnProperty("_reactRootContainer"))return t.currentNode};return new Promise((function(r,u){var i=!1,c=null;!function e(){var u=o();if(u&&(t.isReactLoaded=!0,t.rootReactElement=Object(n.c)(u),t.rootReactElement))return clearTimeout(c),r();i||setTimeout(e,200)}(),c=setTimeout((function(){i=!0,u("Timed out")}),e)}))}}).call(this,r(9))},function(t,e,r){"use strict";r.d(e,"a",(function(){return g}));var n=r(1),o=r.n(n),u=r(4),i=r.n(u),c=r(5),f=r.n(c),s=r(7),a=r.n(s),l=r(13),p=r.n(l),d=r(3),x=r.n(d),y=r(8),h=r.n(y),b=r(0);function v(t){var e=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(t){return!1}}();return function(){var r,n=x()(t);if(e){var o=x()(this).constructor;r=Reflect.construct(n,arguments,o)}else r=n.apply(this,arguments);return p()(this,r)}}var _=function(t){a()(r,t);var e=v(r);function r(t){return i()(this,r),t||(t=[]),e.call.apply(e,[this].concat(o()(t)))}return f()(r,[{key:"byProps",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{exact:!1},n=e.exact,o=Object(b.b)(this,"props",t,n);return new r(o)}},{key:"byState",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{exact:!1},n=e.exact,o=Object(b.b)(this,"state",t,n);return new r(o)}}]),r}(h()(Array)),m=function(t){a()(r,t);var e=v(r);function r(t,n){var o;for(var u in i()(this,r),(o=e.call(this,t))._nodes=n,t)o[u]=t[u];return o}return f()(r,[{key:"byProps",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{exact:!1},n=e.exact,o=Object(b.b)(this._nodes,"props",t,n)[0];return new r(o,this._nodes)}},{key:"byState",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{exact:!1},n=e.exact,o=Object(b.b)(this._nodes,"state",t,n)[0];return new r(o,this._nodes)}}]),r}(h()(Object)),g=function(){function t(e,r){i()(this,t),this.selectors=e.split(" ").filter((function(t){return!!t})).map((function(t){return t.trim()})),this.rootComponent=r,this.tree=Object(b.a)(this.rootComponent)}return f()(t,[{key:"find",value:function(){return this.nodes=new _(Object(b.d)(this.selectors,this.tree,!0)),new m(this.nodes[0],this.nodes)}},{key:"findAll",value:function(){return new _(Object(b.d)(this.selectors,this.tree))}}]),t}()},function(t,e,r){var n=r(2).default,o=r(21);t.exports=function(t,e){return!e||"object"!==n(e)&&"function"!=typeof e?o(t):e},t.exports.default=t.exports,t.exports.__esModule=!0},function(t,e){t.exports=function(t,e,r){return e in t?Object.defineProperty(t,e,{value:r,enumerable:!0,configurable:!0,writable:!0}):t[e]=r,t},t.exports.default=t.exports,t.exports.__esModule=!0},function(t,e,r){"use strict";var n=Array.isArray,o=Object.keys,u=Object.prototype.hasOwnProperty;t.exports=function t(e,r){if(e===r)return!0;if(e&&r&&"object"==typeof e&&"object"==typeof r){var i,c,f,s=n(e),a=n(r);if(s&&a){if((c=e.length)!=r.length)return!1;for(i=c;0!=i--;)if(!t(e[i],r[i]))return!1;return!0}if(s!=a)return!1;var l=e instanceof Date,p=r instanceof Date;if(l!=p)return!1;if(l&&p)return e.getTime()==r.getTime();var d=e instanceof RegExp,x=r instanceof RegExp;if(d!=x)return!1;if(d&&x)return e.toString()==r.toString();var y=o(e);if((c=y.length)!==o(r).length)return!1;for(i=c;0!=i--;)if(!u.call(r,y[i]))return!1;for(i=c;0!=i--;)if(!t(e[f=y[i]],r[f]))return!1;return!0}return e!=e&&r!=r}},function(t,e,r){"use strict";r.r(e),function(t){r.d(e,"resq$",(function(){return c})),r.d(e,"resq$$",(function(){return f}));var n=r(12),o=r(11);r.d(e,"waitToLoadReact",(function(){return o.a}));var u=r(0);function i(e,r,o){if(!o&&!t.isReactLoaded)throw new Error("Could not find the root element of your application");var i=t.rootReactElement;if(o instanceof HTMLElement&&(i=Object(u.c)(o)),!i)throw new Error("Could not find instance of React in given element");return new n.a(e,i)[r]()}function c(t,e){return i(t,"find",e)}function f(t,e){return i(t,"findAll",e)}}.call(this,r(9))},function(t,e,r){var n=r(10);t.exports=function(t){if(Array.isArray(t))return n(t)},t.exports.default=t.exports,t.exports.__esModule=!0},function(t,e){t.exports=function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)},t.exports.default=t.exports,t.exports.__esModule=!0},function(t,e,r){var n=r(10);t.exports=function(t,e){if(t){if("string"==typeof t)return n(t,e);var r=Object.prototype.toString.call(t).slice(8,-1);return"Object"===r&&t.constructor&&(r=t.constructor.name),"Map"===r||"Set"===r?Array.from(t):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?n(t,e):void 0}},t.exports.default=t.exports,t.exports.__esModule=!0},function(t,e){t.exports=function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")},t.exports.default=t.exports,t.exports.__esModule=!0},function(t,e){t.exports=function(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t},t.exports.default=t.exports,t.exports.__esModule=!0},function(t,e){t.exports=function(t){return-1!==Function.toString.call(t).indexOf("[native code]")},t.exports.default=t.exports,t.exports.__esModule=!0},function(t,e,r){var n=r(6),o=r(24);function u(e,r,i){return o()?(t.exports=u=Reflect.construct,t.exports.default=t.exports,t.exports.__esModule=!0):(t.exports=u=function(t,e,r){var o=[null];o.push.apply(o,e);var u=new(Function.bind.apply(t,o));return r&&n(u,r.prototype),u},t.exports.default=t.exports,t.exports.__esModule=!0),u.apply(null,arguments)}t.exports=u,t.exports.default=t.exports,t.exports.__esModule=!0},function(t,e){t.exports=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(t){return!1}},t.exports.default=t.exports,t.exports.__esModule=!0}])}));`;
|
|
var formatArgs = function(scope, actions2) {
|
|
return actions2.map((action2) => {
|
|
if (Array.isArray(action2)) {
|
|
return formatArgs(scope, action2);
|
|
}
|
|
if (typeof action2 === "string") {
|
|
action2 = { action: action2 };
|
|
}
|
|
const formattedAction = {
|
|
action: action2.action,
|
|
options: {}
|
|
};
|
|
const actionElement = action2.element && typeof action2.element.elementId === "string" ? action2.element.elementId : scope.elementId;
|
|
if (POS_ACTIONS.includes(action2.action) && formattedAction.options && actionElement) {
|
|
formattedAction.options.element = actionElement;
|
|
}
|
|
if (formattedAction.options && typeof action2.x === "number" && isFinite(action2.x)) {
|
|
formattedAction.options.x = action2.x;
|
|
}
|
|
if (formattedAction.options && typeof action2.y === "number" && isFinite(action2.y)) {
|
|
formattedAction.options.y = action2.y;
|
|
}
|
|
if (formattedAction.options && action2.ms) {
|
|
formattedAction.options.ms = action2.ms;
|
|
}
|
|
if (formattedAction.options && Object.keys(formattedAction.options).length === 0) {
|
|
delete formattedAction.options;
|
|
}
|
|
return formattedAction;
|
|
});
|
|
};
|
|
var validateParameters = (params) => {
|
|
const options = Object.keys(params.options || {});
|
|
if (params.action === "release" && options.length !== 0) {
|
|
throw new Error(
|
|
`action "release" doesn't accept any options ("${options.join('", "')}" found)`
|
|
);
|
|
}
|
|
if (params.action === "wait" && (options.includes("x") || options.includes("y"))) {
|
|
throw new Error(`action "wait" doesn't accept x or y options`);
|
|
}
|
|
if (POS_ACTIONS.includes(params.action)) {
|
|
for (const option in params.options) {
|
|
if (!ACCEPTED_OPTIONS.includes(option)) {
|
|
throw new Error(`action "${params.action}" doesn't accept "${option}" as option`);
|
|
}
|
|
}
|
|
if (options.length === 0) {
|
|
throw new Error(
|
|
`Touch actions like "${params.action}" need at least some kind of position information like "element", "x" or "y" options, you've none given.`
|
|
);
|
|
}
|
|
}
|
|
};
|
|
var touchAction = function(actions2) {
|
|
if (!this.multiTouchPerform || !this.touchPerform) {
|
|
throw new Error("touchAction can be used with Appium only.");
|
|
}
|
|
if (!Array.isArray(actions2)) {
|
|
actions2 = [actions2];
|
|
}
|
|
const formattedAction = formatArgs(this, actions2);
|
|
const protocolCommand = Array.isArray(actions2[0]) ? this.multiTouchPerform.bind(this) : this.touchPerform.bind(this);
|
|
formattedAction.forEach((params) => validateParameters(params));
|
|
return protocolCommand(formattedAction);
|
|
};
|
|
|
|
// src/utils/bidi/error.ts
|
|
var WebdriverBidiExeception = class extends Error {
|
|
#params;
|
|
#result;
|
|
constructor(params, result) {
|
|
super(result.exceptionDetails.text);
|
|
this.name = "WebdriverBidiExeception";
|
|
this.#params = params;
|
|
this.#result = result;
|
|
this.stack = this.#getCustomStack();
|
|
}
|
|
#getCustomStack() {
|
|
const origStack = this.stack;
|
|
const failureLine = this.#getFailureLine();
|
|
const stack = origStack?.split("\n") || [];
|
|
const wrapCommandIndex = stack.findLastIndex((line) => line.includes("Context.executeAsync"));
|
|
const executeLine = stack[wrapCommandIndex - 1];
|
|
if (failureLine && executeLine) {
|
|
const line = executeLine.replace("file://", "").split(":");
|
|
const row = line.length > 3 ? line[2] : line[1];
|
|
const [errorMessage, ...restOfStack] = stack;
|
|
const linePrefix = ` ${row} \u2502 `;
|
|
const codeLine = [
|
|
linePrefix + failureLine,
|
|
" ".repeat(linePrefix.length - 2) + "\u2575 " + "~".repeat(failureLine.length),
|
|
""
|
|
];
|
|
return [errorMessage, executeLine, ...codeLine, ...restOfStack].join("\n");
|
|
}
|
|
return origStack;
|
|
}
|
|
/**
|
|
* This is an attempt to identify the snippet of code that caused an execute(Async) function to
|
|
* throw an exception
|
|
* @param {string} script script that executed in the browser
|
|
* @param {number} columnNumber column in which the scrpt threw an exception
|
|
* @returns the line of failure in which the code threw an exception or `undefined` if we could not find it
|
|
*/
|
|
#getFailureLine() {
|
|
const script = this.#params.functionDeclaration;
|
|
const exceptionDetails = this.#result.exceptionDetails;
|
|
const userScript = script.split("\n").find((l) => l.includes(SCRIPT_PREFIX));
|
|
if (!userScript) {
|
|
return;
|
|
}
|
|
let length = 0;
|
|
const isMinified = script.split("\n").some((line) => line.includes(SCRIPT_PREFIX) && line.includes(SCRIPT_SUFFIX));
|
|
if (isMinified) {
|
|
for (const line of userScript.split(";")) {
|
|
if (length + line.length >= exceptionDetails.columnNumber) {
|
|
return line.includes(SCRIPT_SUFFIX) ? line.slice(0, line.indexOf(SCRIPT_SUFFIX)) : line;
|
|
}
|
|
length += line.length;
|
|
}
|
|
} else {
|
|
const slicedScript = script.slice(
|
|
script.indexOf(SCRIPT_PREFIX) + SCRIPT_PREFIX.length,
|
|
script.indexOf(SCRIPT_SUFFIX)
|
|
);
|
|
const lineDiff = 9;
|
|
const line = slicedScript.split("\n")[exceptionDetails.lineNumber - lineDiff]?.slice(exceptionDetails.columnNumber);
|
|
return line;
|
|
}
|
|
return void 0;
|
|
}
|
|
};
|
|
|
|
// src/utils/bidi/index.ts
|
|
function parseScriptResult(params, result) {
|
|
const type = result.type;
|
|
if (type === "success" /* Success */) {
|
|
return deserialize(result.result);
|
|
}
|
|
if (type === "exception" /* Exception */) {
|
|
throw new WebdriverBidiExeception(params, result);
|
|
}
|
|
throw new Error(`Unknown evaluate result type: ${type}`);
|
|
}
|
|
var references = /* @__PURE__ */ new Map();
|
|
function deserialize(result) {
|
|
const deserializedValue = deserializeValue(result);
|
|
references.clear();
|
|
return deserializedValue;
|
|
}
|
|
function deserializeValue(result) {
|
|
if (result && "internalId" in result && typeof result.internalId === "string") {
|
|
if ("value" in result) {
|
|
references.set(result.internalId, result.value);
|
|
} else {
|
|
result.value = references.get(result.internalId);
|
|
}
|
|
}
|
|
const { type, value } = result;
|
|
if (type === "regexp" /* RegularExpression */) {
|
|
return new RegExp(value.pattern, value.flags);
|
|
}
|
|
if (type === "array" /* Array */) {
|
|
return value.map((element) => deserializeValue(element));
|
|
}
|
|
if (type === "date" /* Date */) {
|
|
return new Date(value);
|
|
}
|
|
if (type === "map" /* Map */) {
|
|
return new Map(value.map(([key, value2]) => [typeof key === "string" ? key : deserializeValue(key), deserializeValue(value2)]));
|
|
}
|
|
if (type === "set" /* Set */) {
|
|
return new Set(value.map((element) => deserializeValue(element)));
|
|
}
|
|
if (type === "number" /* Number */ && value === "NaN") {
|
|
return NaN;
|
|
}
|
|
if (type === "number" /* Number */ && value === "Infinity") {
|
|
return Infinity;
|
|
}
|
|
if (type === "number" /* Number */ && value === "-Infinity") {
|
|
return -Infinity;
|
|
}
|
|
if (type === "number" /* Number */ && value === "-0") {
|
|
return -0;
|
|
}
|
|
if (type === "bigint" /* BigInt */) {
|
|
return BigInt(value);
|
|
}
|
|
if (type === "null" /* Null */) {
|
|
return null;
|
|
}
|
|
if (type === "object" /* Object */) {
|
|
return Object.fromEntries((value || []).map(([key, value2]) => {
|
|
return [typeof key === "string" ? key : deserializeValue(key), deserializeValue(value2)];
|
|
}));
|
|
}
|
|
if (type === "node" /* Node */) {
|
|
return { [ELEMENT_KEY4]: result.sharedId };
|
|
}
|
|
if (type === "error" /* Error */) {
|
|
return new Error("<unserializable error>");
|
|
}
|
|
return value;
|
|
}
|
|
|
|
// src/commands/browser/addInitScript.ts
|
|
async function addInitScript(script, ...args) {
|
|
if (typeof script !== "function") {
|
|
throw new Error("The `addInitScript` command requires a function as first parameter, but got: " + typeof script);
|
|
}
|
|
if (!this.isBidi) {
|
|
throw new Error("This command is only supported when automating browser using WebDriver Bidi protocol");
|
|
}
|
|
const serializedParameters = (args || []).map((arg) => JSON.stringify(arg));
|
|
const context = await this.getWindowHandle();
|
|
const src = "return " + script.toString();
|
|
const fn = `(emit) => {
|
|
const closure = new Function(${JSON.stringify(src)})
|
|
return closure()(${serializedParameters.length ? `${serializedParameters.join(", ")}, emit` : "emit"})
|
|
}`;
|
|
const channel = btoa(fn.toString());
|
|
const result = await this.scriptAddPreloadScript({
|
|
functionDeclaration: fn,
|
|
arguments: [{
|
|
type: "channel",
|
|
value: { channel }
|
|
}],
|
|
contexts: [context]
|
|
});
|
|
await this.sessionSubscribe({
|
|
events: ["script.message"]
|
|
});
|
|
const eventHandler = /* @__PURE__ */ new Map();
|
|
const messageHandler = (msg) => {
|
|
if (msg.channel === channel) {
|
|
const handler = eventHandler.get("data") || [];
|
|
return handler.forEach((fn2) => fn2(deserialize(msg.data)));
|
|
}
|
|
};
|
|
this.on("script.message", messageHandler);
|
|
const resetFn = (() => {
|
|
eventHandler.clear();
|
|
this.off("script.message", messageHandler);
|
|
return this.scriptRemovePreloadScript({ script: result.script });
|
|
});
|
|
const returnVal = {
|
|
remove: resetFn,
|
|
on: (event, listener) => {
|
|
if (!eventHandler.has(event)) {
|
|
eventHandler.set(event, []);
|
|
}
|
|
eventHandler.get(event)?.push(listener);
|
|
}
|
|
};
|
|
return returnVal;
|
|
}
|
|
|
|
// src/commands/browser/call.ts
|
|
function call(fn) {
|
|
if (typeof fn === "function") {
|
|
return fn();
|
|
}
|
|
throw new Error('Command argument for "call" needs to be a function');
|
|
}
|
|
|
|
// src/commands/browser/custom$$.ts
|
|
import { ELEMENT_KEY as ELEMENT_KEY5 } from "webdriver";
|
|
async function custom$$(strategyName, ...strategyArguments) {
|
|
const strategy = this.strategies.get(strategyName);
|
|
if (!strategy) {
|
|
throw Error("No strategy found for " + strategyName);
|
|
}
|
|
const strategyRef = { strategy, strategyName, strategyArguments };
|
|
let res = await this.execute(strategy, ...strategyArguments);
|
|
if (!Array.isArray(res)) {
|
|
res = [res];
|
|
}
|
|
res = res.filter((el) => !!el && typeof el[ELEMENT_KEY5] === "string");
|
|
const elements = res.length ? await getElements.call(this, strategyRef, res) : [];
|
|
return enhanceElementsArray(elements, this, strategyName, "custom$$", strategyArguments);
|
|
}
|
|
|
|
// src/commands/browser/custom$.ts
|
|
import { ELEMENT_KEY as ELEMENT_KEY6 } from "webdriver";
|
|
async function custom$(strategyName, ...strategyArguments) {
|
|
const strategy = this.strategies.get(strategyName);
|
|
if (!strategy) {
|
|
throw Error("No strategy found for " + strategyName);
|
|
}
|
|
const strategyRef = { strategy, strategyName, strategyArguments };
|
|
let res = await this.execute(strategy, ...strategyArguments);
|
|
if (Array.isArray(res)) {
|
|
res = res[0];
|
|
}
|
|
if (res && typeof res[ELEMENT_KEY6] === "string") {
|
|
return await getElement.call(this, strategyRef, res);
|
|
}
|
|
return await getElement.call(this, strategyRef, new Error("no such element"));
|
|
}
|
|
|
|
// src/commands/browser/debug.ts
|
|
import { serializeError } from "serialize-error";
|
|
import WDIORepl from "@wdio/repl";
|
|
function debug(commandTimeout = 5e3) {
|
|
const repl = new WDIORepl();
|
|
const { introMessage } = WDIORepl;
|
|
const process3 = globalThis.process;
|
|
if (!environment.value.variables.WDIO_WORKER_ID || typeof process3.send !== "function") {
|
|
console.log(WDIORepl.introMessage);
|
|
const context = {
|
|
browser: this,
|
|
driver: this,
|
|
$: this.$.bind(this),
|
|
$$: this.$$.bind(this)
|
|
};
|
|
return repl.start(context);
|
|
}
|
|
process3._debugProcess(process3.pid);
|
|
process3.send({
|
|
origin: "debugger",
|
|
name: "start",
|
|
params: { commandTimeout, introMessage }
|
|
});
|
|
let commandResolve = (
|
|
/* istanbul ignore next */
|
|
() => {
|
|
}
|
|
);
|
|
process3.on("message", (m) => {
|
|
if (m.origin !== "debugger") {
|
|
return;
|
|
}
|
|
if (m.name === "stop") {
|
|
process3._debugEnd(process3.pid);
|
|
return commandResolve();
|
|
}
|
|
if (m.name === "eval") {
|
|
repl.eval(m.content.cmd, global, void 0, (err, result) => {
|
|
if (typeof process3.send !== "function") {
|
|
return;
|
|
}
|
|
if (err) {
|
|
process3.send({
|
|
origin: "debugger",
|
|
name: "result",
|
|
params: {
|
|
error: true,
|
|
...serializeError(err)
|
|
}
|
|
});
|
|
}
|
|
if (typeof result === "function") {
|
|
result = `[Function: ${result.name}]`;
|
|
}
|
|
process3.send({
|
|
origin: "debugger",
|
|
name: "result",
|
|
params: { result }
|
|
});
|
|
});
|
|
}
|
|
});
|
|
return new Promise((resolve) => commandResolve = resolve);
|
|
}
|
|
|
|
// src/commands/browser/deleteCookies.ts
|
|
import logger4 from "@wdio/logger";
|
|
var log4 = logger4("webdriverio");
|
|
async function deleteCookies(filter) {
|
|
const filterArray = typeof filter === "undefined" ? void 0 : Array.isArray(filter) ? filter : [filter];
|
|
if (!this.isBidi) {
|
|
await deleteCookiesClassic.call(this, getNamesForClassic(filterArray));
|
|
return;
|
|
}
|
|
let url2;
|
|
try {
|
|
url2 = new URL(await this.getUrl());
|
|
if (url2.origin === "null") {
|
|
await deleteCookiesClassic.call(this, getNamesForClassic(filterArray));
|
|
return;
|
|
}
|
|
} catch {
|
|
await deleteCookiesClassic.call(this, getNamesForClassic(filterArray));
|
|
return;
|
|
}
|
|
const partition = {
|
|
type: "storageKey",
|
|
sourceOrigin: url2.origin
|
|
};
|
|
try {
|
|
const { cookies } = await this.storageGetCookies({ partition });
|
|
if (cookies.length === 0 && !this.isMobile) {
|
|
await deleteCookiesClassic.call(this, getNamesForClassic(filterArray));
|
|
return;
|
|
}
|
|
} catch (err) {
|
|
log4.warn(`BiDi deleteCookies check failed, falling back to classic: ${err.message}`);
|
|
await deleteCookiesClassic.call(this, getNamesForClassic(filterArray));
|
|
return;
|
|
}
|
|
try {
|
|
if (!filterArray) {
|
|
await this.storageDeleteCookies({ partition });
|
|
return;
|
|
}
|
|
const bidiFilter = filterArray.map((f) => {
|
|
if (typeof f === "string") {
|
|
return { name: f };
|
|
}
|
|
if (typeof f === "object") {
|
|
return f;
|
|
}
|
|
throw new Error(`Invalid value for cookie filter, expected 'string' or 'remote.StorageCookieFilter' but found "${typeof f}"`);
|
|
});
|
|
await Promise.all(bidiFilter.map((filter2) => this.storageDeleteCookies({ filter: filter2, partition })));
|
|
} catch (err) {
|
|
log4.warn(`BiDi deleteCookies failed, falling back to classic: ${err.message}`);
|
|
await deleteCookiesClassic.call(this, getNamesForClassic(filterArray));
|
|
}
|
|
return;
|
|
}
|
|
function getNamesForClassic(filterArray) {
|
|
return filterArray?.map((f) => {
|
|
if (typeof f === "object") {
|
|
const name = f.name;
|
|
if (!name) {
|
|
throw new Error("In WebDriver Classic you can only filter for cookie names");
|
|
}
|
|
return name;
|
|
}
|
|
if (typeof f === "string") {
|
|
return f;
|
|
}
|
|
throw new Error(`Invalid value for cookie filter, expected 'string' or 'remote.StorageCookieFilter' but found "${typeof f}"`);
|
|
});
|
|
}
|
|
function deleteCookiesClassic(names) {
|
|
if (names === void 0) {
|
|
return this.deleteAllCookies();
|
|
}
|
|
const namesList = Array.isArray(names) ? names : [names];
|
|
if (namesList.every((obj) => typeof obj !== "string")) {
|
|
return Promise.reject(new Error("Invalid input (see https://webdriver.io/docs/api/browser/deleteCookies for documentation)"));
|
|
}
|
|
return Promise.all(namesList.map((name) => this.deleteCookie(name)));
|
|
}
|
|
|
|
// src/commands/browser/downloadFile.ts
|
|
async function downloadFile2(fileName, targetDirectory) {
|
|
return environment.value.downloadFile.call(this, fileName, targetDirectory);
|
|
}
|
|
|
|
// src/clock.ts
|
|
import logger5 from "@wdio/logger";
|
|
var log5 = logger5("webdriverio:ClockManager");
|
|
function installFakeTimers(options) {
|
|
window.__clock = window.__wdio_sinon.install(options);
|
|
}
|
|
function uninstallFakeTimers() {
|
|
window.__clock.uninstall();
|
|
}
|
|
var fakerScript = 'function FakeTimers () {\n/*\n * Copyright (c) 2010-2014, Christian Johansen, christian@cjohansen.no. All rights reserved.\n * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\n * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.\n * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n */\n\n/**\n * @typedef {object} IdleDeadline\n * @property {boolean} didTimeout - whether or not the callback was called before reaching the optional timeout\n * @property {function():number} timeRemaining - a floating-point value providing an estimate of the number of milliseconds remaining in the current idle period\n */\n\n/**\n * Queues a function to be called during a browser\'s idle periods\n *\n * @callback RequestIdleCallback\n * @param {function(IdleDeadline)} callback\n * @param {{timeout: number}} options - an options object\n * @returns {number} the id\n */\n\n/**\n * @callback NextTick\n * @param {VoidVarArgsFunc} callback - the callback to run\n * @param {...*} args - optional arguments to call the callback with\n * @returns {void}\n */\n\n/**\n * @callback SetImmediate\n * @param {VoidVarArgsFunc} callback - the callback to run\n * @param {...*} args - optional arguments to call the callback with\n * @returns {NodeImmediate}\n */\n\n/**\n * @callback VoidVarArgsFunc\n * @param {...*} callback - the callback to run\n * @returns {void}\n */\n\n/**\n * @typedef RequestAnimationFrame\n * @property {function(number):void} requestAnimationFrame\n * @returns {number} - the id\n */\n\n/**\n * @typedef Performance\n * @property {function(): number} now\n */\n\n/* eslint-disable jsdoc/require-property-description */\n/**\n * @typedef {object} Clock\n * @property {number} now - the current time\n * @property {Date} Date - the Date constructor\n * @property {number} loopLimit - the maximum number of timers before assuming an infinite loop\n * @property {RequestIdleCallback} requestIdleCallback\n * @property {function(number):void} cancelIdleCallback\n * @property {setTimeout} setTimeout\n * @property {clearTimeout} clearTimeout\n * @property {NextTick} nextTick\n * @property {queueMicrotask} queueMicrotask\n * @property {setInterval} setInterval\n * @property {clearInterval} clearInterval\n * @property {SetImmediate} setImmediate\n * @property {function(NodeImmediate):void} clearImmediate\n * @property {function():number} countTimers\n * @property {RequestAnimationFrame} requestAnimationFrame\n * @property {function(number):void} cancelAnimationFrame\n * @property {function():void} runMicrotasks\n * @property {function(string | number): number} tick\n * @property {function(string | number): Promise<number>} tickAsync\n * @property {function(): number} next\n * @property {function(): Promise<number>} nextAsync\n * @property {function(): number} runAll\n * @property {function(): number} runToFrame\n * @property {function(): Promise<number>} runAllAsync\n * @property {function(): number} runToLast\n * @property {function(): Promise<number>} runToLastAsync\n * @property {function(): void} reset\n * @property {function(number | Date): void} setSystemTime\n * @property {function(number): void} jump\n * @property {Performance} performance\n * @property {function(number[]): number[]} hrtime - process.hrtime (legacy)\n * @property {function(): void} uninstall Uninstall the clock.\n * @property {Function[]} methods - the methods that are faked\n * @property {boolean} [shouldClearNativeTimers] inherited from config\n * @property {{methodName:string, original:any}[] | undefined} timersModuleMethods\n */\n/* eslint-enable jsdoc/require-property-description */\n\n/**\n * Configuration object for the `install` method.\n *\n * @typedef {object} Config\n * @property {number|Date} [now] a number (in milliseconds) or a Date object (default epoch)\n * @property {string[]} [toFake] names of the methods that should be faked.\n * @property {number} [loopLimit] the maximum number of timers that will be run when calling runAll()\n * @property {boolean} [shouldAdvanceTime] tells FakeTimers to increment mocked time automatically (default false)\n * @property {number} [advanceTimeDelta] increment mocked time every <<advanceTimeDelta>> ms (default: 20ms)\n * @property {boolean} [shouldClearNativeTimers] forwards clear timer calls to native functions if they are not fakes (default: false)\n */\n\n/* eslint-disable jsdoc/require-property-description */\n/**\n * The internal structure to describe a scheduled fake timer\n *\n * @typedef {object} Timer\n * @property {Function} func\n * @property {*[]} args\n * @property {number} delay\n * @property {number} callAt\n * @property {number} createdAt\n * @property {boolean} immediate\n * @property {number} id\n * @property {Error} [error]\n */\n\n/**\n * A Node timer\n *\n * @typedef {object} NodeImmediate\n * @property {function(): boolean} hasRef\n * @property {function(): NodeImmediate} ref\n * @property {function(): NodeImmediate} unref\n */\n/* eslint-enable jsdoc/require-property-description */\n\n/**\n * Mocks available features in the specified global namespace.\n *\n * @param {*} _global Namespace to mock (e.g. `window`)\n * @returns {FakeTimers}\n */\nfunction withGlobal(_global) {\n const maxTimeout = Math.pow(2, 31) - 1; //see https://heycam.github.io/webidl/#abstract-opdef-converttoint\n const idCounterStart = 1e12; // arbitrarily large number to avoid collisions with native timer IDs\n const NOOP = function () {\n return undefined;\n };\n const NOOP_ARRAY = function () {\n return [];\n };\n const timeoutResult = _global.setTimeout(NOOP, 0);\n const addTimerReturnsObject = typeof timeoutResult === "object";\n const hrtimePresent =\n _global.process && typeof _global.process.hrtime === "function";\n const hrtimeBigintPresent =\n hrtimePresent && typeof _global.process.hrtime.bigint === "function";\n const nextTickPresent =\n _global.process && typeof _global.process.nextTick === "function";\n const utilPromisify = _global.process && require("util").promisify;\n const performancePresent =\n _global.performance && typeof _global.performance.now === "function";\n const hasPerformancePrototype =\n _global.Performance &&\n (typeof _global.Performance).match(/^(function|object)$/);\n const hasPerformanceConstructorPrototype =\n _global.performance &&\n _global.performance.constructor &&\n _global.performance.constructor.prototype;\n const queueMicrotaskPresent = _global.hasOwnProperty("queueMicrotask");\n const requestAnimationFramePresent =\n _global.requestAnimationFrame &&\n typeof _global.requestAnimationFrame === "function";\n const cancelAnimationFramePresent =\n _global.cancelAnimationFrame &&\n typeof _global.cancelAnimationFrame === "function";\n const requestIdleCallbackPresent =\n _global.requestIdleCallback &&\n typeof _global.requestIdleCallback === "function";\n const cancelIdleCallbackPresent =\n _global.cancelIdleCallback &&\n typeof _global.cancelIdleCallback === "function";\n const setImmediatePresent =\n _global.setImmediate && typeof _global.setImmediate === "function";\n const intlPresent = _global.Intl && typeof _global.Intl === "object";\n\n _global.clearTimeout(timeoutResult);\n\n const NativeDate = _global.Date;\n const NativeIntl = _global.Intl;\n let uniqueTimerId = idCounterStart;\n\n /**\n * @param {number} num\n * @returns {boolean}\n */\n function isNumberFinite(num) {\n if (Number.isFinite) {\n return Number.isFinite(num);\n }\n\n return isFinite(num);\n }\n\n let isNearInfiniteLimit = false;\n\n /**\n * @param {Clock} clock\n * @param {number} i\n */\n function checkIsNearInfiniteLimit(clock, i) {\n if (clock.loopLimit && i === clock.loopLimit - 1) {\n isNearInfiniteLimit = true;\n }\n }\n\n /**\n *\n */\n function resetIsNearInfiniteLimit() {\n isNearInfiniteLimit = false;\n }\n\n /**\n * Parse strings like "01:10:00" (meaning 1 hour, 10 minutes, 0 seconds) into\n * number of milliseconds. This is used to support human-readable strings passed\n * to clock.tick()\n *\n * @param {string} str\n * @returns {number}\n */\n function parseTime(str) {\n if (!str) {\n return 0;\n }\n\n const strings = str.split(":");\n const l = strings.length;\n let i = l;\n let ms = 0;\n let parsed;\n\n if (l > 3 || !/^(\\d\\d:){0,2}\\d\\d?$/.test(str)) {\n throw new Error(\n "tick only understands numbers, \'m:s\' and \'h:m:s\'. Each part must be two digits",\n );\n }\n\n while (i--) {\n parsed = parseInt(strings[i], 10);\n\n if (parsed >= 60) {\n throw new Error(`Invalid time ${str}`);\n }\n\n ms += parsed * Math.pow(60, l - i - 1);\n }\n\n return ms * 1000;\n }\n\n /**\n * Get the decimal part of the millisecond value as nanoseconds\n *\n * @param {number} msFloat the number of milliseconds\n * @returns {number} an integer number of nanoseconds in the range [0,1e6)\n *\n * Example: nanoRemainer(123.456789) -> 456789\n */\n function nanoRemainder(msFloat) {\n const modulo = 1e6;\n const remainder = (msFloat * 1e6) % modulo;\n const positiveRemainder =\n remainder < 0 ? remainder + modulo : remainder;\n\n return Math.floor(positiveRemainder);\n }\n\n /**\n * Used to grok the `now` parameter to createClock.\n *\n * @param {Date|number} epoch the system time\n * @returns {number}\n */\n function getEpoch(epoch) {\n if (!epoch) {\n return 0;\n }\n if (typeof epoch.getTime === "function") {\n return epoch.getTime();\n }\n if (typeof epoch === "number") {\n return epoch;\n }\n throw new TypeError("now should be milliseconds since UNIX epoch");\n }\n\n /**\n * @param {number} from\n * @param {number} to\n * @param {Timer} timer\n * @returns {boolean}\n */\n function inRange(from, to, timer) {\n return timer && timer.callAt >= from && timer.callAt <= to;\n }\n\n /**\n * @param {Clock} clock\n * @param {Timer} job\n */\n function getInfiniteLoopError(clock, job) {\n const infiniteLoopError = new Error(\n `Aborting after running ${clock.loopLimit} timers, assuming an infinite loop!`,\n );\n\n if (!job.error) {\n return infiniteLoopError;\n }\n\n // pattern never matched in Node\n const computedTargetPattern = /target\\.*[<|(|[].*?[>|\\]|)]\\s*/;\n let clockMethodPattern = new RegExp(\n String(Object.keys(clock).join("|")),\n );\n\n if (addTimerReturnsObject) {\n // node.js environment\n clockMethodPattern = new RegExp(\n `\\\\s+at (Object\\\\.)?(?:${Object.keys(clock).join("|")})\\\\s+`,\n );\n }\n\n let matchedLineIndex = -1;\n job.error.stack.split("\\n").some(function (line, i) {\n // If we\'ve matched a computed target line (e.g. setTimeout) then we\n // don\'t need to look any further. Return true to stop iterating.\n const matchedComputedTarget = line.match(computedTargetPattern);\n /* istanbul ignore if */\n if (matchedComputedTarget) {\n matchedLineIndex = i;\n return true;\n }\n\n // If we\'ve matched a clock method line, then there may still be\n // others further down the trace. Return false to keep iterating.\n const matchedClockMethod = line.match(clockMethodPattern);\n if (matchedClockMethod) {\n matchedLineIndex = i;\n return false;\n }\n\n // If we haven\'t matched anything on this line, but we matched\n // previously and set the matched line index, then we can stop.\n // If we haven\'t matched previously, then we should keep iterating.\n return matchedLineIndex >= 0;\n });\n\n const stack = `${infiniteLoopError}\\n${job.type || "Microtask"} - ${\n job.func.name || "anonymous"\n }\\n${job.error.stack\n .split("\\n")\n .slice(matchedLineIndex + 1)\n .join("\\n")}`;\n\n try {\n Object.defineProperty(infiniteLoopError, "stack", {\n value: stack,\n });\n } catch (e) {\n // noop\n }\n\n return infiniteLoopError;\n }\n\n /**\n * @param {Date} target\n * @param {Date} source\n * @returns {Date} the target after modifications\n */\n function mirrorDateProperties(target, source) {\n let prop;\n for (prop in source) {\n if (source.hasOwnProperty(prop)) {\n target[prop] = source[prop];\n }\n }\n\n // set special now implementation\n if (source.now) {\n target.now = function now() {\n return target.clock.now;\n };\n } else {\n delete target.now;\n }\n\n // set special toSource implementation\n if (source.toSource) {\n target.toSource = function toSource() {\n return source.toSource();\n };\n } else {\n delete target.toSource;\n }\n\n // set special toString implementation\n target.toString = function toString() {\n return source.toString();\n };\n\n target.prototype = source.prototype;\n target.parse = source.parse;\n target.UTC = source.UTC;\n target.prototype.toUTCString = source.prototype.toUTCString;\n target.isFake = true;\n\n return target;\n }\n\n //eslint-disable-next-line jsdoc/require-jsdoc\n function createDate() {\n /**\n * @param {number} year\n * @param {number} month\n * @param {number} date\n * @param {number} hour\n * @param {number} minute\n * @param {number} second\n * @param {number} ms\n * @returns {Date}\n */\n function ClockDate(year, month, date, hour, minute, second, ms) {\n // the Date constructor called as a function, ref Ecma-262 Edition 5.1, section 15.9.2.\n // This remains so in the 10th edition of 2019 as well.\n if (!(this instanceof ClockDate)) {\n return new NativeDate(ClockDate.clock.now).toString();\n }\n\n // if Date is called as a constructor with \'new\' keyword\n // Defensive and verbose to avoid potential harm in passing\n // explicit undefined when user does not pass argument\n switch (arguments.length) {\n case 0:\n return new NativeDate(ClockDate.clock.now);\n case 1:\n return new NativeDate(year);\n case 2:\n return new NativeDate(year, month);\n case 3:\n return new NativeDate(year, month, date);\n case 4:\n return new NativeDate(year, month, date, hour);\n case 5:\n return new NativeDate(year, month, date, hour, minute);\n case 6:\n return new NativeDate(\n year,\n month,\n date,\n hour,\n minute,\n second,\n );\n default:\n return new NativeDate(\n year,\n month,\n date,\n hour,\n minute,\n second,\n ms,\n );\n }\n }\n\n return mirrorDateProperties(ClockDate, NativeDate);\n }\n\n /**\n * Mirror Intl by default on our fake implementation\n *\n * Most of the properties are the original native ones,\n * but we need to take control of those that have a\n * dependency on the current clock.\n *\n * @returns {object} the partly fake Intl implementation\n */\n function createIntl() {\n const ClockIntl = {};\n /*\n * All properties of Intl are non-enumerable, so we need\n * to do a bit of work to get them out.\n */\n Object.getOwnPropertyNames(NativeIntl).forEach(\n (property) => (ClockIntl[property] = NativeIntl[property]),\n );\n\n ClockIntl.DateTimeFormat = function (...args) {\n const realFormatter = new NativeIntl.DateTimeFormat(...args);\n const formatter = {};\n\n ["formatRange", "formatRangeToParts", "resolvedOptions"].forEach(\n (method) => {\n formatter[method] =\n realFormatter[method].bind(realFormatter);\n },\n );\n\n ["format", "formatToParts"].forEach((method) => {\n formatter[method] = function (date) {\n return realFormatter[method](date || ClockIntl.clock.now);\n };\n });\n\n return formatter;\n };\n\n ClockIntl.DateTimeFormat.prototype = Object.create(\n NativeIntl.DateTimeFormat.prototype,\n );\n\n ClockIntl.DateTimeFormat.supportedLocalesOf =\n NativeIntl.DateTimeFormat.supportedLocalesOf;\n\n return ClockIntl;\n }\n\n //eslint-disable-next-line jsdoc/require-jsdoc\n function enqueueJob(clock, job) {\n // enqueues a microtick-deferred task - ecma262/#sec-enqueuejob\n if (!clock.jobs) {\n clock.jobs = [];\n }\n clock.jobs.push(job);\n }\n\n //eslint-disable-next-line jsdoc/require-jsdoc\n function runJobs(clock) {\n // runs all microtick-deferred tasks - ecma262/#sec-runjobs\n if (!clock.jobs) {\n return;\n }\n for (let i = 0; i < clock.jobs.length; i++) {\n const job = clock.jobs[i];\n job.func.apply(null, job.args);\n\n checkIsNearInfiniteLimit(clock, i);\n if (clock.loopLimit && i > clock.loopLimit) {\n throw getInfiniteLoopError(clock, job);\n }\n }\n resetIsNearInfiniteLimit();\n clock.jobs = [];\n }\n\n /**\n * @param {Clock} clock\n * @param {Timer} timer\n * @returns {number} id of the created timer\n */\n function addTimer(clock, timer) {\n if (timer.func === undefined) {\n throw new Error("Callback must be provided to timer calls");\n }\n\n if (addTimerReturnsObject) {\n // Node.js environment\n if (typeof timer.func !== "function") {\n throw new TypeError(\n `[ERR_INVALID_CALLBACK]: Callback must be a function. Received ${\n timer.func\n } of type ${typeof timer.func}`,\n );\n }\n }\n\n if (isNearInfiniteLimit) {\n timer.error = new Error();\n }\n\n timer.type = timer.immediate ? "Immediate" : "Timeout";\n\n if (timer.hasOwnProperty("delay")) {\n if (typeof timer.delay !== "number") {\n timer.delay = parseInt(timer.delay, 10);\n }\n\n if (!isNumberFinite(timer.delay)) {\n timer.delay = 0;\n }\n timer.delay = timer.delay > maxTimeout ? 1 : timer.delay;\n timer.delay = Math.max(0, timer.delay);\n }\n\n if (timer.hasOwnProperty("interval")) {\n timer.type = "Interval";\n timer.interval = timer.interval > maxTimeout ? 1 : timer.interval;\n }\n\n if (timer.hasOwnProperty("animation")) {\n timer.type = "AnimationFrame";\n timer.animation = true;\n }\n\n if (timer.hasOwnProperty("idleCallback")) {\n timer.type = "IdleCallback";\n timer.idleCallback = true;\n }\n\n if (!clock.timers) {\n clock.timers = {};\n }\n\n timer.id = uniqueTimerId++;\n timer.createdAt = clock.now;\n timer.callAt =\n clock.now + (parseInt(timer.delay) || (clock.duringTick ? 1 : 0));\n\n clock.timers[timer.id] = timer;\n\n if (addTimerReturnsObject) {\n const res = {\n refed: true,\n ref: function () {\n this.refed = true;\n return res;\n },\n unref: function () {\n this.refed = false;\n return res;\n },\n hasRef: function () {\n return this.refed;\n },\n refresh: function () {\n timer.callAt =\n clock.now +\n (parseInt(timer.delay) || (clock.duringTick ? 1 : 0));\n\n // it _might_ have been removed, but if not the assignment is perfectly fine\n clock.timers[timer.id] = timer;\n\n return res;\n },\n [Symbol.toPrimitive]: function () {\n return timer.id;\n },\n };\n return res;\n }\n\n return timer.id;\n }\n\n /* eslint consistent-return: "off" */\n /**\n * Timer comparitor\n *\n * @param {Timer} a\n * @param {Timer} b\n * @returns {number}\n */\n function compareTimers(a, b) {\n // Sort first by absolute timing\n if (a.callAt < b.callAt) {\n return -1;\n }\n if (a.callAt > b.callAt) {\n return 1;\n }\n\n // Sort next by immediate, immediate timers take precedence\n if (a.immediate && !b.immediate) {\n return -1;\n }\n if (!a.immediate && b.immediate) {\n return 1;\n }\n\n // Sort next by creation time, earlier-created timers take precedence\n if (a.createdAt < b.createdAt) {\n return -1;\n }\n if (a.createdAt > b.createdAt) {\n return 1;\n }\n\n // Sort next by id, lower-id timers take precedence\n if (a.id < b.id) {\n return -1;\n }\n if (a.id > b.id) {\n return 1;\n }\n\n // As timer ids are unique, no fallback `0` is necessary\n }\n\n /**\n * @param {Clock} clock\n * @param {number} from\n * @param {number} to\n * @returns {Timer}\n */\n function firstTimerInRange(clock, from, to) {\n const timers = clock.timers;\n let timer = null;\n let id, isInRange;\n\n for (id in timers) {\n if (timers.hasOwnProperty(id)) {\n isInRange = inRange(from, to, timers[id]);\n\n if (\n isInRange &&\n (!timer || compareTimers(timer, timers[id]) === 1)\n ) {\n timer = timers[id];\n }\n }\n }\n\n return timer;\n }\n\n /**\n * @param {Clock} clock\n * @returns {Timer}\n */\n function firstTimer(clock) {\n const timers = clock.timers;\n let timer = null;\n let id;\n\n for (id in timers) {\n if (timers.hasOwnProperty(id)) {\n if (!timer || compareTimers(timer, timers[id]) === 1) {\n timer = timers[id];\n }\n }\n }\n\n return timer;\n }\n\n /**\n * @param {Clock} clock\n * @returns {Timer}\n */\n function lastTimer(clock) {\n const timers = clock.timers;\n let timer = null;\n let id;\n\n for (id in timers) {\n if (timers.hasOwnProperty(id)) {\n if (!timer || compareTimers(timer, timers[id]) === -1) {\n timer = timers[id];\n }\n }\n }\n\n return timer;\n }\n\n /**\n * @param {Clock} clock\n * @param {Timer} timer\n */\n function callTimer(clock, timer) {\n if (typeof timer.interval === "number") {\n clock.timers[timer.id].callAt += timer.interval;\n } else {\n delete clock.timers[timer.id];\n }\n\n if (typeof timer.func === "function") {\n timer.func.apply(null, timer.args);\n } else {\n /* eslint no-eval: "off" */\n const eval2 = eval;\n (function () {\n eval2(timer.func);\n })();\n }\n }\n\n /**\n * Gets clear handler name for a given timer type\n *\n * @param {string} ttype\n */\n function getClearHandler(ttype) {\n if (ttype === "IdleCallback" || ttype === "AnimationFrame") {\n return `cancel${ttype}`;\n }\n return `clear${ttype}`;\n }\n\n /**\n * Gets schedule handler name for a given timer type\n *\n * @param {string} ttype\n */\n function getScheduleHandler(ttype) {\n if (ttype === "IdleCallback" || ttype === "AnimationFrame") {\n return `request${ttype}`;\n }\n return `set${ttype}`;\n }\n\n /**\n * Creates an anonymous function to warn only once\n */\n function createWarnOnce() {\n let calls = 0;\n return function (msg) {\n // eslint-disable-next-line\n !calls++ && console.warn(msg);\n };\n }\n const warnOnce = createWarnOnce();\n\n /**\n * @param {Clock} clock\n * @param {number} timerId\n * @param {string} ttype\n */\n function clearTimer(clock, timerId, ttype) {\n if (!timerId) {\n // null appears to be allowed in most browsers, and appears to be\n // relied upon by some libraries, like Bootstrap carousel\n return;\n }\n\n if (!clock.timers) {\n clock.timers = {};\n }\n\n // in Node, the ID is stored as the primitive value for `Timeout` objects\n // for `Immediate` objects, no ID exists, so it gets coerced to NaN\n const id = Number(timerId);\n\n if (Number.isNaN(id) || id < idCounterStart) {\n const handlerName = getClearHandler(ttype);\n\n if (clock.shouldClearNativeTimers === true) {\n const nativeHandler = clock[`_${handlerName}`];\n return typeof nativeHandler === "function"\n ? nativeHandler(timerId)\n : undefined;\n }\n warnOnce(\n `FakeTimers: ${handlerName} was invoked to clear a native timer instead of one created by this library.` +\n "\\nTo automatically clean-up native timers, use `shouldClearNativeTimers`.",\n );\n }\n\n if (clock.timers.hasOwnProperty(id)) {\n // check that the ID matches a timer of the correct type\n const timer = clock.timers[id];\n if (\n timer.type === ttype ||\n (timer.type === "Timeout" && ttype === "Interval") ||\n (timer.type === "Interval" && ttype === "Timeout")\n ) {\n delete clock.timers[id];\n } else {\n const clear = getClearHandler(ttype);\n const schedule = getScheduleHandler(timer.type);\n throw new Error(\n `Cannot clear timer: timer created with ${schedule}() but cleared with ${clear}()`,\n );\n }\n }\n }\n\n /**\n * @param {Clock} clock\n * @param {Config} config\n * @returns {Timer[]}\n */\n function uninstall(clock, config) {\n let method, i, l;\n const installedHrTime = "_hrtime";\n const installedNextTick = "_nextTick";\n\n for (i = 0, l = clock.methods.length; i < l; i++) {\n method = clock.methods[i];\n if (method === "hrtime" && _global.process) {\n _global.process.hrtime = clock[installedHrTime];\n } else if (method === "nextTick" && _global.process) {\n _global.process.nextTick = clock[installedNextTick];\n } else if (method === "performance") {\n const originalPerfDescriptor = Object.getOwnPropertyDescriptor(\n clock,\n `_${method}`,\n );\n if (\n originalPerfDescriptor &&\n originalPerfDescriptor.get &&\n !originalPerfDescriptor.set\n ) {\n Object.defineProperty(\n _global,\n method,\n originalPerfDescriptor,\n );\n } else if (originalPerfDescriptor.configurable) {\n _global[method] = clock[`_${method}`];\n }\n } else {\n if (_global[method] && _global[method].hadOwnProperty) {\n _global[method] = clock[`_${method}`];\n } else {\n try {\n delete _global[method];\n } catch (ignore) {\n /* eslint no-empty: "off" */\n }\n }\n }\n }\n\n if (config.shouldAdvanceTime === true) {\n _global.clearInterval(clock.attachedInterval);\n }\n\n // Prevent multiple executions which will completely remove these props\n clock.methods = [];\n\n // return pending timers, to enable checking what timers remained on uninstall\n if (!clock.timers) {\n return [];\n }\n return Object.keys(clock.timers).map(function mapper(key) {\n return clock.timers[key];\n });\n }\n\n /**\n * @param {object} target the target containing the method to replace\n * @param {string} method the keyname of the method on the target\n * @param {Clock} clock\n */\n function hijackMethod(target, method, clock) {\n clock[method].hadOwnProperty = Object.prototype.hasOwnProperty.call(\n target,\n method,\n );\n clock[`_${method}`] = target[method];\n\n if (method === "Date") {\n const date = mirrorDateProperties(clock[method], target[method]);\n target[method] = date;\n } else if (method === "Intl") {\n target[method] = clock[method];\n } else if (method === "performance") {\n const originalPerfDescriptor = Object.getOwnPropertyDescriptor(\n target,\n method,\n );\n // JSDOM has a read only performance field so we have to save/copy it differently\n if (\n originalPerfDescriptor &&\n originalPerfDescriptor.get &&\n !originalPerfDescriptor.set\n ) {\n Object.defineProperty(\n clock,\n `_${method}`,\n originalPerfDescriptor,\n );\n\n const perfDescriptor = Object.getOwnPropertyDescriptor(\n clock,\n method,\n );\n Object.defineProperty(target, method, perfDescriptor);\n } else {\n target[method] = clock[method];\n }\n } else {\n target[method] = function () {\n return clock[method].apply(clock, arguments);\n };\n\n Object.defineProperties(\n target[method],\n Object.getOwnPropertyDescriptors(clock[method]),\n );\n }\n\n target[method].clock = clock;\n }\n\n /**\n * @param {Clock} clock\n * @param {number} advanceTimeDelta\n */\n function doIntervalTick(clock, advanceTimeDelta) {\n clock.tick(advanceTimeDelta);\n }\n\n /**\n * @typedef {object} Timers\n * @property {setTimeout} setTimeout\n * @property {clearTimeout} clearTimeout\n * @property {setInterval} setInterval\n * @property {clearInterval} clearInterval\n * @property {Date} Date\n * @property {Intl} Intl\n * @property {SetImmediate=} setImmediate\n * @property {function(NodeImmediate): void=} clearImmediate\n * @property {function(number[]):number[]=} hrtime\n * @property {NextTick=} nextTick\n * @property {Performance=} performance\n * @property {RequestAnimationFrame=} requestAnimationFrame\n * @property {boolean=} queueMicrotask\n * @property {function(number): void=} cancelAnimationFrame\n * @property {RequestIdleCallback=} requestIdleCallback\n * @property {function(number): void=} cancelIdleCallback\n */\n\n /** @type {Timers} */\n const timers = {\n setTimeout: _global.setTimeout,\n clearTimeout: _global.clearTimeout,\n setInterval: _global.setInterval,\n clearInterval: _global.clearInterval,\n Date: _global.Date,\n };\n\n if (setImmediatePresent) {\n timers.setImmediate = _global.setImmediate;\n timers.clearImmediate = _global.clearImmediate;\n }\n\n if (hrtimePresent) {\n timers.hrtime = _global.process.hrtime;\n }\n\n if (nextTickPresent) {\n timers.nextTick = _global.process.nextTick;\n }\n\n if (performancePresent) {\n timers.performance = _global.performance;\n }\n\n if (requestAnimationFramePresent) {\n timers.requestAnimationFrame = _global.requestAnimationFrame;\n }\n\n if (queueMicrotaskPresent) {\n timers.queueMicrotask = true;\n }\n\n if (cancelAnimationFramePresent) {\n timers.cancelAnimationFrame = _global.cancelAnimationFrame;\n }\n\n if (requestIdleCallbackPresent) {\n timers.requestIdleCallback = _global.requestIdleCallback;\n }\n\n if (cancelIdleCallbackPresent) {\n timers.cancelIdleCallback = _global.cancelIdleCallback;\n }\n\n if (intlPresent) {\n timers.Intl = _global.Intl;\n }\n\n const originalSetTimeout = _global.setImmediate || _global.setTimeout;\n\n /**\n * @param {Date|number} [start] the system time - non-integer values are floored\n * @param {number} [loopLimit] maximum number of timers that will be run when calling runAll()\n * @returns {Clock}\n */\n function createClock(start, loopLimit) {\n // eslint-disable-next-line no-param-reassign\n start = Math.floor(getEpoch(start));\n // eslint-disable-next-line no-param-reassign\n loopLimit = loopLimit || 1000;\n let nanos = 0;\n const adjustedSystemTime = [0, 0]; // [millis, nanoremainder]\n\n if (NativeDate === undefined) {\n throw new Error(\n "The global scope doesn\'t have a `Date` object" +\n " (see https://github.com/sinonjs/sinon/issues/1852#issuecomment-419622780)",\n );\n }\n\n const clock = {\n now: start,\n Date: createDate(),\n loopLimit: loopLimit,\n };\n\n clock.Date.clock = clock;\n\n //eslint-disable-next-line jsdoc/require-jsdoc\n function getTimeToNextFrame() {\n return 16 - ((clock.now - start) % 16);\n }\n\n //eslint-disable-next-line jsdoc/require-jsdoc\n function hrtime(prev) {\n const millisSinceStart = clock.now - adjustedSystemTime[0] - start;\n const secsSinceStart = Math.floor(millisSinceStart / 1000);\n const remainderInNanos =\n (millisSinceStart - secsSinceStart * 1e3) * 1e6 +\n nanos -\n adjustedSystemTime[1];\n\n if (Array.isArray(prev)) {\n if (prev[1] > 1e9) {\n throw new TypeError(\n "Number of nanoseconds can\'t exceed a billion",\n );\n }\n\n const oldSecs = prev[0];\n let nanoDiff = remainderInNanos - prev[1];\n let secDiff = secsSinceStart - oldSecs;\n\n if (nanoDiff < 0) {\n nanoDiff += 1e9;\n secDiff -= 1;\n }\n\n return [secDiff, nanoDiff];\n }\n return [secsSinceStart, remainderInNanos];\n }\n\n /**\n * A high resolution timestamp in milliseconds.\n *\n * @typedef {number} DOMHighResTimeStamp\n */\n\n /**\n * performance.now()\n *\n * @returns {DOMHighResTimeStamp}\n */\n function fakePerformanceNow() {\n const hrt = hrtime();\n const millis = hrt[0] * 1000 + hrt[1] / 1e6;\n return millis;\n }\n\n if (hrtimeBigintPresent) {\n hrtime.bigint = function () {\n const parts = hrtime();\n return BigInt(parts[0]) * BigInt(1e9) + BigInt(parts[1]); // eslint-disable-line\n };\n }\n\n if (intlPresent) {\n clock.Intl = createIntl();\n clock.Intl.clock = clock;\n }\n\n clock.requestIdleCallback = function requestIdleCallback(\n func,\n timeout,\n ) {\n let timeToNextIdlePeriod = 0;\n\n if (clock.countTimers() > 0) {\n timeToNextIdlePeriod = 50; // const for now\n }\n\n const result = addTimer(clock, {\n func: func,\n args: Array.prototype.slice.call(arguments, 2),\n delay:\n typeof timeout === "undefined"\n ? timeToNextIdlePeriod\n : Math.min(timeout, timeToNextIdlePeriod),\n idleCallback: true,\n });\n\n return Number(result);\n };\n\n clock.cancelIdleCallback = function cancelIdleCallback(timerId) {\n return clearTimer(clock, timerId, "IdleCallback");\n };\n\n clock.setTimeout = function setTimeout(func, timeout) {\n return addTimer(clock, {\n func: func,\n args: Array.prototype.slice.call(arguments, 2),\n delay: timeout,\n });\n };\n if (typeof _global.Promise !== "undefined" && utilPromisify) {\n clock.setTimeout[utilPromisify.custom] =\n function promisifiedSetTimeout(timeout, arg) {\n return new _global.Promise(function setTimeoutExecutor(\n resolve,\n ) {\n addTimer(clock, {\n func: resolve,\n args: [arg],\n delay: timeout,\n });\n });\n };\n }\n\n clock.clearTimeout = function clearTimeout(timerId) {\n return clearTimer(clock, timerId, "Timeout");\n };\n\n clock.nextTick = function nextTick(func) {\n return enqueueJob(clock, {\n func: func,\n args: Array.prototype.slice.call(arguments, 1),\n error: isNearInfiniteLimit ? new Error() : null,\n });\n };\n\n clock.queueMicrotask = function queueMicrotask(func) {\n return clock.nextTick(func); // explicitly drop additional arguments\n };\n\n clock.setInterval = function setInterval(func, timeout) {\n // eslint-disable-next-line no-param-reassign\n timeout = parseInt(timeout, 10);\n return addTimer(clock, {\n func: func,\n args: Array.prototype.slice.call(arguments, 2),\n delay: timeout,\n interval: timeout,\n });\n };\n\n clock.clearInterval = function clearInterval(timerId) {\n return clearTimer(clock, timerId, "Interval");\n };\n\n if (setImmediatePresent) {\n clock.setImmediate = function setImmediate(func) {\n return addTimer(clock, {\n func: func,\n args: Array.prototype.slice.call(arguments, 1),\n immediate: true,\n });\n };\n\n if (typeof _global.Promise !== "undefined" && utilPromisify) {\n clock.setImmediate[utilPromisify.custom] =\n function promisifiedSetImmediate(arg) {\n return new _global.Promise(\n function setImmediateExecutor(resolve) {\n addTimer(clock, {\n func: resolve,\n args: [arg],\n immediate: true,\n });\n },\n );\n };\n }\n\n clock.clearImmediate = function clearImmediate(timerId) {\n return clearTimer(clock, timerId, "Immediate");\n };\n }\n\n clock.countTimers = function countTimers() {\n return (\n Object.keys(clock.timers || {}).length +\n (clock.jobs || []).length\n );\n };\n\n clock.requestAnimationFrame = function requestAnimationFrame(func) {\n const result = addTimer(clock, {\n func: func,\n delay: getTimeToNextFrame(),\n get args() {\n return [fakePerformanceNow()];\n },\n animation: true,\n });\n\n return Number(result);\n };\n\n clock.cancelAnimationFrame = function cancelAnimationFrame(timerId) {\n return clearTimer(clock, timerId, "AnimationFrame");\n };\n\n clock.runMicrotasks = function runMicrotasks() {\n runJobs(clock);\n };\n\n /**\n * @param {number|string} tickValue milliseconds or a string parseable by parseTime\n * @param {boolean} isAsync\n * @param {Function} resolve\n * @param {Function} reject\n * @returns {number|undefined} will return the new `now` value or nothing for async\n */\n function doTick(tickValue, isAsync, resolve, reject) {\n const msFloat =\n typeof tickValue === "number"\n ? tickValue\n : parseTime(tickValue);\n const ms = Math.floor(msFloat);\n const remainder = nanoRemainder(msFloat);\n let nanosTotal = nanos + remainder;\n let tickTo = clock.now + ms;\n\n if (msFloat < 0) {\n throw new TypeError("Negative ticks are not supported");\n }\n\n // adjust for positive overflow\n if (nanosTotal >= 1e6) {\n tickTo += 1;\n nanosTotal -= 1e6;\n }\n\n nanos = nanosTotal;\n let tickFrom = clock.now;\n let previous = clock.now;\n // ESLint fails to detect this correctly\n /* eslint-disable prefer-const */\n let timer,\n firstException,\n oldNow,\n nextPromiseTick,\n compensationCheck,\n postTimerCall;\n /* eslint-enable prefer-const */\n\n clock.duringTick = true;\n\n // perform microtasks\n oldNow = clock.now;\n runJobs(clock);\n if (oldNow !== clock.now) {\n // compensate for any setSystemTime() call during microtask callback\n tickFrom += clock.now - oldNow;\n tickTo += clock.now - oldNow;\n }\n\n //eslint-disable-next-line jsdoc/require-jsdoc\n function doTickInner() {\n // perform each timer in the requested range\n timer = firstTimerInRange(clock, tickFrom, tickTo);\n // eslint-disable-next-line no-unmodified-loop-condition\n while (timer && tickFrom <= tickTo) {\n if (clock.timers[timer.id]) {\n tickFrom = timer.callAt;\n clock.now = timer.callAt;\n oldNow = clock.now;\n try {\n runJobs(clock);\n callTimer(clock, timer);\n } catch (e) {\n firstException = firstException || e;\n }\n\n if (isAsync) {\n // finish up after native setImmediate callback to allow\n // all native es6 promises to process their callbacks after\n // each timer fires.\n originalSetTimeout(nextPromiseTick);\n return;\n }\n\n compensationCheck();\n }\n\n postTimerCall();\n }\n\n // perform process.nextTick()s again\n oldNow = clock.now;\n runJobs(clock);\n if (oldNow !== clock.now) {\n // compensate for any setSystemTime() call during process.nextTick() callback\n tickFrom += clock.now - oldNow;\n tickTo += clock.now - oldNow;\n }\n clock.duringTick = false;\n\n // corner case: during runJobs new timers were scheduled which could be in the range [clock.now, tickTo]\n timer = firstTimerInRange(clock, tickFrom, tickTo);\n if (timer) {\n try {\n clock.tick(tickTo - clock.now); // do it all again - for the remainder of the requested range\n } catch (e) {\n firstException = firstException || e;\n }\n } else {\n // no timers remaining in the requested range: move the clock all the way to the end\n clock.now = tickTo;\n\n // update nanos\n nanos = nanosTotal;\n }\n if (firstException) {\n throw firstException;\n }\n\n if (isAsync) {\n resolve(clock.now);\n } else {\n return clock.now;\n }\n }\n\n nextPromiseTick =\n isAsync &&\n function () {\n try {\n compensationCheck();\n postTimerCall();\n doTickInner();\n } catch (e) {\n reject(e);\n }\n };\n\n compensationCheck = function () {\n // compensate for any setSystemTime() call during timer callback\n if (oldNow !== clock.now) {\n tickFrom += clock.now - oldNow;\n tickTo += clock.now - oldNow;\n previous += clock.now - oldNow;\n }\n };\n\n postTimerCall = function () {\n timer = firstTimerInRange(clock, previous, tickTo);\n previous = tickFrom;\n };\n\n return doTickInner();\n }\n\n /**\n * @param {string|number} tickValue number of milliseconds or a human-readable value like "01:11:15"\n * @returns {number} will return the new `now` value\n */\n clock.tick = function tick(tickValue) {\n return doTick(tickValue, false);\n };\n\n if (typeof _global.Promise !== "undefined") {\n /**\n * @param {string|number} tickValue number of milliseconds or a human-readable value like "01:11:15"\n * @returns {Promise}\n */\n clock.tickAsync = function tickAsync(tickValue) {\n return new _global.Promise(function (resolve, reject) {\n originalSetTimeout(function () {\n try {\n doTick(tickValue, true, resolve, reject);\n } catch (e) {\n reject(e);\n }\n });\n });\n };\n }\n\n clock.next = function next() {\n runJobs(clock);\n const timer = firstTimer(clock);\n if (!timer) {\n return clock.now;\n }\n\n clock.duringTick = true;\n try {\n clock.now = timer.callAt;\n callTimer(clock, timer);\n runJobs(clock);\n return clock.now;\n } finally {\n clock.duringTick = false;\n }\n };\n\n if (typeof _global.Promise !== "undefined") {\n clock.nextAsync = function nextAsync() {\n return new _global.Promise(function (resolve, reject) {\n originalSetTimeout(function () {\n try {\n const timer = firstTimer(clock);\n if (!timer) {\n resolve(clock.now);\n return;\n }\n\n let err;\n clock.duringTick = true;\n clock.now = timer.callAt;\n try {\n callTimer(clock, timer);\n } catch (e) {\n err = e;\n }\n clock.duringTick = false;\n\n originalSetTimeout(function () {\n if (err) {\n reject(err);\n } else {\n resolve(clock.now);\n }\n });\n } catch (e) {\n reject(e);\n }\n });\n });\n };\n }\n\n clock.runAll = function runAll() {\n let numTimers, i;\n runJobs(clock);\n for (i = 0; i < clock.loopLimit; i++) {\n if (!clock.timers) {\n resetIsNearInfiniteLimit();\n return clock.now;\n }\n\n numTimers = Object.keys(clock.timers).length;\n if (numTimers === 0) {\n resetIsNearInfiniteLimit();\n return clock.now;\n }\n\n clock.next();\n checkIsNearInfiniteLimit(clock, i);\n }\n\n const excessJob = firstTimer(clock);\n throw getInfiniteLoopError(clock, excessJob);\n };\n\n clock.runToFrame = function runToFrame() {\n return clock.tick(getTimeToNextFrame());\n };\n\n if (typeof _global.Promise !== "undefined") {\n clock.runAllAsync = function runAllAsync() {\n return new _global.Promise(function (resolve, reject) {\n let i = 0;\n /**\n *\n */\n function doRun() {\n originalSetTimeout(function () {\n try {\n runJobs(clock);\n\n let numTimers;\n if (i < clock.loopLimit) {\n if (!clock.timers) {\n resetIsNearInfiniteLimit();\n resolve(clock.now);\n return;\n }\n\n numTimers = Object.keys(\n clock.timers,\n ).length;\n if (numTimers === 0) {\n resetIsNearInfiniteLimit();\n resolve(clock.now);\n return;\n }\n\n clock.next();\n\n i++;\n\n doRun();\n checkIsNearInfiniteLimit(clock, i);\n return;\n }\n\n const excessJob = firstTimer(clock);\n reject(getInfiniteLoopError(clock, excessJob));\n } catch (e) {\n reject(e);\n }\n });\n }\n doRun();\n });\n };\n }\n\n clock.runToLast = function runToLast() {\n const timer = lastTimer(clock);\n if (!timer) {\n runJobs(clock);\n return clock.now;\n }\n\n return clock.tick(timer.callAt - clock.now);\n };\n\n if (typeof _global.Promise !== "undefined") {\n clock.runToLastAsync = function runToLastAsync() {\n return new _global.Promise(function (resolve, reject) {\n originalSetTimeout(function () {\n try {\n const timer = lastTimer(clock);\n if (!timer) {\n runJobs(clock);\n resolve(clock.now);\n }\n\n resolve(clock.tickAsync(timer.callAt - clock.now));\n } catch (e) {\n reject(e);\n }\n });\n });\n };\n }\n\n clock.reset = function reset() {\n nanos = 0;\n clock.timers = {};\n clock.jobs = [];\n clock.now = start;\n };\n\n clock.setSystemTime = function setSystemTime(systemTime) {\n // determine time difference\n const newNow = getEpoch(systemTime);\n const difference = newNow - clock.now;\n let id, timer;\n\n adjustedSystemTime[0] = adjustedSystemTime[0] + difference;\n adjustedSystemTime[1] = adjustedSystemTime[1] + nanos;\n // update \'system clock\'\n clock.now = newNow;\n nanos = 0;\n\n // update timers and intervals to keep them stable\n for (id in clock.timers) {\n if (clock.timers.hasOwnProperty(id)) {\n timer = clock.timers[id];\n timer.createdAt += difference;\n timer.callAt += difference;\n }\n }\n };\n\n /**\n * @param {string|number} tickValue number of milliseconds or a human-readable value like "01:11:15"\n * @returns {number} will return the new `now` value\n */\n clock.jump = function jump(tickValue) {\n const msFloat =\n typeof tickValue === "number"\n ? tickValue\n : parseTime(tickValue);\n const ms = Math.floor(msFloat);\n\n for (const timer of Object.values(clock.timers)) {\n if (clock.now + ms > timer.callAt) {\n timer.callAt = clock.now + ms;\n }\n }\n clock.tick(ms);\n };\n\n if (performancePresent) {\n clock.performance = Object.create(null);\n clock.performance.now = fakePerformanceNow;\n }\n\n if (hrtimePresent) {\n clock.hrtime = hrtime;\n }\n\n return clock;\n }\n\n /* eslint-disable complexity */\n\n /**\n * @param {Config=} [config] Optional config\n * @returns {Clock}\n */\n function install(config) {\n if (\n arguments.length > 1 ||\n config instanceof Date ||\n Array.isArray(config) ||\n typeof config === "number"\n ) {\n throw new TypeError(\n `FakeTimers.install called with ${String(\n config,\n )} install requires an object parameter`,\n );\n }\n\n if (_global.Date.isFake === true) {\n // Timers are already faked; this is a problem.\n // Make the user reset timers before continuing.\n throw new TypeError(\n "Can\'t install fake timers twice on the same global object.",\n );\n }\n\n // eslint-disable-next-line no-param-reassign\n config = typeof config !== "undefined" ? config : {};\n config.shouldAdvanceTime = config.shouldAdvanceTime || false;\n config.advanceTimeDelta = config.advanceTimeDelta || 20;\n config.shouldClearNativeTimers =\n config.shouldClearNativeTimers || false;\n\n if (config.target) {\n throw new TypeError(\n "config.target is no longer supported. Use `withGlobal(target)` instead.",\n );\n }\n\n let i, l;\n const clock = createClock(config.now, config.loopLimit);\n clock.shouldClearNativeTimers = config.shouldClearNativeTimers;\n\n clock.uninstall = function () {\n return uninstall(clock, config);\n };\n\n clock.methods = config.toFake || [];\n\n if (clock.methods.length === 0) {\n // do not fake nextTick by default - GitHub#126\n clock.methods = Object.keys(timers).filter(function (key) {\n return key !== "nextTick" && key !== "queueMicrotask";\n });\n }\n\n if (config.shouldAdvanceTime === true) {\n const intervalTick = doIntervalTick.bind(\n null,\n clock,\n config.advanceTimeDelta,\n );\n const intervalId = _global.setInterval(\n intervalTick,\n config.advanceTimeDelta,\n );\n clock.attachedInterval = intervalId;\n }\n\n if (clock.methods.includes("performance")) {\n const proto = (() => {\n if (hasPerformanceConstructorPrototype) {\n return _global.performance.constructor.prototype;\n }\n if (hasPerformancePrototype) {\n return _global.Performance.prototype;\n }\n })();\n if (proto) {\n Object.getOwnPropertyNames(proto).forEach(function (name) {\n if (name !== "now") {\n clock.performance[name] =\n name.indexOf("getEntries") === 0\n ? NOOP_ARRAY\n : NOOP;\n }\n });\n } else if ((config.toFake || []).includes("performance")) {\n // user explicitly tried to fake performance when not present\n throw new ReferenceError(\n "non-existent performance object cannot be faked",\n );\n }\n }\n for (i = 0, l = clock.methods.length; i < l; i++) {\n const nameOfMethodToReplace = clock.methods[i];\n if (nameOfMethodToReplace === "hrtime") {\n if (\n _global.process &&\n typeof _global.process.hrtime === "function"\n ) {\n hijackMethod(_global.process, nameOfMethodToReplace, clock);\n }\n } else if (nameOfMethodToReplace === "nextTick") {\n if (\n _global.process &&\n typeof _global.process.nextTick === "function"\n ) {\n hijackMethod(_global.process, nameOfMethodToReplace, clock);\n }\n } else {\n hijackMethod(_global, nameOfMethodToReplace, clock);\n }\n }\n\n return clock;\n }\n\n /* eslint-enable complexity */\n\n return {\n timers: timers,\n createClock: createClock,\n install: install,\n withGlobal: withGlobal,\n };\n}\n\n/**\n * @typedef {object} FakeTimers\n * @property {Timers} timers\n * @property {createClock} createClock\n * @property {Function} install\n * @property {withGlobal} withGlobal\n */\n\n// /** @type {FakeTimers} */\nwindow.__wdio_sinon = withGlobal(globalThis);\n\n// // exports.timers = defaultImplementation.timers;\n// // exports.createClock = defaultImplementation.createClock;\n// // exports.install = defaultImplementation.install;\n// // exports.withGlobal = withGlobal;\n}\n';
|
|
var ClockManager = class {
|
|
#browser;
|
|
#resetFn = () => Promise.resolve();
|
|
#isInstalled = false;
|
|
constructor(browser) {
|
|
this.#browser = browser;
|
|
}
|
|
/**
|
|
* Install fake timers on the browser. If you call the `emulate` command, WebdriverIO will automatically install
|
|
* the fake timers for you. You can use this method to re-install the fake timers if you have called `restore`.
|
|
*
|
|
* @param options {FakeTimerInstallOpts} Options to pass to the fake clock
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async install(options) {
|
|
if (this.#isInstalled) {
|
|
return log5.warn("Fake timers are already installed");
|
|
}
|
|
if (globalThis.window) {
|
|
return;
|
|
}
|
|
const emulateOptions = options || {};
|
|
const functionDeclaration = fakerScript;
|
|
const installOptions = {
|
|
...emulateOptions,
|
|
now: emulateOptions.now && emulateOptions.now instanceof Date ? emulateOptions.now.getTime() : emulateOptions.now
|
|
};
|
|
const [, libScript, restoreInstallScript] = await Promise.all([
|
|
/**
|
|
* install fake timers for current ex
|
|
*/
|
|
this.#browser.executeScript(`return (${functionDeclaration}).apply(null, arguments)`, []).then(() => this.#browser.execute(installFakeTimers, installOptions)),
|
|
/**
|
|
* add preload script to to emulate clock for upcoming page loads
|
|
*/
|
|
this.#browser.scriptAddPreloadScript({ functionDeclaration }),
|
|
this.#browser.addInitScript(installFakeTimers, installOptions)
|
|
]);
|
|
this.#resetFn = async () => Promise.all([
|
|
this.#browser.scriptRemovePreloadScript({ script: libScript.script }),
|
|
this.#browser.execute(uninstallFakeTimers),
|
|
restoreInstallScript
|
|
]);
|
|
this.#isInstalled = true;
|
|
}
|
|
/**
|
|
* Restore all overridden native functions. This is automatically called between tests, so should not
|
|
* generally be needed.
|
|
*
|
|
* ```ts
|
|
* it('should restore the clock', async () => {
|
|
* console.log(new Date()) // returns e.g. 1722560447102
|
|
*
|
|
* const clock = await browser.emulate('clock', { now: new Date(2021, 3, 14) })
|
|
* console.log(await browser.execute(() => new Date().getTime())) // returns 1618383600000
|
|
*
|
|
* await clock.restore()
|
|
* console.log(await browser.execute(() => new Date().getTime())) // returns 1722560447102
|
|
* })
|
|
* ```
|
|
*
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async restore() {
|
|
await this.#resetFn();
|
|
this.#isInstalled = false;
|
|
}
|
|
/**
|
|
* Move the clock the specified number of `milliseconds`. Any timers within the affected range of time will be called.
|
|
* @param ms {number} The number of milliseconds to move the clock.
|
|
*
|
|
* ```ts
|
|
* it('should move the clock', async () => {
|
|
* console.log(new Date()) // returns e.g. 1722560447102
|
|
*
|
|
* const clock = await browser.emulate('clock', { now: new Date(2021, 3, 14) })
|
|
* console.log(await browser.execute(() => new Date().getTime())) // returns 1618383600000
|
|
*
|
|
* await clock.tick(1000)
|
|
* console.log(await browser.execute(() => new Date().getTime())) // returns 1618383601000
|
|
* })
|
|
* ```
|
|
*
|
|
* @param {number} ms The number of milliseconds to move the clock.
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async tick(ms) {
|
|
await this.#browser.execute((ms2) => window.__clock.tick(ms2), ms);
|
|
}
|
|
/**
|
|
* Change the system time to the new now. Now can be a timestamp, date object, or not passed in which defaults
|
|
* to 0. No timers will be called, nor will the time left before they trigger change.
|
|
*
|
|
* ```ts
|
|
* it('should set the system time', async () => {
|
|
* const clock = await browser.emulate('clock', { now: new Date(2021, 3, 14) })
|
|
* console.log(await browser.execute(() => new Date().getTime())) // returns 1618383600000
|
|
*
|
|
* await clock.setSystemTime(new Date(2011, 3, 15))
|
|
* console.log(await browser.execute(() => new Date().getTime())) // returns 1302850800000
|
|
* })
|
|
* ```
|
|
*
|
|
* @param date {Date|number} The new date to set the system time to.
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async setSystemTime(date) {
|
|
const serializableSystemTime = date instanceof Date ? date.getTime() : date;
|
|
await this.#browser.execute((date2) => window.__clock.setSystemTime(date2), serializableSystemTime);
|
|
}
|
|
};
|
|
|
|
// src/deviceDescriptorsSource.ts
|
|
var deviceDescriptorsSource = {
|
|
"Blackberry PlayBook": {
|
|
userAgent: "Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/18.0 Safari/536.2+",
|
|
viewport: {
|
|
width: 600,
|
|
height: 1024
|
|
},
|
|
deviceScaleFactor: 1,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Blackberry PlayBook landscape": {
|
|
userAgent: "Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/18.0 Safari/536.2+",
|
|
viewport: {
|
|
width: 1024,
|
|
height: 600
|
|
},
|
|
deviceScaleFactor: 1,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"BlackBerry Z30": {
|
|
userAgent: "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/18.0 Mobile Safari/537.10+",
|
|
viewport: {
|
|
width: 360,
|
|
height: 640
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"BlackBerry Z30 landscape": {
|
|
userAgent: "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/18.0 Mobile Safari/537.10+",
|
|
viewport: {
|
|
width: 640,
|
|
height: 360
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Galaxy Note 3": {
|
|
userAgent: "Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/18.0 Mobile Safari/534.30",
|
|
viewport: {
|
|
width: 360,
|
|
height: 640
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Galaxy Note 3 landscape": {
|
|
userAgent: "Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/18.0 Mobile Safari/534.30",
|
|
viewport: {
|
|
width: 640,
|
|
height: 360
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Galaxy Note II": {
|
|
userAgent: "Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/18.0 Mobile Safari/534.30",
|
|
viewport: {
|
|
width: 360,
|
|
height: 640
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Galaxy Note II landscape": {
|
|
userAgent: "Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/18.0 Mobile Safari/534.30",
|
|
viewport: {
|
|
width: 640,
|
|
height: 360
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Galaxy S III": {
|
|
userAgent: "Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/18.0 Mobile Safari/534.30",
|
|
viewport: {
|
|
width: 360,
|
|
height: 640
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Galaxy S III landscape": {
|
|
userAgent: "Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/18.0 Mobile Safari/534.30",
|
|
viewport: {
|
|
width: 640,
|
|
height: 360
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Galaxy S5": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 360,
|
|
height: 640
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Galaxy S5 landscape": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 640,
|
|
height: 360
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Galaxy S8": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 360,
|
|
height: 740
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Galaxy S8 landscape": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 740,
|
|
height: 360
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Galaxy S9 +": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 320,
|
|
height: 658
|
|
},
|
|
deviceScaleFactor: 4.5,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Galaxy S9 + landscape": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 658,
|
|
height: 320
|
|
},
|
|
deviceScaleFactor: 4.5,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Galaxy Tab S4": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Safari/537.36",
|
|
viewport: {
|
|
width: 712,
|
|
height: 1138
|
|
},
|
|
deviceScaleFactor: 2.25,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Galaxy Tab S4 landscape": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Safari/537.36",
|
|
viewport: {
|
|
width: 1138,
|
|
height: 712
|
|
},
|
|
deviceScaleFactor: 2.25,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPad(gen 5)": {
|
|
userAgent: "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 768,
|
|
height: 1024
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPad(gen 5) landscape": {
|
|
userAgent: "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 1024,
|
|
height: 768
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPad(gen 6)": {
|
|
userAgent: "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 768,
|
|
height: 1024
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPad(gen 6) landscape": {
|
|
userAgent: "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 1024,
|
|
height: 768
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPad(gen 7)": {
|
|
userAgent: "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 810,
|
|
height: 1080
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPad(gen 7) landscape": {
|
|
userAgent: "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 1080,
|
|
height: 810
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPad Mini": {
|
|
userAgent: "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 768,
|
|
height: 1024
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPad Mini landscape": {
|
|
userAgent: "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 1024,
|
|
height: 768
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPad Pro 11": {
|
|
userAgent: "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 834,
|
|
height: 1194
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPad Pro 11 landscape": {
|
|
userAgent: "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 1194,
|
|
height: 834
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 6": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/18.0 Mobile/15A372 Safari/604.1",
|
|
viewport: {
|
|
width: 375,
|
|
height: 667
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 6 landscape": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/18.0 Mobile/15A372 Safari/604.1",
|
|
viewport: {
|
|
width: 667,
|
|
height: 375
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 6 Plus": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/18.0 Mobile/15A372 Safari/604.1",
|
|
viewport: {
|
|
width: 414,
|
|
height: 736
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 6 Plus landscape": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/18.0 Mobile/15A372 Safari/604.1",
|
|
viewport: {
|
|
width: 736,
|
|
height: 414
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 7": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/18.0 Mobile/15A372 Safari/604.1",
|
|
viewport: {
|
|
width: 375,
|
|
height: 667
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 7 landscape": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/18.0 Mobile/15A372 Safari/604.1",
|
|
viewport: {
|
|
width: 667,
|
|
height: 375
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 7 Plus": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/18.0 Mobile/15A372 Safari/604.1",
|
|
viewport: {
|
|
width: 414,
|
|
height: 736
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 7 Plus landscape": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/18.0 Mobile/15A372 Safari/604.1",
|
|
viewport: {
|
|
width: 736,
|
|
height: 414
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 8": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/18.0 Mobile/15A372 Safari/604.1",
|
|
viewport: {
|
|
width: 375,
|
|
height: 667
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 8 landscape": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/18.0 Mobile/15A372 Safari/604.1",
|
|
viewport: {
|
|
width: 667,
|
|
height: 375
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 8 Plus": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/18.0 Mobile/15A372 Safari/604.1",
|
|
viewport: {
|
|
width: 414,
|
|
height: 736
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 8 Plus landscape": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/18.0 Mobile/15A372 Safari/604.1",
|
|
viewport: {
|
|
width: 736,
|
|
height: 414
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone SE": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/18.0 Mobile/14E304 Safari/602.1",
|
|
viewport: {
|
|
width: 320,
|
|
height: 568
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone SE landscape": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/18.0 Mobile/14E304 Safari/602.1",
|
|
viewport: {
|
|
width: 568,
|
|
height: 320
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone X": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/18.0 Mobile/15A372 Safari/604.1",
|
|
viewport: {
|
|
width: 375,
|
|
height: 812
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone X landscape": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/18.0 Mobile/15A372 Safari/604.1",
|
|
viewport: {
|
|
width: 812,
|
|
height: 375
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone XR": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 414,
|
|
height: 896
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone XR landscape": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 896,
|
|
height: 414
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 11": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 414,
|
|
height: 715
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 11 landscape": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 800,
|
|
height: 364
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 11 Pro": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 375,
|
|
height: 635
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 11 Pro landscape": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 724,
|
|
height: 325
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 11 Pro Max": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 414,
|
|
height: 715
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 11 Pro Max landscape": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 808,
|
|
height: 364
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 12": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 390,
|
|
height: 664
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 12 landscape": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 750,
|
|
height: 340
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 12 Pro": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 390,
|
|
height: 664
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 12 Pro landscape": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 750,
|
|
height: 340
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 12 Pro Max": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 428,
|
|
height: 746
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 12 Pro Max landscape": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 832,
|
|
height: 378
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 12 Mini": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 375,
|
|
height: 629
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 12 Mini landscape": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 712,
|
|
height: 325
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 13": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 390,
|
|
height: 664
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 13 landscape": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 750,
|
|
height: 342
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 13 Pro": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 390,
|
|
height: 664
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 13 Pro landscape": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 750,
|
|
height: 342
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 13 Pro Max": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 428,
|
|
height: 746
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 13 Pro Max landscape": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 832,
|
|
height: 380
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 13 Mini": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 375,
|
|
height: 629
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 13 Mini landscape": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 712,
|
|
height: 327
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 14": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 390,
|
|
height: 664
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 14 landscape": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 750,
|
|
height: 340
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 14 Plus": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 428,
|
|
height: 746
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 14 Plus landscape": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 832,
|
|
height: 378
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 14 Pro": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 393,
|
|
height: 660
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 14 Pro landscape": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 734,
|
|
height: 343
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 14 Pro Max": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 430,
|
|
height: 740
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 14 Pro Max landscape": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 814,
|
|
height: 380
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 15": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 393,
|
|
height: 659
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 15 landscape": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 734,
|
|
height: 343
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 15 Plus": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 430,
|
|
height: 739
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 15 Plus landscape": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 814,
|
|
height: 380
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 15 Pro": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 393,
|
|
height: 659
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 15 Pro landscape": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 734,
|
|
height: 343
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 15 Pro Max": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 430,
|
|
height: 739
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"iPhone 15 Pro Max landscape": {
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
viewport: {
|
|
width: 814,
|
|
height: 380
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Kindle Fire HDX": {
|
|
userAgent: "Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true",
|
|
viewport: {
|
|
width: 800,
|
|
height: 1280
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Kindle Fire HDX landscape": {
|
|
userAgent: "Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true",
|
|
viewport: {
|
|
width: 1280,
|
|
height: 800
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"LG Optimus L70": {
|
|
userAgent: "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 384,
|
|
height: 640
|
|
},
|
|
deviceScaleFactor: 1.25,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"LG Optimus L70 landscape": {
|
|
userAgent: "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 640,
|
|
height: 384
|
|
},
|
|
deviceScaleFactor: 1.25,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Microsoft Lumia 550": {
|
|
userAgent: "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36 Edge/14.14263",
|
|
viewport: {
|
|
width: 640,
|
|
height: 360
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Microsoft Lumia 550 landscape": {
|
|
userAgent: "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36 Edge/14.14263",
|
|
viewport: {
|
|
width: 360,
|
|
height: 640
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Microsoft Lumia 950": {
|
|
userAgent: "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36 Edge/14.14263",
|
|
viewport: {
|
|
width: 360,
|
|
height: 640
|
|
},
|
|
deviceScaleFactor: 4,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Microsoft Lumia 950 landscape": {
|
|
userAgent: "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36 Edge/14.14263",
|
|
viewport: {
|
|
width: 640,
|
|
height: 360
|
|
},
|
|
deviceScaleFactor: 4,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Nexus 10": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Safari/537.36",
|
|
viewport: {
|
|
width: 800,
|
|
height: 1280
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Nexus 10 landscape": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Safari/537.36",
|
|
viewport: {
|
|
width: 1280,
|
|
height: 800
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Nexus 4": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 384,
|
|
height: 640
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Nexus 4 landscape": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 640,
|
|
height: 384
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Nexus 5": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 360,
|
|
height: 640
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Nexus 5 landscape": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 640,
|
|
height: 360
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Nexus 5X": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 412,
|
|
height: 732
|
|
},
|
|
deviceScaleFactor: 2.625,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Nexus 5X landscape": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 732,
|
|
height: 412
|
|
},
|
|
deviceScaleFactor: 2.625,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Nexus 6": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 412,
|
|
height: 732
|
|
},
|
|
deviceScaleFactor: 3.5,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Nexus 6 landscape": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 732,
|
|
height: 412
|
|
},
|
|
deviceScaleFactor: 3.5,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Nexus 6P": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 412,
|
|
height: 732
|
|
},
|
|
deviceScaleFactor: 3.5,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Nexus 6P landscape": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 732,
|
|
height: 412
|
|
},
|
|
deviceScaleFactor: 3.5,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Nexus 7": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Safari/537.36",
|
|
viewport: {
|
|
width: 600,
|
|
height: 960
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Nexus 7 landscape": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Safari/537.36",
|
|
viewport: {
|
|
width: 960,
|
|
height: 600
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Nokia Lumia 520": {
|
|
userAgent: "Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)",
|
|
viewport: {
|
|
width: 320,
|
|
height: 533
|
|
},
|
|
deviceScaleFactor: 1.5,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Nokia Lumia 520 landscape": {
|
|
userAgent: "Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)",
|
|
viewport: {
|
|
width: 533,
|
|
height: 320
|
|
},
|
|
deviceScaleFactor: 1.5,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Nokia N9": {
|
|
userAgent: "Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13",
|
|
viewport: {
|
|
width: 480,
|
|
height: 854
|
|
},
|
|
deviceScaleFactor: 1,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Nokia N9 landscape": {
|
|
userAgent: "Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13",
|
|
viewport: {
|
|
width: 854,
|
|
height: 480
|
|
},
|
|
deviceScaleFactor: 1,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Pixel 2": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 411,
|
|
height: 731
|
|
},
|
|
deviceScaleFactor: 2.625,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Pixel 2 landscape": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 731,
|
|
height: 411
|
|
},
|
|
deviceScaleFactor: 2.625,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Pixel 2 XL": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 411,
|
|
height: 823
|
|
},
|
|
deviceScaleFactor: 3.5,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Pixel 2 XL landscape": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 823,
|
|
height: 411
|
|
},
|
|
deviceScaleFactor: 3.5,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Pixel 3": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 393,
|
|
height: 786
|
|
},
|
|
deviceScaleFactor: 2.75,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Pixel 3 landscape": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 786,
|
|
height: 393
|
|
},
|
|
deviceScaleFactor: 2.75,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Pixel 4": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 353,
|
|
height: 745
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Pixel 4 landscape": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 745,
|
|
height: 353
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Pixel 4a(5G)": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 412,
|
|
height: 765
|
|
},
|
|
deviceScaleFactor: 2.63,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Pixel 4a(5G) landscape": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 840,
|
|
height: 312
|
|
},
|
|
deviceScaleFactor: 2.63,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Pixel 5": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 393,
|
|
height: 727
|
|
},
|
|
deviceScaleFactor: 2.75,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Pixel 5 landscape": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 802,
|
|
height: 293
|
|
},
|
|
deviceScaleFactor: 2.75,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Pixel 7": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 412,
|
|
height: 839
|
|
},
|
|
deviceScaleFactor: 2.625,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Pixel 7 landscape": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 863,
|
|
height: 360
|
|
},
|
|
deviceScaleFactor: 2.625,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Moto G4": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 360,
|
|
height: 640
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Moto G4 landscape": {
|
|
userAgent: "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
|
viewport: {
|
|
width: 640,
|
|
height: 360
|
|
},
|
|
deviceScaleFactor: 3,
|
|
isMobile: true,
|
|
hasTouch: true
|
|
},
|
|
"Desktop Chrome HiDPI": {
|
|
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Safari/537.36",
|
|
viewport: {
|
|
width: 1280,
|
|
height: 720
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: false,
|
|
hasTouch: false
|
|
},
|
|
"Desktop Edge HiDPI": {
|
|
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Safari/537.36 Edg/128.0.6613.18",
|
|
viewport: {
|
|
width: 1280,
|
|
height: 720
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: false,
|
|
hasTouch: false
|
|
},
|
|
"Desktop Firefox HiDPI": {
|
|
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0",
|
|
viewport: {
|
|
width: 1280,
|
|
height: 720
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: false,
|
|
hasTouch: false
|
|
},
|
|
"Desktop Safari": {
|
|
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15",
|
|
viewport: {
|
|
width: 1280,
|
|
height: 720
|
|
},
|
|
deviceScaleFactor: 2,
|
|
isMobile: false,
|
|
hasTouch: false
|
|
},
|
|
"Desktop Chrome": {
|
|
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Safari/537.36",
|
|
viewport: {
|
|
width: 1280,
|
|
height: 720
|
|
},
|
|
deviceScaleFactor: 1,
|
|
isMobile: false,
|
|
hasTouch: false
|
|
},
|
|
"Desktop Edge": {
|
|
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Safari/537.36 Edg/128.0.6613.18",
|
|
viewport: {
|
|
width: 1280,
|
|
height: 720
|
|
},
|
|
deviceScaleFactor: 1,
|
|
isMobile: false,
|
|
hasTouch: false
|
|
},
|
|
"Desktop Firefox": {
|
|
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0",
|
|
viewport: {
|
|
width: 1280,
|
|
height: 720
|
|
},
|
|
deviceScaleFactor: 1,
|
|
isMobile: false,
|
|
hasTouch: false
|
|
}
|
|
};
|
|
|
|
// src/commands/browser/emulate.ts
|
|
function storeRestoreFunction(browser, scope, fn) {
|
|
if (!restoreFunctions.has(browser)) {
|
|
restoreFunctions.set(browser, /* @__PURE__ */ new Map());
|
|
}
|
|
const restoreFunctionsList = restoreFunctions.get(browser)?.get(scope);
|
|
const updatedList = restoreFunctionsList ? [...restoreFunctionsList, fn] : [fn];
|
|
restoreFunctions.get(browser)?.set(scope, updatedList);
|
|
}
|
|
async function emulate(scope, options) {
|
|
if (!this.isBidi) {
|
|
throw new Error("emulate command is only supported for Bidi");
|
|
}
|
|
if (scope === "geolocation") {
|
|
if (!options) {
|
|
throw new Error("Missing geolocation emulation options");
|
|
}
|
|
const patchedFn = options instanceof Error ? `cbError(new Error(${JSON.stringify(options.message)}))` : `cbSuccess({
|
|
coords: ${JSON.stringify(options)},
|
|
timestamp: Date.now()
|
|
})`;
|
|
const res = await this.scriptAddPreloadScript({
|
|
functionDeclaration: (
|
|
/*js*/
|
|
`() => {
|
|
Object.defineProperty(navigator.geolocation, 'getCurrentPosition', {
|
|
value: (cbSuccess, cbError) => ${patchedFn}
|
|
})
|
|
}`
|
|
)
|
|
});
|
|
const resetFn = async () => this.scriptRemovePreloadScript({ script: res.script });
|
|
storeRestoreFunction(this, "geolocation", resetFn);
|
|
return resetFn;
|
|
}
|
|
if (scope === "userAgent") {
|
|
if (typeof options !== "string") {
|
|
throw new Error(`Expected userAgent emulation options to be a string, received ${typeof options}`);
|
|
}
|
|
const res = await this.scriptAddPreloadScript({
|
|
functionDeclaration: (
|
|
/*js*/
|
|
`() => {
|
|
Object.defineProperty(navigator, 'userAgent', {
|
|
value: ${JSON.stringify(options)}
|
|
})
|
|
}`
|
|
)
|
|
});
|
|
const resetFn = async () => {
|
|
return this.scriptRemovePreloadScript({ script: res.script });
|
|
};
|
|
storeRestoreFunction(this, "userAgent", resetFn);
|
|
return resetFn;
|
|
}
|
|
if (scope === "clock") {
|
|
const clock = new ClockManager(this);
|
|
await clock.install(options);
|
|
storeRestoreFunction(this, "clock", clock.restore.bind(clock));
|
|
return clock;
|
|
}
|
|
if (scope === "colorScheme") {
|
|
if (options !== "light" && options !== "dark") {
|
|
throw new Error(`Expected "colorScheme" emulation options to be either "light" or "dark", received "${options}"`);
|
|
}
|
|
const res = await this.scriptAddPreloadScript({
|
|
functionDeclaration: (
|
|
/*js*/
|
|
`() => {
|
|
const originalMatchMedia = window.matchMedia
|
|
Object.defineProperty(window, 'matchMedia', {
|
|
value: (query) => {
|
|
const colorSchemeQuery = query.match(/\\(prefers-color-scheme:(\\s)*(dark|light)\\)/i)
|
|
if (colorSchemeQuery) {
|
|
const result = originalMatchMedia(query)
|
|
Object.defineProperty(result, 'matches', {
|
|
value: colorSchemeQuery[2] === "${options}",
|
|
configurable: true
|
|
})
|
|
return result
|
|
}
|
|
|
|
return originalMatchMedia(query)
|
|
},
|
|
configurable: true
|
|
})
|
|
}`
|
|
)
|
|
});
|
|
const resetFn = async () => this.scriptRemovePreloadScript({ script: res.script });
|
|
storeRestoreFunction(this, "colorScheme", resetFn);
|
|
return resetFn;
|
|
}
|
|
if (scope === "onLine") {
|
|
if (typeof options !== "boolean") {
|
|
throw new Error(`Expected "onLine" emulation options to be a boolean, received "${typeof options}"`);
|
|
}
|
|
const res = await this.scriptAddPreloadScript({
|
|
functionDeclaration: (
|
|
/*js*/
|
|
`() => {
|
|
Object.defineProperty(navigator, 'onLine', {
|
|
value: ${options}
|
|
})
|
|
}`
|
|
)
|
|
});
|
|
const resetFn = async () => this.scriptRemovePreloadScript({ script: res.script });
|
|
storeRestoreFunction(this, "onLine", resetFn);
|
|
return resetFn;
|
|
}
|
|
if (scope === "device") {
|
|
if (typeof options !== "string") {
|
|
throw new Error(`Expected "device" emulation options to be a string, received "${typeof options}"`);
|
|
}
|
|
const device = deviceDescriptorsSource[options];
|
|
if (!device) {
|
|
throw new Error(`Unknown device name "${options}", please use one of the following: ${Object.keys(deviceDescriptorsSource).join(", ")}`);
|
|
}
|
|
const [restoreUserAgent] = await Promise.all([
|
|
this.emulate("userAgent", device.userAgent),
|
|
this.setViewport({
|
|
...device.viewport,
|
|
devicePixelRatio: device.deviceScaleFactor
|
|
})
|
|
]);
|
|
const desktopViewport = deviceDescriptorsSource["Desktop Chrome"];
|
|
const restoreFn = async () => Promise.all([
|
|
restoreUserAgent(),
|
|
this.setViewport({ ...desktopViewport.viewport, devicePixelRatio: desktopViewport.deviceScaleFactor })
|
|
]);
|
|
return restoreFn;
|
|
}
|
|
throw new Error(`Invalid scope "${scope}", expected one of "geolocation", "userAgent", "colorScheme", "onLine", "device" or "clock"`);
|
|
}
|
|
|
|
// src/commands/browser/execute.ts
|
|
import { getBrowserObject as getBrowserObject4 } from "@wdio/utils";
|
|
|
|
// src/utils/bidi/value.ts
|
|
import { ELEMENT_KEY as ELEMENT_KEY7 } from "webdriver";
|
|
var TYPE_CONSTANT = "type";
|
|
var VALUE_CONSTANT = "value";
|
|
var LocalValue = class _LocalValue {
|
|
type;
|
|
value;
|
|
constructor(type, value) {
|
|
if (type === "undefined" /* Undefined */ || type === "null" /* Null */) {
|
|
this.type = type;
|
|
} else {
|
|
this.type = type;
|
|
this.value = value;
|
|
}
|
|
}
|
|
/**
|
|
* Creates a new LocalValue object with a string value.
|
|
*
|
|
* @param {string} value - The string value to be stored in the LocalValue object.
|
|
* @returns {LocalValue} - The created LocalValue object.
|
|
*/
|
|
static createStringValue(value) {
|
|
return new _LocalValue("string" /* String */, value);
|
|
}
|
|
/**
|
|
* Creates a new LocalValue object with a number value.
|
|
*
|
|
* @param {number} value - The number value.
|
|
* @returns {LocalValue} - The created LocalValue object.
|
|
*/
|
|
static createNumberValue(value) {
|
|
return new _LocalValue("number" /* Number */, value);
|
|
}
|
|
/**
|
|
* Creates a new LocalValue object with a special number value.
|
|
*
|
|
* @param {number} value - The value of the special number.
|
|
* @returns {LocalValue} - The created LocalValue object.
|
|
*/
|
|
static createSpecialNumberValue(value) {
|
|
if (Number.isNaN(value)) {
|
|
return new _LocalValue("number" /* SpecialNumber */, "NaN");
|
|
}
|
|
if (Object.is(value, -0)) {
|
|
return new _LocalValue("number" /* SpecialNumber */, "-0");
|
|
}
|
|
if (value === Infinity) {
|
|
return new _LocalValue("number" /* SpecialNumber */, "Infinity");
|
|
}
|
|
if (value === -Infinity) {
|
|
return new _LocalValue("number" /* SpecialNumber */, "-Infinity");
|
|
}
|
|
return new _LocalValue("number" /* SpecialNumber */, value);
|
|
}
|
|
/**
|
|
* Creates a new LocalValue object with an undefined value.
|
|
* @returns {LocalValue} - The created LocalValue object.
|
|
*/
|
|
static createUndefinedValue() {
|
|
return new _LocalValue("undefined" /* Undefined */);
|
|
}
|
|
/**
|
|
* Creates a new LocalValue object with a null value.
|
|
* @returns {LocalValue} - The created LocalValue object.
|
|
*/
|
|
static createNullValue() {
|
|
return new _LocalValue("null" /* Null */);
|
|
}
|
|
/**
|
|
* Creates a new LocalValue object with a boolean value.
|
|
*
|
|
* @param {boolean} value - The boolean value.
|
|
* @returns {LocalValue} - The created LocalValue object.
|
|
*/
|
|
static createBooleanValue(value) {
|
|
return new _LocalValue("boolean" /* Boolean */, value);
|
|
}
|
|
/**
|
|
* Creates a new LocalValue object with a BigInt value.
|
|
*
|
|
* @param {BigInt} value - The BigInt value.
|
|
* @returns {LocalValue} - The created LocalValue object.
|
|
*/
|
|
static createBigIntValue(value) {
|
|
return new _LocalValue("bigint" /* BigInt */, value);
|
|
}
|
|
/**
|
|
* Creates a new LocalValue object with an array.
|
|
*
|
|
* @param {Array} value - The array.
|
|
* @returns {LocalValue} - The created LocalValue object.
|
|
*/
|
|
static createArrayValue(value) {
|
|
return new _LocalValue("array" /* Array */, value);
|
|
}
|
|
/**
|
|
* Creates a new LocalValue object with date value.
|
|
*
|
|
* @param {string} value - The date.
|
|
* @returns {LocalValue} - The created LocalValue object.
|
|
*/
|
|
static createDateValue(value) {
|
|
return new _LocalValue("date" /* Date */, value);
|
|
}
|
|
/**
|
|
* Creates a new LocalValue object of map value.
|
|
* @param {Map} map - The map.
|
|
* @returns {LocalValue} - The created LocalValue object.
|
|
*/
|
|
static createMapValue(map) {
|
|
const value = [];
|
|
Object.entries(map).forEach((entry) => {
|
|
value.push(entry);
|
|
});
|
|
return new _LocalValue("map" /* Map */, value);
|
|
}
|
|
/**
|
|
* Creates a new LocalValue object from the passed object.
|
|
*
|
|
* @param {Object} map - The object.
|
|
* @returns {LocalValue} - The created LocalValue object.
|
|
*/
|
|
static createObjectValue(object) {
|
|
const value = [];
|
|
Object.entries(object).forEach(([key, val]) => {
|
|
value.push([key, _LocalValue.getArgument(val)]);
|
|
});
|
|
return new _LocalValue("object" /* Object */, value);
|
|
}
|
|
/**
|
|
* Creates a new LocalValue object of regular expression value.
|
|
*
|
|
* @param {string} value - The value of the regular expression.
|
|
* @returns {LocalValue} - The created LocalValue object.
|
|
*/
|
|
static createRegularExpressionValue(value) {
|
|
return new _LocalValue("regexp" /* RegularExpression */, value);
|
|
}
|
|
/**
|
|
* Creates a new LocalValue object with the specified value.
|
|
* @param {Set} value - The value to be set.
|
|
* @returns {LocalValue} - The created LocalValue object.
|
|
*/
|
|
static createSetValue(value) {
|
|
return new _LocalValue("set" /* Set */, value);
|
|
}
|
|
/**
|
|
* Creates a new LocalValue object with the given channel value
|
|
*
|
|
* @param {ChannelValue} value - The channel value.
|
|
* @returns {LocalValue} - The created LocalValue object.
|
|
*/
|
|
static createChannelValue(value) {
|
|
return new _LocalValue("channel" /* Channel */, value);
|
|
}
|
|
static createReferenceValue(handle, sharedId) {
|
|
return new ReferenceValue(handle, sharedId);
|
|
}
|
|
static getArgument(argument) {
|
|
const type = typeof argument;
|
|
switch (type) {
|
|
case "string" /* String */:
|
|
return _LocalValue.createStringValue(argument);
|
|
case "number" /* Number */:
|
|
if (Number.isNaN(argument) || Object.is(argument, -0) || !Number.isFinite(argument)) {
|
|
return _LocalValue.createSpecialNumberValue(argument);
|
|
}
|
|
return _LocalValue.createNumberValue(argument);
|
|
case "boolean" /* Boolean */:
|
|
return _LocalValue.createBooleanValue(argument);
|
|
case "bigint" /* BigInt */:
|
|
return _LocalValue.createBigIntValue(argument);
|
|
case "undefined" /* Undefined */:
|
|
return _LocalValue.createUndefinedValue();
|
|
case "object" /* Object */:
|
|
if (argument === null) {
|
|
return _LocalValue.createNullValue();
|
|
}
|
|
if (argument instanceof Date) {
|
|
return _LocalValue.createDateValue(argument);
|
|
}
|
|
if (argument instanceof Map) {
|
|
const map = [];
|
|
argument.forEach((value, key) => {
|
|
const objectKey = typeof key === "string" ? key : _LocalValue.getArgument(key);
|
|
const objectValue = _LocalValue.getArgument(value);
|
|
map.push([objectKey, objectValue]);
|
|
});
|
|
return new _LocalValue("map" /* Map */, map);
|
|
}
|
|
if (argument instanceof Set) {
|
|
const set = [];
|
|
argument.forEach((value) => {
|
|
set.push(_LocalValue.getArgument(value));
|
|
});
|
|
return _LocalValue.createSetValue(set);
|
|
}
|
|
if (argument instanceof Array) {
|
|
const arr = [];
|
|
argument.forEach((value) => {
|
|
arr.push(_LocalValue.getArgument(value));
|
|
});
|
|
return _LocalValue.createArrayValue(arr);
|
|
}
|
|
if (argument instanceof RegExp) {
|
|
return _LocalValue.createRegularExpressionValue({
|
|
pattern: argument.source,
|
|
flags: argument.flags
|
|
});
|
|
}
|
|
if (argument && ELEMENT_KEY7 in argument) {
|
|
return _LocalValue.createReferenceValue(
|
|
"sharedId" /* SharedId */,
|
|
argument[ELEMENT_KEY7]
|
|
);
|
|
}
|
|
return _LocalValue.createObjectValue(argument);
|
|
}
|
|
throw new Error(`Unsupported type: ${type}`);
|
|
}
|
|
asMap() {
|
|
return {
|
|
[TYPE_CONSTANT]: this.type,
|
|
...!(this.type === "null" /* Null */ || this.type === "undefined" /* Undefined */) ? { [VALUE_CONSTANT]: this.value } : {}
|
|
};
|
|
}
|
|
};
|
|
var ReferenceValue = class {
|
|
handle;
|
|
sharedId;
|
|
/**
|
|
* Constructs a new ReferenceValue object.
|
|
* @param {string} handle - The handle value.
|
|
* @param {string} sharedId - The shared ID value.
|
|
*/
|
|
constructor(handle, sharedId) {
|
|
if (handle === "handle" /* Handle */) {
|
|
this.handle = sharedId;
|
|
} else if (handle === "sharedId" /* SharedId */) {
|
|
this.sharedId = sharedId;
|
|
} else {
|
|
this.handle = handle;
|
|
this.sharedId = sharedId;
|
|
}
|
|
}
|
|
asMap() {
|
|
const toReturn = {};
|
|
if (typeof this.handle !== "undefined") {
|
|
toReturn["handle" /* Handle */] = this.handle;
|
|
}
|
|
if (typeof this.sharedId !== "undefined") {
|
|
toReturn["sharedId" /* SharedId */] = this.sharedId;
|
|
}
|
|
return toReturn;
|
|
}
|
|
};
|
|
|
|
// src/commands/browser/execute.ts
|
|
import { polyfillFn } from "./scripts/polyfill.js";
|
|
async function execute(script, ...args) {
|
|
if (typeof script !== "string" && typeof script !== "function") {
|
|
throw new Error("number or type of arguments don't agree with execute protocol command");
|
|
}
|
|
if (this.isBidi && !this.isMultiremote) {
|
|
const browser = getBrowserObject4(this);
|
|
const contextManager = getContextManager(browser);
|
|
const context = await contextManager.getCurrentContext();
|
|
const userScript = typeof script === "string" ? new Function(script) : script;
|
|
const functionDeclaration = createFunctionDeclarationFromString(userScript);
|
|
const params = {
|
|
functionDeclaration,
|
|
awaitPromise: true,
|
|
arguments: args.map((arg) => LocalValue.getArgument(arg)),
|
|
target: {
|
|
context
|
|
}
|
|
};
|
|
const result = await browser.scriptCallFunction(params);
|
|
return parseScriptResult(params, result);
|
|
}
|
|
if (typeof script === "function") {
|
|
script = `
|
|
${polyfillFn}
|
|
webdriverioPolyfill();
|
|
return (${script}).apply(null, arguments)
|
|
`;
|
|
}
|
|
return this.executeScript(script, verifyArgsAndStripIfElement(args));
|
|
}
|
|
|
|
// src/commands/browser/executeAsync.ts
|
|
import { getBrowserObject as getBrowserObject5 } from "@wdio/utils";
|
|
import { polyfillFn as polyfillFn2 } from "./scripts/polyfill.js";
|
|
async function executeAsync(script, ...args) {
|
|
if (typeof script !== "string" && typeof script !== "function") {
|
|
throw new Error("number or type of arguments don't agree with execute protocol command");
|
|
}
|
|
if (this.isBidi && !this.isMultiremote) {
|
|
const browser = getBrowserObject5(this);
|
|
const contextManager = getContextManager(browser);
|
|
const context = await contextManager.getCurrentContext();
|
|
const userScript = typeof script === "string" ? new Function(script) : script;
|
|
const functionDeclaration = new Function(`
|
|
const args = Array.from(arguments)
|
|
return new Promise(async (resolve, reject) => {
|
|
const cb = (result) => resolve(result)
|
|
try {
|
|
await (${userScript.toString()}).apply(this, [...args, cb])
|
|
} catch (err) {
|
|
return reject(err)
|
|
}
|
|
})
|
|
`).toString();
|
|
const params = {
|
|
functionDeclaration,
|
|
awaitPromise: true,
|
|
arguments: args.map((arg) => LocalValue.getArgument(arg)),
|
|
target: {
|
|
context
|
|
}
|
|
};
|
|
const result = await browser.scriptCallFunction(params);
|
|
return parseScriptResult(params, result);
|
|
}
|
|
if (typeof script === "function") {
|
|
script = `
|
|
${polyfillFn2}
|
|
webdriverioPolyfill()
|
|
return (${script}).apply(null, arguments)
|
|
`;
|
|
}
|
|
return this.executeAsyncScript(script, verifyArgsAndStripIfElement(args));
|
|
}
|
|
|
|
// src/commands/browser/getCookies.ts
|
|
import logger6 from "@wdio/logger";
|
|
var log6 = logger6("webdriverio");
|
|
async function getCookies(filter) {
|
|
const usesMultipleFilter = Array.isArray(filter) && filter.length > 1;
|
|
if (!this.isBidi || usesMultipleFilter) {
|
|
return getCookiesClassic.call(this, filter);
|
|
}
|
|
const cookieFilter = getCookieFilter(filter);
|
|
let url2;
|
|
try {
|
|
url2 = new URL(await this.getUrl());
|
|
if (url2.origin === "null") {
|
|
return getCookiesClassic.call(this, filter);
|
|
}
|
|
} catch {
|
|
return getCookiesClassic.call(this, filter);
|
|
}
|
|
const params = {
|
|
partition: {
|
|
type: "storageKey",
|
|
sourceOrigin: url2.origin
|
|
}
|
|
};
|
|
if (typeof cookieFilter !== "undefined") {
|
|
params.filter = cookieFilter;
|
|
}
|
|
try {
|
|
const { cookies } = await this.storageGetCookies(params);
|
|
if (cookies.length === 0) {
|
|
log6.debug("BiDi getCookies returned empty, falling back to classic");
|
|
return getCookiesClassic.call(this, filter);
|
|
}
|
|
return cookies.map((cookie) => ({
|
|
...cookie,
|
|
value: cookie.value.type === "base64" ? Buffer.from(cookie.value.value, "base64").toString("utf-8") : cookie.value.value
|
|
}));
|
|
} catch (err) {
|
|
log6.warn(`BiDi getCookies failed, falling back to classic: ${err.message}`);
|
|
return getCookiesClassic.call(this, filter);
|
|
}
|
|
}
|
|
async function getCookiesClassic(names) {
|
|
if (!names) {
|
|
return this.getAllCookies();
|
|
}
|
|
const usesMultipleFilter = Array.isArray(names) && names.length > 1;
|
|
if (usesMultipleFilter) {
|
|
log6.warn(
|
|
"Passing a string array as filter for `getCookies` is deprecated and its support will be removed in an upcoming version of WebdriverIO!"
|
|
);
|
|
const allCookies2 = await this.getAllCookies();
|
|
return allCookies2.filter((cookie) => names.includes(cookie.name));
|
|
}
|
|
const filter = getCookieFilter(names);
|
|
const allCookies = await this.getAllCookies();
|
|
return allCookies.filter((cookie) => !filter || cookie.name && filter.name === cookie.name || cookie.value && filter.value?.value === cookie.value || cookie.path && filter.path === cookie.path || cookie.domain && filter.domain === cookie.domain || cookie.sameSite && filter.sameSite === cookie.sameSite || cookie.expiry && filter.expiry === cookie.expiry || typeof cookie.httpOnly === "boolean" && filter.httpOnly === cookie.httpOnly || typeof cookie.secure === "boolean" && filter.secure === cookie.secure);
|
|
}
|
|
function getCookieFilter(names) {
|
|
if (!names) {
|
|
return;
|
|
}
|
|
if (Array.isArray(names) && names.length > 1) {
|
|
throw new Error("Multiple cookie name filters are not supported");
|
|
}
|
|
return (Array.isArray(names) ? names : [names]).map((filter) => {
|
|
if (typeof filter === "string") {
|
|
log6.warn("Passing string values into `getCookie` is deprecated and its support will be removed in an upcoming version of WebdriverIO!");
|
|
return { name: filter };
|
|
}
|
|
return filter;
|
|
})[0];
|
|
}
|
|
|
|
// src/commands/browser/getPuppeteer.ts
|
|
import logger7 from "@wdio/logger";
|
|
import { userImport } from "@wdio/utils";
|
|
var log7 = logger7("webdriverio");
|
|
var DEBUG_PIPE_FLAG = "remote-debugging-pipe";
|
|
async function getPuppeteer() {
|
|
if (globalThis.wdio) {
|
|
throw new Error("Puppeteer is not supported in browser runner");
|
|
}
|
|
const puppeteer = await userImport("puppeteer-core");
|
|
if (!puppeteer) {
|
|
throw new Error(
|
|
'You need to install "puppeteer-core" package as a dependency in order to use the "getPuppeteer" method'
|
|
);
|
|
}
|
|
if (this.puppeteer?.connected) {
|
|
log7.debug("Reusing existing puppeteer session");
|
|
return this.puppeteer;
|
|
}
|
|
const { headers } = this.options;
|
|
const cdpEndpoint = this.capabilities["se:cdp"];
|
|
if (cdpEndpoint) {
|
|
this.puppeteer = await puppeteer.connect({
|
|
browserWSEndpoint: cdpEndpoint,
|
|
defaultViewport: null,
|
|
headers
|
|
});
|
|
return this.puppeteer;
|
|
}
|
|
const requestedCapabilities = this.requestedCapabilities?.alwaysMatch || this.requestedCapabilities;
|
|
const isAerokubeSession = requestedCapabilities["selenoid:options"] || requestedCapabilities["moon:options"];
|
|
if (isAerokubeSession) {
|
|
const { hostname, port } = this.options;
|
|
this.puppeteer = await puppeteer.connect({
|
|
browserWSEndpoint: `ws://${hostname}:${port}/devtools/${this.sessionId}`,
|
|
defaultViewport: null,
|
|
headers
|
|
});
|
|
return this.puppeteer;
|
|
}
|
|
const chromiumOptions = this.capabilities["goog:chromeOptions"] || this.capabilities["ms:edgeOptions"];
|
|
if (chromiumOptions && chromiumOptions.debuggerAddress) {
|
|
this.puppeteer = await puppeteer.connect({
|
|
browserURL: `http://${chromiumOptions.debuggerAddress.replace("localhost", "0.0.0.0")}`,
|
|
defaultViewport: null
|
|
});
|
|
return this.puppeteer;
|
|
} else if (
|
|
/**
|
|
* if --remote-debugging-pipe is set as Chrome flag, we can't attach to the session
|
|
* as there won't be a `debuggerAddress` available in the capabilities. Provide this
|
|
* better error message to the user.
|
|
*/
|
|
chromiumOptions && (chromiumOptions.args?.includes(DEBUG_PIPE_FLAG) || chromiumOptions.args?.includes(`--${DEBUG_PIPE_FLAG}`))
|
|
) {
|
|
throw new Error(`Cannot attach to Chrome Devtools session if --${DEBUG_PIPE_FLAG} is set as Chrome flag.`);
|
|
}
|
|
if (this.capabilities.browserName?.toLowerCase() === "firefox") {
|
|
if (!this.capabilities.browserVersion) {
|
|
throw new Error(`Can't find "browserVersion" in capabilities`);
|
|
}
|
|
const majorVersion = parseInt(this.capabilities.browserVersion.split(".").shift() || "", 10);
|
|
if (majorVersion >= 79) {
|
|
const reqCaps = this.requestedCapabilities.alwaysMatch || this.requestedCapabilities;
|
|
let browserURL;
|
|
if (this.capabilities["moz:debuggerAddress"]) {
|
|
browserURL = this.capabilities["moz:debuggerAddress"];
|
|
} else {
|
|
const ffOptions = this.capabilities["moz:firefoxOptions"];
|
|
const ffArgs = reqCaps["moz:firefoxOptions"]?.args || [];
|
|
const rdPort = ffOptions && ffOptions.debuggerAddress ? ffOptions.debuggerAddress : ffArgs[ffArgs.findIndex((arg) => arg === FF_REMOTE_DEBUG_ARG) + 1];
|
|
if (rdPort) {
|
|
browserURL = `http://localhost:${rdPort}`;
|
|
}
|
|
}
|
|
if (!browserURL) {
|
|
throw new Error(
|
|
'Could\'t find a websocket url within returned capabilities to connect to! Make sure you have "moz:debuggerAddress" set to `true` in your Firefox capabilities'
|
|
);
|
|
}
|
|
this.puppeteer = await puppeteer.connect({
|
|
browserURL,
|
|
defaultViewport: null
|
|
});
|
|
return this.puppeteer;
|
|
}
|
|
}
|
|
throw new Error(
|
|
"Using DevTools capabilities is not supported for this session. This feature is only supported for local testing on Chrome, Firefox and Chromium Edge."
|
|
);
|
|
}
|
|
|
|
// src/commands/browser/getWindowSize.ts
|
|
import { getBrowserObject as getBrowserObject6 } from "@wdio/utils";
|
|
async function getWindowSize() {
|
|
const browser = getBrowserObject6(this);
|
|
const { width, height } = await browser.getWindowRect();
|
|
return { width, height };
|
|
}
|
|
|
|
// src/commands/browser/keys.ts
|
|
async function keys(value) {
|
|
let keySequence = [];
|
|
if (typeof value === "string") {
|
|
keySequence = checkUnicode(value);
|
|
} else if (Array.isArray(value)) {
|
|
const charArray = value;
|
|
for (const charSet of charArray) {
|
|
keySequence = keySequence.concat(checkUnicode(charSet));
|
|
}
|
|
} else {
|
|
throw new Error('"keys" command requires a string or array of strings as parameter');
|
|
}
|
|
const keyAction = this.action("key");
|
|
keySequence.forEach((value2) => keyAction.down(value2));
|
|
if (!this.isIOS) {
|
|
keyAction.pause(10);
|
|
}
|
|
keySequence.forEach((value2) => keyAction.up(value2));
|
|
return keyAction.perform(true);
|
|
}
|
|
|
|
// src/commands/browser/mock.ts
|
|
import { getBrowserObject as getBrowserObject7 } from "@wdio/utils";
|
|
|
|
// src/utils/interception/index.ts
|
|
import logger8 from "@wdio/logger";
|
|
import { URLPattern } from "urlpattern-polyfill";
|
|
|
|
// src/utils/Timer.ts
|
|
var TIMEOUT_ERROR = "timeout";
|
|
var NOOP = () => {
|
|
};
|
|
var Timer = class {
|
|
constructor(_delay, _timeout, _fn, _leading = false, _signal) {
|
|
this._delay = _delay;
|
|
this._timeout = _timeout;
|
|
this._fn = _fn;
|
|
this._leading = _leading;
|
|
this._signal = _signal;
|
|
this.#retPromise = new Promise((resolve, reject) => {
|
|
this._resolve = resolve;
|
|
this._reject = reject;
|
|
});
|
|
this._start();
|
|
}
|
|
#retPromise;
|
|
_conditionExecutedCnt = 0;
|
|
_resolve = NOOP;
|
|
_reject = NOOP;
|
|
_startTime;
|
|
_ticks = 0;
|
|
_timeoutId;
|
|
_mainTimeoutId;
|
|
_lastError;
|
|
then(thennable, catchable) {
|
|
return this.#retPromise.then(thennable, catchable);
|
|
}
|
|
catch(catchable) {
|
|
return this.#retPromise.catch(catchable);
|
|
}
|
|
_start() {
|
|
this._startTime = Date.now();
|
|
if (this._leading) {
|
|
this._tick();
|
|
} else {
|
|
this._timeoutId = setTimeout(this._tick.bind(this), this._delay);
|
|
}
|
|
if (this._wasConditionExecuted()) {
|
|
return;
|
|
}
|
|
this._mainTimeoutId = setTimeout(() => {
|
|
if (!this._wasConditionExecuted()) {
|
|
return;
|
|
}
|
|
const reason = this._lastError || new Error(TIMEOUT_ERROR);
|
|
this._reject(reason);
|
|
this._stop();
|
|
}, this._timeout);
|
|
}
|
|
_stop() {
|
|
if (this._timeoutId) {
|
|
clearTimeout(this._timeoutId);
|
|
}
|
|
delete this._timeoutId;
|
|
}
|
|
_stopMain() {
|
|
if (this._mainTimeoutId) {
|
|
clearTimeout(this._mainTimeoutId);
|
|
}
|
|
}
|
|
_tick() {
|
|
try {
|
|
const result = this._fn();
|
|
if (!result) {
|
|
return this._checkCondition(new Error(TIMEOUT_ERROR));
|
|
}
|
|
if (typeof result.then !== "function") {
|
|
return this._checkCondition(void 0, result);
|
|
}
|
|
result.then(
|
|
(res) => this._checkCondition(void 0, res),
|
|
(err) => this._checkCondition(err)
|
|
);
|
|
} catch (err) {
|
|
return this._checkCondition(err);
|
|
}
|
|
}
|
|
_checkCondition(err, res) {
|
|
this._lastError = err;
|
|
if (this._signal?.aborted) {
|
|
this._reject(this._lastError || new Error("Aborted"));
|
|
this._stop();
|
|
this._stopMain();
|
|
return;
|
|
}
|
|
++this._conditionExecutedCnt;
|
|
if (res) {
|
|
this._resolve(res);
|
|
this._stop();
|
|
this._stopMain();
|
|
return;
|
|
}
|
|
const diff = Date.now() - (this._startTime || 0) - this._ticks++ * this._delay;
|
|
const delay = Math.max(0, this._delay - diff);
|
|
this._stop();
|
|
if (this._hasTime(delay)) {
|
|
this._timeoutId = setTimeout(this._tick.bind(this), delay);
|
|
} else {
|
|
this._stopMain();
|
|
const reason = this._lastError || new Error(TIMEOUT_ERROR);
|
|
this._reject(reason);
|
|
}
|
|
}
|
|
_hasTime(delay) {
|
|
return Date.now() - (this._startTime || 0) + delay <= this._timeout;
|
|
}
|
|
_wasConditionExecuted() {
|
|
return this._conditionExecutedCnt > 0;
|
|
}
|
|
};
|
|
var Timer_default = Timer;
|
|
|
|
// src/utils/interception/utils.ts
|
|
function parseOverwrite(overwrite, request) {
|
|
const result = {};
|
|
if ("body" in overwrite && overwrite.body) {
|
|
const bodyOverwrite = typeof overwrite.body === "function" ? overwrite.body(request) : overwrite.body;
|
|
result.body = bodyOverwrite?.type === "string" || bodyOverwrite?.type === "base64" ? bodyOverwrite : typeof bodyOverwrite === "string" ? { type: "string", value: bodyOverwrite } : { type: "base64", value: btoa(JSON.stringify(bodyOverwrite || "")) };
|
|
}
|
|
if ("headers" in overwrite) {
|
|
const headersOverwrite = typeof overwrite.headers === "function" ? overwrite.headers(request) : overwrite.headers;
|
|
result.headers = Object.entries(headersOverwrite || {}).map(([name, value]) => ({
|
|
name,
|
|
value: { type: "string", value }
|
|
}));
|
|
}
|
|
if ("cookies" in overwrite && overwrite.cookies) {
|
|
const cookieOverwrite = typeof overwrite.cookies === "function" ? overwrite.cookies(request) || [] : overwrite.cookies;
|
|
result.cookies = cookieOverwrite.map((cookie) => ({
|
|
name: cookie.name,
|
|
value: {
|
|
type: "string",
|
|
value: cookie.value
|
|
},
|
|
domain: cookie.domain,
|
|
path: cookie.path,
|
|
expires: cookie.expiry,
|
|
httpOnly: cookie.httpOnly,
|
|
secure: cookie.secure,
|
|
sameSite: cookie.sameSite?.toLowerCase()
|
|
}));
|
|
}
|
|
if ("statusCode" in overwrite && overwrite.statusCode) {
|
|
const statusCodeOverwrite = typeof overwrite.statusCode === "function" ? overwrite.statusCode(request) : overwrite.statusCode;
|
|
result.statusCode = statusCodeOverwrite;
|
|
}
|
|
if ("method" in overwrite) {
|
|
result.method = typeof overwrite.method === "function" ? overwrite.method(request) : overwrite.method;
|
|
}
|
|
if ("url" in overwrite) {
|
|
result.url = typeof overwrite.url === "function" ? overwrite.url(request) : overwrite.url;
|
|
}
|
|
return result;
|
|
}
|
|
function getPatternParam(pattern, key) {
|
|
const value = pattern[key];
|
|
if (value === "*" || value.includes("*")) {
|
|
return;
|
|
}
|
|
if (key === "port" && pattern.port === "") {
|
|
return pattern.protocol === "https" ? "443" : "80";
|
|
}
|
|
return value;
|
|
}
|
|
|
|
// src/utils/interception/index.ts
|
|
var log8 = logger8("WebDriverInterception");
|
|
var hasSubscribedToEvents = false;
|
|
var WebDriverInterception = class _WebDriverInterception {
|
|
#pattern;
|
|
#mockId;
|
|
#filterOptions;
|
|
#browser;
|
|
#eventHandler = /* @__PURE__ */ new Map();
|
|
#restored = false;
|
|
#requestOverwrites = [];
|
|
#respondOverwrites = [];
|
|
#calls = [];
|
|
#responseBodies = /* @__PURE__ */ new Map();
|
|
constructor(pattern, mockId, filterOptions, browser) {
|
|
this.#pattern = pattern;
|
|
this.#mockId = mockId;
|
|
this.#filterOptions = filterOptions;
|
|
this.#browser = browser;
|
|
browser.on("network.beforeRequestSent", this.#handleBeforeRequestSent.bind(this));
|
|
browser.on("network.responseStarted", this.#handleResponseStarted.bind(this));
|
|
}
|
|
static async initiate(url2, filterOptions, browser) {
|
|
const pattern = parseUrlPattern(url2);
|
|
if (!hasSubscribedToEvents) {
|
|
await browser.sessionSubscribe({
|
|
events: [
|
|
"network.beforeRequestSent",
|
|
"network.responseStarted"
|
|
]
|
|
});
|
|
log8.info("subscribed to network events");
|
|
hasSubscribedToEvents = true;
|
|
}
|
|
const interception = await browser.networkAddIntercept({
|
|
phases: ["beforeRequestSent", "responseStarted"],
|
|
urlPatterns: [{
|
|
type: "pattern",
|
|
protocol: getPatternParam(pattern, "protocol"),
|
|
hostname: getPatternParam(pattern, "hostname"),
|
|
pathname: getPatternParam(pattern, "pathname"),
|
|
port: getPatternParam(pattern, "port"),
|
|
search: getPatternParam(pattern, "search")
|
|
}]
|
|
});
|
|
return new _WebDriverInterception(pattern, interception.intercept, filterOptions, browser);
|
|
}
|
|
#emit(event, args) {
|
|
if (!this.#eventHandler.has(event)) {
|
|
return;
|
|
}
|
|
const handlers = this.#eventHandler.get(event) || [];
|
|
for (const handler of handlers) {
|
|
handler(args);
|
|
}
|
|
}
|
|
#addEventHandler(event, handler) {
|
|
if (!this.#eventHandler.has(event)) {
|
|
this.#eventHandler.set(event, []);
|
|
}
|
|
const handlers = this.#eventHandler.get(event);
|
|
handlers?.push(handler);
|
|
}
|
|
#handleBeforeRequestSent(request) {
|
|
if (!this.#isRequestMatching(request)) {
|
|
if (request.intercepts?.includes(this.#mockId)) {
|
|
return this.#browser.networkContinueRequest({
|
|
request: request.request.request
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
if (!this.#matchesFilterOptions(request)) {
|
|
return this.#browser.networkContinueRequest({
|
|
request: request.request.request
|
|
});
|
|
}
|
|
this.#emit("request", request);
|
|
const hasRequestOverwrites = this.#requestOverwrites.length > 0;
|
|
if (hasRequestOverwrites) {
|
|
const { overwrite, abort } = this.#requestOverwrites[0].once ? this.#requestOverwrites.shift() || {} : this.#requestOverwrites[0];
|
|
if (abort) {
|
|
this.#emit("fail", request.request.request);
|
|
return this.#browser.networkFailRequest({ request: request.request.request });
|
|
}
|
|
this.#emit("overwrite", request);
|
|
return this.#browser.networkContinueRequest({
|
|
request: request.request.request,
|
|
...overwrite ? parseOverwrite(overwrite, request) : {}
|
|
});
|
|
}
|
|
this.#emit("continue", request.request.request);
|
|
return this.#browser.networkContinueRequest({
|
|
request: request.request.request
|
|
});
|
|
}
|
|
#handleResponseStarted(request) {
|
|
if (!this.#isRequestMatching(request)) {
|
|
if (request.intercepts?.includes(this.#mockId)) {
|
|
return this.#browser.networkProvideResponse({
|
|
request: request.request.request
|
|
}).catch(this.#handleNetworkProvideResponseError);
|
|
}
|
|
return;
|
|
}
|
|
if (!this.#matchesFilterOptions(request)) {
|
|
this.#emit("continue", request.request.request);
|
|
return this.#browser.networkProvideResponse({
|
|
request: request.request.request
|
|
}).catch(this.#handleNetworkProvideResponseError);
|
|
}
|
|
this.#calls.push(request);
|
|
if (this.#respondOverwrites.length === 0 || !this.#respondOverwrites[0].overwrite) {
|
|
this.#emit("continue", request.request.request);
|
|
return this.#browser.networkProvideResponse({
|
|
request: request.request.request
|
|
}).catch(this.#handleNetworkProvideResponseError);
|
|
}
|
|
const { overwrite } = this.#respondOverwrites[0].once ? this.#respondOverwrites.shift() || {} : this.#respondOverwrites[0];
|
|
if (overwrite) {
|
|
this.#emit("overwrite", request);
|
|
const responseData = parseOverwrite(overwrite, request);
|
|
if (responseData.body) {
|
|
this.#responseBodies.set(request.request.request, responseData.body);
|
|
}
|
|
return this.#browser.networkProvideResponse({
|
|
request: request.request.request,
|
|
...responseData
|
|
}).catch(this.#handleNetworkProvideResponseError);
|
|
}
|
|
this.#emit("continue", request.request.request);
|
|
return this.#browser.networkProvideResponse({
|
|
request: request.request.request
|
|
}).catch(this.#handleNetworkProvideResponseError);
|
|
}
|
|
/**
|
|
* It appears that the networkProvideResponse method may throw an "no such request" error even though the request
|
|
* is marked as "blocked", in these cases we can safely ignore the error.
|
|
* @param err Bidi message error
|
|
*/
|
|
#handleNetworkProvideResponseError(err) {
|
|
if (err.message.endsWith("no such request")) {
|
|
return;
|
|
}
|
|
throw err;
|
|
}
|
|
/**
|
|
* Get the raw binary data for a mock response by request ID
|
|
* @param {string} requestId The ID of the request to retrieve the binary response for
|
|
* @returns {Buffer | null} The binary data as a Buffer, or null if no matching binary response is found
|
|
*/
|
|
getBinaryResponse(requestId) {
|
|
const body = this.#responseBodies.get(requestId);
|
|
if (body?.type !== "base64") {
|
|
return null;
|
|
}
|
|
if (/[^A-Za-z0-9+/=\s]/.test(body.value)) {
|
|
log8.warn(`Invalid base64 data for request ${requestId}`);
|
|
return null;
|
|
}
|
|
return Buffer.from(body.value, "base64");
|
|
}
|
|
/**
|
|
* Simulate a responseStarted event for testing purposes
|
|
* @param request NetworkResponseCompletedParameters to simulate
|
|
*/
|
|
simulateResponseStarted(request) {
|
|
try {
|
|
this.#handleResponseStarted(request);
|
|
} catch (e) {
|
|
console.log("DEBUG: Error in simulateResponseStarted:", e);
|
|
throw e;
|
|
}
|
|
}
|
|
debugResponseBodies() {
|
|
return this.#responseBodies;
|
|
}
|
|
#isRequestMatching(request) {
|
|
const matches = this.#pattern && this.#pattern.test(request.request.url);
|
|
return request.isBlocked && matches;
|
|
}
|
|
#matchesFilterOptions(request) {
|
|
let isRequestMatching = true;
|
|
if (isRequestMatching && this.#filterOptions.method) {
|
|
isRequestMatching = typeof this.#filterOptions.method === "function" ? this.#filterOptions.method(request.request.method) : this.#filterOptions.method.toLowerCase() === request.request.method.toLowerCase();
|
|
}
|
|
if (isRequestMatching && this.#filterOptions.requestHeaders) {
|
|
isRequestMatching = typeof this.#filterOptions.requestHeaders === "function" ? this.#filterOptions.requestHeaders(request.request.headers.reduce((acc, { name, value }) => {
|
|
acc[name] = value.type === "string" ? value.value : Buffer.from(value.value, "base64").toString();
|
|
return acc;
|
|
}, {})) : Object.entries(this.#filterOptions.requestHeaders).every(([key, value]) => {
|
|
const header = request.request.headers.find(({ name }) => name === key);
|
|
if (!header) {
|
|
return false;
|
|
}
|
|
return header.value.type === "string" ? header.value.value === value : Buffer.from(header.value.value, "base64").toString() === value;
|
|
});
|
|
}
|
|
if (isRequestMatching && this.#filterOptions.responseHeaders && "response" in request) {
|
|
isRequestMatching = typeof this.#filterOptions.responseHeaders === "function" ? this.#filterOptions.responseHeaders(request.response.headers.reduce((acc, { name, value }) => {
|
|
acc[name] = value.type === "string" ? value.value : Buffer.from(value.value, "base64").toString();
|
|
return acc;
|
|
}, {})) : Object.entries(this.#filterOptions.responseHeaders).every(([key, value]) => {
|
|
const header = request.response.headers.find(({ name }) => name === key);
|
|
if (!header) {
|
|
return false;
|
|
}
|
|
return header.value.type === "string" ? header.value.value === value : Buffer.from(header.value.value, "base64").toString() === value;
|
|
});
|
|
}
|
|
if (isRequestMatching && this.#filterOptions.statusCode && "response" in request) {
|
|
isRequestMatching = typeof this.#filterOptions.statusCode === "function" ? this.#filterOptions.statusCode(request.response.status) : this.#filterOptions.statusCode === request.response.status;
|
|
}
|
|
return isRequestMatching;
|
|
}
|
|
#setOverwrite = (overwriteProp, { overwrite, abort, once }) => {
|
|
return once ? [
|
|
...overwriteProp.filter(({ once: once2 }) => once2),
|
|
{ overwrite, abort, once }
|
|
] : [{ overwrite, abort }];
|
|
};
|
|
/**
|
|
* allows access to all requests made with given pattern
|
|
*/
|
|
get calls() {
|
|
return this.#calls;
|
|
}
|
|
/**
|
|
* Resets all information stored in the `mock.calls` set.
|
|
*/
|
|
clear() {
|
|
this.#calls = [];
|
|
this.#responseBodies.clear();
|
|
return this;
|
|
}
|
|
/**
|
|
* Does what `mock.clear()` does and makes removes custom request overrides
|
|
* and response overwrites
|
|
*/
|
|
reset() {
|
|
this.clear();
|
|
this.#respondOverwrites = [];
|
|
this.#requestOverwrites = [];
|
|
return this;
|
|
}
|
|
/**
|
|
* Does everything that `mock.reset()` does, and also
|
|
* removes any mocked return values or implementations.
|
|
* Restored mock does not emit events and could not mock responses
|
|
*/
|
|
async restore() {
|
|
this.reset();
|
|
this.#respondOverwrites = [];
|
|
this.#restored = true;
|
|
const handle = await this.#browser.getWindowHandle();
|
|
log8.trace(`Restoring mock for ${handle}`);
|
|
SESSION_MOCKS[handle].delete(this);
|
|
if (this.#mockId) {
|
|
await this.#browser.networkRemoveIntercept({ intercept: this.#mockId });
|
|
}
|
|
return this;
|
|
}
|
|
/**
|
|
* Always use request modification for the next request done by the browser.
|
|
* @param payload payload to overwrite the request
|
|
* @param once apply overwrite only once for the next request
|
|
* @returns this instance to chain commands
|
|
*/
|
|
request(overwrite, once) {
|
|
this.#ensureNotRestored();
|
|
this.#requestOverwrites = this.#setOverwrite(this.#requestOverwrites, { overwrite, once });
|
|
return this;
|
|
}
|
|
/**
|
|
* alias for `mock.request(…, true)`
|
|
*/
|
|
requestOnce(payload) {
|
|
return this.request(payload, true);
|
|
}
|
|
/**
|
|
* Always respond with same overwrite
|
|
* @param {*} payload payload to overwrite the response
|
|
* @param {*} params additional respond parameters to overwrite
|
|
* @param {boolean} once apply overwrite only once for the next request
|
|
* @returns this instance to chain commands
|
|
*/
|
|
respond(payload, params = {}, once) {
|
|
this.#ensureNotRestored();
|
|
const body = Buffer.isBuffer(payload) ? { type: "base64", value: payload.toString("base64") } : { type: "string", value: typeof payload === "string" ? payload : JSON.stringify(payload) };
|
|
const overwrite = { body, ...params };
|
|
this.#respondOverwrites = this.#setOverwrite(this.#respondOverwrites, { overwrite, once });
|
|
return this;
|
|
}
|
|
/**
|
|
* alias for `mock.respond(…, true)`
|
|
*/
|
|
respondOnce(payload, params = {}) {
|
|
return this.respond(payload, params, true);
|
|
}
|
|
/**
|
|
* Abort the request with an error code
|
|
* @param {string} errorReason error code of the response
|
|
* @param {boolean} once if request should be aborted only once for the next request
|
|
*/
|
|
abort(once) {
|
|
this.#ensureNotRestored();
|
|
this.#requestOverwrites = this.#setOverwrite(this.#requestOverwrites, { abort: true, once });
|
|
return this;
|
|
}
|
|
/**
|
|
* alias for `mock.abort(true)`
|
|
*/
|
|
abortOnce() {
|
|
return this.abort(true);
|
|
}
|
|
/**
|
|
* Redirect request to another URL
|
|
* @param {string} redirectUrl URL to redirect to
|
|
* @param {boolean} sticky if request should be redirected for all following requests
|
|
*/
|
|
redirect(redirectUrl, once) {
|
|
this.#ensureNotRestored();
|
|
const requestWith = { url: redirectUrl };
|
|
this.request(requestWith, once);
|
|
return this;
|
|
}
|
|
/**
|
|
* alias for `mock.redirect(…, true)`
|
|
*/
|
|
redirectOnce(redirectUrl) {
|
|
return this.redirect(redirectUrl, true);
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
on(event, callback) {
|
|
this.#addEventHandler(event, callback);
|
|
return this;
|
|
}
|
|
#ensureNotRestored() {
|
|
if (this.#restored) {
|
|
throw new Error("This can't be done on restored mock");
|
|
}
|
|
}
|
|
waitForResponse({
|
|
timeout = this.#browser.options.waitforTimeout,
|
|
interval = this.#browser.options.waitforInterval,
|
|
timeoutMsg
|
|
} = {}) {
|
|
if (typeof timeout !== "number") {
|
|
timeout = this.#browser.options.waitforTimeout;
|
|
}
|
|
if (typeof interval !== "number") {
|
|
interval = this.#browser.options.waitforInterval;
|
|
}
|
|
const fn = async () => this.calls && (await this.calls).length > 0;
|
|
const timer = new Timer_default(interval, timeout, fn, true);
|
|
return this.#browser.call(() => timer.catch((e) => {
|
|
if (e.message === "timeout") {
|
|
if (typeof timeoutMsg === "string") {
|
|
throw new Error(timeoutMsg);
|
|
}
|
|
throw new Error(`waitForResponse timed out after ${timeout}ms`);
|
|
}
|
|
throw new Error(`waitForResponse failed with the following reason: ${e && e.message || e}`);
|
|
}));
|
|
}
|
|
};
|
|
function parseUrlPattern(url2) {
|
|
if (typeof url2 === "object") {
|
|
return url2;
|
|
}
|
|
if (url2.startsWith("http")) {
|
|
return new URLPattern(url2);
|
|
}
|
|
return new URLPattern({
|
|
pathname: url2
|
|
});
|
|
}
|
|
|
|
// src/commands/browser/mock.ts
|
|
var SESSION_MOCKS = {};
|
|
async function mock(url2, filterOptions) {
|
|
if (!this.isBidi) {
|
|
throw new Error("Mocking is only supported when running tests using WebDriver Bidi");
|
|
}
|
|
const browser = getBrowserObject7(this);
|
|
const contextManager = getContextManager(browser);
|
|
const context = await contextManager.getCurrentContext();
|
|
if (!SESSION_MOCKS[context]) {
|
|
SESSION_MOCKS[context] = /* @__PURE__ */ new Set();
|
|
}
|
|
const networkInterception = await WebDriverInterception.initiate(url2, filterOptions || {}, this);
|
|
SESSION_MOCKS[context].add(networkInterception);
|
|
return networkInterception;
|
|
}
|
|
|
|
// src/commands/browser/mockClearAll.ts
|
|
import logger9 from "@wdio/logger";
|
|
var log9 = logger9("webdriverio:mockClearAll");
|
|
async function mockClearAll() {
|
|
for (const [handle, mocks] of Object.entries(SESSION_MOCKS)) {
|
|
log9.trace(`Clearing mocks for ${handle}`);
|
|
for (const mock2 of mocks) {
|
|
mock2.clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
// src/commands/browser/mockRestoreAll.ts
|
|
import logger10 from "@wdio/logger";
|
|
var log10 = logger10("webdriverio:mockRestoreAll");
|
|
async function mockRestoreAll() {
|
|
for (const [handle, mocks] of Object.entries(SESSION_MOCKS)) {
|
|
log10.trace(`Clearing mocks for ${handle}`);
|
|
for (const mock2 of mocks) {
|
|
await mock2.restore();
|
|
}
|
|
}
|
|
}
|
|
|
|
// src/commands/browser/newWindow.ts
|
|
import { sleep } from "@wdio/utils";
|
|
import newWindowHelper from "./scripts/newWindow.js";
|
|
import logger11 from "@wdio/logger";
|
|
var log11 = logger11("webdriverio:newWindow");
|
|
var WAIT_FOR_NEW_HANDLE_TIMEOUT = 3e3;
|
|
async function newWindow(url2, { type = "window", windowName = "", windowFeatures = "" } = {}) {
|
|
if (typeof url2 !== "string") {
|
|
throw new Error("number or type of arguments don't agree with newWindow command");
|
|
}
|
|
if (!["tab", "window"].includes(type)) {
|
|
throw new Error(`Invalid type '${type}' provided to newWindow command. Use either 'tab' or 'window'`);
|
|
}
|
|
if (windowName || windowFeatures) {
|
|
log11.warn('The "windowName" and "windowFeatures" options are deprecated and only supported in WebDriver Classic sessions.');
|
|
}
|
|
if (this.isMobile) {
|
|
throw new Error("newWindow command is not supported on mobile platforms");
|
|
}
|
|
const tabsBefore = await this.getWindowHandles();
|
|
if (this.isBidi) {
|
|
const contextManager = getContextManager(this);
|
|
const { context } = await this.browsingContextCreate({ type });
|
|
contextManager.setCurrentContext(context);
|
|
await this.browsingContextNavigate({ context, url: url2 });
|
|
} else {
|
|
await this.execute(newWindowHelper, url2, windowName, windowFeatures);
|
|
}
|
|
let tabsAfter = await this.getWindowHandles();
|
|
const now = Date.now();
|
|
while (Date.now() - now < WAIT_FOR_NEW_HANDLE_TIMEOUT) {
|
|
tabsAfter = await this.getWindowHandles();
|
|
if (tabsAfter.length > tabsBefore.length) {
|
|
break;
|
|
}
|
|
await sleep(100);
|
|
}
|
|
const newTab = tabsAfter.pop();
|
|
if (!newTab) {
|
|
throw new Error("No window handle was found to switch to");
|
|
}
|
|
await this.switchToWindow(newTab);
|
|
return { handle: newTab, type };
|
|
}
|
|
|
|
// src/commands/browser/pause.ts
|
|
function pause(milliseconds = 1e3) {
|
|
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
}
|
|
|
|
// src/commands/browser/react$$.ts
|
|
import { waitToLoadReact, react$$ as react$$Script } from "./scripts/resq.js";
|
|
async function react$$(selector, { props = {}, state = {} } = {}) {
|
|
await this.executeScript(resqScript, []);
|
|
await this.execute(waitToLoadReact);
|
|
const res = await this.execute(
|
|
react$$Script,
|
|
selector,
|
|
props,
|
|
state
|
|
);
|
|
const elements = await getElements.call(this, selector, res, { isReactElement: true });
|
|
return enhanceElementsArray(elements, this, selector, "react$$", [props, state]);
|
|
}
|
|
|
|
// src/commands/browser/react$.ts
|
|
import { waitToLoadReact as waitToLoadReact2, react$ as react$Script } from "./scripts/resq.js";
|
|
async function react$(selector, { props = {}, state = {} } = {}) {
|
|
await this.executeScript(resqScript.toString(), []);
|
|
await this.execute(waitToLoadReact2);
|
|
const res = await this.execute(
|
|
react$Script,
|
|
selector,
|
|
props,
|
|
state
|
|
);
|
|
return getElement.call(this, selector, res, { isReactElement: true });
|
|
}
|
|
|
|
// src/commands/browser/reloadSession.ts
|
|
import logger14 from "@wdio/logger";
|
|
|
|
// src/session/polyfill.ts
|
|
import logger12 from "@wdio/logger";
|
|
import { polyfillFn as polyfillFn3 } from "./scripts/polyfill.js";
|
|
function getPolyfillManager(browser) {
|
|
return SessionManager.getSessionManager(browser, PolyfillManager);
|
|
}
|
|
var log12 = logger12("webdriverio:PolyfillManager");
|
|
var PolyfillManager = class _PolyfillManager extends SessionManager {
|
|
#initialize;
|
|
#browser;
|
|
#scriptsRegisteredInContexts = /* @__PURE__ */ new Set();
|
|
#registerScriptsListener = this.#registerScripts.bind(this);
|
|
constructor(browser) {
|
|
super(browser, _PolyfillManager.name);
|
|
this.#browser = browser;
|
|
if (!this.isEnabled()) {
|
|
this.#initialize = Promise.resolve(true);
|
|
return;
|
|
}
|
|
this.#browser.on("browsingContext.contextCreated", this.#registerScriptsListener);
|
|
this.#initialize = Promise.all([
|
|
this.#browser.browsingContextGetTree({}).then(({ contexts }) => {
|
|
return Promise.all(contexts.map((context) => this.#registerScripts(context)));
|
|
}),
|
|
this.#browser.sessionSubscribe({
|
|
events: ["browsingContext.contextCreated"]
|
|
})
|
|
]).then(() => true, () => false);
|
|
}
|
|
removeListeners() {
|
|
super.removeListeners();
|
|
this.#browser.off("browsingContext.contextCreated", this.#registerScriptsListener);
|
|
}
|
|
#registerScripts(context) {
|
|
if (this.#scriptsRegisteredInContexts.has(context.context)) {
|
|
return;
|
|
}
|
|
const functionDeclaration = createFunctionDeclarationFromString(polyfillFn3);
|
|
log12.info(`Adding polyfill script to context with id ${context.context}`);
|
|
this.#scriptsRegisteredInContexts.add(context.context);
|
|
return Promise.all([
|
|
!context.parent ? this.#browser.scriptAddPreloadScript({
|
|
functionDeclaration,
|
|
contexts: [context.context]
|
|
}).catch(() => {
|
|
}) : Promise.resolve(),
|
|
this.#browser.scriptCallFunction({
|
|
functionDeclaration,
|
|
target: context,
|
|
awaitPromise: false
|
|
}).catch(() => {
|
|
})
|
|
]);
|
|
}
|
|
async initialize() {
|
|
return this.#initialize;
|
|
}
|
|
};
|
|
|
|
// src/session/shadowRoot.ts
|
|
import logger13 from "@wdio/logger";
|
|
import customElementWrapper from "./scripts/customElement.js";
|
|
var log13 = logger13("webdriverio:ShadowRootManager");
|
|
function getShadowRootManager(browser) {
|
|
return SessionManager.getSessionManager(browser, ShadowRootManager);
|
|
}
|
|
var ShadowRootManager = class _ShadowRootManager extends SessionManager {
|
|
#browser;
|
|
#initialize;
|
|
#shadowRoots = /* @__PURE__ */ new Map();
|
|
#documentElement;
|
|
#frameDepth = 0;
|
|
#handleLogEntryListener = this.handleLogEntry.bind(this);
|
|
#commandResultHandlerListener = this.#commandResultHandler.bind(this);
|
|
#handleBidiCommandListener = this.#handleBidiCommand.bind(this);
|
|
constructor(browser) {
|
|
super(browser, _ShadowRootManager.name);
|
|
this.#browser = browser;
|
|
if (!this.isEnabled()) {
|
|
this.#initialize = Promise.resolve(true);
|
|
return;
|
|
}
|
|
this.#initialize = this.#browser.sessionSubscribe({
|
|
events: ["log.entryAdded", "browsingContext.navigationStarted"]
|
|
}).then(() => true, () => false);
|
|
this.#browser.on("log.entryAdded", this.#handleLogEntryListener);
|
|
this.#browser.on("result", this.#commandResultHandlerListener);
|
|
this.#browser.on("bidiCommand", this.#handleBidiCommandListener);
|
|
this.#browser.scriptAddPreloadScript({
|
|
functionDeclaration: customElementWrapper.toString()
|
|
});
|
|
}
|
|
removeListeners() {
|
|
super.removeListeners();
|
|
this.#browser.off("log.entryAdded", this.#handleLogEntryListener);
|
|
this.#browser.off("result", this.#commandResultHandlerListener);
|
|
this.#browser.off("bidiCommand", this.#handleBidiCommandListener);
|
|
}
|
|
async initialize() {
|
|
return this.#initialize;
|
|
}
|
|
/**
|
|
* keep track of navigation events and remove shadow roots when they are no longer needed
|
|
*/
|
|
#handleBidiCommand(command) {
|
|
if (command.method !== "browsingContext.navigate") {
|
|
return;
|
|
}
|
|
const params = command.params;
|
|
this.#shadowRoots.delete(params.context);
|
|
}
|
|
/**
|
|
* keep track of frame depth
|
|
*/
|
|
#commandResultHandler(result) {
|
|
const noResultError = typeof result.result === "object" && result.result && "error" in result.result && !result.result.error;
|
|
if (result.command === "switchToFrame" && noResultError) {
|
|
this.#frameDepth++;
|
|
}
|
|
if (result.command === "switchToParentFrame" && noResultError) {
|
|
this.#frameDepth = Math.max(0, this.#frameDepth - 1);
|
|
}
|
|
}
|
|
/**
|
|
* check if we are within a frame
|
|
* @returns {boolean} true if we are within a frame
|
|
*/
|
|
isWithinFrame() {
|
|
return this.#frameDepth > 0;
|
|
}
|
|
/**
|
|
* capture shadow root elements propagated through console.debug
|
|
*/
|
|
handleLogEntry(logEntry) {
|
|
const args = "args" in logEntry && logEntry.level === "debug" ? logEntry.args : void 0;
|
|
if (!args || args[0].type !== "string" || args[0].value !== "[WDIO]" || args[1].type !== "string") {
|
|
return;
|
|
}
|
|
if (!logEntry.source.context) {
|
|
return;
|
|
}
|
|
const eventType = args[1].value;
|
|
if (eventType === "newShadowRoot" && args[2].type === "node" && args[3].type === "node") {
|
|
const [
|
|
/* [WDIO] */
|
|
,
|
|
/* newShadowRoot */
|
|
,
|
|
shadowElem,
|
|
rootElem,
|
|
isDocument,
|
|
documentElement
|
|
] = args;
|
|
if (!this.#shadowRoots.has(logEntry.source.context)) {
|
|
if (!rootElem.sharedId) {
|
|
throw new Error(`Expected "sharedId" parameter from object ${rootElem}`);
|
|
}
|
|
this.#shadowRoots.set(logEntry.source.context, new ShadowRootTree(rootElem.sharedId));
|
|
} else if (isDocument.type === "boolean" && isDocument.value) {
|
|
if (!rootElem.sharedId) {
|
|
throw new Error(`Expected "sharedId" parameter from object ${rootElem}`);
|
|
}
|
|
const tree2 = this.#shadowRoots.get(logEntry.source.context);
|
|
if (tree2?.element !== rootElem.sharedId) {
|
|
this.#shadowRoots.set(logEntry.source.context, new ShadowRootTree(rootElem.sharedId));
|
|
}
|
|
}
|
|
this.#documentElement = documentElement;
|
|
const tree = this.#shadowRoots.get(logEntry.source.context);
|
|
if (!tree) {
|
|
throw new Error(`Couldn't find tree for context id ${logEntry.source.context}`);
|
|
}
|
|
if (
|
|
// we expect an element id
|
|
!shadowElem.sharedId || // we expect the element to have a shadow root
|
|
!shadowElem.value?.shadowRoot?.sharedId || // we expect the shadow root to have a proper type
|
|
shadowElem.value.shadowRoot.value?.nodeType !== 11
|
|
) {
|
|
return log13.warn(`Expected element with shadow root but found <${shadowElem.value?.localName} />`);
|
|
}
|
|
log13.info(`Registered new shadow root for element <${shadowElem.value.localName} /> with id ${shadowElem.value.shadowRoot.sharedId}`);
|
|
const newTree = new ShadowRootTree(
|
|
shadowElem.sharedId,
|
|
shadowElem.value.shadowRoot.sharedId,
|
|
shadowElem.value.shadowRoot.value.mode
|
|
);
|
|
if (rootElem.sharedId) {
|
|
tree.addShadowElement(rootElem.sharedId, newTree);
|
|
} else {
|
|
tree.addShadowElement(newTree);
|
|
}
|
|
return;
|
|
}
|
|
if (eventType === "removeShadowRoot" && args[2].type === "node" && args[2].sharedId) {
|
|
const tree = this.#shadowRoots.get(logEntry.source.context);
|
|
if (!tree) {
|
|
return;
|
|
}
|
|
return tree.remove(args[2].sharedId);
|
|
}
|
|
throw new Error(`Invalid parameters for "${eventType}" event: ${args.join(", ")}`);
|
|
}
|
|
getShadowElementsByContextId(contextId, scope) {
|
|
let tree = this.#shadowRoots.get(contextId);
|
|
if (!tree) {
|
|
return [];
|
|
}
|
|
let documentElement;
|
|
if (scope) {
|
|
const subTree = tree.find(scope);
|
|
if (subTree) {
|
|
tree = subTree;
|
|
}
|
|
} else {
|
|
documentElement = this.#documentElement?.sharedId;
|
|
}
|
|
const elements = tree.getAllLookupScopes();
|
|
return [
|
|
...documentElement ? [documentElement] : [],
|
|
...new Set(elements).values()
|
|
];
|
|
}
|
|
getShadowElementPairsByContextId(contextId, scope) {
|
|
let tree = this.#shadowRoots.get(contextId);
|
|
if (!tree) {
|
|
return [];
|
|
}
|
|
if (scope) {
|
|
const subTree = tree.find(scope);
|
|
if (subTree) {
|
|
tree = subTree;
|
|
}
|
|
}
|
|
return tree.flat().map((tree2) => [tree2.element, tree2.shadowRoot]);
|
|
}
|
|
getShadowRootModeById(contextId, element) {
|
|
const tree = this.#shadowRoots.get(contextId);
|
|
if (!tree) {
|
|
return;
|
|
}
|
|
const shadowTree = tree.find(element);
|
|
if (!shadowTree) {
|
|
return;
|
|
}
|
|
return shadowTree.mode;
|
|
}
|
|
deleteShadowRoot(element, contextId) {
|
|
const tree = this.#shadowRoots.get(contextId);
|
|
if (!tree) {
|
|
return;
|
|
}
|
|
return tree.remove(element);
|
|
}
|
|
};
|
|
var ShadowRootTree = class _ShadowRootTree {
|
|
element;
|
|
shadowRoot;
|
|
mode;
|
|
children = /* @__PURE__ */ new Set();
|
|
constructor(element, shadowRoot, mode) {
|
|
this.element = element;
|
|
this.shadowRoot = shadowRoot;
|
|
this.mode = mode;
|
|
}
|
|
addShadowElement(...args) {
|
|
const [scope, treeArg] = args;
|
|
if (!scope && !treeArg) {
|
|
throw new Error('Method "addShadowElement" expects at least 2 arguments');
|
|
}
|
|
if (scope instanceof _ShadowRootTree) {
|
|
this.children.add(scope);
|
|
return;
|
|
}
|
|
if (typeof scope === "string" && treeArg instanceof _ShadowRootTree) {
|
|
const tree = this.find(scope) || this.findByShadowId(scope);
|
|
if (!tree) {
|
|
return;
|
|
}
|
|
tree.addShadowElement(treeArg);
|
|
return;
|
|
}
|
|
throw new Error('Invalid arguments for "addShadowElement" method');
|
|
}
|
|
find(element) {
|
|
if (this.element === element) {
|
|
return this;
|
|
}
|
|
for (const child of this.children) {
|
|
const elem = child.find(element);
|
|
if (elem) {
|
|
return elem;
|
|
}
|
|
}
|
|
return void 0;
|
|
}
|
|
findByShadowId(shadowRoot) {
|
|
if (this.shadowRoot === shadowRoot) {
|
|
return this;
|
|
}
|
|
for (const child of this.children) {
|
|
const elem = child.findByShadowId(shadowRoot);
|
|
if (elem) {
|
|
return elem;
|
|
}
|
|
}
|
|
return void 0;
|
|
}
|
|
getAllLookupScopes() {
|
|
return [
|
|
this.shadowRoot ?? this.element,
|
|
...Array.from(this.children).map((tree) => tree.getAllLookupScopes())
|
|
].flat();
|
|
}
|
|
flat() {
|
|
return [this, ...Array.from(this.children).map((tree) => tree.flat())].flat();
|
|
}
|
|
remove(element) {
|
|
const childArray = Array.from(this.children);
|
|
for (let i = childArray.length - 1; i >= 0; i--) {
|
|
if (childArray[i].element === element) {
|
|
return this.children.delete(childArray[i]);
|
|
}
|
|
const wasFound = childArray[i].remove(element);
|
|
if (wasFound) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// src/session/networkManager.ts
|
|
function getNetworkManager(browser) {
|
|
return SessionManager.getSessionManager(browser, NetworkManager);
|
|
}
|
|
var UNKNOWN_NAVIGATION_ID = "UNKNOWN_NAVIGATION_ID";
|
|
var SUPPORTED_NAVIGATION_PROTOCOLS = ["http", "https", "data", "file"];
|
|
var NetworkManager = class _NetworkManager extends SessionManager {
|
|
#browser;
|
|
#initialize;
|
|
#requests = /* @__PURE__ */ new Map();
|
|
#lastNetworkId;
|
|
#navigationStartedListener = this.#navigationStarted.bind(this);
|
|
#responseCompletedListener = this.#responseCompleted.bind(this);
|
|
#beforeRequestSentListener = this.#beforeRequestSent.bind(this);
|
|
#fetchErrorListener = this.#fetchError.bind(this);
|
|
constructor(browser) {
|
|
super(browser, _NetworkManager.name);
|
|
this.#browser = browser;
|
|
if (!this.isEnabled()) {
|
|
this.#initialize = Promise.resolve(true);
|
|
return;
|
|
}
|
|
this.#initialize = this.#browser.sessionSubscribe({
|
|
events: [
|
|
"browsingContext.navigationStarted",
|
|
"browsingContext.fragmentNavigated",
|
|
"network.responseCompleted",
|
|
"network.beforeRequestSent",
|
|
"network.fetchError"
|
|
]
|
|
}).then(() => true, () => false);
|
|
this.#browser.on("browsingContext.navigationStarted", this.#navigationStartedListener);
|
|
this.#browser.on("browsingContext.fragmentNavigated", this.#navigationStartedListener);
|
|
this.#browser.on("network.responseCompleted", this.#responseCompletedListener);
|
|
this.#browser.on("network.beforeRequestSent", this.#beforeRequestSentListener);
|
|
this.#browser.on("network.fetchError", this.#fetchErrorListener);
|
|
}
|
|
removeListeners() {
|
|
super.removeListeners();
|
|
this.#browser.off("browsingContext.navigationStarted", this.#navigationStartedListener);
|
|
this.#browser.off("browsingContext.fragmentNavigated", this.#navigationStartedListener);
|
|
this.#browser.off("network.responseCompleted", this.#responseCompletedListener);
|
|
this.#browser.off("network.beforeRequestSent", this.#beforeRequestSentListener);
|
|
this.#browser.off("network.fetchError", this.#fetchErrorListener);
|
|
}
|
|
async initialize() {
|
|
return this.#initialize;
|
|
}
|
|
#beforeRequestSent(log30) {
|
|
if (log30.navigation) {
|
|
return;
|
|
}
|
|
const request = this.#findRootRequest(log30.navigation);
|
|
if (!request) {
|
|
return;
|
|
}
|
|
const { request: id, headers, cookies, url: url2 } = log30.request;
|
|
request.children?.push({
|
|
id,
|
|
url: url2,
|
|
headers: headerListToObject(headers),
|
|
cookies: cookies.map((cookie) => ({
|
|
name: cookie.name,
|
|
value: cookie.value.type === "string" ? cookie.value.value : atob(cookie.value.value),
|
|
domain: cookie.domain,
|
|
path: cookie.path,
|
|
size: cookie.size,
|
|
httpOnly: cookie.httpOnly,
|
|
secure: cookie.secure,
|
|
sameSite: cookie.sameSite,
|
|
expiry: cookie.expiry
|
|
})),
|
|
timestamp: log30.timestamp
|
|
});
|
|
}
|
|
#navigationStarted(log30) {
|
|
if (
|
|
/**
|
|
* we need a navigation id to identify the request
|
|
*/
|
|
!log30.navigation || /**
|
|
* ignore urls that users wouldn't navigate to
|
|
*/
|
|
!SUPPORTED_NAVIGATION_PROTOCOLS.some((protocol) => log30.url.startsWith(protocol))
|
|
) {
|
|
if (log30.navigation === null && log30.url === "") {
|
|
this.#lastNetworkId = UNKNOWN_NAVIGATION_ID;
|
|
return this.#requests.set(UNKNOWN_NAVIGATION_ID, {
|
|
url: "",
|
|
headers: {},
|
|
timestamp: log30.timestamp,
|
|
redirectChain: [],
|
|
children: []
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
this.#lastNetworkId = log30.navigation;
|
|
this.#requests.set(log30.navigation, {
|
|
url: log30.url,
|
|
headers: {},
|
|
timestamp: log30.timestamp,
|
|
navigation: log30.navigation,
|
|
redirectChain: [],
|
|
children: []
|
|
});
|
|
}
|
|
#fetchError(log30) {
|
|
const response = this.#findRootRequest(log30.navigation);
|
|
if (!response) {
|
|
return;
|
|
}
|
|
const request = response.children?.find((child) => child.id === log30.request.request);
|
|
if (!request) {
|
|
return;
|
|
}
|
|
request.error = log30.errorText;
|
|
}
|
|
#findRootRequest(navigationId) {
|
|
const response = this.#requests.get(navigationId || UNKNOWN_NAVIGATION_ID);
|
|
if (response) {
|
|
return response;
|
|
}
|
|
const firstRequest = this.#requests.values().next().value;
|
|
return this.#lastNetworkId ? this.#requests.get(this.#lastNetworkId) || firstRequest : firstRequest;
|
|
}
|
|
#responseCompleted(log30) {
|
|
const response = this.#findRootRequest(log30.navigation);
|
|
if (!response) {
|
|
return;
|
|
}
|
|
if (!response.navigation && response.url === "") {
|
|
response.url = log30.request.url;
|
|
response.navigation = log30.navigation;
|
|
}
|
|
if (log30.navigation === response.navigation) {
|
|
if (response.url !== log30.response.url) {
|
|
response.redirectChain?.push(response.url);
|
|
}
|
|
response.url = log30.response.url;
|
|
const { headers: requestHeaders } = log30.request;
|
|
const { fromCache, headers: responseHeaders, mimeType, status } = log30.response;
|
|
response.headers = headerListToObject(requestHeaders), response.response = {
|
|
fromCache,
|
|
headers: headerListToObject(responseHeaders),
|
|
mimeType,
|
|
status
|
|
};
|
|
return;
|
|
}
|
|
const request = response.children?.find((child) => child.id === log30.request.request);
|
|
if (!request) {
|
|
return;
|
|
}
|
|
request.response = {
|
|
fromCache: log30.response.fromCache,
|
|
headers: headerListToObject(log30.response.headers),
|
|
mimeType: log30.response.mimeType,
|
|
status: log30.response.status
|
|
};
|
|
response.children?.push(request);
|
|
}
|
|
getRequestResponseData(navigationId) {
|
|
return this.#requests.get(navigationId);
|
|
}
|
|
/**
|
|
* Returns the number of requests that are currently pending.
|
|
* @param context browsing context id
|
|
* @returns the number of requests that are currently pending
|
|
*/
|
|
getPendingRequests(navigationId) {
|
|
const request = this.#requests.get(navigationId);
|
|
if (!request) {
|
|
throw new Error(`Couldn't find request for navigation with id ${navigationId}`);
|
|
}
|
|
const subRequests = request.children || [];
|
|
return subRequests.filter((child) => (
|
|
/**
|
|
* either the request has no response yet
|
|
*/
|
|
!child.response && /**
|
|
* and there was no request error
|
|
*/
|
|
!child.error
|
|
));
|
|
}
|
|
};
|
|
function headerListToObject(headers) {
|
|
return headers.reduce((acc, { name, value }) => {
|
|
acc[name] = value.value;
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
// src/session/dialog.ts
|
|
function getDialogManager(browser) {
|
|
return SessionManager.getSessionManager(browser, DialogManager);
|
|
}
|
|
var DialogManager = class _DialogManager extends SessionManager {
|
|
#browser;
|
|
#initialize;
|
|
#autoHandleDialog = true;
|
|
#handleUserPromptListener = this.#handleUserPrompt.bind(this);
|
|
constructor(browser) {
|
|
super(browser, _DialogManager.name);
|
|
this.#browser = browser;
|
|
if (!this.isEnabled()) {
|
|
this.#initialize = Promise.resolve(true);
|
|
return;
|
|
}
|
|
this.#initialize = this.#browser.sessionSubscribe({
|
|
events: ["browsingContext.userPromptOpened"]
|
|
}).then(() => true, () => false);
|
|
this.#browser.on("_dialogListenerRegistered", () => this.#switchListenerFlag(false));
|
|
this.#browser.on("_dialogListenerRemoved", () => this.#switchListenerFlag(true));
|
|
this.#browser.on("browsingContext.userPromptOpened", this.#handleUserPromptListener);
|
|
}
|
|
removeListeners() {
|
|
super.removeListeners();
|
|
this.#browser.off("browsingContext.userPromptOpened", this.#handleUserPromptListener);
|
|
this.#browser.removeAllListeners("_dialogListenerRegistered");
|
|
this.#browser.removeAllListeners("_dialogListenerRemoved");
|
|
}
|
|
async initialize() {
|
|
return this.#initialize;
|
|
}
|
|
/**
|
|
* capture shadow root elements propagated through console.debug
|
|
*/
|
|
async #handleUserPrompt(log30) {
|
|
if (this.#autoHandleDialog) {
|
|
try {
|
|
return await this.#browser.browsingContextHandleUserPrompt({
|
|
accept: false,
|
|
context: log30.context
|
|
});
|
|
} catch (err) {
|
|
if (err instanceof Error && (err.message.includes("no such alert") || err.message.includes("no such frame"))) {
|
|
return;
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
const dialog = new Dialog(log30, this.#browser);
|
|
this.#browser.emit("dialog", dialog);
|
|
}
|
|
/**
|
|
* Is called when a new dialog listener is registered with the `dialog` name.
|
|
* In these cases we set a flag to the `#listener` map to indicate that we
|
|
* are listening to dialog events for this page in this context.
|
|
*/
|
|
#switchListenerFlag(value) {
|
|
this.#autoHandleDialog = value;
|
|
}
|
|
};
|
|
var Dialog = class {
|
|
#browser;
|
|
#context;
|
|
#message;
|
|
#defaultValue;
|
|
#type;
|
|
constructor(event, browser) {
|
|
this.#message = event.message;
|
|
this.#defaultValue = event.defaultValue;
|
|
this.#type = event.type;
|
|
this.#context = event.context;
|
|
this.#browser = browser;
|
|
}
|
|
message() {
|
|
return this.#message;
|
|
}
|
|
defaultValue() {
|
|
return this.#defaultValue;
|
|
}
|
|
type() {
|
|
return this.#type;
|
|
}
|
|
/**
|
|
* Returns when the dialog has been accepted.
|
|
*
|
|
* @alias dialog.accept
|
|
* @param {string=} promptText A text to enter into prompt. Does not cause any effects if the dialog's type is not prompt.
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async accept(userText) {
|
|
const contextManager = getContextManager(this.#browser);
|
|
const context = await contextManager.getCurrentContext();
|
|
if (this.#context !== context) {
|
|
return;
|
|
}
|
|
await this.#browser.browsingContextHandleUserPrompt({
|
|
accept: true,
|
|
context: this.#context,
|
|
userText
|
|
});
|
|
}
|
|
async dismiss() {
|
|
const contextManager = getContextManager(this.#browser);
|
|
const context = await contextManager.getCurrentContext();
|
|
if (this.#context !== context) {
|
|
return;
|
|
}
|
|
await this.#browser.browsingContextHandleUserPrompt({
|
|
accept: false,
|
|
context: this.#context
|
|
});
|
|
}
|
|
};
|
|
|
|
// src/session/index.ts
|
|
function registerSessionManager(instance) {
|
|
const initializationPromises = [
|
|
getContextManager(instance).initialize()
|
|
];
|
|
if (typeof instance.capabilities.webSocketUrl === "string") {
|
|
initializationPromises.push(
|
|
getPolyfillManager(instance).initialize(),
|
|
getShadowRootManager(instance).initialize(),
|
|
getNetworkManager(instance).initialize(),
|
|
getDialogManager(instance).initialize()
|
|
);
|
|
}
|
|
return Promise.all(initializationPromises);
|
|
}
|
|
|
|
// src/commands/browser/reloadSession.ts
|
|
var log14 = logger14("webdriverio");
|
|
async function reloadSession(newCapabilities) {
|
|
const oldSessionId = this.sessionId;
|
|
const shutdownDriver = Boolean(newCapabilities?.browserName);
|
|
try {
|
|
await this.deleteSession({ shutdownDriver });
|
|
} catch (err) {
|
|
log14.warn(`Suppressing error closing the session: ${err.stack}`);
|
|
}
|
|
if (this.puppeteer?.connected) {
|
|
this.puppeteer.disconnect();
|
|
log14.debug("Disconnected puppeteer session");
|
|
}
|
|
const ProtocolDriver = (await import(
|
|
/* @vite-ignore */
|
|
this.options.automationProtocol
|
|
)).default;
|
|
await ProtocolDriver.reloadSession(this, newCapabilities);
|
|
await registerSessionManager(this);
|
|
const options = this.options;
|
|
if (Array.isArray(options.onReload) && options.onReload.length) {
|
|
await Promise.all(options.onReload.map((hook) => hook(oldSessionId, this.sessionId)));
|
|
}
|
|
return this.sessionId;
|
|
}
|
|
|
|
// src/commands/browser/restore.ts
|
|
async function restore(scopes2) {
|
|
const scopeArray = !scopes2 || Array.isArray(scopes2) ? scopes2 : [scopes2];
|
|
const instanceRestoreFunctions = restoreFunctions.get(this);
|
|
if (!instanceRestoreFunctions) {
|
|
return;
|
|
}
|
|
await Promise.all(Array.from(instanceRestoreFunctions.entries()).map(async ([scope, restoreFunctionsList]) => {
|
|
if (!scopeArray || scopeArray.includes(scope)) {
|
|
await Promise.all(restoreFunctionsList.map((fn) => fn()));
|
|
instanceRestoreFunctions.set(scope, []);
|
|
}
|
|
}));
|
|
}
|
|
|
|
// src/commands/browser/savePDF.ts
|
|
async function savePDF2(filepath, options) {
|
|
return environment.value.savePDF.call(this, filepath, options);
|
|
}
|
|
|
|
// src/commands/browser/saveRecordingScreen.ts
|
|
async function saveRecordingScreen2(filepath) {
|
|
return environment.value.saveRecordingScreen.call(this, filepath);
|
|
}
|
|
|
|
// src/commands/browser/saveScreenshot.ts
|
|
async function saveScreenshot2(filepath, options) {
|
|
return environment.value.saveScreenshot.call(this, filepath, options);
|
|
}
|
|
|
|
// src/commands/browser/scroll.ts
|
|
import logger15 from "@wdio/logger";
|
|
var log15 = logger15("webdriverio");
|
|
function scroll(x = 0, y = 0) {
|
|
if (!x && !y) {
|
|
log15.warn('"scroll" command was called with no parameters, skipping execution');
|
|
return Promise.resolve();
|
|
}
|
|
if (this.isMobile) {
|
|
return this.execute((x2, y2) => window.scrollBy(x2, y2), x, y);
|
|
}
|
|
return this.action("wheel").scroll({
|
|
deltaX: x,
|
|
deltaY: y,
|
|
duration: 0
|
|
}).perform();
|
|
}
|
|
|
|
// src/commands/browser/setCookies.ts
|
|
import logger16 from "@wdio/logger";
|
|
var log16 = logger16("webdriverio");
|
|
async function setCookies(cookieObjs) {
|
|
const cookieObjsList = !Array.isArray(cookieObjs) ? [cookieObjs] : cookieObjs;
|
|
if (cookieObjsList.some((obj) => typeof obj !== "object")) {
|
|
throw new Error("Invalid input (see https://webdriver.io/docs/api/browser/setCookies for documentation)");
|
|
}
|
|
if (!this.isBidi) {
|
|
await Promise.all(cookieObjsList.map((cookieObj) => this.addCookie(cookieObj)));
|
|
return;
|
|
}
|
|
let url2;
|
|
try {
|
|
url2 = new URL(await this.getUrl());
|
|
if (url2.origin === "null") {
|
|
await Promise.all(cookieObjsList.map((cookieObj) => this.addCookie(cookieObj)));
|
|
return;
|
|
}
|
|
} catch {
|
|
await Promise.all(cookieObjsList.map((cookieObj) => this.addCookie(cookieObj)));
|
|
return;
|
|
}
|
|
try {
|
|
await Promise.all(cookieObjsList.map((cookie) => this.storageSetCookie({
|
|
cookie: {
|
|
...cookie,
|
|
domain: cookie.domain || url2.hostname,
|
|
value: {
|
|
type: "string",
|
|
value: cookie.value
|
|
}
|
|
},
|
|
partition: {
|
|
type: "storageKey",
|
|
sourceOrigin: url2.origin
|
|
}
|
|
})));
|
|
} catch (err) {
|
|
log16.warn(`BiDi setCookies failed, falling back to classic: ${err.message}`);
|
|
await Promise.all(cookieObjsList.map((cookieObj) => this.addCookie(cookieObj)));
|
|
}
|
|
return;
|
|
}
|
|
|
|
// src/commands/browser/setTimeout.ts
|
|
async function setTimeout2(timeouts) {
|
|
if (typeof timeouts !== "object") {
|
|
throw new Error('Parameter for "setTimeout" command needs to be an object');
|
|
}
|
|
const timeoutValues = Object.values(timeouts);
|
|
if (timeoutValues.length && timeoutValues.every((timeout) => typeof timeout !== "number" || timeout < 0 || timeout > Number.MAX_SAFE_INTEGER)) {
|
|
throw new Error("Specified timeout values are not valid integer (see https://webdriver.io/docs/api/browser/setTimeout for documentation).");
|
|
}
|
|
const implicit = timeouts.implicit;
|
|
const pageLoad = timeouts["page load"] || timeouts.pageLoad;
|
|
const script = timeouts.script;
|
|
const setTimeouts = this.setTimeouts.bind(this);
|
|
return setTimeouts(implicit, pageLoad, script);
|
|
}
|
|
|
|
// src/commands/browser/setViewport.ts
|
|
var minWindowSize = 0;
|
|
var maxWindowSize = Number.MAX_SAFE_INTEGER;
|
|
async function setViewport(options) {
|
|
if (typeof options.width !== "number" || typeof options.height !== "number") {
|
|
throw new Error("setViewport expects width and height of type number");
|
|
}
|
|
if (options.width < minWindowSize || options.width > maxWindowSize || options.height < minWindowSize || options.height > maxWindowSize) {
|
|
throw new Error("setViewport expects width and height to be a number in the 0 to 2^31 \u2212 1 range");
|
|
}
|
|
if (options.devicePixelRatio && (typeof options.devicePixelRatio !== "number" || options.devicePixelRatio < 0)) {
|
|
throw new Error("setViewport expects devicePixelRatio to be a number in the 0 to 2^31 \u2212 1 range");
|
|
}
|
|
const contextManager = getContextManager(this);
|
|
const context = await contextManager.getCurrentContext();
|
|
await this.browsingContextSetViewport({
|
|
context,
|
|
devicePixelRatio: options.devicePixelRatio || 1,
|
|
viewport: {
|
|
width: options.width,
|
|
height: options.height
|
|
}
|
|
});
|
|
}
|
|
|
|
// src/commands/browser/setWindowSize.ts
|
|
import { getBrowserObject as getBrowserObject8 } from "@wdio/utils";
|
|
var minWindowSize2 = 0;
|
|
var maxWindowSize2 = Number.MAX_SAFE_INTEGER;
|
|
async function setWindowSize(width, height) {
|
|
if (typeof width !== "number" || typeof height !== "number") {
|
|
throw new Error("setWindowSize expects width and height of type number");
|
|
}
|
|
if (width < minWindowSize2 || width > maxWindowSize2 || height < minWindowSize2 || height > maxWindowSize2) {
|
|
throw new Error("setWindowSize expects width and height to be a number in the 0 to 2^31 \u2212 1 range");
|
|
}
|
|
const browser = getBrowserObject8(this);
|
|
await browser.setWindowRect(null, null, width, height);
|
|
}
|
|
|
|
// src/commands/browser/switchWindow.ts
|
|
async function switchWindow(matcher) {
|
|
if (typeof matcher !== "string" && !(matcher instanceof RegExp)) {
|
|
throw new Error('Unsupported parameter for switchWindow, required is "string" or a RegExp');
|
|
}
|
|
const contextManager = getContextManager(this);
|
|
const tabs = await this.getWindowHandles();
|
|
if (typeof matcher === "string" && tabs.includes(matcher)) {
|
|
if (matcher === contextManager.getCurrentWindowHandle()) {
|
|
return matcher;
|
|
}
|
|
await this.switchToWindow(matcher);
|
|
contextManager.setCurrentContext(matcher);
|
|
return matcher;
|
|
}
|
|
const matchesTarget = (target) => {
|
|
if (typeof matcher === "string") {
|
|
return target.includes(matcher);
|
|
}
|
|
return matcher.test(target);
|
|
};
|
|
for (const tab of tabs) {
|
|
await this.switchToWindow(tab);
|
|
contextManager.setCurrentContext(tab);
|
|
const url2 = await this.getUrl();
|
|
if (matchesTarget(url2)) {
|
|
return tab;
|
|
}
|
|
const title = await this.getTitle();
|
|
if (matchesTarget(title)) {
|
|
return tab;
|
|
}
|
|
const windowName = await this.execute(
|
|
/* istanbul ignore next */
|
|
() => window.name
|
|
);
|
|
if (windowName && matchesTarget(windowName)) {
|
|
return tab;
|
|
}
|
|
}
|
|
throw new Error(`No window found with title, url, name or window handle matching "${matcher}"`);
|
|
}
|
|
|
|
// src/commands/browser/switchFrame.ts
|
|
import logger17 from "@wdio/logger";
|
|
import { ELEMENT_KEY as ELEMENT_KEY8 } from "webdriver";
|
|
import findIframeInShadowDOM from "./scripts/shadowDom.js";
|
|
var log17 = logger17("webdriverio:switchFrame");
|
|
async function switchFrame(context) {
|
|
function isPossiblyUnresolvedElement(input) {
|
|
return Boolean(input) && typeof input === "object" && typeof input.getElement === "function";
|
|
}
|
|
if (!this.isBidi) {
|
|
if (typeof context === "function") {
|
|
throw new Error("Cannot use a function to fetch a context in WebDriver Classic");
|
|
}
|
|
if (typeof context === "string") {
|
|
throw new Error("Cannot use a string to fetch a context in WebDriver Classic");
|
|
}
|
|
if (isPossiblyUnresolvedElement(context)) {
|
|
const element = await context.getElement();
|
|
await element.waitForExist({
|
|
timeoutMsg: `Can't switch to frame with selector ${element.selector} because it doesn't exist`
|
|
});
|
|
return switchToFrame(this, element);
|
|
}
|
|
return switchToFrame(this, context);
|
|
}
|
|
const sessionContext = getContextManager(this);
|
|
if (context === null) {
|
|
const handle = await this.getWindowHandle();
|
|
switchToFrameHelper(this, handle);
|
|
await switchToFrame(this, context);
|
|
return handle;
|
|
}
|
|
if (typeof context === "string") {
|
|
const newContextId = await this.waitUntil(async () => {
|
|
const tree = await this.browsingContextGetTree({});
|
|
const urlContext = sessionContext.findContext(context, tree.contexts, "byUrl") || /**
|
|
* In case the user provides an url without `/` at the end, e.g. `https://example.com`,
|
|
* the `browsingContextGetTree` command may return a context with the url `https://example.com/`.
|
|
*/
|
|
sessionContext.findContext(`${context}/`, tree.contexts, "byUrl");
|
|
const urlContextContaining = sessionContext.findContext(context, tree.contexts, "byUrlContaining");
|
|
const contextIdContext = sessionContext.findContext(context, tree.contexts, "byContextId");
|
|
if (urlContext) {
|
|
log17.info(`Found context by url "${urlContext.url}" with context id "${urlContext.context}"`);
|
|
return urlContext.context;
|
|
} else if (urlContextContaining) {
|
|
log17.info(`Found context by url containing "${urlContextContaining.url}" with context id "${urlContextContaining.context}"`);
|
|
return urlContextContaining.context;
|
|
} else if (contextIdContext) {
|
|
log17.info(`Found context by id "${contextIdContext}" with url "${contextIdContext.url}"`);
|
|
return contextIdContext.context;
|
|
}
|
|
return false;
|
|
}, {
|
|
timeout: this.options.waitforTimeout,
|
|
interval: this.options.waitforInterval,
|
|
timeoutMsg: `No frame with url or id "${context}" found within the timeout`
|
|
});
|
|
const currentContext = await sessionContext.getCurrentContext();
|
|
const allContexts = await sessionContext.getFlatContextTree();
|
|
const allFrames = (await Promise.all(Object.keys(allContexts).map(async (id) => {
|
|
const { nodes } = await this.browsingContextLocateNodes({
|
|
locator: { type: "css", value: "iframe, frame" },
|
|
context: id
|
|
}).catch(() => ({ nodes: [] }));
|
|
return Promise.all(nodes.map(async (node) => {
|
|
const html = `<iframe${Object.entries(node.value?.attributes || {}).reduce((acc, [key, value]) => `${acc} ${key}="${value}"`, " ")}></iframe>`;
|
|
const args = [{ [ELEMENT_KEY8]: node.sharedId }];
|
|
const userScript = (iframe) => iframe.contentWindow;
|
|
const functionDeclaration = new Function(`
|
|
return (${SCRIPT_PREFIX}${userScript.toString()}${SCRIPT_SUFFIX}).apply(this, arguments);
|
|
`).toString();
|
|
const params = {
|
|
functionDeclaration,
|
|
awaitPromise: false,
|
|
arguments: args.map((arg) => LocalValue.getArgument(arg)),
|
|
target: { context: id }
|
|
};
|
|
const result = await this.scriptCallFunction(params).catch((err) => log17.warn(`Failed to identify frame context id: ${err.message}`));
|
|
if (!result) {
|
|
return [];
|
|
}
|
|
const { context: context2 } = parseScriptResult(params, result);
|
|
return {
|
|
/**
|
|
* the actual frame context we need to switch WebDriver Bidi commands to
|
|
*/
|
|
context: context2,
|
|
/**
|
|
* the element reference of the iframe so we can call `switchToFrame` to
|
|
* switch context for WebDriver Classic commands
|
|
*/
|
|
frameElement: { [ELEMENT_KEY8]: node.sharedId },
|
|
/**
|
|
* the context id in which the iframe was found
|
|
*/
|
|
parentContext: id,
|
|
/**
|
|
* an HTML representation of the iframe for a good error message in case
|
|
* we can't find the desired frame from this list
|
|
*/
|
|
html
|
|
};
|
|
}));
|
|
}))).flat(Infinity);
|
|
if (allFrames.length === 0) {
|
|
const urlFragment = typeof context === "string" ? context.split("/").pop() ?? "" : "";
|
|
const iframeFound = await this.execute(findIframeInShadowDOM, urlFragment);
|
|
if (iframeFound && typeof iframeFound === "object" && iframeFound[ELEMENT_KEY8]) {
|
|
const iframeElement = await this.$(iframeFound);
|
|
if (iframeElement) {
|
|
return this.switchFrame(iframeElement);
|
|
}
|
|
}
|
|
log17.warn(`Shadow DOM iframe with src containing "${urlFragment}" found, but could not be resolved into a WebdriverIO element.`);
|
|
}
|
|
let desiredFrame;
|
|
let desiredContext = newContextId;
|
|
const contextQueue = [];
|
|
log17.info(`Available frames to switch to: ${allFrames.length}, desired context to switch: ${desiredContext}`);
|
|
while (desiredContext !== currentContext) {
|
|
desiredFrame = allFrames.find(({ context: context2 }) => context2 === desiredContext);
|
|
if (!desiredFrame) {
|
|
break;
|
|
}
|
|
log17.info(
|
|
contextQueue.length === 0 ? `Found desired frame with element id ${desiredFrame.frameElement[ELEMENT_KEY8]}` : `to switch to desired frame, we need to switch to ${desiredFrame.context} first`
|
|
);
|
|
contextQueue.unshift(desiredFrame);
|
|
desiredContext = desiredFrame.parentContext;
|
|
}
|
|
if (contextQueue.length === 0) {
|
|
throw new Error(`Frame with url or context id "${context}" not found, available frames to switch to:
|
|
- ${allFrames.map(({ html }) => html).join("\n - ")}`);
|
|
}
|
|
for (const contextToSwitch of contextQueue) {
|
|
switchToFrameHelper(this, contextToSwitch.context);
|
|
await switchToFrame(this, contextToSwitch.frameElement);
|
|
}
|
|
sessionContext.setCurrentContext(newContextId);
|
|
return newContextId;
|
|
}
|
|
if (isPossiblyUnresolvedElement(context)) {
|
|
const element = await context.getElement();
|
|
await element.waitForExist({
|
|
timeoutMsg: `Can't switch to frame with selector ${element.selector} because it doesn't exist`
|
|
});
|
|
return switchToFrameUsingElement(this, element);
|
|
}
|
|
if (typeof context === "function") {
|
|
const foundContextId = await this.waitUntil(async () => {
|
|
const allContexts = await sessionContext.getFlatContextTree();
|
|
const allContextIds = Object.keys(allContexts);
|
|
for (const contextId of allContextIds) {
|
|
const functionDeclaration = new Function(`
|
|
return (${SCRIPT_PREFIX}${context.toString()}${SCRIPT_SUFFIX}).apply(this, arguments);
|
|
`).toString();
|
|
const params = {
|
|
functionDeclaration,
|
|
awaitPromise: false,
|
|
arguments: [],
|
|
target: { context: contextId }
|
|
};
|
|
const result = await this.scriptCallFunction(params).catch((err) => {
|
|
log17.warn(`switchFrame context callback threw error: ${err.message}`);
|
|
return void 0;
|
|
});
|
|
if (result && result.type === "success" && result.result.type === "boolean" && result.result.value) {
|
|
return contextId;
|
|
}
|
|
}
|
|
return false;
|
|
}, {
|
|
timeout: this.options.waitforTimeout,
|
|
interval: this.options.waitforInterval,
|
|
timeoutMsg: "Could not find the desired frame within the timeout"
|
|
});
|
|
await this.switchFrame(null);
|
|
await this.switchFrame(foundContextId);
|
|
return foundContextId;
|
|
}
|
|
throw new Error(
|
|
`Invalid type for context parameter: ${typeof context}, expected one of number, string or null. Check out our docs: https://webdriver.io/docs/api/browser/switchFrame.html`
|
|
);
|
|
}
|
|
function switchToFrameHelper(browser, context) {
|
|
const sessionContext = getContextManager(browser);
|
|
sessionContext.setCurrentContext(context);
|
|
}
|
|
async function switchToFrameUsingElement(browser, element) {
|
|
const frame = await browser.execute(
|
|
(iframe) => iframe.contentWindow,
|
|
element
|
|
);
|
|
switchToFrameHelper(browser, frame.context);
|
|
const elementId = element[ELEMENT_KEY8];
|
|
await switchToFrame(browser, { [ELEMENT_KEY8]: elementId });
|
|
return frame.context;
|
|
}
|
|
function switchToFrame(browser, frame) {
|
|
toggleDisableDeprecationWarning();
|
|
return browser.switchToFrame(frame).finally(toggleDisableDeprecationWarning);
|
|
}
|
|
function toggleDisableDeprecationWarning() {
|
|
if (typeof process !== "undefined" && process.env) {
|
|
process.env.DISABLE_WEBDRIVERIO_DEPRECATION_WARNINGS = process.env.DISABLE_WEBDRIVERIO_DEPRECATION_WARNINGS ? void 0 : "true";
|
|
}
|
|
}
|
|
|
|
// src/commands/browser/throttle.ts
|
|
import logger18 from "@wdio/logger";
|
|
import { getBrowserObject as getBrowserObject9 } from "@wdio/utils";
|
|
var log18 = logger18("webdriverio:throttle");
|
|
async function throttle(params) {
|
|
log18.warn('Command "throttle" is deprecated and will be removed with the next major version release! Use `throttleNetwork` instead.');
|
|
const browser = getBrowserObject9(this);
|
|
await browser.throttleNetwork(params);
|
|
}
|
|
|
|
// src/commands/browser/throttleCPU.ts
|
|
async function throttleCPU(factor) {
|
|
if (typeof factor !== "number") {
|
|
throw new Error('Invalid factor for "throttleCPU". Expected it to be a number (int)');
|
|
}
|
|
const failedConnectionMessage = "No Puppeteer connection could be established which is required to use this command";
|
|
await this.getPuppeteer();
|
|
if (!this.puppeteer) {
|
|
throw new Error(failedConnectionMessage);
|
|
}
|
|
const pages = await this.puppeteer.pages();
|
|
if (!pages.length) {
|
|
throw new Error(failedConnectionMessage);
|
|
}
|
|
const client = await pages[0].target().createCDPSession();
|
|
await client.send("Emulation.setCPUThrottlingRate", { rate: factor });
|
|
}
|
|
|
|
// src/commands/browser/throttleNetwork.ts
|
|
import { getBrowserObject as getBrowserObject10 } from "@wdio/utils";
|
|
var NETWORK_PRESETS = {
|
|
"offline": {
|
|
offline: true,
|
|
downloadThroughput: 0,
|
|
uploadThroughput: 0,
|
|
latency: 1
|
|
},
|
|
"GPRS": {
|
|
offline: false,
|
|
downloadThroughput: 50 * 1024 / 8,
|
|
uploadThroughput: 20 * 1024 / 8,
|
|
latency: 500
|
|
},
|
|
"Regular2G": {
|
|
offline: false,
|
|
downloadThroughput: 250 * 1024 / 8,
|
|
uploadThroughput: 50 * 1024 / 8,
|
|
latency: 300
|
|
},
|
|
"Good2G": {
|
|
offline: false,
|
|
downloadThroughput: 450 * 1024 / 8,
|
|
uploadThroughput: 150 * 1024 / 8,
|
|
latency: 150
|
|
},
|
|
"Regular3G": {
|
|
offline: false,
|
|
downloadThroughput: 750 * 1024 / 8,
|
|
uploadThroughput: 250 * 1024 / 8,
|
|
latency: 100
|
|
},
|
|
"Good3G": {
|
|
offline: false,
|
|
downloadThroughput: 1.5 * 1024 * 1024 / 8,
|
|
uploadThroughput: 750 * 1024 / 8,
|
|
latency: 40
|
|
},
|
|
"Regular4G": {
|
|
offline: false,
|
|
downloadThroughput: 4 * 1024 * 1024 / 8,
|
|
uploadThroughput: 3 * 1024 * 1024 / 8,
|
|
latency: 20
|
|
},
|
|
"DSL": {
|
|
offline: false,
|
|
downloadThroughput: 2 * 1024 * 1024 / 8,
|
|
uploadThroughput: 1 * 1024 * 1024 / 8,
|
|
latency: 5
|
|
},
|
|
"WiFi": {
|
|
offline: false,
|
|
downloadThroughput: 30 * 1024 * 1024 / 8,
|
|
uploadThroughput: 15 * 1024 * 1024 / 8,
|
|
latency: 2
|
|
},
|
|
"online": {
|
|
offline: false,
|
|
latency: 0,
|
|
downloadThroughput: -1,
|
|
uploadThroughput: -1
|
|
}
|
|
};
|
|
var NETWORK_PRESET_TYPES = Object.keys(NETWORK_PRESETS);
|
|
async function throttleNetwork(params) {
|
|
if (
|
|
/**
|
|
* check string parameter
|
|
*/
|
|
(typeof params !== "string" || !NETWORK_PRESET_TYPES.includes(params)) && /**
|
|
* check object parameter
|
|
*/
|
|
typeof params !== "object"
|
|
) {
|
|
throw new Error(`Invalid parameter for "throttleNetwork". Expected it to be typeof object or one of the following values: ${NETWORK_PRESET_TYPES.join(", ")} but found "${params}"`);
|
|
}
|
|
if (this.isSauce) {
|
|
const browser = getBrowserObject10(this);
|
|
await browser.sauceThrottleNetwork(params);
|
|
return;
|
|
}
|
|
const failedConnectionMessage = "No Puppeteer connection could be established which is required to use this command";
|
|
await this.getPuppeteer();
|
|
if (!this.puppeteer) {
|
|
throw new Error(failedConnectionMessage);
|
|
}
|
|
const pages = await this.puppeteer.pages();
|
|
if (!pages.length) {
|
|
throw new Error(failedConnectionMessage);
|
|
}
|
|
const client = await pages[0].target().createCDPSession();
|
|
await client.send(
|
|
"Network.emulateNetworkConditions",
|
|
typeof params === "string" ? NETWORK_PRESETS[params] : params
|
|
);
|
|
return;
|
|
}
|
|
|
|
// src/commands/browser/touchAction.ts
|
|
function touchAction2(actions2) {
|
|
return touchAction.call(this, actions2);
|
|
}
|
|
|
|
// src/commands/browser/uploadFile.ts
|
|
async function uploadFile2(localPath) {
|
|
return environment.value.uploadFile.call(this, localPath);
|
|
}
|
|
|
|
// src/commands/browser/url.ts
|
|
var DEFAULT_NETWORK_IDLE_TIMEOUT = 5e3;
|
|
var DEFAULT_WAIT_STATE = "complete";
|
|
async function url(path8, options = {}) {
|
|
if (typeof path8 !== "string") {
|
|
throw new Error('Parameter for "url" command needs to be type of string');
|
|
}
|
|
if (typeof this.options.baseUrl === "string" && this.options.baseUrl) {
|
|
path8 = new URL(path8, this.options.baseUrl).href;
|
|
}
|
|
if (this.isBidi && path8.startsWith("http")) {
|
|
let resetPreloadScript;
|
|
const contextManager = getContextManager(this);
|
|
const context = await contextManager.getCurrentContext();
|
|
if (options.onBeforeLoad) {
|
|
if (typeof options.onBeforeLoad !== "function") {
|
|
throw new Error(`Option "onBeforeLoad" must be a function, but received: ${typeof options.onBeforeLoad}`);
|
|
}
|
|
resetPreloadScript = await this.addInitScript(options.onBeforeLoad);
|
|
}
|
|
if (options.auth) {
|
|
options.headers = {
|
|
...options.headers || {},
|
|
Authorization: `Basic ${btoa(`${options.auth.user}:${options.auth.pass}`)}`
|
|
};
|
|
}
|
|
let mock2;
|
|
if (options.headers) {
|
|
mock2 = await this.mock(path8);
|
|
mock2.requestOnce({ headers: options.headers });
|
|
}
|
|
const classicPageLoadStrategy = this.capabilities.pageLoadStrategy === "none" ? "none" : this.capabilities.pageLoadStrategy === "normal" ? "complete" : this.capabilities.pageLoadStrategy === "eager" ? "interactive" : void 0;
|
|
const wait = options.wait === "networkIdle" ? "complete" : options.wait || classicPageLoadStrategy || DEFAULT_WAIT_STATE;
|
|
const navigation = await this.browsingContextNavigate({
|
|
context,
|
|
url: path8,
|
|
wait
|
|
}).catch((err) => {
|
|
if (
|
|
// Chrome error message
|
|
err.message.includes("navigation canceled by concurrent navigation") || // Firefox error message
|
|
err.message.includes("failed with error: unknown error") || // Race condition where the context is destroyed before navigation
|
|
err.message.includes("no such frame")
|
|
) {
|
|
return this.navigateTo(validateUrl(path8));
|
|
}
|
|
throw err;
|
|
});
|
|
if (mock2) {
|
|
await mock2.restore();
|
|
}
|
|
const network = getNetworkManager(this);
|
|
if (options.wait === "networkIdle") {
|
|
const timeout = options.timeout || DEFAULT_NETWORK_IDLE_TIMEOUT;
|
|
await this.waitUntil(async () => {
|
|
return network.getPendingRequests(context).length === 0;
|
|
}, {
|
|
timeout,
|
|
timeoutMsg: `Navigation to '${path8}' timed out after ${timeout}ms with ${network.getPendingRequests(context).length} (${network.getPendingRequests(context).map((r) => r.url).join(", ")}) pending requests`
|
|
});
|
|
}
|
|
if (resetPreloadScript) {
|
|
await resetPreloadScript.remove();
|
|
}
|
|
if (!navigation) {
|
|
return;
|
|
}
|
|
const request = await this.waitUntil(
|
|
() => network.getRequestResponseData(navigation.navigation),
|
|
/**
|
|
* set a short interval to immediately return once the first request payload comes in
|
|
*/
|
|
{
|
|
interval: 1,
|
|
timeoutMsg: `Navigation to '${path8}' timed out as no request payload was received`
|
|
}
|
|
);
|
|
return request;
|
|
}
|
|
if (Object.keys(options).length > 0) {
|
|
throw new Error("Setting url options is only supported when automating browser using WebDriver Bidi protocol");
|
|
}
|
|
await this.navigateTo(validateUrl(path8));
|
|
}
|
|
|
|
// src/commands/browser/waitUntil.ts
|
|
import { getBrowserObject as getBrowserObject11 } from "@wdio/utils";
|
|
function waitUntil(condition, {
|
|
timeout = this.options.waitforTimeout,
|
|
interval = this.options.waitforInterval,
|
|
timeoutMsg
|
|
} = {}) {
|
|
if (typeof condition !== "function") {
|
|
throw new Error("Condition is not a function");
|
|
}
|
|
if (typeof timeout !== "number") {
|
|
timeout = this.options.waitforTimeout;
|
|
}
|
|
if (typeof interval !== "number") {
|
|
interval = this.options.waitforInterval;
|
|
}
|
|
const browser = getBrowserObject11(this);
|
|
const abort = new AbortController();
|
|
const abortOnSessionEnd = (result) => {
|
|
if (result.command === "deleteSession") {
|
|
abort.abort();
|
|
}
|
|
};
|
|
browser.on("result", abortOnSessionEnd);
|
|
const fn = condition.bind(this);
|
|
const timer = new Timer_default(interval, timeout, fn, true, abort.signal);
|
|
return timer.catch((e) => {
|
|
if (e.message === "timeout") {
|
|
if (typeof timeoutMsg === "string") {
|
|
throw new Error(timeoutMsg);
|
|
}
|
|
throw new Error(`waitUntil condition timed out after ${timeout}ms`);
|
|
}
|
|
const err = new Error(`waitUntil condition failed with the following reason: ${e && e.message || e}`);
|
|
const origStack = e.stack;
|
|
if (!origStack || !err.stack) {
|
|
throw err;
|
|
}
|
|
const [errMsg, ...waitUntilErrorStackLines] = err.stack.split("\n");
|
|
err.stack = [
|
|
errMsg,
|
|
...origStack.split("\n").slice(1),
|
|
" ---",
|
|
...waitUntilErrorStackLines
|
|
].filter((errorLine) => !errorLine.includes("/node_modules/webdriverio/") && !errorLine.includes("/node_modules/@wdio/")).join("\n");
|
|
throw err;
|
|
}).finally(() => {
|
|
browser.off("result", abortOnSessionEnd);
|
|
});
|
|
}
|
|
|
|
// src/commands/mobile/swipe.ts
|
|
import logger19 from "@wdio/logger";
|
|
|
|
// src/types.ts
|
|
var MobileScrollDirection = /* @__PURE__ */ ((MobileScrollDirection2) => {
|
|
MobileScrollDirection2["Down"] = "down";
|
|
MobileScrollDirection2["Up"] = "up";
|
|
MobileScrollDirection2["Left"] = "left";
|
|
MobileScrollDirection2["Right"] = "right";
|
|
return MobileScrollDirection2;
|
|
})(MobileScrollDirection || {});
|
|
|
|
// src/commands/mobile/swipe.ts
|
|
var log19 = logger19("webdriverio");
|
|
var SWIPE_DEFAULTS = {
|
|
DIRECTION: "up" /* Up */,
|
|
DURATION: 1500,
|
|
PERCENT: 0.95
|
|
};
|
|
async function swipe(options) {
|
|
const browser = this;
|
|
if (!browser.isNativeContext) {
|
|
throw new Error("The swipe command is only available for mobile platforms in the NATIVE context.");
|
|
}
|
|
let { scrollableElement, from, to } = options || {};
|
|
if (scrollableElement && (from || to)) {
|
|
log19.warn("`scrollableElement` is provided, so `from` and `to` will be ignored.");
|
|
}
|
|
if (!from || !to) {
|
|
scrollableElement = scrollableElement || await getScrollableElement(browser);
|
|
({ from, to } = await calculateFromTo({
|
|
browser,
|
|
direction: options?.direction || SWIPE_DEFAULTS.DIRECTION,
|
|
percentage: options?.percent,
|
|
scrollableElement
|
|
}));
|
|
}
|
|
return w3cSwipe({ browser, duration: options?.duration || SWIPE_DEFAULTS.DURATION, from, to });
|
|
}
|
|
async function calculateFromTo({
|
|
browser,
|
|
direction,
|
|
percentage,
|
|
scrollableElement
|
|
}) {
|
|
let swipePercentage = SWIPE_DEFAULTS.PERCENT;
|
|
if (percentage !== void 0) {
|
|
if (isNaN(percentage)) {
|
|
log19.warn("The percentage to swipe should be a number.");
|
|
} else if (percentage < 0 || percentage > 1) {
|
|
log19.warn("The percentage to swipe should be a number between 0 and 1.");
|
|
} else {
|
|
swipePercentage = percentage;
|
|
}
|
|
}
|
|
const { x, y, width, height } = await browser.getElementRect(await scrollableElement?.elementId);
|
|
const verticalOffset = height - height * swipePercentage;
|
|
const horizontalOffset = width - width * swipePercentage;
|
|
const scrollRectangles = {
|
|
top: { x: Math.round(x + width / 2), y: Math.round(y + verticalOffset / 2) },
|
|
right: { x: Math.round(x + width - horizontalOffset / 2), y: Math.round(y + height / 2) },
|
|
bottom: { x: Math.round(x + width / 2), y: Math.round(y + height - verticalOffset / 2) },
|
|
left: { x: Math.round(x + horizontalOffset / 2), y: Math.round(y + height / 2) }
|
|
};
|
|
let from;
|
|
let to;
|
|
switch (direction) {
|
|
case "down" /* Down */:
|
|
from = scrollRectangles.top;
|
|
to = scrollRectangles.bottom;
|
|
break;
|
|
case "left" /* Left */:
|
|
from = scrollRectangles.right;
|
|
to = scrollRectangles.left;
|
|
break;
|
|
case "right" /* Right */:
|
|
from = scrollRectangles.left;
|
|
to = scrollRectangles.right;
|
|
break;
|
|
case "up" /* Up */:
|
|
from = scrollRectangles.bottom;
|
|
to = scrollRectangles.top;
|
|
break;
|
|
default:
|
|
throw new Error(`Unknown direction: ${direction}`);
|
|
}
|
|
return { from, to };
|
|
}
|
|
async function getScrollableElement(browser) {
|
|
const defaultAndroidSelector = "//android.widget.ScrollView";
|
|
const defaultIosSelector = '-ios predicate string:type == "XCUIElementTypeApplication"';
|
|
const selector = browser.isIOS ? (
|
|
// For iOS, we need to find the application element, if we can't find it, we should throw an error
|
|
defaultIosSelector
|
|
) : (
|
|
// There is always a scrollview for Android or, if this fails we should throw an error
|
|
defaultAndroidSelector
|
|
);
|
|
const scrollableElements = await browser.$$(
|
|
selector
|
|
);
|
|
if (scrollableElements.length > 0) {
|
|
return scrollableElements[0];
|
|
}
|
|
throw new Error(
|
|
`Default scrollable element '${browser.isIOS ? defaultIosSelector : defaultAndroidSelector}' was not found. Our advice is to provide a scrollable element like this:
|
|
|
|
await browser.swipe({ scrollableElement: $('#scrollable') });
|
|
|
|
`
|
|
);
|
|
}
|
|
async function w3cSwipe({ browser, duration, from, to }) {
|
|
await browser.action("pointer", {
|
|
parameters: { pointerType: browser.isMobile ? "touch" : "mouse" }
|
|
}).move(from.x, from.y).down().pause(10).move({ duration, x: to.x, y: to.y }).up().perform();
|
|
return browser.pause(500);
|
|
}
|
|
|
|
// src/commands/mobile/tap.ts
|
|
import logger20 from "@wdio/logger";
|
|
import { getBrowserObject as getBrowserObject12 } from "@wdio/utils";
|
|
var log20 = logger20("webdriver");
|
|
async function tap(options) {
|
|
const isElement2 = this.selector !== void 0;
|
|
const element = isElement2 ? this : null;
|
|
const browser = isElement2 ? getBrowserObject12(this) : this;
|
|
if (!browser.isMobile) {
|
|
throw new Error("The tap command is only available for mobile platforms.");
|
|
}
|
|
validateTapOptions(options);
|
|
if (element) {
|
|
return await elementTap(browser, element, options);
|
|
}
|
|
if (!options || options.x === void 0 || options.y === void 0) {
|
|
throw new Error("The tap command requires x and y coordinates to be set for screen taps.");
|
|
}
|
|
return await screenTap(browser, options);
|
|
}
|
|
function validateTapOptions(options) {
|
|
if (options) {
|
|
if (typeof options !== "object" || Array.isArray(options)) {
|
|
throw new TypeError("Options must be an object.");
|
|
}
|
|
const { x, y, ...otherArgs } = options;
|
|
if (x === void 0 !== (y === void 0)) {
|
|
throw new TypeError(`If ${x !== void 0 ? "x" : "y"} is set, then ${x !== void 0 ? "y" : "x"} must also be set.`);
|
|
}
|
|
if (x !== void 0 && y !== void 0 && Object.keys(otherArgs).length > 0) {
|
|
throw new TypeError(`If x and y are provided, no other arguments are allowed. Found: ${Object.keys(otherArgs).join(", ")}`);
|
|
}
|
|
const invalidCoordinates = [];
|
|
if (x !== void 0 && x < 0) {
|
|
invalidCoordinates.push("x");
|
|
}
|
|
if (y !== void 0 && y < 0) {
|
|
invalidCoordinates.push("y");
|
|
}
|
|
if (invalidCoordinates.length > 0) {
|
|
throw new TypeError(`The ${invalidCoordinates.join(" and ")} value${invalidCoordinates.length > 1 ? "s" : ""} must be positive.`);
|
|
}
|
|
}
|
|
}
|
|
async function elementTap(browser, element, options) {
|
|
if (browser.isNativeContext) {
|
|
return await nativeTap(element, browser, options);
|
|
}
|
|
if (options) {
|
|
log20.warn("The options object is not supported in Web environments and will be ignored.");
|
|
}
|
|
return await webTap(element);
|
|
}
|
|
async function webTap(element) {
|
|
return element.click();
|
|
}
|
|
async function executeNativeTap(browser, options) {
|
|
return await browser.execute(
|
|
`mobile: ${browser.isIOS ? "tap" : "clickGesture"}`,
|
|
{ ...browser.isIOS ? { x: 0, y: 0 } : {}, ...options }
|
|
);
|
|
}
|
|
async function nativeTap(element, browser, options = {}) {
|
|
try {
|
|
if (!element.elementId) {
|
|
throw new Error("no such element");
|
|
}
|
|
return await executeNativeTap(browser, { elementId: element.elementId });
|
|
} catch (error) {
|
|
let err = error;
|
|
if (typeof error === "string") {
|
|
err = new Error(error);
|
|
}
|
|
if (!err.message.includes("no such element")) {
|
|
throw err;
|
|
}
|
|
const scrollIntoViewOptions = Object.fromEntries(
|
|
Object.entries({
|
|
direction: options?.direction,
|
|
maxScrolls: options?.maxScrolls,
|
|
scrollableElement: options?.scrollableElement
|
|
}).filter(([_, value]) => value !== void 0)
|
|
);
|
|
try {
|
|
await element.scrollIntoView(scrollIntoViewOptions);
|
|
return await executeNativeTap(browser, { elementId: element.elementId });
|
|
} catch (scrollError) {
|
|
let err2 = scrollError;
|
|
if (typeof scrollError === "string") {
|
|
err2 = new Error(scrollError);
|
|
}
|
|
if (err2.message.includes("Element not found within scroll limit of")) {
|
|
throw new Error(`Element not found within the automatic 'tap' scroll limit of ${scrollIntoViewOptions?.maxScrolls || "10"} scrolls by scrolling "${scrollIntoViewOptions?.direction || "down"}". The 'tap' methods will automatically scroll if it can't find the element. It might be that 'direction|maxScrolls|scrollableElement' are not correct. You can change change them like this:
|
|
|
|
await elem.tap({
|
|
direction: 'left' // possible options are: 'up|down|left|right'
|
|
maxScrolls: 15,
|
|
scrollableElement: $('#scrollable'),
|
|
});
|
|
|
|
`);
|
|
} else if (err2.message.includes("Default scrollable element")) {
|
|
const match = err2.message.match(/Default scrollable element '(.*?)' was not found/);
|
|
const scrollableElement = match?.[1] || "unknown-scrollable-element";
|
|
throw new Error(`The 'tap' method tried to automatically scroll to the element but couldn't find the default scrollable element. '${scrollableElement}' If needed you can provide a custom scrollable element, together with the 'direction' and the 'maxScrolls' like this:
|
|
|
|
await elem.tap({
|
|
scrollableElement: $('#scrollable'),
|
|
});
|
|
|
|
`);
|
|
}
|
|
throw err2;
|
|
}
|
|
}
|
|
}
|
|
async function screenTap(browser, options) {
|
|
const { x, y } = options;
|
|
if (browser.isNativeContext) {
|
|
return await executeNativeTap(browser, options);
|
|
}
|
|
return await browser.action(
|
|
"pointer",
|
|
{
|
|
parameters: { pointerType: "touch" }
|
|
}
|
|
).move({ x, y }).down({ button: 0 }).pause(10).up({ button: 0 }).perform();
|
|
}
|
|
|
|
// src/commands/mobile/getContext.ts
|
|
import logger21 from "@wdio/logger";
|
|
var log21 = logger21("webdriver");
|
|
async function getContext(options) {
|
|
const browser = this;
|
|
if (!browser.isMobile) {
|
|
throw new Error("The `getContext` command is only available for mobile platforms.");
|
|
}
|
|
const currentAppiumContext = await browser.getAppiumContext();
|
|
if (!options || !options?.returnDetailedContext || currentAppiumContext === "NATIVE_APP") {
|
|
return currentAppiumContext;
|
|
}
|
|
delete options.returnDetailedContext;
|
|
return getDetailedContext(browser, currentAppiumContext, options);
|
|
}
|
|
async function getDetailedContext(browser, currentAppiumContext, options) {
|
|
const detailedContexts = await browser.getContexts({
|
|
...options,
|
|
// Defaults
|
|
returnDetailedContexts: true,
|
|
// We want to get back the detailed context information
|
|
isAndroidWebviewVisible: true,
|
|
// We only want to get back the visible webviews
|
|
filterByCurrentAndroidApp: true,
|
|
// We only want to get back the webviews that are attached to the current app
|
|
returnAndroidDescriptionData: false
|
|
// We don't want to get back the Android Webview description data
|
|
});
|
|
const parsedContexts = detailedContexts.filter((context) => context.id === currentAppiumContext);
|
|
if (parsedContexts.length > 1) {
|
|
log21.warn(`We found more than 1 detailed context for the current context '${currentAppiumContext}'. We will return the first context.`);
|
|
return parsedContexts[0];
|
|
} else if (parsedContexts.length === 0) {
|
|
log21.warn(`We did not get back any detailed context for the current context '${currentAppiumContext}'. We will return the current context as a string.`);
|
|
return currentAppiumContext;
|
|
}
|
|
return parsedContexts[0];
|
|
}
|
|
|
|
// src/commands/mobile/getContexts.ts
|
|
import logger22 from "@wdio/logger";
|
|
var log22 = logger22("webdriver");
|
|
async function getContexts(options) {
|
|
const browser = this;
|
|
if (!browser.isMobile) {
|
|
throw new Error("The `getContexts` command is only available for mobile platforms.");
|
|
}
|
|
if (!options || !options.returnDetailedContexts) {
|
|
log22.info("The standard Appium `contexts` method is used. If you want to get more detailed data, you can set `returnDetailedContexts` to `true`.");
|
|
return browser.getAppiumContexts();
|
|
}
|
|
const defaultOptions = {
|
|
androidWebviewConnectionRetryTime: 500,
|
|
androidWebviewConnectTimeout: 5e3,
|
|
filterByCurrentAndroidApp: false,
|
|
isAndroidWebviewVisible: true,
|
|
returnAndroidDescriptionData: false
|
|
};
|
|
return getCurrentContexts({ browser, ...{ ...defaultOptions, ...options } });
|
|
}
|
|
var CHROME_PACKAGE_NAME = "com.android.chrome";
|
|
async function parsedAndroidContexts({
|
|
contexts,
|
|
filterByCurrentAndroidApp,
|
|
isAttachedAndVisible,
|
|
packageName
|
|
}) {
|
|
const currentWebviewName = `WEBVIEW_${packageName}`;
|
|
let parsedContexts = contexts;
|
|
if (filterByCurrentAndroidApp) {
|
|
parsedContexts = contexts.filter((context) => context.webviewName === currentWebviewName);
|
|
}
|
|
const result = [{ id: "NATIVE_APP" }];
|
|
if (!parsedContexts || parsedContexts.length < 1) {
|
|
return result;
|
|
}
|
|
parsedContexts.forEach(
|
|
(context) => context.pages?.filter((page) => {
|
|
if (packageName === CHROME_PACKAGE_NAME) {
|
|
return true;
|
|
}
|
|
if (page.type === "page" && page.description) {
|
|
let descriptionObj;
|
|
try {
|
|
descriptionObj = JSON.parse(page.description);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
return isAttachedAndVisible ? descriptionObj.attached === true && descriptionObj.visible === true : true;
|
|
}
|
|
return !isAttachedAndVisible;
|
|
}).forEach((page) => {
|
|
const {
|
|
attached = false,
|
|
empty = false,
|
|
height = 0,
|
|
never_attached: neverAttached = false,
|
|
screenX = 0,
|
|
screenY = 0,
|
|
visible = false,
|
|
width = 0
|
|
} = JSON.parse(page.description || "{}");
|
|
const pageData = {
|
|
androidWebviewData: {
|
|
attached,
|
|
empty,
|
|
height,
|
|
neverAttached,
|
|
screenX,
|
|
screenY,
|
|
visible,
|
|
width
|
|
},
|
|
id: context.webviewName,
|
|
title: page.title,
|
|
url: page.url,
|
|
packageName: context.info["Android-Package"],
|
|
webviewPageId: page.id
|
|
};
|
|
result.push(pageData);
|
|
})
|
|
);
|
|
return result;
|
|
}
|
|
async function getCurrentContexts({
|
|
browser,
|
|
androidWebviewConnectionRetryTime,
|
|
androidWebviewConnectTimeout,
|
|
filterByCurrentAndroidApp,
|
|
isAndroidWebviewVisible,
|
|
returnAndroidDescriptionData,
|
|
waitForWebviewMs
|
|
}) {
|
|
const contexts = await (waitForWebviewMs !== void 0 ? browser.execute("mobile: getContexts", { waitForWebviewMs }) : browser.execute("mobile: getContexts"));
|
|
if (browser.isIOS) {
|
|
return contexts;
|
|
}
|
|
const packageName = await browser.getCurrentPackage();
|
|
const startTime = Date.now();
|
|
const retryInterval = androidWebviewConnectionRetryTime;
|
|
let isPackageNameMissing = false;
|
|
while (Date.now() - startTime < androidWebviewConnectTimeout) {
|
|
const parsedContexts = await parsedAndroidContexts({
|
|
contexts,
|
|
filterByCurrentAndroidApp,
|
|
isAttachedAndVisible: isAndroidWebviewVisible,
|
|
packageName
|
|
});
|
|
const matchingContexts = parsedContexts.filter((context) => context.packageName === packageName);
|
|
isPackageNameMissing = matchingContexts.length === 0;
|
|
const hasAndroidWebviewData = matchingContexts.some((context) => Boolean(context.androidWebviewData));
|
|
const isAndroidWebviewDataMissing = matchingContexts.length > 0 && !hasAndroidWebviewData;
|
|
const hasNonEmptyAndroidWebviewData = matchingContexts.some((context) => context.androidWebviewData && !context.androidWebviewData.empty);
|
|
const isAndroidWebviewDataEmpty = matchingContexts.length > 0 && hasAndroidWebviewData && !hasNonEmptyAndroidWebviewData;
|
|
if (packageName === CHROME_PACKAGE_NAME) {
|
|
return parsedContexts;
|
|
}
|
|
if (!isPackageNameMissing && !isAndroidWebviewDataMissing && !isAndroidWebviewDataEmpty) {
|
|
if (!returnAndroidDescriptionData) {
|
|
parsedContexts.forEach((context) => {
|
|
if ("androidWebviewData" in context) {
|
|
delete context.androidWebviewData;
|
|
}
|
|
});
|
|
}
|
|
return parsedContexts;
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, retryInterval));
|
|
}
|
|
throw new Error(
|
|
`The packageName '${packageName}' ${isPackageNameMissing ? "could not be found!" : "matches, but no webview with pages was loaded in this response: " + JSON.stringify(contexts) + "'"}`
|
|
);
|
|
}
|
|
|
|
// src/commands/mobile/switchContext.ts
|
|
import logger23 from "@wdio/logger";
|
|
var log23 = logger23("webdriver");
|
|
async function switchContext(options) {
|
|
const browser = this;
|
|
if (!browser.isMobile) {
|
|
throw new Error("The `switchContext` command is only available for mobile platforms.");
|
|
}
|
|
if (!options) {
|
|
throw new Error("You need to provide at least a context name to switch to. See https://webdriver.io/docs/api/mobile/switchContext for more information.");
|
|
}
|
|
if (typeof options === "string") {
|
|
log23.info("The standard Appium `context`-method is used. If you want to switch to a webview with a specific title or url, please provide an object with the `title` or `url` property. See https://webdriver.io/docs/api/mobile/switchContext for more information.");
|
|
return browser.switchAppiumContext(options);
|
|
}
|
|
if (!options.title && !options.url) {
|
|
throw new Error("You need to provide at least a `title` or `url` property to use full potential of the `switchContext` command. See https://webdriver.io/docs/api/mobile/switchContext for more information.");
|
|
}
|
|
return switchToContext({ browser, options });
|
|
}
|
|
async function switchToContext({ browser, options }) {
|
|
const getContextsOptions = {
|
|
returnDetailedContexts: true,
|
|
filterByCurrentAndroidApp: false,
|
|
isAndroidWebviewVisible: false,
|
|
returnAndroidDescriptionData: true,
|
|
...options?.androidWebviewConnectionRetryTime && { androidWebviewConnectionRetryTime: options.androidWebviewConnectionRetryTime },
|
|
...options?.androidWebviewConnectTimeout && { androidWebviewConnectTimeout: options.androidWebviewConnectTimeout }
|
|
};
|
|
const contexts = await browser.getContexts(getContextsOptions);
|
|
let identifier;
|
|
if (options.appIdentifier) {
|
|
identifier = options.appIdentifier;
|
|
} else {
|
|
identifier = browser.isIOS ? (await browser.execute("mobile: activeAppInfo"))?.bundleId : await browser.getCurrentPackage();
|
|
}
|
|
const { matchingContext, reasons } = findMatchingContext({ browser, contexts, identifier, ...options?.title && { title: options.title }, ...options?.url && { url: options.url } });
|
|
if (!matchingContext) {
|
|
throw new Error(reasons.join("\n"));
|
|
}
|
|
log23.info("WebdriverIO found a matching context:", JSON.stringify(matchingContext, null, 2));
|
|
if (!browser.isIOS) {
|
|
const webviewName = `WEBVIEW_${identifier}`;
|
|
await browser.switchAppiumContext(webviewName);
|
|
}
|
|
const switchFunction = browser.isIOS ? browser.switchAppiumContext.bind(browser) : browser.switchToWindow.bind(browser);
|
|
const matchingContextId = browser.isIOS ? matchingContext.id : matchingContext.webviewPageId;
|
|
return switchFunction(matchingContextId);
|
|
}
|
|
function findMatchingContext({
|
|
browser: { isIOS },
|
|
contexts,
|
|
identifier,
|
|
title,
|
|
url: url2
|
|
}) {
|
|
const reasons = [];
|
|
reasons.push(`We parsed a total of ${contexts.length} Webviews but did not find a matching context. The reasons are:`);
|
|
const matchingContext = contexts.find((context, index) => {
|
|
reasons.push(`- Webview ${index + 1}: '${context.id}'`);
|
|
if (context.id === "NATIVE_APP") {
|
|
reasons.push(" - Skipped context because it is NATIVE_APP");
|
|
return false;
|
|
}
|
|
const idMatch = isIOS ? context.bundleId === identifier : context.packageName === identifier;
|
|
const titleMatches = title ? title instanceof RegExp ? title.test(context.title || "") : context.title?.includes(title) : true;
|
|
const urlMatches = url2 ? url2 instanceof RegExp ? url2.test(context.url || "") : context.url?.includes(url2) : true;
|
|
const additionalAndroidChecks = isIOS ? true : context.androidWebviewData?.attached && context.androidWebviewData?.visible;
|
|
if (!idMatch) {
|
|
reasons.push(` - App ${isIOS ? "bundleId" : "packageName"} '${identifier}' did not match: '${context.id}'`);
|
|
}
|
|
if (!titleMatches) {
|
|
reasons.push(` - Title '${title}' did not match: '${context.title}'`);
|
|
}
|
|
if (!urlMatches) {
|
|
reasons.push(` - URL '${url2}' did not match: '${context.url}'`);
|
|
}
|
|
if (!additionalAndroidChecks) {
|
|
reasons.push(" - Additional Android checks failed");
|
|
}
|
|
return idMatch && titleMatches && urlMatches && additionalAndroidChecks;
|
|
});
|
|
return { matchingContext, reasons };
|
|
}
|
|
|
|
// src/commands/mobile/relaunchActiveApp.ts
|
|
async function relaunchActiveApp() {
|
|
const browser = this;
|
|
if (!browser.isMobile) {
|
|
throw new Error("The `relaunchActiveApp` command is only available for mobile platforms.");
|
|
}
|
|
if (browser.isIOS) {
|
|
const { bundleId, processArguments: { args, env } } = await browser.execute("mobile: activeAppInfo");
|
|
const iOSLaunchOptions = {
|
|
bundleId,
|
|
...args.length > 0 && { arguments: args },
|
|
...Object.keys(env).length > 0 && { environment: env }
|
|
};
|
|
await browser.execute("mobile: terminateApp", { bundleId });
|
|
return browser.execute("mobile:launchApp", iOSLaunchOptions);
|
|
}
|
|
const packageName = await browser.getCurrentPackage();
|
|
await browser.execute("mobile: terminateApp", { appId: packageName });
|
|
return browser.execute("mobile: activateApp", { appId: packageName });
|
|
}
|
|
|
|
// src/commands/mobile/deepLink.ts
|
|
async function deepLink(link, appIdentifier) {
|
|
const browser = this;
|
|
if (!browser.isMobile) {
|
|
throw new Error("The `deepLink` command is only available for mobile platforms.");
|
|
}
|
|
if (!isDeepLinkUrl(link)) {
|
|
throw new Error(`The provided link is not a valid deep link URL.${browser.isIOS ? " If your url is a `universal deep link` then use the `url` command instead." : ""}`);
|
|
}
|
|
if (!appIdentifier) {
|
|
const mobileOS = browser.isIOS ? "iOS" : "Android";
|
|
const identifierValue = browser.isIOS ? "bundleId" : "package";
|
|
throw new Error(`When using a deep link URL for ${mobileOS}, you need to provide the \`${identifierValue}\` of the app that the deep link should open.`);
|
|
}
|
|
return browser.execute("mobile:deepLink", {
|
|
url: link,
|
|
[browser.isIOS ? "bundleId" : "package"]: appIdentifier
|
|
});
|
|
}
|
|
function isDeepLinkUrl(link) {
|
|
const deepLinkRegex = /^(?!https?:\/\/)[a-zA-Z][\w+\-.]*:\/\//;
|
|
return deepLinkRegex.test(link);
|
|
}
|
|
|
|
// src/commands/element.ts
|
|
var element_exports = {};
|
|
__export(element_exports, {
|
|
$: () => $2,
|
|
$$: () => $$2,
|
|
addValue: () => addValue,
|
|
clearValue: () => clearValue,
|
|
click: () => click,
|
|
custom$: () => custom$2,
|
|
custom$$: () => custom$$2,
|
|
doubleClick: () => doubleClick,
|
|
dragAndDrop: () => dragAndDrop,
|
|
execute: () => execute2,
|
|
executeAsync: () => executeAsync2,
|
|
getAttribute: () => getAttribute,
|
|
getCSSProperty: () => getCSSProperty,
|
|
getComputedLabel: () => getComputedLabel,
|
|
getComputedRole: () => getComputedRole,
|
|
getElement: () => getElement2,
|
|
getHTML: () => getHTML,
|
|
getLocation: () => getLocation,
|
|
getProperty: () => getProperty,
|
|
getSize: () => getSize,
|
|
getTagName: () => getTagName,
|
|
getText: () => getText,
|
|
getValue: () => getValue,
|
|
isClickable: () => isClickable,
|
|
isDisplayed: () => isDisplayed,
|
|
isEnabled: () => isEnabled,
|
|
isEqual: () => isEqual,
|
|
isExisting: () => isExisting,
|
|
isFocused: () => isFocused,
|
|
isSelected: () => isSelected,
|
|
isStable: () => isStable,
|
|
longPress: () => longPress,
|
|
moveTo: () => moveTo,
|
|
nextElement: () => nextElement,
|
|
parentElement: () => parentElement,
|
|
pinch: () => pinch,
|
|
previousElement: () => previousElement,
|
|
react$: () => react$2,
|
|
react$$: () => react$$2,
|
|
saveScreenshot: () => saveScreenshot3,
|
|
scrollIntoView: () => scrollIntoView,
|
|
selectByAttribute: () => selectByAttribute,
|
|
selectByIndex: () => selectByIndex,
|
|
selectByVisibleText: () => selectByVisibleText,
|
|
setValue: () => setValue,
|
|
shadow$: () => shadow$,
|
|
shadow$$: () => shadow$$,
|
|
tap: () => tap,
|
|
touchAction: () => touchAction3,
|
|
waitForClickable: () => waitForClickable,
|
|
waitForDisplayed: () => waitForDisplayed,
|
|
waitForEnabled: () => waitForEnabled,
|
|
waitForExist: () => waitForExist,
|
|
waitForStable: () => waitForStable,
|
|
waitUntil: () => waitUntil2,
|
|
zoom: () => zoom
|
|
});
|
|
|
|
// src/commands/element/$$.ts
|
|
var $$2 = $$;
|
|
|
|
// src/commands/element/$.ts
|
|
var $2 = $;
|
|
|
|
// src/commands/element/addValue.ts
|
|
import { CommandRuntimeOptions } from "webdriver";
|
|
var VALID_TYPES = ["string", "number"];
|
|
function addValue(value, options) {
|
|
if (!VALID_TYPES.includes(typeof value)) {
|
|
throw new Error(
|
|
'The setValue/addValue command only take string or number values. If you like to use special characters, use the "keys" command.'
|
|
);
|
|
}
|
|
if (options) {
|
|
return this.elementSendKeys(this.elementId, value.toString(), new CommandRuntimeOptions(options));
|
|
}
|
|
return this.elementSendKeys(this.elementId, value.toString());
|
|
}
|
|
|
|
// src/commands/element/clearValue.ts
|
|
function clearValue() {
|
|
return this.elementClear(this.elementId);
|
|
}
|
|
|
|
// src/commands/element/click.ts
|
|
import logger24 from "@wdio/logger";
|
|
import { getBrowserObject as getBrowserObject13 } from "@wdio/utils";
|
|
var log24 = logger24("webdriver");
|
|
function click(options) {
|
|
if (typeof options !== "undefined") {
|
|
if (typeof options !== "object" || Array.isArray(options)) {
|
|
throw new TypeError("Options must be an object");
|
|
}
|
|
return actionClick(this, options);
|
|
}
|
|
return elementClick(this);
|
|
}
|
|
async function workaround(element) {
|
|
await element.scrollIntoView({ block: "center", inline: "center" });
|
|
}
|
|
async function elementClick(element) {
|
|
try {
|
|
return await element.elementClick(element.elementId);
|
|
} catch (error) {
|
|
let err = error;
|
|
if (typeof error === "string") {
|
|
err = new Error(error);
|
|
}
|
|
if (!err.message.includes("element click intercepted")) {
|
|
throw err;
|
|
}
|
|
await workaround(element);
|
|
return element.elementClick(element.elementId);
|
|
}
|
|
}
|
|
async function actionClick(element, options) {
|
|
const defaultOptions = {
|
|
button: 0,
|
|
x: 0,
|
|
y: 0,
|
|
skipRelease: false,
|
|
duration: 0
|
|
};
|
|
const { button, x, y, skipRelease, duration } = { ...defaultOptions, ...options };
|
|
if (typeof x !== "number" || typeof y !== "number" || !Number.isInteger(x) || !Number.isInteger(y)) {
|
|
throw new TypeError("Coordinates must be integers");
|
|
}
|
|
if (!buttonValue.includes(button)) {
|
|
throw new Error("Button type not supported.");
|
|
}
|
|
const browser = getBrowserObject13(element);
|
|
if (x || y) {
|
|
const { width, height } = await browser.getElementRect(element.elementId);
|
|
if (x && x < -Math.floor(width / 2) || x && x > Math.floor(width / 2)) {
|
|
log24.warn("x would cause a out of bounds error as it goes outside of element");
|
|
}
|
|
if (y && y < -Math.floor(height / 2) || y && y > Math.floor(height / 2)) {
|
|
log24.warn("y would cause a out of bounds error as it goes outside of element");
|
|
}
|
|
}
|
|
const clickNested = async () => {
|
|
await browser.action("pointer", {
|
|
parameters: { pointerType: browser.isMobile ? "touch" : "mouse" }
|
|
}).move({ origin: element, x, y }).down({ button }).pause(duration).up({ button }).perform(skipRelease);
|
|
};
|
|
try {
|
|
return await clickNested();
|
|
} catch {
|
|
await workaround(element);
|
|
return clickNested();
|
|
}
|
|
}
|
|
|
|
// src/commands/element/custom$$.ts
|
|
import { ELEMENT_KEY as ELEMENT_KEY9 } from "webdriver";
|
|
import { getBrowserObject as getBrowserObject14 } from "@wdio/utils";
|
|
async function custom$$2(strategyName, ...strategyArguments) {
|
|
const browserObject = getBrowserObject14(this);
|
|
const strategy = browserObject.strategies.get(strategyName);
|
|
if (!strategy) {
|
|
throw Error("No strategy found for " + strategyName);
|
|
}
|
|
if (!this.elementId) {
|
|
throw Error(`Can't call custom$ on element with selector "${this.selector}" because element wasn't found`);
|
|
}
|
|
const strategyRef = { strategy, strategyName, strategyArguments: [...strategyArguments, this] };
|
|
let res = await browserObject.execute(strategy, ...strategyArguments, this);
|
|
if (!Array.isArray(res)) {
|
|
res = [res];
|
|
}
|
|
res = res.filter((el) => !!el && typeof el[ELEMENT_KEY9] === "string");
|
|
const elements = res.length ? await getElements.call(this, strategyRef, res) : [];
|
|
return enhanceElementsArray(elements, this, strategyName, "custom$$", strategyArguments);
|
|
}
|
|
|
|
// src/commands/element/custom$.ts
|
|
import { ELEMENT_KEY as ELEMENT_KEY10 } from "webdriver";
|
|
import { getBrowserObject as getBrowserObject15 } from "@wdio/utils";
|
|
async function custom$2(strategyName, ...strategyArguments) {
|
|
const browserObject = getBrowserObject15(this);
|
|
const strategy = browserObject.strategies.get(strategyName);
|
|
if (!strategy) {
|
|
throw Error("No strategy found for " + strategyName);
|
|
}
|
|
if (!this.elementId) {
|
|
throw Error(`Can't call custom$ on element with selector "${this.selector}" because element wasn't found`);
|
|
}
|
|
const strategyRef = { strategy, strategyName, strategyArguments: [...strategyArguments, this] };
|
|
let res = await browserObject.execute(strategy, ...strategyArguments, this);
|
|
if (Array.isArray(res)) {
|
|
res = res[0];
|
|
}
|
|
if (res && typeof res[ELEMENT_KEY10] === "string") {
|
|
return await getElement.call(this, strategyRef, res);
|
|
}
|
|
return await getElement.call(this, strategyRef, new Error("no such element"));
|
|
}
|
|
|
|
// src/commands/element/doubleClick.ts
|
|
import { getBrowserObject as getBrowserObject16 } from "@wdio/utils";
|
|
async function doubleClick() {
|
|
const browser = getBrowserObject16(this);
|
|
return browser.action("pointer", { parameters: { pointerType: "mouse" } }).move({ origin: this }).down().up().pause(10).down().up().perform();
|
|
}
|
|
|
|
// src/commands/element/dragAndDrop.ts
|
|
import { ELEMENT_KEY as ELEMENT_KEY11 } from "webdriver";
|
|
import { getBrowserObject as getBrowserObject17 } from "@wdio/utils";
|
|
async function dragAndDrop(target, options = {}) {
|
|
const moveToCoordinates = target;
|
|
const moveToElement = await target;
|
|
if (
|
|
/**
|
|
* no target was specified
|
|
*/
|
|
!moveToElement || /**
|
|
* target is not from type element
|
|
*/
|
|
moveToElement.constructor.name !== "Element" && /**
|
|
* and is also not an object with x and y number parameters
|
|
*/
|
|
(typeof moveToCoordinates.x !== "number" || typeof moveToCoordinates.y !== "number")
|
|
) {
|
|
throw new Error('command dragAndDrop requires an WebdriverIO Element or and object with "x" and "y" variables as first parameter');
|
|
}
|
|
const ACTION_BUTTON = 0;
|
|
const browser = getBrowserObject17(this);
|
|
const defaultOptions = { duration: browser.isMobile ? 250 : 10 };
|
|
const { duration } = { ...defaultOptions, ...options };
|
|
const isMovingToElement = moveToElement.constructor.name === "Element";
|
|
const sourceRef = { [ELEMENT_KEY11]: this[ELEMENT_KEY11] };
|
|
const targetRef = { [ELEMENT_KEY11]: moveToElement[ELEMENT_KEY11] };
|
|
const origin = sourceRef;
|
|
const targetOrigin = isMovingToElement ? targetRef : "pointer";
|
|
const targetX = isMovingToElement ? 0 : moveToCoordinates.x;
|
|
const targetY = isMovingToElement ? 0 : moveToCoordinates.y;
|
|
return browser.action("pointer", {
|
|
parameters: { pointerType: browser.isMobile ? "touch" : "mouse" }
|
|
}).move({ duration: 0, origin, x: 0, y: 0 }).down({ button: ACTION_BUTTON }).pause(10).move({ duration, origin: targetOrigin, x: targetX, y: targetY }).up({ button: ACTION_BUTTON }).perform();
|
|
}
|
|
|
|
// src/commands/element/execute.ts
|
|
import { getBrowserObject as getBrowserObject18 } from "@wdio/utils";
|
|
async function execute2(script, ...args) {
|
|
const scope = this;
|
|
const browser = getBrowserObject18(scope);
|
|
await scope.waitForExist();
|
|
return browser.execute(script, scope, ...args);
|
|
}
|
|
|
|
// src/commands/element/executeAsync.ts
|
|
import { getBrowserObject as getBrowserObject19 } from "@wdio/utils";
|
|
async function executeAsync2(script, ...args) {
|
|
const scope = this;
|
|
const browser = getBrowserObject19(scope);
|
|
return browser.executeAsync(script, scope, ...args);
|
|
}
|
|
|
|
// src/commands/element/getAttribute.ts
|
|
function getAttribute(attributeName) {
|
|
return this.getElementAttribute(this.elementId, attributeName);
|
|
}
|
|
|
|
// src/commands/element/getCSSProperty.ts
|
|
import cssShorthandProps from "css-shorthand-properties";
|
|
import { getBrowserObject as getBrowserObject20 } from "@wdio/utils";
|
|
async function getCSSProperty(cssProperty, pseudoElement) {
|
|
const getCSSProperty2 = cssShorthandProps.isShorthand(cssProperty) ? getShorthandPropertyCSSValue : getPropertyCSSValue;
|
|
const cssValue2 = await getCSSProperty2.call(
|
|
this,
|
|
{
|
|
cssProperty,
|
|
pseudoElement
|
|
}
|
|
);
|
|
return parseCSS(cssValue2, cssProperty);
|
|
}
|
|
async function getShorthandPropertyCSSValue(options) {
|
|
const { pseudoElement, cssProperty } = options;
|
|
const properties = getShorthandProperties(cssProperty);
|
|
if (pseudoElement) {
|
|
const cssValues2 = await Promise.all(
|
|
properties.map((prop) => getPseudoElementCSSValue(
|
|
this,
|
|
{
|
|
pseudoElement,
|
|
cssProperty: prop
|
|
}
|
|
))
|
|
);
|
|
return mergeEqualSymmetricalValue(cssValues2);
|
|
}
|
|
const cssValues = await Promise.all(
|
|
properties.map((prop) => this.getElementCSSValue(this.elementId, prop))
|
|
);
|
|
return mergeEqualSymmetricalValue(cssValues);
|
|
}
|
|
async function getPropertyCSSValue(options) {
|
|
const { pseudoElement, cssProperty } = options;
|
|
if (pseudoElement) {
|
|
return await getPseudoElementCSSValue(
|
|
this,
|
|
{
|
|
pseudoElement,
|
|
cssProperty
|
|
}
|
|
);
|
|
}
|
|
return await this.getElementCSSValue(this.elementId, cssProperty);
|
|
}
|
|
function getShorthandProperties(cssProperty) {
|
|
return cssShorthandProps.expand(cssProperty);
|
|
}
|
|
function mergeEqualSymmetricalValue(cssValues) {
|
|
let newCssValues = [...cssValues];
|
|
while (newCssValues.length % 2 === 0) {
|
|
const mergedValues = [
|
|
newCssValues.slice(0, newCssValues.length / 2).join(" "),
|
|
newCssValues.slice(newCssValues.length / 2).join(" ")
|
|
];
|
|
const hasEqualProperties = mergedValues.every((v) => v === mergedValues[0]);
|
|
if (!hasEqualProperties) {
|
|
break;
|
|
}
|
|
newCssValues = newCssValues.slice(0, newCssValues.length / 2);
|
|
}
|
|
return newCssValues.join(" ");
|
|
}
|
|
async function getPseudoElementCSSValue(elem, options) {
|
|
const browser = getBrowserObject20(elem);
|
|
const { cssProperty, pseudoElement } = options;
|
|
const cssValue2 = await browser.execute(
|
|
(elem2, pseudoElement2, cssProperty2) => {
|
|
if (typeof elem2.isConnected === "boolean" && !elem2.isConnected) {
|
|
throw new Error("stale element reference: element is not attached to the page document");
|
|
}
|
|
return window.getComputedStyle(elem2, pseudoElement2)[cssProperty2];
|
|
},
|
|
elem,
|
|
pseudoElement,
|
|
cssProperty
|
|
);
|
|
return cssValue2;
|
|
}
|
|
|
|
// src/commands/element/getComputedRole.ts
|
|
function getComputedRole() {
|
|
return this.getElementComputedRole(this.elementId);
|
|
}
|
|
|
|
// src/commands/element/getComputedLabel.ts
|
|
function getComputedLabel() {
|
|
return this.getElementComputedLabel(this.elementId);
|
|
}
|
|
|
|
// src/commands/element/getElement.ts
|
|
async function getElement2() {
|
|
return this;
|
|
}
|
|
|
|
// src/commands/element/getHTML.ts
|
|
import { ELEMENT_KEY as ELEMENT_KEY12 } from "webdriver";
|
|
import { prettify as prettifyFn } from "htmlfy";
|
|
import { getBrowserObject as getBrowserObject21 } from "@wdio/utils";
|
|
import getHTMLScript from "./scripts/getHTML.js";
|
|
import getHTMLShadowScript from "./scripts/getHTMLShadow.js";
|
|
var SHADOW_ID_ATTR_NAME = "data-wdio-shadow-id";
|
|
var SHADOW_ID_ATTR = `[${SHADOW_ID_ATTR_NAME}]`;
|
|
async function getHTML(options = {}) {
|
|
const browser = getBrowserObject21(this);
|
|
if (typeof options !== "object" && typeof options === "boolean") {
|
|
options = { includeSelectorTag: options };
|
|
} else if (typeof options !== "object") {
|
|
throw new Error("The `getHTML` options parameter must be an object");
|
|
}
|
|
const { includeSelectorTag, pierceShadowRoot, removeCommentNodes, prettify, excludeElements } = Object.assign({
|
|
includeSelectorTag: true,
|
|
pierceShadowRoot: true,
|
|
removeCommentNodes: true,
|
|
prettify: true,
|
|
excludeElements: []
|
|
}, options);
|
|
const basicGetHTML = (elementId, includeSelectorTag2) => {
|
|
return browser.execute(getHTMLScript, {
|
|
[ELEMENT_KEY12]: elementId,
|
|
// w3c compatible
|
|
ELEMENT: elementId
|
|
// jsonwp compatible
|
|
}, includeSelectorTag2);
|
|
};
|
|
if (pierceShadowRoot && this.isBidi) {
|
|
if (globalThis.wdio) {
|
|
return globalThis.wdio.executeWithScope(
|
|
"getHTML",
|
|
this.elementId,
|
|
{ includeSelectorTag, pierceShadowRoot, removeCommentNodes, prettify }
|
|
);
|
|
}
|
|
const { load } = await import("cheerio");
|
|
const shadowRootManager = getShadowRootManager(browser);
|
|
const contextManager = getContextManager(browser);
|
|
const context = await contextManager.getCurrentContext();
|
|
const shadowRootElementPairs = shadowRootManager.getShadowElementPairsByContextId(context, this.elementId);
|
|
const elementsWithShadowRootAndIdVerified = (await Promise.all(
|
|
shadowRootElementPairs.map(([elemId, elem]) => browser.execute((elem2) => elem2.tagName, { [ELEMENT_KEY12]: elemId }).then(
|
|
() => [elemId, elem],
|
|
() => void 0
|
|
))
|
|
)).filter(Boolean).map(([elemId, shadowId]) => [
|
|
elemId,
|
|
{ [ELEMENT_KEY12]: elemId },
|
|
shadowId ? { [ELEMENT_KEY12]: shadowId } : void 0
|
|
]);
|
|
const { html, shadowElementHTML } = await this.execute(
|
|
getHTMLShadowScript,
|
|
includeSelectorTag,
|
|
elementsWithShadowRootAndIdVerified
|
|
);
|
|
const $3 = load(html);
|
|
populateHTML($3, shadowElementHTML.map(({ id, ...props }) => ({
|
|
...props,
|
|
id,
|
|
mode: shadowRootManager.getShadowRootModeById(context, id) || "open"
|
|
})));
|
|
return sanitizeHTML($3, { removeCommentNodes, prettify, excludeElements });
|
|
}
|
|
const returnHTML = await basicGetHTML(this.elementId, includeSelectorTag);
|
|
return sanitizeHTML(returnHTML, { removeCommentNodes, prettify });
|
|
}
|
|
function populateHTML($3, shadowElementHTML) {
|
|
const shadowElements = $3(SHADOW_ID_ATTR);
|
|
if (shadowElements.length === 0) {
|
|
return;
|
|
}
|
|
for (const elem of shadowElements) {
|
|
const id = elem.attribs[SHADOW_ID_ATTR_NAME];
|
|
const shadowReference = shadowElementHTML.find(({ id: shadowRootId }) => id === shadowRootId);
|
|
if (!shadowReference) {
|
|
continue;
|
|
}
|
|
$3(`[${SHADOW_ID_ATTR_NAME}="${id}"]`).append([
|
|
`<template shadowrootmode="${shadowReference.mode}">`,
|
|
shadowReference.styles && shadowReference.styles.length > 0 ? ` <style>${shadowReference.styles.join("\n")}</style>` : "",
|
|
` ${shadowReference.html}`,
|
|
"</template>"
|
|
].join("\n"));
|
|
delete elem.attribs[SHADOW_ID_ATTR_NAME];
|
|
}
|
|
populateHTML($3, shadowElementHTML);
|
|
}
|
|
function sanitizeHTML($3, options = {}) {
|
|
const isCheerioObject = $3 && typeof $3 !== "string";
|
|
if (isCheerioObject) {
|
|
for (const elemToRemove of options.excludeElements || []) {
|
|
$3(elemToRemove).remove();
|
|
}
|
|
if (options.removeCommentNodes) {
|
|
$3("*").contents().filter(function() {
|
|
return this.type === "comment";
|
|
}).remove();
|
|
}
|
|
}
|
|
let returnHTML = isCheerioObject ? $3("body").html() : $3;
|
|
if (!isCheerioObject && options.removeCommentNodes && returnHTML) {
|
|
returnHTML = returnHTML.replace(/<!--[\s\S]*?-->/g, "");
|
|
}
|
|
return options.prettify ? prettifyFn(returnHTML) : returnHTML;
|
|
}
|
|
|
|
// src/commands/element/getLocation.ts
|
|
async function getLocation(prop) {
|
|
const { x, y } = await getElementRect(this);
|
|
const location = { x, y };
|
|
if (prop === "x" || prop === "y") {
|
|
return location[prop];
|
|
}
|
|
return location;
|
|
}
|
|
|
|
// src/commands/element/getProperty.ts
|
|
function getProperty(property) {
|
|
return this.getElementProperty(this.elementId, property);
|
|
}
|
|
|
|
// src/commands/element/getSize.ts
|
|
async function getSize(prop) {
|
|
const rect = await getElementRect(this);
|
|
if (prop && typeof rect[prop] === "number") {
|
|
return rect[prop];
|
|
}
|
|
return {
|
|
width: rect.width,
|
|
height: rect.height
|
|
};
|
|
}
|
|
|
|
// src/commands/element/getTagName.ts
|
|
function getTagName() {
|
|
return this.getElementTagName(this.elementId);
|
|
}
|
|
|
|
// src/commands/element/getText.ts
|
|
function getText() {
|
|
return this.getElementText(this.elementId);
|
|
}
|
|
|
|
// src/commands/element/getValue.ts
|
|
function getValue() {
|
|
const value = this.isW3C && !this.isMobile ? this.getElementProperty(this.elementId, "value") : this.getElementAttribute(this.elementId, "value");
|
|
return value.then((res) => typeof res === "string" ? res : "");
|
|
}
|
|
|
|
// src/commands/element/isClickable.ts
|
|
import { ELEMENT_KEY as ELEMENT_KEY13 } from "webdriver";
|
|
import { getBrowserObject as getBrowserObject22 } from "@wdio/utils";
|
|
import isElementClickableScript from "./scripts/isElementClickable.js";
|
|
async function isClickable() {
|
|
if (!await this.isDisplayed()) {
|
|
return false;
|
|
}
|
|
if (this.isMobile && this.isNativeContext) {
|
|
throw new Error("Method not supported in mobile native environment. It is unlikely that you need to use this command.");
|
|
}
|
|
const browser = getBrowserObject22(this);
|
|
return browser.execute(isElementClickableScript, {
|
|
[ELEMENT_KEY13]: this.elementId,
|
|
// w3c compatible
|
|
ELEMENT: this.elementId
|
|
// jsonwp compatible
|
|
});
|
|
}
|
|
|
|
// src/commands/element/isDisplayed.ts
|
|
import { getBrowserObject as getBrowserObject23 } from "@wdio/utils";
|
|
import isElementDisplayedLegacyScript from "./scripts/isElementDisplayed.js";
|
|
import isElementInViewportScript from "./scripts/isElementInViewport.js";
|
|
async function isDisplayed(commandParams = DEFAULT_PARAMS) {
|
|
const browser = getBrowserObject23(this);
|
|
if (!await hasElementId(this)) {
|
|
return false;
|
|
}
|
|
if (browser.isMobile && (browser.isNativeContext || browser.isWindowsApp || browser.isMacApp)) {
|
|
if (commandParams?.withinViewport) {
|
|
throw new Error(
|
|
"Cannot determine element visibility within viewport for native mobile apps as it is not feasible to determine full vertical and horizontal application bounds. In most cases a basic visibility check should suffice."
|
|
);
|
|
}
|
|
return await this.isElementDisplayed(this.elementId);
|
|
}
|
|
let hadToFallback = false;
|
|
const [isDisplayed2, displayProperty] = await Promise.all([
|
|
browser.execute(function checkVisibility(elem, params) {
|
|
if (typeof elem.checkVisibility === "function") {
|
|
return elem.checkVisibility(params);
|
|
}
|
|
return null;
|
|
}, this, {
|
|
...DEFAULT_PARAMS,
|
|
...commandParams
|
|
}).then((result) => {
|
|
if (result === null) {
|
|
hadToFallback = true;
|
|
return browser.execute(isElementDisplayedLegacyScript, this);
|
|
}
|
|
return result;
|
|
}),
|
|
browser.execute(function(elem) {
|
|
try {
|
|
const style = window.getComputedStyle(elem);
|
|
return { value: style?.display ?? "" };
|
|
} catch {
|
|
if (typeof elem.isConnected === "boolean" && !elem.isConnected) {
|
|
throw new Error("stale element reference: element is not attached to the page document");
|
|
}
|
|
return { value: "" };
|
|
}
|
|
}, this)
|
|
]);
|
|
const hasDisplayContentsCSSProperty = displayProperty.value === "contents";
|
|
const shouldRecheckContentVisibility = !hadToFallback && hasDisplayContentsCSSProperty;
|
|
const finalResponse = shouldRecheckContentVisibility ? await browser.execute(isElementDisplayedLegacyScript, this).catch(() => false) : isDisplayed2;
|
|
if (finalResponse && commandParams?.withinViewport) {
|
|
return browser.execute(isElementInViewportScript, this);
|
|
}
|
|
return finalResponse;
|
|
}
|
|
var DEFAULT_PARAMS = {
|
|
withinViewport: false,
|
|
contentVisibilityAuto: true,
|
|
opacityProperty: true,
|
|
visibilityProperty: true
|
|
};
|
|
|
|
// src/commands/element/isEnabled.ts
|
|
function isEnabled() {
|
|
return this.isElementEnabled(this.elementId);
|
|
}
|
|
|
|
// src/commands/element/isEqual.ts
|
|
import { ELEMENT_KEY as ELEMENT_KEY14 } from "webdriver";
|
|
import { getBrowserObject as getBrowserObject24 } from "@wdio/utils";
|
|
var getWebElement = (el) => ({
|
|
[ELEMENT_KEY14]: el.elementId,
|
|
// w3c compatible
|
|
ELEMENT: el.elementId
|
|
// jsonwp compatible
|
|
});
|
|
async function isEqual(el) {
|
|
const browser = getBrowserObject24(this);
|
|
if (browser.isMobile) {
|
|
const context = await browser.getContext().catch(() => void 0);
|
|
const contextId = typeof context === "string" ? context : context?.id;
|
|
if (contextId && contextId.toLowerCase().includes("native")) {
|
|
return this.elementId === el.elementId;
|
|
}
|
|
}
|
|
let result;
|
|
try {
|
|
result = await browser.execute(
|
|
/* istanbul ignore next */
|
|
function(el1, el2) {
|
|
return el1 === el2;
|
|
},
|
|
getWebElement(this),
|
|
getWebElement(el)
|
|
);
|
|
} catch {
|
|
result = false;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// src/commands/element/isExisting.ts
|
|
async function isExisting() {
|
|
if (!this.selector) {
|
|
return this.getElementTagName(this.elementId).then(
|
|
() => true,
|
|
() => false
|
|
);
|
|
}
|
|
const command = this.isReactElement ? this.parent.react$$.bind(this.parent) : this.isShadowElement ? this.shadow$$.bind(this.parent) : this.parent.$$.bind(this.parent);
|
|
return command(this.selector).getElements().then((res) => res.length > 0);
|
|
}
|
|
|
|
// src/commands/element/isFocused.ts
|
|
import { ELEMENT_KEY as ELEMENT_KEY15 } from "webdriver";
|
|
import { getBrowserObject as getBrowserObject25 } from "@wdio/utils";
|
|
import isFocusedScript from "./scripts/isFocused.js";
|
|
async function isFocused() {
|
|
const browser = await getBrowserObject25(this);
|
|
return browser.execute(isFocusedScript, {
|
|
[ELEMENT_KEY15]: this.elementId,
|
|
// w3c compatible
|
|
ELEMENT: this.elementId
|
|
// jsonwp compatible
|
|
});
|
|
}
|
|
|
|
// src/commands/element/isSelected.ts
|
|
function isSelected() {
|
|
return this.isElementSelected(this.elementId);
|
|
}
|
|
|
|
// src/commands/element/isStable.ts
|
|
import { ELEMENT_KEY as ELEMENT_KEY16 } from "webdriver";
|
|
import { getBrowserObject as getBrowserObject26 } from "@wdio/utils";
|
|
import isElementStable from "./scripts/isElementStable.js";
|
|
async function isStable() {
|
|
const browser = getBrowserObject26(this);
|
|
if (browser.isMobile && browser.isNativeContext) {
|
|
throw new Error("The `isStable` command is only available for desktop and mobile browsers.");
|
|
}
|
|
return await browser.executeAsync(isElementStable, {
|
|
[ELEMENT_KEY16]: this.elementId,
|
|
// w3c compatible
|
|
ELEMENT: this.elementId
|
|
// jsonwp compatible
|
|
});
|
|
}
|
|
|
|
// src/commands/element/moveTo.ts
|
|
import logger25 from "@wdio/logger";
|
|
import { getBrowserObject as getBrowserObject27 } from "@wdio/utils";
|
|
var log25 = logger25("webdriver");
|
|
async function moveTo({ xOffset, yOffset } = {}) {
|
|
const browser = getBrowserObject27(this);
|
|
if (xOffset || yOffset) {
|
|
const { width, height } = await browser.getElementRect(this.elementId);
|
|
if (xOffset && xOffset < -Math.floor(width / 2) || xOffset && xOffset > Math.floor(width / 2)) {
|
|
log25.warn("xOffset would cause a out of bounds error as it goes outside of element");
|
|
}
|
|
if (yOffset && yOffset < -Math.floor(height / 2) || yOffset && yOffset > Math.floor(height / 2)) {
|
|
log25.warn("yOffset would cause a out of bounds error as it goes outside of element");
|
|
}
|
|
}
|
|
const moveToNested = async () => {
|
|
await browser.action("pointer", { parameters: { pointerType: "mouse" } }).move({ origin: this, x: xOffset || 0, y: yOffset || 0 }).perform();
|
|
};
|
|
try {
|
|
await moveToNested();
|
|
} catch {
|
|
await this.scrollIntoView({ block: "center", inline: "center" });
|
|
await moveToNested();
|
|
}
|
|
}
|
|
|
|
// src/commands/element/nextElement.ts
|
|
function nextElement() {
|
|
return this.$(
|
|
/* istanbul ignore next */
|
|
function nextElement2() {
|
|
return this.nextElementSibling;
|
|
}
|
|
);
|
|
}
|
|
|
|
// src/commands/element/parentElement.ts
|
|
function parentElement() {
|
|
return this.$(
|
|
/* istanbul ignore next */
|
|
function parentElement2() {
|
|
return this.parentElement;
|
|
}
|
|
);
|
|
}
|
|
|
|
// src/commands/element/previousElement.ts
|
|
function previousElement() {
|
|
return this.$(
|
|
/* istanbul ignore next */
|
|
function previousElement2() {
|
|
return this.previousElementSibling;
|
|
}
|
|
);
|
|
}
|
|
|
|
// src/commands/element/react$$.ts
|
|
import { getBrowserObject as getBrowserObject28 } from "@wdio/utils";
|
|
import { waitToLoadReact as waitToLoadReact3, react$$ as react$$Script2 } from "./scripts/resq.js";
|
|
async function react$$2(selector, { props = {}, state = {} } = {}) {
|
|
const browser = await getBrowserObject28(this);
|
|
await this.executeScript(resqScript.toString(), []);
|
|
await browser.execute(waitToLoadReact3);
|
|
const res = await browser.execute(
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
react$$Script2,
|
|
selector,
|
|
props,
|
|
state,
|
|
this
|
|
);
|
|
const elements = await getElements.call(this, selector, res, { isReactElement: true });
|
|
return enhanceElementsArray(elements, this, selector, "react$$", [props, state]);
|
|
}
|
|
|
|
// src/commands/element/react$.ts
|
|
import { getBrowserObject as getBrowserObject29 } from "@wdio/utils";
|
|
import { waitToLoadReact as waitToLoadReact4, react$ as react$Script2 } from "./scripts/resq.js";
|
|
async function react$2(selector, { props = {}, state = {} } = {}) {
|
|
const browser = await getBrowserObject29(this);
|
|
await this.executeScript(resqScript.toString(), []);
|
|
await browser.execute(waitToLoadReact4);
|
|
const res = await browser.execute(
|
|
react$Script2,
|
|
selector,
|
|
props,
|
|
state,
|
|
this
|
|
);
|
|
return getElement.call(this, selector, res, { isReactElement: true });
|
|
}
|
|
|
|
// src/commands/element/saveScreenshot.ts
|
|
async function saveScreenshot3(filepath) {
|
|
return environment.value.saveElementScreenshot.call(this, filepath);
|
|
}
|
|
|
|
// src/commands/element/scrollIntoView.ts
|
|
import logger26 from "@wdio/logger";
|
|
import { ELEMENT_KEY as ELEMENT_KEY17 } from "webdriver";
|
|
import { getBrowserObject as getBrowserObject30 } from "@wdio/utils";
|
|
var log26 = logger26("webdriverio");
|
|
async function scrollIntoView(options = { block: "start", inline: "nearest" }) {
|
|
const browser = getBrowserObject30(this);
|
|
if (browser.isMobile) {
|
|
if (await browser.isNativeContext) {
|
|
return nativeMobileScrollIntoView({
|
|
browser,
|
|
element: this,
|
|
options: options || {}
|
|
});
|
|
}
|
|
return scrollIntoViewWeb.call(this, options);
|
|
}
|
|
try {
|
|
const elemRect = await browser.getElementRect(this.elementId);
|
|
const viewport = await browser.getWindowSize();
|
|
let [scrollX, scrollY] = await browser.execute(() => [
|
|
window.scrollX,
|
|
window.scrollY
|
|
]);
|
|
scrollX = elemRect.x <= viewport.width ? elemRect.x : viewport.width / 2;
|
|
scrollY = elemRect.y <= viewport.height ? elemRect.y : viewport.height / 2;
|
|
const deltaByOption = {
|
|
start: { y: elemRect.y - elemRect.height, x: elemRect.x - elemRect.width },
|
|
center: { y: elemRect.y - Math.round((viewport.height - elemRect.height) / 2), x: elemRect.x - Math.round((viewport.width - elemRect.width) / 2) },
|
|
end: { y: elemRect.y - (viewport.height - elemRect.height), x: elemRect.x - (viewport.width - elemRect.width) }
|
|
};
|
|
let [deltaX, deltaY] = [deltaByOption.start.x, deltaByOption.start.y];
|
|
if (options === true) {
|
|
options = { block: "start", inline: "nearest" };
|
|
}
|
|
if (options === false) {
|
|
options = { block: "end", inline: "nearest" };
|
|
}
|
|
if (options && typeof options === "object") {
|
|
const { block, inline } = options;
|
|
if (block === "nearest") {
|
|
const nearestYDistance = Math.min(...Object.values(deltaByOption).map((delta) => delta.y));
|
|
deltaY = Object.values(deltaByOption).find((delta) => delta.y === nearestYDistance).y;
|
|
} else if (block) {
|
|
deltaY = deltaByOption[block].y;
|
|
}
|
|
if (inline === "nearest") {
|
|
const nearestXDistance = Math.min(...Object.values(deltaByOption).map((delta) => delta.x));
|
|
deltaX = Object.values(deltaByOption).find((delta) => delta.x === nearestXDistance).x;
|
|
} else if (inline) {
|
|
deltaX = deltaByOption[inline].x;
|
|
}
|
|
}
|
|
deltaX = Math.round(deltaX - scrollX);
|
|
deltaY = Math.round(deltaY - scrollY);
|
|
await browser.action("wheel").scroll({ duration: 0, x: deltaX, y: deltaY, origin: this }).perform();
|
|
} catch (err) {
|
|
log26.warn(
|
|
`Failed to execute "scrollIntoView" using WebDriver Actions API: ${err.message}!
|
|
Re-attempting using \`Element.scrollIntoView\` via Web API.`
|
|
);
|
|
await scrollIntoViewWeb.call(this, options);
|
|
}
|
|
}
|
|
async function mobileScrollUntilVisible({
|
|
browser,
|
|
direction,
|
|
duration,
|
|
element,
|
|
maxScrolls,
|
|
percent,
|
|
scrollableElement
|
|
}) {
|
|
let isVisible = false;
|
|
let hasScrolled = false;
|
|
let scrolls = 0;
|
|
while (!isVisible && scrolls < maxScrolls) {
|
|
try {
|
|
isVisible = await element.isDisplayed();
|
|
} catch {
|
|
isVisible = false;
|
|
}
|
|
if (isVisible) {
|
|
break;
|
|
}
|
|
await browser.swipe({
|
|
direction,
|
|
...duration ? { duration } : {},
|
|
...percent ? { percent } : {},
|
|
...scrollableElement ? { scrollableElement } : {}
|
|
});
|
|
hasScrolled = true;
|
|
scrolls++;
|
|
}
|
|
return { hasScrolled, isVisible };
|
|
}
|
|
async function nativeMobileScrollIntoView({
|
|
browser,
|
|
element,
|
|
options
|
|
}) {
|
|
const defaultOptions = {
|
|
maxScrolls: 10,
|
|
direction: "up" /* Up */
|
|
};
|
|
const mobileOptions = {
|
|
...defaultOptions,
|
|
...options || {}
|
|
};
|
|
const { hasScrolled, isVisible } = await mobileScrollUntilVisible({
|
|
browser,
|
|
element,
|
|
maxScrolls: mobileOptions.maxScrolls,
|
|
direction: mobileOptions.direction,
|
|
...mobileOptions?.duration ? { duration: mobileOptions.duration } : {},
|
|
...mobileOptions?.percent ? { percent: mobileOptions.percent } : {},
|
|
...mobileOptions?.scrollableElement ? { scrollableElement: mobileOptions.scrollableElement } : {}
|
|
});
|
|
if (hasScrolled && isVisible) {
|
|
return browser.pause(1e3);
|
|
} else if (isVisible) {
|
|
return;
|
|
}
|
|
throw new Error(`Element not found within scroll limit of ${mobileOptions.maxScrolls} scrolls by scrolling "${mobileOptions.direction}". Are you sure the element is within the scrollable element or the direction is correct? You can change the scrollable element or direction like this:
|
|
|
|
await elem.scrollIntoView({
|
|
direction: 'left' // possible options are: 'up|down|left|right'
|
|
scrollableElement: $('#scrollable'),
|
|
});
|
|
|
|
`);
|
|
}
|
|
function scrollIntoViewWeb(options = { block: "start", inline: "nearest" }) {
|
|
const browser = getBrowserObject30(this);
|
|
return browser.execute(
|
|
(elem, options2) => elem.scrollIntoView(options2),
|
|
{
|
|
[ELEMENT_KEY17]: this.elementId,
|
|
// w3c compatible
|
|
ELEMENT: this.elementId
|
|
// jsonwp compatible
|
|
},
|
|
options
|
|
);
|
|
}
|
|
|
|
// src/commands/element/selectByAttribute.ts
|
|
import { ELEMENT_KEY as ELEMENT_KEY18 } from "webdriver";
|
|
async function selectByAttribute(attribute, value) {
|
|
value = typeof value === "number" ? value.toString() : value;
|
|
const normalized = `[normalize-space(@${attribute.trim()}) = "${value.trim()}"]`;
|
|
let optionElement;
|
|
await this.waitUntil(async () => {
|
|
optionElement = await this.findElementFromElement(
|
|
this.elementId,
|
|
"xpath",
|
|
`./option${normalized}|./optgroup/option${normalized}`
|
|
);
|
|
return ELEMENT_KEY18 in optionElement;
|
|
}, {
|
|
timeoutMsg: `Option with attribute "${attribute}=${value}" not found.`
|
|
});
|
|
return this.elementClick(getElementFromResponse(optionElement));
|
|
}
|
|
|
|
// src/commands/element/selectByIndex.ts
|
|
async function selectByIndex(index) {
|
|
if (index < 0) {
|
|
throw new Error("Index needs to be 0 or any other positive number");
|
|
}
|
|
const fetchOptionElements = async () => {
|
|
return this.findElementsFromElement(this.elementId, "css selector", "option");
|
|
};
|
|
let optionElements = [];
|
|
await this.waitUntil(async () => {
|
|
optionElements = await fetchOptionElements();
|
|
return optionElements.length > 0;
|
|
}, {
|
|
timeoutMsg: "Select element doesn't contain any option element"
|
|
});
|
|
await this.waitUntil(async () => {
|
|
optionElements = await fetchOptionElements();
|
|
return typeof optionElements[index] !== "undefined";
|
|
}, {
|
|
timeoutMsg: `Option with index "${index}" not found. Select element only contains ${optionElements.length} option elements`
|
|
});
|
|
return this.elementClick(getElementFromResponse(optionElements[index]));
|
|
}
|
|
|
|
// src/commands/element/selectByVisibleText.ts
|
|
async function selectByVisibleText(text) {
|
|
text = typeof text === "number" ? text.toString() : text;
|
|
const normalized = text.trim().replace(/\s+/, " ");
|
|
const formatted = /"/.test(normalized) ? 'concat("' + normalized.split('"').join(`", '"', "`) + '")' : `"${normalized}"`;
|
|
const dotFormat = `[. = ${formatted}]`;
|
|
const spaceFormat = `[normalize-space(text()) = ${formatted}]`;
|
|
const selections = [
|
|
`./option${dotFormat}`,
|
|
`./option${spaceFormat}`,
|
|
`./optgroup/option${dotFormat}`,
|
|
`./optgroup/option${spaceFormat}`
|
|
];
|
|
const optionElement = await this.$(selections.join("|"));
|
|
await optionElement.waitForExist({
|
|
timeoutMsg: `Option with text "${text}" not found.`
|
|
});
|
|
return this.elementClick(getElementFromResponse(optionElement));
|
|
}
|
|
|
|
// src/commands/element/setValue.ts
|
|
async function setValue(value, options) {
|
|
await this.clearValue();
|
|
return this.addValue(value, options);
|
|
}
|
|
|
|
// src/commands/element/shadow$$.ts
|
|
import logger27 from "@wdio/logger";
|
|
import { getBrowserObject as getBrowserObject31 } from "@wdio/utils";
|
|
import { SHADOW_ELEMENT_KEY } from "webdriver";
|
|
import { shadowFnFactory } from "./scripts/shadowFnFactory.js";
|
|
|
|
// src/utils/findStrategy.ts
|
|
import { roleElements } from "aria-query";
|
|
var DEFAULT_STRATEGY = "css selector";
|
|
var DIRECT_SELECTOR_REGEXP = /^(id|css selector|xpath|link text|partial link text|name|tag name|class name|-android uiautomator|-android datamatcher|-android viewmatcher|-android viewtag|-ios uiautomation|-ios predicate string|-ios class chain|accessibility id):(.+)/;
|
|
var XPATH_SELECTORS_START = [
|
|
"/",
|
|
"(",
|
|
"../",
|
|
"./",
|
|
"*/"
|
|
];
|
|
var NAME_MOBILE_SELECTORS_START = [
|
|
"uia",
|
|
"xcuielementtype",
|
|
"android.widget",
|
|
"cyi",
|
|
"android.view"
|
|
];
|
|
var XPATH_SELECTOR_REGEXP = [
|
|
// HTML tag
|
|
/^([a-z0-9|-]*)/,
|
|
// optional . or # + class or id
|
|
/(?:(\.|#)(-?[_a-zA-Z]+[_a-zA-Z0-9-]*))?/,
|
|
// optional [attribute-name="attribute-selector"]
|
|
/(?:\[(-?[_a-zA-Z]+[_a-zA-Z0-9-]*)(?:=(?:"|')([a-zA-Z0-9\-_. ]+)(?:"|'))?\])?/,
|
|
// optional case insensitive
|
|
/(\.)?/,
|
|
// *=query or =query
|
|
/(\*)?=(.+)$/
|
|
];
|
|
var IMAGEPATH_MOBILE_SELECTORS_ENDSWITH = [
|
|
".jpg",
|
|
".jpeg",
|
|
".gif",
|
|
".png",
|
|
".bmp",
|
|
".svg"
|
|
];
|
|
var defineStrategy = function(selector) {
|
|
if (typeof selector === "object") {
|
|
if (JSON.stringify(selector).indexOf("test.espresso.matcher.ViewMatchers") < 0) {
|
|
return "-android datamatcher";
|
|
}
|
|
return "-android viewmatcher";
|
|
}
|
|
const stringSelector = selector;
|
|
if (DIRECT_SELECTOR_REGEXP.test(stringSelector)) {
|
|
return "directly";
|
|
}
|
|
if (IMAGEPATH_MOBILE_SELECTORS_ENDSWITH.some((path8) => {
|
|
const selector2 = stringSelector.toLowerCase();
|
|
return selector2.endsWith(path8) && selector2 !== path8;
|
|
})) {
|
|
return "-image";
|
|
}
|
|
if (XPATH_SELECTORS_START.some((option) => stringSelector.startsWith(option))) {
|
|
return "xpath";
|
|
}
|
|
if (stringSelector.startsWith("=")) {
|
|
return "link text";
|
|
}
|
|
if (stringSelector.startsWith("*=")) {
|
|
return "partial link text";
|
|
}
|
|
if (stringSelector.startsWith("id=")) {
|
|
return "id";
|
|
}
|
|
if (stringSelector.startsWith(DEEP_SELECTOR)) {
|
|
return "shadow";
|
|
}
|
|
if (stringSelector.startsWith(ARIA_SELECTOR)) {
|
|
return "aria";
|
|
}
|
|
if (stringSelector.startsWith("android=")) {
|
|
return "-android uiautomator";
|
|
}
|
|
if (stringSelector.startsWith("ios=")) {
|
|
return "-ios uiautomation";
|
|
}
|
|
if (stringSelector.startsWith("~")) {
|
|
return "accessibility id";
|
|
}
|
|
if (NAME_MOBILE_SELECTORS_START.some((option) => stringSelector.toLowerCase().startsWith(option))) {
|
|
return "class name";
|
|
}
|
|
if (stringSelector.search(/<[0-9a-zA-Z-]+( \/)*>/g) >= 0) {
|
|
return "tag name";
|
|
}
|
|
if (stringSelector.search(/^\[name=(?:"(.[^"]*)"|'(.[^']*)')]$/) >= 0) {
|
|
return "name";
|
|
}
|
|
if (selector === ".." || selector === ".") {
|
|
return "xpath";
|
|
}
|
|
if (stringSelector.match(new RegExp(XPATH_SELECTOR_REGEXP.map((rx) => rx.source).join("")))) {
|
|
return "xpath extended";
|
|
}
|
|
if (/^\[role=[A-Za-z]+]$/.test(stringSelector)) {
|
|
return "role";
|
|
}
|
|
};
|
|
var findStrategy = function(selector, isW3C, isMobile) {
|
|
const stringSelector = selector;
|
|
let using = DEFAULT_STRATEGY;
|
|
let value = selector;
|
|
switch (defineStrategy(selector)) {
|
|
// user has specified locator strategy directly
|
|
case "directly": {
|
|
const match = stringSelector.match(DIRECT_SELECTOR_REGEXP);
|
|
if (!match) {
|
|
throw new Error("InvalidSelectorStrategy");
|
|
}
|
|
using = match[1];
|
|
value = match[2];
|
|
break;
|
|
}
|
|
case "xpath": {
|
|
using = "xpath";
|
|
break;
|
|
}
|
|
case "id": {
|
|
using = "id";
|
|
value = stringSelector.slice(3);
|
|
break;
|
|
}
|
|
case "link text": {
|
|
using = "link text";
|
|
value = stringSelector.slice(1);
|
|
break;
|
|
}
|
|
case "partial link text": {
|
|
using = "partial link text";
|
|
value = stringSelector.slice(2);
|
|
break;
|
|
}
|
|
case "shadow":
|
|
using = "shadow";
|
|
value = stringSelector.slice(DEEP_SELECTOR.length);
|
|
break;
|
|
case "aria": {
|
|
const label = stringSelector.slice(ARIA_SELECTOR.length);
|
|
const conditions = [
|
|
// aria label is recevied by other element with aria-labelledBy
|
|
// https://www.w3.org/TR/accname-1.1/#step2B
|
|
`.//*[@aria-labelledby=(//*[normalize-space(text()) = "${label}"]/@id)]`,
|
|
// aria label is recevied by other element with aria-labelledBy
|
|
// https://www.w3.org/TR/accname-1.1/#step2B
|
|
`.//*[@aria-describedby=(//*[normalize-space(text()) = "${label}"]/@id)]`,
|
|
// element has direct aria label
|
|
// https://www.w3.org/TR/accname-1.1/#step2C
|
|
`.//*[@aria-label = "${label}"]`,
|
|
// input and textarea with a label
|
|
// https://www.w3.org/TR/accname-1.1/#step2D
|
|
`.//input[@id = (//label[normalize-space() = "${label}"]/@for)]`,
|
|
`.//textarea[@id = (//label[normalize-space() = "${label}"]/@for)]`,
|
|
// input and textarea with a label as parent
|
|
// https://www.w3.org/TR/accname-1.1/#step2D
|
|
`.//input[ancestor::label[normalize-space(text()) = "${label}"]]`,
|
|
`.//textarea[ancestor::label[normalize-space(text()) = "${label}"]]`,
|
|
// aria label is received by a placeholder
|
|
// https://www.w3.org/TR/accname-1.1/#step2D
|
|
`.//input[@placeholder="${label}"]`,
|
|
`.//textarea[@placeholder="${label}"]`,
|
|
// aria label is received by a aria-placeholder
|
|
// https://www.w3.org/TR/accname-1.1/#step2D
|
|
`.//input[@aria-placeholder="${label}"]`,
|
|
`.//textarea[@aria-placeholder="${label}"]`,
|
|
// aria label is received by a title
|
|
// https://www.w3.org/TR/accname-1.1/#step2D
|
|
`.//*[not(self::label)][@title="${label}"]`,
|
|
// images with an alt tag
|
|
// https://www.w3.org/TR/accname-1.1/#step2D
|
|
`.//img[@alt="${label}"]`,
|
|
// aria label is received from element text content
|
|
// https://www.w3.org/TR/accname-1.1/#step2G
|
|
`.//*[not(self::label)][normalize-space(text()) = "${label}"]`
|
|
];
|
|
using = "xpath";
|
|
value = conditions.join(" | ");
|
|
break;
|
|
}
|
|
case "-android uiautomator": {
|
|
using = "-android uiautomator";
|
|
value = stringSelector.slice(8);
|
|
break;
|
|
}
|
|
case "-android datamatcher": {
|
|
using = "-android datamatcher";
|
|
value = JSON.stringify(value);
|
|
break;
|
|
}
|
|
case "-android viewmatcher": {
|
|
using = "-android viewmatcher";
|
|
value = JSON.stringify(value);
|
|
break;
|
|
}
|
|
case "-ios uiautomation": {
|
|
using = "-ios uiautomation";
|
|
value = stringSelector.slice(4);
|
|
break;
|
|
}
|
|
case "accessibility id": {
|
|
using = "accessibility id";
|
|
value = stringSelector.slice(1);
|
|
break;
|
|
}
|
|
case "class name": {
|
|
using = "class name";
|
|
break;
|
|
}
|
|
case "tag name": {
|
|
using = "tag name";
|
|
value = stringSelector.replace(/<|>|\/|\s/g, "");
|
|
break;
|
|
}
|
|
case "name": {
|
|
if (isMobile || !isW3C) {
|
|
const match = stringSelector.match(/^\[name=(?:"(.[^"]*)"|'(.[^']*)')]$/);
|
|
if (!match) {
|
|
throw new Error(`InvalidSelectorMatch. Strategy 'name' has failed to match '${stringSelector}'`);
|
|
}
|
|
using = "name";
|
|
value = match[1] || match[2];
|
|
}
|
|
break;
|
|
}
|
|
case "xpath extended": {
|
|
using = "xpath";
|
|
const match = stringSelector.match(new RegExp(XPATH_SELECTOR_REGEXP.map((rx) => rx.source).join("")));
|
|
if (!match) {
|
|
throw new Error(`InvalidSelectorMatch: Strategy 'xpath extended' has failed to match '${stringSelector}'`);
|
|
}
|
|
const PREFIX_NAME = { ".": "class", "#": "id" };
|
|
const conditions = [];
|
|
const [
|
|
tag,
|
|
prefix,
|
|
name,
|
|
attrName,
|
|
attrValue,
|
|
insensitive,
|
|
partial,
|
|
query
|
|
] = match.slice(1);
|
|
if (prefix) {
|
|
if (prefix === ".") {
|
|
conditions.push(`contains(concat(" ",@${PREFIX_NAME[prefix]}," "), " ${name} ")`);
|
|
} else {
|
|
conditions.push(`contains(@${PREFIX_NAME[prefix]}, "${name}")`);
|
|
}
|
|
}
|
|
if (attrName) {
|
|
conditions.push(
|
|
attrValue ? `contains(@${attrName}, "${attrValue}")` : `@${attrName}`
|
|
);
|
|
}
|
|
const partialNot = ` and not(${`.//${tag || "*"}${conditions.length ? `[${conditions.join(" and ")}]` : ""}`})`;
|
|
if (insensitive) {
|
|
conditions.push(
|
|
partial ? `contains(translate(., "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz"), "${query.toLowerCase()}")${partialNot}` : `normalize-space(translate(text(), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")) = "${query.toLowerCase()}"`
|
|
);
|
|
} else {
|
|
conditions.push(partial ? `contains(., "${query}")${partialNot}` : `normalize-space(text()) = "${query}"`);
|
|
}
|
|
const getValue2 = () => `.//${tag || "*"}[${conditions.join(" and ")}]`;
|
|
value = getValue2();
|
|
if (!partial) {
|
|
conditions.pop();
|
|
conditions.push(
|
|
`not(${value})`,
|
|
`normalize-space() = "${insensitive ? query.toLowerCase() : query}"`
|
|
);
|
|
value = value + " | " + getValue2();
|
|
}
|
|
break;
|
|
}
|
|
case "-image": {
|
|
using = "-image";
|
|
value = environment.value.readFileSync(stringSelector, { encoding: "base64" });
|
|
break;
|
|
}
|
|
case "role": {
|
|
const match = stringSelector.match(/^\[role=(.+)\]/);
|
|
if (!match) {
|
|
throw new Error(`InvalidSelectorMatch. Strategy 'role' has failed to match '${stringSelector}'`);
|
|
}
|
|
using = "css selector";
|
|
value = createRoleBaseXpathSelector(match[1]);
|
|
break;
|
|
}
|
|
}
|
|
return { using, value };
|
|
};
|
|
var createRoleBaseXpathSelector = (role) => {
|
|
const locatorArr = [];
|
|
roleElements.get(role)?.forEach((value) => {
|
|
let locator;
|
|
let tagAttribute, tagAttributevalue;
|
|
const tagname = value.name;
|
|
if (value.attributes instanceof Array) {
|
|
value.attributes.forEach((val) => {
|
|
tagAttribute = val.name;
|
|
tagAttributevalue = val.value;
|
|
});
|
|
}
|
|
if (!tagAttribute) {
|
|
locator = tagname;
|
|
} else if (!tagAttributevalue) {
|
|
locator = `${tagname}[${tagAttribute}]`;
|
|
} else {
|
|
locator = `${tagname}[${tagAttribute}="${tagAttributevalue}"]`;
|
|
}
|
|
locatorArr.push(locator);
|
|
});
|
|
let xpathLocator = `[role="${role}"]`;
|
|
locatorArr.forEach((loc) => {
|
|
xpathLocator += "," + loc;
|
|
});
|
|
return xpathLocator;
|
|
};
|
|
|
|
// src/commands/element/shadow$$.ts
|
|
var log27 = logger27("webdriverio");
|
|
async function shadow$$(selector) {
|
|
const browser = getBrowserObject31(this);
|
|
try {
|
|
const shadowRoot = await browser.getElementShadowRoot(this.elementId);
|
|
const { using, value } = findStrategy(selector, this.isW3C, this.isMobile);
|
|
const res = await browser.findElementsFromShadowRoot(shadowRoot[SHADOW_ELEMENT_KEY], using, value);
|
|
const elements = await getElements.call(this, selector, res, { isShadowElement: true });
|
|
return enhanceElementsArray(elements, this, selector);
|
|
} catch (err) {
|
|
log27.warn(
|
|
`Failed to fetch element within shadow DOM using WebDriver command: ${err.message}!
|
|
Falling back to JavaScript shim.`
|
|
);
|
|
return await this.$$(shadowFnFactory(selector, true));
|
|
}
|
|
}
|
|
|
|
// src/commands/element/shadow$.ts
|
|
import logger28 from "@wdio/logger";
|
|
import { SHADOW_ELEMENT_KEY as SHADOW_ELEMENT_KEY2 } from "webdriver";
|
|
import { shadowFnFactory as shadowFnFactory2 } from "./scripts/shadowFnFactory.js";
|
|
import { getBrowserObject as getBrowserObject32 } from "@wdio/utils";
|
|
var log28 = logger28("webdriverio");
|
|
async function shadow$(selector) {
|
|
const browser = getBrowserObject32(this);
|
|
try {
|
|
const shadowRoot = await browser.getElementShadowRoot(this.elementId);
|
|
const { using, value } = findStrategy(selector, this.isW3C, this.isMobile);
|
|
const res = await browser.findElementFromShadowRoot(shadowRoot[SHADOW_ELEMENT_KEY2], using, value);
|
|
return getElement.call(this, selector, res, { isShadowElement: true });
|
|
} catch (err) {
|
|
log28.warn(
|
|
`Failed to fetch element within shadow DOM using WebDriver command: ${err.message}!
|
|
Falling back to JavaScript shim.`
|
|
);
|
|
return this.$(shadowFnFactory2(selector));
|
|
}
|
|
}
|
|
|
|
// src/commands/element/touchAction.ts
|
|
function touchAction3(actions2) {
|
|
return touchAction.call(this, actions2);
|
|
}
|
|
|
|
// src/commands/element/waitForClickable.ts
|
|
import { getBrowserObject as getBrowserObject33 } from "@wdio/utils";
|
|
async function waitForClickable({
|
|
timeout = this.options.waitforTimeout,
|
|
interval = this.options.waitforInterval,
|
|
reverse = false,
|
|
timeoutMsg = `element ("${this.selector}") still ${reverse ? "" : "not "}clickable after ${timeout}ms`
|
|
} = {}) {
|
|
const browser = getBrowserObject33(this);
|
|
if (browser.isMobile && browser.isNativeContext && !browser.capabilities?.browserName) {
|
|
throw new Error("The `waitForClickable` command is only available for desktop and mobile browsers.");
|
|
}
|
|
return this.waitUntil(
|
|
async () => reverse !== await this.isClickable(),
|
|
{ timeout, timeoutMsg, interval }
|
|
);
|
|
}
|
|
|
|
// src/commands/element/waitForDisplayed.ts
|
|
function waitForDisplayed({
|
|
timeout = this.options.waitforTimeout,
|
|
interval = this.options.waitforInterval,
|
|
reverse = false,
|
|
withinViewport = false,
|
|
contentVisibilityAuto = true,
|
|
opacityProperty = true,
|
|
visibilityProperty = true,
|
|
timeoutMsg = `element ("${this.selector}") still ${reverse ? "" : "not "}displayed${withinViewport ? " within viewport" : ""} after ${timeout}ms`
|
|
} = {}) {
|
|
return this.waitUntil(
|
|
async () => reverse !== await this.isDisplayed({ withinViewport, contentVisibilityAuto, opacityProperty, visibilityProperty }),
|
|
{ timeout, interval, timeoutMsg }
|
|
);
|
|
}
|
|
|
|
// src/commands/element/waitForEnabled.ts
|
|
async function waitForEnabled({
|
|
timeout = this.options.waitforTimeout,
|
|
interval = this.options.waitforInterval,
|
|
reverse = false,
|
|
timeoutMsg = `element ("${this.selector}") still ${reverse ? "" : "not "}enabled after ${timeout}ms`
|
|
} = {}) {
|
|
if (!this.elementId && !reverse) {
|
|
await this.waitForExist({ timeout, interval, timeoutMsg });
|
|
}
|
|
return this.waitUntil(
|
|
async () => reverse !== await this.isEnabled(),
|
|
{ timeout, interval, timeoutMsg }
|
|
);
|
|
}
|
|
|
|
// src/commands/element/waitForExist.ts
|
|
import { ELEMENT_KEY as ELEMENT_KEY19 } from "webdriver";
|
|
async function waitForExist({
|
|
timeout = this.options.waitforTimeout,
|
|
interval = this.options.waitforInterval,
|
|
reverse = false,
|
|
timeoutMsg = `element ("${this.selector}") still ${reverse ? "" : "not "}existing after ${timeout}ms`
|
|
} = {}) {
|
|
const isExisting2 = await this.waitUntil(
|
|
async () => reverse !== await this.isExisting(),
|
|
{ timeout, interval, timeoutMsg }
|
|
);
|
|
if (!reverse && isExisting2 && typeof this.selector === "string") {
|
|
let isCurrentIdValid = false;
|
|
if (this.elementId) {
|
|
try {
|
|
await this.getElementTagName(this.elementId);
|
|
isCurrentIdValid = true;
|
|
} catch {
|
|
}
|
|
}
|
|
if (!isCurrentIdValid) {
|
|
let element;
|
|
if (this.index !== void 0) {
|
|
const elements = this.isShadowElement ? await this.parent.shadow$$(this.selector) : await this.parent.$$(this.selector);
|
|
element = elements[this.index];
|
|
} else {
|
|
element = this.isShadowElement ? this.parent.shadow$(this.selector) : this.parent.$(this.selector);
|
|
}
|
|
this.elementId = await element.elementId;
|
|
this[ELEMENT_KEY19] = this.elementId;
|
|
}
|
|
delete this.error;
|
|
}
|
|
return isExisting2;
|
|
}
|
|
|
|
// src/commands/element/waitForStable.ts
|
|
import { getBrowserObject as getBrowserObject34 } from "@wdio/utils";
|
|
async function waitForStable({
|
|
timeout = this.options.waitforTimeout,
|
|
interval = this.options.waitforInterval,
|
|
reverse = false,
|
|
timeoutMsg = `element ("${this.selector}") still ${reverse ? "" : "not "}stable after ${timeout}ms`
|
|
} = {}) {
|
|
let errorMsg;
|
|
const browser = getBrowserObject34(this);
|
|
if (browser.isMobile && browser.isNativeContext) {
|
|
throw new Error("The `waitForStable` command is only available for desktop and mobile browsers.");
|
|
}
|
|
await this.waitUntil(
|
|
async () => {
|
|
try {
|
|
return reverse !== await this.isStable();
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
errorMsg = error.message;
|
|
} else if (typeof error === "string") {
|
|
errorMsg = error;
|
|
} else {
|
|
errorMsg = "The waitForStable command got an unknown error";
|
|
}
|
|
return !reverse;
|
|
}
|
|
},
|
|
{ timeout, interval, timeoutMsg }
|
|
);
|
|
if (errorMsg) {
|
|
throw Error(errorMsg);
|
|
}
|
|
}
|
|
|
|
// src/commands/element/waitUntil.ts
|
|
var waitUntil2 = waitUntil;
|
|
|
|
// src/commands/mobile/longPress.ts
|
|
import { getBrowserObject as getBrowserObject35 } from "@wdio/utils";
|
|
function longPress(options) {
|
|
const browser = getBrowserObject35(this);
|
|
if (!browser.isMobile) {
|
|
throw new Error("The longPress command is only available for mobile platforms.");
|
|
}
|
|
if (typeof options !== "undefined" && (typeof options !== "object" || Array.isArray(options))) {
|
|
throw new TypeError("Options must be an object");
|
|
}
|
|
const defaultOptions = {
|
|
duration: 1500,
|
|
x: 0,
|
|
y: 0
|
|
};
|
|
const { duration, x, y } = { ...defaultOptions, ...options };
|
|
if (!browser.isNativeContext && browser.isIOS) {
|
|
return browser.execute(
|
|
(el, duration2) => {
|
|
const touchStart = new TouchEvent("touchstart", {
|
|
touches: [new Touch({ identifier: 0, target: el, clientX: 0, clientY: 0 })],
|
|
bubbles: true,
|
|
cancelable: true
|
|
});
|
|
el.dispatchEvent(touchStart);
|
|
setTimeout(() => {
|
|
const touchEnd = new TouchEvent("touchend", {
|
|
changedTouches: [new Touch({ identifier: 0, target: el, clientX: 0, clientY: 0 })],
|
|
bubbles: true,
|
|
cancelable: true
|
|
});
|
|
el.dispatchEvent(touchEnd);
|
|
}, duration2);
|
|
},
|
|
this,
|
|
duration
|
|
);
|
|
}
|
|
return this.click({ duration, x, y });
|
|
}
|
|
|
|
// src/commands/mobile/pinch.ts
|
|
import { getBrowserObject as getBrowserObject36 } from "@wdio/utils";
|
|
async function pinch(options = {}) {
|
|
const browser = getBrowserObject36(this);
|
|
if (!browser.isMobile) {
|
|
throw new Error("The pinch command is only available for mobile platforms.");
|
|
}
|
|
const { duration, scale } = validatePinchAndZoomOptions({ browser, gesture: "pinch", options });
|
|
const gestureConfig = browser.isIOS ? {
|
|
elementId: await this.elementId,
|
|
scale,
|
|
velocity: -Math.abs(duration)
|
|
// Velocity is always negative for iOS pinch
|
|
} : {
|
|
elementId: await this.elementId,
|
|
percent: scale,
|
|
speed: calculateAndroidPinchAndZoomSpeed({ browser, duration, scale })
|
|
};
|
|
return browser.execute(browser.isIOS ? "mobile: pinch" : "mobile: pinchCloseGesture", gestureConfig);
|
|
}
|
|
|
|
// src/commands/mobile/zoom.ts
|
|
import { getBrowserObject as getBrowserObject37 } from "@wdio/utils";
|
|
async function zoom(options = {}) {
|
|
const browser = getBrowserObject37(this);
|
|
if (!browser.isMobile) {
|
|
throw new Error("The zoom command is only available for mobile platforms.");
|
|
}
|
|
const { duration, scale } = validatePinchAndZoomOptions({ browser, gesture: "zoom", options });
|
|
const gestureConfig = browser.isIOS ? {
|
|
elementId: await this.elementId,
|
|
scale,
|
|
velocity: duration
|
|
} : {
|
|
elementId: await this.elementId,
|
|
percent: scale,
|
|
speed: calculateAndroidPinchAndZoomSpeed({ browser, duration, scale })
|
|
};
|
|
return browser.execute(browser.isIOS ? "mobile: pinch" : "mobile: pinchOpenGesture", gestureConfig);
|
|
}
|
|
|
|
// src/utils/index.ts
|
|
import elementContains from "./scripts/elementContains.js";
|
|
|
|
// src/utils/thirdParty/querySelectorShadowDom.ts
|
|
function querySelectorAllDeep(findMany, s, r) {
|
|
function normalizeSelector(sel) {
|
|
function saveUnmatched() {
|
|
if (unmatched) {
|
|
if (tokens.length > 0 && /^[~+>]$/.test(tokens[tokens.length - 1])) {
|
|
tokens.push(" ");
|
|
}
|
|
tokens.push(unmatched);
|
|
}
|
|
}
|
|
const tokens = [], state = [0], not_escaped_pattern = /(?:[^\\]|(?:^|[^\\])(?:\\\\)+)$/, whitespace_pattern = /^\s+$/, state_patterns = [
|
|
/\s+|\/\*|["'>~+[(]/g,
|
|
// general
|
|
/\s+|\/\*|["'[\]()]/g,
|
|
// [..] set
|
|
/\s+|\/\*|["'[\]()]/g,
|
|
// (..) set
|
|
null,
|
|
// string literal (placeholder)
|
|
/\*\//g
|
|
// comment
|
|
];
|
|
let match, unmatched, regex, next_match_idx = 0, prev_match_idx;
|
|
sel = sel.trim();
|
|
while (true) {
|
|
unmatched = "";
|
|
regex = state_patterns[state[state.length - 1]];
|
|
regex.lastIndex = next_match_idx;
|
|
match = regex.exec(sel);
|
|
if (match) {
|
|
prev_match_idx = next_match_idx;
|
|
next_match_idx = regex.lastIndex;
|
|
if (prev_match_idx < next_match_idx - match[0].length) {
|
|
unmatched = sel.substring(
|
|
prev_match_idx,
|
|
next_match_idx - match[0].length
|
|
);
|
|
}
|
|
if (state[state.length - 1] < 3) {
|
|
saveUnmatched();
|
|
if (match[0] === "[") {
|
|
state.push(1);
|
|
} else if (match[0] === "(") {
|
|
state.push(2);
|
|
} else if (/^["']$/.test(match[0])) {
|
|
state.push(3);
|
|
state_patterns[3] = new RegExp(match[0], "g");
|
|
} else if (match[0] === "/*") {
|
|
state.push(4);
|
|
} else if (/^[\])]$/.test(match[0]) && state.length > 0) {
|
|
state.pop();
|
|
} else if (/^(?:\s+|[~+>])$/.test(match[0])) {
|
|
if (tokens.length > 0 && !whitespace_pattern.test(tokens[tokens.length - 1]) && state[state.length - 1] === 0) {
|
|
tokens.push(" ");
|
|
}
|
|
if (state[state.length - 1] === 1 && tokens.length === 5 && tokens[2].charAt(tokens[2].length - 1) === "=") {
|
|
tokens[4] = " " + tokens[4];
|
|
}
|
|
if (whitespace_pattern.test(match[0])) {
|
|
continue;
|
|
}
|
|
}
|
|
tokens.push(match[0]);
|
|
} else {
|
|
tokens[tokens.length - 1] += unmatched;
|
|
if (not_escaped_pattern.test(tokens[tokens.length - 1])) {
|
|
if (state[state.length - 1] === 4) {
|
|
if (tokens.length < 2 || whitespace_pattern.test(tokens[tokens.length - 2])) {
|
|
tokens.pop();
|
|
} else {
|
|
tokens[tokens.length - 1] = " ";
|
|
}
|
|
match[0] = "";
|
|
}
|
|
state.pop();
|
|
}
|
|
tokens[tokens.length - 1] += match[0];
|
|
}
|
|
} else {
|
|
unmatched = sel.substr(next_match_idx);
|
|
saveUnmatched();
|
|
break;
|
|
}
|
|
}
|
|
return tokens.join("").trim();
|
|
}
|
|
function _querySelectorDeep(selector, root, allElements = null) {
|
|
selector = normalizeSelector(selector);
|
|
const lightElement = root.querySelector(selector);
|
|
if (document.head.createShadowRoot || document.head.attachShadow) {
|
|
if (!findMany && lightElement) {
|
|
return lightElement;
|
|
}
|
|
const selectionsToMake = splitByCharacterUnlessQuoted(selector, ",");
|
|
return selectionsToMake.reduce((acc, minimalSelector) => {
|
|
if (!findMany && acc) {
|
|
return acc;
|
|
}
|
|
const splitSelector = splitByCharacterUnlessQuoted(minimalSelector.replace(/^\s+/g, "").replace(/\s*([>+~]+)\s*/g, "$1"), " ").filter((entry) => !!entry).map((entry) => splitByCharacterUnlessQuoted(entry, ">"));
|
|
const possibleElementsIndex = splitSelector.length - 1;
|
|
const lastSplitPart = splitSelector[possibleElementsIndex][splitSelector[possibleElementsIndex].length - 1];
|
|
const possibleElements = collectAllElementsDeep(lastSplitPart, root, allElements);
|
|
const findElements2 = findMatchingElement(splitSelector, possibleElementsIndex, root);
|
|
if (findMany) {
|
|
acc = acc.concat(possibleElements.filter(findElements2));
|
|
return acc;
|
|
}
|
|
acc = possibleElements.find(findElements2);
|
|
return acc || null;
|
|
}, findMany ? [] : null);
|
|
}
|
|
return !findMany ? lightElement : root.querySelectorAll(selector);
|
|
}
|
|
function findMatchingElement(splitSelector, possibleElementsIndex, root) {
|
|
return (element) => {
|
|
let position = possibleElementsIndex;
|
|
let parent = element;
|
|
let foundElement = false;
|
|
while (parent && !isDocumentNode(parent)) {
|
|
let foundMatch = true;
|
|
if (splitSelector[position].length === 1) {
|
|
foundMatch = parent.matches(splitSelector[position]);
|
|
} else {
|
|
const reversedParts = [].concat(splitSelector[position]).reverse();
|
|
let newParent = parent;
|
|
for (const part of reversedParts) {
|
|
if (!newParent || !newParent.matches(part)) {
|
|
foundMatch = false;
|
|
break;
|
|
}
|
|
newParent = findParentOrHost(newParent, root);
|
|
}
|
|
}
|
|
if (foundMatch && position === 0) {
|
|
foundElement = true;
|
|
break;
|
|
}
|
|
if (foundMatch) {
|
|
position--;
|
|
}
|
|
parent = findParentOrHost(parent, root);
|
|
}
|
|
return foundElement;
|
|
};
|
|
}
|
|
function splitByCharacterUnlessQuoted(selector, character) {
|
|
return selector.match(/\\?.|^$/g).reduce((p, c) => {
|
|
if (c === '"' && !p.sQuote) {
|
|
p.quote ^= 1;
|
|
p.a[p.a.length - 1] += c;
|
|
} else if (c === "'" && !p.quote) {
|
|
p.sQuote ^= 1;
|
|
p.a[p.a.length - 1] += c;
|
|
} else if (!p.quote && !p.sQuote && c === character) {
|
|
p.a.push("");
|
|
} else {
|
|
p.a[p.a.length - 1] += c;
|
|
}
|
|
return p;
|
|
}, { a: [""] }).a;
|
|
}
|
|
function isDocumentNode(node) {
|
|
return node.nodeType === Node.DOCUMENT_FRAGMENT_NODE || node.nodeType === Node.DOCUMENT_NODE;
|
|
}
|
|
function findParentOrHost(element, root) {
|
|
const parentNode = element.parentNode;
|
|
return parentNode && parentNode.host && parentNode.nodeType === 11 ? parentNode.host : parentNode === root ? null : parentNode;
|
|
}
|
|
function collectAllElementsDeep(selector = null, root, cachedElements = null) {
|
|
let allElements = [];
|
|
if (cachedElements) {
|
|
allElements = cachedElements;
|
|
} else {
|
|
const findAllElements = function(nodes) {
|
|
for (let i = 0; i < nodes.length; i++) {
|
|
const el = nodes[i];
|
|
allElements.push(el);
|
|
if (el.shadowRoot) {
|
|
findAllElements(el.shadowRoot.querySelectorAll("*"));
|
|
}
|
|
}
|
|
};
|
|
const shadowRoot = root.shadowRoot;
|
|
if (shadowRoot) {
|
|
findAllElements(shadowRoot.querySelectorAll("*"));
|
|
}
|
|
findAllElements(root.querySelectorAll("*"));
|
|
}
|
|
return selector ? allElements.filter((el) => el.matches(selector)) : allElements;
|
|
}
|
|
return _querySelectorDeep(s, r || document);
|
|
}
|
|
|
|
// src/utils/index.ts
|
|
var log29 = logger29("webdriverio");
|
|
var INVALID_SELECTOR_ERROR = "selector needs to be typeof `string` or `function`";
|
|
var IGNORED_COMMAND_FILE_EXPORTS = ["SESSION_MOCKS", "CDP_SESSIONS"];
|
|
var scopes = {
|
|
browser: browser_exports,
|
|
element: element_exports
|
|
};
|
|
var applyScopePrototype = (prototype, scope) => {
|
|
Object.entries(scopes[scope]).filter(([exportName]) => !IGNORED_COMMAND_FILE_EXPORTS.includes(exportName)).forEach(([commandName, command]) => {
|
|
prototype[commandName] = { value: command };
|
|
});
|
|
};
|
|
var getPrototype = (scope) => {
|
|
const prototype = {
|
|
/**
|
|
* used to store the puppeteer instance in the browser scope
|
|
*/
|
|
puppeteer: { value: null, writable: true }
|
|
};
|
|
if (scope === "browser") {
|
|
prototype.isNativeContext = {
|
|
get: function() {
|
|
const context = getContextManager(this);
|
|
return context.isNativeContext;
|
|
}
|
|
};
|
|
prototype.mobileContext = {
|
|
get: function() {
|
|
const context = getContextManager(this);
|
|
return context.mobileContext;
|
|
}
|
|
};
|
|
}
|
|
applyScopePrototype(prototype, scope);
|
|
prototype.strategies = { value: /* @__PURE__ */ new Map() };
|
|
return prototype;
|
|
};
|
|
var getElementFromResponse = (res) => {
|
|
if (!res) {
|
|
return null;
|
|
}
|
|
if (res.ELEMENT) {
|
|
return res.ELEMENT;
|
|
}
|
|
if (res[ELEMENT_KEY20]) {
|
|
return res[ELEMENT_KEY20];
|
|
}
|
|
return null;
|
|
};
|
|
function sanitizeCSS(value) {
|
|
if (!value) {
|
|
return value;
|
|
}
|
|
return value.trim().replace(/'/g, "").replace(/"/g, "").toLowerCase();
|
|
}
|
|
function parseCSS(cssPropertyValue, cssProperty) {
|
|
const parsedValue = {
|
|
property: cssProperty,
|
|
value: cssPropertyValue.toLowerCase().trim(),
|
|
parsed: {}
|
|
};
|
|
if (parsedValue.value?.indexOf("rgb") === 0) {
|
|
parsedValue.value = parsedValue.value.replace(/\s/g, "");
|
|
const color = parsedValue.value;
|
|
parsedValue.parsed = rgb2hex(parsedValue.value);
|
|
parsedValue.parsed.type = "color";
|
|
const colorType = /[rgba]+/g.exec(color) || [];
|
|
parsedValue.parsed[colorType[0]] = color;
|
|
} else if (parsedValue.property === "font-family") {
|
|
const font = cssValue(cssPropertyValue);
|
|
const string = parsedValue.value;
|
|
const value = cssPropertyValue.split(/,/).map(sanitizeCSS);
|
|
parsedValue.value = sanitizeCSS(font[0].value || font[0].string);
|
|
parsedValue.parsed = { value, type: "font", string };
|
|
} else {
|
|
try {
|
|
const value = cssValue(cssPropertyValue);
|
|
if (value.length === 1) {
|
|
parsedValue.parsed = value[0];
|
|
}
|
|
if (parsedValue.parsed.type && parsedValue.parsed.type === "number" && parsedValue.parsed.unit === "") {
|
|
parsedValue.value = parsedValue.parsed.value;
|
|
}
|
|
} catch {
|
|
}
|
|
}
|
|
return parsedValue;
|
|
}
|
|
function checkUnicode(value) {
|
|
if (value === Key.Ctrl) {
|
|
return [value];
|
|
}
|
|
if (!Object.prototype.hasOwnProperty.call(UNICODE_CHARACTERS2, value)) {
|
|
return new GraphemeSplitter().splitGraphemes(value);
|
|
}
|
|
return [UNICODE_CHARACTERS2[value]];
|
|
}
|
|
function fetchElementByJSFunction(selector, scope, referenceId) {
|
|
if (!("elementId" in scope)) {
|
|
return scope.execute(selector, referenceId);
|
|
}
|
|
const script = (function(elem, id) {
|
|
return selector.call(elem, id);
|
|
}).toString().replace("selector", `(${selector.toString()})`);
|
|
const args = [scope];
|
|
if (referenceId) {
|
|
args.push(referenceId);
|
|
}
|
|
return getBrowserObject38(scope).executeScript(`return (${script}).apply(null, arguments)`, args);
|
|
}
|
|
function isElement(o) {
|
|
return typeof HTMLElement === "object" ? o instanceof HTMLElement : o && typeof o === "object" && o !== null && o.nodeType === 1 && typeof o.nodeName === "string";
|
|
}
|
|
function isStaleElementError(err) {
|
|
return (
|
|
// Chrome
|
|
err.message.includes("stale element reference") || // Firefox
|
|
err.message.includes("is no longer attached to the DOM") || // Safari
|
|
err.message.toLowerCase().includes("stale element found") || // Chrome through JS execution
|
|
err.message.includes("stale element not found in the current frame") || // BIDI
|
|
err.message.includes("belongs to different document")
|
|
);
|
|
}
|
|
function transformClassicToBidiSelector(using, value) {
|
|
if (using === "css selector" || using === "tag name") {
|
|
return { type: "css", value };
|
|
}
|
|
if (using === "xpath") {
|
|
return { type: "xpath", value };
|
|
}
|
|
if (using === "link text") {
|
|
return { type: "innerText", value };
|
|
}
|
|
if (using === "partial link text") {
|
|
return { type: "innerText", value, matchType: "partial" };
|
|
}
|
|
throw new Error(`Can't transform classic selector ${using} to Bidi selector`);
|
|
}
|
|
async function findDeepElement(selector) {
|
|
const browser = getBrowserObject38(this);
|
|
const shadowRootManager = getShadowRootManager(browser);
|
|
const contextManager = getContextManager(browser);
|
|
const context = await contextManager.getCurrentContext();
|
|
const shadowRoots = shadowRootManager.getShadowElementsByContextId(
|
|
context,
|
|
this.elementId
|
|
);
|
|
const { using, value } = findStrategy(selector, this.isW3C, this.isMobile);
|
|
if (using === "xpath" && (value.startsWith("./") || value.startsWith("..")) && this.elementId) {
|
|
return this.findElementFromElement(this.elementId, using, value);
|
|
}
|
|
const locator = transformClassicToBidiSelector(using, value);
|
|
const startNodes = shadowRoots.length > 0 ? shadowRoots.map((shadowRootNodeId) => ({ sharedId: shadowRootNodeId })) : this.elementId ? [{ sharedId: this.elementId }] : void 0;
|
|
const deepElementResult = await browser.browsingContextLocateNodes({ locator, context, startNodes }).then(async (result) => {
|
|
let nodes = result.nodes.filter((node) => Boolean(node.sharedId)).map((node) => ({
|
|
[ELEMENT_KEY20]: node.sharedId,
|
|
locator
|
|
}));
|
|
nodes = returnUniqueNodes(nodes);
|
|
if (!this.elementId) {
|
|
return nodes[0];
|
|
}
|
|
const scopedNodes = await Promise.all(nodes.map(async (node) => {
|
|
const isIn = await browser.execute(
|
|
elementContains,
|
|
{ [ELEMENT_KEY20]: this.elementId },
|
|
node
|
|
);
|
|
return [isIn, node];
|
|
})).then((elems) => elems.filter(([isIn]) => isIn).map(([, elem]) => elem));
|
|
return scopedNodes[0];
|
|
}, (err) => {
|
|
log29.warn(`Failed to execute browser.browsingContextLocateNodes({ ... }) due to ${err}, falling back to regular WebDriver Classic command`);
|
|
return this && "elementId" in this && this.elementId ? this.findElementFromElement(this.elementId, using, value) : browser.findElement(using, value);
|
|
});
|
|
return deepElementResult;
|
|
}
|
|
async function findDeepElements(selector) {
|
|
const browser = getBrowserObject38(this);
|
|
const shadowRootManager = getShadowRootManager(browser);
|
|
const contextManager = getContextManager(browser);
|
|
const context = await contextManager.getCurrentContext();
|
|
const shadowRoots = shadowRootManager.getShadowElementsByContextId(
|
|
context,
|
|
this.elementId
|
|
);
|
|
const { using, value } = findStrategy(selector, this.isW3C, this.isMobile);
|
|
if (using === "xpath" && (value.startsWith("./") || value.startsWith("..")) && this.elementId) {
|
|
return this.findElementsFromElement(this.elementId, using, value);
|
|
}
|
|
const locator = transformClassicToBidiSelector(using, value);
|
|
const startNodes = shadowRoots.length > 0 ? shadowRoots.map((shadowRootNodeId) => ({ sharedId: shadowRootNodeId })) : this.elementId ? [{ sharedId: this.elementId }] : void 0;
|
|
const deepElementResult = await browser.browsingContextLocateNodes({ locator, context, startNodes }).then(async (result) => {
|
|
let nodes = result.nodes.filter((node) => Boolean(node.sharedId)).map((node) => ({
|
|
[ELEMENT_KEY20]: node.sharedId,
|
|
locator
|
|
}));
|
|
nodes = returnUniqueNodes(nodes);
|
|
if (!this.elementId) {
|
|
return nodes;
|
|
}
|
|
const scopedNodes = await Promise.all(nodes.map(async (node) => {
|
|
const isIn = await browser.execute(
|
|
elementContains,
|
|
{ [ELEMENT_KEY20]: this.elementId },
|
|
node
|
|
);
|
|
return [isIn, node];
|
|
})).then((elems) => elems.filter(([isIn]) => isIn).map(([, elem]) => elem));
|
|
return scopedNodes;
|
|
}, (err) => {
|
|
log29.warn(`Failed to execute browser.browsingContextLocateNodes({ ... }) due to ${err}, falling back to regular WebDriver Classic command`);
|
|
return this && "elementId" in this && this.elementId ? this.findElementsFromElement(this.elementId, using, value) : browser.findElements(using, value);
|
|
});
|
|
return deepElementResult;
|
|
}
|
|
function returnUniqueNodes(nodes) {
|
|
const ids = /* @__PURE__ */ new Set();
|
|
return nodes.filter((node) => !ids.has(node[ELEMENT_KEY20]) && ids.add(node[ELEMENT_KEY20]));
|
|
}
|
|
async function findElement(selector) {
|
|
const browserObject = getBrowserObject38(this);
|
|
const shadowRootManager = getShadowRootManager(browserObject);
|
|
if (this.isBidi && typeof selector === "string" && !selector.startsWith(DEEP_SELECTOR) && !shadowRootManager.isWithinFrame()) {
|
|
const notFoundError = new Error(`Couldn't find element with selector "${selector}"`);
|
|
const elem = await findDeepElement.call(this, selector);
|
|
return getElementFromResponse(elem) ? elem : notFoundError;
|
|
}
|
|
if (typeof selector === "string" && selector.startsWith(DEEP_SELECTOR)) {
|
|
const notFoundError = new Error(`shadow selector "${selector.slice(DEEP_SELECTOR.length)}" did not return an HTMLElement`);
|
|
let elem = await browserObject.execute(
|
|
querySelectorAllDeep,
|
|
false,
|
|
selector.slice(DEEP_SELECTOR.length),
|
|
// hard conversion from element id to Element is done by browser driver
|
|
this.elementId ? this : void 0
|
|
);
|
|
elem = Array.isArray(elem) ? elem[0] : elem;
|
|
return getElementFromResponse(elem) ? elem : notFoundError;
|
|
}
|
|
if (selector && typeof selector === "object" && typeof selector.strategy === "function") {
|
|
const { strategy, strategyName, strategyArguments } = selector;
|
|
const notFoundError = new Error(`Custom Strategy "${strategyName}" did not return an HTMLElement`);
|
|
let elem = await browserObject.execute(strategy, ...strategyArguments);
|
|
elem = Array.isArray(elem) ? elem[0] : elem;
|
|
return getElementFromResponse(elem) ? elem : notFoundError;
|
|
}
|
|
if (typeof selector === "string" || isPlainObject(selector)) {
|
|
const { using, value } = findStrategy(selector, this.isW3C, this.isMobile);
|
|
return this.elementId ? this.findElementFromElement(this.elementId, using, value) : this.findElement(using, value);
|
|
}
|
|
if (typeof selector === "function") {
|
|
const notFoundError = new Error(`Function selector "${selector.toString()}" did not return an HTMLElement`);
|
|
let elem = await fetchElementByJSFunction(selector, this);
|
|
elem = Array.isArray(elem) ? elem[0] : elem;
|
|
return getElementFromResponse(elem) ? elem : notFoundError;
|
|
}
|
|
if (isElement(selector)) {
|
|
if (!window.__wdio_element) {
|
|
window.__wdio_element = {};
|
|
}
|
|
const notFoundError = new Error("DOM Node couldn't be found anymore");
|
|
const uid = Math.random().toString().slice(2);
|
|
window.__wdio_element[uid] = selector;
|
|
selector = ((id) => window.__wdio_element[id]);
|
|
let elem = await fetchElementByJSFunction(selector, this, uid).catch((err) => {
|
|
if (isStaleElementError(err)) {
|
|
return void 0;
|
|
}
|
|
throw err;
|
|
});
|
|
elem = Array.isArray(elem) ? elem[0] : elem;
|
|
return getElementFromResponse(elem) ? elem : notFoundError;
|
|
}
|
|
throw new Error(`${INVALID_SELECTOR_ERROR}, but found: \`${typeof selector}\``);
|
|
}
|
|
async function findElements(selector) {
|
|
const browserObject = getBrowserObject38(this);
|
|
if (typeof selector === "string" && selector.startsWith(DEEP_SELECTOR)) {
|
|
const elems = await browserObject.execute(
|
|
querySelectorAllDeep,
|
|
true,
|
|
selector.slice(DEEP_SELECTOR.length),
|
|
// hard conversion from element id to Element is done by browser driver
|
|
this.elementId ? this : void 0
|
|
);
|
|
const elemArray = Array.isArray(elems) ? elems : [elems];
|
|
return elemArray.filter((elem) => elem && getElementFromResponse(elem));
|
|
}
|
|
if (isPlainObject(selector) && typeof selector.strategy === "function") {
|
|
const { strategy, strategyArguments } = selector;
|
|
const elems = await browserObject.execute(strategy, ...strategyArguments);
|
|
const elemArray = Array.isArray(elems) ? elems : [elems];
|
|
return elemArray.filter((elem) => elem && getElementFromResponse(elem));
|
|
}
|
|
if (typeof selector === "string" || isPlainObject(selector)) {
|
|
const { using, value } = findStrategy(selector, this.isW3C, this.isMobile);
|
|
return this.elementId ? this.findElementsFromElement(this.elementId, using, value) : this.findElements(using, value);
|
|
}
|
|
if (typeof selector === "function") {
|
|
const elems = await fetchElementByJSFunction(selector, this);
|
|
const elemArray = Array.isArray(elems) ? elems : [elems];
|
|
return elemArray.filter((elem) => elem && getElementFromResponse(elem));
|
|
}
|
|
throw new Error(`${INVALID_SELECTOR_ERROR}, but found: \`${typeof selector}\``);
|
|
}
|
|
function verifyArgsAndStripIfElement(args) {
|
|
function verify(arg) {
|
|
if (arg && typeof arg === "object" && arg.constructor.name === "Element") {
|
|
const elem = arg;
|
|
if (!elem.elementId) {
|
|
throw new Error(`The element with selector "${elem.selector}" you are trying to pass into the execute method wasn't found`);
|
|
}
|
|
return {
|
|
[ELEMENT_KEY20]: elem.elementId,
|
|
ELEMENT: elem.elementId
|
|
};
|
|
}
|
|
return arg;
|
|
}
|
|
return !Array.isArray(args) ? verify(args) : args.map(verify);
|
|
}
|
|
async function getElementRect(scope) {
|
|
const rect = await scope.getElementRect(scope.elementId);
|
|
const defaults = { x: 0, y: 0, width: 0, height: 0 };
|
|
if (Object.keys(defaults).some((key) => rect[key] === void 0)) {
|
|
const rectJs = await getBrowserObject38(scope).execute(function(el) {
|
|
if (!el || !el.getBoundingClientRect) {
|
|
return;
|
|
}
|
|
const { left, top, width, height } = el.getBoundingClientRect();
|
|
return {
|
|
x: left + this.scrollX,
|
|
y: top + this.scrollY,
|
|
width,
|
|
height
|
|
};
|
|
}, scope);
|
|
Object.keys(defaults).forEach((key) => {
|
|
if (typeof rect[key] !== "undefined") {
|
|
return;
|
|
}
|
|
if (rectJs && typeof rectJs[key] === "number") {
|
|
rect[key] = Math.floor(rectJs[key]);
|
|
} else {
|
|
log29.error("getElementRect", { rect, rectJs, key });
|
|
throw new Error("Failed to receive element rects via execute command");
|
|
}
|
|
});
|
|
}
|
|
return rect;
|
|
}
|
|
function validateUrl(url2, origError) {
|
|
try {
|
|
const urlObject = new URL(url2);
|
|
return urlObject.href;
|
|
} catch {
|
|
if (origError) {
|
|
throw origError;
|
|
}
|
|
return validateUrl(`http://${url2}`, new Error(`Invalid URL: ${url2}`));
|
|
}
|
|
}
|
|
async function hasElementId(element) {
|
|
if (!element.elementId) {
|
|
const command = element.isReactElement ? element.parent.react$.bind(element.parent) : element.isShadowElement ? element.parent.shadow$.bind(element.parent) : element.parent.$.bind(element.parent);
|
|
element.elementId = (await command(element.selector).getElement()).elementId;
|
|
}
|
|
if (!element.elementId) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
function addLocatorStrategyHandler(scope) {
|
|
return (name, func) => {
|
|
if (scope.strategies.get(name)) {
|
|
throw new Error(`Strategy ${name} already exists`);
|
|
}
|
|
scope.strategies.set(name, func);
|
|
};
|
|
}
|
|
var enhanceElementsArray = (elements, parent, selector, foundWith = "$$", props = []) => {
|
|
const elementArray = elements;
|
|
if (!Array.isArray(selector)) {
|
|
elementArray.selector = selector;
|
|
}
|
|
const elems = selector;
|
|
if (Array.isArray(selector) && elems.length && elems.every((elem) => elem.selector && elem.selector === elems[0].selector)) {
|
|
elementArray.selector = elems[0].selector;
|
|
}
|
|
for (const [name, fn] of Object.entries(asyncIterators)) {
|
|
elementArray[name] = fn.bind(null, elementArray);
|
|
}
|
|
elementArray.parent = parent;
|
|
elementArray.foundWith = foundWith;
|
|
elementArray.props = props;
|
|
elementArray.getElements = async () => elementArray;
|
|
return elementArray;
|
|
};
|
|
var isStub = (automationProtocol) => automationProtocol === "./protocol-stub.js";
|
|
function createFunctionDeclarationFromString(userScript) {
|
|
if (typeof userScript === "string") {
|
|
return `(${SCRIPT_PREFIX}function () {
|
|
${userScript.toString()}
|
|
}${SCRIPT_SUFFIX}).apply(this, arguments);`;
|
|
}
|
|
return new Function(`return (${SCRIPT_PREFIX}${userScript.toString()}${SCRIPT_SUFFIX}).apply(this, arguments);`).toString();
|
|
}
|
|
|
|
// src/middlewares.ts
|
|
var IMPLICIT_WAIT_EXCLUSION_LIST = ["getElement", "getElements", "emit"];
|
|
var elementErrorHandler = (fn) => (commandName, commandFn) => {
|
|
return function elementErrorHandlerCallback(...args) {
|
|
return fn(commandName, async function elementErrorHandlerCallbackFn() {
|
|
if (IMPLICIT_WAIT_EXCLUSION_LIST.includes(commandName)) {
|
|
return fn(commandName, commandFn).apply(this, args);
|
|
}
|
|
const element = await implicitWait(this, commandName);
|
|
this.elementId = element.elementId;
|
|
this[ELEMENT_KEY21] = element.elementId;
|
|
try {
|
|
const result = await fn(commandName, commandFn).apply(this, args);
|
|
const caps = getBrowserObject39(this).capabilities;
|
|
if (caps?.browserName === "safari" && result?.error === "no such element") {
|
|
const errorName = "stale element reference";
|
|
const err = new Error(errorName);
|
|
err.name = errorName;
|
|
throw err;
|
|
}
|
|
return result;
|
|
} catch (_err) {
|
|
const err = _err;
|
|
if (err.name === "element not interactable") {
|
|
try {
|
|
await element.waitForClickable();
|
|
return await fn(commandName, commandFn).apply(this, args);
|
|
} catch {
|
|
const elementHTML = await element.getHTML();
|
|
err.name = "webdriverio(middleware): element did not become interactable";
|
|
err.message = `Element ${elementHTML} did not become interactable`;
|
|
err.stack = err.stack ?? Error.captureStackTrace(err) ?? "";
|
|
}
|
|
}
|
|
if (err.name === "stale element reference" || isStaleElementError(err)) {
|
|
const element2 = await refetchElement(this, commandName);
|
|
this.elementId = element2.elementId;
|
|
this.parent = element2.parent;
|
|
return await fn(commandName, commandFn).apply(this, args);
|
|
}
|
|
throw err;
|
|
}
|
|
}).apply(this);
|
|
};
|
|
};
|
|
var multiremoteHandler = (wrapCommand4) => (commandName) => {
|
|
return wrapCommand4(commandName, function(...args) {
|
|
const commandResults = this.instances.map((instanceName) => {
|
|
return this[instanceName][commandName](...args);
|
|
});
|
|
return Promise.all(commandResults);
|
|
});
|
|
};
|
|
|
|
// src/multiremote.ts
|
|
var MultiRemote = class _MultiRemote {
|
|
instances = {};
|
|
baseInstance;
|
|
sessionId;
|
|
/**
|
|
* add instance to multibrowser instance
|
|
*/
|
|
async addInstance(browserName, client) {
|
|
this.instances[browserName] = client;
|
|
return this.instances[browserName];
|
|
}
|
|
/**
|
|
* modifier for multibrowser instance
|
|
*/
|
|
modifier(wrapperClient) {
|
|
const propertiesObject = {};
|
|
propertiesObject.commandList = { value: wrapperClient.commandList };
|
|
propertiesObject.options = { value: wrapperClient.options };
|
|
propertiesObject.getInstance = {
|
|
value: (browserName) => this.instances[browserName]
|
|
};
|
|
for (const commandName of wrapperClient.commandList) {
|
|
propertiesObject[commandName] = {
|
|
value: this.commandWrapper(commandName),
|
|
configurable: true
|
|
};
|
|
}
|
|
propertiesObject.__propertiesObject__ = {
|
|
value: propertiesObject
|
|
};
|
|
this.baseInstance = new MultiRemoteDriver(this.instances, propertiesObject);
|
|
const client = Object.create(this.baseInstance, propertiesObject);
|
|
for (const [identifier, instance] of Object.entries(this.instances)) {
|
|
client[identifier] = instance;
|
|
}
|
|
return client;
|
|
}
|
|
/**
|
|
* helper method to generate element objects from results, so that we can call, e.g.
|
|
*
|
|
* ```
|
|
* const elem = $('#elem')
|
|
* elem.getHTML()
|
|
* ```
|
|
*
|
|
* or in case multiremote is used
|
|
*
|
|
* ```
|
|
* const elems = $$('div')
|
|
* elems[0].getHTML()
|
|
* ```
|
|
*/
|
|
static elementWrapper(instances, result, propertiesObject, scope) {
|
|
const prototype = { ...propertiesObject, ...clone2(getPrototype("element")), scope: { value: "element" } };
|
|
const element = webdriverMonad2({}, (client) => {
|
|
for (const [i, identifier] of Object.entries(Object.keys(instances))) {
|
|
client[identifier] = result[i];
|
|
}
|
|
client.instances = Object.keys(instances);
|
|
client.isMultiremote = true;
|
|
client.selector = Array.isArray(result) && result[0] ? result[0].selector : null;
|
|
delete client.sessionId;
|
|
return client;
|
|
}, prototype);
|
|
return element(this.sessionId, multiremoteHandler(scope.commandWrapper.bind(scope)));
|
|
}
|
|
/**
|
|
* handle commands for multiremote instances
|
|
*/
|
|
commandWrapper(commandName) {
|
|
const instances = this.instances;
|
|
const self = this;
|
|
if (commandName === "getInstance") {
|
|
return function(browserName) {
|
|
if (!this[browserName]) {
|
|
throw new Error(`Multiremote object has no instance named "${browserName}"`);
|
|
}
|
|
return this[browserName];
|
|
};
|
|
}
|
|
return wrapCommand2(commandName, async function(...args) {
|
|
const mElem = this;
|
|
const scope = this.selector ? Object.entries(mElem.instances.reduce((ins, instanceName) => (
|
|
// @ts-expect-error ToDo(Christian): deprecate
|
|
{ ...ins, [instanceName]: mElem[instanceName] }
|
|
), {})) : Object.entries(instances);
|
|
const result = await Promise.all(
|
|
scope.map(
|
|
([, instance]) => instance[commandName](...args)
|
|
)
|
|
);
|
|
if (commandName === "$") {
|
|
const elem = _MultiRemote.elementWrapper(instances, result, this.__propertiesObject__, self);
|
|
return elem;
|
|
} else if (commandName === "$$") {
|
|
const zippedResult = zip(...result);
|
|
return zippedResult.map((singleResult) => _MultiRemote.elementWrapper(instances, singleResult, this.__propertiesObject__, self));
|
|
}
|
|
return result;
|
|
});
|
|
}
|
|
};
|
|
var MultiRemoteDriver = class {
|
|
instances;
|
|
isMultiremote = true;
|
|
__propertiesObject__;
|
|
constructor(instances, propertiesObject) {
|
|
this.instances = Object.keys(instances);
|
|
this.__propertiesObject__ = propertiesObject;
|
|
}
|
|
on(eventName, emitter) {
|
|
this.instances.forEach((instanceName) => this.getInstance(instanceName).on(eventName, emitter));
|
|
return void 0;
|
|
}
|
|
once(eventName, emitter) {
|
|
this.instances.forEach((instanceName) => this.getInstance(instanceName).once(eventName, emitter));
|
|
return void 0;
|
|
}
|
|
emit(eventName, emitter) {
|
|
return this.instances.map(
|
|
(instanceName) => this.getInstance(instanceName).emit(eventName, emitter)
|
|
).some(Boolean);
|
|
}
|
|
eventNames() {
|
|
return this.instances.map(
|
|
(instanceName) => this.getInstance(instanceName).eventNames()
|
|
);
|
|
}
|
|
getMaxListeners() {
|
|
return this.instances.map(
|
|
(instanceName) => this.getInstance(instanceName).getMaxListeners()
|
|
);
|
|
}
|
|
listenerCount(eventName) {
|
|
return this.instances.map(
|
|
(instanceName) => this.getInstance(instanceName).listenerCount(eventName)
|
|
);
|
|
}
|
|
listeners(eventName) {
|
|
return this.instances.map(
|
|
(instanceName) => this.getInstance(instanceName).listeners(eventName)
|
|
).reduce((prev, cur) => {
|
|
prev.concat(cur);
|
|
return prev;
|
|
}, []);
|
|
}
|
|
removeListener(eventName, emitter) {
|
|
this.instances.forEach((instanceName) => this.getInstance(instanceName).removeListener(eventName, emitter));
|
|
return void 0;
|
|
}
|
|
removeAllListeners(eventName) {
|
|
this.instances.forEach((instanceName) => this.getInstance(instanceName).removeAllListeners(eventName));
|
|
return void 0;
|
|
}
|
|
};
|
|
|
|
// src/utils/SevereServiceError.ts
|
|
var SevereServiceError = class extends Error {
|
|
constructor(message = "Severe Service Error occurred.") {
|
|
super(message);
|
|
this.name = "SevereServiceError";
|
|
}
|
|
};
|
|
|
|
// src/utils/detectBackend.ts
|
|
var DEFAULT_HOSTNAME = "127.0.0.1";
|
|
var DEFAULT_PORT = 4444;
|
|
var DEFAULT_PROTOCOL = "http";
|
|
var DEFAULT_PATH = "/";
|
|
var LEGACY_PATH = "/wd/hub";
|
|
var REGION_MAPPING = {
|
|
"us": "us-west-1.",
|
|
// default endpoint
|
|
"eu": "eu-central-1.",
|
|
"eu-central-1": "eu-central-1.",
|
|
"us-east-4": "us-east-4."
|
|
};
|
|
function getSauceEndpoint(region, { isRDC, isVisual } = {}) {
|
|
const shortRegion = REGION_MAPPING[region] ? region : "us";
|
|
if (isRDC) {
|
|
return `${shortRegion}1.appium.testobject.com`;
|
|
} else if (isVisual) {
|
|
return "hub.screener.io";
|
|
}
|
|
return `ondemand.${REGION_MAPPING[shortRegion]}saucelabs.com`;
|
|
}
|
|
function detectBackend(options = {}) {
|
|
const { port, hostname, user, key, protocol, region, path: path8, capabilities } = options;
|
|
if (typeof user === "string" && typeof key === "string" && key.length === 20) {
|
|
return {
|
|
protocol: protocol || "https",
|
|
hostname: hostname || "hub-cloud.browserstack.com",
|
|
port: port || 443,
|
|
path: path8 || LEGACY_PATH
|
|
};
|
|
}
|
|
if (typeof user === "string" && typeof key === "string" && key.length === 32) {
|
|
return {
|
|
protocol: protocol || "https",
|
|
hostname: hostname || "hub.testingbot.com",
|
|
port: port || 443,
|
|
path: path8 || LEGACY_PATH
|
|
};
|
|
}
|
|
const isVisual = Boolean(!Array.isArray(capabilities) && capabilities && capabilities["sauce:visual"]?.apiKey);
|
|
if (typeof user === "string" && typeof key === "string" && key.length === 36 || // Or only RDC or visual
|
|
isVisual) {
|
|
const sauceRegion = region;
|
|
return {
|
|
protocol: protocol || "https",
|
|
hostname: hostname || getSauceEndpoint(sauceRegion, { isVisual }),
|
|
port: port || 443,
|
|
path: path8 || LEGACY_PATH
|
|
};
|
|
}
|
|
if (typeof user === "string" && typeof key === "string" && key.length === 50) {
|
|
return {
|
|
protocol: protocol || DEFAULT_PROTOCOL,
|
|
hostname: hostname || "hub.lambdatest.com",
|
|
port: port || 80,
|
|
path: path8 || LEGACY_PATH
|
|
};
|
|
}
|
|
if (
|
|
/**
|
|
* user and key are set in config
|
|
*/
|
|
(typeof user === "string" || typeof key === "string") && /**
|
|
* but no custom WebDriver endpoint was configured
|
|
*/
|
|
!hostname
|
|
) {
|
|
throw new Error(
|
|
'A "user" or "key" was provided but could not be connected to a known cloud service (Sauce Labs, Browerstack, Testingbot or Lambdatest). Please check if given user and key properties are correct!'
|
|
);
|
|
}
|
|
if (hostname || port || protocol || path8) {
|
|
return {
|
|
hostname: hostname || DEFAULT_HOSTNAME,
|
|
port: port || DEFAULT_PORT,
|
|
protocol: protocol || DEFAULT_PROTOCOL,
|
|
path: path8 || DEFAULT_PATH
|
|
};
|
|
}
|
|
return { hostname, port, protocol, path: path8 };
|
|
}
|
|
|
|
// src/protocol-stub.ts
|
|
import { capabilitiesEnvironmentDetector } from "@wdio/utils";
|
|
var NOOP2 = () => {
|
|
};
|
|
var ProtocolStub = class {
|
|
static async newSession(options) {
|
|
const capabilities = emulateSessionCapabilities(options.capabilities);
|
|
const browser = {
|
|
options,
|
|
capabilities,
|
|
requestedCapabilities: capabilities,
|
|
customCommands: [],
|
|
// internally used to transfer custom commands to the actual protocol instance
|
|
overwrittenCommands: [],
|
|
// internally used to transfer overwritten commands to the actual protocol instance
|
|
commandList: [],
|
|
getWindowHandle: NOOP2,
|
|
on: NOOP2,
|
|
off: NOOP2,
|
|
addCommand: NOOP2,
|
|
overwriteCommand: NOOP2,
|
|
...capabilitiesEnvironmentDetector(capabilities)
|
|
};
|
|
browser.addCommand = (...args) => browser.customCommands.push(args);
|
|
browser.overwriteCommand = (...args) => browser.overwrittenCommands.push(args);
|
|
return browser;
|
|
}
|
|
/**
|
|
* added just in case user wants to somehow reload webdriver before it was started.
|
|
*/
|
|
static reloadSession() {
|
|
throw new Error("Protocol Stub: Make sure to start the session before reloading it.");
|
|
}
|
|
static attachToSession(options, modifier) {
|
|
if (options || !modifier) {
|
|
throw new Error("You are trying to attach to a protocol stub, this should never occur, please file an issue.");
|
|
}
|
|
return modifier({
|
|
commandList: []
|
|
});
|
|
}
|
|
};
|
|
function emulateSessionCapabilities(caps) {
|
|
const capabilities = {};
|
|
Object.entries(caps).forEach(([key, value]) => {
|
|
const newKey = key.replace("appium:", "");
|
|
capabilities[newKey] = value;
|
|
});
|
|
const c = "alwaysMatch" in caps ? caps.alwaysMatch : caps;
|
|
if (c.browserName && c.browserName.toLowerCase() === "chrome") {
|
|
capabilities["goog:chromeOptions"] = {};
|
|
}
|
|
return capabilities;
|
|
}
|
|
|
|
// src/utils/driver.ts
|
|
var webdriverImport;
|
|
async function getProtocolDriver(options) {
|
|
if (isStub(options.automationProtocol)) {
|
|
return { Driver: ProtocolStub, options };
|
|
}
|
|
if (typeof options.user === "string" && typeof options.key === "string") {
|
|
Object.assign(options, detectBackend(options));
|
|
}
|
|
const Driver = webdriverImport || (await import(
|
|
/* @vite-ignore */
|
|
options.automationProtocol || "webdriver"
|
|
)).default;
|
|
return { Driver, options };
|
|
}
|
|
|
|
// src/index.ts
|
|
var Key2 = Key;
|
|
var SevereServiceError2 = SevereServiceError;
|
|
var remote = async function(params, remoteModifier) {
|
|
const keysToKeep = Object.keys(environment.value.variables.WDIO_WORKER_ID ? params : DEFAULTS);
|
|
const config = validateConfig(WDIO_DEFAULTS, params, keysToKeep);
|
|
await enableFileLogging(config.outputDir);
|
|
logger30.setLogLevelsConfig(config.logLevels, config.logLevel);
|
|
const modifier = (client, options2) => {
|
|
Object.assign(options2, Object.entries(config).reduce((a, [k, v]) => typeof v === "undefined" ? a : { ...a, [k]: v }, {}));
|
|
if (typeof remoteModifier === "function") {
|
|
client = remoteModifier(client, options2);
|
|
}
|
|
return client;
|
|
};
|
|
const { Driver, options } = await getProtocolDriver({ ...params, ...config });
|
|
const prototype = getPrototype("browser");
|
|
const instance = await Driver.newSession(options, modifier, prototype, wrapCommand3, IMPLICIT_WAIT_EXCLUSION_LIST);
|
|
if (params.framework && !isStub(params.automationProtocol)) {
|
|
instance.addCommand = instance.addCommand.bind(instance);
|
|
instance.overwriteCommand = instance.overwriteCommand.bind(instance);
|
|
}
|
|
instance.addLocatorStrategy = addLocatorStrategyHandler(instance);
|
|
if (!isStub(options.automationProtocol)) {
|
|
await registerSessionManager(instance);
|
|
}
|
|
return instance;
|
|
};
|
|
var attach = async function(attachOptions) {
|
|
const params = {
|
|
automationProtocol: "webdriver",
|
|
...detectBackend(attachOptions.options),
|
|
...attachOptions.options,
|
|
...attachOptions,
|
|
capabilities: attachOptions.capabilities || {},
|
|
requestedCapabilities: attachOptions.requestedCapabilities || {}
|
|
};
|
|
const prototype = getPrototype("browser");
|
|
const { Driver } = await getProtocolDriver(params);
|
|
const driver = Driver.attachToSession(
|
|
params,
|
|
void 0,
|
|
prototype,
|
|
wrapCommand3
|
|
);
|
|
driver.addLocatorStrategy = addLocatorStrategyHandler(driver);
|
|
if (isBidi(driver.capabilities) && "_bidiHandler" in driver) {
|
|
await driver["_bidiHandler"].waitForConnected();
|
|
}
|
|
await registerSessionManager(driver);
|
|
return driver;
|
|
};
|
|
var multiremote = async function(params, { automationProtocol } = {}) {
|
|
const multibrowser = new MultiRemote();
|
|
const browserNames = Object.keys(params);
|
|
await Promise.all(
|
|
browserNames.map(async (browserName) => {
|
|
const instance = await remote(params[browserName]);
|
|
return multibrowser.addInstance(browserName, instance);
|
|
})
|
|
);
|
|
const prototype = getPrototype("browser");
|
|
const sessionParams = isStub(automationProtocol) ? void 0 : {
|
|
sessionId: "",
|
|
isW3C: multibrowser.instances[browserNames[0]].isW3C,
|
|
logLevel: multibrowser.instances[browserNames[0]].options.logLevel
|
|
};
|
|
const ProtocolDriver = typeof automationProtocol === "string" ? (await import(
|
|
/* @vite-ignore */
|
|
automationProtocol
|
|
)).default : WebDriver;
|
|
const driver = ProtocolDriver.attachToSession(
|
|
sessionParams,
|
|
multibrowser.modifier.bind(multibrowser),
|
|
prototype,
|
|
wrapCommand3
|
|
);
|
|
if (!isStub(automationProtocol)) {
|
|
const origAddCommand = driver.addCommand.bind(driver);
|
|
driver.addCommand = function(name, fn, attachToElementOrOptions) {
|
|
const options = typeof attachToElementOrOptions === "object" && attachToElementOrOptions !== null ? attachToElementOrOptions : { attachToElement: attachToElementOrOptions };
|
|
driver.instances.forEach(
|
|
(instanceName) => driver.getInstance(instanceName).addCommand(name, fn, options)
|
|
);
|
|
return origAddCommand(
|
|
name,
|
|
fn,
|
|
{
|
|
attachToElement: options.attachToElement,
|
|
proto: Object.getPrototypeOf(multibrowser.baseInstance),
|
|
instances: multibrowser.instances
|
|
}
|
|
);
|
|
};
|
|
const origOverwriteCommand = driver.overwriteCommand.bind(driver);
|
|
driver.overwriteCommand = (name, fn, attachToElement) => {
|
|
return origOverwriteCommand(
|
|
name,
|
|
fn,
|
|
attachToElement,
|
|
Object.getPrototypeOf(multibrowser.baseInstance),
|
|
multibrowser.instances
|
|
);
|
|
};
|
|
}
|
|
driver.addLocatorStrategy = addLocatorStrategyHandler(driver);
|
|
return driver;
|
|
};
|
|
|
|
// src/node.ts
|
|
environment.value = {
|
|
osType: () => os.type(),
|
|
readFileSync: fs8.readFileSync,
|
|
downloadFile,
|
|
savePDF,
|
|
saveRecordingScreen,
|
|
uploadFile,
|
|
saveScreenshot,
|
|
saveElementScreenshot,
|
|
variables: process2.env
|
|
};
|
|
export {
|
|
Key2 as Key,
|
|
KeyAction,
|
|
MobileScrollDirection,
|
|
PointerAction,
|
|
SevereServiceError2 as SevereServiceError,
|
|
WheelAction,
|
|
attach,
|
|
buttonValue,
|
|
multiremote,
|
|
remote
|
|
};
|
|
/*!
|
|
* ensure that timeout and interval are set properly
|
|
*/
|