appium-espresso-driver
Version:
Espresso integration for Appium
190 lines (170 loc) • 6.94 kB
text/typescript
import type {PostProcessOptions, PostProcessResult, CachedAppInfo} from '@appium/types';
import {utils} from 'appium-android-driver';
import {fs, tempDir, zip} from 'appium/support.js';
import {SETTINGS_HELPER_ID} from 'io.appium.settings';
import path from 'node:path';
import type {EspressoDriver} from '../driver.js';
import {TEST_APK_PKG} from './server/index.js';
import {isPlainObject} from '../utils/index.js';
export const APK_EXT = '.apk';
export const AAB_EXT = '.aab';
export const SUPPORTED_EXTENSIONS = [APK_EXT, AAB_EXT];
/** Cached app entry with integrity path (narrower than {@link CachedAppInfo}). */
export type StrictCachedAppInfo = CachedAppInfo & {fullPath: string};
/** Whether the session targets an app already installed on the device (no app path). */
export function isAppOnDevice(driver: EspressoDriver): boolean {
const appPackage = driver.opts.appPackage;
return !driver.opts.app && !!appPackage && driver.helpers.isPackageOrBundle(appPackage);
}
/**
* Unzip the given app path and return the first package that has SUPPORTED_EXTENSIONS
* in the archived file.
*
* @param appPath The path to app file.
* @returns The path to an unzipped app file path.
* @throws If the zip did not have any SUPPORTED_EXTENSIONS packages.
*/
export async function unzipApp(this: EspressoDriver, appPath: string): Promise<string> {
const useSystemUnzipEnv = process.env.APPIUM_PREFER_SYSTEM_UNZIP;
const useSystemUnzip =
!useSystemUnzipEnv || !['0', 'false'].includes(useSystemUnzipEnv.toLowerCase());
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) {
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, sortedBundleItems[0]);
this.log.debug(`'${unzippedAppPath}' is the unzipped file from '${appPath}'`);
return unzippedAppPath;
}
/** Post-processes configured apps and reuses a valid cache entry when possible. */
export async function onPostConfigureApp(
this: EspressoDriver,
opts: PostProcessOptions,
): Promise<PostProcessResult | undefined> {
const {cachedAppInfo, isUrl, appPath} = opts;
if (!appPath) {
return undefined;
}
const presignApp = async (appLocation: string) => {
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))) {
await this.adb.sign(appLocation);
}
};
const hasApkExt = (p: string) => p.toLowerCase().endsWith(APK_EXT);
const hasAabExt = (p: string) => p.toLowerCase().endsWith(AAB_EXT);
const extractUniversalApk = async (shouldExtract: boolean, p: string) =>
shouldExtract ? p : await this.adb.extractUniversalApk(p);
let pathInCache: string | null = null;
let isResultAppPathAlreadyCached = false;
if (isCachedAppInfo(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;
}
}
const isApk = hasApkExt(appPath);
const shouldResultAppPathBeCached = !isApk || (isApk && isUrl);
if (!isResultAppPathAlreadyCached) {
if (shouldResultAppPathBeCached) {
let unzippedAppPath: string | undefined;
let isUnzippedApk = false;
if (!(hasApkExt(appPath) || hasAabExt(appPath))) {
unzippedAppPath = await this.unzipApp(appPath);
isUnzippedApk = hasApkExt(unzippedAppPath);
}
pathInCache = unzippedAppPath
? await extractUniversalApk(isUnzippedApk, unzippedAppPath)
: await extractUniversalApk(isApk, appPath);
if (!isApk && isUrl) {
await fs.rimraf(appPath);
}
if (unzippedAppPath !== undefined && hasAabExt(unzippedAppPath)) {
await fs.rimraf(unzippedAppPath);
}
if (pathInCache == null) {
throw this.log.errorWithException('Expected a cached app path after post-processing');
}
await presignApp(pathInCache);
} else if (isApk) {
await presignApp(appPath);
}
}
return shouldResultAppPathBeCached && pathInCache != null ? {appPath: pathInCache} : undefined;
}
/** Prepare the application under test on the device. */
export async function initAUT(this: EspressoDriver): Promise<void> {
if (this.opts.uninstallOtherPackages) {
await this.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.resetAUT();
}
}
const appPackage = this.opts.appPackage;
if (!appPackage) {
throw this.log.errorWithException('appPackage is required');
}
if (!this.opts.skipUninstall) {
await this.adb.uninstallApk(appPackage);
}
if (this.opts.app) {
await this.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: unknown) {
const stderr =
typeof e === 'object' &&
e !== null &&
'stderr' in e &&
typeof (e as {stderr: unknown}).stderr === 'string'
? (e as {stderr: string}).stderr
: undefined;
const message = e instanceof Error ? e.message : String(e);
this.log.warn(
`Cannot add server packages to the Doze whitelist. Original error: ` + (stderr || message),
);
}
}
/** Type guard for cached app metadata shape. */
function isCachedAppInfo(value: unknown): value is StrictCachedAppInfo {
return (
isPlainObject(value) &&
typeof value.packageHash === 'string' &&
typeof value.fullPath === 'string'
);
}