appium-xcuitest-driver
Version:
Appium driver for iOS using XCUITest for backend
325 lines (297 loc) • 10.6 kB
JavaScript
import {getSimulator} from 'appium-ios-simulator';
import {Simctl} from 'node-simctl';
import {resetTestProcesses} from 'appium-webdriveragent';
import _ from 'lodash';
import {util, timing} from 'appium/support';
import {UDID_AUTO, normalizePlatformName} from './utils';
import {buildSafariPreferences} from './app-utils';
const APPIUM_SIM_PREFIX = 'appiumTest';
/**
* Create a new simulator with `appiumTest-` prefix and return the object.
*
* @this {import('./driver').XCUITestDriver}
* @returns {Promise<object>} Simulator object associated with the udid passed in.
*/
export async function createSim() {
const {simulatorDevicesSetPath: devicesSetPath, deviceName, platformVersion} = this.opts;
const platform = normalizePlatformName(this.opts.platformName);
const simctl = new Simctl({devicesSetPath});
if (!deviceName) {
let deviceNames = 'none';
try {
deviceNames = (await simctl
.getDevices(platformVersion, platform))
.map(({deviceName}) => deviceName);
} catch {}
throw new Error(
`'deviceName' must be provided in order to create a new Simulator for ${platform} platform. ` +
`Currently available device names: ${deviceNames}`,
);
}
if (!platformVersion) {
throw new Error(`'platformVersion' is required.`);
}
const simName = `${APPIUM_SIM_PREFIX}-${util.uuidV4().toUpperCase()}-${deviceName}`;
this.log.debug(`Creating a temporary Simulator device '${simName}'`);
const udid = await simctl.createDevice(simName, deviceName, platformVersion, {platform});
return await getSimulator(udid, {
platform,
checkExistence: false,
devicesSetPath,
// @ts-ignore This is ok
logger: this.log,
});
}
/**
* @typedef {Object} SimulatorLookupOptions
* @property {string} [deviceName] - The name of the device to lookup
* @property {string} platformVersion - The platform version string
* @property {string} [simulatorDevicesSetPath] - The full path to the simulator devices set
* @property {string} [udid] - Simulator udid
* @property {string} [platformName] The name of the current platform
*/
/**
* Get an existing simulator matching the provided capabilities.
*
* @this {import('./driver').XCUITestDriver}
* @returns {Promise<import('./driver').Simulator|null>} The matched Simulator instance or `null` if no matching device is found.
*/
export async function getExistingSim() {
const {
platformVersion,
deviceName,
udid,
simulatorDevicesSetPath: devicesSetPath,
platformName,
} = this.opts;
const platform = normalizePlatformName(platformName);
const selectSim = async (/** @type {{ udid: string; platform: string; }} */ dev) =>
await getSimulator(dev.udid, {
platform,
checkExistence: false,
devicesSetPath,
// @ts-ignore This is ok
logger: this.log,
});
const simctl = new Simctl({devicesSetPath});
let devicesMap;
if (udid && _.toLower(udid) !== UDID_AUTO) {
this.log.debug(`Looking for an existing Simulator with UDID '${udid}'`);
devicesMap = await simctl.getDevices(null, platform);
for (const device of _.flatMap(_.values(devicesMap))) {
if (device.udid === udid) {
return await selectSim(device);
}
}
return null;
}
if (!platformVersion) {
this.log.debug(
`Provide 'platformVersion' capability if you prefer an existing Simulator to be selected`,
);
return null;
}
const devices =
devicesMap?.[platformVersion] ?? (await simctl.getDevices(platformVersion, platform));
this.log.debug(
`Looking for an existing Simulator with platformName: ${platform}, ` +
`platformVersion: ${platformVersion}, deviceName: ${deviceName}`,
);
for (const device of devices) {
if ((deviceName && device.name === deviceName) || !deviceName) {
if (!deviceName) {
this.log.debug(
`The 'deviceName' capability value is empty. ` +
`Selecting the first matching device '${device.name}' having the ` +
`'platformVersion' set to ${platformVersion}`,
);
}
return await selectSim(device);
}
}
return null;
}
/**
* @this {import('./driver').XCUITestDriver}
*/
export async function shutdownSimulator() {
const device = /** @type {import('./driver').Simulator} */ (this.device);
// stop XCTest processes if running to avoid unexpected side effects
await resetTestProcesses(device.udid, true);
await device.shutdown();
}
/**
* @this {import('./driver').XCUITestDriver}
* @property {boolean} [enforceSimulatorShutdown=false]
* @returns {Promise<void>}
*/
export async function runSimulatorReset(enforceSimulatorShutdown = false) {
const {
noReset,
fullReset,
keychainsExcludePatterns,
keepKeyChains,
bundleId,
app,
browserName,
} = this.opts;
if (noReset && !fullReset) {
// noReset === true && fullReset === false
this.log.debug('Reset: noReset is on. Leaving simulator as is');
return;
}
const device = /** @type {import('./driver').Simulator} */ (this.device);
if (!this.device) {
this.log.debug('Reset: no device available. Skipping');
return;
}
if (fullReset) {
this.log.debug('Reset: fullReset is on. Cleaning simulator');
await shutdownSimulator.bind(this)();
const isKeychainsBackupSuccessful = (keychainsExcludePatterns || keepKeyChains) && (await device.backupKeychains());
await device.clean();
if (isKeychainsBackupSuccessful) {
await device.restoreKeychains(keychainsExcludePatterns || []);
this.log.info(`Successfully restored keychains after full reset`);
} else if (keychainsExcludePatterns || keepKeyChains) {
this.log.warn(
'Cannot restore keychains after full reset, because ' +
'the backup operation did not succeed',
);
}
} else if (bundleId) {
// fastReset or noReset
// Terminate the app under test if it is still running on Simulator
try {
await device.terminateApp(bundleId);
} catch {
this.log.warn(`Reset: failed to terminate Simulator application with id "${bundleId}"`);
}
if (app) {
this.log.info('Not scrubbing third party app in anticipation of uninstall');
} else {
const isSafari = _.toLower(browserName) === 'safari';
try {
if (isSafari) {
await device.scrubSafari(true);
} else {
await device.scrubApp(bundleId);
}
} catch (err) {
this.log.debug(err.stack);
this.log.warn(err.message);
this.log.warn(
`Reset: could not scrub ${
isSafari ? 'Safari browser' : 'application with id "' + this.opts.bundleId + '"'
}. ` + `Leaving as is.`,
);
}
}
if (enforceSimulatorShutdown && (await device.isRunning())) {
await shutdownSimulator.bind(this)(device);
}
}
}
/**
* @typedef {Object} InstallOptions
*
* @property {boolean} [skipUninstall] Whether to skip app uninstall before installing it
* @property {boolean} [newSimulator=false] Whether the simulator is brand new
*/
/**
* @this {import('./driver').XCUITestDriver}
* @param {string} app The app to the path
* @param {string} [bundleId] The bundle id to ensure it is already installed and uninstall it
* @param {InstallOptions} [opts={}]
*/
export async function installToSimulator(
app,
bundleId,
opts = {},
) {
if (!app) {
this.log.debug('No app path is given. Nothing to install.');
return;
}
const {skipUninstall, newSimulator = false} = opts;
const device = /** @type {import('./driver').Simulator} */ (this.device);
if (!skipUninstall && !newSimulator && bundleId && (await device.isAppInstalled(bundleId))) {
this.log.debug(`Reset requested. Removing app with id '${bundleId}' from the device`);
await device.removeApp(bundleId);
}
this.log.debug(`Installing '${app}' on Simulator with UUID '${device.udid}'...`);
const timer = new timing.Timer().start();
await device.installApp(app);
this.log.info(`The app has been successfully installed in ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`);
}
/**
* @this {import('./driver').XCUITestDriver}
*/
export async function shutdownOtherSimulators() {
const device = /** @type {import('./driver').Simulator} */ (this.device);
const simctl = new Simctl({
devicesSetPath: device.devicesSetPath,
});
const allDevices = _.flatMap(_.values(await simctl.getDevices()));
const otherBootedDevices = allDevices
.filter(({udid, state}) => udid !== device.udid && state === 'Booted');
if (_.isEmpty(otherBootedDevices)) {
this.log.info('No other running simulators have been detected');
return;
}
this.log.info(
`Detected ${util.pluralize(
'other running Simulator',
otherBootedDevices.length,
true
)}. Shutting them down...`,
);
for (const {udid} of otherBootedDevices) {
// It is necessary to stop the corresponding xcodebuild process before killing
// the simulator, otherwise it will be automatically restarted
await resetTestProcesses(udid, true);
simctl.udid = udid;
await simctl.shutdownDevice();
}
}
/**
* Configures Safari options based on the given session capabilities
*
* @this {import('./driver').XCUITestDriver}
* @return {Promise<boolean>} true if any preferences have been updated
*/
export async function setSafariPrefs() {
const prefs = buildSafariPreferences(this.opts);
if (_.isEmpty(prefs)) {
return false;
}
this.log.debug(`About to update Safari preferences: ${JSON.stringify(prefs)}`);
await /** @type {import('./driver').Simulator} */ (this.device).updateSafariSettings(prefs);
return true;
}
/**
* Changes Simulator localization preferences
*
* @this {import('./driver').XCUITestDriver}
* @returns {Promise<boolean>} True if preferences were changed
*/
export async function setLocalizationPrefs() {
const {language, locale, calendarFormat, skipSyncUiDialogTranslation} = this.opts;
/** @type {import('appium-ios-simulator').LocalizationOptions} */
const l10nConfig = {};
if (language) {
l10nConfig.language = {name: language, skipSyncUiDialogTranslation };
}
if (locale) {
l10nConfig.locale = {name: locale};
if (calendarFormat) {
l10nConfig.locale.calendar = calendarFormat;
}
}
if (_.isEmpty(l10nConfig)) {
return false;
}
this.log.debug(`About to update localization preferences: ${JSON.stringify(l10nConfig)}`);
await /** @type {import('./driver').Simulator} */ (this.device).configureLocalization(l10nConfig);
return true;
}