UNPKG

appium-xcuitest-driver

Version:

Appium driver for iOS using XCUITest for backend

657 lines 28.2 kB
"use strict"; 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