How to use this.logs.syslog.stopCapture method in Appium Xcuitest Driver

Best JavaScript code snippet using appium-xcuitest-driver

Run Appium Xcuitest Driver automation tests on LambdaTest cloud grid

Perform automation testing on 3000+ real desktop and mobile devices online.

Sign up Free
_

driver.js

Source: driver.js Github

copy
1import { BaseDriver, DeviceSettings, errors } from 'appium-base-driver';
2import * as utils from './utils';
3import logger from './logger';
4import path from 'path';
5import _ from 'lodash';
6import B from 'bluebird';
7import { fs } from 'appium-support';
8import { getSimulator, installSSLCert, uninstallSSLCert } from 'appium-ios-simulator';
9import { prepareBootstrap, UIAutoClient } from './uiauto/uiauto';
10import { Instruments, instrumentsUtils } from './instruments';
11import { retry, waitForCondition } from 'asyncbox';
12import commands from './commands/index';
13import { desiredCapConstraints, desiredCapValidation } from './desired-caps';
14import _iDevice from 'node-idevice';
15import { SAFARI_BUNDLE } from './commands/safari';
16import { install, needsInstall, SAFARI_LAUNCHER_BUNDLE } from './safari-launcher';
17import { setLocaleAndPreferences } from './settings';
18import { runSimulatorReset, isolateSimulatorDevice, checkSimulatorAvailable,
19         moveBuiltInApp, getAdjustedDeviceName, endSimulator, runRealDeviceReset } from './device';
20import { IWDP } from './iwdp';
21
22
23// promisify _iDevice
24let iDevice = function (...args) {
25  let device = _iDevice(...args);
26  let promisified = {};
27  for (let m of ['install', 'installAndWait', 'remove', 'isInstalled']) {
28    promisified[m] = B.promisify(device[m].bind(device));
29  }
30  return promisified;
31};
32
33const defaultServerCaps = {
34  webStorageEnabled: false,
35  locationContextEnabled: false,
36  browserName: '',
37  platform: 'MAC',
38  javascriptEnabled: true,
39  databaseEnabled: false,
40  takesScreenshot: true,
41  networkConnectionEnabled: false,
42};
43
44const LOG_LOCATIONS = [
45  path.resolve('/', 'Library', 'Caches', 'com.apple.dt.instruments'),
46];
47if (process.env.HOME) {
48  LOG_LOCATIONS.push(path.resolve(process.env.HOME, 'Library', 'Logs', 'CoreSimulator'));
49}
50
51class IosDriver extends BaseDriver {
52  resetIos () {
53    this.appExt = '.app';
54    this.xcodeVersion = null;
55    this.iosSdkVersion = null;
56    this.logs = {};
57    this.instruments = null;
58    this.uiAutoClient = null;
59    this.onInstrumentsDie = function onInstrumentsDie () {};
60    this.stopping = false;
61    this.cbForCurrentCmd = null;
62    this.remote = null;
63    this.curContext = null;
64    this.curWebFrames = [];
65    this.selectingNewPage = false;
66    this.windowHandleCache = [];
67    this.webElementIds = [];
68    this.implicitWaitMs = 0;
69    this.asynclibWaitMs = 0;
70    this.pageLoadMs = 6000;
71    this.asynclibResponseCb = null;
72    this.returnedFromExecuteAtom = {};
73    this.executedAtomsCounter = 0;
74    this.curCoords = null;
75    this.curWebCoords = null;
76    this.landscapeWebCoordsOffset = 0;
77    this.keepAppToRetainPrefs = false;
78    this.ready = false;
79    this.asyncWaitMs = 0;
80
81    this.settings = new DeviceSettings({}, _.noop);
82
83    this.locatorStrategies = [
84      'xpath',
85      'id',
86      'class name',
87      '-ios uiautomation',
88      'accessibility id'
89    ];
90    this.webLocatorStrategies = [
91      'link text',
92      'css selector',
93      'tag name',
94      'partial link text'
95    ];
96  }
97
98  constructor (opts, shouldValidateCaps) {
99    super(opts, shouldValidateCaps);
100
101    this.desiredCapConstraints = desiredCapConstraints;
102    this.resetIos();
103    this.getDevicePixelRatio = _.memoize(this.getDevicePixelRatio);
104  }
105
106  validateLocatorStrategy (strategy) {
107    super.validateLocatorStrategy(strategy, this.isWebContext());
108  }
109
110  async start () {
111    if (this.isRealDevice()) {
112      await this.startRealDevice();
113    } else {
114      await this.startSimulator();
115    }
116    this.ready = true;
117  }
118
119  async createSession (...args) {
120    let [sessionId, caps] = await super.createSession(...args);
121
122    // appium-ios-driver uses Instruments to automate the device
123    // but Xcode 8 does not have Instruments, so short circuit
124    this.xcodeVersion = await utils.getAndCheckXcodeVersion(this.opts);
125    logger.debug(`Xcode version set to ${this.xcodeVersion.versionString}`);
126    if (this.xcodeVersion.major >= 8) {
127      let msg = `Appium's IosDriver does not support Xcode version ${this.xcodeVersion.versionString}. ` +
128                'Apple has deprecated UIAutomation. Use the "XCUITest" automationName capability instead.';
129      logger.errorAndThrow(new errors.SessionNotCreatedError(msg));
130    }
131
132    // merge server capabilities + desired capabilities
133    this.caps = Object.assign({}, defaultServerCaps, this.caps);
134    this.caps.desired = caps;
135
136    await utils.detectUdid(this.opts);
137    await utils.prepareIosOpts(this.opts);
138    this.realDevice = null;
139    this.useRobot = this.opts.useRobot;
140    this.safari = this.opts.safari;
141    this.opts.curOrientation = this.opts.initialOrientation;
142
143    this.sock = path.resolve(this.opts.tmpDir || '/tmp', 'instruments_sock');
144
145    try {
146      await this.configureApp();
147    } catch (err) {
148      logger.error(`Bad app: '${this.opts.app}'. App paths need to ` +
149                   `be absolute, or relative to the appium server ` +
150                   `install dir, or a URL to compressed file, or a ` +
151                   `special app name.`);
152      throw err;
153    }
154
155    await this.start();
156
157    // TODO: this should be in BaseDriver.postCreateSession
158    this.startNewCommandTimeout('createSession');
159    return [sessionId, this.caps];
160  }
161
162  async stop () {
163    this.ready = false;
164
165    if (this.uiAutoClient) {
166      await this.uiAutoClient.shutdown();
167    }
168
169    if (this.instruments) {
170      try {
171        await this.instruments.shutdown();
172      } catch (err) {
173        logger.error(`Instruments didn't shut down. ${err}`);
174      }
175    }
176
177    if (this.caps && this.caps.customSSLCert && !this.isRealDevice()) {
178      logger.debug(`Uninstalling ssl certificate for udid '${this.sim.udid}'`);
179      await uninstallSSLCert(this.caps.customSSLCert, this.sim.udid);
180    }
181
182    if (this.opts.enableAsyncExecuteFromHttps && !this.isRealDevice()) {
183      await this.stopHttpsAsyncServer();
184    }
185
186    this.uiAutoClient = null;
187    this.instruments = null;
188    this.realDevice = null;
189
190    // postcleanup
191    this.curCoords = null;
192    this.opts.curOrientation = null;
193    if (!_.isEmpty(this.logs)) {
194      await this.logs.syslog.stopCapture();
195      this.logs = {};
196    }
197
198    if (this.remote) {
199      await this.stopRemote();
200    }
201
202    await this.stopIWDP();
203  }
204
205  async deleteSession () {
206    logger.debug('Deleting ios session');
207
208    await this.stop();
209
210    if (this.opts.clearSystemFiles) {
211      await utils.clearLogs(LOG_LOCATIONS);
212    } else {
213      logger.debug('Not clearing log files. Use `clearSystemFiles` capability to turn on.');
214    }
215
216    if (this.isRealDevice()) {
217      await runRealDeviceReset(this.realDevice, this.opts);
218    } else {
219      await runSimulatorReset(this.sim, this.opts, this.keepAppToRetainPrefs);
220    }
221    await super.deleteSession();
222  }
223
224  async getSession () {
225    let caps = await super.getSession();
226
227    const viewportRect = await this.getViewportRect();
228    const pixelRatio = await this.getDevicePixelRatio();
229    const statBarHeight = await this.getStatusBarHeight();
230
231    caps.viewportRect = viewportRect;
232    caps.pixelRatio = pixelRatio;
233    caps.statBarHeight = statBarHeight;
234
235    return caps;
236  }
237
238  async executeCommand (cmd, ...args) {
239    logger.debug(`Executing iOS command '${cmd}'`);
240    if (cmd === 'receiveAsyncResponse') {
241      return await this.receiveAsyncResponse(...args);
242    } else if (this.ready || _.includes(['launchApp'], cmd)) {
243      return await super.executeCommand(cmd, ...args);
244    }
245
246    throw new errors.NoSuchDriverError(`Driver is not ready, cannot execute ${cmd}.`);
247  }
248
249  // TODO: reformat this.helpers + configureApp
250  async configureApp () {
251    try {
252      // if the app name is a bundleId assign it to the bundleId property
253      if (!this.opts.bundleId && utils.appIsPackageOrBundle(this.opts.app)) {
254        this.opts.bundleId = this.opts.app;
255      }
256
257      if (this.opts.app && this.opts.app.toLowerCase() === 'settings') {
258        if (parseFloat(this.opts.platformVersion) >= 8) {
259          logger.debug('We are on iOS8+ so not copying preferences app');
260          this.opts.bundleId = 'com.apple.Preferences';
261          this.opts.app = null;
262        }
263      } else if (this.opts.app && this.opts.app.toLowerCase() === 'calendar') {
264        if (parseFloat(this.opts.platformVersion) >= 8) {
265          logger.debug('We are on iOS8+ so not copying calendar app');
266          this.opts.bundleId = 'com.apple.mobilecal';
267          this.opts.app = null;
268        }
269      } else if (this.isSafari()) {
270        if (!this.isRealDevice()) {
271          if (parseFloat(this.opts.platformVersion) >= 8) {
272            logger.debug('We are on iOS8+ so not copying Safari app');
273            this.opts.bundleId = SAFARI_BUNDLE;
274            this.opts.app = null;
275          }
276        } else {
277          // on real device, need to check if safari launcher exists
278          // first check if it is already on the device
279          if (!await this.realDevice.isInstalled(this.opts.bundleId)) {
280            // it's not on the device, so check if we need to build
281            if (await needsInstall()) {
282              logger.debug('SafariLauncher not found, building...');
283              await install();
284            }
285            this.opts.bundleId = SAFARI_LAUNCHER_BUNDLE;
286          }
287        }
288      } else if (this.opts.bundleId &&
289                 utils.appIsPackageOrBundle(this.opts.bundleId) &&
290                 (this.opts.app === '' || utils.appIsPackageOrBundle(this.opts.app))) {
291        // we have a bundle ID, but no app, or app is also a bundle
292        logger.debug('App is an iOS bundle, will attempt to run as pre-existing');
293      } else {
294        this.opts.app = await this.helpers.configureApp(this.opts.app, '.app');
295      }
296    } catch (err) {
297      logger.error(err);
298      throw new Error(
299        `Bad app: ${this.opts.app}. App paths need to be absolute, or relative to the appium ` +
300        'server install dir, or a URL to compressed file, or a special app name.');
301    }
302  }
303
304  async startSimulator () {
305    await utils.removeInstrumentsSocket(this.sock);
306
307    if (!this.xcodeVersion) {
308      logger.debug('Setting Xcode version');
309      this.xcodeVersion = await utils.getAndCheckXcodeVersion(this.opts);
310      logger.debug(`Xcode version set to ${this.xcodeVersion.versionString}`);
311    }
312
313    logger.debug('Setting iOS SDK Version');
314    this.iosSdkVersion = await utils.getAndCheckIosSdkVersion();
315    logger.debug(`iOS SDK Version set to ${this.iosSdkVersion}`);
316
317    let timeout = _.isObject(this.opts.launchTimeout) ? this.opts.launchTimeout.global : this.opts.launchTimeout;
318    let availableDevices = await retry(3, instrumentsUtils.getAvailableDevices, timeout);
319
320    let iosSimUdid = await checkSimulatorAvailable(this.opts, this.iosSdkVersion, availableDevices);
321
322    this.sim = await getSimulator(iosSimUdid, this.xcodeVersion.versionString);
323
324    await moveBuiltInApp(this.sim);
325
326    this.opts.localizableStrings = await utils.parseLocalizableStrings(this.opts);
327
328    await utils.setBundleIdFromApp(this.opts);
329
330    await this.createInstruments();
331
332    {
333      // previously setDeviceInfo()
334      this.shouldPrelaunchSimulator = utils.shouldPrelaunchSimulator(this.opts, this.iosSdkVersion);
335      let dString = await getAdjustedDeviceName(this.opts);
336      if (this.caps.app) {
337        await utils.setDeviceTypeInInfoPlist(this.opts.app, dString);
338      }
339    }
340
341    await runSimulatorReset(this.sim, this.opts, this.keepAppToRetainPrefs);
342
343    if (this.caps.customSSLCert && !this.isRealDevice()) {
344      await installSSLCert(this.caps.customSSLCert, this.sim.udid);
345    }
346
347    if (this.opts.enableAsyncExecuteFromHttps && !this.isRealDevice()) {
348      // await this.sim.shutdown();
349      await this.startHttpsAsyncServer();
350    }
351
352    await isolateSimulatorDevice(this.sim, this.opts);
353    this.localConfig = await setLocaleAndPreferences(this.sim, this.opts, this.isSafari(), endSimulator);
354    await this.setUpLogCapture();
355    await this.prelaunchSimulator();
356    await this.startInstruments();
357    await this.onInstrumentsLaunch();
358    await this.configureBootstrap();
359    await this.setBundleId();
360    await this.setInitialOrientation();
361    await this.initAutoWebview();
362    await this.waitForAppLaunched();
363  }
364
365  async startRealDevice () {
366    await utils.removeInstrumentsSocket(this.sock);
367    this.opts.localizableStrings = await utils.parseLocalizableStrings(this.opts);
368    await utils.setBundleIdFromApp(this.opts);
369    await this.createInstruments();
370    await runRealDeviceReset(this.realDevice, this.opts);
371    await this.setUpLogCapture();
372    await this.installToRealDevice();
373    await this.startInstruments();
374    await this.onInstrumentsLaunch();
375    await this.configureBootstrap();
376    await this.setBundleId();
377    await this.setInitialOrientation();
378    await this.initAutoWebview();
379    await this.waitForAppLaunched();
380  }
381
382  async installToRealDevice () {
383    // if user has passed in desiredCaps.autoLaunch = false
384    // meaning they will manage app install / launching
385    if (this.opts.autoLaunch === false) {
386      return;
387    }
388
389    // if we have an ipa file, set it in opts
390    if (this.opts.app) {
391      let ext = this.opts.app.substring(this.opts.app.length - 3).toLowerCase();
392      if (ext === 'ipa') {
393        this.opts.ipa = this.opts.app;
394      }
395    }
396
397    if (this.opts.udid) {
398      if (await this.realDevice.isInstalled(this.opts.bundleId)) {
399        logger.debug('App is installed.');
400        if (this.opts.fullReset) {
401          logger.debug('fullReset requested. Forcing app install.');
402        } else {
403          logger.debug('fullReset not requested. No need to install.');
404          return;
405        }
406      } else {
407        logger.debug('App is not installed. Will try to install.');
408      }
409
410      if (this.opts.ipa && this.opts.bundleId) {
411        await this.installIpa();
412        logger.debug('App installed.');
413      } else if (this.opts.ipa) {
414        let msg = 'You specified a UDID and ipa but did not include the bundle id';
415        logger.warn(msg);
416        throw new errors.UnknownError(msg);
417      } else if (this.opts.app) {
418        await this.realDevice.install(this.opts.app);
419        logger.debug('App installed.');
420      } else {
421        logger.debug('Real device specified but no ipa or app path, assuming bundle ID is ' +
422                     'on device');
423      }
424    } else {
425      logger.debug('No device id or app, not installing to real device.');
426    }
427  }
428
429  getIDeviceObj () {
430    let idiPath = path.resolve(__dirname, '../../../build/',
431                               'libimobiledevice-macosx/ideviceinstaller');
432    logger.debug(`Creating iDevice object with udid ${this.opts.udid}`);
433    try {
434      return iDevice(this.opts.udid);
435    } catch (e1) {
436      logger.debug(`Couldn't find ideviceinstaller, trying built-in at ${idiPath}`);
437      try {
438        return iDevice(this.opts.udid, {cmd: idiPath});
439      } catch (e2) {
440        let msg = 'Could not initialize ideviceinstaller; make sure it is ' +
441                  'installed and works on your system';
442        logger.error(msg);
443        throw new Error(msg);
444      }
445    }
446  }
447
448  async installIpa () {
449    logger.debug(`Installing ipa found at ${this.opts.ipa}`);
450    if (await this.realDevice.isInstalled(this.opts.bundleId)) {
451      logger.debug('Bundle found on device, removing before reinstalling.');
452      await this.realDevice.remove(this.opts.bundleId);
453    } else {
454      logger.debug('Nothing found on device, going ahead and installing.');
455    }
456    await this.realDevice.installAndWait(this.opts.ipa, this.opts.bundleId);
457  }
458
459  validateDesiredCaps (caps) {
460    // check with the base class, and return if it fails
461    let res = super.validateDesiredCaps(caps);
462    if (!res) return res; // eslint-disable-line curly
463
464    return desiredCapValidation(caps);
465  }
466
467  async prelaunchSimulator () {
468    if (!this.shouldPrelaunchSimulator) {
469      logger.debug('Not pre-launching simulator');
470      return;
471    }
472    await endSimulator(this.sim);
473    // TODO: implement prelaunch sim in simulator package
474  }
475
476  async onInstrumentsLaunch () {
477    logger.debug('Instruments launched. Starting poll loop for new commands.');
478    if (this.opts.origAppPath) {
479      logger.debug('Copying app back to its original place');
480      return await fs.copyFile(this.opts.app, this.opts.origAppPath);
481    }
482  }
483
484  async setBundleId () {
485    if (this.opts.bundleId) {
486      // We already have a bundle Id
487      return;
488    } else {
489      let bId = await this.uiAutoClient.sendCommand('au.bundleId()');
490      logger.debug(`Bundle ID for open app is ${bId.value}`);
491      this.opts.bundleId = bId.value;
492    }
493  }
494
495  async startIWDP () {
496    if (this.opts.startIWDP) {
497      this.iwdpServer = new IWDP({
498        webkitDebugProxyPort: this.opts.webkitDebugProxyPort,
499        udid: this.opts.udid,
500        logStdout: !!this.opts.showIWDPLog,
501      });
502      await this.iwdpServer.start();
503    }
504  }
505
506  async stopIWDP () {
507    if (this.iwdpServer) {
508      await this.iwdpServer.stop();
509      delete this.iwdpServer;
510    }
511  }
512
513  async setInitialOrientation () {
514    if (_.isString(this.opts.initialOrientation) &&
515        _.includes(['LANDSCAPE', 'PORTRAIT'], this.opts.initialOrientation.toUpperCase())) {
516      logger.debug(`Setting initial orientation to ${this.opts.initialOrientation}`);
517      let command = `au.setScreenOrientation('${this.opts.initialOrientation.toUpperCase()}')`;
518      try {
519        await this.uiAutoClient.sendCommand(command);
520        this.opts.curOrientation = this.opts.initialOrientation;
521      } catch (err) {
522        logger.warn(`Setting initial orientation failed with: ${err}`);
523      }
524    }
525  }
526
527  isRealDevice () {
528    return !!this.opts.udid;
529  }
530
531  isSafari () {
532    return this.opts.safari;
533  }
534
535  async waitForAppLaunched () {
536    // on iOS8 in particular, we can get a working session before the app
537    // is ready to respond to commands; in that case the source will be empty
538    // so we just spin until it's not
539    let condFn;
540    if (this.opts.waitForAppScript) {
541      // the default getSourceForElementForXML does not fit some use case, so making this customizable.
542      // TODO: collect script from customer and propose several options, please comment in issue #4190.
543      logger.debug(`Using custom script to wait for app start: ${this.opts.waitForAppScript}`);
544      condFn = async () => {
545        let res;
546        try {
547          res = await this.uiAutoClient.sendCommand(`try{\n${this.opts.waitForAppScript}` +
548                     `\n} catch(err) { $.log("waitForAppScript err: " + error); false; };`);
549        } catch (err) {
550          logger.debug(`Cannot eval waitForAppScript script, err: ${err}`);
551          return false;
552        }
553        if (typeof res !== 'boolean') {
554          logger.debug('Unexpected return type in waitForAppScript script');
555          return false;
556        }
557        return res;
558      };
559    } else if (this.isSafari()) {
560      if (this.isRealDevice()) {
561        await this.clickButtonToLaunchSafari();
562      }
563      logger.debug('Waiting for initial webview');
564      await this.navToInitialWebview();
565      condFn = async () => true; // eslint-disable-line require-await
566    } else {
567      logger.debug('Waiting for app source to contain elements');
568      condFn = async () => {
569        try {
570          let source = await this.getSourceForElementForXML();
571          source = JSON.parse(source || '{}');
572          let appEls = (source.UIAApplication || {})['>'];
573          return appEls && appEls.length > 0 && !IosDriver.isSpringBoard(source.UIAApplication);
574        } catch (e) {
575          logger.warn(`Couldn't extract app element from source, error was: ${e}`);
576          return false;
577        }
578      };
579    }
580    try {
581      await waitForCondition(condFn, {logger, waitMs: 10000, intervalMs: 500});
582    } catch (err) {
583      if (err.message && err.message.match(/Condition unmet/)) {
584        logger.warn('Initial spin timed out, continuing but the app might not be ready.');
585        logger.debug(`Initial spin error was: ${err}`);
586      } else {
587        throw err;
588      }
589    }
590  }
591
592  static isSpringBoard (uiAppObj) {
593  // TODO: move to helpers
594  // Test for iOS homescreen (SpringBoard). AUT occassionally start the sim, but fails to load
595  // the app. If that occurs, getSourceForElementFoXML will return a doc object that meets our
596  // app-check conditions, resulting in a false positive. This function tests the UiApplication
597  // property's meta data to ensure that the Appium doesn't confuse SpringBoard with the app
598  // under test.
599    return _.propertyOf(uiAppObj['@'])('name') === 'SpringBoard';
600  }
601
602  async createInstruments () {
603    logger.debug('Creating instruments');
604    this.uiAutoClient = new UIAutoClient(this.sock);
605    this.instruments = await this.makeInstruments();
606    this.instruments.onShutdown.catch(async () => { // eslint-disable-line promise/catch-or-return
607      // unexpected exit
608      await this.startUnexpectedShutdown(new errors.UnknownError('Abnormal Instruments termination!'));
609    }).done();
610  }
611
612  shouldIgnoreInstrumentsExit () {
613    return this.safari && this.isRealDevice();
614  }
615
616  async makeInstruments () {
617    // at the moment all the logging in uiauto is at debug level
618    let bootstrapPath = await prepareBootstrap({
619      sock: this.sock,
620      interKeyDelay: this.opts.interKeyDelay,
621      justLoopInfinitely: false,
622      autoAcceptAlerts: this.opts.autoAcceptAlerts,
623      autoDismissAlerts: this.opts.autoDismissAlerts,
624      sendKeyStrategy: this.opts.sendKeyStrategy || (this.isRealDevice() ? 'grouped' : 'oneByOne')
625    });
626    let instruments = new Instruments({
627      // on real devices bundleId is always used
628      app: (!this.isRealDevice() ? this.opts.app : null) || this.opts.bundleId,
629      udid: this.opts.udid,
630      processArguments: this.opts.processArguments,
631      ignoreStartupExit: this.shouldIgnoreInstrumentsExit(),
632      bootstrap: bootstrapPath,
633      template: this.opts.automationTraceTemplatePath,
634      instrumentsPath: this.opts.instrumentsPath,
635      withoutDelay: this.opts.withoutDelay,
636      platformVersion: this.opts.platformVersion,
637      webSocket: this.opts.webSocket,
638      launchTimeout: this.opts.launchTimeout,
639      flakeyRetries: this.opts.backendRetries,
640      realDevice: this.isRealDevice(),
641      simulatorSdkAndDevice: this.iosSdkVersion >= 7.1 ? await getAdjustedDeviceName(this.opts) : null,
642      tmpDir: path.resolve(this.opts.tmpDir || '/tmp', 'appium-instruments'),
643      traceDir: this.opts.traceDir,
644      locale: this.opts.locale,
645      language: this.opts.language
646    });
647    return instruments;
648  }
649
650  async startInstruments () {
651    logger.debug('Starting UIAutoClient, and launching Instruments.');
652
653    await B.all([
654      this.uiAutoClient.start().then(() => { this.instruments.registerLaunch(); }),
655      this.instruments.launch()
656    ]);
657  }
658
659  async configureBootstrap () {
660    logger.debug('Setting bootstrap config keys/values');
661    let isVerbose = true; // TODO: level was configured according to logger
662    let cmd = 'target = $.target();\n';
663    cmd += 'au = $;\n';
664    cmd += `$.isVerbose = ${isVerbose};\n`;
665    // Not using uiauto grace period because of bug.
666    // cmd += '$.target().setTimeout(1);\n';
667    await this.uiAutoClient.sendCommand(cmd);
668  }
669
670  async getSourceForElementForXML (ctx) {
671    let source;
672    if (!ctx) {
673      source = await this.uiAutoClient.sendCommand('au.mainApp().getTreeForXML()');
674    } else {
675      source = await this.uiAutoClient.sendCommand(`au.getElement('${ctx}').getTreeForXML()`);
676    }
677    // TODO: all this json/xml logic is very expensive, we need
678    // to use a SAX parser instead.
679    if (source) {
680      return JSON.stringify(source);
681    } else {
682      // this should never happen but we've received bug reports; this will help us track down
683      // what's wrong in getTreeForXML
684      throw new Error(`Bad response from getTreeForXML. res was ${JSON.stringify(source)}`);
685    }
686  }
687
688  async setUpLogCapture () {
689    if (this.caps.skipLogCapture) {
690      logger.info("'skipLogCapture' is set. Skipping the collection of system logs and crash reports.");
691      return;
692    }
693
694    if (this.isRealDevice()) {
695      await this.startLogCapture();
696    } else {
697      await this.startLogCapture(this.sim);
698    }
699  }
700
701  get realDevice () {
702    this._realDevice = this._realDevice || this.getIDeviceObj();
703    return this._realDevice;
704  }
705
706  set realDevice (rd) {
707    this._realDevice = rd;
708  }
709}
710
711for (let [cmd, fn] of _.toPairs(commands)) {
712  IosDriver.prototype[cmd] = fn;
713}
714
715
716export { IosDriver, defaultServerCaps };
717export default IosDriver;
718
Full Screen

