UNPKG

appium-ios-simulator

Version:
602 lines 23.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.SimulatorXcode14 = 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 = require("./logger"); const events_1 = __importDefault(require("events")); const async_lock_1 = __importDefault(require("async-lock")); const lodash_1 = __importDefault(require("lodash")); const node_path_1 = __importDefault(require("node: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 settingsExtensions = __importStar(require("./extensions/settings")); const permissionsExtensions = __importStar(require("./extensions/permissions")); const miscExtensions = __importStar(require("./extensions/misc")); const geolocationExtensions = __importStar(require("./extensions/geolocation")); 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; class SimulatorXcode14 extends events_1.default { _keychainsBackupPath; _platformVersion; _webInspectorSocket; _udid; _simctl; _xcodeVersion; _log; /** * Constructs the object with the `udid` and version of Xcode. * Use the exported `getSimulator(udid)` method instead. * * @param udid - The Simulator ID. * @param xcodeVersion - The target Xcode version in format {major, minor, build}. * @param log - Optional logger instance. */ constructor(udid, xcodeVersion, log = null) { super(); 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._webInspectorSocket = null; this._log = log ?? logger_1.log; } /** * @returns The unique device identifier (UDID) of the simulator. */ get udid() { return this._udid; } /** * @returns The Simctl instance for interacting with the simulator. */ get simctl() { return this._simctl; } /** * @returns The Xcode version information. */ get xcodeVersion() { return this._xcodeVersion; } /** * @returns The full path to the keychain directory for this simulator. */ get keychainPath() { return node_path_1.default.resolve(this.getDir(), 'Library', 'Keychains'); } /** * @returns The logger instance used by this simulator. */ get log() { return this._log; } /** * @returns The bundle identifier of the Simulator UI client. */ get uiClientBundleId() { return UI_CLIENT_BUNDLE_ID; } /** * @returns The maximum number of milliseconds to wait until Simulator booting is completed. */ get startupTimeout() { return STARTUP_TIMEOUT_MS; } /** * @returns The full path to the devices set where the current simulator is located. * `null` value means that the default path is used. */ 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 value - The full path to the devices set root on the local file system. */ set devicesSetPath(value) { this.simctl.devicesSetPath = value; } /** * Retrieve the full path to the directory where Simulator stuff is located. * * @returns The path string. */ getRootDir() { return node_path_1.default.resolve(process.env.HOME ?? '', 'Library', 'Developer', 'CoreSimulator', 'Devices'); } /** * Retrieve the full path to the directory where Simulator applications data is located. * * @returns The path string. */ getDir() { return node_path_1.default.resolve(this.getRootDir(), this.udid, 'data'); } /** * Retrieve the full path to the directory where Simulator logs are stored. * * @returns The path string. */ getLogDir() { return node_path_1.default.resolve(process.env.HOME ?? '', 'Library', 'Logs', 'CoreSimulator', this.udid); } /** * Get the state and specifics of this simulator. * * @returns Simulator stats mapping, for example: * { name: 'iPhone 4s', * udid: 'C09B34E5-7DCB-442E-B79C-AB6BC0357417', * state: 'Shutdown', * sdk: '8.3' * } */ async stat() { const devices = await this.simctl.getDevices(); for (const [sdk, deviceArr] of lodash_1.default.toPairs(devices)) { for (const 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. * * @returns True if the current Simulator has never been started before. */ async isFresh() { const cachesRoot = node_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. * * @returns 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. * * @returns 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. * * @returns 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. * * @returns True 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. * * @returns SDK version, for example '18.3'. */ async getPlatformVersion() { if (!this._platformVersion) { const stat = await this.stat(); this._platformVersion = 'sdk' in stat ? stat.sdk : ''; } return 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 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 opts - Shutdown options including timeout. * @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. * In xcode 11.4, UI Client must be first launched, otherwise * sim window stays minimized * * @param isUiClientRunning - whether the simulator UI client is already running. * @param opts - arguments to start simulator UI client with. */ async launchWindow(isUiClientRunning, opts = {}) { // In xcode 11.4, UI Client must be first launched, otherwise // sim window stays minimized if (!isUiClientRunning) { await this.startUIClient(opts); } await this.boot(); } /** * Start the Simulator UI client with the given arguments. * * @param opts - Simulator startup options. */ async startUIClient(opts = {}) { opts = lodash_1.default.cloneDeep(opts); lodash_1.default.defaultsDeep(opts, { startupTimeout: this.startupTimeout, }); const simulatorApp = node_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 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 opts - Options including process ID and signal number. * @returns 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 = {}) { const { 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. * * @returns The list of retrieved process information. * @throws {Error} If no process information could be retrieved. */ async ps() { const { stdout } = await this.simctl.spawnProcess([ 'launchctl', 'list' ]); /* Example match: PID Status Label - 0 com.apple.progressd 22109 0 com.apple.CoreAuthentication.daemon 21995 0 com.apple.cloudphotod 22045 0 com.apple.homed 22042 0 com.apple.dataaccess.dataaccessd - 0 com.apple.DragUI.druid 22076 0 UIKitApplication:com.apple.mobilesafari[2b0f][rb-legacy] */ const extractGroup = (lbl) => lbl.includes(':') ? lbl.split(':')[0] : null; const extractName = (lbl) => { let res = lbl; const colonIdx = res.indexOf(':'); if (colonIdx >= 0 && res.length > colonIdx) { res = res.substring(colonIdx + 1); } const bracketIdx = res.indexOf('['); if (bracketIdx >= 0) { res = res.substring(0, bracketIdx); } return res; }; const result = []; for (const line of stdout.split('\n')) { const trimmedLine = lodash_1.default.trim(line); if (!trimmedLine) { continue; } const [pidStr, , label] = trimmedLine.split(/\s+/); const pid = parseInt(pidStr, 10); if (!pid || !label) { continue; } result.push({ pid, group: extractGroup(label), name: extractName(label), }); } return result; } /** * @returns The full path to the LaunchDaemons directory. */ async getLaunchDaemonsRoot() { const devRoot = await (0, utils_1.getDeveloperRoot)(); return node_path_1.default.resolve(devRoot, 'Platforms', 'iPhoneOS.platform', 'Library', 'Developer', 'CoreSimulator', 'Profiles', 'Runtimes', 'iOS.simruntime', 'Contents', 'Resources', 'RuntimeRoot', 'System', 'Library', 'LaunchDaemons'); } // Extension methods installApp = appExtensions.installApp; getUserInstalledBundleIdsByBundleName = appExtensions.getUserInstalledBundleIdsByBundleName; isAppInstalled = appExtensions.isAppInstalled; removeApp = appExtensions.removeApp; launchApp = appExtensions.launchApp; terminateApp = appExtensions.terminateApp; isAppRunning = appExtensions.isAppRunning; scrubApp = appExtensions.scrubApp; openUrl = safariExtensions.openUrl; scrubSafari = safariExtensions.scrubSafari; updateSafariSettings = safariExtensions.updateSafariSettings; getWebInspectorSocket = safariExtensions.getWebInspectorSocket; isBiometricEnrolled = biometricExtensions.isBiometricEnrolled; enrollBiometric = biometricExtensions.enrollBiometric; sendBiometricMatch = biometricExtensions.sendBiometricMatch; backupKeychains = keychainExtensions.backupKeychains; restoreKeychains = keychainExtensions.restoreKeychains; clearKeychains = keychainExtensions.clearKeychains; setGeolocation = geolocationExtensions.setGeolocation; shake = miscExtensions.shake; addCertificate = miscExtensions.addCertificate; pushNotification = miscExtensions.pushNotification; setPermission = permissionsExtensions.setPermission; setPermissions = permissionsExtensions.setPermissions; getPermission = permissionsExtensions.getPermission; updateSettings = settingsExtensions.updateSettings; setAppearance = settingsExtensions.setAppearance; getAppearance = settingsExtensions.getAppearance; setIncreaseContrast = settingsExtensions.setIncreaseContrast; getIncreaseContrast = settingsExtensions.getIncreaseContrast; setContentSize = settingsExtensions.setContentSize; getContentSize = settingsExtensions.getContentSize; configureLocalization = settingsExtensions.configureLocalization; setAutoFillPasswords = settingsExtensions.setAutoFillPasswords; setReduceMotion = settingsExtensions.setReduceMotion; setReduceTransparency = settingsExtensions.setReduceTransparency; disableKeyboardIntroduction = settingsExtensions.disableKeyboardIntroduction; } exports.SimulatorXcode14 = SimulatorXcode14; //# sourceMappingURL=simulator-xcode-14.js.map