UNPKG

ti-appium

Version:

An Appium wrapper to test Titanium applications

275 lines (225 loc) 8.17 kB
'use strict'; const ps = require('ps-list'), output = require('./output.js'), childProcess = require('child_process'); /** * @class Device_Helper * @desc * Devices handling class, used to make shortcuts around the launching and * shutting down of emulators. Also has functions for finding iOS certificates * and provisioning profiles. */ class Device_Helper { /** * Kill the iOS simulator using it's UDID, and then repeatedly check whether * it has fully shut down * * @param {String} simName - The name of the iOS device to find * @param {String} simVersion - The version of the iOS device to find * @param {Object} opts - Optional arguments * @param {Int} opts.initialWait - How long to wait for the first boot check * @param {Int} opts.intervalWait - How long to wait between each check following the first */ static async killSim(simName, simVersion, { initialWait = 10000, intervalWait = 5000 } = {}) { if (!simName) { throw new Error('Empty simulator name argument passed'); } if (!simVersion) { throw new Error('Empty simulator version passed'); } output.debug(`Shutting Down the iOS Simulator: ${simName} (${simVersion})`); const udid = getUdid(simName, simVersion); output.debug(`Found UDID for Simulator: ${udid}`); childProcess.execSync(`xcrun simctl shutdown ${udid}`); await isShutdown(simName, simVersion, initialWait, intervalWait); // The simulator process hangs around even after the sim itself is shut down childProcess.spawn('killall', [ 'Simulator' ]); } /** * Get the boot status of an iOS simulator via its UDID * * @param {String} simName - The name of the iOS device to find * @param {String} simVersion - The version of the iOS device to find */ static getSimState(simName, simVersion) { if (!simName) { throw new Error('Empty simulator name argument passed'); } if (!simVersion) { throw new Error('Empty simulator version passed'); } return getState(simName, simVersion); } /** * Kill all the Android emulators. * * @param {String} deviceName - The name of the Android device to kill */ static async killEmu(deviceName) { const devicePID = await getAndroidPID(deviceName); output.debug(`Found Android emulator PID: ${devicePID}`); if (process.platform === 'win32') { output.debug('Detected Windows, killing emulator with taskkill command'); await childProcess.execSync(`taskkill /F /PID ${devicePID}`); } else { output.debug('Presuming UNIX, killing emulator with kill command'); await childProcess.execSync(`kill -9 ${devicePID}`); } } /** * Use ioslib to probe the machine for a particular iOS certificate, then * return the certificate object. * * @param {String} type - The type of operation the cert is defined for * @param {String} search - Part of the cert name, used to help locate it */ static async getCert(type, search) { if (process.platform !== 'darwin') { return undefined; } const ioslib = require('ioslib'); const certs = await ioslib.certs.getCerts(), subCerts = certs[type], valid = [ 'developer', 'distribution' ]; if (!valid.includes(type)) { throw Error(`Argument '${type}' is not a valid type of certificate`); } let foundCert; subCerts.forEach(cert => { if (cert.name.includes(search)) { foundCert = cert; } }); if (!foundCert) { throw Error(`No certificate found with a name including '${search}'`); } return foundCert; } /** * Use ioslib to probe the machine for a particular iOS provisioning profile, * then return the provisioning profile object. * * @param {String} type - The type of operation the pp is defined for * @param {String} search - Part of the pp name, used to help locate it */ static async getProfile(type, search) { if (process.platform !== 'darwin') { return undefined; } const ioslib = require('ioslib'); const profiles = await ioslib.provisioning.getProvisioningProfiles(), subProfiles = profiles[type], valid = [ 'adhoc', 'development', 'distribution' ]; if (!valid.includes(type)) { throw Error(`Argument '${type}' is not a valid type of provisioning profile`); } let foundProfile; subProfiles.forEach(profile => { if (profile.name === search) { foundProfile = profile; } }); if (!foundProfile) { throw Error(`No provisioning profile found with a name matching '${search}'`); } return foundProfile; } } /** * Use the status of the simulator to determine whether the device is shutdown. The * initial wait value is used because a simulator will sometimes show as shutdown * before it really is and then change it's mind back to booted. So we use a long * wait to allow it to get past this phase * @private * * @param {String} simName - The name of the iOS device to find * @param {String} simVersion - The version of the iOS device to find * @param {Int} initialWait - How long to wait for the first boot check * @param {Int} intervalWait - How long to wait between each check following the first */ function isShutdown(simName, simVersion, initialWait, intervalWait) { return new Promise((resolve, reject) => { output.debug(`Starting Check for Simulator Shutdown, Waiting ${initialWait}ms for First Check Then Every ${intervalWait}ms`); setTimeout(() => { let count = 0; const interval = setInterval(() => { let state = getState(simName, simVersion); count++; output.debug(`${simName} (${simVersion}) is Currently ${state}`); if (state === 'Shutdown') { clearInterval(interval); return resolve(); } else if (count >= 20) { clearInterval(interval); return reject('iOS simulator didn\'t shutdown in expected time, you may expierience instability'); } }, intervalWait); }, initialWait); }); } /** * Get the boot status of an iOS simulator via its UDID * @private * * @param {String} simName - The name of the iOS device to find * @param {String} simVersion - The version of the iOS device to find */ function getState(simName, simVersion) { const versionParts = simVersion.split('.'), major = versionParts[0], minor = versionParts[1]; const udid = getUdid(simName, simVersion); const xcrunOut = childProcess.execSync('xcrun simctl list --json'); const xcrunSims = JSON.parse(xcrunOut).devices[`com.apple.CoreSimulator.SimRuntime.iOS-${major}-${minor}`]; try { for (const xcrunSim of xcrunSims) { if (xcrunSim.udid === udid) { return xcrunSim.state; } } } catch (e) { throw new Error(`An issue occured trying to get the boot status of iOS simulator "${simName} (${simVersion})", do you have this simulator configured?`); } throw new Error(`Cannot retrieve a status for simulator ${simName} (${simVersion})`); } /** * Get the UDID of an iOS simulator using the Titanium CLI * @private * * @param {String} simName - The name of the iOS device to find * @param {String} simVersion - The version of the iOS device to find */ function getUdid(simName, simVersion) { const tiOut = childProcess.execSync('ti info -t ios -o json'); const tiSims = JSON.parse(tiOut).ios.simulators.ios[simVersion]; try { for (const tiSim of tiSims) { if (tiSim.name === simName) { return tiSim.udid; } } } catch (e) { throw new Error(`An issue occured trying to find the UDID of iOS simulator "${simName} (${simVersion})", do you have this simulator configured?`); } throw new Error(`Cannot find a UDID for simulator ${simName} (${simVersion})`); } /** * Search the running processes on the system, and look for one with the * Android device name that we booted for testing. * @private * * @param {String} deviceName - The name of the Android device to find */ async function getAndroidPID(deviceName) { try { const list = await ps(); let pid; if (process.platform === 'win32') { const proc = list.find(x => x.name.includes('qemu-system-x86_64')); pid = proc.pid; } else { const proc = list.find(x => x.cmd.includes(deviceName)); pid = proc.pid; } return pid; } catch (err) { throw Error(`Cannot find an Android PID for ${deviceName}`); } } module.exports = Device_Helper;