import { BaseDriver, DeviceSettings, errors } from 'appium-base-driver';
import * as utils from './utils';
import logger from './logger';
import path from 'path';
import _ from 'lodash';
import B from 'bluebird';
import { fs } from 'appium-support';
import { getSimulator, installSSLCert, uninstallSSLCert } from 'appium-ios-simulator';
import { prepareBootstrap, UIAutoClient } from './uiauto/uiauto';
import { Instruments, instrumentsUtils } from './instruments';
import { retry, waitForCondition } from 'asyncbox';
import commands from './commands/index';
import { desiredCapConstraints, desiredCapValidation } from './desired-caps';
import _iDevice from 'node-idevice';
import { SAFARI_BUNDLE } from './commands/safari';
import { install, needsInstall, SAFARI_LAUNCHER_BUNDLE } from './safari-launcher';
import { setLocaleAndPreferences } from './settings';
import { runSimulatorReset, isolateSimulatorDevice, checkSimulatorAvailable,
moveBuiltInApp, getAdjustedDeviceName, endSimulator, runRealDeviceReset } from './device';
import { IWDP } from './iwdp';
// promisify _iDevice
let iDevice = function (...args) {
let device = _iDevice(...args);
let promisified = {};
for (let m of ['install', 'installAndWait', 'remove', 'isInstalled']) {
promisified[m] = B.promisify(device[m].bind(device));
}
return promisified;
};
const defaultServerCaps = {
webStorageEnabled: false,
locationContextEnabled: false,
browserName: '',
platform: 'MAC',
javascriptEnabled: true,
databaseEnabled: false,
takesScreenshot: true,
networkConnectionEnabled: false,
};
const LOG_LOCATIONS = [
path.resolve('/', 'Library', 'Caches', 'com.apple.dt.instruments'),
];
if (process.env.HOME) {
LOG_LOCATIONS.push(path.resolve(process.env.HOME, 'Library', 'Logs', 'CoreSimulator'));
}
class IosDriver extends BaseDriver {
resetIos () {
this.appExt = '.app';
this.xcodeVersion = null;
this.iosSdkVersion = null;
this.logs = {};
this.instruments = null;
this.uiAutoClient = null;
this.onInstrumentsDie = function onInstrumentsDie () {};
this.stopping = false;
this.cbForCurrentCmd = null;
this.remote = null;
this.curContext = null;
this.curWebFrames = [];
this.selectingNewPage = false;
this.windowHandleCache = [];
this.webElementIds = [];
this.implicitWaitMs = 0;
this.asynclibWaitMs = 0;
this.pageLoadMs = 6000;
this.asynclibResponseCb = null;
this.returnedFromExecuteAtom = {};
this.executedAtomsCounter = 0;
this.curCoords = null;
this.curWebCoords = null;
this.landscapeWebCoordsOffset = 0;
this.keepAppToRetainPrefs = false;
this.ready = false;
this.asyncWaitMs = 0;
this.settings = new DeviceSettings({}, _.noop);
this.locatorStrategies = [
'xpath',
'id',
'class name',
'-ios uiautomation',
'accessibility id'
];
this.webLocatorStrategies = [
'link text',
'css selector',
'tag name',
'partial link text'
];
}
constructor (opts, shouldValidateCaps) {
super(opts, shouldValidateCaps);
this.desiredCapConstraints = desiredCapConstraints;
this.resetIos();
this.getDevicePixelRatio = _.memoize(this.getDevicePixelRatio);
}
validateLocatorStrategy (strategy) {
super.validateLocatorStrategy(strategy, this.isWebContext());
}
async start () {
if (this.isRealDevice()) {
await this.startRealDevice();
} else {
await this.startSimulator();
}
this.ready = true;
}
async createSession (...args) {
let [sessionId, caps] = await super.createSession(...args);
// appium-ios-driver uses Instruments to automate the device
// but Xcode 8 does not have Instruments, so short circuit
this.xcodeVersion = await utils.getAndCheckXcodeVersion(this.opts);
logger.debug(`Xcode version set to ${this.xcodeVersion.versionString}`);
if (this.xcodeVersion.major >= 8) {
let msg = `Appium's IosDriver does not support Xcode version ${this.xcodeVersion.versionString}. ` +
'Apple has deprecated UIAutomation. Use the "XCUITest" automationName capability instead.';
logger.errorAndThrow(new errors.SessionNotCreatedError(msg));
}
// merge server capabilities + desired capabilities
this.caps = Object.assign({}, defaultServerCaps, this.caps);
this.caps.desired = caps;
await utils.detectUdid(this.opts);
await utils.prepareIosOpts(this.opts);
this.realDevice = null;
this.useRobot = this.opts.useRobot;
this.safari = this.opts.safari;
this.opts.curOrientation = this.opts.initialOrientation;
this.sock = path.resolve(this.opts.tmpDir || '/tmp', 'instruments_sock');
try {
await this.configureApp();
} catch (err) {
logger.error(`Bad app: '${this.opts.app}'. App paths need to ` +
`be absolute, or relative to the appium server ` +
`install dir, or a URL to compressed file, or a ` +
`special app name.`);
throw err;
}
await this.start();
// TODO: this should be in BaseDriver.postCreateSession
this.startNewCommandTimeout('createSession');
return [sessionId, this.caps];
}
async stop () {
this.ready = false;
if (this.uiAutoClient) {
await this.uiAutoClient.shutdown();
}
if (this.instruments) {
try {
await this.instruments.shutdown();
} catch (err) {
logger.error(`Instruments didn't shut down. ${err}`);
}
}
if (this.caps && this.caps.customSSLCert && !this.isRealDevice()) {
logger.debug(`Uninstalling ssl certificate for udid '${this.sim.udid}'`);
await uninstallSSLCert(this.caps.customSSLCert, this.sim.udid);
}
if (this.opts.enableAsyncExecuteFromHttps && !this.isRealDevice()) {
await this.stopHttpsAsyncServer();
}
this.uiAutoClient = null;
this.instruments = null;
this.realDevice = null;
// postcleanup
this.curCoords = null;
this.opts.curOrientation = null;
if (!_.isEmpty(this.logs)) {
await this.logs.syslog.stopCapture();
this.logs = {};
}
if (this.remote) {
await this.stopRemote();
}
await this.stopIWDP();
}
async deleteSession () {
logger.debug('Deleting ios session');
await this.stop();
if (this.opts.clearSystemFiles) {
await utils.clearLogs(LOG_LOCATIONS);
} else {
logger.debug('Not clearing log files. Use `clearSystemFiles` capability to turn on.');
}
if (this.isRealDevice()) {
await runRealDeviceReset(this.realDevice, this.opts);
} else {
await runSimulatorReset(this.sim, this.opts, this.keepAppToRetainPrefs);
}
await super.deleteSession();
}
async getSession () {
let caps = await super.getSession();
const viewportRect = await this.getViewportRect();
const pixelRatio = await this.getDevicePixelRatio();
const statBarHeight = await this.getStatusBarHeight();
caps.viewportRect = viewportRect;
caps.pixelRatio = pixelRatio;
caps.statBarHeight = statBarHeight;
return caps;
}
async executeCommand (cmd, ...args) {
logger.debug(`Executing iOS command '${cmd}'`);
if (cmd === 'receiveAsyncResponse') {
return await this.receiveAsyncResponse(...args);
} else if (this.ready || _.includes(['launchApp'], cmd)) {
return await super.executeCommand(cmd, ...args);
}
throw new errors.NoSuchDriverError(`Driver is not ready, cannot execute ${cmd}.`);
}
// TODO: reformat this.helpers + configureApp
async configureApp () {
try {
// if the app name is a bundleId assign it to the bundleId property
if (!this.opts.bundleId && utils.appIsPackageOrBundle(this.opts.app)) {
this.opts.bundleId = this.opts.app;
}
if (this.opts.app && this.opts.app.toLowerCase() === 'settings') {
if (parseFloat(this.opts.platformVersion) >= 8) {
logger.debug('We are on iOS8+ so not copying preferences app');
this.opts.bundleId = 'com.apple.Preferences';
this.opts.app = null;
}
} else if (this.opts.app && this.opts.app.toLowerCase() === 'calendar') {
if (parseFloat(this.opts.platformVersion) >= 8) {
logger.debug('We are on iOS8+ so not copying calendar app');
this.opts.bundleId = 'com.apple.mobilecal';
this.opts.app = null;
}
} else if (this.isSafari()) {
if (!this.isRealDevice()) {
if (parseFloat(this.opts.platformVersion) >= 8) {
logger.debug('We are on iOS8+ so not copying Safari app');
this.opts.bundleId = SAFARI_BUNDLE;
this.opts.app = null;
}
} else {
// on real device, need to check if safari launcher exists
// first check if it is already on the device
if (!await this.realDevice.isInstalled(this.opts.bundleId)) {
// it's not on the device, so check if we need to build
if (await needsInstall()) {
logger.debug('SafariLauncher not found, building...');
await install();
}
this.opts.bundleId = SAFARI_LAUNCHER_BUNDLE;
}
}
} else if (this.opts.bundleId &&
utils.appIsPackageOrBundle(this.opts.bundleId) &&
(this.opts.app === '' || utils.appIsPackageOrBundle(this.opts.app))) {
// we have a bundle ID, but no app, or app is also a bundle
logger.debug('App is an iOS bundle, will attempt to run as pre-existing');
} else {
this.opts.app = await this.helpers.configureApp(this.opts.app, '.app');
}
} catch (err) {
logger.error(err);
throw new Error(
`Bad app: ${this.opts.app}. App paths need to be absolute, or relative to the appium ` +
'server install dir, or a URL to compressed file, or a special app name.');
}
}
async startSimulator () {
await utils.removeInstrumentsSocket(this.sock);
if (!this.xcodeVersion) {
logger.debug('Setting Xcode version');
this.xcodeVersion = await utils.getAndCheckXcodeVersion(this.opts);
logger.debug(`Xcode version set to ${this.xcodeVersion.versionString}`);
}
logger.debug('Setting iOS SDK Version');
this.iosSdkVersion = await utils.getAndCheckIosSdkVersion();
logger.debug(`iOS SDK Version set to ${this.iosSdkVersion}`);
let timeout = _.isObject(this.opts.launchTimeout) ? this.opts.launchTimeout.global : this.opts.launchTimeout;
let availableDevices = await retry(3, instrumentsUtils.getAvailableDevices, timeout);
let iosSimUdid = await checkSimulatorAvailable(this.opts, this.iosSdkVersion, availableDevices);
this.sim = await getSimulator(iosSimUdid, this.xcodeVersion.versionString);
await moveBuiltInApp(this.sim);
this.opts.localizableStrings = await utils.parseLocalizableStrings(this.opts);
await utils.setBundleIdFromApp(this.opts);
await this.createInstruments();
{
// previously setDeviceInfo()
this.shouldPrelaunchSimulator = utils.shouldPrelaunchSimulator(this.opts, this.iosSdkVersion);
let dString = await getAdjustedDeviceName(this.opts);
if (this.caps.app) {
await utils.setDeviceTypeInInfoPlist(this.opts.app, dString);
}
}
await runSimulatorReset(this.sim, this.opts, this.keepAppToRetainPrefs);
if (this.caps.customSSLCert && !this.isRealDevice()) {
await installSSLCert(this.caps.customSSLCert, this.sim.udid);
}
if (this.opts.enableAsyncExecuteFromHttps && !this.isRealDevice()) {
// await this.sim.shutdown();
await this.startHttpsAsyncServer();
}
await isolateSimulatorDevice(this.sim, this.opts);
this.localConfig = await setLocaleAndPreferences(this.sim, this.opts, this.isSafari(), endSimulator);
await this.setUpLogCapture();
await this.prelaunchSimulator();
await this.startInstruments();
await this.onInstrumentsLaunch();
await this.configureBootstrap();
await this.setBundleId();
await this.setInitialOrientation();
await this.initAutoWebview();
await this.waitForAppLaunched();
}
async startRealDevice () {
await utils.removeInstrumentsSocket(this.sock);
this.opts.localizableStrings = await utils.parseLocalizableStrings(this.opts);
await utils.setBundleIdFromApp(this.opts);
await this.createInstruments();
await runRealDeviceReset(this.realDevice, this.opts);
await this.setUpLogCapture();
await this.installToRealDevice();
await this.startInstruments();
await this.onInstrumentsLaunch();
await this.configureBootstrap();
await this.setBundleId();
await this.setInitialOrientation();
await this.initAutoWebview();
await this.waitForAppLaunched();
}
async installToRealDevice () {
// if user has passed in desiredCaps.autoLaunch = false
// meaning they will manage app install / launching
if (this.opts.autoLaunch === false) {
return;
}
// if we have an ipa file, set it in opts
if (this.opts.app) {
let ext = this.opts.app.substring(this.opts.app.length - 3).toLowerCase();
if (ext === 'ipa') {
this.opts.ipa = this.opts.app;
}
}
if (this.opts.udid) {
if (await this.realDevice.isInstalled(this.opts.bundleId)) {
logger.debug('App is installed.');
if (this.opts.fullReset) {
logger.debug('fullReset requested. Forcing app install.');
} else {
logger.debug('fullReset not requested. No need to install.');
return;
}
} else {
logger.debug('App is not installed. Will try to install.');
}
if (this.opts.ipa && this.opts.bundleId) {
await this.installIpa();
logger.debug('App installed.');
} else if (this.opts.ipa) {
let msg = 'You specified a UDID and ipa but did not include the bundle id';
logger.warn(msg);
throw new errors.UnknownError(msg);
} else if (this.opts.app) {
await this.realDevice.install(this.opts.app);
logger.debug('App installed.');
} else {
logger.debug('Real device specified but no ipa or app path, assuming bundle ID is ' +
'on device');
}
} else {
logger.debug('No device id or app, not installing to real device.');
}
}
getIDeviceObj () {
let idiPath = path.resolve(__dirname, '../../../build/',
'libimobiledevice-macosx/ideviceinstaller');
logger.debug(`Creating iDevice object with udid ${this.opts.udid}`);
try {
return iDevice(this.opts.udid);
} catch (e1) {
logger.debug(`Couldn't find ideviceinstaller, trying built-in at ${idiPath}`);
try {
return iDevice(this.opts.udid, {cmd: idiPath});
} catch (e2) {
let msg = 'Could not initialize ideviceinstaller; make sure it is ' +
'installed and works on your system';
logger.error(msg);
throw new Error(msg);
}
}
}
async installIpa () {
logger.debug(`Installing ipa found at ${this.opts.ipa}`);
if (await this.realDevice.isInstalled(this.opts.bundleId)) {
logger.debug('Bundle found on device, removing before reinstalling.');
await this.realDevice.remove(this.opts.bundleId);
} else {
logger.debug('Nothing found on device, going ahead and installing.');
}
await this.realDevice.installAndWait(this.opts.ipa, this.opts.bundleId);
}
validateDesiredCaps (caps) {
// check with the base class, and return if it fails
let res = super.validateDesiredCaps(caps);
if (!res) return res; // eslint-disable-line curly
return desiredCapValidation(caps);
}
async prelaunchSimulator () {
if (!this.shouldPrelaunchSimulator) {
logger.debug('Not pre-launching simulator');
return;
}
await endSimulator(this.sim);
// TODO: implement prelaunch sim in simulator package
}
async onInstrumentsLaunch () {
logger.debug('Instruments launched. Starting poll loop for new commands.');
if (this.opts.origAppPath) {
logger.debug('Copying app back to its original place');
return await fs.copyFile(this.opts.app, this.opts.origAppPath);
}
}
async setBundleId () {
if (this.opts.bundleId) {
// We already have a bundle Id
return;
} else {
let bId = await this.uiAutoClient.sendCommand('au.bundleId()');
logger.debug(`Bundle ID for open app is ${bId.value}`);
this.opts.bundleId = bId.value;
}
}
async startIWDP () {
if (this.opts.startIWDP) {
this.iwdpServer = new IWDP({
webkitDebugProxyPort: this.opts.webkitDebugProxyPort,
udid: this.opts.udid,
logStdout: !!this.opts.showIWDPLog,
});
await this.iwdpServer.start();
}
}
async stopIWDP () {
if (this.iwdpServer) {
await this.iwdpServer.stop();
delete this.iwdpServer;
}
}
async setInitialOrientation () {
if (_.isString(this.opts.initialOrientation) &&
_.includes(['LANDSCAPE', 'PORTRAIT'], this.opts.initialOrientation.toUpperCase())) {
logger.debug(`Setting initial orientation to ${this.opts.initialOrientation}`);
let command = `au.setScreenOrientation('${this.opts.initialOrientation.toUpperCase()}')`;
try {
await this.uiAutoClient.sendCommand(command);
this.opts.curOrientation = this.opts.initialOrientation;
} catch (err) {
logger.warn(`Setting initial orientation failed with: ${err}`);
}
}
}
isRealDevice () {
return !!this.opts.udid;
}
isSafari () {
return this.opts.safari;
}
async waitForAppLaunched () {
// on iOS8 in particular, we can get a working session before the app
// is ready to respond to commands; in that case the source will be empty
// so we just spin until it's not
let condFn;
if (this.opts.waitForAppScript) {
// the default getSourceForElementForXML does not fit some use case, so making this customizable.
// TODO: collect script from customer and propose several options, please comment in issue #4190.
logger.debug(`Using custom script to wait for app start: ${this.opts.waitForAppScript}`);
condFn = async () => {
let res;
try {
res = await this.uiAutoClient.sendCommand(`try{\n${this.opts.waitForAppScript}` +
`\n} catch(err) { $.log("waitForAppScript err: " + error); false; };`);
} catch (err) {
logger.debug(`Cannot eval waitForAppScript script, err: ${err}`);
return false;
}
if (typeof res !== 'boolean') {
logger.debug('Unexpected return type in waitForAppScript script');
return false;
}
return res;
};
} else if (this.isSafari()) {
if (this.isRealDevice()) {
await this.clickButtonToLaunchSafari();
}
logger.debug('Waiting for initial webview');
await this.navToInitialWebview();
condFn = async () => true; // eslint-disable-line require-await
} else {
logger.debug('Waiting for app source to contain elements');
condFn = async () => {
try {
let source = await this.getSourceForElementForXML();
source = JSON.parse(source || '{}');
let appEls = (source.UIAApplication || {})['>'];
return appEls && appEls.length > 0 && !IosDriver.isSpringBoard(source.UIAApplication);
} catch (e) {
logger.warn(`Couldn't extract app element from source, error was: ${e}`);
return false;
}
};
}
try {
await waitForCondition(condFn, {logger, waitMs: 10000, intervalMs: 500});
} catch (err) {
if (err.message && err.message.match(/Condition unmet/)) {
logger.warn('Initial spin timed out, continuing but the app might not be ready.');
logger.debug(`Initial spin error was: ${err}`);
} else {
throw err;
}
}
}
static isSpringBoard (uiAppObj) {
// TODO: move to helpers
// Test for iOS homescreen (SpringBoard). AUT occassionally start the sim, but fails to load
// the app. If that occurs, getSourceForElementFoXML will return a doc object that meets our
// app-check conditions, resulting in a false positive. This function tests the UiApplication
// property's meta data to ensure that the Appium doesn't confuse SpringBoard with the app
// under test.
return _.propertyOf(uiAppObj['@'])('name') === 'SpringBoard';
}
async createInstruments () {
logger.debug('Creating instruments');
this.uiAutoClient = new UIAutoClient(this.sock);
this.instruments = await this.makeInstruments();
this.instruments.onShutdown.catch(async () => { // eslint-disable-line promise/catch-or-return
// unexpected exit
await this.startUnexpectedShutdown(new errors.UnknownError('Abnormal Instruments termination!'));
}).done();
}
shouldIgnoreInstrumentsExit () {
return this.safari && this.isRealDevice();
}
async makeInstruments () {
// at the moment all the logging in uiauto is at debug level
let bootstrapPath = await prepareBootstrap({
sock: this.sock,
interKeyDelay: this.opts.interKeyDelay,
justLoopInfinitely: false,
autoAcceptAlerts: this.opts.autoAcceptAlerts,
autoDismissAlerts: this.opts.autoDismissAlerts,
sendKeyStrategy: this.opts.sendKeyStrategy || (this.isRealDevice() ? 'grouped' : 'oneByOne')
});
let instruments = new Instruments({
// on real devices bundleId is always used
app: (!this.isRealDevice() ? this.opts.app : null) || this.opts.bundleId,
udid: this.opts.udid,
processArguments: this.opts.processArguments,
ignoreStartupExit: this.shouldIgnoreInstrumentsExit(),
bootstrap: bootstrapPath,
template: this.opts.automationTraceTemplatePath,
instrumentsPath: this.opts.instrumentsPath,
withoutDelay: this.opts.withoutDelay,
platformVersion: this.opts.platformVersion,
webSocket: this.opts.webSocket,
launchTimeout: this.opts.launchTimeout,
flakeyRetries: this.opts.backendRetries,
realDevice: this.isRealDevice(),
simulatorSdkAndDevice: this.iosSdkVersion >= 7.1 ? await getAdjustedDeviceName(this.opts) : null,
tmpDir: path.resolve(this.opts.tmpDir || '/tmp', 'appium-instruments'),
traceDir: this.opts.traceDir,
locale: this.opts.locale,
language: this.opts.language
});
return instruments;
}
async startInstruments () {
logger.debug('Starting UIAutoClient, and launching Instruments.');
await B.all([
this.uiAutoClient.start().then(() => { this.instruments.registerLaunch(); }),
this.instruments.launch()
]);
}
async configureBootstrap () {
logger.debug('Setting bootstrap config keys/values');
let isVerbose = true; // TODO: level was configured according to logger
let cmd = 'target = $.target();\n';
cmd += 'au = $;\n';
cmd += `$.isVerbose = ${isVerbose};\n`;
// Not using uiauto grace period because of bug.
// cmd += '$.target().setTimeout(1);\n';
await this.uiAutoClient.sendCommand(cmd);
}
async getSourceForElementForXML (ctx) {
let source;
if (!ctx) {
source = await this.uiAutoClient.sendCommand('au.mainApp().getTreeForXML()');
} else {
source = await this.uiAutoClient.sendCommand(`au.getElement('${ctx}').getTreeForXML()`);
}
// TODO: all this json/xml logic is very expensive, we need
// to use a SAX parser instead.
if (source) {
return JSON.stringify(source);
} else {
// this should never happen but we've received bug reports; this will help us track down
// what's wrong in getTreeForXML
throw new Error(`Bad response from getTreeForXML. res was ${JSON.stringify(source)}`);
}
}
async setUpLogCapture () {
if (this.caps.skipLogCapture) {
logger.info("'skipLogCapture' is set. Skipping the collection of system logs and crash reports.");
return;
}
if (this.isRealDevice()) {
await this.startLogCapture();
} else {
await this.startLogCapture(this.sim);
}
}
get realDevice () {
this._realDevice = this._realDevice || this.getIDeviceObj();
return this._realDevice;
}
set realDevice (rd) {
this._realDevice = rd;
}
}
for (let [cmd, fn] of _.toPairs(commands)) {
IosDriver.prototype[cmd] = fn;
}
export { IosDriver, defaultServerCaps };
export default IosDriver;
"use strict";
var path = require('path')
, rimraf = require('rimraf')
, ncp = require('ncp').ncp
, fs = require('fs')
, _ = require('underscore')
, which = require('which')
, logger = require('../../server/logger.js').get('appium')
, exec = require('child_process').exec
, spawn = require('child_process').spawn
, bplistCreate = require('bplist-creator')
, bplistParse = require('bplist-parser')
, xmlplist = require('plist')
, Device = require('../device.js')
, Instruments = require('./instruments.js')
, xcode = require('../../future.js').xcode
, errors = require('../../server/errors.js')
, deviceCommon = require('../common.js')
, iOSLog = require('./ios-log.js')
, iOSCrashLog = require('./ios-crash-log.js')
, status = require("../../server/status.js")
, iDevice = require('node-idevice')
, async = require('async')
, iOSController = require('./ios-controller.js')
, iOSHybrid = require('./ios-hybrid.js')
, settings = require('./settings.js')
, Simulator = require('./simulator.js')
, prepareBootstrap = require('./uiauto').prepareBootstrap
, CommandProxy = require('./uiauto').CommandProxy
, UnknownError = errors.UnknownError
, binaryPlist = true
, Args = require("vargs").Constructor
, logCustomDeprecationWarning = require('../../helpers.js').logCustomDeprecationWarning;
// XML Plist library helper
var parseXmlPlistFile = function (plistFilename, cb) {
try {
var xmlContent = fs.readFileSync(plistFilename, 'utf8');
var result = xmlplist.parse(xmlContent);
return cb(null, result);
} catch (ex) {
return cb(ex);
}
};
var parsePlistFile = function (plist, cb) {
bplistParse.parseFile(plist, function (err, obj) {
if (err) {
logger.debug("Could not parse plist file (as binary) at " + plist);
logger.info("Will try to parse the plist file as XML");
parseXmlPlistFile(plist, function (err, obj) {
if (err) {
logger.debug("Could not parse plist file (as XML) at " + plist);
return cb(err, null);
} else {
logger.debug("Parsed app Info.plist (as XML)");
binaryPlist = false;
cb(null, obj);
}
});
} else {
binaryPlist = true;
if (obj.length) {
logger.debug("Parsed app Info.plist (as binary)");
cb(null, obj[0]);
} else {
cb(new Error("Binary Info.plist appears to be empty"));
}
}
});
};
var IOS = function () {
this.init();
};
_.extend(IOS.prototype, Device.prototype);
IOS.prototype._deviceInit = Device.prototype.init;
IOS.prototype.init = function () {
this._deviceInit();
this.appExt = ".app";
this.capabilities = {
webStorageEnabled: false
, locationContextEnabled: false
, browserName: 'iOS'
, platform: 'MAC'
, javascriptEnabled: true
, databaseEnabled: false
, takesScreenshot: true
, networkConnectionEnabled: false
};
this.xcodeVersion = null;
this.iOSSDKVersion = null;
this.iosSimProcess = null;
this.iOSSimUdid = null;
this.logs = {};
this.instruments = null;
this.commandProxy = null;
this.initQueue();
this.onInstrumentsDie = function () {};
this.stopping = false;
this.cbForCurrentCmd = null;
this.remote = null;
this.curContext = null;
this.curWebFrames = [];
this.selectingNewPage = false;
this.processingRemoteCmd = false;
this.remoteAppKey = null;
this.windowHandleCache = [];
this.webElementIds = [];
this.implicitWaitMs = 0;
this.asyncWaitMs = 0;
this.pageLoadMs = 60000;
this.asyncResponseCb = null;
this.returnedFromExecuteAtom = {};
this.executedAtomsCounter = 0;
this.curCoords = null;
this.curWebCoords = null;
this.onPageChangeCb = null;
this.supportedStrategies = ["name", "xpath", "id", "-ios uiautomation",
"class name", "accessibility id"];
this.landscapeWebCoordsOffset = 0;
this.localizableStrings = {};
this.keepAppToRetainPrefs = false;
this.isShuttingDown = false;
};
IOS.prototype._deviceConfigure = Device.prototype.configure;
IOS.prototype.configure = function (args, caps, cb) {
var msg;
this._deviceConfigure(args, caps);
this.setIOSArgs();
if (this.args.locationServicesAuthorized && !this.args.bundleId) {
msg = "You must set the bundleId cap if using locationServicesEnabled";
logger.error(msg);
return cb(new Error(msg));
}
// on iOS8 we can use a bundleId to launch an app on the simulator, but
// on previous versions we can only do so on a real device, so we need
// to do a check of which situation we're in
var ios8 = caps.platformVersion &&
parseFloat(caps.platformVersion) >= 8;
if (!this.args.app &&
!((ios8 || this.args.udid) && this.args.bundleId)) {
msg = "Please provide the 'app' or 'browserName' capability or start " +
"appium with the --app or --browser-name argument. Alternatively, " +
"you may provide the 'bundleId' and 'udid' capabilities for an app " +
"under test on a real device.";
logger.error(msg);
return cb(new Error(msg));
}
if (parseFloat(caps.platformVersion) < 7.1) {
logCustomDeprecationWarning('iOS version', caps.platformVersion,
'iOS ' + caps.platformVersion + ' support has ' +
'been deprecated and will be removed in a ' +
'future version of Appium.');
}
return this.configureApp(cb);
};
IOS.prototype.setIOSArgs = function () {
this.args.withoutDelay = !this.args.nativeInstrumentsLib;
this.args.reset = !this.args.noReset;
this.args.initialOrientation = this.capabilities.deviceOrientation ||
this.args.orientation ||
"PORTRAIT";
this.useRobot = this.args.robotPort > 0;
this.args.robotUrl = this.useRobot ?
"http://" + this.args.robotAddress + ":" + this.args.robotPort + "" :
null;
this.curOrientation = this.args.initialOrientation;
this.sock = path.resolve(this.args.tmpDir || '/tmp', 'instruments_sock');
this.perfLogEnabled = !!(typeof this.args.loggingPrefs === 'object' && this.args.loggingPrefs.performance);
};
IOS.prototype.configureApp = function (cb) {
var _cb = cb;
cb = function (err) {
if (err) {
err = new Error("Bad app: " + this.args.app + ". App paths need to " +
"be absolute, or relative to the appium server " +
"install dir, or a URL to compressed file, or a " +
"special app name. cause: " + err);
}
_cb(err);
}.bind(this);
var app = this.appString();
// if the app name is a bundleId assign it to the bundleId property
if (!this.args.bundleId && this.appIsPackageOrBundle(app)) {
this.args.bundleId = app;
}
if (app !== "" && app.toLowerCase() === "settings") {
if (parseFloat(this.args.platformVersion) >= 8) {
logger.debug("We're on iOS8+ so not copying preferences app");
this.args.bundleId = "com.apple.Preferences";
this.args.app = null;
}
cb();
} else if (this.args.bundleId &&
this.appIsPackageOrBundle(this.args.bundleId) &&
(app === "" || this.appIsPackageOrBundle(app))) {
// we have a bundle ID, but no app, or app is also a bundle
logger.debug("App is an iOS bundle, will attempt to run as pre-existing");
cb();
} else {
Device.prototype.configureApp.call(this, cb);
}
};
IOS.prototype.removeInstrumentsSocket = function (cb) {
var removeSocket = function (innerCb) {
logger.debug("Removing any remaining instruments sockets");
rimraf(this.sock, function (err) {
if (err) return innerCb(err);
logger.debug("Cleaned up instruments socket " + this.sock);
innerCb();
}.bind(this));
}.bind(this);
removeSocket(cb);
};
IOS.prototype.getNumericVersion = function () {
return parseFloat(this.args.platformVersion);
};
IOS.prototype.startRealDevice = function (cb) {
async.series([
this.removeInstrumentsSocket.bind(this),
this.detectUdid.bind(this),
this.parseLocalizableStrings.bind(this),
this.setBundleIdFromApp.bind(this),
this.createInstruments.bind(this),
this.startLogCapture.bind(this),
this.installToRealDevice.bind(this),
this.startInstruments.bind(this),
this.onInstrumentsLaunch.bind(this),
this.configureBootstrap.bind(this),
this.setBundleId.bind(this),
this.setInitialOrientation.bind(this),
this.initAutoWebview.bind(this),
this.waitForAppLaunched.bind(this),
], function (err) {
cb(err);
});
};
IOS.prototype.startSimulator = function (cb) {
async.series([
this.removeInstrumentsSocket.bind(this),
this.setXcodeVersion.bind(this),
this.setiOSSDKVersion.bind(this),
this.checkSimAvailable.bind(this),
this.createSimulator.bind(this),
this.moveBuiltInApp.bind(this),
this.detectUdid.bind(this),
this.parseLocalizableStrings.bind(this),
this.setBundleIdFromApp.bind(this),
this.createInstruments.bind(this),
this.setDeviceInfo.bind(this),
this.checkPreferences.bind(this),
this.runSimReset.bind(this),
this.isolateSimDevice.bind(this),
this.setLocale.bind(this),
this.setPreferences.bind(this),
this.startLogCapture.bind(this),
this.prelaunchSimulator.bind(this),
this.startInstruments.bind(this),
this.onInstrumentsLaunch.bind(this),
this.configureBootstrap.bind(this),
this.setBundleId.bind(this),
this.setInitialOrientation.bind(this),
this.initAutoWebview.bind(this),
this.waitForAppLaunched.bind(this),
], function (err) {
cb(err);
});
};
IOS.prototype.start = function (cb, onDie) {
if (this.instruments !== null) {
var msg = "Trying to start a session but instruments is still around";
logger.error(msg);
return cb(new Error(msg));
}
if (typeof onDie === "function") {
this.onInstrumentsDie = onDie;
}
if (this.args.udid) {
this.startRealDevice(cb);
} else {
this.startSimulator(cb);
}
};
IOS.prototype.createInstruments = function (cb) {
logger.debug("Creating instruments");
this.commandProxy = new CommandProxy({ sock: this.sock });
this.makeInstruments(function (err, instruments) {
if (err) return cb(err);
this.instruments = instruments;
cb();
}.bind(this));
};
IOS.prototype.startInstruments = function (cb) {
cb = _.once(cb);
var treatError = function (err, cb) {
if (!_.isEmpty(this.logs)) {
this.logs.syslog.stopCapture();
this.logs = {};
}
this.postCleanup(function () {
cb(err);
});
}.bind(this);
logger.debug("Starting command proxy.");
this.commandProxy.start(
function onFirstConnection(err) {
// first let instruments know so that it does not restart itself
this.instruments.launchHandler(err);
// then we call the callback
cb(err);
}.bind(this)
, function regularCallback(err) {
if (err) return treatError(err, cb);
logger.debug("Starting instruments");
this.instruments.start(
function (err) {
if (err) return treatError(err, cb);
// we don't call cb here, waiting for first connection or error
}.bind(this)
, function (code) {
if (!this.shouldIgnoreInstrumentsExit()) {
this.onUnexpectedInstrumentsExit(code);
}
}.bind(this)
);
}.bind(this)
);
};
IOS.prototype.makeInstruments = function (cb) {
// at the moment all the logging in uiauto is at debug level
// TODO: be able to use info in appium-uiauto
var bootstrap = prepareBootstrap({
sock: this.sock,
interKeyDelay: this.args.interKeyDelay,
justLoopInfinitely: false,
autoAcceptAlerts: !(!this.args.autoAcceptAlerts || this.args.autoAcceptAlerts === 'false'),
autoDismissAlerts: !(!this.args.autoDismissAlerts || this.args.autoDismissAlerts === 'false'),
sendKeyStrategy: this.args.sendKeyStrategy || (this.args.udid ? 'grouped' : 'oneByOne')
});
bootstrap.then(function (bootstrapPath) {
var instruments = new Instruments({
// on real devices bundleId is always used
app: (!this.args.udid ? this.args.app : null) || this.args.bundleId
, udid: this.args.udid
, processArguments: this.args.processArguments
, ignoreStartupExit: this.shouldIgnoreInstrumentsExit()
, bootstrap: bootstrapPath
, template: this.args.automationTraceTemplatePath
, instrumentsPath: this.args.instrumentsPath
, withoutDelay: this.args.withoutDelay
, platformVersion: this.args.platformVersion
, webSocket: this.args.webSocket
, launchTimeout: this.args.launchTimeout
, flakeyRetries: this.args.backendRetries
, simulatorSdkAndDevice: this.iOSSDKVersion >= 7.1 ? this.getDeviceString() : null
, tmpDir: path.resolve(this.args.tmpDir , 'appium-instruments')
, traceDir: this.args.traceDir
});
cb(null, instruments);
}.bind(this), cb).fail(cb);
};
IOS.prototype.shouldIgnoreInstrumentsExit = function () {
return false;
};
IOS.prototype.onInstrumentsLaunch = function (cb) {
logger.debug('Instruments launched. Starting poll loop for new commands.');
this.instruments.setDebug(true);
if (this.args.origAppPath) {
logger.debug("Copying app back to its original place");
return ncp(this.args.app, this.args.origAppPath, cb);
}
cb();
};
IOS.prototype.setBundleId = function (cb) {
if (this.args.bundleId) {
// We already have a bundle Id
cb();
} else {
this.proxy('au.bundleId()', function (err, bId) {
if (err) return cb(err);
logger.debug('Bundle ID for open app is ' + bId.value);
this.args.bundleId = bId.value;
cb();
}.bind(this));
}
};
IOS.prototype.setInitialOrientation = function (cb) {
if (typeof this.args.initialOrientation === "string" &&
_.contains(["LANDSCAPE", "PORTRAIT"],
this.args.initialOrientation.toUpperCase())
) {
logger.debug("Setting initial orientation to " + this.args.initialOrientation);
var command = ["au.setScreenOrientation('",
this.args.initialOrientation.toUpperCase(), "')"].join('');
this.proxy(command, function (err, res) {
if (err || res.status !== status.codes.Success.code) {
logger.warn("Setting initial orientation did not work!");
} else {
this.curOrientation = this.args.initialOrientation;
}
cb();
}.bind(this));
} else {
cb();
}
};
IOS.isSpringBoard = function (uiAppObj) {
// Test for iOS homescreen (SpringBoard). AUT occassionally start the sim, but fails to load
// the app. If that occurs, getSourceForElementFoXML will return a doc object that meets our
// app-check conditions, resulting in a false positive. This function tests the UiApplication
// property's meta data to ensure that the Appium doesn't confuse SpringBoard with the app
// under test.
return _.propertyOf(uiAppObj['@'])('name') === 'SpringBoard';
};
IOS.prototype.waitForAppLaunched = function (cb) {
// on iOS8 in particular, we can get a working session before the app
// is ready to respond to commands; in that case the source will be empty
// so we just spin until it's not
var condFn;
if (this.args.waitForAppScript) {
// the default getSourceForElementForXML does not fit some use case, so making this customizable.
// TODO: collect script from customer and propose several options, please comment in issue #4190.
logger.debug("Using custom script to wait for app start:" + this.args.waitForAppScript);
condFn = function (cb) {
this.proxy('try{\n' + this.args.waitForAppScript +
'\n} catch(err) { $.log("waitForAppScript err: " + error); false; };',
function (err, res) {
cb(!!res.value, err);
});
}.bind(this);
} else {
logger.debug("Waiting for app source to contain elements");
condFn = function (cb) {
this.getSourceForElementForXML(null, function (err, res) {
if (err || !res || res.status !== status.codes.Success.code) {
return cb(false, err);
}
var sourceObj, appEls;
try {
sourceObj = JSON.parse(res.value);
appEls = sourceObj.UIAApplication['>'];
if (appEls.length > 0 && !IOS.isSpringBoard(sourceObj.UIAApplication)) {
return cb(true);
} else {
return cb(false, new Error("App did not have elements"));
}
} catch (e) {
return cb(false, new Error("Couldn't parse JSON source"));
}
return cb(true, err);
});
}.bind(this);
}
this.waitForCondition(10000, condFn, cb, 500);
};
IOS.prototype.configureBootstrap = function (cb) {
logger.debug("Setting bootstrap config keys/values");
var isVerbose = logger.transports.console.level === 'debug';
var cmd = '';
cmd += 'target = $.target();\n';
cmd += 'au = $;\n';
cmd += '$.isVerbose = ' + isVerbose + ';\n';
// Not using uiauto grace period because of bug.
// cmd += '$.target().setTimeout(1);\n';
this.proxy(cmd, cb);
};
IOS.prototype.onUnexpectedInstrumentsExit = function (code) {
logger.debug("Instruments exited unexpectedly");
this.isShuttingDown = true;
var postShutdown = function () {
if (typeof this.cbForCurrentCmd === "function") {
logger.debug("We were in the middle of processing a command when " +
"instruments died; responding with a generic error");
var error = new UnknownError("Instruments died while responding to " +
"command, please check appium logs");
this.onInstrumentsDie(error, this.cbForCurrentCmd);
} else {
this.onInstrumentsDie();
}
}.bind(this);
if (this.commandProxy) {
this.commandProxy.safeShutdown(function () {
this.shutdown(code, postShutdown);
}.bind(this));
} else {
this.shutdown(code, postShutdown);
}
};
IOS.prototype.setXcodeVersion = function (cb) {
logger.debug("Setting Xcode version");
xcode.getVersion(function (err, versionNumber) {
if (err) {
logger.error("Could not determine Xcode version:" + err.message);
} else {
var minorVersion = parseFloat(versionNumber.slice(0, 3));
var pv = parseFloat(this.args.platformVersion);
// we deprecate Xcodes < 6.3, except for iOS 8.0 in which case we
// support Xcode 6.0 as well
if (minorVersion < 6.3 && (!(minorVersion === 6.0 && pv === 8.0))) {
logCustomDeprecationWarning('Xcode version', versionNumber,
'Support for Xcode ' + versionNumber + ' ' +
'has been deprecated and will be removed ' +
'in a future version. Please upgrade ' +
'to version 6.3 or higher (or version ' +
'6.0.1 for iOS 8.0)');
}
}
this.xcodeVersion = versionNumber;
logger.debug("Xcode version set to " + versionNumber);
cb();
}.bind(this));
};
IOS.prototype.setiOSSDKVersion = function (cb) {
logger.debug("Setting iOS SDK Version");
xcode.getMaxIOSSDK(function (err, versionNumber) {
if (err) {
logger.error("Could not determine iOS SDK version");
return cb(err);
}
this.iOSSDKVersion = versionNumber;
logger.debug("iOS SDK Version set to " + this.iOSSDKVersion);
cb();
}.bind(this));
};
IOS.prototype.setLocale = function (cb) {
var msg;
var setLoc = function (err) {
logger.debug("Setting locale information");
if (err) return cb(err);
var needSimRestart = false;
this.localeConfig = this.localeConfig || {};
_(['language', 'locale', 'calendarFormat']).each(function (key) {
needSimRestart = needSimRestart ||
(this.args[key] &&
this.args[key] !== this.localeConfig[key]);
}, this);
this.localeConfig = {
language: this.args.language,
locale: this.args.locale,
calendarFormat: this.args.calendarFormat
};
var simRoots = this.sim.getDirs();
if (simRoots.length < 1) {
msg = "Cannot set locale information because the iOS Simulator directory could not be determined.";
logger.error(msg);
return cb(new Error(msg));
}
try {
this.sim.setLocale(this.args.language, this.args.locale, this.args.calendarFormat);
} catch (e) {
msg = "Appium was unable to set locale info: " + e;
logger.error(msg);
return cb(new Error(msg));
}
logger.debug("Locale was set");
if (needSimRestart) {
logger.debug("First time setting locale, or locale changed, killing existing Instruments and Sim procs.");
Instruments.killAllSim();
Instruments.killAll();
setTimeout(cb, 250);
} else {
cb();
}
}.bind(this);
if ((this.args.language || this.args.locale || this.args.calendarFormat) && this.args.udid === null) {
if (this.args.fullReset && this.args.platformVersion <= 6.1) {
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";
logger.error(msg);
return cb(new Error(msg));
}
if (!this.sim.dirsExist()) {
this.instantLaunchAndQuit(false, setLoc);
} else {
setLoc();
}
} else if (this.args.udid) {
logger.debug("Not setting locale because we're using a real device");
cb();
} else {
logger.debug("Not setting locale");
cb();
}
};
IOS.prototype.checkPreferences = function (cb) {
logger.debug("Checking whether we need to set app preferences");
if (this.args.udid !== null) {
logger.debug("Not setting iOS and app preferences since we're on a real " +
"device");
return cb();
}
var settingsCaps = [
'locationServicesEnabled',
'locationServicesAuthorized',
'safariAllowPopups',
'safariIgnoreFraudWarning',
'safariOpenLinksInBackground'
];
var safariSettingsCaps = settingsCaps.slice(2, 5);
this.needToSetPrefs = false;
this.needToSetSafariPrefs = false;
_.each(settingsCaps, function (cap) {
if (_.has(this.capabilities, cap)) {
this.needToSetPrefs = true;
if (_.contains(safariSettingsCaps, cap)) {
this.needToSetSafariPrefs = true;
}
}
}.bind(this));
this.keepAppToRetainPrefs = this.needToSetPrefs;
cb();
};
IOS.prototype.setPreferences = function (cb) {
if (!this.needToSetPrefs) {
logger.debug("No iOS / app preferences to set");
return cb();
} else if (this.args.fullReset) {
var msg = "Cannot set preferences because a full-reset was requested";
logger.debug(msg);
logger.error(msg);
return cb(new Error(msg));
}
var setPrefs = function (err) {
if (err) return cb(err);
try {
this.setLocServicesPrefs();
} catch (e) {
logger.error("Error setting location services preferences, prefs will not work");
logger.error(e);
logger.error(e.stack);
}
try {
this.setSafariPrefs();
} catch (e) {
logger.error("Error setting safari preferences, prefs will not work");
logger.error(e);
logger.error(e.stack);
}
cb();
}.bind(this);
logger.debug("Setting iOS and app preferences");
if (!this.sim.dirsExist() ||
!settings.locServicesDirsExist(this.sim) ||
(this.needToSetSafariPrefs && !this.sim.safariDirsExist())) {
this.instantLaunchAndQuit(this.needToSetSafariPrefs, setPrefs);
} else {
setPrefs();
}
};
IOS.prototype.instantLaunchAndQuit = function (needSafariDirs, cb) {
logger.debug("Sim files for the " + this.iOSSDKVersion + " SDK do not yet exist, launching the sim " +
"to populate the applications and preference dirs");
var condition = function () {
var simDirsExist = this.sim.dirsExist();
var locServicesExist = settings.locServicesDirsExist(this.sim);
var safariDirsExist = this.args.platformVersion < 7.0 ||
(this.sim.safariDirsExist() &&
(this.args.platformVersion < 8.0 ||
this.sim.userSettingsPlistExists())
);
var okToGo = simDirsExist && locServicesExist &&
(!needSafariDirs || safariDirsExist);
if (!okToGo) {
logger.debug("We launched the simulator but the required dirs don't " +
"yet exist. Waiting some more...");
}
return okToGo;
}.bind(this);
this.prelaunchSimulator(function (err) {
if (err) return cb(err);
this.makeInstruments(function (err, instruments) {
instruments.launchAndKill(condition, function (err) {
if (err) return cb(err);
this.endSimulator(cb);
}.bind(this));
}.bind(this));
}.bind(this));
};
IOS.prototype.setLocServicesPrefs = function () {
if (typeof this.capabilities.locationServicesEnabled !== "undefined" ||
this.capabilities.locationServicesAuthorized) {
var locServ = this.capabilities.locationServicesEnabled;
locServ = locServ || this.capabilities.locationServicesAuthorized;
locServ = locServ ? 1 : 0;
logger.debug("Setting location services to " + locServ);
settings.updateSettings(this.sim, 'locationServices', {
LocationServicesEnabled: locServ,
'LocationServicesEnabledIn7.0': locServ,
'LocationServicesEnabledIn8.0': locServ
}
);
}
if (typeof this.capabilities.locationServicesAuthorized !== "undefined") {
if (!this.args.bundleId) {
var msg = "Can't set location services for app without bundle ID";
logger.error(msg);
throw new Error(msg);
}
var locAuth = !!this.capabilities.locationServicesAuthorized;
if (locAuth) {
logger.debug("Authorizing location services for app");
} else {
logger.debug("De-authorizing location services for app");
}
settings.updateLocationSettings(this.sim, this.args.bundleId, locAuth);
}
};
IOS.prototype.setSafariPrefs = function () {
var safariSettings = {};
var val;
if (_.has(this.capabilities, 'safariAllowPopups')) {
val = !!this.capabilities.safariAllowPopups;
logger.debug("Setting javascript window opening to " + val);
safariSettings.WebKitJavaScriptCanOpenWindowsAutomatically = val;
safariSettings.JavaScriptCanOpenWindowsAutomatically = val;
}
if (_.has(this.capabilities, 'safariIgnoreFraudWarning')) {
val = !this.capabilities.safariIgnoreFraudWarning;
logger.debug("Setting fraudulent website warning to " + val);
safariSettings.WarnAboutFraudulentWebsites = val;
}
if (_.has(this.capabilities, 'safariOpenLinksInBackground')) {
val = this.capabilities.safariOpenLinksInBackground ? 1 : 0;
logger.debug("Setting opening links in background to " + !!val);
safariSettings.OpenLinksInBackground = val;
}
if (_.size(safariSettings) > 0) {
settings.updateSafariSettings(this.sim, safariSettings);
}
};
IOS.prototype.detectUdid = function (cb) {
var msg;
logger.debug("Auto-detecting iOS udid...");
if (this.args.udid !== null && this.args.udid === "auto") {
which('idevice_id', function (notFound, cmdPath) {
var udidetectPath;
if (notFound) {
udidetectPath = require.resolve('udidetect');
} else {
udidetectPath = cmdPath + " -l";
}
exec(udidetectPath, { maxBuffer: 524288, timeout: 3000 }, function (err, stdout) {
if (err) {
msg = "Error detecting udid: " + err.message;
logger.error(msg);
cb(err);
}
if (stdout && stdout.length > 2) {
this.args.udid = stdout.split("\n")[0];
logger.debug("Detected udid as " + this.args.udid);
cb();
} else {
msg = "Could not detect udid.";
logger.error(msg);
cb(new Error(msg));
}
}.bind(this));
}.bind(this));
} else {
logger.debug("Not auto-detecting udid, running on sim");
cb();
}
};
IOS.prototype.setBundleIdFromApp = function (cb) {
// This method will try to extract the bundleId from the app
if (this.args.bundleId) {
// We aleady have a bundle Id
cb();
} else {
this.getBundleIdFromApp(function (err, bundleId) {
if (err) {
logger.error("Could not set the bundleId from app.");
return cb(err);
}
this.args.bundleId = bundleId;
cb();
}.bind(this));
}
};
IOS.prototype.installToRealDevice = function (cb) {
// if user has passed in desiredCaps.autoLaunch = false
// meaning they will manage app install / launching
if (this.args.autoLaunch === false) {
cb();
} else {
if (this.args.udid) {
try {
this.realDevice = this.getIDeviceObj();
} catch (e) {
return cb(e);
}
this.isAppInstalled(this.args.bundleId, function (err, installed) {
if (err || !installed) {
logger.debug("App is not installed. Will try to install the app.");
} else {
logger.debug("App is installed.");
if (this.args.fullReset) {
logger.debug("fullReset requested. Forcing app install.");
} else {
logger.debug("fullReset not requested. No need to install.");
return cb();
}
}
if (this.args.ipa && this.args.bundleId) {
this.installIpa(cb);
} else if (this.args.ipa) {
var msg = "You specified a UDID and ipa but did not include the bundle " +
"id";
logger.error(msg);
cb(new Error(msg));
} else if (this.args.app) {
this.installApp(this.args.app, cb);
} else {
logger.debug("Real device specified but no ipa or app path, assuming bundle ID is " +
"on device");
cb();
}
}.bind(this));
} else {
logger.debug("No device id or app, not installing to real device.");
cb();
}
}
};
IOS.prototype.getIDeviceObj = function () {
var idiPath = path.resolve(__dirname, "../../../build/",
"libimobiledevice-macosx/ideviceinstaller");
logger.debug("Creating iDevice object with udid " + this.args.udid);
try {
return iDevice(this.args.udid);
} catch (e1) {
logger.debug("Couldn't find ideviceinstaller, trying built-in at " +
idiPath);
try {
return iDevice(this.args.udid, {cmd: idiPath});
} catch (e2) {
var msg = "Could not initialize ideviceinstaller; make sure it is " +
"installed and works on your system";
logger.error(msg);
throw new Error(msg);
}
}
};
IOS.prototype.installIpa = function (cb) {
logger.debug("Installing ipa found at " + this.args.ipa);
if (!this.realDevice) {
this.realDevice = this.getIDeviceObj();
}
var d = this.realDevice;
async.waterfall([
function (cb) { d.isInstalled(this.args.bundleId, cb); }.bind(this),
function (installed, cb) {
if (installed) {
logger.debug("Bundle found on device, removing before reinstalling.");
d.remove(this.args.bundleId, cb);
} else {
logger.debug("Nothing found on device, going ahead and installing.");
cb();
}
}.bind(this),
function (cb) { d.installAndWait(this.args.ipa, this.args.bundleId, cb); }.bind(this)
], cb);
};
IOS.getDeviceStringFromOpts = function (opts) {
logger.debug("Getting device string from opts: " + JSON.stringify({
forceIphone: opts.forceIphone,
forceIpad: opts.forceIpad,
xcodeVersion: opts.xcodeVersion,
iOSSDKVersion: opts.iOSSDKVersion,
deviceName: opts.deviceName,
platformVersion: opts.platformVersion
}));
var xcodeMajorVer = parseInt(opts.xcodeVersion.substr(0,
opts.xcodeVersion.indexOf('.')), 10);
var isiPhone = opts.forceIphone || opts.forceIpad === null || (opts.forceIpad !== null && !opts.forceIpad);
var isTall = isiPhone;
var isRetina = opts.xcodeVersion[0] !== '4';
var is64bit = false;
var deviceName = opts.deviceName;
var fixDevice = true;
if (deviceName && deviceName[0] === '=') {
return deviceName.substring(1);
}
logger.debug("fixDevice is " + (fixDevice ? "on" : "off"));
if (deviceName) {
var device = deviceName.toLowerCase();
if (device.indexOf("iphone") !== -1) {
isiPhone = true;
} else if (device.indexOf("ipad") !== -1) {
isiPhone = false;
}
if (deviceName !== opts.platformName) {
isTall = isiPhone && (device.indexOf("4-inch") !== -1);
isRetina = (device.indexOf("retina") !== -1);
is64bit = (device.indexOf("64-bit") !== -1);
}
}
var iosDeviceString = isiPhone ? "iPhone" : "iPad";
if (xcodeMajorVer === 4) {
if (isiPhone && isRetina) {
iosDeviceString += isTall ? " (Retina 4-inch)" : " (Retina 3.5-inch)";
} else {
iosDeviceString += isRetina ? " (Retina)" : "";
}
} else if (xcodeMajorVer === 5) {
iosDeviceString += isRetina ? " Retina" : "";
if (isiPhone) {
if (isRetina && isTall) {
iosDeviceString += is64bit ? " (4-inch 64-bit)" : " (4-inch)";
} else if (deviceName.toLowerCase().indexOf("3.5") !== -1) {
iosDeviceString += " (3.5-inch)";
}
} else {
iosDeviceString += is64bit ? " (64-bit)" : "";
}
} else if (_.contains([6, 7], xcodeMajorVer)) {
iosDeviceString = opts.deviceName ||
(isiPhone ? "iPhone Simulator" : "iPad Simulator");
}
var reqVersion = opts.platformVersion || opts.iOSSDKVersion;
if (opts.iOSSDKVersion >= 8 && xcodeMajorVer === 7) {
iosDeviceString += " (" + reqVersion + ")";
} else if (opts.iOSSDKVersion >= 8) {
iosDeviceString += " (" + reqVersion + " Simulator)";
} else if (opts.iOSSDKVersion >= 7.1) {
iosDeviceString += " - Simulator - iOS " + reqVersion;
}
if (fixDevice) {
// Some device config are broken in 5.1
var CONFIG_FIX = {
'iPhone - Simulator - iOS 7.1': 'iPhone Retina (4-inch 64-bit) - ' +
'Simulator - iOS 7.1',
'iPad - Simulator - iOS 7.1': 'iPad Retina (64-bit) - Simulator - ' +
'iOS 7.1',
'iPad Simulator (8.0 Simulator)': 'iPad 2 (8.0 Simulator)',
'iPad Simulator (8.1 Simulator)': 'iPad 2 (8.1 Simulator)',
'iPad Simulator (8.2 Simulator)': 'iPad 2 (8.2 Simulator)',
'iPad Simulator (8.3 Simulator)': 'iPad 2 (8.3 Simulator)',
'iPad Simulator (8.4 Simulator)': 'iPad 2 (8.4 Simulator)',
'iPad Simulator (7.1 Simulator)': 'iPad 2 (7.1 Simulator)',
'iPhone Simulator (8.4 Simulator)': 'iPhone 6 (8.4 Simulator)',
'iPhone Simulator (8.3 Simulator)': 'iPhone 6 (8.3 Simulator)',
'iPhone Simulator (8.2 Simulator)': 'iPhone 6 (8.2 Simulator)',
'iPhone Simulator (8.1 Simulator)': 'iPhone 6 (8.1 Simulator)',
'iPhone Simulator (8.0 Simulator)': 'iPhone 6 (8.0 Simulator)',
'iPhone Simulator (7.1 Simulator)': 'iPhone 5s (7.1 Simulator)'
};
// For xcode major version 7
var CONFIG_FIX__XCODE_7 = {
'iPad Simulator (8.1)': 'iPad 2 (8.1)',
'iPad Simulator (8.2)': 'iPad 2 (8.2)',
'iPad Simulator (8.3)': 'iPad 2 (8.3)',
'iPad Simulator (8.4)': 'iPad 2 (8.4)',
'iPhone Simulator (8.1)': 'iPhone 6 (8.1)',
'iPhone Simulator (8.2)': 'iPhone 6 (8.2)',
'iPhone Simulator (8.3)': 'iPhone 6 (8.3)',
'iPhone Simulator (8.4)': 'iPhone 6 (8.4)',
'iPad Simulator (9.0)': 'iPad 2 (9.0)',
'iPad Simulator (9.1)': 'iPad 2 (9.1)',
// Fixing ambiguous device name by adding '[' at the end so intruments
// correctly starts iPhone 6 [udid] and not the iPhone 6 (9.0) + Apple Watch
// for ios9.0 and above; see #5619
'iPhone Simulator (9.0)': 'iPhone 6 (9.0) [',
'iPhone Simulator (9.1)': 'iPhone 6 (9.1) [',
'iPhone 6 (9.0)': 'iPhone 6 (9.0) [',
'iPhone 6 (9.1)': 'iPhone 6 (9.1) ['
};
var configFix = xcodeMajorVer === 7 ? CONFIG_FIX__XCODE_7 : CONFIG_FIX;
if (configFix[iosDeviceString]) {
var oldDeviceString = iosDeviceString;
iosDeviceString = configFix[iosDeviceString];
logger.debug("Fixing device. Changed from: \"" + oldDeviceString +
"\" to: \"" + iosDeviceString + "\"");
}
}
logger.debug("Final device string is: '" + iosDeviceString + "'");
return iosDeviceString;
};
IOS.prototype.getDeviceString = function () {
var opts = _.clone(this.args);
_.extend(opts, {
xcodeVersion: this.xcodeVersion,
iOSSDKVersion: this.iOSSDKVersion
});
return IOS.getDeviceStringFromOpts(opts);
};
IOS.prototype.setDeviceTypeInInfoPlist = function (cb) {
var plist = path.resolve(this.args.app, "Info.plist");
var dString = this.getDeviceString();
var isiPhone = dString.toLowerCase().indexOf("ipad") === -1;
var deviceTypeCode = isiPhone ? 1 : 2;
parsePlistFile(plist, function (err, obj) {
if (err) {
logger.error("Could not set the device type in Info.plist");
return cb(err, null);
} else {
var newPlist;
obj.UIDeviceFamily = [deviceTypeCode];
if (binaryPlist) {
newPlist = bplistCreate(obj);
} else {
newPlist = xmlplist.build(obj);
}
fs.writeFile(plist, newPlist, function (err) {
if (err) {
logger.error("Could not save new Info.plist");
cb(err);
} else {
logger.debug("Wrote new app Info.plist with device type");
cb();
}
}.bind(this));
}
}.bind(this));
};
IOS.prototype.getBundleIdFromApp = function (cb) {
logger.debug("Getting bundle ID from app");
var plist = path.resolve(this.args.app, "Info.plist");
parsePlistFile(plist, function (err, obj) {
if (err) {
logger.error("Could not get the bundleId from app.");
cb(err, null);
} else {
cb(null, obj.CFBundleIdentifier);
}
}.bind(this));
};
IOS.getSimForDeviceString = function (dString, availDevices) {
var matchedDevice = null;
var matchedUdid = null;
_.each(availDevices, function (device) {
if (device.indexOf(dString) !== -1) {
matchedDevice = device;
try {
matchedUdid = /.+\[([^\]]+)\]/.exec(device)[1];
} catch (e) {
matchedUdid = null;
}
}
});
return [matchedDevice, matchedUdid];
};
IOS.prototype.checkSimAvailable = function (cb) {
if (this.args.udid) {
logger.debug("Not checking whether simulator is available since we're on " +
"a real device");
return cb();
}
if (this.iOSSDKVersion < 7.1) {
logger.debug("Instruments v < 7.1, not checking device string support");
return cb();
}
logger.debug("Checking whether instruments supports our device string");
Instruments.getAvailableDevicesWithRetry(3, function (err, availDevices) {
if (err) return cb(err);
var noDevicesError = function () {
var msg = "Could not find a device to launch. You requested '" +
dString + "', but the available devices were: " +
JSON.stringify(availDevices);
logger.error(msg);
cb(new Error(msg));
};
var dString = this.getDeviceString();
if (this.iOSSDKVersion >= 8) {
var sim = IOS.getSimForDeviceString(dString, availDevices);
if (sim[0] === null || sim[1] === null) {
return noDevicesError();
}
this.iOSSimUdid = sim[1];
logger.debug("iOS sim UDID is " + this.iOSSimUdid);
return cb();
} else if (!_.contains(availDevices, dString)) {
return noDevicesError();
}
cb();
}.bind(this));
};
IOS.prototype.setDeviceInfo = function (cb) {
this.shouldPrelaunchSimulator = false;
if (this.args.udid) {
logger.debug("Not setting device type since we're on a real device");
return cb();
}
if (!this.args.app && this.args.bundleId) {
logger.debug("Not setting device type since we're using bundle ID and " +
"assuming app is already installed");
return cb();
}
if (!this.args.deviceName &&
this.args.forceIphone === null &&
this.args.forceIpad === null) {
logger.debug("No device specified, current device in the iOS " +
"simulator will be used.");
return cb();
}
if (this.args.defaultDevice || this.iOSSDKVersion >= 7.1) {
if (this.iOSSDKVersion >= 7.1) {
logger.debug("We're on iOS7.1+ so forcing defaultDevice on");
} else {
logger.debug("User specified default device, letting instruments launch it");
}
} else {
this.shouldPrelaunchSimulator = true;
}
this.setDeviceTypeInInfoPlist(cb);
};
IOS.prototype.createSimulator = function (cb) {
this.sim = new Simulator({
platformVer: this.args.platformVersion,
sdkVer: this.iOSSDKVersion,
udid: this.iOSSimUdid
});
cb();
};
IOS.prototype.moveBuiltInApp = function (cb) {
if (this.appString().toLowerCase() === "settings") {
logger.debug("Trying to use settings app, version " +
this.args.platformVersion);
this.sim.preparePreferencesApp(this.args.tmpDir, function (err, attemptedApp, origApp) {
if (err) {
logger.error("Could not prepare settings app: " + err);
return cb(err);
}
logger.debug("Using settings app at " + attemptedApp);
this.args.app = attemptedApp;
this.args.origAppPath = origApp;
cb();
}.bind(this));
} else {
cb();
}
};
IOS.prototype.prelaunchSimulator = function (cb) {
var msg;
if (!this.shouldPrelaunchSimulator) {
logger.debug("Not pre-launching simulator");
return cb();
}
xcode.getPath(function (err, xcodePath) {
if (err) {
return cb(new Error('Could not find xcode folder. Needed to start simulator. ' + err.message));
}
logger.debug("Pre-launching simulator");
var iosSimPath = path.resolve(xcodePath,
"Platforms/iPhoneSimulator.platform/Developer/Applications" +
"/iPhone Simulator.app/Contents/MacOS/iPhone Simulator");
if (!fs.existsSync(iosSimPath)) {
msg = "Could not find ios simulator binary at " + iosSimPath;
logger.error(msg);
return cb(new Error(msg));
}
this.endSimulator(function (err) {
if (err) return cb(err);
logger.debug("Launching device: " + this.getDeviceString());
var iosSimArgs = ["-SimulateDevice", this.getDeviceString()];
this.iosSimProcess = spawn(iosSimPath, iosSimArgs);
var waitForSimulatorLogs = function (countdown) {
if (countdown <= 0 ||
(this.logs.syslog && (this.logs.syslog.getAllLogs().length > 0 ||
(this.logs.crashlog && this.logs.crashlog.getAllLogs().length > 0)))) {
logger.debug(countdown > 0 ? "Simulator is now ready." :
"Waited 10 seconds for simulator to start.");
cb();
} else {
setTimeout(function () {
waitForSimulatorLogs(countdown - 1);
}, 1000);
}
}.bind(this);
waitForSimulatorLogs(10);
}.bind(this));
}.bind(this));
};
IOS.prototype.parseLocalizableStrings = function (/* language, stringFile, cb */) {
var args = new Args(arguments);
var cb = args.callback;
if (this.args.app === null) {
logger.debug("Localizable.strings is not currently supported when using real devices.");
return cb();
}
var language = args.all[0] || this.args.language
, stringFile = args.all[1] || "Localizable.strings"
, strings = null;
if (language) {
strings = path.resolve(this.args.app, language + ".lproj", stringFile);
}
if (!fs.existsSync(strings)) {
if (language) {
logger.debug("No strings file '" + stringFile + "' for language '" + language + "', getting default strings");
}
strings = path.resolve(this.args.app, stringFile);
}
if (!fs.existsSync(strings)) {
strings = path.resolve(this.args.app, this.args.localizableStringsDir, stringFile);
}
parsePlistFile(strings, function (err, obj) {
if (err) {
logger.warn("Could not parse app " + stringFile +" assuming it " +
"doesn't exist");
} else {
logger.debug("Parsed app " + stringFile);
this.localizableStrings = obj;
}
cb();
}.bind(this));
};
IOS.prototype.deleteSim = function (cb) {
this.sim.deleteSim(cb);
};
IOS.prototype.clearAppData = function (cb) {
if (!this.keepAppToRetainPrefs && this.args.app && this.args.bundleId) {
this.sim.cleanCustomApp(path.basename(this.args.app), this.args.bundleId);
}
cb();
};
IOS.prototype.cleanupSimState = function (cb) {
if (this.realDevice && this.args.bundleId && this.args.fullReset) {
logger.debug("fullReset requested. Will try to uninstall the app.");
var bundleId = this.args.bundleId;
this.realDevice.remove(bundleId, function (err) {
if (err) {
this.removeApp(bundleId, function (err) {
if (err) {
logger.error("Could not remove " + bundleId + " from device");
cb(err);
} else {
logger.debug("Removed " + bundleId);
cb();
}
}.bind(this));
} else {
logger.debug("Removed " + bundleId);
cb();
}
}.bind(this));
} else if (!this.args.udid) {
this.sim.cleanSim(this.args.keepKeyChains, this.args.tmpDir, function (err) {
if (err) {
logger.error("Could not reset simulator. Leaving as is. Error: " + err.message);
}
this.clearAppData(cb);
}.bind(this));
} else {
logger.debug("On a real device; cannot clean device state");
cb();
}
};
IOS.prototype.runSimReset = function (cb) {
if (this.args.reset || this.args.fullReset) {
logger.debug("Running ios sim reset flow");
// The simulator process must be ended before we delete applications.
async.series([
this.endSimulator.bind(this),
function (cb) {
if (this.args.reset) {
this.cleanupSimState(cb);
} else {
cb();
}
}.bind(this),
function (cb) {
if (this.args.fullReset && !this.args.udid) {
this.deleteSim(cb);
} else {
cb();
}
}.bind(this)
], cb);
} else {
logger.debug("Reset not set, not ending sim or cleaning up app state");
cb();
}
};
IOS.prototype.isolateSimDevice = function (cb) {
if (!this.args.udid && this.args.isolateSimDevice &&
this.iOSSDKVersion >= 8) {
this.sim.deleteOtherSims(cb);
} else {
cb();
}
};
IOS.prototype.postCleanup = function (cb) {
this.curCoords = null;
this.curOrientation = null;
if (!_.isEmpty(this.logs)) {
this.logs.syslog.stopCapture();
this.logs = {};
}
if (this.remote) {
this.stopRemote();
}
this.runSimReset(function () {
// ignore any errors during reset and continue shutting down
this.isShuttingDown = false;
cb();
}.bind(this));
};
IOS.prototype.endSimulator = function (cb) {
logger.debug("Killing the simulator process");
if (this.iosSimProcess) {
this.iosSimProcess.kill("SIGHUP");
this.iosSimProcess = null;
} else {
Instruments.killAllSim();
}
this.endSimulatorDaemons(cb);
};
IOS.prototype.endSimulatorDaemons = function (cb) {
logger.debug("Killing any other simulator daemons");
var stopCmd = 'launchctl list | grep com.apple.iphonesimulator | cut -f 3 | xargs -n 1 launchctl stop';
exec(stopCmd, { maxBuffer: 524288 }, function () {
var removeCmd = 'launchctl list | grep com.apple.iphonesimulator | cut -f 3 | xargs -n 1 launchctl remove';
exec(removeCmd, { maxBuffer: 524288 }, function () {
cb();
});
});
};
IOS.prototype.stop = function (cb) {
logger.debug("Stopping ios");
if (this.instruments === null) {
logger.debug("Trying to stop instruments but it already exited");
this.postCleanup(cb);
} else {
this.commandProxy.shutdown(function (err) {
if (err) logger.warn("Got warning when trying to close command proxy:", err);
this.instruments.shutdown(function (code) {
this.shutdown(code, cb);
}.bind(this));
}.bind(this));
}
};
IOS.prototype.shutdown = function (code, cb) {
this.commandProxy = null;
this.instruments = null;
this.postCleanup(cb);
};
IOS.prototype.resetTimeout = deviceCommon.resetTimeout;
IOS.prototype.waitForCondition = deviceCommon.waitForCondition;
IOS.prototype.implicitWaitForCondition = deviceCommon.implicitWaitForCondition;
IOS.prototype.proxy = deviceCommon.proxy;
IOS.prototype.proxyWithMinTime = deviceCommon.proxyWithMinTime;
IOS.prototype.respond = deviceCommon.respond;
IOS.prototype.getSettings = deviceCommon.getSettings;
IOS.prototype.updateSettings = deviceCommon.updateSettings;
IOS.prototype.initQueue = function () {
this.queue = async.queue(function (command, cb) {
if (!this.commandProxy) return cb();
async.series([
function (cb) {
async.whilst(
function () { return this.selectingNewPage && this.curContext; }.bind(this),
function (cb) {
logger.debug("We're in the middle of selecting a new page, " +
"waiting to run next command until done");
setTimeout(cb, 100);
},
cb
);
}.bind(this),
function (cb) {
var matched = false;
var matches = ["au.alertIsPresent", "au.getAlertText", "au.acceptAlert",
"au.dismissAlert", "au.setAlertText",
"au.waitForAlertToClose"];
_.each(matches, function (match) {
if (command.indexOf(match) === 0) {
matched = true;
}
});
async.whilst(
function () { return !matched && this.curContext && this.processingRemoteCmd; }.bind(this),
function (cb) {
logger.debug("We're in the middle of processing a remote debugger " +
"command, waiting to run next command until done");
setTimeout(cb, 100);
},
cb
);
}.bind(this)
], function (err) {
if (err) return cb(err);
this.cbForCurrentCmd = cb;
if (this.commandProxy) {
this.commandProxy.sendCommand(command, function (response) {
this.cbForCurrentCmd = null;
if (typeof cb === 'function') {
this.respond(response, cb);
}
}.bind(this));
}
}.bind(this));
}.bind(this), 1);
};
IOS.prototype.push = function (elem) {
this.queue.push(elem[0], elem[1]);
};
IOS.prototype.isAppInstalled = function (bundleId, cb) {
if (this.args.udid) {
this.realDevice.isInstalled(bundleId, cb);
} else {
cb(new Error("You can not call isInstalled for the iOS simulator!"));
}
};
IOS.prototype.removeApp = function (bundleId, cb) {
if (this.args.udid) {
this.realDevice.remove(bundleId, cb);
} else {
cb(new Error("You can not call removeApp for the iOS simulator!"));
}
};
IOS.prototype.installApp = function (unzippedAppPath, cb) {
if (this.args.udid) {
this.realDevice.install(unzippedAppPath, cb);
} else {
cb(new Error("You can not call installApp for the iOS simulator!"));
}
};
IOS.prototype.unpackApp = function (req, cb) {
deviceCommon.unpackApp(req, '.app', cb);
};
IOS.prototype.startLogCapture = function (cb) {
if (!_.isEmpty(this.logs)) {
cb(new Error("Trying to start iOS log capture but it's already started!"));
return;
}
this.logs.crashlog = new iOSCrashLog();
this.logs.syslog = new iOSLog({
udid: this.args.udid
, simUdid: this.iOSSimUdid
, showLogs: this.args.showSimulatorLog || this.args.showIOSLog
});
this.logs.syslog.startCapture(function (err) {
if (err) {
logger.warn("Could not capture logs from device. Continuing without capturing logs.");
return cb();
}
this.logs.crashlog.startCapture(cb);
}.bind(this));
};
IOS.prototype.initAutoWebview = function (cb) {
if (this.args.autoWebview) {
logger.debug('Setting auto webview');
this.navToInitialWebview(cb);
} else {
cb();
}
};
IOS.prototype.getContextsAndViews = function (cb) {
this.listWebFrames(function (err, webviews) {
if (err) return cb(err);
var ctxs = [{id: this.NATIVE_WIN}];
this.contexts = [this.NATIVE_WIN];
_.each(webviews, function (view) {
ctxs.push({id: this.WEBVIEW_BASE + view.id, view: view});
this.contexts.push(view.id.toString());
}.bind(this));
cb(null, ctxs);
}.bind(this));
};
IOS.prototype.getLatestWebviewContextForTitle = function (titleRegex, cb) {
this.getContextsAndViews(function (err, contexts) {
if (err) return cb(err);
var matchingCtx;
_(contexts).each(function (ctx) {
if (ctx.view && (ctx.view.title || "").match(titleRegex)) {
if (ctx.view.url === "about:blank") {
// in the cases of Xcode < 5 (i.e., iOS SDK Version less than 7)
// iOS 7.1, iOS 9.0 & iOS 9.1 in a webview (not in Safari)
// we can have the url be `about:blank`
if (parseFloat(this.iOSSDKVersion) < 7 || parseFloat(this.iOSSDKVersion) >= 9 ||
(this.args.platformVersion === '7.1' && this.args.app && this.args.app.toLowerCase() !== 'safari')) {
matchingCtx = ctx;
}
} else {
matchingCtx = ctx;
}
}
}.bind(this));
cb(null, matchingCtx ? matchingCtx.id : undefined);
}.bind(this));
};
// Right now we don't necessarily wait for webview
// and frame to load, which leads to race conditions and flakiness
// , let see if we can transition to something better
IOS.prototype.useNewSafari = function () {
return parseFloat(this.iOSSDKVersion) >= 8.1 &&
parseFloat(this.args.platformVersion) >= 8.1 &&
!this.args.udid &&
this.capabilities.safari;
};
IOS.prototype.navToInitialWebview = function (cb) {
var timeout = 0;
if (this.args.udid) timeout = parseInt(this.iOSSDKVersion, 10) >= 8 ? 4000 : 6000;
if (timeout > 0) logger.debug('Waiting for ' + timeout + ' ms before navigating to view.');
setTimeout(function () {
if (this.useNewSafari()) {
return this.typeAndNavToUrl(cb);
} else if (parseInt(this.iOSSDKVersion, 10) >= 7 && !this.args.udid && this.capabilities.safari) {
this.navToViewThroughFavorites(cb);
} else {
this.navToViewWithTitle(/.*/, cb);
}
}.bind(this), timeout);
};
IOS.prototype.typeAndNavToUrl = function (cb) {
var initialUrl = this.args.safariInitialUrl || 'http://127.0.0.1:' + this.args.port + '/welcome';
var oldImpWait = this.implicitWaitMs;
this.implicitWaitMs = 7000;
function noArgsCb(cb) { return function (err) { cb(err); }; }
async.waterfall([
this.findElement.bind(this, 'name', 'URL'),
function (res, cb) {
this.implicitWaitMs = oldImpWait;
this.nativeTap(res.value.ELEMENT, noArgsCb(cb));
}.bind(this),
this.findElements.bind(this, 'name', 'Address'),
function (res, cb) {
var addressEl = res.value[res.value.length -1].ELEMENT;
this.setValueImmediate(addressEl, initialUrl, noArgsCb(cb));
}.bind(this),
this.findElement.bind(this, 'name', 'Go'),
function (res, cb) {
this.nativeTap(res.value.ELEMENT, noArgsCb(cb));
}.bind(this)
], function () {
this.navToViewWithTitle(/.*/i, function (err) {
if (err) return cb(err);
// Waits for page to finish loading.
this.remote.pageUnload(cb);
}.bind(this));
}.bind(this));
};
IOS.prototype.navToViewThroughFavorites = function (cb) {
logger.debug("We're on iOS7+ simulator: clicking apple button to get into " +
"a webview");
var oldImpWait = this.implicitWaitMs;
this.implicitWaitMs = 7000; // wait 7s for apple button to exist
this.findElement('xpath', '//UIAScrollView[1]/UIAButton[1]', function (err, res) {
this.implicitWaitMs = oldImpWait;
if (err || res.status !== status.codes.Success.code) {
var msg = "Could not find button to click to get into webview. " +
"Proceeding on the assumption we have a working one.";
logger.error(msg);
return this.navToViewWithTitle(/.*/i, cb);
}
this.nativeTap(res.value.ELEMENT, function (err, res) {
if (err || res.status !== status.codes.Success.code) {
var msg = "Could not click button to get into webview. " +
"Proceeding on the assumption we have a working one.";
logger.error(msg);
}
this.navToViewWithTitle(/apple/i, cb);
}.bind(this));
}.bind(this));
};
IOS.prototype.navToViewWithTitle = function (titleRegex, cb) {
logger.debug("Navigating to most recently opened webview");
var start = Date.now();
var spinTime = 500;
var spinHandles = function () {
this.getLatestWebviewContextForTitle(titleRegex, function (err, res) {
if (err) {
cb(new Error("Could not navigate to webview! Err: " + err));
} else if (!res) {
if ((Date.now() - start) < 90000) {
logger.warn("Could not find any webviews yet, refreshing/retrying");
if (this.args.udid || !this.capabilities.safari) {
return setTimeout(spinHandles, spinTime);
}
this.findUIElementOrElements('accessibility id', 'ReloadButton',
'', false, function (err, res) {
if (err || !res || !res.value || !res.value.ELEMENT) {
logger.warn("Could not find reload button, continuing");
setTimeout(spinHandles, spinTime);
} else {
this.nativeTap(res.value.ELEMENT, function (err, res) {
if (err || !res) {
logger.warn("Could not click reload button, continuing");
}
setTimeout(spinHandles, spinTime);
}.bind(this));
}
}.bind(this));
} else {
cb(new Error("Could not navigate to webview; there aren't any!"));
}
} else {
var latestWindow = res;
logger.debug("Picking webview " + latestWindow);
this.setContext(latestWindow, function (err) {
if (err) return cb(err);
this.remote.cancelPageLoad();
cb();
}.bind(this), true);
}
}.bind(this));
}.bind(this);
spinHandles();
};
_.extend(IOS.prototype, iOSHybrid);
_.extend(IOS.prototype, iOSController);
module.exports = IOS;