UNPKG

appium-uiautomator2-driver

Version:
494 lines 23.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AndroidUiautomator2Driver = void 0; const appium_adb_1 = require("appium-adb"); const appium_android_driver_1 = require("appium-android-driver"); const driver_1 = require("appium/driver"); const support_1 = require("appium/support"); const constraints_1 = __importDefault(require("./constraints")); const method_map_1 = require("./method-map"); const utils_1 = require("./utils"); const uiautomator2_server_1 = require("./uiautomator2-server"); const actions_1 = require("./commands/actions"); const alert_1 = require("./commands/alert"); const app_management_1 = require("./commands/app-management"); const aut_1 = require("./commands/aut"); const battery_1 = require("./commands/battery"); const clipboard_1 = require("./commands/clipboard"); const element_1 = require("./commands/element"); const find_1 = require("./commands/find"); const gestures_1 = require("./commands/gestures"); const keyboard_1 = require("./commands/keyboard"); const misc_1 = require("./commands/misc"); const windows_1 = require("./commands/windows"); const navigation_1 = require("./commands/navigation"); const screenshot_1 = require("./commands/screenshot"); const viewport_1 = require("./commands/viewport"); const execute_method_map_1 = require("./execute-method-map"); // NO_PROXY contains the paths that we never want to proxy to UiAutomator2 server. // TODO: Add the list of paths that we never want to proxy to UiAutomator2 server. // TODO: Need to segregate the paths better way using regular expressions wherever applicable. // (Not segregating right away because more paths to be added in the NO_PROXY list) const NO_PROXY = [ ['DELETE', new RegExp('^/session/[^/]+/actions')], ['GET', new RegExp('^/session/(?!.*/)')], ['GET', new RegExp('^/session/[^/]+/alert_[^/]+')], ['GET', new RegExp('^/session/[^/]+/alert/[^/]+')], ['GET', new RegExp('^/session/[^/]+/appium/[^/]+/current_activity')], ['GET', new RegExp('^/session/[^/]+/appium/[^/]+/current_package')], ['GET', new RegExp('^/session/[^/]+/appium/app/[^/]+')], ['GET', new RegExp('^/session/[^/]+/appium/capabilities')], ['GET', new RegExp('^/session/[^/]+/appium/commands')], ['GET', new RegExp('^/session/[^/]+/appium/device/[^/]+')], ['GET', new RegExp('^/session/[^/]+/appium/extensions')], ['GET', new RegExp('^/session/[^/]+/appium/settings')], ['GET', new RegExp('^/session/[^/]+/context')], ['GET', new RegExp('^/session/[^/]+/contexts')], ['GET', new RegExp('^/session/[^/]+/element/[^/]+/attribute')], ['GET', new RegExp('^/session/[^/]+/element/[^/]+/displayed')], ['GET', new RegExp('^/session/[^/]+/element/[^/]+/enabled')], ['GET', new RegExp('^/session/[^/]+/element/[^/]+/location_in_view')], ['GET', new RegExp('^/session/[^/]+/element/[^/]+/name')], ['GET', new RegExp('^/session/[^/]+/element/[^/]+/screenshot')], ['GET', new RegExp('^/session/[^/]+/element/[^/]+/selected')], ['GET', new RegExp('^/session/[^/]+/ime/[^/]+')], ['GET', new RegExp('^/session/[^/]+/location')], ['GET', new RegExp('^/session/[^/]+/network_connection')], ['GET', new RegExp('^/session/[^/]+/screenshot')], ['GET', new RegExp('^/session/[^/]+/timeouts')], ['GET', new RegExp('^/session/[^/]+/url')], ['POST', new RegExp('^/session/[^/]+/[^/]+_alert$')], ['POST', new RegExp('^/session/[^/]+/actions')], ['POST', new RegExp('^/session/[^/]+/alert/[^/]+')], ['POST', new RegExp('^/session/[^/]+/app/[^/]')], ['POST', new RegExp('^/session/[^/]+/appium/[^/]+/start_activity')], ['POST', new RegExp('^/session/[^/]+/appium/app/[^/]+')], ['POST', new RegExp('^/session/[^/]+/appium/compare_images')], ['POST', new RegExp('^/session/[^/]+/appium/device/(?!set_clipboard)[^/]+')], ['POST', new RegExp('^/session/[^/]+/appium/element/[^/]+/replace_value')], ['POST', new RegExp('^/session/[^/]+/appium/element/[^/]+/value')], ['POST', new RegExp('^/session/[^/]+/appium/getPerformanceData')], ['POST', new RegExp('^/session/[^/]+/appium/performanceData/types')], ['POST', new RegExp('^/session/[^/]+/appium/settings')], ['POST', new RegExp('^/session/[^/]+/appium/execute_driver')], ['POST', new RegExp('^/session/[^/]+/appium/start_recording_screen')], ['POST', new RegExp('^/session/[^/]+/appium/stop_recording_screen')], ['POST', new RegExp('^/session/[^/]+/appium/.*event')], ['POST', new RegExp('^/session/[^/]+/context')], ['POST', new RegExp('^/session/[^/]+/element')], ['POST', new RegExp('^/session/[^/]+/ime/[^/]+')], ['POST', new RegExp('^/session/[^/]+/keys')], ['POST', new RegExp('^/session/[^/]+/location')], ['POST', new RegExp('^/session/[^/]+/network_connection')], ['POST', new RegExp('^/session/[^/]+/timeouts')], ['POST', new RegExp('^/session/[^/]+/url')], // MJSONWP commands ['GET', new RegExp('^/session/[^/]+/log/types')], ['POST', new RegExp('^/session/[^/]+/execute')], ['POST', new RegExp('^/session/[^/]+/execute_async')], ['POST', new RegExp('^/session/[^/]+/log')], // W3C commands // For Selenium v4 (W3C does not have this route) ['GET', new RegExp('^/session/[^/]+/se/log/types')], ['GET', new RegExp('^/session/[^/]+/window/rect')], ['POST', new RegExp('^/session/[^/]+/execute/async')], ['POST', new RegExp('^/session/[^/]+/execute/sync')], // For Selenium v4 (W3C does not have this route) ['POST', new RegExp('^/session/[^/]+/se/log')], ]; // This is a set of methods and paths that we never want to proxy to Chromedriver. const CHROME_NO_PROXY = [ ['GET', new RegExp('^/session/[^/]+/appium')], ['GET', new RegExp('^/session/[^/]+/context')], ['GET', new RegExp('^/session/[^/]+/element/[^/]+/rect')], ['GET', new RegExp('^/session/[^/]+/orientation')], ['POST', new RegExp('^/session/[^/]+/appium')], ['POST', new RegExp('^/session/[^/]+/context')], ['POST', new RegExp('^/session/[^/]+/orientation')], // this is needed to make the mobile: commands working in web context ['POST', new RegExp('^/session/[^/]+/execute$')], ['POST', new RegExp('^/session/[^/]+/execute/sync')], // MJSONWP commands ['GET', new RegExp('^/session/[^/]+/log/types$')], ['POST', new RegExp('^/session/[^/]+/log$')], // W3C commands // For Selenium v4 (W3C does not have this route) ['GET', new RegExp('^/session/[^/]+/se/log/types$')], // For Selenium v4 (W3C does not have this route) ['POST', new RegExp('^/session/[^/]+/se/log$')], ]; class AndroidUiautomator2Driver extends appium_android_driver_1.AndroidDriver { static newMethodMap = method_map_1.newMethodMap; static executeMethodMap = execute_method_map_1.executeMethodMap; uiautomator2; systemPort; _originalIme; mjpegStream; caps; opts; desiredCapConstraints; mobileGetActionHistory = actions_1.mobileGetActionHistory; mobileScheduleAction = actions_1.mobileScheduleAction; mobileUnscheduleAction = actions_1.mobileUnscheduleAction; performActions = actions_1.performActions; releaseActions = actions_1.releaseActions; getAlertText = alert_1.getAlertText; mobileAcceptAlert = alert_1.mobileAcceptAlert; mobileDismissAlert = alert_1.mobileDismissAlert; postAcceptAlert = alert_1.postAcceptAlert; postDismissAlert = alert_1.postDismissAlert; mobileInstallMultipleApks = app_management_1.mobileInstallMultipleApks; mobileGetBatteryInfo = battery_1.mobileGetBatteryInfo; active = element_1.active; getAttribute = element_1.getAttribute; elementEnabled = element_1.elementEnabled; elementDisplayed = element_1.elementDisplayed; elementSelected = element_1.elementSelected; getName = element_1.getName; getLocation = element_1.getLocation; getSize = element_1.getSize; getElementRect = element_1.getElementRect; getElementScreenshot = element_1.getElementScreenshot; getText = element_1.getText; setValueImmediate = element_1.setValueImmediate; doSetElementValue = element_1.doSetElementValue; click = element_1.click; clear = element_1.clear; mobileReplaceElementValue = element_1.mobileReplaceElementValue; doFindElementOrEls = find_1.doFindElementOrEls; mobileClickGesture = gestures_1.mobileClickGesture; mobileDoubleClickGesture = gestures_1.mobileDoubleClickGesture; mobileDragGesture = gestures_1.mobileDragGesture; mobileFlingGesture = gestures_1.mobileFlingGesture; mobileLongClickGesture = gestures_1.mobileLongClickGesture; mobilePinchCloseGesture = gestures_1.mobilePinchCloseGesture; mobilePinchOpenGesture = gestures_1.mobilePinchOpenGesture; mobileScroll = gestures_1.mobileScroll; mobileScrollBackTo = gestures_1.mobileScrollBackTo; mobileScrollGesture = gestures_1.mobileScrollGesture; mobileSwipeGesture = gestures_1.mobileSwipeGesture; pressKeyCode = keyboard_1.pressKeyCode; longPressKeyCode = keyboard_1.longPressKeyCode; mobilePressKey = keyboard_1.mobilePressKey; mobileType = keyboard_1.mobileType; doSendKeys = keyboard_1.doSendKeys; keyevent = keyboard_1.keyevent; getPageSource = misc_1.getPageSource; getOrientation = misc_1.getOrientation; setOrientation = misc_1.setOrientation; openNotifications = misc_1.openNotifications; suspendChromedriverProxy = misc_1.suspendChromedriverProxy; mobileGetDeviceInfo = misc_1.mobileGetDeviceInfo; mobileResetAccessibilityCache = misc_1.mobileResetAccessibilityCache; mobileListWindows = windows_1.mobileListWindows; mobileListDisplays = windows_1.mobileListDisplays; getClipboard = clipboard_1.getClipboard; setClipboard = clipboard_1.setClipboard; setUrl = navigation_1.setUrl; mobileDeepLink = navigation_1.mobileDeepLink; back = navigation_1.back; mobileScreenshots = screenshot_1.mobileScreenshots; mobileViewportScreenshot = screenshot_1.mobileViewportScreenshot; getScreenshot = screenshot_1.getScreenshot; getViewportScreenshot = screenshot_1.getViewportScreenshot; getStatusBarHeight = viewport_1.getStatusBarHeight; getDevicePixelRatio = viewport_1.getDevicePixelRatio; getDisplayDensity = viewport_1.getDisplayDensity; getViewPortRect = viewport_1.getViewPortRect; getWindowRect = viewport_1.getWindowRect; getWindowSize = viewport_1.getWindowSize; mobileViewPortRect = viewport_1.mobileViewPortRect; prepareSessionApp = aut_1.prepareSessionApp; checkAppPresent = aut_1.checkAppPresent; initAUT = aut_1.initAUT; ensureAppStarts = aut_1.ensureAppStarts; allocateSystemPort = uiautomator2_server_1.allocateSystemPort; releaseSystemPort = uiautomator2_server_1.releaseSystemPort; allocateMjpegServerPort = uiautomator2_server_1.allocateMjpegServerPort; releaseMjpegServerPort = uiautomator2_server_1.releaseMjpegServerPort; performSessionPreExecSetup = uiautomator2_server_1.performPreExecSetup; performSessionExecution = uiautomator2_server_1.performExecution; performSessionPostExecSetup = uiautomator2_server_1.performPostExecSetup; startUiAutomator2Session = uiautomator2_server_1.startSession; initUiAutomator2Server = uiautomator2_server_1.initServer; requireUiautomator2 = uiautomator2_server_1.requireServer; constructor(opts = {}, shouldValidateCaps = true) { // `shell` overwrites adb.shell, so remove // @ts-expect-error FIXME: what is this? delete opts.shell; super(opts, shouldValidateCaps); this.locatorStrategies = [ 'xpath', 'id', 'class name', 'accessibility id', 'css selector', '-android uiautomator', ]; this.desiredCapConstraints = structuredClone(constraints_1.default); this.jwpProxyActive = false; this.jwpProxyAvoid = NO_PROXY; this._originalIme = null; this.settings = new driver_1.DeviceSettings({ ignoreUnimportantViews: false, allowInvisibleElements: false }, this.onSettingsUpdate.bind(this)); // handle webview mechanics from AndroidDriver this.sessionChromedrivers = {}; this.caps = {}; this.opts = opts; // memoize functions here, so that they are done on a per-instance basis this.getStatusBarHeight = (0, utils_1.memoize)(this.getStatusBarHeight); this.getDevicePixelRatio = (0, utils_1.memoize)(this.getDevicePixelRatio); } get driverData() { // TODO fill out resource info here return {}; } validateDesiredCaps(caps) { return super.validateDesiredCaps(caps); } async createSession(w3cCaps1, w3cCaps2, w3cCaps3, driverData) { try { // TODO handle otherSessionData for multiple sessions const [sessionId, caps] = (await driver_1.BaseDriver.prototype.createSession.call(this, w3cCaps1, w3cCaps2, w3cCaps3, driverData)); const startSessionOpts = { ...caps, platform: 'LINUX', webStorageEnabled: false, takesScreenshot: true, javascriptEnabled: true, databaseEnabled: false, networkConnectionEnabled: true, locationContextEnabled: false, warnings: {}, desired: caps, }; const defaultOpts = { fullReset: false, autoLaunch: true, adbPort: appium_adb_1.DEFAULT_ADB_PORT, androidInstallTimeout: 90000, }; (0, utils_1.assignDefaults)(this.opts, defaultOpts); this.opts.adbPort = this.opts.adbPort || appium_adb_1.DEFAULT_ADB_PORT; // get device udid for this session const { udid, emPort } = await this.getDeviceInfoFromCaps(); this.opts.udid = udid; // @ts-expect-error do not put random stuff on opts this.opts.emPort = emPort; // now that we know our java version and device info, we can create our // ADB instance this.adb = await this.createADB(); if (this.isChromeSession && this.opts.browserName) { this.log.info(`We're going to run a Chrome-based session`); const { pkg, activity: defaultActivity } = appium_android_driver_1.utils.getChromePkg(this.opts.browserName); let activity = defaultActivity; try { activity = await this.adb.resolveLaunchableActivity(pkg); } catch (e) { this.log.warn(`Using the default ${pkg} activity ${activity}. Original error: ${e.message}`); } this.opts.appPackage = this.caps.appPackage = pkg; this.opts.appActivity = this.caps.appActivity = activity; this.log.info(`Chrome-type package and activity are ${pkg} and ${activity}`); } await this.prepareSessionApp(); const result = await this.startUiAutomator2Session(startSessionOpts); 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(); } return [sessionId, result]; } catch (e) { await this.deleteSession(); throw e; } } async getDeviceDetails() { const [pixelRatio, statBarHeight, viewportRect, { apiVersion, platformVersion, manufacturer, model, realDisplaySize, displayDensity },] = await Promise.all([ this.getDevicePixelRatio(), this.getStatusBarHeight(), this.getViewPortRect(), this.mobileGetDeviceInfo(), ]); return { pixelRatio, statBarHeight, viewportRect, deviceApiLevel: Number.parseInt(String(apiVersion), 10), platformVersion, deviceManufacturer: manufacturer, deviceModel: model, deviceScreenSize: realDisplaySize, deviceScreenDensity: displayDensity, }; } async getSession() { const sessionData = await driver_1.BaseDriver.prototype.getSession.call(this); this.log.debug('Getting session details from server to mix in'); const uia2Data = (await this.requireUiautomator2().jwproxy.command('/', 'GET', {})); return { ...sessionData, ...uia2Data }; } async deleteSession() { this.log.debug('Deleting UiAutomator2 session'); const screenRecordingStopTasks = [ async () => { if (this._screenRecordingProperties) { await this.stopRecordingScreen(); } }, async () => { if (await this.mobileIsMediaProjectionRecordingRunning()) { await this.mobileStopMediaProjectionRecording(); } }, async () => { if (this._screenStreamingProps) { await this.mobileStopScreenStreaming(); } }, ]; try { await this.stopChromedriverProxies(); } catch (err) { this.log.warn(`Unable to stop ChromeDriver proxies: ${err.message}`); } if (this.jwpProxyActive) { try { await this.uiautomator2.deleteSession(); } catch (err) { this.log.warn(`Unable to proxy deleteSession to UiAutomator2: ${err.message}`); } this.jwpProxyActive = false; } if (this.adb) { await Promise.all(screenRecordingStopTasks.map((task) => (async () => { try { await task(); } catch { } })())); if (this.opts.appPackage) { if (!this.isChromeSession && ((!this.opts.dontStopAppOnReset && !this.opts.noReset) || (this.opts.noReset && this.opts.shouldTerminateApp))) { try { await this.adb.forceStop(this.opts.appPackage); } catch (err) { this.log.warn(`Unable to force stop app: ${err.message}`); } } if (this.opts.fullReset && !this.opts.skipUninstall) { this.log.debug(`Capability 'fullReset' set to 'true', Uninstalling '${this.opts.appPackage}'`); try { await this.adb.uninstallApk(this.opts.appPackage); } catch (err) { this.log.warn(`Unable to uninstall app: ${err.message}`); } } } // This value can be true if test target device is <= 26 if (this._wasWindowAnimationDisabled) { this.log.info('Restoring window animation state'); await this.settingsApp.setAnimationState(true); } if (this._originalIme) { try { await this.adb.setIME(this._originalIme); } catch (e) { this.log.warn(`Cannot restore the original IME: ${e.message}`); } } try { await this.releaseSystemPort(); } catch (error) { this.log.warn(`Unable to remove system port forward: ${error.message}`); // Ignore, this block will also be called when we fall in catch block // and before even port forward. } try { await this.releaseMjpegServerPort(); } catch (error) { this.log.warn(`Unable to remove MJPEG server port forward: ${error.message}`); // Ignore, this block will also be called when we fall in catch block // and before even port forward. } if ((await this.adb.getApiLevel()) >= 28) { // Android P this.log.info('Restoring hidden api policy to the device default configuration'); await this.adb.setDefaultHiddenApiPolicy(!!this.opts.ignoreHiddenApiPolicyError); } } if (this.mjpegStream) { this.log.info('Closing MJPEG stream'); this.mjpegStream.stop(); } await super.deleteSession(); } async onSettingsUpdate() { // intentionally do nothing here, since commands.updateSettings proxies // settings to the uiauto2 server already } proxyActive(sessionId) { void sessionId; // we always have an active proxy to the UiAutomator2 server return true; } canProxy(sessionId) { void sessionId; // we can always proxy to the uiautomator2 server return true; } getProxyAvoidList() { // we are maintaining two sets of NO_PROXY lists, one for chromedriver(CHROME_NO_PROXY) // and one for uiautomator2(NO_PROXY), based on current context will return related NO_PROXY list if (support_1.util.hasValue(this.chromedriver)) { // if the current context is webview(chromedriver), then return CHROME_NO_PROXY list this.jwpProxyAvoid = CHROME_NO_PROXY; } else { this.jwpProxyAvoid = NO_PROXY; } if (this.opts.nativeWebScreenshot) { this.jwpProxyAvoid = [ ...this.jwpProxyAvoid, ['GET', new RegExp('^/session/[^/]+/screenshot')], ]; } return this.jwpProxyAvoid; } // @ts-expect-error narrower parameter type than the base class override allows async updateSettings(settings) { await this.settings.update(settings); await this.requireUiautomator2().jwproxy.command('/appium/settings', 'POST', { settings }); } async getSettings() { const driverSettings = this.settings.getSettings(); const serverSettings = (await this.requireUiautomator2().jwproxy.command('/appium/settings', 'GET')); return { ...driverSettings, ...serverSettings }; } // needed to make the typechecker happy async getAppiumSessionCapabilities() { return (await super.getAppiumSessionCapabilities()); } requireAdb() { const adb = this.adb; if (!adb) { throw this.log.errorWithException('ADB must be initialized before this operation'); } return adb; } } exports.AndroidUiautomator2Driver = AndroidUiautomator2Driver; //# sourceMappingURL=driver.js.map