appium-ios-simulator
Version:
iOS Simulator interface for Appium.
421 lines • 19.2 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.setReduceMotion = setReduceMotion;
exports.setReduceTransparency = setReduceTransparency;
exports.disableKeyboardIntroduction = disableKeyboardIntroduction;
exports.updateSettings = updateSettings;
exports.setAppearance = setAppearance;
exports.getAppearance = getAppearance;
exports.setIncreaseContrast = setIncreaseContrast;
exports.getIncreaseContrast = getIncreaseContrast;
exports.setContentSize = setContentSize;
exports.getContentSize = getContentSize;
exports.configureLocalization = configureLocalization;
exports.setAutoFillPasswords = setAutoFillPasswords;
exports.updatePreferences = updatePreferences;
exports.compileSimulatorPreferences = compileSimulatorPreferences;
exports.verifyDevicePreferences = verifyDevicePreferences;
const lodash_1 = __importDefault(require("lodash"));
const defaults_utils_1 = require("../defaults-utils");
const bluebird_1 = __importDefault(require("bluebird"));
const path_1 = __importDefault(require("path"));
const teen_process_1 = require("teen_process");
const async_lock_1 = __importDefault(require("async-lock"));
const support_1 = require("@appium/support");
// com.apple.SpringBoard: translates com.apple.SpringBoard and system prompts for push notification
// com.apple.locationd: translates system prompts for location
// com.apple.tccd: translates system prompts for camera, microphone, contact, photos and app tracking transparency
// com.apple.akd: translates `Sign in with your Apple ID` system prompt
const SERVICES_FOR_TRANSLATION = ['com.apple.SpringBoard', 'com.apple.locationd', 'com.apple.tccd', 'com.apple.akd'];
const GLOBAL_PREFS_PLIST = '.GlobalPreferences.plist';
const PREFERENCES_PLIST_GUARD = new async_lock_1.default();
const DOMAIN = Object.freeze({
KEYBOARD: 'com.apple.keyboard.preferences',
ACCESSIBILITY: 'com.apple.Accessibility',
});
/**
* Updates Reduce Motion setting state.
*
* @param reduceMotion Whether to enable or disable the setting.
*/
async function setReduceMotion(reduceMotion) {
return await this.updateSettings(DOMAIN.ACCESSIBILITY, {
ReduceMotionEnabled: Number(reduceMotion)
});
}
/**
* Updates Reduce Transparency setting state.
*
* @param reduceTransparency Whether to enable or disable the setting.
*/
async function setReduceTransparency(reduceTransparency) {
return await this.updateSettings(DOMAIN.ACCESSIBILITY, {
EnhancedBackgroundContrastEnabled: Number(reduceTransparency)
});
}
/**
* Disable keyboard tutorial as 'com.apple.keyboard.preferences' domain via 'defaults' command.
* @returns Promise that resolves to true if settings were updated
*/
async function disableKeyboardIntroduction() {
return await this.updateSettings(DOMAIN.KEYBOARD, {
// To disable 'DidShowContinuousPathIntroduction' for iOS 15+ simulators since changing the preference via WDA
// does not work on them. Lower than the versions also can have this preference, but nothing happen.
DidShowContinuousPathIntroduction: 1
});
}
/**
* Allows to update Simulator preferences in runtime.
*
* @param domain The name of preferences domain to be updated,
* for example, 'com.apple.Preferences' or 'com.apple.Accessibility' or
* full path to a plist file on the local file system.
* @param updates Mapping of keys/values to be updated
* @returns True if settings were actually changed
*/
async function updateSettings(domain, updates) {
if (lodash_1.default.isEmpty(updates)) {
return false;
}
const argChunks = (0, defaults_utils_1.generateDefaultsCommandArgs)(updates);
await bluebird_1.default.all(argChunks.map((args) => this.simctl.spawnProcess([
'defaults', 'write', domain, ...args
])));
return true;
}
/**
* Sets UI appearance style.
* This function can only be called on a booted simulator.
*
* @param value one of possible appearance values:
* - dark: to switch to the Dark mode
* - light: to switch to the Light mode
* @since Xcode SDK 11.4
*/
async function setAppearance(value) {
await this.simctl.setAppearance(lodash_1.default.toLower(value));
}
/**
* Gets the current UI appearance style
* This function can only be called on a booted simulator.
*
* @returns the current UI appearance style.
* Possible values are:
* - dark: to switch to the Dark mode
* - light: to switch to the Light mode
* @since Xcode SDK 11.4
*/
async function getAppearance() {
return await this.simctl.getAppearance();
}
/**
* Sets the increase contrast configuration for the given simulator.
* This function can only be called on a booted simulator.
*
* @param _value valid increase contrast configuration value.
* Acceptable value is 'enabled' or 'disabled' with Xcode 16.2.
* @since Xcode SDK 15 (but lower xcode could have this command)
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function setIncreaseContrast(_value) {
throw new Error(`Xcode SDK '${this.xcodeVersion}' is too old to set content size`);
}
/**
* Retrieves the current increase contrast configuration value from the given simulator.
* This function can only be called on a booted simulator.
*
* @returns the contrast configuration value.
* Possible return value is 'enabled', 'disabled',
* 'unsupported' or 'unknown' with Xcode 16.2.
* @since Xcode SDK 15 (but lower xcode could have this command)
*/
async function getIncreaseContrast() {
throw new Error(`Xcode SDK '${this.xcodeVersion}' is too old to get content size`);
}
/**
* Sets content size for the given simulator.
* This function can only be called on a booted simulator.
*
* @param _value valid content size or action value. Acceptable value is
* extra-small, small, medium, large, extra-large, extra-extra-large,
* extra-extra-extra-large, accessibility-medium, accessibility-large,
* accessibility-extra-large, accessibility-extra-extra-large,
* accessibility-extra-extra-extra-large with Xcode 16.2.
* @since Xcode SDK 15 (but lower xcode could have this command)
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function setContentSize(_value) {
throw new Error(`Xcode SDK '${this.xcodeVersion}' is too old to set content size`);
}
/**
* Retrieves the current content size value from the given simulator.
* This function can only be called on a booted simulator.
*
* @return the content size value. Possible return value is
* extra-small, small, medium, large, extra-large, extra-extra-large,
* extra-extra-extra-large, accessibility-medium, accessibility-large,
* accessibility-extra-large, accessibility-extra-extra-large,
* accessibility-extra-extra-extra-large,
* unknown or unsupported with Xcode 16.2.
* @since Xcode SDK 15 (but lower xcode could have this command)
*/
async function getContentSize() {
throw new Error(`Xcode SDK '${this.xcodeVersion}' is too old to get content size`);
}
/**
* Change localization settings on the currently booted simulator
*
* @param opts Localization options
* @throws {Error} If there was a failure while setting the preferences
* @returns `true` if any of settings has been successfully changed
*/
async function configureLocalization(opts = {}) {
if (lodash_1.default.isEmpty(opts)) {
return false;
}
const { language, locale, keyboard } = opts;
const globalPrefs = {};
let keyboardId = null;
if (lodash_1.default.isPlainObject(keyboard) && keyboard) {
const { name, layout, hardware } = keyboard;
if (!name) {
throw new Error(`The 'keyboard' field must have a valid name set`);
}
if (!layout) {
throw new Error(`The 'keyboard' field must have a valid layout set`);
}
keyboardId = `${name}@sw=${layout}`;
if (hardware) {
keyboardId += `;@hw=${hardware}`;
}
globalPrefs.AppleKeyboards = [keyboardId];
}
if (lodash_1.default.isPlainObject(language) && language) {
const { name } = language;
if (!name) {
throw new Error(`The 'language' field must have a valid name set`);
}
globalPrefs.AppleLanguages = [name];
}
if (lodash_1.default.isPlainObject(locale) && locale) {
const { name, calendar } = locale;
if (!name) {
throw new Error(`The 'locale' field must have a valid name set`);
}
let localeId = name;
if (calendar) {
localeId += `@calendar=${calendar}`;
}
globalPrefs.AppleLocale = localeId;
}
if (lodash_1.default.isEmpty(globalPrefs)) {
return false;
}
let previousAppleLanguages = null;
if (globalPrefs.AppleLanguages) {
const absolutePrefsPath = path_1.default.join(this.getDir(), 'Library', 'Preferences', GLOBAL_PREFS_PLIST);
try {
const { stdout } = await (0, teen_process_1.exec)('plutil', ['-convert', 'json', absolutePrefsPath, '-o', '-']);
previousAppleLanguages = JSON.parse(stdout).AppleLanguages;
}
catch (e) {
this.log.debug(`Cannot retrieve the current value of the 'AppleLanguages' preference: ${e.message}`);
}
}
const argChunks = (0, defaults_utils_1.generateDefaultsCommandArgs)(globalPrefs, true);
await bluebird_1.default.all(argChunks.map((args) => this.simctl.spawnProcess([
'defaults', 'write', GLOBAL_PREFS_PLIST, ...args
])));
if (keyboard && keyboardId) {
const argChunks = (0, defaults_utils_1.generateDefaultsCommandArgs)({
KeyboardsCurrentAndNext: [keyboardId],
KeyboardLastUsed: keyboardId,
KeyboardLastUsedForLanguage: { [keyboard.name]: keyboardId }
}, true);
await bluebird_1.default.all(argChunks.map((args) => this.simctl.spawnProcess([
'defaults', 'write', 'com.apple.Preferences', ...args
])));
}
if (globalPrefs.AppleLanguages) {
if (lodash_1.default.isEqual(previousAppleLanguages, globalPrefs.AppleLanguages)) {
this.log.info(`The 'AppleLanguages' preference is already set to '${globalPrefs.AppleLanguages}'. ` +
`Skipping services reset`);
}
else if (language?.skipSyncUiDialogTranslation) {
this.log.info('Skipping services reset as requested. This might leave some system UI alerts untranslated');
}
else {
this.log.info(`Will restart the following services in order to sync UI dialogs translation: ` +
`${SERVICES_FOR_TRANSLATION}. This might have unexpected side effects, ` +
`see https://github.com/appium/appium/issues/19440 for more details`);
await bluebird_1.default.all(SERVICES_FOR_TRANSLATION.map((arg) => this.simctl.spawnProcess([
'launchctl', 'stop', arg
])));
}
}
return true;
}
/**
* Updates Auto Fill Passwords setting state.
*
* @param isEnabled Whether to enable or disable the setting.
* @returns Promise that resolves to true if settings were updated
*/
async function setAutoFillPasswords(isEnabled) {
return await this.updateSettings('com.apple.WebUI', {
AutoFillPasswords: Number(isEnabled)
});
}
/**
* Update the common iOS Simulator preferences file with new values.
* It is necessary to restart the corresponding Simulator before
* these changes are applied.
*
* @param devicePrefs The mapping, which represents new device preference values
* for the given Simulator.
* @param commonPrefs The mapping, which represents new common preference values
* for all Simulators.
* @return True if the preferences were successfully updated.
*/
async function updatePreferences(devicePrefs = {}, commonPrefs = {}) {
if (!lodash_1.default.isEmpty(devicePrefs)) {
this.log.debug(`Setting preferences of ${this.udid} Simulator to ${JSON.stringify(devicePrefs)}`);
}
if (!lodash_1.default.isEmpty(commonPrefs)) {
this.log.debug(`Setting common Simulator preferences to ${JSON.stringify(commonPrefs)}`);
}
const homeFolderPath = process.env.HOME;
if (!homeFolderPath) {
this.log.warn(`Cannot get the path to HOME folder from the process environment. ` +
`Ignoring Simulator preferences update.`);
return false;
}
verifyDevicePreferences.bind(this)(devicePrefs);
const plistPath = path_1.default.resolve(homeFolderPath, 'Library', 'Preferences', 'com.apple.iphonesimulator.plist');
return await PREFERENCES_PLIST_GUARD.acquire(this.constructor.name, async () => {
const defaults = new defaults_utils_1.NSUserDefaults(plistPath);
const prefsToUpdate = lodash_1.default.clone(commonPrefs);
try {
if (!lodash_1.default.isEmpty(devicePrefs)) {
let existingDevicePrefs;
const udidKey = this.udid.toUpperCase();
if (await support_1.fs.exists(plistPath)) {
const currentPlistContent = await defaults.asJson();
if (lodash_1.default.isPlainObject(currentPlistContent.DevicePreferences)
&& lodash_1.default.isPlainObject(currentPlistContent.DevicePreferences[udidKey])) {
existingDevicePrefs = currentPlistContent.DevicePreferences[udidKey];
}
}
Object.assign(prefsToUpdate, {
DevicePreferences: {
[udidKey]: Object.assign({}, existingDevicePrefs || {}, devicePrefs)
}
});
}
await defaults.update(prefsToUpdate);
this.log.debug(`Updated ${this.udid} Simulator preferences at '${plistPath}' with ` +
JSON.stringify(prefsToUpdate));
return true;
}
catch (e) {
this.log.warn(`Cannot update ${this.udid} Simulator preferences at '${plistPath}'. ` +
`Try to delete the file manually in order to reset it. Original error: ${e.message}`);
return false;
}
});
}
/**
* Creates device and common Simulator preferences, which could
* be later applied using `defaults` CLI utility.
*
* @param opts Run options
* @returns The first array item is the resulting device preferences
* object and the second one is common preferences object
*/
function compileSimulatorPreferences(opts = {}) {
const { connectHardwareKeyboard, tracePointer, pasteboardAutomaticSync, scaleFactor, } = opts;
const commonPreferences = {
// This option is necessary to make the Simulator window follow
// the actual XCUIDevice orientation
RotateWindowWhenSignaledByGuest: true,
// https://github.com/appium/appium/issues/16418
StartLastDeviceOnLaunch: false,
DetachOnWindowClose: false,
AttachBootedOnStart: true,
};
const devicePreferences = opts.devicePreferences ? lodash_1.default.cloneDeep(opts.devicePreferences) : {};
if (scaleFactor) {
devicePreferences.SimulatorWindowLastScale = parseFloat(scaleFactor);
}
if (lodash_1.default.isBoolean(connectHardwareKeyboard) || lodash_1.default.isNil(connectHardwareKeyboard)) {
devicePreferences.ConnectHardwareKeyboard = connectHardwareKeyboard ?? false;
commonPreferences.ConnectHardwareKeyboard = connectHardwareKeyboard ?? false;
}
if (lodash_1.default.isBoolean(tracePointer)) {
commonPreferences.ShowSingleTouches = tracePointer;
commonPreferences.ShowPinches = tracePointer;
commonPreferences.ShowPinchPivotPoint = tracePointer;
commonPreferences.HighlightEdgeGestures = tracePointer;
}
switch (lodash_1.default.lowerCase(pasteboardAutomaticSync || '')) {
case 'on':
commonPreferences.PasteboardAutomaticSync = true;
break;
case 'off':
// Improve launching simulator performance
// https://github.com/WebKit/webkit/blob/master/Tools/Scripts/webkitpy/xcode/simulated_device.py#L413
commonPreferences.PasteboardAutomaticSync = false;
break;
case 'system':
// Do not add -PasteboardAutomaticSync
break;
default:
this.log.info(`['on', 'off' or 'system'] are available as the pasteboard automatic sync option. Defaulting to 'off'`);
commonPreferences.PasteboardAutomaticSync = false;
}
return [devicePreferences, commonPreferences];
}
/**
* Perform verification of device preferences correctness.
*
* @param prefs The preferences to be verified
* @returns void
* @throws {Error} If any of the given preference values does not match the expected
* format.
*/
function verifyDevicePreferences(prefs = {}) {
if (lodash_1.default.isEmpty(prefs)) {
return;
}
if (!lodash_1.default.isUndefined(prefs.SimulatorWindowLastScale)) {
if (!lodash_1.default.isNumber(prefs.SimulatorWindowLastScale) || prefs.SimulatorWindowLastScale <= 0) {
throw this.log.errorWithException(`SimulatorWindowLastScale is expected to be a positive float value. ` +
`'${prefs.SimulatorWindowLastScale}' is assigned instead.`);
}
}
if (!lodash_1.default.isUndefined(prefs.SimulatorWindowCenter)) {
// https://regex101.com/r/2ZXOij/2
const verificationPattern = /{-?\d+(\.\d+)?,-?\d+(\.\d+)?}/;
if (!lodash_1.default.isString(prefs.SimulatorWindowCenter) || !verificationPattern.test(prefs.SimulatorWindowCenter)) {
throw this.log.errorWithException(`SimulatorWindowCenter is expected to match "{floatXPosition,floatYPosition}" format (without spaces). ` +
`'${prefs.SimulatorWindowCenter}' is assigned instead.`);
}
}
if (!lodash_1.default.isUndefined(prefs.SimulatorWindowOrientation)) {
const acceptableValues = ['Portrait', 'LandscapeLeft', 'PortraitUpsideDown', 'LandscapeRight'];
if (!prefs.SimulatorWindowOrientation || !acceptableValues.includes(prefs.SimulatorWindowOrientation)) {
throw this.log.errorWithException(`SimulatorWindowOrientation is expected to be one of ${acceptableValues}. ` +
`'${prefs.SimulatorWindowOrientation}' is assigned instead.`);
}
}
if (!lodash_1.default.isUndefined(prefs.SimulatorWindowRotationAngle)) {
if (!lodash_1.default.isNumber(prefs.SimulatorWindowRotationAngle)) {
throw this.log.errorWithException(`SimulatorWindowRotationAngle is expected to be a valid number. ` +
`'${prefs.SimulatorWindowRotationAngle}' is assigned instead.`);
}
}
}
//# sourceMappingURL=settings.js.map