appium-xcuitest-driver
Version:
Appium driver for iOS using XCUITest for backend
952 lines • 92.6 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.XCUITestDriver = void 0;
const appium_idb_1 = __importDefault(require("appium-idb"));
const appium_ios_simulator_1 = require("appium-ios-simulator");
const appium_webdriveragent_1 = require("appium-webdriveragent");
const driver_1 = require("appium/driver");
const support_1 = require("appium/support");
const async_lock_1 = __importDefault(require("async-lock"));
const asyncbox_1 = require("asyncbox");
const bluebird_1 = __importDefault(require("bluebird"));
const lodash_1 = __importDefault(require("lodash"));
const lru_cache_1 = require("lru-cache");
const node_events_1 = __importDefault(require("node:events"));
const node_path_1 = __importDefault(require("node:path"));
const node_url_1 = __importDefault(require("node:url"));
const app_utils_1 = require("./app-utils");
const commands_1 = __importDefault(require("./commands"));
const desired_caps_1 = require("./desired-caps");
const device_connections_factory_1 = require("./device-connections-factory");
const execute_method_map_1 = require("./execute-method-map");
const method_map_1 = require("./method-map");
const py_ios_device_client_1 = require("./real-device-clients/py-ios-device-client");
const real_device_management_1 = require("./real-device-management");
const real_device_1 = require("./real-device");
const simulator_management_1 = require("./simulator-management");
const utils_1 = require("./utils");
const app_infos_cache_1 = require("./app-infos-cache");
const context_1 = require("./commands/context");
const SHUTDOWN_OTHER_FEAT_NAME = 'shutdown_other_sims';
const CUSTOMIZE_RESULT_BUNDLE_PATH = 'customize_result_bundle_path';
const defaultServerCaps = {
webStorageEnabled: false,
locationContextEnabled: false,
browserName: '',
platform: 'MAC',
javascriptEnabled: true,
databaseEnabled: false,
takesScreenshot: true,
networkConnectionEnabled: false,
};
const WDA_SIM_STARTUP_RETRIES = 2;
const WDA_REAL_DEV_STARTUP_RETRIES = 1;
const WDA_REAL_DEV_TUTORIAL_URL = 'https://appium.github.io/appium-xcuitest-driver/latest/preparation/real-device-config/';
const WDA_STARTUP_RETRY_INTERVAL = 10000;
const DEFAULT_SETTINGS = {
nativeWebTap: false,
nativeWebTapStrict: false,
useJSONSource: false,
webScreenshotMode: 'native',
shouldUseCompactResponses: true,
elementResponseAttributes: 'type,label',
// Read https://github.com/appium/WebDriverAgent/blob/master/WebDriverAgentLib/Utilities/FBConfiguration.m for following settings' values
mjpegServerScreenshotQuality: 25,
mjpegServerFramerate: 10,
screenshotQuality: 1,
mjpegScalingFactor: 100,
// set `reduceMotion` to `null` so that it will be verified but still set either true/false
reduceMotion: null,
pageSourceExcludedAttributes: ''
};
// This lock assures, that each driver session does not
// affect shared resources of the other parallel sessions
const SHARED_RESOURCES_GUARD = new async_lock_1.default();
const WEB_ELEMENTS_CACHE_SIZE = 500;
const SUPPORTED_ORIENATIONS = ['LANDSCAPE', 'PORTRAIT'];
const DEFAULT_MJPEG_SERVER_PORT = 9100;
/* eslint-disable no-useless-escape */
/** @type {import('@appium/types').RouteMatcher[]} */
const NO_PROXY_NATIVE_LIST = [
['DELETE', /window/],
['GET', /^\/session\/[^\/]+$/],
['GET', /alert_text/],
['GET', /alert\/[^\/]+/],
['GET', /appium/],
['GET', /attribute/],
['GET', /context/],
['GET', /location/],
['GET', /log/],
['GET', /screenshot/],
['GET', /size/],
['GET', /source/],
['GET', /timeouts$/],
['GET', /url/],
['GET', /window/],
['POST', /accept_alert/],
['POST', /actions$/],
['DELETE', /actions$/],
['POST', /alert_text/],
['POST', /alert\/[^\/]+/],
['POST', /appium/],
['POST', /appium\/device\/is_locked/],
['POST', /appium\/device\/lock/],
['POST', /appium\/device\/unlock/],
['POST', /back/],
['POST', /clear/],
['POST', /context/],
['POST', /dismiss_alert/],
['POST', /element\/active/], // MJSONWP get active element should proxy
['POST', /element$/],
['POST', /elements$/],
['POST', /execute/],
['POST', /keys/],
['POST', /log/],
['POST', /receive_async_response/], // always, in case context switches while waiting
['POST', /session\/[^\/]+\/location/], // geo location, but not element location
['POST', /shake/],
['POST', /timeouts/],
['POST', /url/],
['POST', /value/],
['POST', /window/],
['DELETE', /cookie/],
['GET', /cookie/],
['POST', /cookie/],
];
const NO_PROXY_WEB_LIST = /** @type {import('@appium/types').RouteMatcher[]} */ ([
['GET', /attribute/],
['GET', /element/],
['GET', /text/],
['GET', /title/],
['POST', /clear/],
['POST', /click/],
['POST', /element/],
['POST', /forward/],
['POST', /frame/],
['POST', /keys/],
['POST', /refresh/],
]).concat(NO_PROXY_NATIVE_LIST);
/* eslint-enable no-useless-escape */
const MEMOIZED_FUNCTIONS = ['getStatusBarHeight', 'getDevicePixelRatio', 'getScreenInfo'];
// Capabilities that do not have xcodebuild process
const CAP_NAMES_NO_XCODEBUILD_REQUIRED = ['webDriverAgentUrl', 'usePreinstalledWDA'];
const BUNDLE_VERSION_PATTERN = /CFBundleVersion\s+=\s+"?([^(;|")]+)/;
/**
* @implements {ExternalDriver<XCUITestDriverConstraints, FullContext|string>}
* @extends {BaseDriver<XCUITestDriverConstraints>}
* @privateRemarks **This class should be considered "final"**. It cannot be extended
* due to use of public class field assignments. If extending this class becomes a hard requirement, refer to the implementation of `BaseDriver` on how to do so.
*/
class XCUITestDriver extends driver_1.BaseDriver {
/**
*
* @param {XCUITestDriverOpts} opts
* @param {boolean} shouldValidateCaps
*/
constructor(opts = /** @type {XCUITestDriverOpts} */ ({}), shouldValidateCaps = true) {
super(opts, shouldValidateCaps);
/*---------------+
| ACTIVEAPPINFO |
+---------------+*/
this.mobileGetActiveAppInfo = commands_1.default.activeAppInfoExtensions.mobileGetActiveAppInfo;
/*-------+
| ALERT |
+-------+*/
this.getAlertText = commands_1.default.alertExtensions.getAlertText;
this.setAlertText = commands_1.default.alertExtensions.setAlertText;
this.postAcceptAlert = commands_1.default.alertExtensions.postAcceptAlert;
this.postDismissAlert = commands_1.default.alertExtensions.postDismissAlert;
this.getAlertButtons = commands_1.default.alertExtensions.getAlertButtons;
this.mobileHandleAlert = commands_1.default.alertExtensions.mobileHandleAlert;
/*---------------+
| APPMANAGEMENT |
+---------------+*/
this.mobileInstallApp = commands_1.default.appManagementExtensions.mobileInstallApp;
this.mobileIsAppInstalled = commands_1.default.appManagementExtensions.mobileIsAppInstalled;
this.mobileRemoveApp = commands_1.default.appManagementExtensions.mobileRemoveApp;
this.mobileLaunchApp = commands_1.default.appManagementExtensions.mobileLaunchApp;
this.mobileTerminateApp = commands_1.default.appManagementExtensions.mobileTerminateApp;
this.mobileActivateApp = commands_1.default.appManagementExtensions.mobileActivateApp;
this.mobileKillApp = commands_1.default.appManagementExtensions.mobileKillApp;
this.mobileQueryAppState = commands_1.default.appManagementExtensions.mobileQueryAppState;
this.installApp = commands_1.default.appManagementExtensions.installApp;
this.activateApp = commands_1.default.appManagementExtensions.activateApp;
this.isAppInstalled = commands_1.default.appManagementExtensions.isAppInstalled;
// @ts-ignore it must return boolean
this.terminateApp = commands_1.default.appManagementExtensions.terminateApp;
this.queryAppState = commands_1.default.appManagementExtensions.queryAppState;
this.mobileListApps = commands_1.default.appManagementExtensions.mobileListApps;
this.mobileClearApp = commands_1.default.appManagementExtensions.mobileClearApp;
/*------------+
| APPEARANCE |
+------------+*/
this.mobileSetAppearance = commands_1.default.appearanceExtensions.mobileSetAppearance;
this.mobileGetAppearance = commands_1.default.appearanceExtensions.mobileGetAppearance;
/*------------+
| INCREASE CONTRAST |
+------------+*/
this.mobileSetIncreaseContrast = commands_1.default.increaseContrastExtensions.mobileSetIncreaseContrast;
this.mobileGetIncreaseContrast = commands_1.default.increaseContrastExtensions.mobileGetIncreaseContrast;
/*------------+
| CONTENT SIZE |
+------------+*/
this.mobileSetContentSize = commands_1.default.contentSizeExtensions.mobileSetContentSize;
this.mobileGetContentSize = commands_1.default.contentSizeExtensions.mobileGetContentSize;
/*------------+
| AUDIT |
+------------+*/
this.mobilePerformAccessibilityAudit = commands_1.default.auditExtensions.mobilePerformAccessibilityAudit;
/*---------+
| BATTERY |
+---------+*/
this.mobileGetBatteryInfo = commands_1.default.batteryExtensions.mobileGetBatteryInfo;
/*-----------+
| BIOMETRIC |
+-----------+*/
this.mobileEnrollBiometric = commands_1.default.biometricExtensions.mobileEnrollBiometric;
this.mobileSendBiometricMatch = commands_1.default.biometricExtensions.mobileSendBiometricMatch;
this.mobileIsBiometricEnrolled = commands_1.default.biometricExtensions.mobileIsBiometricEnrolled;
/*-------------+
| CERTIFICATE |
+-------------+*/
this.mobileInstallCertificate = commands_1.default.certificateExtensions.mobileInstallCertificate;
this.mobileListCertificates = commands_1.default.certificateExtensions.mobileListCertificates;
this.mobileRemoveCertificate = commands_1.default.certificateExtensions.mobileRemoveCertificate;
/*-----------+
| CLIPBOARD |
+-----------+*/
this.setClipboard = commands_1.default.clipboardExtensions.setClipboard;
this.getClipboard = commands_1.default.clipboardExtensions.getClipboard;
/*-----------+
| CONDITION |
+-----------+*/
this.listConditionInducers = commands_1.default.conditionExtensions.listConditionInducers;
this.enableConditionInducer = commands_1.default.conditionExtensions.enableConditionInducer;
this.disableConditionInducer = commands_1.default.conditionExtensions.disableConditionInducer;
/*---------+
| CONTEXT |
+---------+*/
this.getContexts = commands_1.default.contextExtensions.getContexts;
this.getCurrentContext = commands_1.default.contextExtensions.getCurrentContext;
// @ts-ignore This is OK
this.getWindowHandle = commands_1.default.contextExtensions.getWindowHandle;
this.getWindowHandles = commands_1.default.contextExtensions.getWindowHandles;
this.setContext = commands_1.default.contextExtensions.setContext;
// @ts-ignore This is OK
this.setWindow = commands_1.default.contextExtensions.setWindow;
this.activateRecentWebview = commands_1.default.contextExtensions.activateRecentWebview;
this.connectToRemoteDebugger = commands_1.default.contextExtensions.connectToRemoteDebugger;
this.getContextsAndViews = commands_1.default.contextExtensions.getContextsAndViews;
this.listWebFrames = commands_1.default.contextExtensions.listWebFrames;
this.mobileGetContexts = commands_1.default.contextExtensions.mobileGetContexts;
this.onPageChange = commands_1.default.contextExtensions.onPageChange;
this.useNewSafari = commands_1.default.contextExtensions.useNewSafari;
this.getCurrentUrl = commands_1.default.contextExtensions.getCurrentUrl;
this.getNewRemoteDebugger = commands_1.default.contextExtensions.getNewRemoteDebugger;
this.getRecentWebviewContextId = commands_1.default.contextExtensions.getRecentWebviewContextId;
this.isWebContext = commands_1.default.contextExtensions.isWebContext;
this.isWebview = commands_1.default.contextExtensions.isWebview;
this.setCurrentUrl = commands_1.default.contextExtensions.setCurrentUrl;
this.stopRemote = commands_1.default.contextExtensions.stopRemote;
/*------------+
| DEVICEINFO |
+------------+*/
this.mobileGetDeviceInfo = commands_1.default.deviceInfoExtensions.mobileGetDeviceInfo;
/*---------+
| ELEMENT |
+---------+*/
this.elementDisplayed = commands_1.default.elementExtensions.elementDisplayed;
this.elementEnabled = commands_1.default.elementExtensions.elementEnabled;
this.elementSelected = commands_1.default.elementExtensions.elementSelected;
this.getName = commands_1.default.elementExtensions.getName;
this.getNativeAttribute = commands_1.default.elementExtensions.getNativeAttribute;
this.getAttribute = commands_1.default.elementExtensions.getAttribute;
this.getProperty = commands_1.default.elementExtensions.getProperty;
this.getText = commands_1.default.elementExtensions.getText;
this.getElementRect = commands_1.default.elementExtensions.getElementRect;
this.getLocation = commands_1.default.elementExtensions.getLocation;
this.getLocationInView = commands_1.default.elementExtensions.getLocationInView;
this.getSize = commands_1.default.elementExtensions.getSize;
/** @deprecated */
this.setValueImmediate = commands_1.default.elementExtensions.setValueImmediate;
this.setValue = commands_1.default.elementExtensions.setValue;
this.setValueWithWebAtom = commands_1.default.elementExtensions.setValueWithWebAtom;
this.keys = commands_1.default.elementExtensions.keys;
this.clear = commands_1.default.elementExtensions.clear;
this.getContentSize = commands_1.default.elementExtensions.getContentSize;
this.getNativeRect = commands_1.default.elementExtensions.getNativeRect;
/*---------+
| EXECUTE |
+---------+*/
this.receiveAsyncResponse = commands_1.default.executeExtensions.receiveAsyncResponse;
this.execute = commands_1.default.executeExtensions.execute;
this.executeAsync = commands_1.default.executeExtensions.executeAsync;
this.executeMobile = commands_1.default.executeExtensions.executeMobile;
this.mobileSimctl = commands_1.default.simctl.mobileSimctl;
/*--------------+
| FILEMOVEMENT |
+--------------+*/
this.pushFile = commands_1.default.fileMovementExtensions.pushFile;
this.mobilePushFile = commands_1.default.fileMovementExtensions.mobilePushFile;
this.pullFile = commands_1.default.fileMovementExtensions.pullFile;
this.mobilePullFile = commands_1.default.fileMovementExtensions.mobilePullFile;
this.mobileDeleteFolder = commands_1.default.fileMovementExtensions.mobileDeleteFolder;
this.mobileDeleteFile = commands_1.default.fileMovementExtensions.mobileDeleteFile;
this.pullFolder = commands_1.default.fileMovementExtensions.pullFolder;
this.mobilePullFolder = commands_1.default.fileMovementExtensions.mobilePullFolder;
/*--------+
| MEMORY |
+--------+*/
this.mobileSendMemoryWarning = commands_1.default.memoryExtensions.mobileSendMemoryWarning;
/*------+
| FIND |
+------+*/
this.findElOrEls = commands_1.default.findExtensions.findElOrEls;
this.findNativeElementOrElements = commands_1.default.findExtensions.findNativeElementOrElements;
this.doNativeFind = commands_1.default.findExtensions.doNativeFind;
this.getFirstVisibleChild = commands_1.default.findExtensions.getFirstVisibleChild;
/*---------+
| GENERAL |
+---------+*/
this.active = commands_1.default.generalExtensions.active;
this.background = commands_1.default.appManagementExtensions.background;
this.touchId = commands_1.default.generalExtensions.touchId;
this.toggleEnrollTouchId = commands_1.default.generalExtensions.toggleEnrollTouchId;
this.getWindowSize = commands_1.default.generalExtensions.getWindowSize;
this.getDeviceTime = commands_1.default.generalExtensions.getDeviceTime;
this.mobileGetDeviceTime = commands_1.default.generalExtensions.mobileGetDeviceTime;
this.getWindowRect = commands_1.default.generalExtensions.getWindowRect;
this.getStrings = commands_1.default.appStringsExtensions.getStrings;
this.removeApp = commands_1.default.generalExtensions.removeApp;
this.launchApp = commands_1.default.generalExtensions.launchApp;
this.closeApp = commands_1.default.generalExtensions.closeApp;
this.setUrl = commands_1.default.generalExtensions.setUrl;
this.getViewportRect = commands_1.default.generalExtensions.getViewportRect;
this.getScreenInfo = commands_1.default.generalExtensions.getScreenInfo;
this.getStatusBarHeight = commands_1.default.generalExtensions.getStatusBarHeight;
this.getDevicePixelRatio = commands_1.default.generalExtensions.getDevicePixelRatio;
this.mobilePressButton = commands_1.default.generalExtensions.mobilePressButton;
this.mobileSiriCommand = commands_1.default.generalExtensions.mobileSiriCommand;
/*-------------+
| GEOLOCATION |
+-------------+*/
this.mobileGetSimulatedLocation = commands_1.default.geolocationExtensions.mobileGetSimulatedLocation;
this.mobileSetSimulatedLocation = commands_1.default.geolocationExtensions.mobileSetSimulatedLocation;
this.mobileResetSimulatedLocation = commands_1.default.geolocationExtensions.mobileResetSimulatedLocation;
/*---------+
| GESTURE |
+---------+*/
this.mobileShake = commands_1.default.gestureExtensions.mobileShake;
this.click = commands_1.default.gestureExtensions.click;
this.releaseActions = commands_1.default.gestureExtensions.releaseActions;
this.performActions = commands_1.default.gestureExtensions.performActions;
this.nativeClick = commands_1.default.gestureExtensions.nativeClick;
this.mobileScrollToElement = commands_1.default.gestureExtensions.mobileScrollToElement;
this.mobileScroll = commands_1.default.gestureExtensions.mobileScroll;
this.mobileSwipe = commands_1.default.gestureExtensions.mobileSwipe;
this.mobilePinch = commands_1.default.gestureExtensions.mobilePinch;
this.mobileDoubleTap = commands_1.default.gestureExtensions.mobileDoubleTap;
this.mobileTwoFingerTap = commands_1.default.gestureExtensions.mobileTwoFingerTap;
this.mobileTouchAndHold = commands_1.default.gestureExtensions.mobileTouchAndHold;
this.mobileTap = commands_1.default.gestureExtensions.mobileTap;
this.mobileDragFromToForDuration = commands_1.default.gestureExtensions.mobileDragFromToForDuration;
this.mobileDragFromToWithVelocity = commands_1.default.gestureExtensions.mobileDragFromToWithVelocity;
this.mobileTapWithNumberOfTaps = commands_1.default.gestureExtensions.mobileTapWithNumberOfTaps;
this.mobileForcePress = commands_1.default.gestureExtensions.mobileForcePress;
this.mobileSelectPickerWheelValue = commands_1.default.gestureExtensions.mobileSelectPickerWheelValue;
this.mobileRotateElement = commands_1.default.gestureExtensions.mobileRotateElement;
/*-------+
| IOHID |
+-------+*/
this.mobilePerformIoHidEvent = commands_1.default.iohidExtensions.mobilePerformIoHidEvent;
/*-----------+
| KEYCHAINS |
+-----------+*/
this.mobileClearKeychains = commands_1.default.keychainsExtensions.mobileClearKeychains;
/*----------+
| KEYBOARD |
+----------+*/
this.hideKeyboard = commands_1.default.keyboardExtensions.hideKeyboard;
this.mobileHideKeyboard = commands_1.default.keyboardExtensions.mobileHideKeyboard;
this.isKeyboardShown = commands_1.default.keyboardExtensions.isKeyboardShown;
this.mobileKeys = commands_1.default.keyboardExtensions.mobileKeys;
/*--------------+
| LOCALIZATION |
+--------------+*/
this.mobileConfigureLocalization = commands_1.default.localizationExtensions.mobileConfigureLocalization;
/*----------+
| LOCATION |
+----------+*/
this.getGeoLocation = commands_1.default.locationExtensions.getGeoLocation;
this.setGeoLocation = commands_1.default.locationExtensions.setGeoLocation;
this.mobileResetLocationService = commands_1.default.locationExtensions.mobileResetLocationService;
/*------+
| LOCK |
+------+*/
this.lock = commands_1.default.lockExtensions.lock;
this.unlock = commands_1.default.lockExtensions.unlock;
this.isLocked = commands_1.default.lockExtensions.isLocked;
/*-----+
| LOG |
+-----+*/
this.extractLogs = commands_1.default.logExtensions.extractLogs;
this.supportedLogTypes = commands_1.default.logExtensions.supportedLogTypes;
this.startLogCapture = commands_1.default.logExtensions.startLogCapture;
this.mobileStartLogsBroadcast = commands_1.default.logExtensions.mobileStartLogsBroadcast;
this.mobileStopLogsBroadcast = commands_1.default.logExtensions.mobileStopLogsBroadcast;
/*------------+
| NAVIGATION |
+------------+*/
this.back = commands_1.default.navigationExtensions.back;
this.forward = commands_1.default.navigationExtensions.forward;
this.closeWindow = commands_1.default.navigationExtensions.closeWindow;
this.nativeBack = commands_1.default.navigationExtensions.nativeBack;
this.mobileDeepLink = commands_1.default.navigationExtensions.mobileDeepLink;
/*---------------+
| NOTIFICATIONS |
+---------------+*/
this.mobilePushNotification = commands_1.default.notificationsExtensions.mobilePushNotification;
this.mobileExpectNotification = commands_1.default.notificationsExtensions.mobileExpectNotification;
/*------------+
| PASTEBOARD |
+------------+*/
this.mobileSetPasteboard = commands_1.default.pasteboardExtensions.mobileSetPasteboard;
this.mobileGetPasteboard = commands_1.default.pasteboardExtensions.mobileGetPasteboard;
/*------+
| PCAP |
+------+*/
this.mobileStartPcap = commands_1.default.pcapExtensions.mobileStartPcap;
this.mobileStopPcap = commands_1.default.pcapExtensions.mobileStopPcap;
/*-------------+
| PERFORMANCE |
+-------------+*/
this.mobileStartPerfRecord = commands_1.default.performanceExtensions.mobileStartPerfRecord;
this.mobileStopPerfRecord = commands_1.default.performanceExtensions.mobileStopPerfRecord;
/*-------------+
| PERMISSIONS |
+-------------+*/
this.mobileResetPermission = commands_1.default.permissionsExtensions.mobileResetPermission;
this.mobileGetPermission = commands_1.default.permissionsExtensions.mobileGetPermission;
this.mobileSetPermissions = commands_1.default.permissionsExtensions.mobileSetPermissions;
/*-------------+
| PROXYHELPER |
+-------------+*/
this.proxyCommand = commands_1.default.proxyHelperExtensions.proxyCommand;
/*-------------+
| RECORDAUDIO |
+-------------+*/
this.startAudioRecording = commands_1.default.recordAudioExtensions.startAudioRecording;
this.stopAudioRecording = commands_1.default.recordAudioExtensions.stopAudioRecording;
/*--------------+
| RECORDSCREEN |
+--------------+*/
this._recentScreenRecorder = commands_1.default.recordScreenExtensions._recentScreenRecorder;
this.startRecordingScreen = commands_1.default.recordScreenExtensions.startRecordingScreen;
this.stopRecordingScreen = commands_1.default.recordScreenExtensions.stopRecordingScreen;
/*-------------+
| SCREENSHOTS |
+-------------+*/
this.getScreenshot = commands_1.default.screenshotExtensions.getScreenshot;
this.getElementScreenshot = commands_1.default.screenshotExtensions.getElementScreenshot;
this.getViewportScreenshot = commands_1.default.screenshotExtensions.getViewportScreenshot;
/*--------+
| SOURCE |
+--------+*/
this.getPageSource = commands_1.default.sourceExtensions.getPageSource;
this.mobileGetSource = commands_1.default.sourceExtensions.mobileGetSource;
/*----------+
| TIMEOUTS |
+----------+*/
this.pageLoadTimeoutW3C = commands_1.default.timeoutExtensions.pageLoadTimeoutW3C;
this.pageLoadTimeoutMJSONWP = commands_1.default.timeoutExtensions.pageLoadTimeoutMJSONWP;
this.scriptTimeoutW3C = commands_1.default.timeoutExtensions.scriptTimeoutW3C;
this.scriptTimeoutMJSONWP = commands_1.default.timeoutExtensions.scriptTimeoutMJSONWP;
this.asyncScriptTimeout = commands_1.default.timeoutExtensions.asyncScriptTimeout;
this.setPageLoadTimeout = commands_1.default.timeoutExtensions.setPageLoadTimeout;
this.setAsyncScriptTimeout = commands_1.default.timeoutExtensions.setAsyncScriptTimeout;
/*-----+
| WEB |
+-----+*/
this.setFrame = commands_1.default.webExtensions.setFrame;
this.getCssProperty = commands_1.default.webExtensions.getCssProperty;
this.submit = commands_1.default.webExtensions.submit;
this.refresh = commands_1.default.webExtensions.refresh;
this.getUrl = commands_1.default.webExtensions.getUrl;
this.title = commands_1.default.webExtensions.title;
this.getCookies = commands_1.default.webExtensions.getCookies;
this.setCookie = commands_1.default.webExtensions.setCookie;
this.deleteCookie = commands_1.default.webExtensions.deleteCookie;
this.deleteCookies = commands_1.default.webExtensions.deleteCookies;
this._deleteCookie = commands_1.default.webExtensions._deleteCookie;
this.cacheWebElement = commands_1.default.webExtensions.cacheWebElement;
this.cacheWebElements = commands_1.default.webExtensions.cacheWebElements;
this.executeAtom = commands_1.default.webExtensions.executeAtom;
this.executeAtomAsync = commands_1.default.webExtensions.executeAtomAsync;
this.getAtomsElement = commands_1.default.webExtensions.getAtomsElement;
this.convertElementsForAtoms = commands_1.default.webExtensions.convertElementsForAtoms;
this.getElementId = commands_1.default.webExtensions.getElementId;
this.hasElementId = commands_1.default.webExtensions.hasElementId;
this.findWebElementOrElements = commands_1.default.webExtensions.findWebElementOrElements;
this.clickWebCoords = commands_1.default.webExtensions.clickWebCoords;
this.getSafariIsIphone = commands_1.default.webExtensions.getSafariIsIphone;
this.getSafariDeviceSize = commands_1.default.webExtensions.getSafariDeviceSize;
this.getSafariIsNotched = commands_1.default.webExtensions.getSafariIsNotched;
this.getExtraTranslateWebCoordsOffset = commands_1.default.webExtensions.getExtraTranslateWebCoordsOffset;
this.getExtraNativeWebTapOffset = commands_1.default.webExtensions.getExtraNativeWebTapOffset;
this.nativeWebTap = commands_1.default.webExtensions.nativeWebTap;
this.translateWebCoords = commands_1.default.webExtensions.translateWebCoords;
this.checkForAlert = commands_1.default.webExtensions.checkForAlert;
this.waitForAtom = commands_1.default.webExtensions.waitForAtom;
this.mobileWebNav = commands_1.default.webExtensions.mobileWebNav;
this.getWdaLocalhostRoot = commands_1.default.webExtensions.getWdaLocalhostRoot;
this.mobileCalibrateWebToRealCoordinatesTranslation = commands_1.default.webExtensions.mobileCalibrateWebToRealCoordinatesTranslation;
this.mobileUpdateSafariPreferences = commands_1.default.webExtensions.mobileUpdateSafariPreferences;
/*--------+
| XCTEST |
+--------+*/
this.mobileRunXCTest = commands_1.default.xctestExtensions.mobileRunXCTest;
this.mobileInstallXCTestBundle = commands_1.default.xctestExtensions.mobileInstallXCTestBundle;
this.mobileListXCTestBundles = commands_1.default.xctestExtensions.mobileListXCTestBundles;
this.mobileListXCTestsInTestBundle = commands_1.default.xctestExtensions.mobileListXCTestsInTestBundle;
/*----------------------+
| XCTEST SCREEN RECORD |
+---------------------+*/
this.mobileStartXctestScreenRecording = commands_1.default.xctestRecordScreenExtensions.mobileStartXctestScreenRecording;
this.mobileGetXctestScreenRecordingInfo = commands_1.default.xctestRecordScreenExtensions.mobileGetXctestScreenRecordingInfo;
this.mobileStopXctestScreenRecording = commands_1.default.xctestRecordScreenExtensions.mobileStopXctestScreenRecording;
this.locatorStrategies = [
'xpath',
'id',
'name',
'class name',
'-ios predicate string',
'-ios class chain',
'accessibility id',
'css selector',
];
this.webLocatorStrategies = [
'link text',
'css selector',
'tag name',
'link text',
'partial link text',
];
this.curWebFrames = [];
this._perfRecorders = [];
this.desiredCapConstraints = desired_caps_1.desiredCapConstraints;
this.webElementsCache = new lru_cache_1.LRUCache({
max: WEB_ELEMENTS_CACHE_SIZE,
});
this.webviewCalibrationResult = null;
this._waitingAtoms = {
count: 0,
alertNotifier: new node_events_1.default(),
alertMonitor: bluebird_1.default.resolve(),
};
this.resetIos();
this.settings = new driver_1.DeviceSettings(DEFAULT_SETTINGS, this.onSettingsUpdate.bind(this));
this.logs = {};
this._trafficCapture = null;
// memoize functions here, so that they are done on a per-instance basis
for (const fn of MEMOIZED_FUNCTIONS) {
this[fn] = lodash_1.default.memoize(this[fn]);
}
this.lifecycleData = {};
this._audioRecorder = null;
this.appInfosCache = new app_infos_cache_1.AppInfosCache(this.log);
this.remote = null;
this.doesSupportBidi = true;
}
async onSettingsUpdate(key, value) {
// skip sending the update request to the WDA nor saving it in opts
// to not spend unnecessary time.
if (['pageSourceExcludedAttributes'].includes(key)) {
return;
}
if (key !== 'nativeWebTap' && key !== 'nativeWebTapStrict') {
return await this.proxyCommand('/appium/settings', 'POST', {
settings: { [key]: value },
});
}
this.opts[key] = !!value;
}
resetIos() {
this.opts = this.opts || {};
// @ts-ignore this is ok
this.wda = null;
this.jwpProxyActive = false;
this.proxyReqRes = null;
this.safari = false;
this.cachedWdaStatus = null;
this.curWebFrames = [];
this._currentUrl = null;
this.curContext = null;
this.xcodeVersion = undefined;
this.contexts = [];
this.implicitWaitMs = 0;
this.pageLoadMs = 6000;
this.landscapeWebCoordsOffset = 0;
this.remote = null;
this._conditionInducerService = null;
this.webElementsCache = new lru_cache_1.LRUCache({
max: WEB_ELEMENTS_CACHE_SIZE,
});
this._waitingAtoms = {
count: 0,
alertNotifier: new node_events_1.default(),
alertMonitor: bluebird_1.default.resolve(),
};
}
get driverData() {
// TODO fill out resource info here
return {};
}
async getStatus() {
const status = {
ready: true,
message: 'The driver is ready to accept new connections',
build: await (0, utils_1.getDriverInfo)(),
};
if (this.cachedWdaStatus) {
status.wda = this.cachedWdaStatus;
}
return status;
}
mergeCliArgsToOpts() {
let didMerge = false;
// this.cliArgs should never include anything we do not expect.
for (const [key, value] of Object.entries(this.cliArgs ?? {})) {
if (lodash_1.default.has(this.opts, key)) {
this.log.info(`CLI arg '${key}' with value '${value}' overwrites value '${this.opts[key]}' sent in via caps)`);
didMerge = true;
}
this.opts[key] = value;
}
return didMerge;
}
/**
* @returns {Simulator|RealDevice}
*/
get device() {
return this._device;
}
isXcodebuildNeeded() {
return !(CAP_NAMES_NO_XCODEBUILD_REQUIRED.some((x) => Boolean(this.opts[x])));
}
async createSession(w3cCaps1, w3cCaps2, w3cCaps3, driverData) {
try {
let [sessionId, caps] = await super.createSession(w3cCaps1, w3cCaps2, w3cCaps3, driverData);
// merge cli args to opts, and if we did merge any, revalidate opts to ensure the final set
// is also consistent
if (this.mergeCliArgsToOpts()) {
this.validateDesiredCaps({ ...caps, ...this.cliArgs });
}
await this.start();
// merge server capabilities + desired capabilities
caps = Object.assign({}, defaultServerCaps, caps);
// update the udid with what is actually used
caps.udid = this.opts.udid;
// ensure we track nativeWebTap capability as a setting as well
if (lodash_1.default.has(this.opts, 'nativeWebTap')) {
await this.updateSettings({ nativeWebTap: this.opts.nativeWebTap });
}
// ensure we track nativeWebTapStrict capability as a setting as well
if (lodash_1.default.has(this.opts, 'nativeWebTapStrict')) {
await this.updateSettings({ nativeWebTapStrict: this.opts.nativeWebTapStrict });
}
// ensure we track useJSONSource capability as a setting as well
if (lodash_1.default.has(this.opts, 'useJSONSource')) {
await this.updateSettings({ useJSONSource: this.opts.useJSONSource });
}
/** @type {import('appium-webdriveragent').WDASettings} */
let wdaSettings = {
elementResponseAttributes: DEFAULT_SETTINGS.elementResponseAttributes,
shouldUseCompactResponses: DEFAULT_SETTINGS.shouldUseCompactResponses,
};
if ('elementResponseAttributes' in this.opts && lodash_1.default.isString(this.opts.elementResponseAttributes)) {
wdaSettings.elementResponseAttributes = this.opts.elementResponseAttributes;
}
if ('shouldUseCompactResponses' in this.opts && lodash_1.default.isBoolean(this.opts.shouldUseCompactResponses)) {
wdaSettings.shouldUseCompactResponses = this.opts.shouldUseCompactResponses;
}
if ('mjpegServerScreenshotQuality' in this.opts && lodash_1.default.isNumber(this.opts.mjpegServerScreenshotQuality)) {
wdaSettings.mjpegServerScreenshotQuality = this.opts.mjpegServerScreenshotQuality;
}
if ('mjpegServerFramerate' in this.opts && lodash_1.default.isNumber(this.opts.mjpegServerFramerate)) {
wdaSettings.mjpegServerFramerate = this.opts.mjpegServerFramerate;
}
if (lodash_1.default.has(this.opts, 'screenshotQuality')) {
this.log.info(`Setting the quality of phone screenshot: '${this.opts.screenshotQuality}'`);
wdaSettings.screenshotQuality = this.opts.screenshotQuality;
}
// ensure WDA gets our defaults instead of whatever its own might be
await this.updateSettings(wdaSettings);
await this.handleMjpegOptions();
return /** @type {[string, import('@appium/types').DriverCaps<XCUITestDriverConstraints>]} */ ([
sessionId,
caps,
]);
}
catch (e) {
this.log.error(JSON.stringify(e));
await this.deleteSession();
throw e;
}
}
/**
* Handles MJPEG server-related capabilities
* @returns {Promise<void>}
*/
async handleMjpegOptions() {
await this.allocateMjpegServerPort();
// turn on mjpeg stream reading if requested
if (this.opts.mjpegScreenshotUrl) {
this.log.info(`Starting MJPEG stream reading URL: '${this.opts.mjpegScreenshotUrl}'`);
this.mjpegStream = new support_1.mjpeg.MJpegStream(this.opts.mjpegScreenshotUrl);
await this.mjpegStream.start();
}
}
/**
* Allocates and configures port forwarding for the MJPEG server
* @returns {Promise<void>}
* @throws {Error} If port forwarding fails and mjpegServerPort capability value is provided explicitly
*/
async allocateMjpegServerPort() {
const mjpegServerPort = this.opts.mjpegServerPort || DEFAULT_MJPEG_SERVER_PORT;
this.log.debug(`Forwarding MJPEG server port ${mjpegServerPort} to local port ${mjpegServerPort}`);
try {
await device_connections_factory_1.DEVICE_CONNECTIONS_FACTORY.requestConnection(this.opts.udid, mjpegServerPort, {
devicePort: mjpegServerPort,
usePortForwarding: this.isRealDevice(),
});
}
catch (error) {
if (lodash_1.default.isUndefined(this.opts.mjpegServerPort)) {
this.log.warn(`Cannot forward the device port ${DEFAULT_MJPEG_SERVER_PORT} to the local port ${DEFAULT_MJPEG_SERVER_PORT}. ` +
`Certain features, like MJPEG-based screen recording, will be unavailable during this session. ` +
`Try to customize the value of 'mjpegServerPort' capability as a possible solution`);
}
else {
this.log.debug(error.stack);
throw new Error(`Cannot ensure MJPEG broadcast functionality by forwarding the local port ${mjpegServerPort} ` +
`requested by the 'mjpegServerPort' capability to the device port ${mjpegServerPort}. ` +
`Original error: ${error}`);
}
}
}
/**
* Returns the default URL for Safari browser
* @returns {string} The default URL
*/
getDefaultUrl() {
// Setting this to some external URL slows down the session init
return `${this.getWdaLocalhostRoot()}/health`;
}
async start() {
this.opts.noReset = !!this.opts.noReset;
this.opts.fullReset = !!this.opts.fullReset;
await (0, utils_1.printUser)();
this._iosSdkVersion = null; // For WDA and xcodebuild
const { device, udid, realDevice } = await this.determineDevice();
this.log.info(`Determining device to run tests on: udid: '${udid}', real device: ${realDevice}`);
this._device = device;
this.opts.udid = udid;
if (this.opts.simulatorDevicesSetPath) {
if (realDevice) {
this.log.info(`The 'simulatorDevicesSetPath' capability is only supported for Simulator devices`);
}
else {
this.log.info(`Setting simulator devices set path to '${this.opts.simulatorDevicesSetPath}'`);
( /** @type {Simulator} */(this.device)).devicesSetPath = this.opts.simulatorDevicesSetPath;
}
}
// at this point if there is no platformVersion, get it from the device
if (!this.opts.platformVersion) {
this.opts.platformVersion = await this.device.getPlatformVersion();
this.log.info(`No platformVersion specified. Using device version: '${this.opts.platformVersion}'`);
}
const normalizedVersion = (0, utils_1.normalizePlatformVersion)(this.opts.platformVersion);
if (this.opts.platformVersion !== normalizedVersion) {
this.log.info(`Normalized platformVersion capability value '${this.opts.platformVersion}' to '${normalizedVersion}'`);
this.opts.platformVersion = normalizedVersion;
}
this.caps.platformVersion = this.opts.platformVersion;
if (lodash_1.default.isEmpty(this.xcodeVersion) && (this.isXcodebuildNeeded() || this.isSimulator())) {
// no `webDriverAgentUrl`, or on a simulator, so we need an Xcode version
this.xcodeVersion = await (0, utils_1.getAndCheckXcodeVersion)();
}
this.logEvent('xcodeDetailsRetrieved');
if (lodash_1.default.toLower(this.opts.browserName) === 'safari') {
this.log.info('Safari test requested');
this.safari = true;
this.opts.app = undefined;
this.opts.processArguments = this.opts.processArguments || {};
real_device_management_1.applySafariStartupArgs.bind(this)();
this.opts.bundleId = app_utils_1.SAFARI_BUNDLE_ID;
this._currentUrl = this.opts.safariInitialUrl || this.getDefaultUrl();
}
else if (this.opts.app || this.opts.bundleId) {
await this.configureApp();
}
this.logEvent('appConfigured');
// fail very early if the app doesn't actually exist
// or if bundle id doesn't point to an installed app
if (this.opts.app) {
await (0, utils_1.checkAppPresent)(this.opts.app);
if (!this.opts.bundleId) {
this.opts.bundleId = await this.appInfosCache.extractBundleId(this.opts.app);
}
}
await this.runReset();
this.wda = new appium_webdriveragent_1.WebDriverAgent(
/** @type {import('appium-xcode').XcodeVersion} */ (this.xcodeVersion), {
...this.opts,
device: this.device,
realDevice: this.isRealDevice(),
iosSdkVersion: this._iosSdkVersion,
reqBasePath: this.basePath,
},
// @ts-ignore this is ok
this.log);
// Derived data path retrieval is an expensive operation
// We could start that now in background and get the cached result
// whenever it is needed
// eslint-disable-next-line promise/prefer-await-to-then
this.wda.retrieveDerivedDataPath().catch((e) => this.log.debug(e));
const memoizedLogInfo = lodash_1.default.memoize(() => {
this.log.info("'skipLogCapture' is set. Skipping starting logs such as crash, system, safari console and safari network.");
});
const startLogCapture = async () => {
if (this.opts.skipLogCapture) {
memoizedLogInfo();
return false;
}
const result = await this.startLogCapture();
if (result) {
this.logEvent('logCaptureStarted');
}
return result;
};
const isLogCaptureStarted = await startLogCapture();
this.log.info(`Setting up ${this.isRealDevice() ? 'real device' : 'simulator'}`);
if (this.isSimulator()) {
await this.initSimulator();
if (!isLogCaptureStarted) {
// Retry log capture if Simulator was not running before
await startLogCapture();
}
}
else if (this.opts.customSSLCert) {
await new py_ios_device_client_1.Pyidevice({
udid,
log: this.log,
}).installProfile({ payload: this.opts.customSSLCert });
this.logEvent('customCertInstalled');
}
await this.installAUT();
// if we only have bundle identifier and no app, fail if it is not already installed
if (!this.opts.app &&
this.opts.bundleId &&
!this.isSafari() &&
!(await this.device.isAppInstalled(this.opts.bundleId))) {
throw this.log.errorWithException(`App with bundle identifier '${this.opts.bundleId}' unknown`);
}
if (this.isSimulator()) {
if (this.opts.permissions) {
this.log.debug('Setting the requested permissions before WDA is started');
for (const [bundleId, permissionsMapping] of lodash_1.default.toPairs(JSON.parse(this.opts.permissions))) {
await /** @type {Simulator} */ (this.device).setPermissions(bundleId, permissionsMapping);
}
}
// TODO: Deprecate and remove this block together with calendarAccessAuthorized capability
if (lodash_1.default.isBoolean(this.opts.calendarAccessAuthorized)) {
this.log.warn(`The 'calendarAccessAuthorized' capability is deprecated and will be removed soon. ` +
`Consider using 'permissions' one instead with 'calendar' key`);
const methodName = `${this.opts.calendarAccessAuthorized ? 'enable' : 'disable'}CalendarAccess`;
await this.device[methodName](this.opts.bundleId);
}
}
await this.startWda();
if (lodash_1.default.isString(this.opts.orientation)) {
await this.setInitialOrientation(this.opts.orientation);
this.logEvent('orientationSet');
}
if (this.isSafari() || this.opts.autoWebview) {
await this.activateRecentWebview();
}
else {
// We want to always setup the initial context value upon session startup
await context_1.notifyBiDiContextChange.bind(this)();
}
if (this.isSafari()) {
if ((0, utils_1.shouldSetInitialSafariUrl)(this.opts)) {
this.log.info(`About to set the initial Safari URL to '${this.getCurrentUrl()}'`);
if (lodash_1.default.isNil(this.opts.safariInitialUrl) && lodash_1.default.isNil(this.opts.initialDeeplinkUrl)) {
this.log.info(`Use the 'safariInitialUrl' capability to customize it`);
}
;
await this.setUrl(this.getCurrentUrl());
}
else {
const currentUrl = await this.getUrl();
this.log.info(`Current URL: ${currentUrl}`);
this.setCurrentUrl(currentUrl);
}
}
}
/**
* Start the simulator and initialize based on capabilities
*/
async initSimulator() {
const device = /** @type {Simulator} */ (this.device);
if (this.opts.shutdownOtherSimulators) {
this.assertFeatureEnabled(SHUTDOWN_OTHER_FEAT_NAME);
await simulator_management_1.shutdownOtherSimulators.bind(this)();
}
await this.startSim();
if (this.opts.customSSLCert) {
// Simulator must be booted in order to call this helper
await device.addCertificate(this.opts.customSSLCert);
this.logEvent('customCertInstalled');
}
if (await simulator_management_1.setSafariPrefs.bind(this)()) {
this.log.debug('Safari preferences have been updated');
}
if (await simulator_management_1.setLocalizationPrefs.bind(this)()) {
this.log.debug('Localization preferences have been updated');
}
/** @type {Promise[]} */
const promises = ['reduceMotion', 'reduceTransparency', 'autoFillPasswords']
.filter((optName) => lodash_1.default.isBoolean(this.opts[optName]))
.map((optName) => {
this.log.info(`Setting ${optName} to ${this.opts[optName]}`);
return device[`set${lodash_1.default.upperFirst(optName)}`](this.opts[optName]);
});
await bluebird_1.default.all(promises);
if (this.opts.launchWithIDB) {
try {
const idb = new appium_idb_1.default({ udid: this.opts.udid });
await idb.connect();
device.idb = idb;
}
catch (e) {
this.log.debug(e.stack);
this.log.warn(`idb will not be used for Simulator interaction. Original error: ${e.message}`);
}
}
this.logEvent('simStarted');
}
/**
* Start WebDriverAgentRunner
*/
async startWda() {
// Don't cleanup the processes if webDriverAgentUrl is set
if (!support_1.util.hasValue(this.wda.webDriverAgentUrl)) {
await this.wda.cleanupObsoleteProcesses();
}
const usePortForwarding = this.isRealDevice() && !this.wda.webDriverAgentUrl && (0, utils_1.isLocalHost)(this.wda.wdaBaseUrl);
await device_connections_factory_1.DEVICE_CONNECTIONS_FACTORY.requestConnection(this.opts.udid, this.wda.url.port, {
devicePort: usePortForwarding ? this.wda.wdaR