appium-uiautomator2-driver
Version:
UiAutomator2 integration for Appium
494 lines • 23.2 kB
JavaScript
;
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