appium-uiautomator2-driver
Version:
UiAutomator2 integration for Appium
495 lines • 23.5 kB
JavaScript
import { DEFAULT_ADB_PORT } from 'appium-adb';
import { AndroidDriver, utils } from 'appium-android-driver';
import { BaseDriver, DeviceSettings } from 'appium/driver.js';
import { mjpeg, util } from 'appium/support.js';
import UIAUTOMATOR2_CONSTRAINTS from './constraints.js';
import { newMethodMap } from './method-map.js';
import { assignDefaults, memoize } from './utils/index.js';
import { allocateMjpegServerPort, allocateSystemPort, initServer, performExecution, performPostExecSetup, performPreExecSetup, releaseMjpegServerPort, releaseSystemPort, requireServer, startSession, } from './uiautomator2-server/index.js';
import { mobileGetActionHistory, mobileScheduleAction, mobileUnscheduleAction, performActions, releaseActions, } from './commands/actions.js';
import { getAlertText, mobileAcceptAlert, mobileDismissAlert, postAcceptAlert, postDismissAlert, } from './commands/alert.js';
import { mobileInstallMultipleApks } from './commands/app-management.js';
import { checkAppPresent, ensureAppStarts, initAUT, prepareSessionApp } from './commands/aut.js';
import { mobileGetBatteryInfo } from './commands/battery.js';
import { getClipboard, setClipboard } from './commands/clipboard.js';
import { active, getAttribute, elementEnabled, elementDisplayed, elementSelected, getName, getLocation, getSize, getElementRect, getElementScreenshot, getText, setValueImmediate, doSetElementValue, click, clear, mobileReplaceElementValue, } from './commands/element.js';
import { doFindElementOrEls } from './commands/find.js';
import { mobileClickGesture, mobileDoubleClickGesture, mobileDragGesture, mobileFlingGesture, mobileLongClickGesture, mobilePinchCloseGesture, mobilePinchOpenGesture, mobileScroll, mobileScrollBackTo, mobileScrollGesture, mobileSwipeGesture, } from './commands/gestures.js';
import { pressKeyCode, longPressKeyCode, mobilePressKey, mobileType, doSendKeys, keyevent, } from './commands/keyboard.js';
import { getPageSource, getOrientation, setOrientation, openNotifications, suspendChromedriverProxy, mobileGetDeviceInfo, mobileResetAccessibilityCache, } from './commands/misc.js';
import { mobileListWindows, mobileListDisplays } from './commands/windows.js';
import { setUrl, mobileDeepLink, back } from './commands/navigation.js';
import { mobileScreenshots, mobileViewportScreenshot, getScreenshot, getViewportScreenshot, } from './commands/screenshot.js';
import { getStatusBarHeight, getDevicePixelRatio, getDisplayDensity, getViewPortRect, getWindowRect, getWindowSize, mobileViewPortRect, } from './commands/viewport.js';
import { executeMethodMap } from './execute-method-map.js';
import { sessionClaimHandler } from './session-claim-handler.js';
// 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 AndroidDriver {
static newMethodMap = newMethodMap;
static executeMethodMap = executeMethodMap;
uiautomator2;
systemPort;
_originalIme;
mjpegStream;
caps;
opts;
desiredCapConstraints;
mobileGetActionHistory = mobileGetActionHistory;
mobileScheduleAction = mobileScheduleAction;
mobileUnscheduleAction = mobileUnscheduleAction;
performActions = performActions;
releaseActions = releaseActions;
getAlertText = getAlertText;
mobileAcceptAlert = mobileAcceptAlert;
mobileDismissAlert = mobileDismissAlert;
postAcceptAlert = postAcceptAlert;
postDismissAlert = postDismissAlert;
mobileInstallMultipleApks = mobileInstallMultipleApks;
mobileGetBatteryInfo = mobileGetBatteryInfo;
active = active;
getAttribute = getAttribute;
elementEnabled = elementEnabled;
elementDisplayed = elementDisplayed;
elementSelected = elementSelected;
getName = getName;
getLocation = getLocation;
getSize = getSize;
getElementRect = getElementRect;
getElementScreenshot = getElementScreenshot;
getText = getText;
setValueImmediate = setValueImmediate;
doSetElementValue = doSetElementValue;
click = click;
clear = clear;
mobileReplaceElementValue = mobileReplaceElementValue;
doFindElementOrEls = doFindElementOrEls;
mobileClickGesture = mobileClickGesture;
mobileDoubleClickGesture = mobileDoubleClickGesture;
mobileDragGesture = mobileDragGesture;
mobileFlingGesture = mobileFlingGesture;
mobileLongClickGesture = mobileLongClickGesture;
mobilePinchCloseGesture = mobilePinchCloseGesture;
mobilePinchOpenGesture = mobilePinchOpenGesture;
mobileScroll = mobileScroll;
mobileScrollBackTo = mobileScrollBackTo;
mobileScrollGesture = mobileScrollGesture;
mobileSwipeGesture = mobileSwipeGesture;
pressKeyCode = pressKeyCode;
longPressKeyCode = longPressKeyCode;
mobilePressKey = mobilePressKey;
mobileType = mobileType;
doSendKeys = doSendKeys;
keyevent = keyevent;
getPageSource = getPageSource;
getOrientation = getOrientation;
setOrientation = setOrientation;
openNotifications = openNotifications;
suspendChromedriverProxy = suspendChromedriverProxy;
mobileGetDeviceInfo = mobileGetDeviceInfo;
mobileResetAccessibilityCache = mobileResetAccessibilityCache;
mobileListWindows = mobileListWindows;
mobileListDisplays = mobileListDisplays;
getClipboard = getClipboard;
setClipboard = setClipboard;
setUrl = setUrl;
mobileDeepLink = mobileDeepLink;
back = back;
mobileScreenshots = mobileScreenshots;
mobileViewportScreenshot = mobileViewportScreenshot;
getScreenshot = getScreenshot;
getViewportScreenshot = getViewportScreenshot;
getStatusBarHeight = getStatusBarHeight;
getDevicePixelRatio = getDevicePixelRatio;
getDisplayDensity = getDisplayDensity;
getViewPortRect = getViewPortRect;
getWindowRect = getWindowRect;
getWindowSize = getWindowSize;
mobileViewPortRect = mobileViewPortRect;
prepareSessionApp = prepareSessionApp;
checkAppPresent = checkAppPresent;
initAUT = initAUT;
ensureAppStarts = ensureAppStarts;
allocateSystemPort = allocateSystemPort;
releaseSystemPort = releaseSystemPort;
allocateMjpegServerPort = allocateMjpegServerPort;
releaseMjpegServerPort = releaseMjpegServerPort;
performSessionPreExecSetup = performPreExecSetup;
performSessionExecution = performExecution;
performSessionPostExecSetup = performPostExecSetup;
startUiAutomator2Session = startSession;
initUiAutomator2Server = initServer;
requireUiautomator2 = 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(UIAUTOMATOR2_CONSTRAINTS);
this.jwpProxyActive = false;
this.jwpProxyAvoid = NO_PROXY;
this._originalIme = null;
this.settings = new 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 = memoize(this.getStatusBarHeight);
this.getDevicePixelRatio = 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 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: DEFAULT_ADB_PORT,
androidInstallTimeout: 90000,
};
assignDefaults(this.opts, defaultOpts);
this.opts.adbPort = this.opts.adbPort || 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;
await sessionClaimHandler.registerActiveSession(this);
await sessionClaimHandler.claimSessionUdid(this);
// 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 } = 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 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 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 onIpcInit() {
await sessionClaimHandler.registerActiveSession(this);
}
async deleteSession() {
sessionClaimHandler.unregisterActiveSession(this);
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 (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;
}
}
export { AndroidUiautomator2Driver };
//# sourceMappingURL=driver.js.map