Skip to content

Commit

Permalink
feat: Drop obsolete Simulator versions (#420)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Dropped obsolete simulator classes: SimulatorXcode8, SimulatorXcode9 and SimulatorXcode9_3. APIs that are still relevant have been moved to SimulatorXcode10
BREAKING CHANGE: Added proper type definitions. Interfaces were refactored and connected to appropriate extension classes.

Now it is possible to provide a logger to the factory method, which improves the visibility of session identifiers
  • Loading branch information
mykola-mokhnach authored Mar 26, 2024
1 parent 723f16f commit bff7e56
Show file tree
Hide file tree
Showing 27 changed files with 1,691 additions and 1,846 deletions.
13 changes: 3 additions & 10 deletions lib/defaults-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import log from './logger';
* the given value
* @throws {TypeError} If it is not known how to serialize the given value
*/
function toXmlArg (value, serialize = true) {
export function toXmlArg (value, serialize = true) {
let xmlDoc = null;

if (_.isPlainObject(value)) {
Expand Down Expand Up @@ -72,7 +72,7 @@ function toXmlArg (value, serialize = true) {
* @returns {string[][]} Each item in the array
* is the `defaults write <plist>` command suffix
*/
function generateDefaultsCommandArgs (valuesMap, replace = false) {
export function generateDefaultsCommandArgs (valuesMap, replace = false) {
/** @type {string[][]} */
const resultArgs = [];
for (const [key, value] of _.toPairs(valuesMap)) {
Expand Down Expand Up @@ -106,8 +106,7 @@ function generateDefaultsCommandArgs (valuesMap, replace = false) {
return resultArgs;
}


class NSUserDefaults {
export class NSUserDefaults {
constructor (plist) {
this.plist = plist;
}
Expand Down Expand Up @@ -155,9 +154,3 @@ class NSUserDefaults {
}
}
}


export {
NSUserDefaults,
toXmlArg, generateDefaultsCommandArgs,
};
2 changes: 1 addition & 1 deletion lib/device-utils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Simctl from 'node-simctl';

/**
* @param {Record<string, any>} [simctlOpts]
* @param {import('@appium/types').StringRecord} [simctlOpts]
* @returns {Promise<any[]>}
*/
export async function getDevices(simctlOpts) {
Expand Down
71 changes: 36 additions & 35 deletions lib/extensions/applications.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,27 @@
import _ from 'lodash';
import path from 'path';
import { fs, plist, util } from '@appium/support';
import log from '../logger';
import B from 'bluebird';
import { waitForCondition } from 'asyncbox';

const extensions = {};

/**
* Install valid .app package on Simulator.
*
* @this {CoreSimulatorWithApps}
* @param {string} app - The path to the .app package.
*/
extensions.installApp = async function installApp (app) {
export async function installApp (app) {
return await this.simctl.installApp(app);
};
}

/**
* Returns user installed bundle ids which has 'bundleName' in their Info.Plist as 'CFBundleName'
*
* @this {CoreSimulatorWithApps}
* @param {string} bundleName - The bundle name of the application to be checked.
* @return {Promise<string[]>} - The list of bundle ids which have 'bundleName'
*/
extensions.getUserInstalledBundleIdsByBundleName = async function getUserInstalledBundleIdsByBundleName (bundleName) {
export async function getUserInstalledBundleIdsByBundleName (bundleName) {
const appsRoot = path.resolve(this.getDir(), 'Containers', 'Bundle', 'Application');
// glob all Info.plist from simdir/data/Containers/Bundle/Application
const infoPlists = await fs.glob('*/*.app/Info.plist', {
Expand All @@ -49,60 +48,57 @@ extensions.getUserInstalledBundleIdsByBundleName = async function getUserInstall
return [];
}

log.debug(
this.log.debug(
`The simulator has ${util.pluralize('bundle', bundleIds.length, true)} which ` +
`have '${bundleName}' as their 'CFBundleName': ${JSON.stringify(bundleIds)}`
);
return bundleIds;
};
}

/**
* Verify whether the particular application is installed on Simulator.
*
* @this {CoreSimulatorWithApps}
* @param {string} bundleId - The bundle id of the application to be checked.
* @return {Promise<boolean>} True if the given application is installed.
*/
extensions.isAppInstalled = async function isAppInstalled (bundleId) {
export async function isAppInstalled (bundleId) {
try {
const appContainer = await this.simctl.getAppContainer(bundleId);
return appContainer.endsWith('.app');
if (!appContainer.endsWith('.app')) {
return false;
}
return await fs.exists(appContainer);
} catch (err) {
// get_app_container subcommand fails for system applications,
// so we try the hidden appinfo subcommand, which prints correct info for
// system/hidden apps
try {
const info = await this.simctl.appInfo(bundleId);
return info.includes('ApplicationType');
} catch (e) {
return false;
}
} catch (ign) {}
}
};
return false;
}

/**
* Uninstall the given application from the current Simulator.
*
* @this {CoreSimulatorWithApps}
* @param {string} bundleId - The buindle ID of the application to be removed.
*/
extensions.removeApp = async function removeApp (bundleId) {
export async function removeApp (bundleId) {
await this.simctl.removeApp(bundleId);
};

/**
* @typedef {Object} LaunchAppOpts
* @property {boolean} wait [false] Whether to wait until the app has fully started and
* is present in processes list
* @property {number} timeoutMs [10000] The number of milliseconds to wait until
* the app is fully started. Only applicatble if `wait` is true.
*/
}

/**
* Starts the given application on Simulator
*
* @this {CoreSimulatorWithApps}
* @param {string} bundleId - The buindle ID of the application to be launched
* @param {Partial<LaunchAppOpts>} opts
* @param {import('../types').LaunchAppOptions} [opts={}]
*/
extensions.launchApp = async function launchApp (bundleId, opts = {}) {
export async function launchApp (bundleId, opts = {}) {
await this.simctl.launchApp(bundleId);
const {
wait = false,
Expand All @@ -120,41 +116,44 @@ extensions.launchApp = async function launchApp (bundleId, opts = {}) {
} catch (e) {
throw new Error(`App '${bundleId}' is not runnning after ${timeoutMs}ms timeout.`);
}
};
}

/**
* Stops the given application on Simulator.
*
* @this {CoreSimulatorWithApps}
* @param {string} bundleId - The buindle ID of the application to be stopped
*/
extensions.terminateApp = async function terminateApp (bundleId) {
export async function terminateApp (bundleId) {
await this.simctl.terminateApp(bundleId);
};
}

/**
* Check if app with the given identifier is running.
*
* @this {CoreSimulatorWithApps}
* @param {string} bundleId - The buindle ID of the application to be checked.
*/
extensions.isAppRunning = async function isAppRunning (bundleId) {
export async function isAppRunning (bundleId) {
return (await this.ps()).some(({name}) => name === bundleId);
};
}

/**
* Scrub (delete the preferences and changed files) the particular application on Simulator.
* The app will be terminated automatically if it is running.
*
* @this {CoreSimulatorWithApps}
* @param {string} bundleId - Bundle identifier of the application.
* @throws {Error} if the given app is not installed.
*/
extensions.scrubApp = async function scrubApp (bundleId) {
export async function scrubApp (bundleId) {
const appDataRoot = await this.simctl.getAppContainer(bundleId, 'data');
const appFiles = await fs.glob('**/*', {
cwd: appDataRoot,
nodir: true,
absolute: true,
});
log.info(`Found ${appFiles.length} ${bundleId} app ${util.pluralize('file', appFiles.length, false)} to scrub`);
this.log.info(`Found ${appFiles.length} ${bundleId} app ${util.pluralize('file', appFiles.length, false)} to scrub`);
if (_.isEmpty(appFiles)) {
return;
}
Expand All @@ -163,6 +162,8 @@ extensions.scrubApp = async function scrubApp (bundleId) {
await this.terminateApp(bundleId);
} catch (ign) {}
await B.all(appFiles.map((p) => fs.rimraf(p)));
};
}

export default extensions;
/**
* @typedef {import('../types').CoreSimulator & import('../types').InteractsWithApps} CoreSimulatorWithApps
*/
114 changes: 66 additions & 48 deletions lib/extensions/biometric.js
Original file line number Diff line number Diff line change
@@ -1,62 +1,80 @@
import _ from 'lodash';
import log from '../logger';

const extensions = {};
const ENROLLMENT_NOTIFICATION_RECEIVER = 'com.apple.BiometricKit.enrollmentChanged';
const BIOMETRICS = {
touchId: 'fingerTouch',
faceId: 'pearl',
};

/**
* Get the current state of Biometric Enrollment feature.
*
* @returns {Promise<boolean>} Either true or false
* @throws {Error} If Enrollment state cannot be determined
* @this {CoreSimulatorWithBiometric}
* @returns {Promise<boolean>}
*/
extensions.isBiometricEnrolled = async function isBiometricEnrolled () {
const output = await this.executeUIClientScript(`
tell application "System Events"
tell process "Simulator"
set dstMenuItem to menu item "Toggle Enrolled State" of menu 1 of menu item "Touch ID" of menu 1 of menu bar item "Hardware" of menu bar 1
set isChecked to (value of attribute "AXMenuItemMarkChar" of dstMenuItem) is "✓"
end tell
end tell
`);
log.debug(`Touch ID enrolled state: ${output}`);
return _.isString(output) && output.trim() === 'true';
};
export async function isBiometricEnrolled () {
const {stdout} = await this.simctl.spawnProcess([
'notifyutil',
'-g', ENROLLMENT_NOTIFICATION_RECEIVER
]);
const match = (new RegExp(`${_.escapeRegExp(ENROLLMENT_NOTIFICATION_RECEIVER)}\\s+([01])`))
.exec(stdout);
if (!match) {
throw new Error(`Cannot parse biometric enrollment state from '${stdout}'`);
}
this.log.info(`Current biometric enrolled state for ${this.udid} Simulator: ${match[1]}`);
return match[1] === '1';
}

/**
* Enrolls biometric (TouchId, FaceId) feature testing in Simulator UI client.
*
* @param {boolean} isEnabled - Defines whether biometric state is enabled/disabled
* @throws {Error} If the enrolled state cannot be changed
* @this {CoreSimulatorWithBiometric}
* @param {boolean} isEnabled
*/
extensions.enrollBiometric = async function enrollBiometric (isEnabled = true) {
await this.executeUIClientScript(`
tell application "System Events"
tell process "Simulator"
set dstMenuItem to menu item "Toggle Enrolled State" of menu 1 of menu item "Touch ID" of menu 1 of menu bar item "Hardware" of menu bar 1
set isChecked to (value of attribute "AXMenuItemMarkChar" of dstMenuItem) is "✓"
if ${isEnabled ? 'not ' : ''}isChecked then
click dstMenuItem
end if
end tell
end tell
`);
};
export async function enrollBiometric (isEnabled = true) {
this.log.debug(`Setting biometric enrolled state for ${this.udid} Simulator to '${isEnabled ? 'enabled' : 'disabled'}'`);
await this.simctl.spawnProcess([
'notifyutil',
'-s', ENROLLMENT_NOTIFICATION_RECEIVER, isEnabled ? '1' : '0'
]);
await this.simctl.spawnProcess([
'notifyutil',
'-p', ENROLLMENT_NOTIFICATION_RECEIVER
]);
if (await this.isBiometricEnrolled() !== isEnabled) {
throw new Error(`Cannot set biometric enrolled state for ${this.udid} Simulator to '${isEnabled ? 'enabled' : 'disabled'}'`);
}
}

/**
* Sends a notification to match/not match the touch id.
* Sends a notification to match/not match the particular biometric.
*
* @param {?boolean} shouldMatch [true] - Set it to true or false in order to emulate
* @this {CoreSimulatorWithBiometric}
* @param {boolean} shouldMatch [true] - Set it to true or false in order to emulate
* matching/not matching the corresponding biometric
* @param {string} biometricName [touchId] - Either touchId or faceId (faceId is only available since iOS 11)
*/
extensions.sendBiometricMatch = async function sendBiometricMatch (shouldMatch = true) {
await this.executeUIClientScript(`
tell application "System Events"
tell process "Simulator"
set dstMenuItem to menu item "${shouldMatch ? 'Matching Touch' : 'Non-matching Touch'}" of menu 1 of menu item "Touch ID" of menu 1 of menu bar item "Hardware" of menu bar 1
click dstMenuItem
end tell
end tell
`);
};
export async function sendBiometricMatch (shouldMatch = true, biometricName = 'touchId') {
const domainComponent = toBiometricDomainComponent(biometricName);
const domain = `com.apple.BiometricKit_Sim.${domainComponent}.${shouldMatch ? '' : 'no'}match`;
await this.simctl.spawnProcess([
'notifyutil',
'-p', domain
]);
this.log.info(
`Sent notification ${domain} to ${shouldMatch ? 'match' : 'not match'} ${biometricName} biometric ` +
`for ${this.udid} Simulator`
);
}

export default extensions;
/**
* @param {string} name
* @returns {string}
*/
export function toBiometricDomainComponent (name) {
if (!BIOMETRICS[name]) {
throw new Error(`'${name}' is not a valid biometric. Use one of: ${JSON.stringify(_.keys(BIOMETRICS))}`);
}
return BIOMETRICS[name];
}

/**
* @typedef {import('../types').CoreSimulator & import('../types').SupportsBiometric} CoreSimulatorWithBiometric
*/
Loading

0 comments on commit bff7e56

Please sign in to comment.