ios.js

Source: ios.js Github

copy
1"use strict";
2var path = require('path')
3  , rimraf = require('rimraf')
4  , ncp = require('ncp').ncp
5  , fs = require('fs')
6  , _ = require('underscore')
7  , which = require('which')
8  , logger = require('../../server/logger.js').get('appium')
9  , exec = require('child_process').exec
10  , spawn = require('child_process').spawn
11  , bplistCreate = require('bplist-creator')
12  , bplistParse = require('bplist-parser')
13  , xmlplist = require('plist')
14  , Device = require('../device.js')
15  , Instruments = require('./instruments.js')
16  , xcode = require('../../future.js').xcode
17  , errors = require('../../server/errors.js')
18  , deviceCommon = require('../common.js')
19  , iOSLog = require('./ios-log.js')
20  , iOSCrashLog = require('./ios-crash-log.js')
21  , status = require("../../server/status.js")
22  , iDevice = require('node-idevice')
23  , async = require('async')
24  , iOSController = require('./ios-controller.js')
25  , iOSHybrid = require('./ios-hybrid.js')
26  , settings = require('./settings.js')
27  , Simulator = require('./simulator.js')
28  , prepareBootstrap = require('./uiauto').prepareBootstrap
29  , CommandProxy = require('./uiauto').CommandProxy
30  , UnknownError = errors.UnknownError
31  , binaryPlist = true
32  , Args = require("vargs").Constructor
33  , logCustomDeprecationWarning = require('../../helpers.js').logCustomDeprecationWarning;
34
35// XML Plist library helper
36var parseXmlPlistFile = function (plistFilename, cb) {
37  try {
38    var xmlContent = fs.readFileSync(plistFilename, 'utf8');
39    var result = xmlplist.parse(xmlContent);
40    return cb(null, result);
41  } catch (ex) {
42    return cb(ex);
43  }
44};
45
46var parsePlistFile = function (plist, cb) {
47  bplistParse.parseFile(plist, function (err, obj) {
48    if (err) {
49      logger.debug("Could not parse plist file (as binary) at " + plist);
50      logger.info("Will try to parse the plist file as XML");
51      parseXmlPlistFile(plist, function (err, obj) {
52        if (err) {
53          logger.debug("Could not parse plist file (as XML) at " + plist);
54          return cb(err, null);
55        } else {
56          logger.debug("Parsed app Info.plist (as XML)");
57          binaryPlist = false;
58          cb(null, obj);
59        }
60      });
61    } else {
62      binaryPlist = true;
63      if (obj.length) {
64        logger.debug("Parsed app Info.plist (as binary)");
65        cb(null, obj[0]);
66      } else {
67        cb(new Error("Binary Info.plist appears to be empty"));
68      }
69    }
70  });
71};
72
73var IOS = function () {
74  this.init();
75};
76
77_.extend(IOS.prototype, Device.prototype);
78
79IOS.prototype._deviceInit = Device.prototype.init;
80IOS.prototype.init = function () {
81  this._deviceInit();
82  this.appExt = ".app";
83  this.capabilities = {
84    webStorageEnabled: false
85  , locationContextEnabled: false
86  , browserName: 'iOS'
87  , platform: 'MAC'
88  , javascriptEnabled: true
89  , databaseEnabled: false
90  , takesScreenshot: true
91  , networkConnectionEnabled: false
92  };
93  this.xcodeVersion = null;
94  this.iOSSDKVersion = null;
95  this.iosSimProcess = null;
96  this.iOSSimUdid = null;
97  this.logs = {};
98  this.instruments = null;
99  this.commandProxy = null;
100  this.initQueue();
101  this.onInstrumentsDie = function () {};
102  this.stopping = false;
103  this.cbForCurrentCmd = null;
104  this.remote = null;
105  this.curContext = null;
106  this.curWebFrames = [];
107  this.selectingNewPage = false;
108  this.processingRemoteCmd = false;
109  this.remoteAppKey = null;
110  this.windowHandleCache = [];
111  this.webElementIds = [];
112  this.implicitWaitMs = 0;
113  this.asyncWaitMs = 0;
114  this.pageLoadMs = 60000;
115  this.asyncResponseCb = null;
116  this.returnedFromExecuteAtom = {};
117  this.executedAtomsCounter = 0;
118  this.curCoords = null;
119  this.curWebCoords = null;
120  this.onPageChangeCb = null;
121  this.supportedStrategies = ["name", "xpath", "id", "-ios uiautomation",
122                              "class name", "accessibility id"];
123  this.landscapeWebCoordsOffset = 0;
124  this.localizableStrings = {};
125  this.keepAppToRetainPrefs = false;
126  this.isShuttingDown = false;
127};
128
129IOS.prototype._deviceConfigure = Device.prototype.configure;
130IOS.prototype.configure = function (args, caps, cb) {
131  var msg;
132  this._deviceConfigure(args, caps);
133  this.setIOSArgs();
134
135  if (this.args.locationServicesAuthorized && !this.args.bundleId) {
136    msg = "You must set the bundleId cap if using locationServicesEnabled";
137    logger.error(msg);
138    return cb(new Error(msg));
139  }
140
141  // on iOS8 we can use a bundleId to launch an app on the simulator, but
142  // on previous versions we can only do so on a real device, so we need
143  // to do a check of which situation we're in
144  var ios8 = caps.platformVersion &&
145             parseFloat(caps.platformVersion) >= 8;
146
147  if (!this.args.app &&
148      !((ios8 || this.args.udid) && this.args.bundleId)) {
149    msg = "Please provide the 'app' or 'browserName' capability or start " +
150          "appium with the --app or --browser-name argument. Alternatively, " +
151          "you may provide the 'bundleId' and 'udid' capabilities for an app " +
152          "under test on a real device.";
153    logger.error(msg);
154
155    return cb(new Error(msg));
156  }
157
158  if (parseFloat(caps.platformVersion) < 7.1) {
159    logCustomDeprecationWarning('iOS version', caps.platformVersion,
160                                'iOS ' + caps.platformVersion + ' support has ' +
161                                'been deprecated and will be removed in a ' +
162                                'future version of Appium.');
163  }
164
165  return this.configureApp(cb);
166};
167
168IOS.prototype.setIOSArgs = function () {
169  this.args.withoutDelay = !this.args.nativeInstrumentsLib;
170  this.args.reset = !this.args.noReset;
171  this.args.initialOrientation = this.capabilities.deviceOrientation ||
172                                 this.args.orientation ||
173                                 "PORTRAIT";
174  this.useRobot = this.args.robotPort > 0;
175  this.args.robotUrl = this.useRobot ?
176    "http://" + this.args.robotAddress + ":" + this.args.robotPort + "" :
177    null;
178  this.curOrientation = this.args.initialOrientation;
179  this.sock = path.resolve(this.args.tmpDir || '/tmp', 'instruments_sock');
180
181  this.perfLogEnabled = !!(typeof this.args.loggingPrefs === 'object' && this.args.loggingPrefs.performance);
182};
183
184IOS.prototype.configureApp = function (cb) {
185  var _cb = cb;
186  cb = function (err) {
187    if (err) {
188      err = new Error("Bad app: " + this.args.app + ". App paths need to " +
189                      "be absolute, or relative to the appium server " +
190                      "install dir, or a URL to compressed file, or a " +
191                      "special app name. cause: " + err);
192    }
193    _cb(err);
194  }.bind(this);
195
196  var app = this.appString();
197
198  // if the app name is a bundleId assign it to the bundleId property
199  if (!this.args.bundleId && this.appIsPackageOrBundle(app)) {
200    this.args.bundleId = app;
201  }
202
203  if (app !== "" && app.toLowerCase() === "settings") {
204    if (parseFloat(this.args.platformVersion) >= 8) {
205      logger.debug("We're on iOS8+ so not copying preferences app");
206      this.args.bundleId = "com.apple.Preferences";
207      this.args.app = null;
208    }
209    cb();
210  } else if (this.args.bundleId &&
211             this.appIsPackageOrBundle(this.args.bundleId) &&
212             (app === "" || this.appIsPackageOrBundle(app))) {
213    // we have a bundle ID, but no app, or app is also a bundle
214    logger.debug("App is an iOS bundle, will attempt to run as pre-existing");
215    cb();
216  } else {
217    Device.prototype.configureApp.call(this, cb);
218  }
219};
220
221IOS.prototype.removeInstrumentsSocket = function (cb) {
222  var removeSocket = function (innerCb) {
223    logger.debug("Removing any remaining instruments sockets");
224    rimraf(this.sock, function (err) {
225      if (err) return innerCb(err);
226      logger.debug("Cleaned up instruments socket " + this.sock);
227      innerCb();
228    }.bind(this));
229  }.bind(this);
230  removeSocket(cb);
231};
232
233IOS.prototype.getNumericVersion = function () {
234  return parseFloat(this.args.platformVersion);
235};
236
237IOS.prototype.startRealDevice = function (cb) {
238  async.series([
239    this.removeInstrumentsSocket.bind(this),
240    this.detectUdid.bind(this),
241    this.parseLocalizableStrings.bind(this),
242    this.setBundleIdFromApp.bind(this),
243    this.createInstruments.bind(this),
244    this.startLogCapture.bind(this),
245    this.installToRealDevice.bind(this),
246    this.startInstruments.bind(this),
247    this.onInstrumentsLaunch.bind(this),
248    this.configureBootstrap.bind(this),
249    this.setBundleId.bind(this),
250    this.setInitialOrientation.bind(this),
251    this.initAutoWebview.bind(this),
252    this.waitForAppLaunched.bind(this),
253  ], function (err) {
254    cb(err);
255  });
256};
257
258IOS.prototype.startSimulator = function (cb) {
259  async.series([
260    this.removeInstrumentsSocket.bind(this),
261    this.setXcodeVersion.bind(this),
262    this.setiOSSDKVersion.bind(this),
263    this.checkSimAvailable.bind(this),
264    this.createSimulator.bind(this),
265    this.moveBuiltInApp.bind(this),
266    this.detectUdid.bind(this),
267    this.parseLocalizableStrings.bind(this),
268    this.setBundleIdFromApp.bind(this),
269    this.createInstruments.bind(this),
270    this.setDeviceInfo.bind(this),
271    this.checkPreferences.bind(this),
272    this.runSimReset.bind(this),
273    this.isolateSimDevice.bind(this),
274    this.setLocale.bind(this),
275    this.setPreferences.bind(this),
276    this.startLogCapture.bind(this),
277    this.prelaunchSimulator.bind(this),
278    this.startInstruments.bind(this),
279    this.onInstrumentsLaunch.bind(this),
280    this.configureBootstrap.bind(this),
281    this.setBundleId.bind(this),
282    this.setInitialOrientation.bind(this),
283    this.initAutoWebview.bind(this),
284    this.waitForAppLaunched.bind(this),
285  ], function (err) {
286    cb(err);
287  });
288};
289
290IOS.prototype.start = function (cb, onDie) {
291  if (this.instruments !== null) {
292    var msg = "Trying to start a session but instruments is still around";
293    logger.error(msg);
294    return cb(new Error(msg));
295  }
296
297  if (typeof onDie === "function") {
298    this.onInstrumentsDie = onDie;
299  }
300
301  if (this.args.udid) {
302    this.startRealDevice(cb);
303  } else {
304    this.startSimulator(cb);
305  }
306};
307
308IOS.prototype.createInstruments = function (cb) {
309  logger.debug("Creating instruments");
310  this.commandProxy = new CommandProxy({ sock: this.sock });
311  this.makeInstruments(function (err, instruments) {
312    if (err) return cb(err);
313    this.instruments = instruments;
314    cb();
315  }.bind(this));
316};
317
318IOS.prototype.startInstruments = function (cb) {
319  cb = _.once(cb);
320
321  var treatError = function (err, cb) {
322    if (!_.isEmpty(this.logs)) {
323      this.logs.syslog.stopCapture();
324      this.logs = {};
325    }
326    this.postCleanup(function () {
327      cb(err);
328    });
329  }.bind(this);
330
331  logger.debug("Starting command proxy.");
332  this.commandProxy.start(
333    function onFirstConnection(err) {
334      // first let instruments know so that it does not restart itself
335      this.instruments.launchHandler(err);
336      // then we call the callback
337      cb(err);
338    }.bind(this)
339  , function regularCallback(err) {
340      if (err) return treatError(err, cb);
341      logger.debug("Starting instruments");
342      this.instruments.start(
343        function (err) {
344          if (err) return treatError(err, cb);
345          // we don't call cb here, waiting for first connection or error
346        }.bind(this)
347      , function (code) {
348          if (!this.shouldIgnoreInstrumentsExit()) {
349            this.onUnexpectedInstrumentsExit(code);
350          }
351        }.bind(this)
352      );
353    }.bind(this)
354  );
355};
356
357IOS.prototype.makeInstruments = function (cb) {
358
359  // at the moment all the logging in uiauto is at debug level
360  // TODO: be able to use info in appium-uiauto
361  var bootstrap = prepareBootstrap({
362    sock: this.sock,
363    interKeyDelay: this.args.interKeyDelay,
364    justLoopInfinitely: false,
365    autoAcceptAlerts: !(!this.args.autoAcceptAlerts || this.args.autoAcceptAlerts === 'false'),
366    autoDismissAlerts: !(!this.args.autoDismissAlerts || this.args.autoDismissAlerts === 'false'),
367    sendKeyStrategy: this.args.sendKeyStrategy || (this.args.udid ? 'grouped' : 'oneByOne')
368  });
369
370  bootstrap.then(function (bootstrapPath) {
371      var instruments = new Instruments({
372        // on real devices bundleId is always used
373        app: (!this.args.udid ? this.args.app : null) || this.args.bundleId
374      , udid: this.args.udid
375      , processArguments: this.args.processArguments
376      , ignoreStartupExit: this.shouldIgnoreInstrumentsExit()
377      , bootstrap: bootstrapPath
378      , template: this.args.automationTraceTemplatePath
379      , instrumentsPath: this.args.instrumentsPath
380      , withoutDelay: this.args.withoutDelay
381      , platformVersion: this.args.platformVersion
382      , webSocket: this.args.webSocket
383      , launchTimeout: this.args.launchTimeout
384      , flakeyRetries: this.args.backendRetries
385      , simulatorSdkAndDevice: this.iOSSDKVersion >= 7.1 ? this.getDeviceString() : null
386      , tmpDir: path.resolve(this.args.tmpDir , 'appium-instruments')
387      , traceDir: this.args.traceDir
388      });
389      cb(null, instruments);
390    }.bind(this), cb).fail(cb);
391};
392
393IOS.prototype.shouldIgnoreInstrumentsExit = function () {
394  return false;
395};
396
397IOS.prototype.onInstrumentsLaunch = function (cb) {
398  logger.debug('Instruments launched. Starting poll loop for new commands.');
399  this.instruments.setDebug(true);
400  if (this.args.origAppPath) {
401    logger.debug("Copying app back to its original place");
402    return ncp(this.args.app, this.args.origAppPath, cb);
403  }
404
405  cb();
406};
407
408IOS.prototype.setBundleId = function (cb) {
409  if (this.args.bundleId) {
410    // We already have a bundle Id
411    cb();
412  } else {
413    this.proxy('au.bundleId()', function (err, bId) {
414      if (err) return cb(err);
415      logger.debug('Bundle ID for open app is ' + bId.value);
416      this.args.bundleId = bId.value;
417      cb();
418    }.bind(this));
419  }
420};
421
422IOS.prototype.setInitialOrientation = function (cb) {
423  if (typeof this.args.initialOrientation === "string" &&
424      _.contains(["LANDSCAPE", "PORTRAIT"],
425                 this.args.initialOrientation.toUpperCase())
426      ) {
427    logger.debug("Setting initial orientation to " + this.args.initialOrientation);
428    var command = ["au.setScreenOrientation('",
429      this.args.initialOrientation.toUpperCase(), "')"].join('');
430    this.proxy(command, function (err, res) {
431      if (err || res.status !== status.codes.Success.code) {
432        logger.warn("Setting initial orientation did not work!");
433      } else {
434        this.curOrientation = this.args.initialOrientation;
435      }
436      cb();
437    }.bind(this));
438  } else {
439    cb();
440  }
441};
442
443IOS.isSpringBoard = function (uiAppObj) {
444// Test for iOS homescreen (SpringBoard). AUT occassionally start the sim, but fails to load
445// the app. If that occurs, getSourceForElementFoXML will return a doc object that meets our
446// app-check conditions, resulting in a false positive. This function tests the UiApplication
447// property's meta data to ensure that the Appium doesn't confuse SpringBoard with the app
448// under test.
449  return _.propertyOf(uiAppObj['@'])('name') === 'SpringBoard';
450};
451
452IOS.prototype.waitForAppLaunched = function (cb) {
453  // on iOS8 in particular, we can get a working session before the app
454  // is ready to respond to commands; in that case the source will be empty
455  // so we just spin until it's not
456  var condFn;
457  if (this.args.waitForAppScript) {
458    // the default getSourceForElementForXML does not fit some use case, so making this customizable.
459    // TODO: collect script from customer and propose several options, please comment in issue #4190.
460    logger.debug("Using custom script to wait for app start:" + this.args.waitForAppScript);
461    condFn = function (cb) {
462      this.proxy('try{\n' + this.args.waitForAppScript +
463                 '\n} catch(err) { $.log("waitForAppScript err: " + error); false; };',
464        function (err, res) {
465          cb(!!res.value, err);
466        });
467    }.bind(this);
468  } else {
469    logger.debug("Waiting for app source to contain elements");
470    condFn = function (cb) {
471      this.getSourceForElementForXML(null, function (err, res) {
472        if (err || !res || res.status !== status.codes.Success.code) {
473          return cb(false, err);
474        }
475        var sourceObj, appEls;
476        try {
477          sourceObj = JSON.parse(res.value);
478          appEls = sourceObj.UIAApplication['>'];
479
480          if (appEls.length > 0 && !IOS.isSpringBoard(sourceObj.UIAApplication)) {
481            return cb(true);
482          } else {
483            return cb(false, new Error("App did not have elements"));
484          }
485        } catch (e) {
486          return cb(false, new Error("Couldn't parse JSON source"));
487        }
488        return cb(true, err);
489      });
490    }.bind(this);
491  }
492  this.waitForCondition(10000, condFn, cb, 500);
493};
494
495IOS.prototype.configureBootstrap = function (cb) {
496  logger.debug("Setting bootstrap config keys/values");
497  var isVerbose = logger.transports.console.level === 'debug';
498  var cmd = '';
499  cmd += 'target = $.target();\n';
500  cmd += 'au = $;\n';
501  cmd += '$.isVerbose = ' + isVerbose + ';\n';
502  // Not using uiauto grace period because of bug.
503  // cmd += '$.target().setTimeout(1);\n';
504  this.proxy(cmd, cb);
505};
506
507IOS.prototype.onUnexpectedInstrumentsExit = function (code) {
508  logger.debug("Instruments exited unexpectedly");
509  this.isShuttingDown = true;
510  var postShutdown = function () {
511    if (typeof this.cbForCurrentCmd === "function") {
512      logger.debug("We were in the middle of processing a command when " +
513                   "instruments died; responding with a generic error");
514      var error = new UnknownError("Instruments died while responding to " +
515                                   "command, please check appium logs");
516      this.onInstrumentsDie(error, this.cbForCurrentCmd);
517    } else {
518      this.onInstrumentsDie();
519    }
520  }.bind(this);
521  if (this.commandProxy) {
522    this.commandProxy.safeShutdown(function () {
523      this.shutdown(code, postShutdown);
524    }.bind(this));
525  } else {
526    this.shutdown(code, postShutdown);
527  }
528};
529
530IOS.prototype.setXcodeVersion = function (cb) {
531  logger.debug("Setting Xcode version");
532  xcode.getVersion(function (err, versionNumber) {
533    if (err) {
534      logger.error("Could not determine Xcode version:" + err.message);
535    } else {
536      var minorVersion = parseFloat(versionNumber.slice(0, 3));
537      var pv = parseFloat(this.args.platformVersion);
538      // we deprecate Xcodes < 6.3, except for iOS 8.0 in which case we
539      // support Xcode 6.0 as well
540      if (minorVersion < 6.3 && (!(minorVersion === 6.0 && pv === 8.0))) {
541        logCustomDeprecationWarning('Xcode version', versionNumber,
542                                    'Support for Xcode ' + versionNumber + ' ' +
543                                    'has been deprecated and will be removed ' +
544                                    'in a future version. Please upgrade ' +
545                                    'to version 6.3 or higher (or version ' +
546                                    '6.0.1 for iOS 8.0)');
547      }
548    }
549    this.xcodeVersion = versionNumber;
550    logger.debug("Xcode version set to " + versionNumber);
551    cb();
552  }.bind(this));
553};
554
555IOS.prototype.setiOSSDKVersion = function (cb) {
556  logger.debug("Setting iOS SDK Version");
557  xcode.getMaxIOSSDK(function (err, versionNumber) {
558    if (err) {
559      logger.error("Could not determine iOS SDK version");
560      return cb(err);
561    }
562    this.iOSSDKVersion = versionNumber;
563    logger.debug("iOS SDK Version set to " + this.iOSSDKVersion);
564    cb();
565  }.bind(this));
566};
567
568IOS.prototype.setLocale = function (cb) {
569  var msg;
570  var setLoc = function (err) {
571    logger.debug("Setting locale information");
572    if (err) return cb(err);
573    var needSimRestart = false;
574    this.localeConfig = this.localeConfig || {};
575    _(['language', 'locale', 'calendarFormat']).each(function (key) {
576      needSimRestart = needSimRestart ||
577                      (this.args[key] &&
578                       this.args[key] !== this.localeConfig[key]);
579    }, this);
580    this.localeConfig = {
581      language: this.args.language,
582      locale: this.args.locale,
583      calendarFormat: this.args.calendarFormat
584    };
585    var simRoots = this.sim.getDirs();
586    if (simRoots.length < 1) {
587      msg = "Cannot set locale information because the iOS Simulator directory could not be determined.";
588      logger.error(msg);
589      return cb(new Error(msg));
590    }
591
592    try {
593      this.sim.setLocale(this.args.language, this.args.locale, this.args.calendarFormat);
594    } catch (e) {
595      msg = "Appium was unable to set locale info: " + e;
596      logger.error(msg);
597      return cb(new Error(msg));
598    }
599
600    logger.debug("Locale was set");
601    if (needSimRestart) {
602      logger.debug("First time setting locale, or locale changed, killing existing Instruments and Sim procs.");
603      Instruments.killAllSim();
604      Instruments.killAll();
605      setTimeout(cb, 250);
606    } else {
607      cb();
608    }
609  }.bind(this);
610
611  if ((this.args.language || this.args.locale || this.args.calendarFormat) && this.args.udid === null) {
612
613    if (this.args.fullReset && this.args.platformVersion <= 6.1) {
614      msg = "Cannot set locale information because a full-reset was requested. full-reset interferes with the language/locale caps on iOS 6.1 and older";
615      logger.error(msg);
616      return cb(new Error(msg));
617    }
618
619    if (!this.sim.dirsExist()) {
620      this.instantLaunchAndQuit(false, setLoc);
621    } else {
622      setLoc();
623    }
624  } else if (this.args.udid) {
625    logger.debug("Not setting locale because we're using a real device");
626    cb();
627  } else {
628    logger.debug("Not setting locale");
629    cb();
630  }
631};
632
633IOS.prototype.checkPreferences = function (cb) {
634  logger.debug("Checking whether we need to set app preferences");
635  if (this.args.udid !== null) {
636    logger.debug("Not setting iOS and app preferences since we're on a real " +
637                "device");
638    return cb();
639  }
640
641  var settingsCaps = [
642    'locationServicesEnabled',
643    'locationServicesAuthorized',
644    'safariAllowPopups',
645    'safariIgnoreFraudWarning',
646    'safariOpenLinksInBackground'
647  ];
648  var safariSettingsCaps = settingsCaps.slice(2, 5);
649  this.needToSetPrefs = false;
650  this.needToSetSafariPrefs = false;
651  _.each(settingsCaps, function (cap) {
652    if (_.has(this.capabilities, cap)) {
653      this.needToSetPrefs = true;
654      if (_.contains(safariSettingsCaps, cap)) {
655        this.needToSetSafariPrefs = true;
656      }
657    }
658  }.bind(this));
659
660  this.keepAppToRetainPrefs = this.needToSetPrefs;
661
662  cb();
663
664};
665
666IOS.prototype.setPreferences = function (cb) {
667  if (!this.needToSetPrefs) {
668    logger.debug("No iOS / app preferences to set");
669    return cb();
670  } else if (this.args.fullReset) {
671    var msg = "Cannot set preferences because a full-reset was requested";
672    logger.debug(msg);
673    logger.error(msg);
674    return cb(new Error(msg));
675  }
676
677  var setPrefs = function (err) {
678    if (err) return cb(err);
679    try {
680      this.setLocServicesPrefs();
681    } catch (e) {
682      logger.error("Error setting location services preferences, prefs will not work");
683      logger.error(e);
684      logger.error(e.stack);
685    }
686    try {
687      this.setSafariPrefs();
688    } catch (e) {
689      logger.error("Error setting safari preferences, prefs will not work");
690      logger.error(e);
691      logger.error(e.stack);
692    }
693    cb();
694  }.bind(this);
695
696  logger.debug("Setting iOS and app preferences");
697  if (!this.sim.dirsExist() ||
698      !settings.locServicesDirsExist(this.sim) ||
699      (this.needToSetSafariPrefs && !this.sim.safariDirsExist())) {
700    this.instantLaunchAndQuit(this.needToSetSafariPrefs, setPrefs);
701  } else {
702    setPrefs();
703  }
704};
705
706IOS.prototype.instantLaunchAndQuit = function (needSafariDirs, cb) {
707  logger.debug("Sim files for the " + this.iOSSDKVersion + " SDK do not yet exist, launching the sim " +
708      "to populate the applications and preference dirs");
709
710  var condition = function () {
711    var simDirsExist = this.sim.dirsExist();
712    var locServicesExist = settings.locServicesDirsExist(this.sim);
713    var safariDirsExist = this.args.platformVersion < 7.0 ||
714                          (this.sim.safariDirsExist() &&
715                           (this.args.platformVersion < 8.0 ||
716                            this.sim.userSettingsPlistExists())
717                          );
718    var okToGo = simDirsExist && locServicesExist &&
719                 (!needSafariDirs || safariDirsExist);
720    if (!okToGo) {
721      logger.debug("We launched the simulator but the required dirs don't " +
722                   "yet exist. Waiting some more...");
723    }
724    return okToGo;
725  }.bind(this);
726
727  this.prelaunchSimulator(function (err) {
728    if (err) return cb(err);
729    this.makeInstruments(function (err, instruments) {
730      instruments.launchAndKill(condition, function (err) {
731        if (err) return cb(err);
732        this.endSimulator(cb);
733      }.bind(this));
734    }.bind(this));
735  }.bind(this));
736};
737
738IOS.prototype.setLocServicesPrefs = function () {
739  if (typeof this.capabilities.locationServicesEnabled !== "undefined" ||
740      this.capabilities.locationServicesAuthorized) {
741    var locServ = this.capabilities.locationServicesEnabled;
742    locServ = locServ || this.capabilities.locationServicesAuthorized;
743    locServ = locServ ? 1 : 0;
744    logger.debug("Setting location services to " + locServ);
745    settings.updateSettings(this.sim, 'locationServices', {
746         LocationServicesEnabled: locServ,
747        'LocationServicesEnabledIn7.0': locServ,
748        'LocationServicesEnabledIn8.0': locServ
749       }
750    );
751  }
752  if (typeof this.capabilities.locationServicesAuthorized !== "undefined") {
753    if (!this.args.bundleId) {
754      var msg = "Can't set location services for app without bundle ID";
755      logger.error(msg);
756      throw new Error(msg);
757    }
758    var locAuth = !!this.capabilities.locationServicesAuthorized;
759    if (locAuth) {
760      logger.debug("Authorizing location services for app");
761    } else {
762      logger.debug("De-authorizing location services for app");
763    }
764    settings.updateLocationSettings(this.sim, this.args.bundleId, locAuth);
765  }
766};
767
768
769IOS.prototype.setSafariPrefs = function () {
770  var safariSettings = {};
771  var val;
772  if (_.has(this.capabilities, 'safariAllowPopups')) {
773    val = !!this.capabilities.safariAllowPopups;
774    logger.debug("Setting javascript window opening to " + val);
775    safariSettings.WebKitJavaScriptCanOpenWindowsAutomatically = val;
776    safariSettings.JavaScriptCanOpenWindowsAutomatically = val;
777  }
778  if (_.has(this.capabilities, 'safariIgnoreFraudWarning')) {
779    val = !this.capabilities.safariIgnoreFraudWarning;
780    logger.debug("Setting fraudulent website warning to " + val);
781    safariSettings.WarnAboutFraudulentWebsites = val;
782  }
783  if (_.has(this.capabilities, 'safariOpenLinksInBackground')) {
784    val = this.capabilities.safariOpenLinksInBackground ? 1 : 0;
785    logger.debug("Setting opening links in background to " + !!val);
786    safariSettings.OpenLinksInBackground = val;
787  }
788  if (_.size(safariSettings) > 0) {
789    settings.updateSafariSettings(this.sim, safariSettings);
790  }
791};
792
793IOS.prototype.detectUdid = function (cb) {
794  var msg;
795  logger.debug("Auto-detecting iOS udid...");
796  if (this.args.udid !== null && this.args.udid === "auto") {
797    which('idevice_id', function (notFound, cmdPath) {
798
799      var udidetectPath;
800      if (notFound) {
801        udidetectPath = require.resolve('udidetect');
802      } else {
803        udidetectPath = cmdPath + " -l";
804      }
805
806      exec(udidetectPath, { maxBuffer: 524288, timeout: 3000 }, function (err, stdout) {
807        if (err) {
808          msg = "Error detecting udid: " + err.message;
809          logger.error(msg);
810          cb(err);
811        }
812
813        if (stdout && stdout.length > 2) {
814          this.args.udid = stdout.split("\n")[0];
815          logger.debug("Detected udid as " + this.args.udid);
816          cb();
817        } else {
818          msg = "Could not detect udid.";
819          logger.error(msg);
820          cb(new Error(msg));
821        }
822      }.bind(this));
823
824    }.bind(this));
825  } else {
826    logger.debug("Not auto-detecting udid, running on sim");
827    cb();
828  }
829};
830
831IOS.prototype.setBundleIdFromApp = function (cb) {
832  // This method will try to extract the bundleId from the app
833  if (this.args.bundleId) {
834    // We aleady have a bundle Id
835    cb();
836  } else {
837    this.getBundleIdFromApp(function (err, bundleId) {
838      if (err) {
839        logger.error("Could not set the bundleId from app.");
840        return cb(err);
841      }
842      this.args.bundleId = bundleId;
843      cb();
844    }.bind(this));
845  }
846};
847
848IOS.prototype.installToRealDevice = function (cb) {
849  // if user has passed in desiredCaps.autoLaunch = false
850  // meaning they will manage app install / launching
851  if (this.args.autoLaunch === false) {
852    cb();
853  } else {
854    if (this.args.udid) {
855      try {
856        this.realDevice = this.getIDeviceObj();
857      } catch (e) {
858        return cb(e);
859      }
860      this.isAppInstalled(this.args.bundleId, function (err, installed) {
861        if (err || !installed) {
862          logger.debug("App is not installed. Will try to install the app.");
863        } else {
864          logger.debug("App is installed.");
865          if (this.args.fullReset) {
866            logger.debug("fullReset requested. Forcing app install.");
867          } else {
868            logger.debug("fullReset not requested. No need to install.");
869            return cb();
870          }
871        }
872        if (this.args.ipa && this.args.bundleId) {
873          this.installIpa(cb);
874        } else if (this.args.ipa) {
875          var msg = "You specified a UDID and ipa but did not include the bundle " +
876            "id";
877          logger.error(msg);
878          cb(new Error(msg));
879        } else if (this.args.app) {
880          this.installApp(this.args.app, cb);
881        } else {
882          logger.debug("Real device specified but no ipa or app path, assuming bundle ID is " +
883                       "on device");
884          cb();
885        }
886      }.bind(this));
887    } else {
888      logger.debug("No device id or app, not installing to real device.");
889      cb();
890    }
891  }
892};
893
894IOS.prototype.getIDeviceObj = function () {
895  var idiPath = path.resolve(__dirname, "../../../build/",
896                             "libimobiledevice-macosx/ideviceinstaller");
897  logger.debug("Creating iDevice object with udid " + this.args.udid);
898  try {
899    return iDevice(this.args.udid);
900  } catch (e1) {
901    logger.debug("Couldn't find ideviceinstaller, trying built-in at " +
902                idiPath);
903    try {
904      return iDevice(this.args.udid, {cmd: idiPath});
905    } catch (e2) {
906      var msg = "Could not initialize ideviceinstaller; make sure it is " +
907                "installed and works on your system";
908      logger.error(msg);
909      throw new Error(msg);
910    }
911  }
912};
913
914IOS.prototype.installIpa = function (cb) {
915  logger.debug("Installing ipa found at " + this.args.ipa);
916  if (!this.realDevice) {
917    this.realDevice = this.getIDeviceObj();
918  }
919  var d = this.realDevice;
920  async.waterfall([
921    function (cb) { d.isInstalled(this.args.bundleId, cb); }.bind(this),
922    function (installed, cb) {
923      if (installed) {
924        logger.debug("Bundle found on device, removing before reinstalling.");
925        d.remove(this.args.bundleId, cb);
926      } else {
927        logger.debug("Nothing found on device, going ahead and installing.");
928        cb();
929      }
930    }.bind(this),
931    function (cb) { d.installAndWait(this.args.ipa, this.args.bundleId, cb); }.bind(this)
932  ], cb);
933};
934
935IOS.getDeviceStringFromOpts = function (opts) {
936  logger.debug("Getting device string from opts: " + JSON.stringify({
937    forceIphone: opts.forceIphone,
938    forceIpad: opts.forceIpad,
939    xcodeVersion: opts.xcodeVersion,
940    iOSSDKVersion: opts.iOSSDKVersion,
941    deviceName: opts.deviceName,
942    platformVersion: opts.platformVersion
943  }));
944  var xcodeMajorVer = parseInt(opts.xcodeVersion.substr(0,
945        opts.xcodeVersion.indexOf('.')), 10);
946  var isiPhone = opts.forceIphone || opts.forceIpad === null || (opts.forceIpad !== null && !opts.forceIpad);
947  var isTall = isiPhone;
948  var isRetina = opts.xcodeVersion[0] !== '4';
949  var is64bit = false;
950  var deviceName = opts.deviceName;
951  var fixDevice = true;
952  if (deviceName && deviceName[0] === '=') {
953    return deviceName.substring(1);
954  }
955  logger.debug("fixDevice is " + (fixDevice ? "on" : "off"));
956  if (deviceName) {
957    var device = deviceName.toLowerCase();
958    if (device.indexOf("iphone") !== -1) {
959      isiPhone = true;
960    } else if (device.indexOf("ipad") !== -1) {
961      isiPhone = false;
962    }
963    if (deviceName !== opts.platformName) {
964      isTall = isiPhone && (device.indexOf("4-inch") !== -1);
965      isRetina =  (device.indexOf("retina") !== -1);
966      is64bit = (device.indexOf("64-bit") !== -1);
967    }
968  }
969
970  var iosDeviceString = isiPhone ? "iPhone" : "iPad";
971  if (xcodeMajorVer === 4) {
972    if (isiPhone && isRetina) {
973      iosDeviceString += isTall ? " (Retina 4-inch)" : " (Retina 3.5-inch)";
974    } else {
975      iosDeviceString += isRetina ? " (Retina)" : "";
976    }
977  } else if (xcodeMajorVer === 5) {
978    iosDeviceString += isRetina ? " Retina" : "";
979    if (isiPhone) {
980      if (isRetina && isTall) {
981        iosDeviceString += is64bit ? " (4-inch 64-bit)" : " (4-inch)";
982      } else if (deviceName.toLowerCase().indexOf("3.5") !== -1) {
983        iosDeviceString += " (3.5-inch)";
984      }
985    } else {
986      iosDeviceString += is64bit ? " (64-bit)" : "";
987    }
988  } else if (_.contains([6, 7], xcodeMajorVer)) {
989    iosDeviceString = opts.deviceName ||
990      (isiPhone ? "iPhone Simulator" : "iPad Simulator");
991  }
992  var reqVersion = opts.platformVersion || opts.iOSSDKVersion;
993  if (opts.iOSSDKVersion >= 8 && xcodeMajorVer === 7) {
994    iosDeviceString += " (" + reqVersion + ")";
995  } else if (opts.iOSSDKVersion >= 8) {
996    iosDeviceString += " (" + reqVersion + " Simulator)";
997  } else if (opts.iOSSDKVersion >= 7.1) {
998    iosDeviceString += " - Simulator - iOS " + reqVersion;
999  }
1000  if (fixDevice) {
1001    // Some device config are broken in 5.1
1002    var CONFIG_FIX = {
1003      'iPhone - Simulator - iOS 7.1': 'iPhone Retina (4-inch 64-bit) - ' +
1004                                      'Simulator - iOS 7.1',
1005      'iPad - Simulator - iOS 7.1': 'iPad Retina (64-bit) - Simulator - ' +
1006                                    'iOS 7.1',
1007      'iPad Simulator (8.0 Simulator)': 'iPad 2 (8.0 Simulator)',
1008      'iPad Simulator (8.1 Simulator)': 'iPad 2 (8.1 Simulator)',
1009      'iPad Simulator (8.2 Simulator)': 'iPad 2 (8.2 Simulator)',
1010      'iPad Simulator (8.3 Simulator)': 'iPad 2 (8.3 Simulator)',
1011      'iPad Simulator (8.4 Simulator)': 'iPad 2 (8.4 Simulator)',
1012      'iPad Simulator (7.1 Simulator)': 'iPad 2 (7.1 Simulator)',
1013      'iPhone Simulator (8.4 Simulator)': 'iPhone 6 (8.4 Simulator)',
1014      'iPhone Simulator (8.3 Simulator)': 'iPhone 6 (8.3 Simulator)',
1015      'iPhone Simulator (8.2 Simulator)': 'iPhone 6 (8.2 Simulator)',
1016      'iPhone Simulator (8.1 Simulator)': 'iPhone 6 (8.1 Simulator)',
1017      'iPhone Simulator (8.0 Simulator)': 'iPhone 6 (8.0 Simulator)',
1018      'iPhone Simulator (7.1 Simulator)': 'iPhone 5s (7.1 Simulator)'
1019    };
1020    // For xcode major version 7
1021    var CONFIG_FIX__XCODE_7 = {
1022      'iPad Simulator (8.1)': 'iPad 2 (8.1)',
1023      'iPad Simulator (8.2)': 'iPad 2 (8.2)',
1024      'iPad Simulator (8.3)': 'iPad 2 (8.3)',
1025      'iPad Simulator (8.4)': 'iPad 2 (8.4)',
1026      'iPhone Simulator (8.1)': 'iPhone 6 (8.1)',
1027      'iPhone Simulator (8.2)': 'iPhone 6 (8.2)',
1028      'iPhone Simulator (8.3)': 'iPhone 6 (8.3)',
1029      'iPhone Simulator (8.4)': 'iPhone 6 (8.4)',
1030      'iPad Simulator (9.0)': 'iPad 2 (9.0)',
1031      'iPad Simulator (9.1)': 'iPad 2 (9.1)',
1032      // Fixing ambiguous device name by adding '[' at the end so intruments
1033      // correctly starts iPhone 6 [udid] and not the iPhone 6 (9.0) + Apple Watch
1034      // for ios9.0 and above; see #5619
1035      'iPhone Simulator (9.0)': 'iPhone 6 (9.0) [',
1036      'iPhone Simulator (9.1)': 'iPhone 6 (9.1) [',
1037      'iPhone 6 (9.0)': 'iPhone 6 (9.0) [',
1038      'iPhone 6 (9.1)': 'iPhone 6 (9.1) ['
1039    };
1040    var configFix = xcodeMajorVer === 7 ? CONFIG_FIX__XCODE_7 : CONFIG_FIX;
1041    if (configFix[iosDeviceString]) {
1042      var oldDeviceString = iosDeviceString;
1043      iosDeviceString = configFix[iosDeviceString];
1044      logger.debug("Fixing device. Changed from: \"" + oldDeviceString +
1045                   "\" to: \"" + iosDeviceString + "\"");
1046    }
1047  }
1048  logger.debug("Final device string is: '" + iosDeviceString + "'");
1049  return iosDeviceString;
1050};
1051
1052IOS.prototype.getDeviceString = function () {
1053  var opts = _.clone(this.args);
1054  _.extend(opts, {
1055    xcodeVersion: this.xcodeVersion,
1056    iOSSDKVersion: this.iOSSDKVersion
1057  });
1058  return IOS.getDeviceStringFromOpts(opts);
1059};
1060
1061IOS.prototype.setDeviceTypeInInfoPlist = function (cb) {
1062  var plist = path.resolve(this.args.app, "Info.plist");
1063  var dString = this.getDeviceString();
1064  var isiPhone = dString.toLowerCase().indexOf("ipad") === -1;
1065  var deviceTypeCode = isiPhone ? 1 : 2;
1066  parsePlistFile(plist, function (err, obj) {
1067    if (err) {
1068      logger.error("Could not set the device type in Info.plist");
1069      return cb(err, null);
1070    } else {
1071      var newPlist;
1072      obj.UIDeviceFamily = [deviceTypeCode];
1073      if (binaryPlist) {
1074        newPlist = bplistCreate(obj);
1075      } else {
1076        newPlist = xmlplist.build(obj);
1077      }
1078      fs.writeFile(plist, newPlist, function (err) {
1079        if (err) {
1080          logger.error("Could not save new Info.plist");
1081          cb(err);
1082        } else {
1083          logger.debug("Wrote new app Info.plist with device type");
1084          cb();
1085        }
1086      }.bind(this));
1087    }
1088  }.bind(this));
1089};
1090
1091IOS.prototype.getBundleIdFromApp = function (cb) {
1092  logger.debug("Getting bundle ID from app");
1093  var plist = path.resolve(this.args.app, "Info.plist");
1094  parsePlistFile(plist, function (err, obj) {
1095    if (err) {
1096      logger.error("Could not get the bundleId from app.");
1097      cb(err, null);
1098    } else {
1099      cb(null, obj.CFBundleIdentifier);
1100    }
1101  }.bind(this));
1102};
1103
1104IOS.getSimForDeviceString = function (dString, availDevices) {
1105  var matchedDevice = null;
1106  var matchedUdid = null;
1107  _.each(availDevices, function (device) {
1108    if (device.indexOf(dString) !== -1) {
1109      matchedDevice = device;
1110      try {
1111        matchedUdid = /.+\[([^\]]+)\]/.exec(device)[1];
1112      } catch (e) {
1113        matchedUdid = null;
1114      }
1115    }
1116  });
1117  return [matchedDevice, matchedUdid];
1118};
1119
1120IOS.prototype.checkSimAvailable = function (cb) {
1121  if (this.args.udid) {
1122    logger.debug("Not checking whether simulator is available since we're on " +
1123                 "a real device");
1124    return cb();
1125  }
1126
1127  if (this.iOSSDKVersion < 7.1) {
1128    logger.debug("Instruments v < 7.1, not checking device string support");
1129    return cb();
1130  }
1131
1132  logger.debug("Checking whether instruments supports our device string");
1133  Instruments.getAvailableDevicesWithRetry(3, function (err, availDevices) {
1134    if (err) return cb(err);
1135    var noDevicesError = function () {
1136      var msg = "Could not find a device to launch. You requested '" +
1137                dString + "', but the available devices were: " +
1138                JSON.stringify(availDevices);
1139      logger.error(msg);
1140      cb(new Error(msg));
1141    };
1142    var dString = this.getDeviceString();
1143    if (this.iOSSDKVersion >= 8) {
1144      var sim = IOS.getSimForDeviceString(dString, availDevices);
1145      if (sim[0] === null || sim[1] === null) {
1146        return noDevicesError();
1147      }
1148      this.iOSSimUdid = sim[1];
1149      logger.debug("iOS sim UDID is " + this.iOSSimUdid);
1150      return cb();
1151    } else if (!_.contains(availDevices, dString)) {
1152      return noDevicesError();
1153    }
1154    cb();
1155  }.bind(this));
1156};
1157
1158IOS.prototype.setDeviceInfo = function (cb) {
1159  this.shouldPrelaunchSimulator = false;
1160  if (this.args.udid) {
1161    logger.debug("Not setting device type since we're on a real device");
1162    return cb();
1163  }
1164
1165  if (!this.args.app && this.args.bundleId) {
1166    logger.debug("Not setting device type since we're using bundle ID and " +
1167                "assuming app is already installed");
1168    return cb();
1169  }
1170
1171  if (!this.args.deviceName &&
1172      this.args.forceIphone === null &&
1173      this.args.forceIpad === null) {
1174    logger.debug("No device specified, current device in the iOS " +
1175                 "simulator will be used.");
1176    return cb();
1177  }
1178
1179  if (this.args.defaultDevice || this.iOSSDKVersion >= 7.1) {
1180    if (this.iOSSDKVersion >= 7.1) {
1181      logger.debug("We're on iOS7.1+ so forcing defaultDevice on");
1182    } else {
1183      logger.debug("User specified default device, letting instruments launch it");
1184    }
1185  } else {
1186    this.shouldPrelaunchSimulator = true;
1187  }
1188  this.setDeviceTypeInInfoPlist(cb);
1189};
1190
1191IOS.prototype.createSimulator = function (cb) {
1192  this.sim = new Simulator({
1193    platformVer: this.args.platformVersion,
1194    sdkVer: this.iOSSDKVersion,
1195    udid: this.iOSSimUdid
1196  });
1197  cb();
1198};
1199
1200IOS.prototype.moveBuiltInApp = function (cb) {
1201  if (this.appString().toLowerCase() === "settings") {
1202    logger.debug("Trying to use settings app, version " +
1203                 this.args.platformVersion);
1204    this.sim.preparePreferencesApp(this.args.tmpDir, function (err, attemptedApp, origApp) {
1205      if (err) {
1206        logger.error("Could not prepare settings app: " + err);
1207        return cb(err);
1208      }
1209      logger.debug("Using settings app at " + attemptedApp);
1210      this.args.app = attemptedApp;
1211      this.args.origAppPath = origApp;
1212      cb();
1213    }.bind(this));
1214  } else {
1215    cb();
1216  }
1217};
1218
1219IOS.prototype.prelaunchSimulator = function (cb) {
1220  var msg;
1221  if (!this.shouldPrelaunchSimulator) {
1222    logger.debug("Not pre-launching simulator");
1223    return cb();
1224  }
1225
1226  xcode.getPath(function (err, xcodePath) {
1227    if (err) {
1228      return cb(new Error('Could not find xcode folder. Needed to start simulator. ' + err.message));
1229    }
1230
1231    logger.debug("Pre-launching simulator");
1232    var iosSimPath = path.resolve(xcodePath,
1233        "Platforms/iPhoneSimulator.platform/Developer/Applications" +
1234        "/iPhone Simulator.app/Contents/MacOS/iPhone Simulator");
1235    if (!fs.existsSync(iosSimPath)) {
1236      msg = "Could not find ios simulator binary at " + iosSimPath;
1237      logger.error(msg);
1238      return cb(new Error(msg));
1239    }
1240    this.endSimulator(function (err) {
1241      if (err) return cb(err);
1242      logger.debug("Launching device: " + this.getDeviceString());
1243      var iosSimArgs = ["-SimulateDevice", this.getDeviceString()];
1244      this.iosSimProcess = spawn(iosSimPath, iosSimArgs);
1245      var waitForSimulatorLogs = function (countdown) {
1246        if (countdown <= 0 ||
1247          (this.logs.syslog && (this.logs.syslog.getAllLogs().length > 0 ||
1248          (this.logs.crashlog && this.logs.crashlog.getAllLogs().length > 0)))) {
1249          logger.debug(countdown > 0 ? "Simulator is now ready." :
1250                       "Waited 10 seconds for simulator to start.");
1251          cb();
1252        } else {
1253          setTimeout(function () {
1254            waitForSimulatorLogs(countdown - 1);
1255          }, 1000);
1256        }
1257      }.bind(this);
1258      waitForSimulatorLogs(10);
1259    }.bind(this));
1260
1261  }.bind(this));
1262};
1263
1264IOS.prototype.parseLocalizableStrings = function (/* language, stringFile, cb */) {
1265  var args = new Args(arguments);
1266  var cb = args.callback;
1267  if (this.args.app === null) {
1268    logger.debug("Localizable.strings is not currently supported when using real devices.");
1269    return cb();
1270  }
1271  var language = args.all[0] || this.args.language
1272    , stringFile = args.all[1] || "Localizable.strings"
1273    , strings = null;
1274
1275  if (language) {
1276    strings = path.resolve(this.args.app, language + ".lproj", stringFile);
1277  }
1278  if (!fs.existsSync(strings)) {
1279    if (language) {
1280      logger.debug("No strings file '" + stringFile + "' for language '" + language + "', getting default strings");
1281    }
1282    strings = path.resolve(this.args.app, stringFile);
1283  }
1284  if (!fs.existsSync(strings)) {
1285    strings = path.resolve(this.args.app, this.args.localizableStringsDir, stringFile);
1286  }
1287
1288  parsePlistFile(strings, function (err, obj) {
1289    if (err) {
1290      logger.warn("Could not parse app " + stringFile +" assuming it " +
1291                  "doesn't exist");
1292    } else {
1293      logger.debug("Parsed app " + stringFile);
1294      this.localizableStrings = obj;
1295    }
1296    cb();
1297  }.bind(this));
1298};
1299
1300
1301IOS.prototype.deleteSim = function (cb) {
1302  this.sim.deleteSim(cb);
1303};
1304
1305IOS.prototype.clearAppData = function (cb) {
1306  if (!this.keepAppToRetainPrefs && this.args.app && this.args.bundleId) {
1307    this.sim.cleanCustomApp(path.basename(this.args.app), this.args.bundleId);
1308  }
1309  cb();
1310};
1311
1312IOS.prototype.cleanupSimState = function (cb) {
1313  if (this.realDevice && this.args.bundleId && this.args.fullReset) {
1314    logger.debug("fullReset requested. Will try to uninstall the app.");
1315    var bundleId = this.args.bundleId;
1316    this.realDevice.remove(bundleId, function (err) {
1317      if (err) {
1318        this.removeApp(bundleId, function (err) {
1319          if (err) {
1320            logger.error("Could not remove " + bundleId + " from device");
1321            cb(err);
1322          } else {
1323            logger.debug("Removed " + bundleId);
1324            cb();
1325          }
1326        }.bind(this));
1327      } else {
1328        logger.debug("Removed " + bundleId);
1329        cb();
1330      }
1331    }.bind(this));
1332  } else if (!this.args.udid) {
1333    this.sim.cleanSim(this.args.keepKeyChains, this.args.tmpDir, function (err) {
1334      if (err) {
1335        logger.error("Could not reset simulator. Leaving as is. Error: " + err.message);
1336      }
1337      this.clearAppData(cb);
1338    }.bind(this));
1339  } else {
1340    logger.debug("On a real device; cannot clean device state");
1341    cb();
1342  }
1343};
1344
1345IOS.prototype.runSimReset = function (cb) {
1346  if (this.args.reset || this.args.fullReset) {
1347    logger.debug("Running ios sim reset flow");
1348    // The simulator process must be ended before we delete applications.
1349    async.series([
1350      this.endSimulator.bind(this),
1351      function (cb) {
1352        if (this.args.reset) {
1353          this.cleanupSimState(cb);
1354        } else {
1355          cb();
1356        }
1357      }.bind(this),
1358      function (cb) {
1359        if (this.args.fullReset && !this.args.udid) {
1360          this.deleteSim(cb);
1361        } else {
1362          cb();
1363        }
1364      }.bind(this)
1365    ], cb);
1366  } else {
1367    logger.debug("Reset not set, not ending sim or cleaning up app state");
1368    cb();
1369  }
1370};
1371
1372IOS.prototype.isolateSimDevice = function (cb) {
1373  if (!this.args.udid && this.args.isolateSimDevice &&
1374      this.iOSSDKVersion >= 8) {
1375    this.sim.deleteOtherSims(cb);
1376  } else {
1377    cb();
1378  }
1379};
1380
1381IOS.prototype.postCleanup = function (cb) {
1382  this.curCoords = null;
1383  this.curOrientation = null;
1384
1385  if (!_.isEmpty(this.logs)) {
1386    this.logs.syslog.stopCapture();
1387    this.logs = {};
1388  }
1389
1390  if (this.remote) {
1391    this.stopRemote();
1392  }
1393
1394  this.runSimReset(function () {
1395    // ignore any errors during reset and continue shutting down
1396    this.isShuttingDown = false;
1397    cb();
1398  }.bind(this));
1399
1400
1401};
1402
1403IOS.prototype.endSimulator = function (cb) {
1404  logger.debug("Killing the simulator process");
1405  if (this.iosSimProcess) {
1406    this.iosSimProcess.kill("SIGHUP");
1407    this.iosSimProcess = null;
1408  } else {
1409    Instruments.killAllSim();
1410  }
1411  this.endSimulatorDaemons(cb);
1412};
1413
1414IOS.prototype.endSimulatorDaemons = function (cb) {
1415  logger.debug("Killing any other simulator daemons");
1416  var stopCmd = 'launchctl list | grep com.apple.iphonesimulator | cut -f 3 | xargs -n 1 launchctl stop';
1417  exec(stopCmd, { maxBuffer: 524288 }, function () {
1418    var removeCmd = 'launchctl list | grep com.apple.iphonesimulator | cut -f 3 | xargs -n 1 launchctl remove';
1419    exec(removeCmd, { maxBuffer: 524288 }, function () {
1420      cb();
1421    });
1422  });
1423};
1424
1425IOS.prototype.stop = function (cb) {
1426  logger.debug("Stopping ios");
1427  if (this.instruments === null) {
1428    logger.debug("Trying to stop instruments but it already exited");
1429    this.postCleanup(cb);
1430  } else {
1431    this.commandProxy.shutdown(function (err) {
1432      if (err) logger.warn("Got warning when trying to close command proxy:", err);
1433      this.instruments.shutdown(function (code) {
1434        this.shutdown(code, cb);
1435      }.bind(this));
1436    }.bind(this));
1437  }
1438};
1439
1440IOS.prototype.shutdown = function (code, cb) {
1441  this.commandProxy = null;
1442  this.instruments = null;
1443  this.postCleanup(cb);
1444};
1445
1446IOS.prototype.resetTimeout = deviceCommon.resetTimeout;
1447IOS.prototype.waitForCondition = deviceCommon.waitForCondition;
1448IOS.prototype.implicitWaitForCondition = deviceCommon.implicitWaitForCondition;
1449IOS.prototype.proxy = deviceCommon.proxy;
1450IOS.prototype.proxyWithMinTime = deviceCommon.proxyWithMinTime;
1451IOS.prototype.respond = deviceCommon.respond;
1452IOS.prototype.getSettings = deviceCommon.getSettings;
1453IOS.prototype.updateSettings = deviceCommon.updateSettings;
1454
1455IOS.prototype.initQueue = function () {
1456
1457  this.queue = async.queue(function (command, cb) {
1458    if (!this.commandProxy) return cb();
1459    async.series([
1460      function (cb) {
1461        async.whilst(
1462          function () { return this.selectingNewPage && this.curContext; }.bind(this),
1463          function (cb) {
1464            logger.debug("We're in the middle of selecting a new page, " +
1465                        "waiting to run next command until done");
1466            setTimeout(cb, 100);
1467          },
1468          cb
1469        );
1470      }.bind(this),
1471      function (cb) {
1472        var matched = false;
1473        var matches = ["au.alertIsPresent", "au.getAlertText", "au.acceptAlert",
1474                       "au.dismissAlert", "au.setAlertText",
1475                       "au.waitForAlertToClose"];
1476        _.each(matches, function (match) {
1477          if (command.indexOf(match) === 0) {
1478            matched = true;
1479          }
1480        });
1481        async.whilst(
1482          function () { return !matched && this.curContext && this.processingRemoteCmd; }.bind(this),
1483          function (cb) {
1484            logger.debug("We're in the middle of processing a remote debugger " +
1485                        "command, waiting to run next command until done");
1486            setTimeout(cb, 100);
1487          },
1488          cb
1489        );
1490      }.bind(this)
1491    ], function (err) {
1492      if (err) return cb(err);
1493      this.cbForCurrentCmd = cb;
1494      if (this.commandProxy) {
1495        this.commandProxy.sendCommand(command, function (response) {
1496          this.cbForCurrentCmd = null;
1497          if (typeof cb === 'function') {
1498            this.respond(response, cb);
1499          }
1500        }.bind(this));
1501      }
1502    }.bind(this));
1503  }.bind(this), 1);
1504};
1505
1506IOS.prototype.push = function (elem) {
1507  this.queue.push(elem[0], elem[1]);
1508};
1509
1510IOS.prototype.isAppInstalled = function (bundleId, cb) {
1511  if (this.args.udid) {
1512    this.realDevice.isInstalled(bundleId, cb);
1513  } else {
1514    cb(new Error("You can not call isInstalled for the iOS simulator!"));
1515  }
1516};
1517
1518IOS.prototype.removeApp = function (bundleId, cb) {
1519  if (this.args.udid) {
1520    this.realDevice.remove(bundleId, cb);
1521  } else {
1522    cb(new Error("You can not call removeApp for the iOS simulator!"));
1523  }
1524};
1525
1526IOS.prototype.installApp = function (unzippedAppPath, cb) {
1527  if (this.args.udid) {
1528    this.realDevice.install(unzippedAppPath, cb);
1529  } else {
1530    cb(new Error("You can not call installApp for the iOS simulator!"));
1531  }
1532};
1533
1534IOS.prototype.unpackApp = function (req, cb) {
1535  deviceCommon.unpackApp(req, '.app', cb);
1536};
1537
1538IOS.prototype.startLogCapture = function (cb) {
1539  if (!_.isEmpty(this.logs)) {
1540    cb(new Error("Trying to start iOS log capture but it's already started!"));
1541    return;
1542  }
1543  this.logs.crashlog = new iOSCrashLog();
1544  this.logs.syslog = new iOSLog({
1545    udid: this.args.udid
1546  , simUdid: this.iOSSimUdid
1547  , showLogs: this.args.showSimulatorLog || this.args.showIOSLog
1548  });
1549  this.logs.syslog.startCapture(function (err) {
1550    if (err) {
1551      logger.warn("Could not capture logs from device. Continuing without capturing logs.");
1552      return cb();
1553    }
1554    this.logs.crashlog.startCapture(cb);
1555  }.bind(this));
1556};
1557
1558IOS.prototype.initAutoWebview = function (cb) {
1559  if (this.args.autoWebview) {
1560    logger.debug('Setting auto webview');
1561    this.navToInitialWebview(cb);
1562  } else {
1563    cb();
1564  }
1565};
1566
1567IOS.prototype.getContextsAndViews = function (cb) {
1568  this.listWebFrames(function (err, webviews) {
1569    if (err) return cb(err);
1570    var ctxs = [{id: this.NATIVE_WIN}];
1571    this.contexts = [this.NATIVE_WIN];
1572    _.each(webviews, function (view) {
1573      ctxs.push({id: this.WEBVIEW_BASE + view.id, view: view});
1574      this.contexts.push(view.id.toString());
1575    }.bind(this));
1576    cb(null, ctxs);
1577  }.bind(this));
1578};
1579
1580IOS.prototype.getLatestWebviewContextForTitle = function (titleRegex, cb) {
1581  this.getContextsAndViews(function (err, contexts) {
1582    if (err) return cb(err);
1583    var matchingCtx;
1584    _(contexts).each(function (ctx) {
1585      if (ctx.view && (ctx.view.title || "").match(titleRegex)) {
1586        if (ctx.view.url === "about:blank") {
1587          // in the cases of Xcode < 5 (i.e., iOS SDK Version less than 7)
1588          // iOS 7.1, iOS 9.0 & iOS 9.1 in a webview (not in Safari)
1589          // we can have the url be `about:blank`
1590          if (parseFloat(this.iOSSDKVersion) < 7 || parseFloat(this.iOSSDKVersion) >= 9 ||
1591              (this.args.platformVersion === '7.1' && this.args.app && this.args.app.toLowerCase() !== 'safari')) {
1592            matchingCtx = ctx;
1593          }
1594        } else {
1595          matchingCtx = ctx;
1596        }
1597      }
1598    }.bind(this));
1599    cb(null, matchingCtx ? matchingCtx.id : undefined);
1600  }.bind(this));
1601};
1602
1603// Right now we don't necessarily wait for webview
1604// and frame to load, which leads to race conditions and flakiness
1605// , let see if we can transition to something better
1606IOS.prototype.useNewSafari = function () {
1607  return parseFloat(this.iOSSDKVersion) >= 8.1 &&
1608         parseFloat(this.args.platformVersion) >= 8.1 &&
1609         !this.args.udid &&
1610         this.capabilities.safari;
1611};
1612
1613IOS.prototype.navToInitialWebview = function (cb) {
1614  var timeout = 0;
1615  if (this.args.udid) timeout = parseInt(this.iOSSDKVersion, 10) >= 8 ? 4000 : 6000;
1616  if (timeout > 0) logger.debug('Waiting for ' + timeout + ' ms before navigating to view.');
1617  setTimeout(function () {
1618    if (this.useNewSafari()) {
1619      return this.typeAndNavToUrl(cb);
1620    } else if (parseInt(this.iOSSDKVersion, 10) >= 7 && !this.args.udid && this.capabilities.safari) {
1621      this.navToViewThroughFavorites(cb);
1622    } else {
1623      this.navToViewWithTitle(/.*/, cb);
1624    }
1625  }.bind(this), timeout);
1626};
1627
1628IOS.prototype.typeAndNavToUrl = function (cb) {
1629  var initialUrl = this.args.safariInitialUrl || 'http://127.0.0.1:' + this.args.port + '/welcome';
1630  var oldImpWait = this.implicitWaitMs;
1631  this.implicitWaitMs = 7000;
1632  function noArgsCb(cb) { return function (err) { cb(err); }; }
1633  async.waterfall([
1634    this.findElement.bind(this, 'name', 'URL'),
1635    function (res, cb) {
1636      this.implicitWaitMs = oldImpWait;
1637      this.nativeTap(res.value.ELEMENT, noArgsCb(cb));
1638    }.bind(this),
1639    this.findElements.bind(this, 'name', 'Address'),
1640    function (res, cb) {
1641      var addressEl = res.value[res.value.length -1].ELEMENT;
1642      this.setValueImmediate(addressEl, initialUrl, noArgsCb(cb));
1643    }.bind(this),
1644    this.findElement.bind(this, 'name', 'Go'),
1645    function (res, cb) {
1646      this.nativeTap(res.value.ELEMENT, noArgsCb(cb));
1647    }.bind(this)
1648  ], function () {
1649    this.navToViewWithTitle(/.*/i, function (err) {
1650      if (err) return cb(err);
1651      // Waits for page to finish loading.
1652      this.remote.pageUnload(cb);
1653    }.bind(this));
1654  }.bind(this));
1655};
1656
1657IOS.prototype.navToViewThroughFavorites = function (cb) {
1658  logger.debug("We're on iOS7+ simulator: clicking apple button to get into " +
1659              "a webview");
1660  var oldImpWait = this.implicitWaitMs;
1661  this.implicitWaitMs = 7000; // wait 7s for apple button to exist
1662  this.findElement('xpath', '//UIAScrollView[1]/UIAButton[1]', function (err, res) {
1663    this.implicitWaitMs = oldImpWait;
1664    if (err || res.status !== status.codes.Success.code) {
1665      var msg = "Could not find button to click to get into webview. " +
1666                "Proceeding on the assumption we have a working one.";
1667      logger.error(msg);
1668      return this.navToViewWithTitle(/.*/i, cb);
1669    }
1670    this.nativeTap(res.value.ELEMENT, function (err, res) {
1671      if (err || res.status !== status.codes.Success.code) {
1672        var msg = "Could not click button to get into webview. " +
1673                  "Proceeding on the assumption we have a working one.";
1674        logger.error(msg);
1675      }
1676      this.navToViewWithTitle(/apple/i, cb);
1677    }.bind(this));
1678  }.bind(this));
1679};
1680
1681IOS.prototype.navToViewWithTitle = function (titleRegex, cb) {
1682  logger.debug("Navigating to most recently opened webview");
1683  var start = Date.now();
1684  var spinTime = 500;
1685  var spinHandles = function () {
1686    this.getLatestWebviewContextForTitle(titleRegex, function (err, res) {
1687      if (err) {
1688        cb(new Error("Could not navigate to webview! Err: " + err));
1689      } else if (!res) {
1690        if ((Date.now() - start) < 90000) {
1691          logger.warn("Could not find any webviews yet, refreshing/retrying");
1692          if (this.args.udid || !this.capabilities.safari) {
1693            return setTimeout(spinHandles, spinTime);
1694          }
1695          this.findUIElementOrElements('accessibility id', 'ReloadButton',
1696              '', false, function (err, res) {
1697            if (err || !res || !res.value || !res.value.ELEMENT) {
1698              logger.warn("Could not find reload button, continuing");
1699              setTimeout(spinHandles, spinTime);
1700            } else {
1701              this.nativeTap(res.value.ELEMENT, function (err, res) {
1702                if (err || !res) {
1703                  logger.warn("Could not click reload button, continuing");
1704                }
1705                setTimeout(spinHandles, spinTime);
1706              }.bind(this));
1707            }
1708          }.bind(this));
1709        } else {
1710          cb(new Error("Could not navigate to webview; there aren't any!"));
1711        }
1712      } else {
1713        var latestWindow = res;
1714        logger.debug("Picking webview " + latestWindow);
1715        this.setContext(latestWindow, function (err) {
1716          if (err) return cb(err);
1717          this.remote.cancelPageLoad();
1718          cb();
1719        }.bind(this), true);
1720      }
1721    }.bind(this));
1722  }.bind(this);
1723  spinHandles();
1724};
1725
1726_.extend(IOS.prototype, iOSHybrid);
1727_.extend(IOS.prototype, iOSController);
1728
1729module.exports = IOS;
1730
Full Screen