All files / lib simulator-xcode-8.js

16.28% Statements 7/43
0% Branches 0/30
0% Functions 0/16
16.28% Lines 7/43
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234                  1x 1x 1x 1x 1x 1x 1x                                                                                                                                                                                                                                                                                                                                                                                                                                                    
import { BOOT_COMPLETED_EVENT } from './simulator-xcode-6';
import SimulatorXcode7 from './simulator-xcode-7';
import log from './logger';
import { waitForCondition } from 'asyncbox';
import { exec } from 'teen_process';
import { getAppContainer, openUrl as simctlOpenUrl, terminate } from 'node-simctl';
 
 
// these sims are sloooooooow
const STARTUP_TIMEOUT = 120 * 1000;
const SAFARI_STARTUP_TIMEOUT = 25 * 1000;
const UI_CLIENT_STARTUP_TIMEOUT = 5 * 1000;
const SPRINGBOARD_BUNDLE_ID = 'com.apple.springboard';
const MOBILE_SAFARI_BUNDLE_ID = 'com.apple.mobilesafari';
const UI_CLIENT_BUNDLE_ID = 'com.apple.iphonesimulator';
const PROCESS_LAUNCH_OK_PATTERN = (bundleId) => new RegExp(`${bundleId.replace('.', '\\.')}:\\s+\\d+`);
 
class SimulatorXcode8 extends SimulatorXcode7 {
  constructor (udid, xcodeVersion) {
    super(udid, xcodeVersion);
 
    // list of files to check for when seeing if a simulator is "fresh"
    // (meaning it has never been booted).
    // If these files are present, we assume it's been successfully booted
    this.isFreshFiles = [
      'Library/Cookies',
      'Library/Preferences/.GlobalPreferences.plist',
      'Library/Preferences/com.apple.springboard.plist',
      'var/run/syslog.pid'
    ];
  }
 
  /**
   * @return {string} Bundle identifier of Simulator UI client.
   */
  get uiClientBundleId () {
    return UI_CLIENT_BUNDLE_ID;
  }
 
  /**
   * Check the state of Simulator UI client.
   * @Override
   *
   * @return {boolean} True of if UI client is running or false otherwise.
   */
  async isUIClientRunning () {
    const args = ['-e', `tell application "System Events" to count processes whose bundle identifier is "${this.uiClientBundleId}"`];
    const {stdout} = await exec('osascript', args);
    const count = parseInt(stdout, 10);
    if (isNaN(count)) {
      log.errorAndThrow(`Cannot parse the count of running Simulator UI client instances from 'osascript ${args}' output: ${stdout}`);
    }
    log.debug(`The count of running Simulator UI client instances is ${count}`);
    return count >= 1;
  }
 
  /**
   * Kill the UI client if it is running.
   *
   * @param {boolean} force - Set it to true to send SIGKILL signal to Simulator process.
   *                          SIGTERM will be sent by default.
   * @return {boolean} True if the UI client was successfully killed or false
   *                   if it is not running.
   */
  async killUIClient (force = false) {
    const osascriptArgs = ['-e', `tell application "System Events" to unix id of processes whose bundle identifier is "${this.uiClientBundleId}"`];
    const {stdout} = await exec('osascript', osascriptArgs);
    if (!stdout.trim().length) {
      return false;
    }
    const killArgs = force ? ['-9', stdout.trim()] : [stdout.trim()];
    await exec('kill', killArgs);
    return true;
  }
 
  /**
   * Start the Simulator UI client with the given arguments
   * and wait until it is running.
   * @override
   */
  async startUIClient (opts = {}) {
    await super.startUIClient(opts);
    try {
      await waitForCondition(async () => {
        return await this.isUIClientRunning();
      }, {waitMs: UI_CLIENT_STARTUP_TIMEOUT, intervalMs: 300});
    } catch (ign) {
      log.warn(`The Simulator UI client is not running after ${UI_CLIENT_STARTUP_TIMEOUT} ms timeout`);
    }
  }
 
  /**
   * Verify whether the particular application is installed on Simulator.
   * @override
   *
   * @param {string} bundleId - The bundle id of the application to be checked.
   * @param {string} appFule - Application name minus ".app" (for iOS 7.1)
   * @return {boolean} True if the given application is installed
   */
  async isAppInstalled (bundleId) {
    try {
      let appContainer = await getAppContainer(this.udid, bundleId, false);
      return appContainer.endsWith('.app');
    } catch (err) {
      return false;
    }
  }
 
