appium-espresso-driver
Version:
Espresso integration for Appium
779 lines (693 loc) • 31.5 kB
text/typescript
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type {
DefaultCreateSessionResult,
DriverData,
ExternalDriver,
InitialOpts,
StringRecord,
SingularSessionData,
RouteMatcher,
SessionCapabilities
} from '@appium/types';
import type { EspressoConstraints } from './constraints';
import _ from 'lodash';
import path from 'path';
import B from 'bluebird';
import { errors, isErrorType, DeviceSettings, BaseDriver} from 'appium/driver';
import { EspressoRunner, TEST_APK_PKG } from './espresso-runner';
import { fs, tempDir, zip } from 'appium/support';
import * as appManagementCmds from './commands/app-management';
import * as contextCmds from './commands/context';
import * as elementCmds from './commands/element';
import * as miscCmds from './commands/misc';
import * as servicesCmds from './commands/services';
import * as screenshotCmds from './commands/screenshot';
import * as idlingResourcesCmds from './commands/idling-resources';
import * as actionsCmds from './commands/actions';
import * as clipboardCmds from './commands/clipboard';
import { DEFAULT_ADB_PORT } from 'appium-adb';
import { AndroidDriver, utils } from 'appium-android-driver';
import { SETTINGS_HELPER_ID } from 'io.appium.settings';
import { ESPRESSO_CONSTRAINTS } from './constraints';
import { findAPortNotInUse } from 'portscanner';
import { retryInterval } from 'asyncbox';
import { qualifyActivityName, getPackageInfo } from './utils';
import { newMethodMap } from './method-map';
import type { EspressoDriverCaps, EspressoDriverOpts, W3CEspressoDriverCaps } from './types';
import { executeMethodMap } from './execute-method-map';
// The range of ports we can use on the system for communicating to the
// Espresso HTTP server on the device
const SYSTEM_PORT_RANGE = [8300, 8399];
// This is the port that the espresso server listens to on the device. We will
// forward one of the ports above on the system to this port on the device.
const DEVICE_PORT = 6791;
// 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: RouteMatcher[] = [
['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: RouteMatcher[] = [
['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')],
];
const APK_EXT = '.apk';
const AAB_EXT = '.aab';
const SUPPORTED_EXTENSIONS = [APK_EXT, AAB_EXT];
export class EspressoDriver extends AndroidDriver implements ExternalDriver<
EspressoConstraints,
string,
StringRecord
> {
_originalIme: string|null;
espresso: EspressoRunner;
wasAnimationEnabled?: boolean;
static newMethodMap = newMethodMap;
static executeMethodMap = executeMethodMap;
override caps: EspressoDriverCaps;
override opts: EspressoDriverOpts;
override desiredCapConstraints: EspressoConstraints;
constructor (opts: InitialOpts = {} as InitialOpts, shouldValidateCaps = true) {
// `shell` overwrites adb.shell, so remove
// @ts-expect-error FIXME: what is this?
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 = {} as EspressoDriverCaps;
this.opts = opts as EspressoDriverOpts;
this.settings = new DeviceSettings({}, this.onSettingsUpdate.bind(this));
this.chromedriver = undefined;
}
override async getSession(): Promise<SingularSessionData<EspressoConstraints>> {
return await BaseDriver.prototype.getSession.call(this);
}
// needed to make the typechecker happy
async getAppiumSessionCapabilities(): Promise<SessionCapabilities<EspressoConstraints>> {
return (await super.getAppiumSessionCapabilities()) as SessionCapabilities<EspressoConstraints>;
}
async createSession (
w3cCaps1: W3CEspressoDriverCaps,
w3cCaps2?: W3CEspressoDriverCaps,
w3cCaps3?: W3CEspressoDriverCaps,
driverData?: DriverData[]
): Promise<any> {
try {
const [sessionId, caps] = (await BaseDriver.prototype.createSession.call(
this,
w3cCaps1,
w3cCaps2,
w3cCaps3,
driverData
)) as DefaultCreateSessionResult<EspressoConstraints>;
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 as unknown as AndroidDriver).defaultContextName();
const defaultOpts = {
fullReset: false,
autoLaunch: true,
adbPort: DEFAULT_ADB_PORT,
androidInstallTimeout: 90000
};
_.defaults(this.opts, defaultOpts);
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(SYSTEM_PORT_RANGE[0], SYSTEM_PORT_RANGE[1]);
this.opts.adbPort = this.opts.adbPort || DEFAULT_ADB_PORT;
// get device udid for this session
const {udid, emPort} = await (this as unknown as AndroidDriver).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 as unknown as AndroidDriver).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: 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();
e.message += `${_.endsWith(e.message, '.') ? '' : '.'} Check ` +
'https://github.com/appium/appium-espresso-driver#troubleshooting ' +
'regarding advanced session startup troubleshooting.';
if (isErrorType(e, errors.SessionNotCreatedError)) {
throw e;
}
const err = new errors.SessionNotCreatedError(e.message);
err.stack = e.stack;
throw err;
}
}
/**
* Unzip the given app path and return the first package that has SUPPORTED_EXTENSIONS
* in the archived file.
*
* @param {string} appPath The path to app file.
* @returns {Promise<string>} Returns the path to an unzipped app file path.
* @throws Raise an exception if the zip did not have any SUPPORTED_EXTENSIONS packages.
*/
async unzipApp (appPath: string): Promise<string> {
const useSystemUnzipEnv = process.env.APPIUM_PREFER_SYSTEM_UNZIP;
const useSystemUnzip = _.isEmpty(useSystemUnzipEnv)
|| !['0', 'false'].includes(_.toLower(useSystemUnzipEnv));
const tmpRoot = await tempDir.openDir();
await zip.extractAllTo(appPath, tmpRoot, {useSystemUnzip});
const globPattern = `**/*.+(${SUPPORTED_EXTENSIONS.map((ext) => ext.replace(/^\./, '')).join('|')})`;
const sortedBundleItems = (await fs.glob(globPattern, {
cwd: tmpRoot,
})).sort((a, b) => a.split(path.sep).length - b.split(path.sep).length);
if (sortedBundleItems.length === 0) {
// no expected packages in the zip
throw this.log.errorWithException(
`${this.opts.app} did not have any of '${SUPPORTED_EXTENSIONS.join(', ')}' ` +
`extension packages. Please make sure the provided .zip archive contains at ` +
`least one valid application package.`
);
}
const unzippedAppPath = path.join(tmpRoot, _.first(sortedBundleItems)!);
this.log.debug(`'${unzippedAppPath}' is the unzipped file from '${appPath}'`);
return unzippedAppPath;
}
async onPostConfigureApp ({cachedAppInfo, isUrl, appPath}) {
const presignApp = async (appLocation) => {
if (this.opts.noSign) {
this.log.info('Skipping application signing because noSign capability is set to true. ' +
'Having the application under test with improper signature/non-signed will cause ' +
'Espresso automation startup failure.');
} else if (!await this.adb.checkApkCert(appLocation, this.opts.appPackage!)) {
await this.adb.sign(appLocation);
}
};
const hasApkExt = (appPath) => _.endsWith(_.toLower(appPath), APK_EXT);
const hasAabExt = (appPath) => _.endsWith(_.toLower(appPath), AAB_EXT);
const extractUniversalApk = async (shouldExtract, appPath) =>
shouldExtract ? appPath : await this.adb.extractUniversalApk(appPath);
let pathInCache = null;
let isResultAppPathAlreadyCached = false;
if (_.isPlainObject(cachedAppInfo)) {
const packageHash = await fs.hash(appPath);
if (packageHash === cachedAppInfo.packageHash && await fs.exists(cachedAppInfo.fullPath)) {
this.log.info(`Using '${cachedAppInfo.fullPath}' which is cached from '${appPath}'`);
isResultAppPathAlreadyCached = true;
pathInCache = cachedAppInfo.fullPath;
}
}
// appPath can be .zip, .apk or .aab
const isApk = hasApkExt(appPath);
// Only local .apk files that are available in-place should not be cached
const shouldResultAppPathBeCached = !isApk || (isApk && isUrl);
if (!isResultAppPathAlreadyCached) {
if (shouldResultAppPathBeCached) {
// .zip, .aab or downloaded .apk
let unzippedAppPath;
let isUnzippedApk = false;
if (!(hasApkExt(appPath) || hasAabExt(appPath))) {
unzippedAppPath = await this.unzipApp(appPath);
isUnzippedApk = hasApkExt(unzippedAppPath);
}
// unzippedAppPath or appPath has SUPPORTED_EXTENSIONS.
pathInCache = unzippedAppPath
? await extractUniversalApk(isUnzippedApk, unzippedAppPath)
: await extractUniversalApk(isApk, appPath);
if (!isApk && isUrl) {
// Clean up the temporarily downloaded .aab or .zip package
await fs.rimraf(appPath);
}
if (hasAabExt(unzippedAppPath)) {
// Cleanup the local unzipped .aab file
await fs.rimraf(/** @type {string} */ (unzippedAppPath));
}
await presignApp(pathInCache);
} else if (isApk) {
// It is probably not the best idea to modify the provided app in-place,
// but this is how it was always working
await presignApp(appPath);
}
}
return shouldResultAppPathBeCached ? {appPath: pathInCache} : false;
}
get driverData () {
// TODO fill out resource info here
return {};
}
// TODO much of this logic is duplicated from uiautomator2
async startEspressoSession(): Promise<void> {
const {manifestPayload} = await getPackageInfo();
this.log.info(`EspressoDriver version: ${manifestPayload.version}`);
// Read https://github.com/appium/appium-android-driver/pull/461 what happens if there is no setHiddenApiPolicy for Android P+
if (await this.adb.getApiLevel() >= 28) { // Android P
this.log.warn('Relaxing hidden api policy');
await this.adb.setHiddenApiPolicy('1', !!this.opts.ignoreHiddenApiPolicyError);
}
// get appPackage et al from manifest if necessary
let appInfo = await (this as unknown as AndroidDriver).getLaunchInfo();
if (appInfo) {
// and get it onto our 'opts' object so we use it from now on
Object.assign(this.opts, appInfo);
} else {
appInfo = this.opts;
}
// start an avd, set the language/locale, pick an emulator, etc...
if (this.opts.hideKeyboard) {
this._originalIme = await this.adb.defaultIME();
}
await (this as unknown as AndroidDriver).initDevice();
// Default state is window animation disabled.
await this.setWindowAnimationState(this.caps.disableWindowAnimation === false);
// set actual device name, udid
this.caps.deviceName = this.adb.curDeviceId;
this.caps.deviceUDID = this.opts.udid!;
// set up the modified espresso server etc
this.initEspressoServer();
// Further prepare the device by forwarding the espresso port
this.log.debug(`Forwarding Espresso Server port ${DEVICE_PORT} to ${this.opts.systemPort}`);
await this.adb.forwardPort(this.opts.systemPort!, DEVICE_PORT);
if (!this.opts.skipUnlock) {
// unlock the device to prepare it for testing
// @ts-ignore This is ok
await this.unlock();
} else {
this.log.debug(`'skipUnlock' capability set, so skipping device unlock`);
}
// set up app under test
// prepare our actual AUT, get it on the device, etc...
await this.initAUT();
//Adding AUT package name in the capabilities if package name not exist in caps
if (!this.caps.appPackage) {
this.caps.appPackage = appInfo.appPackage;
}
if (!this.caps.appWaitPackage) {
this.caps.appWaitPackage = appInfo.appWaitPackage || appInfo.appPackage || this.caps.appPackage;
}
if (this.caps.appActivity) {
this.caps.appActivity = qualifyActivityName(
this.caps.appActivity, this.caps.appPackage!
);
} else {
this.caps.appActivity = qualifyActivityName(appInfo.appActivity!, this.caps.appPackage!);
}
if (this.caps.appWaitActivity) {
this.caps.appWaitActivity = qualifyActivityName(this.caps.appWaitActivity, this.caps.appWaitPackage!);
} else {
this.caps.appWaitActivity = qualifyActivityName(
appInfo.appWaitActivity || appInfo.appActivity || this.caps.appActivity, this.caps.appWaitPackage!
);
}
// launch espresso and wait till its online and we have a session
await this.espresso.startSession(this.caps);
if (this.caps.autoLaunch === false) {
this.log.info(`Not waiting for the application activity to start because 'autoLaunch' is disabled`);
} else {
await this.adb.waitForActivity(
this.caps.appWaitPackage!, this.caps.appWaitActivity, this.opts.appWaitDuration
);
}
// if we want to immediately get into a webview, set our context
// appropriately
if (this.opts.autoWebview) {
await this.initWebview();
}
// now that everything has started successfully, turn on proxying so all
// subsequent session requests go straight to/from espresso
this.jwpProxyActive = true;
await this.addDeviceInfoToCaps();
}
/**
* 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: boolean): Promise<void> {
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 (): Promise<void> {
const viewName = (this as unknown as AndroidDriver).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 (): Promise<void> {
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;
}
initEspressoServer (): void {
// now that we have package and activity, we can create an instance of
// espresso with the appropriate data
this.espresso = new EspressoRunner(this.log, {
// @ts-ignore TODO: Is .host a legacy property?
host: this.opts.remoteAdbHost || this.opts.host || '127.0.0.1',
systemPort: this.opts.systemPort,
devicePort: DEVICE_PORT,
adb: this.adb,
apk: this.opts.app,
tmpDir: this.opts.tmpDir,
appPackage: this.opts.appPackage,
appActivity: this.opts.appActivity,
forceEspressoRebuild: !!this.opts.forceEspressoRebuild,
espressoBuildConfig: this.opts.espressoBuildConfig,
showGradleLog: !!this.opts.showGradleLog,
serverLaunchTimeout: this.opts.espressoServerLaunchTimeout,
androidInstallTimeout: this.opts.androidInstallTimeout,
skipServerInstallation: this.opts.skipServerInstallation,
useKeystore: this.opts.useKeystore,
keystorePath: this.opts.keystorePath,
keystorePassword: this.opts.keystorePassword,
keyAlias: this.opts.keyAlias,
keyPassword: this.opts.keyPassword,
disableSuppressAccessibilityService: this.opts.disableSuppressAccessibilityService,
reqBasePath: this.basePath,
});
this.proxyReqRes = this.espresso.proxyReqRes.bind(this.espresso);
this.proxyCommand = this.espresso.proxyCommand.bind(this.espresso);
}
async initAUT (): Promise<void> {
// Uninstall any uninstallOtherPackages which were specified in caps
if (this.opts.uninstallOtherPackages) {
await (this as unknown as AndroidDriver).uninstallOtherPackages(
utils.parseArray(this.opts.uninstallOtherPackages),
[SETTINGS_HELPER_ID, TEST_APK_PKG]
);
}
if (!this.opts.app) {
if (this.opts.fullReset) {
throw this.log.errorWithException(
'Full reset requires an app capability, use fastReset if app is not provided'
);
}
this.log.debug('No app capability. Assuming it is already on the device');
if (this.opts.fastReset) {
await (this as unknown as AndroidDriver).resetAUT();
}
}
if (!this.opts.skipUninstall) {
await this.adb.uninstallApk(this.opts.appPackage!);
}
if (this.opts.app) {
await (this as unknown as AndroidDriver).installAUT();
}
if (this.opts.skipServerInstallation) {
this.log.debug('skipServerInstallation capability is set. Not installig espresso-server ');
} else {
await this.espresso.installTestApk();
try {
await this.adb.addToDeviceIdleWhitelist(SETTINGS_HELPER_ID, TEST_APK_PKG);
} catch (e) {
this.log.warn(`Cannot add server packages to the Doze whitelist. Original error: ` +
(e.stderr || e.message));
}
}
}
override async deleteSession () {
this.log.debug('Deleting espresso session');
const screenRecordingStopTasks = [async () => {
if (!_.isEmpty(this._screenRecordingProperties)) {
await (this as unknown as AndroidDriver).stopRecordingScreen();
}
}, async () => {
if (await (this as unknown as AndroidDriver).mobileIsMediaProjectionRecordingRunning()) {
await (this as unknown as AndroidDriver).mobileStopMediaProjectionRecording();
}
}, async () => {
if (!_.isEmpty(this._screenStreamingProps)) {
await (this as unknown as AndroidDriver).mobileStopScreenStreaming();
}
}];
if (this.espresso) {
if (this.jwpProxyActive) {
await this.espresso.deleteSession();
}
// @ts-ignore This is ok
this.espresso = null;
}
this.jwpProxyActive = false;
if (this.adb) {
await B.all(screenRecordingStopTasks.map((task) => {
(async () => {
try {
await task();
} catch {}
})();
}));
if (this.wasAnimationEnabled) {
try {
await this.settingsApp.setAnimationState(true);
} catch (err) {
this.log.warn(`Unable to reset animation: ${err.message}`);
}
}
if (this._originalIme) {
try {
await this.adb.setIME(this._originalIme);
} catch (e) {
this.log.warn(`Cannot restore the original IME: ${e.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();
if (this.opts.systemPort) {
try {
await this.adb.removePortForward(this.opts.systemPort);
} catch (error) {
this.log.warn(`Unable to remove port forward '${error.message}'`);
//Ignore, this block will also be called when we fall in catch block
// and before even port forward.
}
}
}
async onSettingsUpdate () {
// intentionally do nothing here, since commands.updateSettings proxies
// settings to the espresso server already
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
proxyActive (sessionId) {
// we always have an active proxy to the espresso server
return true;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
canProxy (sessionId) {
// we can always proxy to the espresso server
return true;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getProxyAvoidList (sessionId): RouteMatcher[] {
// 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 = _.isNil(this.chromedriver) ? NO_PROXY : CHROME_NO_PROXY;
if (this.opts.nativeWebScreenshot) {
this.jwpProxyAvoid = ([
...this.jwpProxyAvoid,
['GET', new RegExp('^/session/[^/]+/screenshot')]
]);
}
return this.jwpProxyAvoid;
}
get appOnDevice (): boolean {
return !this.opts.app && this.helpers.isPackageOrBundle(this.opts.appPackage!);
}
// @ts-ignore It's expected
performActions = actionsCmds.performActions;
startActivity = appManagementCmds.startActivity;
// @ts-ignore It's expected
mobileStartActivity = appManagementCmds.mobileStartActivity;
mobileWebAtoms = contextCmds.mobileWebAtoms;
// @ts-ignore It's expected
suspendChromedriverProxy = contextCmds.suspendChromedriverProxy;
// @ts-ignore It's expected
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;
// @ts-ignore It's expected
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;
// @ts-ignore It's expected
mobileStartService = servicesCmds.mobileStartService;
// @ts-ignore It's expected
mobileStopService = servicesCmds.mobileStopService;
getScreenshot = screenshotCmds.getScreenshot;
mobileScreenshots = screenshotCmds.mobileScreenshots;
mobileRegisterIdlingResources = idlingResourcesCmds.mobileRegisterIdlingResources;
mobileUnregisterIdlingResources = idlingResourcesCmds.mobileUnregisterIdlingResources;
mobileListIdlingResources = idlingResourcesCmds.mobileListIdlingResources;
mobileWaitForUIThread = idlingResourcesCmds.mobileWaitForUIThread;
}
export default EspressoDriver;