webdriverio
Version:
Next-gen browser and mobile automation test framework for Node.js
1,504 lines (1,472 loc) • 284 kB
JavaScript
var __defProp = Object.defineProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
// src/index.ts
import logger25 from "@wdio/logger";
import WebDriver, { DEFAULTS } from "webdriver";
import { validateConfig } from "@wdio/config";
import { enableFileLogging, wrapCommand as wrapCommand3 } 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_KEY19 } from "webdriver";
import { getBrowserObject as getBrowserObject36 } from "@wdio/utils";
// src/utils/implicitWait.ts
import logger from "@wdio/logger";
import { getBrowserObject } from "@wdio/utils";
var log = logger("webdriverio");
async function implicitWait(currentElement, commandName) {
const browser = getBrowserObject(currentElement);
const skipForMobileScroll = browser.isMobile && await browser.isNativeContext && (commandName === "scrollIntoView" || commandName === "tap");
if (!currentElement.elementId && !commandName.match(/(waitUntil|waitFor|isExisting|is?\w+Displayed|is?\w+Clickable)/) && !skipForMobileScroll) {
log.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 fs12 from "node:fs/promises";
import path3 from "node:path";
import { URL as URL2 } from "node:url";
import cssValue from "css-value";
import rgb2hex from "rgb2hex";
import GraphemeSplitter from "grapheme-splitter";
import logger24 from "@wdio/logger";
import isPlainObject from "is-plain-obj";
import { ELEMENT_KEY as ELEMENT_KEY18 } from "webdriver";
import { UNICODE_CHARACTERS as UNICODE_CHARACTERS2, asyncIterators, getBrowserObject as getBrowserObject35 } 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,
deleteCookies: () => deleteCookies,
downloadFile: () => downloadFile,
emulate: () => emulate,
execute: () => execute,
executeAsync: () => executeAsync,
getCookies: () => getCookies,
getPuppeteer: () => getPuppeteer,
getWindowSize: () => getWindowSize,
keys: () => keys,
mock: () => mock,
mockClearAll: () => mockClearAll,
mockRestoreAll: () => mockRestoreAll,
newWindow: () => newWindow,
pause: () => pause,
react$: () => react$,
react$$: () => react$$,
reloadSession: () => reloadSession,
restore: () => restore,
savePDF: () => savePDF,
saveRecordingScreen: () => saveRecordingScreen,
saveScreenshot: () => saveScreenshot,
scroll: () => scroll,
setCookies: () => setCookies,
setTimeout: () => setTimeout2,
setViewport: () => setViewport,
setWindowSize: () => setWindowSize,
swipe: () => swipe,
switchFrame: () => switchFrame,
switchWindow: () => switchWindow,
tap: () => tap,
throttle: () => throttle,
throttleCPU: () => throttleCPU,
throttleNetwork: () => throttleNetwork,
touchAction: () => touchAction2,
uploadFile: () => uploadFile,
url: () => url3,
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 getBrowserObject2 } 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 = getBrowserObject2(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 = getBrowserObject2(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 SupportedAutomationProtocols = /* @__PURE__ */ ((SupportedAutomationProtocols2) => {
SupportedAutomationProtocols2["webdriver"] = "webdriver";
SupportedAutomationProtocols2["stub"] = "./protocol-stub.js";
return SupportedAutomationProtocols2;
})(SupportedAutomationProtocols || {});
var WDIO_DEFAULTS = {
/**
* allows to specify automation protocol
*/
automationProtocol: {
type: "string",
default: "webdriver" /* webdriver */,
validate: (param) => {
if (param.endsWith("driver.js")) {
return;
}
if (!Object.values(SupportedAutomationProtocols).includes(param.toLowerCase())) {
throw new Error(`Currently only "webdriver" and "devtools" is supported as automationProtocol, you set "${param}"`);
}
}
},
/**
* 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: 500
},
/**
* 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/key.ts
import os from "node:os";
// src/utils/actions/base.ts
import { ELEMENT_KEY as ELEMENT_KEY3 } from "webdriver";
var actionIds = 0;
var BaseAction = class {
constructor(instance, type, params) {
this.instance = instance;
this.#instance = instance;
this.#id = params?.id || `action${++actionIds}`;
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) && os.type().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/commands/browser/addInitScript.ts
import { EventEmitter } from "node:events";
// 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 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 fn = `(emit) => {
const closure = new Function(\`return ${script.toString()}\`)
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 emitter = new EventEmitter();
const messageHandler = (msg) => {
if (msg.channel === channel) {
emitter.emit("data", deserialize(msg.data));
}
};
this.on("script.message", messageHandler);
const resetFn = () => {
this.off("script.message", messageHandler);
return this.scriptRemovePreloadScript({ script: result.script });
};
const returnVal = {
remove: resetFn,
on: emitter.on.bind(emitter)
};
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;
if (!process.env.WDIO_WORKER_ID || typeof process.send !== "function") {
console.log(WDIORepl.introMessage);
const context = {
browser: this,
driver: this,
$: this.$.bind(this),
$$: this.$$.bind(this)
};
return repl.start(context);
}
process._debugProcess(process.pid);
process.send({
origin: "debugger",
name: "start",
params: { commandTimeout, introMessage }
});
let commandResolve = (
/* istanbul ignore next */
() => {
}
);
process.on("message", (m) => {
if (m.origin !== "debugger") {
return;
}
if (m.name === "stop") {
process._debugEnd(process.pid);
return commandResolve();
}
if (m.name === "eval") {
repl.eval(m.content.cmd, global, void 0, (err, result) => {
if (typeof process.send !== "function") {
return;
}
if (err) {
process.send({
origin: "debugger",
name: "result",
params: {
error: true,
...serializeError(err)
}
});
}
if (typeof result === "function") {
result = `[Function: ${result.name}]`;
}
process.send({
origin: "debugger",
name: "result",
params: { result }
});
});
}
});
return new Promise((resolve5) => commandResolve = resolve5);
}
// src/commands/browser/deleteCookies.ts
async function deleteCookies(filter) {
const filterArray = typeof filter === "undefined" ? void 0 : Array.isArray(filter) ? filter : [filter];
if (!this.isBidi) {
const names = 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}"`);
});
await deleteCookiesClassic.call(this, names);
return;
}
if (!filterArray) {
await this.storageDeleteCookies({});
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 })));
return;
}
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
import fs from "node:fs";
import path from "node:path";
import JSZip from "jszip";
import logger2 from "@wdio/logger";
var log2 = logger2("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);
log2.info(`File extracted: ${keys2[i]}`);
filesData.push(dir);
}
} catch (error) {
log2.error("Error unzipping file:", error);
}
return Promise.resolve({
files: filesData
});
}
// src/clock.ts
import logger3 from "@wdio/logger";
var log3 = logger3("webdriverio:ClockManager");
function installFakeTimers(options) {
window.__clock = window.__wdio_sinon.install(options);
}
function uninstallFakeTimers() {
window.__clock.uninstall();
}
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 log3.warn("Fake timers are already installed");
}
if (globalThis.window) {
return;
}
const url6 = await import("node:url");
const path4 = await import("node:path");
const fs13 = await import("node:fs/promises");
const __dirname = path4.dirname(url6.fileURLToPath(import.meta.url));
const rootDir = path4.resolve(__dirname, "..");
const emulateOptions = options || {};
const scriptPath = path4.join(rootDir, "third_party", "fake-timers.js");
const functionDeclaration = await fs13.readFile(scriptPath, "utf-8");
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: 8