  /**
   * @return {string} Application bundle id, which signals that Simulator booting is
   * competed if it is running.
   */
  get startupPollBundleId () {
    return SPRINGBOARD_BUNDLE_ID;
  }
 
  /**
   * @return {number} The max number of milliseconds to wait until Simulator booting is completed.
   */
  get startupTimeout () {
    return STARTUP_TIMEOUT;
  }
 
  /**
   * Verify whether the Simulator booting is completed and/or wait for it
   * until the timeout expires.
   * @override
   *
   * @param {number} startupTimeout - the number of milliseconds to wait until booting is completed.
   * @emits BOOT_COMPLETED_EVENT if the current Simulator is ready to accept simctl commands, like 'install'.
   */
  async waitForBoot (startupTimeout) {
    const startupTimestamp = process.hrtime();
    let lastError = null;
    try {
      let isOnBootCompletedEmitted = false;
      await waitForCondition(async () => {
        try {
          // 'springboard' process should be the last one to start after boot
          // 'simctl launch' will block until this process is running or fail if booting is still in progress
          const {stdout} = await exec('xcrun', ['simctl', 'launch', this.udid, this.startupPollBundleId]);
          if (PROCESS_LAUNCH_OK_PATTERN(this.startupPollBundleId).test(stdout)) {
            if (!isOnBootCompletedEmitted) {
              isOnBootCompletedEmitted = true;
              this.emit(BOOT_COMPLETED_EVENT);
            }
 
            return true;
          }
        } catch (err) {
          lastError = err.stderr || err.message;
        }
        return false;
      }, {waitMs: startupTimeout, intervalMs: 1500});
    } catch (err) {
      log.errorAndThrow(`Simulator is not booted after ${process.hrtime(startupTimestamp)[0]} seconds ` +
                        `because of: ${lastError || 'an unknown error'}`);
    }
  }
 
  /**
   * Open the given URL in mobile Safari browser.
   * The browser will be started automatically if it is not running.
   * @override
   *
   * @param {string} url - The URL to be opened.
   */
  async openUrl (url) {
    if (!await this.isRunning()) {
      throw new Error(`Tried to open ${url}, but Simulator is not in Booted state`);
    }
    const launchTimestamp = process.hrtime();
    let lastError = null;
    try {
      await waitForCondition(async () => {
        try {
          // This is to make sure Safari is already running
          const {stdout} = await exec('xcrun', ['simctl', 'launch', this.udid, MOBILE_SAFARI_BUNDLE_ID]);
          if (PROCESS_LAUNCH_OK_PATTERN(MOBILE_SAFARI_BUNDLE_ID).test(stdout)) {
            await simctlOpenUrl(this.udid, url);
            return true;
          }
        } catch (err) {
          log.error(`Failed to open '${url}' in Safari. Retrying...`);
          lastError = err.stderr || err.message;
        }
        return false;
      }, {waitMs: SAFARI_STARTUP_TIMEOUT, intervalMs: 500});
    } catch (err) {
      log.errorAndThrow(`Safari cannot open '${url}' after ${process.hrtime(launchTimestamp)[0]} seconds ` +
                        `because of: ${lastError || 'an unknown error'}`);
    }
    log.debug(`Safari has successfully opened '${url}' in ${process.hrtime(launchTimestamp)[0]} seconds`);
  }
 
  /**
   * Clean up the directories for mobile Safari.
   * @override
   *
   * @param {boolean} keepPrefs - Whether to keep Safari preferences from being deleted.
   */
  async cleanSafari (keepPrefs = true) {
    try {
      await terminate(this.udid, MOBILE_SAFARI_BUNDLE_ID);
    } catch (ign) {
      // ignore error
    }
    await super.cleanSafari(keepPrefs);
  }
 
  /**
   * Clean/scrub the particular application on Simulator.
   * @override
   *
   * @param {string} appFile - Application name minus ".app".
   * @param {string} appBundleId - Bundle identifier of the application.
   * @param {boolean} scrub - If `scrub` is false, we want to clean by deleting the app and all
   *   files associated with it. If `scrub` is true, we just want to delete the preferences and
   *   changed files.
   * @return {array} Array of deletion promises.
   */
  async cleanCustomApp (appFile, appBundleId, scrub = false) {
    try {
      await terminate(this.udid, appBundleId);
    } catch (ign) {
      // ignore error
    }
    await super.cleanCustomApp(appFile, appBundleId, scrub);
  }
 
}
 
export default SimulatorXcode8;