UNPKG

appium-ios-simulator

Version:
613 lines 24.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SimulatorXcode10 = void 0; const support_1 = require("@appium/support"); const asyncbox_1 = require("asyncbox"); const utils_1 = require("./utils"); const teen_process_1 = require("teen_process"); const logger_1 = __importDefault(require("./logger")); const events_1 = __importDefault(require("events")); const async_lock_1 = __importDefault(require("async-lock")); const lodash_1 = __importDefault(require("lodash")); const path_1 = __importDefault(require("path")); const bluebird_1 = __importDefault(require("bluebird")); const appium_xcode_1 = require("appium-xcode"); const node_simctl_1 = require("node-simctl"); const appExtensions = __importStar(require("./extensions/applications")); const biometricExtensions = __importStar(require("./extensions/biometric")); const safariExtensions = __importStar(require("./extensions/safari")); const keychainExtensions = __importStar(require("./extensions/keychain")); const geolocationExtensions = __importStar(require("./extensions/geolocation")); const settingsExtensions = __importStar(require("./extensions/settings")); const permissionsExtensions = __importStar(require("./extensions/permissions")); const miscExtensions = __importStar(require("./extensions/misc")); const SIMULATOR_SHUTDOWN_TIMEOUT = 15 * 1000; const STARTUP_LOCK = new async_lock_1.default(); const UI_CLIENT_BUNDLE_ID = 'com.apple.iphonesimulator'; const STARTUP_TIMEOUT_MS = 120 * 1000; /** * @typedef {import('./types').CoreSimulator} CoreSimulator * @typedef {import('./types').HasSettings} HasSettings * @typedef {import('./types').InteractsWithApps} InteractsWithApps * @typedef {import('./types').InteractsWithKeychain} InteractsWithKeychain * @typedef {import('./types').SupportsGeolocation} SupportsGeolocation * @typedef {import('./types').HasMiscFeatures} HasMiscFeatures * @typedef {import('./types').InteractsWithSafariBrowser} InteractsWithSafariBrowser * @typedef {import('./types').SupportsBiometric} SupportsBiometric */ /** * @implements {CoreSimulator} * @implements {HasSettings} * @implements {InteractsWithApps} * @implements {InteractsWithKeychain} * @implements {SupportsGeolocation} * @implements {HasMiscFeatures} * @implements {InteractsWithSafariBrowser} * @implements {SupportsBiometric} */ class SimulatorXcode10 extends events_1.default { /** * Constructs the object with the `udid` and version of Xcode. Use the exported `getSimulator(udid)` method instead. * * @param {string} udid - The Simulator ID. * @param {import('appium-xcode').XcodeVersion} xcodeVersion - The target Xcode version in format {major, minor, build}. * @param {import('@appium/types').AppiumLogger?} log */ constructor(udid, xcodeVersion, log = null) { super(); this.installApp = appExtensions.installApp; this.getUserInstalledBundleIdsByBundleName = appExtensions.getUserInstalledBundleIdsByBundleName; this.isAppInstalled = appExtensions.isAppInstalled; this.removeApp = appExtensions.removeApp; this.launchApp = appExtensions.launchApp; this.terminateApp = appExtensions.terminateApp; this.isAppRunning = appExtensions.isAppRunning; this.scrubApp = appExtensions.scrubApp; this.openUrl = safariExtensions.openUrl; this.scrubSafari = safariExtensions.scrubSafari; this.updateSafariSettings = safariExtensions.updateSafariSettings; this.getWebInspectorSocket = ( /** @type {unknown} */ (safariExtensions.getWebInspectorSocket)); this.isBiometricEnrolled = biometricExtensions.isBiometricEnrolled; this.enrollBiometric = biometricExtensions.enrollBiometric; this.sendBiometricMatch = biometricExtensions.sendBiometricMatch; this.setGeolocation = geolocationExtensions.setGeolocation; this.backupKeychains = ( /** @type {unknown} */ (keychainExtensions.backupKeychains)); this.restoreKeychains = ( /** @type {unknown} */ (keychainExtensions.restoreKeychains)); this.clearKeychains = keychainExtensions.clearKeychains; this.shake = miscExtensions.shake; this.addCertificate = miscExtensions.addCertificate; this.pushNotification = miscExtensions.pushNotification; this.setPermission = permissionsExtensions.setPermission; this.setPermissions = permissionsExtensions.setPermissions; this.getPermission = permissionsExtensions.getPermission; this.updateSettings = settingsExtensions.updateSettings; this.setAppearance = settingsExtensions.setAppearance; this.getAppearance = settingsExtensions.getAppearance; this.setIncreaseContrast = settingsExtensions.setIncreaseContrast; this.getIncreaseContrast = settingsExtensions.getIncreaseContrast; this.setContentSize = settingsExtensions.setContentSize; this.getContentSize = settingsExtensions.getContentSize; this.configureLocalization = settingsExtensions.configureLocalization; this.setAutoFillPasswords = settingsExtensions.setAutoFillPasswords; this.setReduceMotion = settingsExtensions.setReduceMotion; this.setReduceTransparency = settingsExtensions.setReduceTransparency; this.disableKeyboardIntroduction = settingsExtensions.disableKeyboardIntroduction; this._udid = String(udid); this._simctl = new node_simctl_1.Simctl({ udid: this._udid, }); this._xcodeVersion = xcodeVersion; // platformVersion cannot be found initially, since getting it has side effects for // our logic for figuring out if a sim has been run // it will be set when it is needed this._platformVersion = null; this._idb = null; this._webInspectorSocket = null; this._log = log ?? logger_1.default; } /** * @returns {string} */ get udid() { return this._udid; } /** * @returns {Simctl} */ get simctl() { return this._simctl; } /** * @returns {import('appium-xcode').XcodeVersion} */ get xcodeVersion() { return this._xcodeVersion; } /** * @returns {string} */ get keychainPath() { return path_1.default.resolve(this.getDir(), 'Library', 'Keychains'); } /** * @return {import('@appium/types').AppiumLogger} */ get log() { return this._log; } /** * @return {string} Bundle identifier of Simulator UI client. */ get uiClientBundleId() { return UI_CLIENT_BUNDLE_ID; } /** * @return {number} The max number of milliseconds to wait until Simulator booting is completed. */ get startupTimeout() { return STARTUP_TIMEOUT_MS; } /** * @return {?string} The full path to the devices set where the current simulator is located. * `null` value means that the default path is used, which is usually `~/Library/Developer/CoreSimulator/Devices` */ get devicesSetPath() { return this.simctl.devicesSetPath; } /** * Set the full path to the devices set. It is recommended to set this value * once right after Simulator instance is created and to not change it during * the instance lifecycle * * @param {?string} value The full path to the devices set root on the * local file system */ set devicesSetPath(value) { this.simctl.devicesSetPath = value; } /** * IDB instance setter * * @param {any} value */ set idb(value) { this._idb = value; } /** * @return {Promise<any>} idb instance */ get idb() { return this._idb; } /** * Retrieve the full path to the directory where Simulator stuff is located. * * @return {string} The path string. */ getRootDir() { return path_1.default.resolve(process.env.HOME ?? '', 'Library', 'Developer', 'CoreSimulator', 'Devices'); } /** * Retrieve the full path to the directory where Simulator applications data is located. * * @return {string} The path string. */ getDir() { return path_1.default.resolve(this.getRootDir(), this.udid, 'data'); } /** * Retrieve the full path to the directory where Simulator logs are stored. * * @return {string} The path string. */ getLogDir() { return path_1.default.resolve(process.env.HOME ?? '', 'Library', 'Logs', 'CoreSimulator', this.udid); } /** * Get the state and specifics of this sim. * * @return {Promise<import('./types').DeviceStat|import('@appium/types').StringRecord<never>>} Simulator stats mapping, for example: * { name: 'iPhone 4s', * udid: 'C09B34E5-7DCB-442E-B79C-AB6BC0357417', * state: 'Shutdown', * sdk: '8.3' * } */ async stat() { for (const [sdk, deviceArr] of lodash_1.default.toPairs(await this.simctl.getDevices())) { for (let device of deviceArr) { if (device.udid === this.udid) { device.sdk = sdk; return device; } } } return {}; } /** * Check if the Simulator has been booted at least once * and has not been erased before * * @return {Promise<boolean>} True if the current Simulator has never been started before */ async isFresh() { const cachesRoot = path_1.default.resolve(this.getDir(), 'Library', 'Caches'); return (await support_1.fs.exists(cachesRoot)) ? (await support_1.fs.glob('*', { cwd: cachesRoot })).length === 0 : true; } /** * Retrieves the state of the current Simulator. One should distinguish the * states of Simulator UI and the Simulator itself. * * @return {Promise<boolean>} True if the current Simulator is running. */ async isRunning() { try { await this.simctl.getEnv('dummy'); return true; } catch { return false; } } /** * Checks if the simulator is in shutdown state. * This method is necessary, because Simulator might also be * in the transitional Shutting Down state right after the `shutdown` * command has been issued. * * @return {Promise<boolean>} True if the current Simulator is shut down. */ async isShutdown() { try { await this.simctl.getEnv('dummy'); return false; } catch (e) { return lodash_1.default.includes(e.stderr, 'Current state: Shutdown'); } } /** * Retrieves the current process id of the UI client * * @return {Promise<string|null>} The process ID or null if the UI client is not running */ async getUIClientPid() { let stdout; try { ({ stdout } = await (0, teen_process_1.exec)('pgrep', ['-fn', `${utils_1.SIMULATOR_APP_NAME}/Contents/MacOS/`])); } catch { return null; } if (isNaN(parseInt(stdout, 10))) { return null; } stdout = stdout.trim(); this.log.debug(`Got Simulator UI client PID: ${stdout}`); return stdout; } /** * Check the state of Simulator UI client. * * @return {Promise<boolean>} True of if UI client is running or false otherwise. */ async isUIClientRunning() { return !lodash_1.default.isNull(await this.getUIClientPid()); } /** * Get the platform version of the current Simulator. * * @return {Promise<string>} SDK version, for example '8.3'. */ async getPlatformVersion() { if (!this._platformVersion) { const { sdk } = await this.stat(); this._platformVersion = sdk; } return /** @type {string} */ (this._platformVersion); } /** * Boots Simulator if not already booted. * Does nothing if it is already running. * This API does NOT wait until Simulator is fully booted. * * @throws {Error} If there was a failure while booting the Simulator. */ async boot() { const bootEventsEmitter = new events_1.default(); await this.simctl.startBootMonitor({ onError: (err) => bootEventsEmitter.emit('failure', err), onFinished: () => bootEventsEmitter.emit('finish'), shouldPreboot: true, }); try { await new bluebird_1.default((resolve, reject) => { // Historically this call was always asynchronous, // e.g. it was not waiting until Simulator is fully booted. // So we preserve that behavior, and if no errors are received for a while // then we assume the Simulator booting is still in progress. setTimeout(resolve, 3000); bootEventsEmitter.once('failure', (err) => { if (lodash_1.default.includes(err?.message, 'state: Booted')) { resolve(); } else { reject(err); } }); bootEventsEmitter.once('finish', resolve); }); } finally { bootEventsEmitter.removeAllListeners(); } } /** * Verify whether the Simulator booting is completed and/or wait for it * until the timeout expires. * * @param {number} startupTimeout - the number of milliseconds to wait until booting is completed. */ async waitForBoot(startupTimeout) { await this.simctl.startBootMonitor({ timeout: startupTimeout }); } /** * Reset the current Simulator to the clean state. * It is expected the simulator is in shutdown state when this API is called. */ async clean() { this.log.info(`Cleaning simulator ${this.udid}`); await this.simctl.eraseDevice(10000); } /** * Delete the particular Simulator from devices list */ async delete() { await this.simctl.deleteDevice(); } /** * Shut down the current Simulator. * * @param {import('./types').ShutdownOptions} [opts={}] * @throws {Error} If Simulator fails to transition into Shutdown state after * the given timeout */ async shutdown(opts = {}) { if (await this.isShutdown()) { return; } await (0, asyncbox_1.retryInterval)(5, 500, this.simctl.shutdownDevice.bind(this.simctl)); const waitMs = parseInt(`${opts.timeout ?? 0}`, 10); if (waitMs > 0) { try { await (0, asyncbox_1.waitForCondition)(async () => await this.isShutdown(), { waitMs, intervalMs: 100, }); } catch { throw new Error(`Simulator is not in 'Shutdown' state after ${waitMs}ms`); } } } /** * Boots simulator and opens simulators UI Client if not already opened. * * @param {boolean} isUiClientRunning - process id of simulator UI client. * @param {import('./types').RunOptions} [opts={}] - arguments to start simulator UI client with. */ async launchWindow(isUiClientRunning, opts = {}) { await this.boot(); if (!isUiClientRunning) { await this.startUIClient(opts); } } /** * Start the Simulator UI client with the given arguments * * @param {import('./types').StartUiClientOptions} [opts={}] - Simulator startup options */ async startUIClient(opts = {}) { opts = lodash_1.default.cloneDeep(opts); lodash_1.default.defaultsDeep(opts, { startupTimeout: this.startupTimeout, }); const simulatorApp = path_1.default.resolve(await (0, appium_xcode_1.getPath)(), 'Applications', utils_1.SIMULATOR_APP_NAME); const args = ['-Fn', simulatorApp]; this.log.info(`Starting Simulator UI: ${support_1.util.quote(['open', ...args])}`); try { await (0, teen_process_1.exec)('open', args, { timeout: opts.startupTimeout }); } catch (err) { throw new Error(`Got an unexpected error while opening Simulator UI: ` + err.stderr || err.stdout || err.message); } } /** * Executes given Simulator with options. The Simulator will not be restarted if * it is already running and the current UI state matches to `isHeadless` option. * * @param {import('./types').RunOptions} [opts={}] - One or more of available Simulator options */ async run(opts = {}) { opts = lodash_1.default.cloneDeep(opts); lodash_1.default.defaultsDeep(opts, { isHeadless: false, startupTimeout: this.startupTimeout, }); const [devicePreferences, commonPreferences] = settingsExtensions.compileSimulatorPreferences.bind(this)(opts); await settingsExtensions.updatePreferences.bind(this)(devicePreferences, commonPreferences); const timer = new support_1.timing.Timer().start(); const shouldWaitForBoot = await STARTUP_LOCK.acquire(this.uiClientBundleId, async () => { const isServerRunning = await this.isRunning(); const uiClientPid = await this.getUIClientPid(); if (opts.isHeadless) { if (isServerRunning && !uiClientPid) { this.log.info(`Simulator with UDID '${this.udid}' is already booted in headless mode.`); return false; } if (await this.killUIClient({ pid: uiClientPid })) { this.log.info(`Detected the Simulator UI client was running and killed it. Verifying the current Simulator state`); } try { // Stopping the UI client kills all running servers for some early XCode versions. This is a known bug await (0, asyncbox_1.waitForCondition)(async () => await this.isShutdown(), { waitMs: 5000, intervalMs: 100, }); } catch { if (!await this.isRunning()) { throw new Error(`Simulator with UDID '${this.udid}' cannot be transitioned to headless mode`); } return false; } this.log.info(`Booting Simulator with UDID '${this.udid}' in headless mode. ` + `All UI-related capabilities are going to be ignored`); await this.boot(); } else { if (isServerRunning && uiClientPid) { this.log.info(`Both Simulator with UDID '${this.udid}' and the UI client are currently running`); return false; } if (isServerRunning) { this.log.info(`Simulator '${this.udid}' is booted while its UI is not visible. ` + `Trying to restart it with the Simulator window visible`); await this.shutdown({ timeout: SIMULATOR_SHUTDOWN_TIMEOUT }); } await this.launchWindow(Boolean(uiClientPid), opts); } return true; }); if (shouldWaitForBoot && opts.startupTimeout) { await this.waitForBoot(opts.startupTimeout); this.log.info(`Simulator with UDID ${this.udid} booted in ${timer.getDuration().asSeconds.toFixed(3)}s`); } (async () => { try { await this.disableKeyboardIntroduction(); } catch (e) { this.log.info(`Cannot disable Simulator keyboard introduction. Original error: ${e.message}`); } })(); } /** * Kill the UI client if it is running. * * @param {import('./types').KillUiClientOptions} [opts={}] * @return {Promise<boolean>} True if the UI client was successfully killed or false * if it is not running. * @throws {Error} If sending the signal to the client process fails */ async killUIClient(opts = {}) { let { pid, signal = 2, } = opts; const clientPid = pid || await this.getUIClientPid(); if (!clientPid) { return false; } this.log.debug(`Sending ${signal} kill signal to Simulator UI client with PID ${clientPid}`); try { await (0, teen_process_1.exec)('kill', [`-${signal}`, `${clientPid}`]); return true; } catch (e) { if (e.code === 1) { return false; } throw new Error(`Cannot kill the Simulator UI client. Original error: ${e.message}`); } } /** * Lists processes that are currently running on the given Simulator. * The simulator must be in running state in order for this * method to work properly. * * @return {Promise<import('./types').ProcessInfo[]>} The list of retrieved process * information * @throws {Error} if no process information could be retrieved. */ async ps() { const { stdout } = await this.simctl.spawnProcess([ 'launchctl', 'print', 'system', ]); const servicesMatch = /^\s*services\s*=\s*{([^}]+)/m.exec(stdout); if (!servicesMatch) { this.log.debug(stdout); throw new Error(`The list of active processes cannot be retrieved`); } /* Example match: 0 78 com.apple.resourcegrabberd 82158 - com.apple.assistant_service 82120 - com.apple.nanoregistryd 82087 - com.apple.notifyd 82264 - UIKitApplication:com.apple.Preferences[704b][rb-legacy] */ /** @type {import('./types').ProcessInfo[]} */ const result = []; const pattern = /^\s*(\d+)\s+[\d-]+\s+([\w\-.]+:)?([\w\-.]+)/gm; let match; while ((match = pattern.exec(servicesMatch[1]))) { result.push({ pid: parseInt(match[1], 10), group: lodash_1.default.trimEnd(match[2], ':') || null, name: match[3], }); } return result; } /** * @returns {Promise<string>} */ async getLaunchDaemonsRoot() { const devRoot = await (0, utils_1.getDeveloperRoot)(); return path_1.default.resolve(devRoot, 'Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/LaunchDaemons'); } } exports.SimulatorXcode10 = SimulatorXcode10; //# sourceMappingURL=simulator-xcode-10.js.map