UNPKG

kuben-appium-xcuitest-driver

Version:

Appium driver for iOS using XCUITest for backend

295 lines (265 loc) 12.1 kB
import { fs, tempDir, plist } from 'appium-support'; import { exec } from 'teen_process'; import path from 'path'; import log from '../logger'; import _ from 'lodash'; const WDA_RUNNER_BUNDLE_ID = 'com.facebook.WebDriverAgentRunner'; const PROJECT_FILE = 'project.pbxproj'; const XCUICOORDINATE_FILE = 'PrivateHeaders/XCTest/XCUICoordinate.h'; const FBMACROS_FILE = 'WebDriverAgentLib/Utilities/FBMacros.h'; const XCUIAPPLICATION_FILE = 'PrivateHeaders/XCTest/XCUIApplication.h'; const FBSESSION_FILE = 'WebDriverAgentLib/Routing/FBSession.m'; const CARTHAGE_ROOT = 'Carthage'; async function replaceInFile (file, find, replace) { let contents = await fs.readFile(file, 'utf8'); let newContents = contents.replace(find, replace); if (newContents !== contents) { await fs.writeFile(file, newContents, 'utf8'); } } /** * Update WebDriverAgentRunner project bundle ID with newBundleId. * This method assumes project file is in the correct state. * @param {string} agentPath - Path to the .xcodeproj directory. * @param {string} newBundleId the new bundle ID used to update. */ async function updateProjectFile (agentPath, newBundleId) { let projectFilePath = `${agentPath}/${PROJECT_FILE}`; try { // Assuming projectFilePath is in the correct state, create .old from projectFilePath await fs.copyFile(projectFilePath, `${projectFilePath}.old`); await replaceInFile(projectFilePath, new RegExp(WDA_RUNNER_BUNDLE_ID.replace('.', '\.'), 'g'), newBundleId); // eslint-disable-line no-useless-escape log.debug(`Successfully updated '${projectFilePath}' with bundle id '${newBundleId}'`); } catch (err) { log.debug(`Error updating project file: ${err.message}`); log.warn(`Unable to update project file '${projectFilePath}' with ` + `bundle id '${newBundleId}'. WebDriverAgent may not start`); } } /** * Reset WebDriverAgentRunner project bundle ID to correct state. * @param {string} agentPath - Path to the .xcodeproj directory. */ async function resetProjectFile (agentPath) { let projectFilePath = `${agentPath}/${PROJECT_FILE}`; try { // restore projectFilePath from .old file if (!await fs.exists(`${projectFilePath}.old`)) { return; // no need to reset } await fs.mv(`${projectFilePath}.old`, projectFilePath); log.debug(`Successfully reset '${projectFilePath}' with bundle id '${WDA_RUNNER_BUNDLE_ID}'`); } catch (err) { log.debug(`Error resetting project file: ${err.message}`); log.warn(`Unable to reset project file '${projectFilePath}' with ` + `bundle id '${WDA_RUNNER_BUNDLE_ID}'. WebDriverAgent has been ` + `modified and not returned to the original state.`); } } async function checkForDependencies (bootstrapPath, useSsl = false) { try { let carthagePath = await fs.which('carthage'); log.debug(`Carthage found: '${carthagePath}'`); } catch (err) { log.errorAndThrow('Carthage binary is not found. Install using `brew install carthage` if it is not installed ' + 'and make sure the root folder, where carthage binary is installed, is present in PATH environment variable. ' + `The current PATH value: '${process.env.PATH ? process.env.PATH : "<not defined for the Appium process>"}'`); } const carthageRoot = path.resolve(bootstrapPath, CARTHAGE_ROOT); let areDependenciesUpdated = false; if (!await fs.hasAccess(carthageRoot)) { log.debug('Running WebDriverAgent bootstrap script to install dependencies'); try { let args = useSsl ? ['-d', '-D'] : ['-d']; await exec('Scripts/bootstrap.sh', args, {cwd: bootstrapPath}); areDependenciesUpdated = true; } catch (err) { // print out the stdout and stderr reports for (let std of ['stdout', 'stderr']) { for (let line of (err[std] || '').split('\n')) { if (!line.length) { continue; } log.error(line); } } // remove the carthage directory, or else subsequent runs will see it and // assume the dependencies are already downloaded await fs.rimraf(carthageRoot); throw err; } } if (!await fs.hasAccess(`${bootstrapPath}/Resources`)) { log.debug('Creating WebDriverAgent resources directory'); await fs.mkdir(`${bootstrapPath}/Resources`); areDependenciesUpdated = true; } if (!await fs.hasAccess(`${bootstrapPath}/Resources/WebDriverAgent.bundle`)) { log.debug('Creating WebDriverAgent resource bundle directory'); await fs.mkdir(`${bootstrapPath}/Resources/WebDriverAgent.bundle`); areDependenciesUpdated = true; } return areDependenciesUpdated; } async function setRealDeviceSecurity (keychainPath, keychainPassword) { log.debug('Setting security for iOS device'); await exec('security', ['-v', 'list-keychains', '-s', keychainPath]); await exec('security', ['-v', 'unlock-keychain', '-p', keychainPassword, keychainPath]); await exec('security', ['set-keychain-settings', '-t', '3600', '-l', keychainPath]); } async function fixXCUICoordinateFile (bootstrapPath, initial = true) { // the way the updated XCTest headers are in the WDA project, building in // Xcode 8.0 causes a duplicate declaration of method // so fix the offending line in the local headers const file = path.resolve(bootstrapPath, XCUICOORDINATE_FILE); let oldDef = '- (void)pressForDuration:(double)arg1 thenDragToCoordinate:(id)arg2;'; let newDef = '- (void)pressForDuration:(NSTimeInterval)duration thenDragToCoordinate:(XCUICoordinate *)otherCoordinate;'; if (!initial) { [oldDef, newDef] = [newDef, oldDef]; } await replaceInFile(file, oldDef, newDef); } async function fixFBSessionFile (bootstrapPath, initial = true) { const file = path.resolve(bootstrapPath, FBSESSION_FILE); let oldLine = 'return [FBApplication fb_activeApplication] ?: self.testedApplication;'; let newLine = 'FBApplication *application = [FBApplication fb_activeApplication] ?: self.testedApplication;\n' + ' return application;'; if (!initial) { [oldLine, newLine] = [newLine, oldLine]; } await replaceInFile(file, oldLine, newLine); } async function fixForXcode7 (bootstrapPath, initial = true, fixXcode9 = true) { if (fixXcode9) { await fixForXcode9(bootstrapPath, !initial, false); } await fixXCUICoordinateFile(bootstrapPath, initial); await fixFBSessionFile(bootstrapPath, initial); } async function fixFBMacrosFile (bootstrapPath, initial = true) { const file = path.resolve(bootstrapPath, FBMACROS_FILE); let oldDef = '#define FBStringify(class, property) ({if(NO){[class.new property];} @#property;})'; let newDef = '#define FBStringify(class, property) ({@#property;})'; if (!initial) { [oldDef, newDef] = [newDef, oldDef]; } await replaceInFile(file, oldDef, newDef); } async function fixXCUIApplicationFile (bootstrapPath, initial = true) { const file = path.resolve(bootstrapPath, XCUIAPPLICATION_FILE); let oldDef = '@property(nonatomic, readonly) NSUInteger state; // @synthesize state=_state;'; let newDef = '@property XCUIApplicationState state;'; if (!initial) { [oldDef, newDef] = [newDef, oldDef]; } await replaceInFile(file, oldDef, newDef); } async function fixForXcode9 (bootstrapPath, initial = true, fixXcode7 = true) { if (fixXcode7) { await fixForXcode7(bootstrapPath, !initial, false); } await fixFBMacrosFile(bootstrapPath, initial); await fixXCUIApplicationFile(bootstrapPath, initial); } async function generateXcodeConfigFile (orgId, signingId) { log.debug(`Generating xcode config file for orgId '${orgId}' and signingId ` + `'${signingId}'`); let contents = `DEVELOPMENT_TEAM = ${orgId} CODE_SIGN_IDENTITY = ${signingId} `; let xcconfigPath = await tempDir.path('appium-temp.xcconfig'); log.debug(`Writing xcode config file to ${xcconfigPath}`); await fs.writeFile(xcconfigPath, contents, "utf8"); return xcconfigPath; } /** * Creates xctestrun file per device & platform version. * We expects to have WebDriverAgentRunner_iphoneos${platformVersion}-arm64.xctestrun for real device * and WebDriverAgentRunner_iphonesimulator${platformVersion}-x86_64.xctestrun for simulator located @bootstrapPath * * @param {boolean} isRealDevice - Equals to true if the current device is a real device * @param {string} udid - The device UDID. * @param {string} platformVersion - The platform version of OS. * @param {string} bootstrapPath - The folder path containing xctestrun file. * @param {string} wdaRemotePort - The remote port WDA is listening on. * @return {string} returns xctestrunFilePath for given device * @throws if WebDriverAgentRunner_iphoneos${platformVersion}-arm64.xctestrun for real device * or WebDriverAgentRunner_iphonesimulator${platformVersion}-x86_64.xctestrun for simulator is not found @bootstrapPath, * then it will throw file not found exception */ async function setXctestrunFile (isRealDevice, udid, platformVersion, bootstrapPath, wdaRemotePort) { let xctestrunDeviceFileName = `${udid}_${platformVersion}.xctestrun`; let xctestrunFilePath = path.resolve(bootstrapPath, xctestrunDeviceFileName); if (!await fs.exists(xctestrunFilePath)) { let xctestBaseFileName = isRealDevice ? `WebDriverAgentRunner_iphoneos${platformVersion}-arm64.xctestrun` : `WebDriverAgentRunner_iphonesimulator${platformVersion}-x86_64.xctestrun`; let originalXctestrunFile = path.resolve(bootstrapPath, xctestBaseFileName); if (!await fs.exists(originalXctestrunFile)) { log.errorAndThrow(`if you are using useXctestrunFile capability then you need to have ${originalXctestrunFile} file`); } // If this is first time run for given device, then first generate xctestrun file for device. // We need to have a xctestrun file per device because we cant not have same wda port for all devices. await fs.copyFile(originalXctestrunFile, xctestrunFilePath); } let xctestRunContent = await plist.parsePlistFile(xctestrunFilePath); let updateWDAPort = { WebDriverAgentRunner: { EnvironmentVariables: { USE_PORT: wdaRemotePort } } }; let newXctestRunContent = _.merge(xctestRunContent, updateWDAPort); await plist.updatePlistFile(xctestrunFilePath, newXctestRunContent, true); return xctestrunFilePath; } async function killProcess (name, proc) { if (proc && proc.proc) { log.info(`Shutting down ${name} process (pid ${proc.proc.pid})`); try { await proc.stop('SIGTERM', 1000); } catch (err) { if (!err.message.includes(`Process didn't end after`)) { throw err; } log.debug(`${name} process did not end in a timely fashion: '${err.message}'. ` + `Sending 'SIGKILL'...`); try { await proc.stop('SIGKILL'); } catch (err) { if (err.message.includes('not currently running')) { // the process ended but for some reason we were not informed return; } throw err; } } } } /** * Generate a random integer. * * @return {number} A random integer number in range [low, hight). `low`` is inclusive and `high` is exclusive. */ function randomInt (low, high) { return Math.floor(Math.random() * (high - low) + low); } /** * Retrieves WDA upgrade timestamp * * @param {string} bootstrapPath The full path to the folder where WDA source is located * @return {?number} The UNIX timestamp of the carthage root folder, where dependencies are downloaded. * This folder is created only once on module upgrade/first install. */ async function getWDAUpgradeTimestamp (bootstrapPath) { const carthageRootPath = path.resolve(bootstrapPath, CARTHAGE_ROOT); if (await fs.exists(carthageRootPath)) { const {mtime} = await fs.stat(carthageRootPath); return mtime.getTime(); } return null; } export { updateProjectFile, resetProjectFile, checkForDependencies, setRealDeviceSecurity, fixForXcode7, fixForXcode9, generateXcodeConfigFile, setXctestrunFile, killProcess, randomInt, WDA_RUNNER_BUNDLE_ID, getWDAUpgradeTimestamp };