appium-xcuitest-driver
Version:
Appium driver for iOS using XCUITest for backend
657 lines • 28.2 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SUPPORTED_EXTENSIONS = exports.IPA_EXT = exports.APP_EXT = exports.SAFARI_BUNDLE_ID = void 0;
exports.verifyApplicationPlatform = verifyApplicationPlatform;
exports.parseLocalizableStrings = parseLocalizableStrings;
exports.unzipFile = unzipFile;
exports.unzipStream = unzipStream;
exports.buildSafariPreferences = buildSafariPreferences;
exports.onDownloadApp = onDownloadApp;
exports.onPostConfigureApp = onPostConfigureApp;
const lodash_1 = __importDefault(require("lodash"));
const path_1 = __importDefault(require("path"));
const support_1 = require("appium/support");
const logger_js_1 = __importDefault(require("./logger.js"));
const node_os_1 = __importDefault(require("node:os"));
const teen_process_1 = require("teen_process");
const bluebird_1 = __importDefault(require("bluebird"));
const node_child_process_1 = require("node:child_process");
const node_assert_1 = __importDefault(require("node:assert"));
const utils_js_1 = require("./utils.js");
const STRINGSDICT_RESOURCE = '.stringsdict';
const STRINGS_RESOURCE = '.strings';
exports.SAFARI_BUNDLE_ID = 'com.apple.mobilesafari';
exports.APP_EXT = '.app';
exports.IPA_EXT = '.ipa';
const ZIP_EXT = '.zip';
const SAFARI_OPTS_ALIASES_MAP = /** @type {const} */ ({
safariAllowPopups: [
['WebKitJavaScriptCanOpenWindowsAutomatically', 'JavaScriptCanOpenWindowsAutomatically'],
(x) => Number(Boolean(x)),
],
safariIgnoreFraudWarning: [['WarnAboutFraudulentWebsites'], (x) => Number(!x)],
safariOpenLinksInBackground: [['OpenLinksInBackground'], (x) => Number(Boolean(x))],
});
const MAX_ARCHIVE_SCAN_DEPTH = 1;
exports.SUPPORTED_EXTENSIONS = [exports.IPA_EXT, exports.APP_EXT];
const MACOS_RESOURCE_FOLDER = '__MACOSX';
const SANITIZE_REPLACEMENT = '-';
const INTEL_ARCH = 'x86_64';
/**
* Verify whether the given application is compatible to the
* platform where it is going to be installed and tested.
*
* @this {XCUITestDriver}
* @returns {Promise<void>}
* @throws {Error} If bundle architecture does not match the expected device architecture.
*/
async function verifyApplicationPlatform() {
this.log.debug('Verifying application platform');
const supportedPlatforms = await this.appInfosCache.extractAppPlatforms(this.opts.app);
const isTvOS = (0, utils_js_1.isTvOs)(this.opts.platformName);
const prefix = isTvOS ? 'AppleTV' : 'iPhone';
const suffix = this.isSimulator() ? 'Simulator' : 'OS';
const dstPlatform = `${prefix}${suffix}`;
if (!supportedPlatforms.includes(dstPlatform)) {
throw new Error(`${this.isSimulator() ? 'Simulator' : 'Real device'} architecture is not supported by the ${this.opts.bundleId} application. ` +
`Make sure the correct deployment target has been selected for its compilation in Xcode.`);
}
if (this.isRealDevice()) {
return;
}
const executablePath = path_1.default.resolve(this.opts.app, await this.appInfosCache.extractExecutableName(this.opts.app));
const [resFile, resUname] = await bluebird_1.default.all([
(0, teen_process_1.exec)('lipo', ['-info', executablePath]),
(0, teen_process_1.exec)('uname', ['-m']),
]);
const bundleExecutableInfo = lodash_1.default.trim(resFile.stdout);
this.log.debug(bundleExecutableInfo);
const processArch = lodash_1.default.trim(resUname.stdout);
this.log.debug(`Current process architecture: ${processArch}`);
const isAppleSiliconCpu = isAppleSilicon();
this.log.debug(`Is Apple Silicon CPU: ${isAppleSiliconCpu}`);
if (isAppleSiliconCpu && processArch === INTEL_ARCH) {
this.log.warn(`It looks like the Appium server process is running under Rosetta emulation. ` +
`This might lead to various performance/compatibility issues while running tests on Simulator. ` +
`Consider using binaries compiled natively for the ARM64 architecture to run Appium server ` +
`with this driver.`);
}
if (lodash_1.default.includes(bundleExecutableInfo, processArch)) {
return;
}
const hasRosetta = isAppleSiliconCpu && await isRosettaInstalled();
const isIntelApp = lodash_1.default.includes(bundleExecutableInfo, INTEL_ARCH);
// We cannot run Simulator builds compiled for arm64 on Intel machines
// Rosetta allows only to run Intel ones on arm64
if ((isIntelApp && (!isAppleSiliconCpu || hasRosetta)) || (!isIntelApp && isAppleSiliconCpu)) {
return;
}
const advice = isIntelApp && isAppleSiliconCpu && !hasRosetta
? `Please install Rosetta and try again.`
: `Please rebuild your application to support the ${processArch} platform.`;
throw new Error(`The ${this.opts.bundleId} application does not support the ${processArch} Simulator ` +
`architecture:\n${bundleExecutableInfo}\n\n${advice}`);
}
/**
*
* @param {string} resourcePath
* @returns {Promise<import('@appium/types').StringRecord>}
*/
async function readResource(resourcePath) {
const data = await support_1.plist.parsePlistFile(resourcePath);
const result = {};
for (const [key, value] of lodash_1.default.toPairs(data)) {
result[key] = lodash_1.default.isString(value) ? value : JSON.stringify(value);
}
return result;
}
/**
* @typedef {Object} LocalizableStringsOptions
* @property {string} [app]
* @property {string} [language='en']
* @property {string} [localizableStringsDir]
* @property {string} [stringFile]
* @property {boolean} [strictMode]
*/
/**
* Extracts string resources from an app
*
* @this {XCUITestDriver}
* @param {LocalizableStringsOptions} opts
* @returns {Promise<import('@appium/types').StringRecord>}
*/
async function parseLocalizableStrings(opts = {}) {
const { app, language = 'en', localizableStringsDir, stringFile, strictMode } = opts;
if (!app) {
const message = `Strings extraction is not supported if 'app' capability is not set`;
if (strictMode) {
throw new Error(message);
}
this.log.info(message);
return {};
}
let bundleRoot = app;
const isArchive = (await support_1.fs.stat(app)).isFile();
let tmpRoot;
try {
if (isArchive) {
tmpRoot = await support_1.tempDir.openDir();
this.log.info(`Extracting '${app}' into a temporary location to parse its resources`);
await support_1.zip.extractAllTo(app, tmpRoot);
const relativeBundleRoot = /** @type {string} */ (lodash_1.default.first(await findApps(tmpRoot, [exports.APP_EXT])));
this.log.info(`Selecting '${relativeBundleRoot}'`);
bundleRoot = path_1.default.join(tmpRoot, relativeBundleRoot);
}
/** @type {string|undefined} */
let lprojRoot;
for (const subfolder of [`${language}.lproj`, localizableStringsDir, ''].filter(lodash_1.default.isString)) {
lprojRoot = path_1.default.resolve(bundleRoot, /** @type {string} */ (subfolder));
if (await support_1.fs.exists(lprojRoot)) {
break;
}
const message = `No '${lprojRoot}' resources folder has been found`;
if (strictMode) {
throw new Error(message);
}
this.log.debug(message);
}
if (!lprojRoot) {
return {};
}
this.log.info(`Retrieving resource strings from '${lprojRoot}'`);
const resourcePaths = [];
if (stringFile) {
const dstPath = path_1.default.resolve(/** @type {string} */ (lprojRoot), stringFile);
if (await support_1.fs.exists(dstPath)) {
resourcePaths.push(dstPath);
}
else {
const message = `No '${dstPath}' resource file has been found for '${app}'`;
if (strictMode) {
throw new Error(message);
}
this.log.info(message);
}
}
if (lodash_1.default.isEmpty(resourcePaths) && (await support_1.fs.exists(lprojRoot))) {
const resourceFiles = (await support_1.fs.readdir(lprojRoot))
.filter((name) => lodash_1.default.some([STRINGS_RESOURCE, STRINGSDICT_RESOURCE], (x) => name.endsWith(x)))
.map((name) => path_1.default.resolve(lprojRoot, name));
resourcePaths.push(...resourceFiles);
}
this.log.info(`Got ${support_1.util.pluralize('resource file', resourcePaths.length, true)} in '${lprojRoot}'`);
if (lodash_1.default.isEmpty(resourcePaths)) {
return {};
}
const resultStrings = {};
const toAbsolutePath = (/** @type {string} */ p) => path_1.default.isAbsolute(p) ? p : path_1.default.resolve(process.cwd(), p);
for (const resourcePath of resourcePaths) {
if (!support_1.util.isSubPath(toAbsolutePath(resourcePath), toAbsolutePath(bundleRoot))) {
// security precaution
throw new Error(`'${resourcePath}' is expected to be located under '${bundleRoot}'`);
}
try {
const data = await readResource(resourcePath);
this.log.debug(`Parsed ${support_1.util.pluralize('string', lodash_1.default.keys(data).length, true)} from '${resourcePath}'`);
lodash_1.default.merge(resultStrings, data);
}
catch (e) {
this.log.warn(`Cannot parse '${resourcePath}' resource. Original error: ${e.message}`);
}
}
this.log.info(`Retrieved ${support_1.util.pluralize('string', lodash_1.default.keys(resultStrings).length, true)} from '${lprojRoot}'`);
return resultStrings;
}
finally {
if (tmpRoot) {
await support_1.fs.rimraf(tmpRoot);
}
}
}
/**
* Check whether the given path on the file system points to the .app bundle root
*
* @param {string} appPath Possible .app bundle root
* @returns {Promise<boolean>} Whether the given path points to an .app bundle
*/
async function isAppBundle(appPath) {
return (lodash_1.default.endsWith(lodash_1.default.toLower(appPath), exports.APP_EXT) &&
(await support_1.fs.stat(appPath)).isDirectory() &&
(await support_1.fs.exists(path_1.default.join(appPath, 'Info.plist'))));
}
/**
* Check whether the given path on the file system points to the .ipa file
*
* @param {string} appPath Possible .ipa file
* @returns {Promise<boolean>} Whether the given path points to an .ipa bundle
*/
async function isIpaBundle(appPath) {
return lodash_1.default.endsWith(lodash_1.default.toLower(appPath), exports.IPA_EXT) && (await support_1.fs.stat(appPath)).isFile();
}
/**
* @typedef {Object} UnzipInfo
* @property {string} rootDir
* @property {number} archiveSize
*/
/**
* Unzips a ZIP archive on the local file system.
*
* @param {string} archivePath Full path to a .zip archive
* @returns {Promise<UnzipInfo>} temporary folder root where the archive has been extracted
*/
async function unzipFile(archivePath) {
const useSystemUnzipEnv = process.env.APPIUM_PREFER_SYSTEM_UNZIP;
const useSystemUnzip = lodash_1.default.isEmpty(useSystemUnzipEnv) || !['0', 'false'].includes(lodash_1.default.toLower(useSystemUnzipEnv));
const tmpRoot = await support_1.tempDir.openDir();
try {
await support_1.zip.extractAllTo(archivePath, tmpRoot, {
useSystemUnzip,
// https://github.com/appium/appium/issues/14100
fileNamesEncoding: 'utf8',
});
}
catch (e) {
await support_1.fs.rimraf(tmpRoot);
throw e;
}
return {
rootDir: tmpRoot,
archiveSize: (await support_1.fs.stat(archivePath)).size,
};
}
/**
* Unzips a ZIP archive from a stream.
* Uses bdstar tool for this purpose.
* This allows to optimize the time needed to prepare the app under test
* to MAX(download, unzip) instead of SUM(download, unzip)
*
* @param {import('node:stream').Readable} zipStream
* @returns {Promise<UnzipInfo>}
*/
async function unzipStream(zipStream) {
const tmpRoot = await support_1.tempDir.openDir();
const bsdtarProcess = (0, node_child_process_1.spawn)(await support_1.fs.which('bsdtar'), [
'-x',
'--exclude', MACOS_RESOURCE_FOLDER,
'--exclude', `${MACOS_RESOURCE_FOLDER}/*`,
'-',
], {
cwd: tmpRoot,
});
let archiveSize = 0;
bsdtarProcess.stderr.on('data', (chunk) => {
const stderr = chunk.toString();
if (lodash_1.default.trim(stderr)) {
logger_js_1.default.warn(stderr);
}
});
zipStream.on('data', (chunk) => {
archiveSize += lodash_1.default.size(chunk);
});
zipStream.pipe(bsdtarProcess.stdin);
try {
await new bluebird_1.default((resolve, reject) => {
zipStream.once('error', reject);
bsdtarProcess.once('exit', (code, signal) => {
zipStream.unpipe(bsdtarProcess.stdin);
logger_js_1.default.debug(`bsdtar process exited with code ${code}, signal ${signal}`);
if (code === 0) {
resolve();
}
else {
reject(new Error('Is it a valid ZIP archive?'));
}
});
bsdtarProcess.once('error', (e) => {
zipStream.unpipe(bsdtarProcess.stdin);
reject(e);
});
});
}
catch (err) {
bsdtarProcess.kill(9);
await support_1.fs.rimraf(tmpRoot);
throw new Error(`The response data cannot be unzipped: ${err.message}`);
}
finally {
bsdtarProcess.removeAllListeners();
zipStream.removeAllListeners();
}
return {
rootDir: tmpRoot,
archiveSize,
};
}
/**
* Used to parse the file name value from response headers
*
* @param {import('@appium/types').HTTPHeaders} headers
* @returns {string?}
*/
function parseFileName(headers) {
const contentDisposition = headers['content-disposition'];
if (!lodash_1.default.isString(contentDisposition)) {
return null;
}
if (/^attachment/i.test(/** @type {string} */ (contentDisposition))) {
const match = /filename="([^"]+)/i.exec(/** @type {string} */ (contentDisposition));
if (match) {
return support_1.fs.sanitizeName(match[1], { replacement: SANITIZE_REPLACEMENT });
}
}
return null;
}
/**
* Downloads and verifies remote applications for real devices
*
* @this {XCUITestDriver}
* @param {import('node:stream').Readable} stream
* @param {import('@appium/types').HTTPHeaders} headers
* @returns {Promise<string>}
*/
async function downloadIpa(stream, headers) {
const timer = new support_1.timing.Timer().start();
const logPerformance = (/** @type {string} */ dstPath, /** @type {number} */ fileSize, /** @type {string} */ action) => {
const secondsElapsed = timer.getDuration().asSeconds;
this.log.info(`The remote file (${support_1.util.toReadableSizeString(fileSize)}) ` +
`has been ${action} to '${dstPath}' in ${secondsElapsed.toFixed(3)}s`);
if (secondsElapsed >= 1) {
const bytesPerSec = Math.floor(fileSize / secondsElapsed);
this.log.debug(`Approximate speed: ${support_1.util.toReadableSizeString(bytesPerSec)}/s`);
}
};
// Check if the file to be downloaded is a .zip rather than .ipa
const fileName = parseFileName(headers) ?? `appium-app-${new Date().getTime()}${exports.IPA_EXT}`;
if (fileName.toLowerCase().endsWith(ZIP_EXT)) {
const { rootDir, archiveSize } = await unzipStream(stream);
logPerformance(rootDir, archiveSize, 'downloaded and unzipped');
try {
const matchedPaths = await findApps(rootDir, [exports.IPA_EXT]);
if (!lodash_1.default.isEmpty(matchedPaths)) {
this.log.debug(`Found ${support_1.util.pluralize(`${exports.IPA_EXT} applicaition`, matchedPaths.length, true)} in ` +
`'${path_1.default.basename(rootDir)}': ${matchedPaths}`);
}
for (const matchedPath of matchedPaths) {
try {
await this.appInfosCache.put(matchedPath);
}
catch (e) {
this.log.info(e.message);
continue;
}
this.log.debug(`Selecting the application at '${matchedPath}'`);
const isolatedPath = path_1.default.join(await support_1.tempDir.openDir(), path_1.default.basename(matchedPath));
await support_1.fs.mv(matchedPath, isolatedPath);
return isolatedPath;
}
throw new Error(`The remote archive does not contain any valid ${exports.IPA_EXT} applications`);
}
finally {
await support_1.fs.rimraf(rootDir);
}
}
const ipaPath = await support_1.tempDir.path({
prefix: fileName,
suffix: fileName.toLowerCase().endsWith(exports.IPA_EXT) ? '' : exports.IPA_EXT,
});
try {
const writer = support_1.fs.createWriteStream(ipaPath);
stream.pipe(writer);
await new bluebird_1.default((resolve, reject) => {
stream.once('error', reject);
writer.once('finish', resolve);
writer.once('error', (e) => {
stream.unpipe(writer);
reject(e);
});
});
}
catch (err) {
throw new Error(`Cannot fetch the remote file: ${err.message}`);
}
const { size } = await support_1.fs.stat(ipaPath);
logPerformance(ipaPath, size, 'downloaded');
try {
await this.appInfosCache.put(ipaPath);
}
catch (e) {
await support_1.fs.rimraf(ipaPath);
throw e;
}
return ipaPath;
}
/**
* Looks for items with given extensions in the given folder
*
* @param {string} appPath Full path to an app bundle
* @param {Array<string>} appExtensions List of matching item extensions
* @returns {Promise<string[]>} List of relative paths to matched items
*/
async function findApps(appPath, appExtensions) {
const globPattern = `**/*.+(${appExtensions.map((ext) => ext.replace(/^\./, '')).join('|')})`;
const sortedBundleItems = (await support_1.fs.glob(globPattern, {
cwd: appPath,
})).sort((a, b) => a.split(path_1.default.sep).length - b.split(path_1.default.sep).length);
return sortedBundleItems;
}
/**
* Moves the application bundle to a newly created temporary folder
*
* @param {string} appPath Full path to the .app or .ipa bundle
* @returns {Promise<string>} The new path to the app bundle.
* The name of the app bundle remains the same
*/
async function isolateApp(appPath) {
const appFileName = path_1.default.basename(appPath);
if ((await support_1.fs.stat(appPath)).isFile()) {
const isolatedPath = await support_1.tempDir.path({
prefix: appFileName,
suffix: '',
});
await support_1.fs.mv(appPath, isolatedPath, { mkdirp: true });
return isolatedPath;
}
const tmpRoot = await support_1.tempDir.openDir();
const isolatedRoot = path_1.default.join(tmpRoot, appFileName);
await support_1.fs.mv(appPath, isolatedRoot, { mkdirp: true });
return isolatedRoot;
}
/**
* Builds Safari preferences object based on the given session capabilities
*
* @param {import('./driver').XCUITestDriverOpts} opts
* @return {Promise<import('@appium/types').StringRecord>}
*/
function buildSafariPreferences(opts) {
const safariSettings = lodash_1.default.cloneDeep(opts?.safariGlobalPreferences ?? {});
for (const [name, [aliases, valueConverter]] of lodash_1.default.toPairs(SAFARI_OPTS_ALIASES_MAP)) {
if (!lodash_1.default.has(opts, name)) {
continue;
}
for (const alias of aliases) {
safariSettings[alias] = valueConverter(opts[name]);
}
}
return safariSettings;
}
/**
* Unzip the given archive and find a matching .app bundle in it
*
* @this {XCUITestDriver}
* @param {string|import('node:stream').Readable} appPathOrZipStream The path to the archive.
* @param {number} depth [0] the current nesting depth. App bundles whose nesting level
* is greater than 1 are not supported.
* @returns {Promise<string>} Full path to the first matching .app bundle..
* @throws If no matching .app bundles were found in the provided archive.
*/
async function unzipApp(appPathOrZipStream, depth = 0) {
const errMsg = `The archive did not have any matching ${exports.APP_EXT} or ${exports.IPA_EXT} ` +
`bundles. Please make sure the provided package is valid and contains at least one matching ` +
`application bundle which is not nested.`;
if (depth > MAX_ARCHIVE_SCAN_DEPTH) {
throw new Error(errMsg);
}
const timer = new support_1.timing.Timer().start();
/** @type {string} */
let rootDir;
/** @type {number} */
let archiveSize;
try {
if (lodash_1.default.isString(appPathOrZipStream)) {
({ rootDir, archiveSize } = await unzipFile(/** @type {string} */ (appPathOrZipStream)));
}
else {
if (depth > 0) {
node_assert_1.default.fail('Streaming unzip cannot be invoked for nested archive items');
}
({ rootDir, archiveSize } = await unzipStream(
/** @type {import('node:stream').Readable} */ (appPathOrZipStream)));
}
}
catch (e) {
this.log.debug(e.stack);
throw new Error(`Cannot prepare the application for testing. Original error: ${e.message}`);
}
const secondsElapsed = timer.getDuration().asSeconds;
this.log.info(`The file (${support_1.util.toReadableSizeString(archiveSize)}) ` +
`has been ${lodash_1.default.isString(appPathOrZipStream) ? 'extracted' : 'downloaded and extracted'} ` +
`to '${rootDir}' in ${secondsElapsed.toFixed(3)}s`);
// it does not make much sense to approximate the speed for short downloads
if (secondsElapsed >= 1) {
const bytesPerSec = Math.floor(archiveSize / secondsElapsed);
this.log.debug(`Approximate decompression speed: ${support_1.util.toReadableSizeString(bytesPerSec)}/s`);
}
const isCompatibleWithCurrentPlatform = async (/** @type {string} */ appPath) => {
let platforms;
try {
platforms = await this.appInfosCache.extractAppPlatforms(appPath);
}
catch (e) {
this.log.info(e.message);
return false;
}
if (this.isSimulator() && !platforms.some((p) => lodash_1.default.includes(p, 'Simulator'))) {
this.log.info(`'${appPath}' does not have Simulator devices in the list of supported platforms ` +
`(${platforms.join(',')}). Skipping it`);
return false;
}
if (this.isRealDevice() && !platforms.some((p) => lodash_1.default.includes(p, 'OS'))) {
this.log.info(`'${appPath}' does not have real devices in the list of supported platforms ` +
`(${platforms.join(',')}). Skipping it`);
return false;
}
return true;
};
const matchedPaths = await findApps(rootDir, exports.SUPPORTED_EXTENSIONS);
if (lodash_1.default.isEmpty(matchedPaths)) {
this.log.debug(`'${path_1.default.basename(rootDir)}' has no bundles`);
}
else {
this.log.debug(`Found ${support_1.util.pluralize('bundle', matchedPaths.length, true)} in ` +
`'${path_1.default.basename(rootDir)}': ${matchedPaths}`);
}
try {
for (const matchedPath of matchedPaths) {
const fullPath = path_1.default.join(rootDir, matchedPath);
if ((await isAppBundle(fullPath) || (this.isRealDevice() && await isIpaBundle(fullPath)))
&& await isCompatibleWithCurrentPlatform(fullPath)) {
this.log.debug(`Selecting the application at '${matchedPath}'`);
return await isolateApp(fullPath);
}
}
}
finally {
await support_1.fs.rimraf(rootDir);
}
throw new Error(errMsg);
}
/**
* The callback invoked by configureApp helper
* when it is necessary to download the remote application.
* We assume the remote file could be anythingm, but only
* .zip and .ipa formats are supported.
* A .zip archive can contain one or more
*
* @this {XCUITestDriver}
* @param {import('@appium/types').DownloadAppOptions} opts
* @returns {Promise<string>}
*/
async function onDownloadApp({ stream, headers }) {
return this.isRealDevice()
? await downloadIpa.bind(this)(stream, headers)
: await unzipApp.bind(this)(stream);
}
/**
* @this {XCUITestDriver}
* @param {import('@appium/types').PostProcessOptions} opts
* @returns {Promise<import('@appium/types').PostProcessResult|false>}
*/
async function onPostConfigureApp({ cachedAppInfo, isUrl, appPath }) {
// Pick the previously cached entry if its integrity has been preserved
/** @type {import('@appium/types').CachedAppInfo|undefined} */
const appInfo = lodash_1.default.isPlainObject(cachedAppInfo) ? cachedAppInfo : undefined;
const cachedPath = appInfo ? /** @type {string} */ (appInfo.fullPath) : undefined;
const shouldUseCachedApp = async () => {
if (!appInfo || !cachedPath || !await support_1.fs.exists(cachedPath)) {
return false;
}
const isCachedPathAFile = (await support_1.fs.stat(cachedPath)).isFile();
if (isCachedPathAFile) {
return await support_1.fs.hash(cachedPath) === /** @type {any} */ (appInfo.integrity)?.file;
}
// If the cached path is a folder then it is expected to be previously extracted from
// an archive located under appPath whose hash is stored as `cachedAppInfo.packageHash`
if (!isCachedPathAFile
&& cachedAppInfo?.packageHash
&& await support_1.fs.exists(/** @type {string} */ (appPath))
&& (await support_1.fs.stat(/** @type {string} */ (appPath))).isFile()
&& cachedAppInfo.packageHash === await support_1.fs.hash(/** @type {string} */ (appPath))) {
/** @type {number|undefined} */
const nestedItemsCountInCache = /** @type {any} */ (appInfo.integrity)?.folder;
if (nestedItemsCountInCache !== undefined) {
return (await support_1.fs.glob('**/*', { cwd: cachedPath })).length >= nestedItemsCountInCache;
}
}
return false;
};
if (await shouldUseCachedApp()) {
this.log.info(`Using '${cachedPath}' which was cached from '${appPath}'`);
return { appPath: /** @type {string} */ (cachedPath) };
}
const isLocalIpa = await isIpaBundle(/** @type {string} */ (appPath));
const isLocalApp = !isLocalIpa && await isAppBundle(/** @type {string} */ (appPath));
const isPackageReadyForInstall = isLocalApp || (this.isRealDevice() && isLocalIpa);
if (isPackageReadyForInstall) {
await this.appInfosCache.put(/** @type {string} */ (appPath));
}
// Only local .app bundles (real device/Simulator)
// and .ipa packages for real devices should not be cached
if (!isUrl && isPackageReadyForInstall) {
return false;
}
// Cache the app while unpacking the bundle if necessary
return {
appPath: isPackageReadyForInstall
? appPath
: await unzipApp.bind(this)(/** @type {string} */ (appPath))
};
}
/**
* @returns {Promise<boolean>}
*/
async function isRosettaInstalled() {
return await support_1.fs.exists('/Library/Apple/usr/share/rosetta/rosetta');
}
/**
* @returns {boolean}
*/
function isAppleSilicon() {
return node_os_1.default.cpus()[0].model.includes('Apple');
}
/**
* @typedef {import('./driver').XCUITestDriver} XCUITestDriver
*/
//# sourceMappingURL=app-utils.js.map