appium-espresso-driver
Version:
Espresso integration for Appium
397 lines • 19.8 kB
JavaScript
import { errors, isErrorType, DeviceSettings, BaseDriver } from 'appium/driver.js';
import * as serverCmds from './commands/server/index.js';
import * as appManagementCmds from './commands/app-management.js';
import * as contextCmds from './commands/context.js';
import * as elementCmds from './commands/element.js';
import * as miscCmds from './commands/misc.js';
import * as servicesCmds from './commands/services.js';
import * as screenshotCmds from './commands/screenshot.js';
import * as idlingResourcesCmds from './commands/idling-resources.js';
import * as actionsCmds from './commands/actions.js';
import * as clipboardCmds from './commands/clipboard.js';
import * as appInstallCmds from './commands/app-install.js';
import { DEFAULT_ADB_PORT } from 'appium-adb';
import { AndroidDriver } from 'appium-android-driver';
import { ESPRESSO_CONSTRAINTS } from './constraints.js';
import { findAPortNotInUse } from 'portscanner';
import { retryInterval } from 'asyncbox';
import { isEmptyValue } from './utils/index.js';
import { newMethodMap } from './method-map.js';
import { executeMethodMap } from './execute-method-map.js';
// NO_PROXY contains the paths that we never want to proxy to espresso server.
// TODO: Add the list of paths that we never want to proxy to espresso 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 = [
['GET', new RegExp('^/session/(?!.*/)')],
['GET', new RegExp('^/session/[^/]+/appium/capabilities')],
['GET', new RegExp('^/session/[^/]+/appium/commands')],
['GET', new RegExp('^/session/[^/]+/appium/device/current_activity')],
['GET', new RegExp('^/session/[^/]+/appium/device/current_package')],
['GET', new RegExp('^/session/[^/]+/appium/device/display_density')],
['GET', new RegExp('^/session/[^/]+/appium/device/is_keyboard_shown')],
['GET', new RegExp('^/session/[^/]+/appium/device/system_bars')],
['GET', new RegExp('^/session/[^/]+/appium/device/system_time')],
['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/[^/]+/ime/[^/]+')],
['GET', new RegExp('^/session/[^/]+/network_connection')],
['GET', new RegExp('^/session/[^/]+/timeouts')],
['GET', new RegExp('^/session/[^/]+/url')],
['POST', new RegExp('^/session/[^/]+/appium/app/background')],
['POST', new RegExp('^/session/[^/]+/appium/app/close')],
['POST', new RegExp('^/session/[^/]+/appium/app/launch')],
['POST', new RegExp('^/session/[^/]+/appium/app/reset')],
['POST', new RegExp('^/session/[^/]+/appium/app/strings')],
['POST', new RegExp('^/session/[^/]+/appium/compare_images')],
['POST', new RegExp('^/session/[^/]+/appium/device/activate_app')],
['POST', new RegExp('^/session/[^/]+/appium/device/app_installed')],
['POST', new RegExp('^/session/[^/]+/appium/device/app_state')],
['POST', new RegExp('^/session/[^/]+/appium/device/finger_print')],
['POST', new RegExp('^/session/[^/]+/appium/device/get_clipboard')],
['POST', new RegExp('^/session/[^/]+/appium/device/install_app')],
['POST', new RegExp('^/session/[^/]+/appium/device/is_locked')],
['POST', new RegExp('^/session/[^/]+/appium/device/lock')],
['POST', new RegExp('^/session/[^/]+/appium/device/pull_file')],
['POST', new RegExp('^/session/[^/]+/appium/device/pull_folder')],
['POST', new RegExp('^/session/[^/]+/appium/device/push_file')],
['POST', new RegExp('^/session/[^/]+/appium/device/remove_app')],
['POST', new RegExp('^/session/[^/]+/appium/device/start_activity')],
['POST', new RegExp('^/session/[^/]+/appium/device/terminate_app')],
['POST', new RegExp('^/session/[^/]+/appium/device/unlock')],
['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/[^/]+/context')],
['POST', new RegExp('^/session/[^/]+/execute')],
['POST', new RegExp('^/session/[^/]+/execute/async')],
['POST', new RegExp('^/session/[^/]+/execute/sync')],
['POST', new RegExp('^/session/[^/]+/execute_async')],
['POST', new RegExp('^/session/[^/]+/ime/[^/]+')],
['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/[^/]+/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')],
];
// 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')],
['POST', new RegExp('^/session/[^/]+/touch/multi/perform')],
['POST', new RegExp('^/session/[^/]+/touch/perform')],
// 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')],
];
export class EspressoDriver extends AndroidDriver {
static newMethodMap = newMethodMap;
static executeMethodMap = executeMethodMap;
_originalIme;
espresso;
wasAnimationEnabled;
caps;
opts;
desiredCapConstraints;
performActions = actionsCmds.performActions;
startActivity = appManagementCmds.startActivity;
mobileStartActivity = appManagementCmds.mobileStartActivity;
mobileWebAtoms = contextCmds.mobileWebAtoms;
suspendChromedriverProxy = contextCmds.suspendChromedriverProxy;
mobilePerformEditorAction = elementCmds.mobilePerformEditorAction;
mobileSwipe = elementCmds.mobileSwipe;
mobileOpenDrawer = elementCmds.mobileOpenDrawer;
mobileCloseDrawer = elementCmds.mobileCloseDrawer;
mobileSetDate = elementCmds.mobileSetDate;
mobileSetTime = elementCmds.mobileSetTime;
mobileNavigateTo = elementCmds.mobileNavigateTo;
mobileScrollToPage = elementCmds.mobileScrollToPage;
mobileFlashElement = elementCmds.mobileFlashElement;
mobileClickAction = elementCmds.mobileClickAction;
mobileDismissAutofill = elementCmds.mobileDismissAutofill;
mobilePressKey = miscCmds.mobilePressKey;
mobileGetDeviceInfo = miscCmds.mobileGetDeviceInfo;
mobileIsToastVisible = miscCmds.mobileIsToastVisible;
getDisplayDensity = miscCmds.getDisplayDensity;
mobileBackdoor = miscCmds.mobileBackdoor;
mobileUiautomator = miscCmds.mobileUiautomator;
mobileUiautomatorPageSource = miscCmds.mobileUiautomatorPageSource;
updateSettings = miscCmds.updateSettings;
getSettings = miscCmds.getSettings;
getClipboard = clipboardCmds.getClipboard;
mobileGetClipboard = clipboardCmds.getClipboard;
mobileSetClipboard = clipboardCmds.mobileSetClipboard;
mobileStartService = servicesCmds.mobileStartService;
mobileStopService = servicesCmds.mobileStopService;
getScreenshot = screenshotCmds.getScreenshot;
mobileScreenshots = screenshotCmds.mobileScreenshots;
mobileRegisterIdlingResources = idlingResourcesCmds.mobileRegisterIdlingResources;
mobileUnregisterIdlingResources = idlingResourcesCmds.mobileUnregisterIdlingResources;
mobileListIdlingResources = idlingResourcesCmds.mobileListIdlingResources;
mobileWaitForUIThread = idlingResourcesCmds.mobileWaitForUIThread;
startEspressoSession = serverCmds.startSession;
initEspressoServer = serverCmds.initServer;
unzipApp = appInstallCmds.unzipApp;
onPostConfigureApp = appInstallCmds.onPostConfigureApp;
initAUT = appInstallCmds.initAUT;
constructor(opts = {}, shouldValidateCaps = true) {
// `shell` overwrites adb.shell, so remove
if ('shell' in opts) {
delete opts.shell;
}
super(opts, shouldValidateCaps);
this.locatorStrategies = ['id', 'class name', 'accessibility id'];
this.desiredCapConstraints = ESPRESSO_CONSTRAINTS;
this.jwpProxyAvoid = NO_PROXY;
this._originalIme = null;
this.caps = {};
this.opts = opts;
this.settings = new DeviceSettings({}, this.onSettingsUpdate.bind(this));
this.chromedriver = undefined;
}
get driverData() {
return {};
}
get appOnDevice() {
return appInstallCmds.isAppOnDevice(this);
}
async getSession() {
return await BaseDriver.prototype.getSession.call(this);
}
// needed to make the typechecker happy
async getAppiumSessionCapabilities() {
return (await super.getAppiumSessionCapabilities());
}
async createSession(w3cCaps1, w3cCaps2, w3cCaps3, driverData) {
try {
const [sessionId, caps] = (await BaseDriver.prototype.createSession.call(this, w3cCaps1, w3cCaps2, w3cCaps3, driverData));
const serverDetails = {
platform: 'LINUX',
webStorageEnabled: false,
takesScreenshot: true,
javascriptEnabled: true,
databaseEnabled: false,
networkConnectionEnabled: true,
locationContextEnabled: false,
warnings: {},
desired: Object.assign({}, this.caps),
};
this.caps = Object.assign(serverDetails, this.caps);
this.curContext = this.defaultContextName();
const defaultOpts = {
fullReset: false,
autoLaunch: true,
adbPort: DEFAULT_ADB_PORT,
androidInstallTimeout: 90000,
};
this.opts = Object.assign({}, defaultOpts, this.opts);
if (this.isChromeSession) {
if (this.opts.app) {
this.log.warn(`'browserName' capability will be ignored`);
this.log.warn(`Chrome browser cannot be run in Espresso sessions because Espresso automation doesn't ` +
`have permission to access Chrome`);
}
else {
throw this.log.errorWithException(`Chrome browser sessions cannot be run in Espresso because Espresso ` +
`automation doesn't have permission to access Chrome`);
}
}
this.opts.systemPort =
this.opts.systemPort ||
(await findAPortNotInUse(serverCmds.SYSTEM_PORT_RANGE[0], serverCmds.SYSTEM_PORT_RANGE[1]));
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;
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.opts.app) {
// find and copy, or download and unzip an app url or path
this.opts.app = await this.helpers.configureApp(this.opts.app, {
onPostProcess: this.onPostConfigureApp.bind(this),
supportedExtensions: appInstallCmds.SUPPORTED_EXTENSIONS,
});
}
else if (this.appOnDevice) {
// the app isn't an actual app file but rather something we want to
// assume is on the device and just launch via the appPackage
this.log.info(`App file was not listed, instead we're going to run ` +
`${this.opts.appPackage} directly on the device`);
if (!(await this.adb.isAppInstalled(this.opts.appPackage))) {
throw this.log.errorWithException(`Could not find the package '${this.opts.appPackage}' installed on the device`);
}
}
await this.startEspressoSession();
return [sessionId, caps];
}
catch (e) {
await this.deleteSession();
const cause = e instanceof Error ? e : new Error(String(e));
cause.message +=
`${cause.message.endsWith('.') ? '' : '.'} Check ` +
'https://github.com/appium/appium-espresso-driver#troubleshooting ' +
'regarding advanced session startup troubleshooting.';
if (isErrorType(e, errors.SessionNotCreatedError)) {
throw e;
}
throw new errors.SessionNotCreatedError(cause.message, cause);
}
}
async deleteSession() {
this.log.debug('Deleting espresso session');
const screenRecordingStopTasks = [
async () => {
if (!isEmptyValue(this._screenRecordingProperties)) {
await this.stopRecordingScreen();
}
},
async () => {
if (await this.mobileIsMediaProjectionRecordingRunning()) {
await this.mobileStopMediaProjectionRecording();
}
},
async () => {
if (!isEmptyValue(this._screenStreamingProps)) {
await this.mobileStopScreenStreaming();
}
},
];
await serverCmds.teardown(this);
if (this.adb) {
await Promise.all(screenRecordingStopTasks.map((task) => (async () => {
try {
await task();
}
catch { }
})()));
if (this.wasAnimationEnabled) {
try {
await this.settingsApp.setAnimationState(true);
}
catch (err) {
const message = err instanceof Error ? err.message : String(err);
this.log.warn(`Unable to reset animation: ${message}`);
}
}
if (this._originalIme) {
try {
await this.adb.setIME(this._originalIme);
}
catch (e) {
const message = e instanceof Error ? e.message : String(e);
this.log.warn(`Cannot restore the original IME: ${message}`);
}
}
if (!this.isChromeSession && this.opts.appPackage && !this.opts.dontStopAppOnReset) {
await this.adb.forceStop(this.opts.appPackage);
}
if (this.opts.fullReset && !this.opts.skipUninstall && !this.appOnDevice) {
this.log.debug(`FULL_RESET set to 'true', Uninstalling '${this.opts.appPackage}'`);
await this.adb.uninstallApk(this.opts.appPackage);
}
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);
}
}
await super.deleteSession();
await serverCmds.removePortForward(this);
}
/**
* Turn on or off animation scale.
* '--no-window-animation' instrument argument for Espresso disables window animations,
* but it does not bring the animation scale back to the pre-instrument process start state in Espresso
* unlike Appium UIA2 driver case. We want to disable/enable the animation scale only in an appium espresso session as possible.
* @param isEnabled
*/
async setWindowAnimationState(isEnabled) {
const isAnimationOn = await this.adb.isAnimationOn();
const shouldDisableAnimation = !isEnabled && isAnimationOn;
const shouldEnableAnimation = isEnabled && !isAnimationOn;
if (shouldDisableAnimation) {
this.log.debug('Disabling window animation as "disableWindowAnimation" capability is set to true/fallback to default value "true"');
await this.settingsApp.setAnimationState(false);
this.wasAnimationEnabled = true;
}
else if (shouldEnableAnimation) {
this.log.debug('Enabling window animation as "disableWindowAnimation" capability is set to false');
await this.settingsApp.setAnimationState(true);
this.wasAnimationEnabled = false;
}
else {
this.log.debug(`Window animation is already ${isEnabled ? 'enabled' : 'disabled'}`);
}
}
async initWebview() {
const viewName = this.defaultWebviewName();
const timeout = this.opts.autoWebviewTimeout || 2000;
this.log.info(`Setting webview to context '${viewName}' with timeout ${timeout}ms`);
await retryInterval(timeout / 500, 500, this.setContext.bind(this), viewName);
}
async addDeviceInfoToCaps() {
const { apiVersion, platformVersion, manufacturer, model, realDisplaySize, displayDensity } = await this.mobileGetDeviceInfo();
this.caps.deviceApiLevel = parseInt(apiVersion, 10);
this.caps.platformVersion = platformVersion;
this.caps.deviceScreenSize = realDisplaySize;
this.caps.deviceScreenDensity = displayDensity;
this.caps.deviceModel = model;
this.caps.deviceManufacturer = manufacturer;
}
async onSettingsUpdate() {
// intentionally do nothing here, since commands.updateSettings proxies
// settings to the espresso server already
}
proxyActive(sessionId) {
void sessionId;
// we always have an active proxy to the espresso server
return true;
}
canProxy(sessionId) {
void sessionId;
// we can always proxy to the espresso server
return true;
}
getProxyAvoidList(sessionId) {
void sessionId;
// we are maintaining two sets of NO_PROXY lists, one for chromedriver(CHROME_NO_PROXY)
// and one for Espresso(NO_PROXY), based on current context will return related NO_PROXY list
this.jwpProxyAvoid = this.chromedriver == null ? NO_PROXY : CHROME_NO_PROXY;
if (this.opts.nativeWebScreenshot) {
this.jwpProxyAvoid = [
...this.jwpProxyAvoid,
['GET', new RegExp('^/session/[^/]+/screenshot')],
];
}
return this.jwpProxyAvoid;
}
}
export default EspressoDriver;
//# sourceMappingURL=driver.js.map