From 92148aae6088297162a884516ed8a471b3eeb113 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Wed, 27 Mar 2024 07:02:40 +0100 Subject: [PATCH] feat: Add the `device` property to the driver (#2364) --- lib/commands/app-management.js | 29 ++-- lib/commands/appearance.js | 8 +- lib/commands/biometric.js | 9 +- lib/commands/certificate.js | 8 +- lib/commands/condition.js | 6 +- lib/commands/context.js | 3 +- lib/commands/deviceInfo.js | 3 +- lib/commands/file-movement.js | 36 ++--- lib/commands/general.js | 3 +- lib/commands/gesture.js | 3 +- lib/commands/keychains.js | 4 +- lib/commands/localization.js | 5 +- lib/commands/location.js | 3 +- lib/commands/log.js | 6 +- lib/commands/memory.js | 4 +- lib/commands/notifications.js | 3 +- lib/commands/pasteboard.js | 8 +- lib/commands/pcap.js | 3 +- lib/commands/performance.js | 12 +- lib/commands/permissions.js | 10 +- lib/commands/recordscreen.js | 3 +- lib/commands/screenshots.js | 11 +- lib/commands/web.js | 3 +- lib/commands/xctest-record-screen.js | 6 +- lib/commands/xctest.js | 6 +- lib/driver.js | 157 ++++++++++---------- lib/real-device-management.js | 81 +++++----- lib/real-device.js | 64 +++++--- lib/simulator-management.js | 165 +++++++++++---------- package.json | 2 +- test/functional/driver/driver-e2e-specs.js | 13 +- test/functional/helpers/simulator.js | 21 ++- test/functional/tv/tvos-e2e-specs.js | 5 +- test/unit/driver-specs.js | 6 - test/unit/real-device-management-specs.js | 45 +++++- test/unit/simulator-management-specs.js | 38 +++-- 36 files changed, 412 insertions(+), 380 deletions(-) diff --git a/lib/commands/app-management.js b/lib/commands/app-management.js index ed5f72597..c5888a04d 100644 --- a/lib/commands/app-management.js +++ b/lib/commands/app-management.js @@ -23,8 +23,7 @@ export default { const srcAppPath = await this.helpers.configureApp(app, '.app'); this.log.info( `Installing '${srcAppPath}' to the ${this.isRealDevice() ? 'real device' : 'Simulator'} ` + - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - `with UDID '${this.opts.device.udid}'`, + `with UDID '${this.device.udid}'`, ); if (!(await fs.exists(srcAppPath))) { throw this.log.errorWithException( @@ -36,7 +35,7 @@ export default { const bundleId = await extractBundleId(srcAppPath); const {install} = await this.checkAutInstallationState( // @ts-expect-error - do not assign arbitrary properties to `this.opts` - {enforceAppInstall: false, fullReset: false, noReset: false, bundleId, device: this.opts.device, app: srcAppPath} + {enforceAppInstall: false, fullReset: false, noReset: false, bundleId, device: this.device, app: srcAppPath} ); if (!install) { @@ -45,8 +44,7 @@ export default { } } - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await this.opts.device.installApp( + await this.device.installApp( srcAppPath, timeoutMs ?? this.opts.appPushTimeout, strategy ?? this.opts.appInstallStrategy, @@ -62,8 +60,7 @@ export default { * @this {XCUITestDriver} */ async mobileIsAppInstalled(bundleId) { - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - const installed = await this.opts.device.isAppInstalled(bundleId); + const installed = await this.device.isAppInstalled(bundleId); this.log.info(`App '${bundleId}' is${installed ? '' : ' not'} installed`); return installed; }, @@ -78,14 +75,10 @@ export default { async mobileRemoveApp(bundleId) { this.log.info( `Uninstalling the application with bundle identifier '${bundleId}' ` + - `from the ${this.isRealDevice() ? 'real device' : 'Simulator'} with UDID '${ - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - this.opts.device.udid - }'`, + `from the ${this.isRealDevice() ? 'real device' : 'Simulator'} with UDID '${this.device.udid}'`, ); try { - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await this.opts.device.removeApp(bundleId); + await this.device.removeApp(bundleId); this.log.info(`Removal of '${bundleId}' succeeded`); return true; } catch (err) { @@ -158,8 +151,9 @@ export default { throw new errors.UnsupportedOperationError('A real device is required'); } - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - return await this.opts.device.terminateApp(bundleId, this.opts.platformVersion); + return await /** @type {import('../real-device').RealDevice} */ (this.device).terminateApp( + bundleId, String(this.opts.platformVersion) + ); }, /** @@ -232,7 +226,7 @@ export default { throw new errors.NotImplementedError(`This extension is only supported on real devices`); } - const service = await services.startInstallationProxyService(this.opts.device.udid); + const service = await services.startInstallationProxyService(this.device.udid); try { return await service.listApplications({applicationType}); } finally { @@ -258,8 +252,7 @@ export default { ); } - // @ts-ignore This opt must exist - const simctl = this.opts.device.simctl; + const simctl = /** @type {import('../driver').Simulator} */ (this.device).simctl; const dataRoot = await simctl.getAppContainer(bundleId, 'data'); this.log.debug(`Got the data container root of ${bundleId} at '${dataRoot}'`); if (!await fs.exists(dataRoot)) { diff --git a/lib/commands/appearance.js b/lib/commands/appearance.js index a588c8e09..85356be6b 100644 --- a/lib/commands/appearance.js +++ b/lib/commands/appearance.js @@ -21,8 +21,7 @@ export default { if (this.isSimulator()) { try { - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - return void (await this.opts.device.setAppearance(style)); + return void (await /** @type {import('../driver').Simulator} */ (this.device).setAppearance(style)); } catch (e) { this.log.debug(e.stack); } @@ -53,8 +52,9 @@ export default { let style; if (this.isSimulator()) { try { - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - style = await this.opts.device.getAppearance(); + style = /** @type {Style} */ ( + await /** @type {import('../driver').Simulator} */ (this.device).getAppearance() + ); } catch (ign) {} } if (!style) { diff --git a/lib/commands/biometric.js b/lib/commands/biometric.js index bd0708b35..ef581f5f1 100644 --- a/lib/commands/biometric.js +++ b/lib/commands/biometric.js @@ -15,8 +15,7 @@ export default { async mobileEnrollBiometric(isEnabled = true) { assertSimulator(this); - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await this.opts.device.enrollBiometric(isEnabled); + await /** @type {import('../driver').Simulator} */ (this.device).enrollBiometric(isEnabled); }, /** @@ -33,8 +32,7 @@ export default { async mobileSendBiometricMatch(type = 'touchId', match = true) { assertSimulator(this); - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await this.opts.device.sendBiometricMatch(match, type); + await /** @type {import('../driver').Simulator} */ (this.device).sendBiometricMatch(match, type); }, /** @@ -48,8 +46,7 @@ export default { async mobileIsBiometricEnrolled() { assertSimulator(this); - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - return await this.opts.device.isBiometricEnrolled(); + return await /** @type {import('../driver').Simulator} */ (this.device).isBiometricEnrolled(); }, }; diff --git a/lib/commands/certificate.js b/lib/commands/certificate.js index 177feb17a..2f427a990 100644 --- a/lib/commands/certificate.js +++ b/lib/commands/certificate.js @@ -295,7 +295,7 @@ export default { * @param {string} [commonName] - Common name of the certificate. If this is not set, the command will try to parse it from the provided `content`. * @param {boolean} isRoot - Defines where the certificate should be installed; either the Trusted Root Store (`true`) or the Keychain (`false`). On environments other than Xcode 11.4+ Simulator, this option is ignored. * @returns {Promise} The content of the generated `.mobileconfig` file as - a base64-encoded string. This config might be useful for debugging purposes. If the certificate has been successfully set via CLI, then nothing is returned. + * a base64-encoded string. This config might be useful for debugging purposes. If the certificate has been successfully set via CLI, then nothing is returned. * @this {XCUITestDriver} */ async mobileInstallCertificate(content, commonName, isRoot = true) { @@ -306,8 +306,7 @@ export default { if (this.isSimulator()) { try { const methodName = isRoot ? 'addRootCertificate' : 'addCertificate'; - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await this.opts.device.simctl[methodName](Buffer.from(content, 'base64').toString(), { + await /** @type {import('../driver').Simulator} */ (this.device).simctl[methodName](Buffer.from(content, 'base64').toString(), { raw: true, }); return; @@ -387,8 +386,7 @@ export default { } } } else { - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await this.opts.device.openUrl(certUrl); + await /** @type {import('../driver').Simulator} */ (this.device).openUrl(certUrl); } let isCertAlreadyInstalled = false; diff --git a/lib/commands/condition.js b/lib/commands/condition.js index 515bf5065..124985f4a 100644 --- a/lib/commands/condition.js +++ b/lib/commands/condition.js @@ -63,8 +63,7 @@ export default { */ async listConditionInducers() { requireConditionInducerCompatibleDevice(this); - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - const conditionInducerService = await services.startInstrumentService(this.opts.device.udid); + const conditionInducerService = await services.startInstrumentService(this.device.udid); try { const ret = await conditionInducerService.callChannel( INSTRUMENT_CHANNEL.CONDITION_INDUCER, @@ -100,8 +99,7 @@ export default { if (this._conditionInducerService && !this._conditionInducerService._socketClient.destroyed) { this.log.errorAndThrow(`Condition inducer has been started. A condition is already active.`); } - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - this._conditionInducerService = await services.startInstrumentService(this.opts.device.udid); + this._conditionInducerService = await services.startInstrumentService(this.device.udid); const ret = await this._conditionInducerService.callChannel( INSTRUMENT_CHANNEL.CONDITION_INDUCER, 'enableConditionWithIdentifier:profileIdentifier:', diff --git a/lib/commands/context.js b/lib/commands/context.js index 2b10e6c8f..3b61465f0 100644 --- a/lib/commands/context.js +++ b/lib/commands/context.js @@ -439,8 +439,7 @@ const helpers = { async getNewRemoteDebugger() { let socketPath; if (!this.isRealDevice()) { - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - socketPath = await this.opts.device.getWebInspectorSocket(); + socketPath = await /** @type {import('../driver').Simulator} */ (this.device).getWebInspectorSocket(); } return createRemoteDebugger( { diff --git a/lib/commands/deviceInfo.js b/lib/commands/deviceInfo.js index 66e1e7c76..295cd997c 100644 --- a/lib/commands/deviceInfo.js +++ b/lib/commands/deviceInfo.js @@ -14,8 +14,7 @@ export default { ); if (this.isRealDevice()) { - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - const lockdownInfo = await utilities.getDeviceInfo(this.opts.device.udid); + const lockdownInfo = await utilities.getDeviceInfo(this.device.udid); return {...infoByWda, ...{lockdownInfo}}; } diff --git a/lib/commands/file-movement.js b/lib/commands/file-movement.js index 066f0b5c8..c9e64289a 100644 --- a/lib/commands/file-movement.js +++ b/lib/commands/file-movement.js @@ -93,7 +93,7 @@ async function createService(udid, remotePath) { /** * Save the given base64 data chunk as a binary file on the Simulator under test. * - * @param {Object} device - The device object, which represents the device under test. + * @param {import('../driver').Simulator} device - The device object, which represents the device under test. * This object is expected to have the `udid` property containing the * valid device ID. * @param {string} remotePath - The remote path on the device. This variable can be prefixed with @@ -140,7 +140,7 @@ async function pushFileToSimulator(device, remotePath, base64Data) { /** * Save the given base64 data chunk as a binary file on the device under test. * - * @param {Object} device - The device object, which represents the device under test. + * @param {import('../real-device').RealDevice} device - The device object, which represents the device under test. * This object is expected to have the `udid` property containing the * valid device ID. * @param {string} remotePath - The remote path on the device. This variable can be prefixed with @@ -179,7 +179,7 @@ async function deleteFileOrFolder(device, remotePath, isSimulator) { * Get the content of given file or folder from iOS Simulator and return it as base-64 encoded string. * Folder content is recursively packed into a zip archive. * - * @param {Object} device - The device object, which represents the device under test. + * @param {import('../driver').Simulator} device - The device object, which represents the device under test. * This object is expected to have the `udid` property containing the * valid device ID. * @param {string} remotePath - The path to a file or a folder, which exists in the corresponding application @@ -225,7 +225,7 @@ async function pullFromSimulator(device, remotePath, isFile) { * Get the content of given file or folder from the real device under test and return it as base-64 encoded string. * Folder content is recursively packed into a zip archive. * - * @param {Object} device - The device object, which represents the device under test. + * @param {import('../real-device').RealDevice} device - The device object, which represents the device under test. * This object is expected to have the `udid` property containing the * valid device ID. * @param {string} remotePath - The path to an existing remote file on the device. This variable can be prefixed with @@ -266,7 +266,7 @@ async function pullFromRealDevice(device, remotePath, isFile) { /** * Remove the file or folder from the device * - * @param {Object} device - The device object, which represents the device under test. + * @param {import('../driver').Simulator} device - The device object, which represents the device under test. * This object is expected to have the `udid` property containing the * valid device ID. * @param {string} remotePath - The path to a file or a folder, which exists in the corresponding application @@ -304,7 +304,7 @@ async function deleteFromSimulator(device, remotePath) { /** * Remove the file or folder from the device * - * @param {Object} device - The device object, which represents the device under test. + * @param {import('../real-device').RealDevice} device - The device object, which represents the device under test. * This object is expected to have the `udid` property containing the * valid device ID. * @param {string} remotePath - The path to an existing remote file on the device. This variable can be prefixed with @@ -361,10 +361,8 @@ export default { base64Data = Buffer.from(base64Data).toString('utf8'); } return this.isSimulator() - ? // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await pushFileToSimulator(this.opts.device, remotePath, base64Data) - : // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await pushFileToRealDevice(this.opts.device, remotePath, base64Data); + ? await pushFileToSimulator(/** @type {import('../driver').Simulator} */ (this.device), remotePath, base64Data) + : await pushFileToRealDevice(/** @type {import('../real-device').RealDevice} */ (this.device), remotePath, base64Data); }, /** @@ -398,10 +396,8 @@ export default { ); } return this.isSimulator() - ? // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await pullFromSimulator(this.opts.device, remotePath, true) - : // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await pullFromRealDevice(this.opts.device, remotePath, true); + ? await pullFromSimulator(/** @type {import('../driver').Simulator} */ (this.device), remotePath, true) + : await pullFromRealDevice(/** @type {import('../real-device').RealDevice} */ (this.device), remotePath, true); }, /** @@ -427,8 +423,7 @@ export default { if (!remotePath.endsWith('/')) { remotePath = `${remotePath}/`; } - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await deleteFileOrFolder(this.opts.device, remotePath, this.isSimulator()); + await deleteFileOrFolder(this.device, remotePath, this.isSimulator()); }, /** @@ -445,8 +440,7 @@ export default { `'${remotePath}' is given instead`, ); } - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await deleteFileOrFolder(this.opts.device, remotePath, this.isSimulator()); + await deleteFileOrFolder(this.device, remotePath, this.isSimulator()); }, /** @@ -463,10 +457,8 @@ export default { remotePath = `${remotePath}/`; } return this.isSimulator() - ? // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await pullFromSimulator(this.opts.device, remotePath, false) - : // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await pullFromRealDevice(this.opts.device, remotePath, false); + ? await pullFromSimulator(/** @type {import('../driver').Simulator} */ (this.device), remotePath, false) + : await pullFromRealDevice(/** @type {import('../real-device').RealDevice} */ (this.device), remotePath, false); }, /** diff --git a/lib/commands/general.js b/lib/commands/general.js index b155730e1..44de9cd29 100644 --- a/lib/commands/general.js +++ b/lib/commands/general.js @@ -169,8 +169,7 @@ const commands = { if (this.isRealDevice()) { await this.proxyCommand('/url', 'POST', {url}); } else { - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await this.opts.device.simctl.openUrl(url); + await /** @type {import('../driver').Simulator} */ (this.device).simctl.openUrl(url); } }, /** diff --git a/lib/commands/gesture.js b/lib/commands/gesture.js index 52d3f26dc..812078ce2 100644 --- a/lib/commands/gesture.js +++ b/lib/commands/gesture.js @@ -57,8 +57,7 @@ const commands = { if (!this.isSimulator()) { throw new errors.UnknownError('Shake is not supported on real devices'); } - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await this.opts.device.shake(); + await /** @type {import('../driver').Simulator} */ (this.device).shake(); }, /** * @this {XCUITestDriver} diff --git a/lib/commands/keychains.js b/lib/commands/keychains.js index 6b7cae95f..7ccf1693e 100644 --- a/lib/commands/keychains.js +++ b/lib/commands/keychains.js @@ -14,7 +14,7 @@ export default { */ async mobileClearKeychains() { assertSimulator(this); - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await this.opts.device.clearKeychains(); + + await /** @type {import('../driver').Simulator} */ (this.device).clearKeychains(); }, }; diff --git a/lib/commands/localization.js b/lib/commands/localization.js index bc771350d..49f369a4e 100644 --- a/lib/commands/localization.js +++ b/lib/commands/localization.js @@ -27,9 +27,6 @@ export default { // Assign skipSyncUiDialogTranslation: true option in order to avoid shutting down the WDA session localizationOptions.language = Object.assign(language, {skipSyncUiDialogTranslation: true}); } - return /** @type {boolean} */ ( - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await this.opts.device.configureLocalization(localizationOptions) - ); + return await /** @type {import('../driver').Simulator} */ (this.device).configureLocalization(localizationOptions); }, }; diff --git a/lib/commands/location.js b/lib/commands/location.js index d32811a6d..bf5cf746a 100644 --- a/lib/commands/location.js +++ b/lib/commands/location.js @@ -77,8 +77,7 @@ export default { } if (this.isSimulator()) { - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await this.opts.device.setGeolocation(`${latitude}`, `${longitude}`); + await /** @type {import('../driver').Simulator} */ (this.device).setGeolocation(`${latitude}`, `${longitude}`); return /** @type {Location} */ ({latitude, longitude, altitude: 0}); } diff --git a/lib/commands/log.js b/lib/commands/log.js index c7fd31696..8699eb83b 100644 --- a/lib/commands/log.js +++ b/lib/commands/log.js @@ -103,8 +103,7 @@ export default { } if (_.isUndefined(this.logs.syslog)) { this.logs.crashlog = new IOSCrashLog({ - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - sim: this.opts.device, + sim: this.device, udid: this.isRealDevice() ? this.opts.udid : undefined, }); @@ -115,8 +114,7 @@ export default { }); } else { this.logs.syslog = new IOSSimulatorLog({ - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - sim: this.opts.device, + sim: this.device, showLogs: this.opts.showIOSLog, xcodeVersion: this.xcodeVersion, iosSimulatorLogsPredicate: this.opts.iosSimulatorLogsPredicate, diff --git a/lib/commands/memory.js b/lib/commands/memory.js index d99174721..96d9db0ab 100644 --- a/lib/commands/memory.js +++ b/lib/commands/memory.js @@ -1,5 +1,6 @@ import _ from 'lodash'; import { errors } from 'appium/driver'; +import RealDevice from '../real-device'; export default { /** @@ -15,8 +16,7 @@ export default { throw new Error('Memory warning simulation is only supported on real devices'); } - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - const device = this.opts.device; + const device = /** @type {RealDevice} */ (this.device); /** @type {import('../devicectl').AppInfo[]} */ const appInfos = await device.devicectl.listApps(bundleId); diff --git a/lib/commands/notifications.js b/lib/commands/notifications.js index e74512f5e..333008278 100644 --- a/lib/commands/notifications.js +++ b/lib/commands/notifications.js @@ -35,8 +35,7 @@ export default { `Got ${JSON.stringify(payload.aps)} instead`, ); } - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - return await this.opts.device.pushNotification({ + return await /** @type {import('../driver').Simulator} */ (this.device).pushNotification({ ...payload, 'Simulator Target Bundle': bundleId, }); diff --git a/lib/commands/pasteboard.js b/lib/commands/pasteboard.js index 0c31f8b3a..6ca69fc1a 100644 --- a/lib/commands/pasteboard.js +++ b/lib/commands/pasteboard.js @@ -17,8 +17,9 @@ export default { // can be empty string throw new Error('Pasteboard content is mandatory to set'); } - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - return await this.opts.device.simctl.setPasteboard(content, encoding); + return await /** @type {import('../driver').Simulator} */ (this.device).simctl.setPasteboard( + content, /** @type {BufferEncoding} */ (encoding) + ); }, /** @@ -34,8 +35,7 @@ export default { if (!this.isSimulator()) { throw new Error('Getting pasteboard content is not supported on real devices'); } - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - return await this.opts.device.simctl.getPasteboard(encoding); + return await /** @type {import('../driver').Simulator} */ (this.device).simctl.getPasteboard(encoding); }, }; diff --git a/lib/commands/pcap.js b/lib/commands/pcap.js index 8ef97a999..89818c379 100644 --- a/lib/commands/pcap.js +++ b/lib/commands/pcap.js @@ -110,8 +110,7 @@ export default { prefix: `appium_${util.uuidV4().substring(0, 8)}`, suffix: DEFAULT_EXT, }); - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - const trafficCollector = new TrafficCapture(this.opts.device.udid, this.log, resultPath); + const trafficCollector = new TrafficCapture(this.device.udid, this.log, resultPath); const timeoutSeconds = parseInt(String(timeLimitSec), 10); if (isNaN(timeoutSeconds) || timeoutSeconds > MAX_CAPTURE_TIME_SEC || timeoutSeconds <= 0) { diff --git a/lib/commands/performance.js b/lib/commands/performance.js index a0b04121e..6b5ee5c95 100644 --- a/lib/commands/performance.js +++ b/lib/commands/performance.js @@ -288,8 +288,7 @@ export default { for (const recorder of this._perfRecorders.filter((x) => x.profileName === profileName)) { if (recorder.isRunning()) { this.log.debug( - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - `Performance recorder for '${profileName}' on device '${this.opts.device.udid}' ` + + `Performance recorder for '${profileName}' on device '${this.device.udid}' ` + ` is already running. Doing nothing`, ); return; @@ -310,8 +309,7 @@ export default { realPid = pid; } } - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - const recorder = new PerfRecorder(await tempDir.openDir(), this.opts.device.udid, { + const recorder = new PerfRecorder(await tempDir.openDir(), this.device.udid, { timeout: parseInt(String(timeout), 10), profileName, pid: parseInt(String(realPid), 10), @@ -364,8 +362,7 @@ export default { if (_.isEmpty(recorders)) { this.log.errorAndThrow( `There are no records for performance profile '${profileName}' ` + - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - `and device ${this.opts.device.udid}. Have you started the profiling before?`, + `and device ${this.device.udid}. Have you started the profiling before?`, ); } @@ -374,8 +371,7 @@ export default { if (!(await fs.exists(resultPath))) { this.log.errorAndThrow( `There is no ${DEFAULT_EXT} file found for performance profile '${profileName}' ` + - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - `and device ${this.opts.device.udid}. Make sure the selected profile is supported on this device`, + `and device ${this.device.udid}. Make sure the selected profile is supported on this device`, ); } diff --git a/lib/commands/permissions.js b/lib/commands/permissions.js index db62012e6..f353d93de 100644 --- a/lib/commands/permissions.js +++ b/lib/commands/permissions.js @@ -55,8 +55,11 @@ export default { } assertSimulator(this); - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - return await this.opts.device.getPermission(bundleId, service); + return /** @type {import('./types').PermissionState} */ ( + await /** @type {import('../driver').Simulator} */ (this.device).getPermission( + bundleId, String(service) + ) + ); }, /** @@ -75,8 +78,7 @@ export default { } assertSimulator(this); - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await this.opts.device.setPermissions(bundleId, access); + await /** @type {import('../driver').Simulator} */ (this.device).setPermissions(bundleId, access); }, }; diff --git a/lib/commands/recordscreen.js b/lib/commands/recordscreen.js index e657e6e29..536080c44 100644 --- a/lib/commands/recordscreen.js +++ b/lib/commands/recordscreen.js @@ -225,8 +225,7 @@ export default { }); const wdaBaseUrl = this.opts.wdaBaseUrl || WDA_BASE_URL; - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - const screenRecorder = new ScreenRecorder(this.opts.device.udid, this.log, videoPath, { + const screenRecorder = new ScreenRecorder(this.device.udid, this.log, videoPath, { remotePort: this.opts.mjpegServerPort || DEFAULT_MJPEG_SERVER_PORT, remoteUrl: wdaBaseUrl, usePortForwarding: this.isRealDevice() && isLocalHost(wdaBaseUrl), diff --git a/lib/commands/screenshots.js b/lib/commands/screenshots.js index 470cdf0ac..9d55009fa 100644 --- a/lib/commands/screenshots.js +++ b/lib/commands/screenshots.js @@ -1,10 +1,12 @@ import {retryInterval} from 'asyncbox'; import _ from 'lodash'; +import {errors} from 'appium/driver'; import {util, imageUtil} from 'appium/support'; export default { /** * @this {XCUITestDriver} + * @returns {Promise} */ async getScreenshot() { const getScreenshotFromWDA = async () => { @@ -38,12 +40,15 @@ export default { // simulator attempt if (this.isSimulator()) { this.log.info(`Falling back to 'simctl io screenshot' API`); - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - return await this.opts.device.simctl.getScreenshot(); + const payload = await /** @type {import('../driver').Simulator} */ (this.device).simctl.getScreenshot(); + if (!payload) { + throw new errors.UnableToCaptureScreen(); + } + return payload; } // Retry for real devices only. Fail fast on Simulator if simctl does not work as expected - return await retryInterval(2, 1000, getScreenshotFromWDA); + return /** @type {string} */ (await retryInterval(2, 1000, getScreenshotFromWDA)); }, /** * @this {XCUITestDriver} diff --git a/lib/commands/web.js b/lib/commands/web.js index 67b0948ba..27dda1112 100644 --- a/lib/commands/web.js +++ b/lib/commands/web.js @@ -995,8 +995,7 @@ const extensions = { } this.log.debug(`About to update Safari preferences: ${JSON.stringify(preferences)}`); - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await this.opts.device.updateSafariSettings(preferences); + await /** @type {import('../driver').Simulator} */ (this.device).updateSafariSettings(preferences); }, }; diff --git a/lib/commands/xctest-record-screen.js b/lib/commands/xctest-record-screen.js index f910397de..7c245adea 100644 --- a/lib/commands/xctest-record-screen.js +++ b/lib/commands/xctest-record-screen.js @@ -32,8 +32,7 @@ const SUBDIRECTORY = 'Attachments'; * @returns {Promise} The full path to the screen recording movie */ async function retrieveRecodingFromSimulator(uuid) { - // @ts-ignore The property is there - const device = this.opts.device; + const device = /** @type {import('../driver').Simulator} */ (this.device); const dataRoot = /** @type {string} */ (device.getDir()); // On Simulators the path looks like // $HOME/Library/Developer/CoreSimulator/Devices/F8E1968A-8443-4A9A-AB86-27C54C36A2F6/data/Containers/Data/InternalDaemon/4E3FE8DF-AD0A-41DA-B6EC-C35E5798C219/Attachments/A044DAF7-4A58-4CD5-95C3-29B4FE80C377 @@ -59,8 +58,7 @@ async function retrieveRecodingFromSimulator(uuid) { * @returns {Promise} The full path to the screen recording movie */ async function retrieveRecodingFromRealDevice(uuid) { - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - const device = this.opts.device; + const device = /** @type {import('../real-device').RealDevice} */ (this.device); const fileNames = await device.devicectl.listFiles(DOMAIN_TYPE, DOMAIN_IDENTIFIER, { username: USERNAME, diff --git a/lib/commands/xctest.js b/lib/commands/xctest.js index e37da88c0..eec8d8863 100644 --- a/lib/commands/xctest.js +++ b/lib/commands/xctest.js @@ -16,15 +16,13 @@ const xctestLog = logger.getLogger('XCTest'); * @param {XCUITestDriver['opts']} opts Opts object from the driver instance */ export function assertIDB(opts) { - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - if (!opts.device?.idb || !opts.launchWithIDB) { + if (!this.device?.idb || !opts.launchWithIDB) { throw new Error( `To use XCTest runner, IDB (https://github.com/facebook/idb) must be installed ` + `and sessions must be run with the "launchWithIDB" capability`, ); } - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - return opts.device.idb; + return this.device.idb; } /** diff --git a/lib/driver.js b/lib/driver.js index 811aadc65..4230503ed 100644 --- a/lib/driver.js +++ b/lib/driver.js @@ -384,6 +384,14 @@ class XCUITestDriver extends BaseDriver { return didMerge; } + /** + * @returns {Simulator|RealDevice} + */ + get device() { + // @ts-ignore This property should exist + return this.opts?.device; + } + isXcodebuildNeeded() { return !(CAP_NAMES_NO_XCODEBUILD_REQUIRED.some((x) => Boolean(this.opts[x]))); } @@ -484,6 +492,7 @@ class XCUITestDriver extends BaseDriver { this.log.info( `Determining device to run tests on: udid: '${udid}', real device: ${realDevice}`, ); + // TODO: extract device out of opts to a separate driver property // @ts-expect-error - do not assign arbitrary properties to `this.opts` this.opts.device = device; this.opts.udid = udid; @@ -499,22 +508,18 @@ class XCUITestDriver extends BaseDriver { this.log.info( `Setting simulator devices set path to '${this.opts.simulatorDevicesSetPath}'`, ); - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - this.opts.device.devicesSetPath = this.opts.simulatorDevicesSetPath; + (/** @type {Simulator} */ (this.device)).devicesSetPath = this.opts.simulatorDevicesSetPath; } } // at this point if there is no platformVersion, get it from the device - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - if (!this.opts.platformVersion && this.opts.device) { - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - this.opts.platformVersion = await this.opts.device.getPlatformVersion(); + if (!this.opts.platformVersion) { + this.opts.platformVersion = await this.device.getPlatformVersion(); this.log.info( `No platformVersion specified. Using device version: '${this.opts.platformVersion}'`, ); } - // @ts-expect-error - do not assign arbitrary properties to `this.opts` const normalizedVersion = normalizePlatformVersion(this.opts.platformVersion); if (this.opts.platformVersion !== normalizedVersion) { this.log.info( @@ -604,8 +609,7 @@ class XCUITestDriver extends BaseDriver { !this.opts.app && this.opts.bundleId && !this.isSafari() && - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - !(await this.opts.device.isAppInstalled(this.opts.bundleId)) + !(await this.device.isAppInstalled(this.opts.bundleId)) ) { this.log.errorAndThrow(`App with bundle identifier '${this.opts.bundleId}' unknown`); } @@ -614,8 +618,7 @@ class XCUITestDriver extends BaseDriver { if (this.opts.permissions) { this.log.debug('Setting the requested permissions before WDA is started'); for (const [bundleId, permissionsMapping] of _.toPairs(JSON.parse(this.opts.permissions))) { - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await this.opts.device.setPermissions(bundleId, permissionsMapping); + await /** @type {Simulator} */ (this.device).setPermissions(bundleId, permissionsMapping); } } @@ -628,15 +631,14 @@ class XCUITestDriver extends BaseDriver { const methodName = `${ this.opts.calendarAccessAuthorized ? 'enable' : 'disable' }CalendarAccess`; - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await this.opts.device[methodName](this.opts.bundleId); + await this.device[methodName](this.opts.bundleId); } } // @ts-expect-error - do not assign arbitrary properties to `this.opts` await this.startWda(this.opts.sessionId); - if (this.opts.orientation) { + if (_.isString(this.opts.orientation)) { await this.setInitialOrientation(this.opts.orientation); this.logEvent('orientationSet'); } @@ -663,12 +665,11 @@ class XCUITestDriver extends BaseDriver { * Start the simulator and initialize based on capabilities */ async initSimulator() { - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - const device = this.opts.device; + const device = /** @type {Simulator} */ (this.device); if (this.opts.shutdownOtherSimulators) { this.assertFeatureEnabled(SHUTDOWN_OTHER_FEAT_NAME); - await shutdownOtherSimulators(device); + await shutdownOtherSimulators.bind(this)(); } await this.startSim(); @@ -679,17 +680,21 @@ class XCUITestDriver extends BaseDriver { this.logEvent('customCertInstalled'); } - if (await setSafariPrefs(device, this.opts)) { + if (await setSafariPrefs.bind(this)()) { this.log.debug('Safari preferences have been updated'); } - if (await setLocalizationPrefs(device, this.opts)) { + if (await setLocalizationPrefs.bind(this)()) { this.log.debug('Localization preferences have been updated'); } + /** @type {Promise[]} */ const promises = ['reduceMotion', 'reduceTransparency', 'autoFillPasswords'] .filter((optName) => _.isBoolean(this.opts[optName])) - .map((optName) => device[`set${_.upperFirst(optName)}`](this.opts[optName])); + .map((optName) => { + this.log.info(`Setting ${optName} to ${this.opts[optName]}`); + return device[`set${_.upperFirst(optName)}`](this.opts[optName]); + }); await B.all(promises); if (this.opts.launchWithIDB) { @@ -886,16 +891,14 @@ class XCUITestDriver extends BaseDriver { /** * - * @param {XCUITestDriverOpts} [opts] + * @param {boolean} [enforceSimulatorShutdown=false] */ - async runReset(opts) { + async runReset(enforceSimulatorShutdown = false) { this.logEvent('resetStarted'); if (this.isRealDevice()) { - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await runRealDeviceReset(this.opts.device, opts || this.opts); + await runRealDeviceReset.bind(this)(); } else { - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await runSimulatorReset(this.opts.device, opts || this.opts); + await runSimulatorReset.bind(this)(enforceSimulatorShutdown); } this.logEvent('resetComplete'); } @@ -944,18 +947,13 @@ class XCUITestDriver extends BaseDriver { } if (this.opts.resetOnSessionStartOnly === false) { - await this.runReset( - Object.assign({}, this.opts, { - enforceSimulatorShutdown: true, - }), - ); + await this.runReset(true); } - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - const simulatorDevice = this.isSimulator() ? this.opts.device : null; + const simulatorDevice = this.isSimulator() ? /** @type {Simulator} */ (this.device) : null; if (simulatorDevice && this.lifecycleData.createSim) { this.log.debug(`Deleting simulator created for this run (udid: '${simulatorDevice.udid}')`); - await shutdownSimulator(simulatorDevice); + await shutdownSimulator.bind(this)(); await simulatorDevice.delete(); } @@ -1168,19 +1166,16 @@ class XCUITestDriver extends BaseDriver { this.opts.deviceName = translateDeviceName(this.opts.platformVersion, this.opts.deviceName); const setupVersionCaps = async () => { + const iosSdkVersion = await getAndCheckIosSdkVersion(); // @ts-expect-error - do not assign arbitrary properties to `this.opts` - this.opts.iosSdkVersion = await getAndCheckIosSdkVersion(); - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - this.log.info(`iOS SDK Version set to '${this.opts.iosSdkVersion}'`); - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - if (!this.opts.platformVersion && this.opts.iosSdkVersion) { + this.opts.iosSdkVersion = iosSdkVersion; + this.log.info(`iOS SDK Version set to '${iosSdkVersion}'`); + if (!this.opts.platformVersion && iosSdkVersion) { this.log.info( - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - `No platformVersion specified. Using the latest version Xcode supports: '${this.opts.iosSdkVersion}'. ` + + `No platformVersion specified. Using the latest version Xcode supports: '${iosSdkVersion}'. ` + `This may cause problems if a simulator does not exist for this platform version.`, ); - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - this.opts.platformVersion = normalizePlatformVersion(this.opts.iosSdkVersion); + this.opts.platformVersion = normalizePlatformVersion(iosSdkVersion); } }; @@ -1195,11 +1190,10 @@ class XCUITestDriver extends BaseDriver { ); await setupVersionCaps(); - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - const device = await getExistingSim(this.opts); + const device = await getExistingSim.bind(this)(); if (!device) { // No matching Simulator is found. Throw an error - this.log.errorAndThrow( + throw this.log.errorWithException( `Cannot detect udid for ${this.opts.deviceName} Simulator running iOS ${this.opts.platformVersion}`, ); } @@ -1221,6 +1215,7 @@ class XCUITestDriver extends BaseDriver { try { const device = await getSimulator(this.opts.udid, { devicesSetPath: this.opts.simulatorDevicesSetPath, + logger: this.log, }); return {device, realDevice: false, udid: this.opts.udid}; } catch (ign) { @@ -1230,7 +1225,7 @@ class XCUITestDriver extends BaseDriver { } } - const device = getRealDeviceObj(this.opts.udid, this.log); + const device = getRealDeviceObj.bind(this)(); return {device, realDevice: true, udid: this.opts.udid}; } @@ -1245,8 +1240,7 @@ class XCUITestDriver extends BaseDriver { ); } else { // figure out the correct simulator to use, given the desired capabilities - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - const device = await getExistingSim(this.opts); + const device = await getExistingSim.bind(this)(); // check for an existing simulator if (device) { return {device, realDevice: false, udid: device.udid}; @@ -1260,20 +1254,21 @@ class XCUITestDriver extends BaseDriver { } async startSim() { + /** @type {import('appium-ios-simulator').DevicePreferences} */ + const devicePreferences = {}; + /** @type {import('appium-ios-simulator').RunOptions} */ const runOpts = { scaleFactor: this.opts.scaleFactor, connectHardwareKeyboard: !!this.opts.connectHardwareKeyboard, pasteboardAutomaticSync: this.opts.simulatorPasteboardAutomaticSync ?? 'off', isHeadless: !!this.opts.isHeadless, tracePointer: this.opts.simulatorTracePointer, - devicePreferences: {}, + devicePreferences, }; // add the window center, if it is specified - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - if (this.opts.SimulatorWindowCenter) { - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - runOpts.devicePreferences.SimulatorWindowCenter = this.opts.SimulatorWindowCenter; + if (this.opts.simulatorWindowCenter) { + devicePreferences.SimulatorWindowCenter = this.opts.simulatorWindowCenter; } if (_.isInteger(this.opts.simulatorStartupTimeout)) { @@ -1285,23 +1280,22 @@ class XCUITestDriver extends BaseDriver { const orientation = _.isString(this.opts.orientation) && this.opts.orientation.toUpperCase(); switch (orientation) { case 'LANDSCAPE': - runOpts.devicePreferences.SimulatorWindowOrientation = 'LandscapeLeft'; - runOpts.devicePreferences.SimulatorWindowRotationAngle = 90; + devicePreferences.SimulatorWindowOrientation = 'LandscapeLeft'; + devicePreferences.SimulatorWindowRotationAngle = 90; break; case 'PORTRAIT': - runOpts.devicePreferences.SimulatorWindowOrientation = 'Portrait'; - runOpts.devicePreferences.SimulatorWindowRotationAngle = 0; + devicePreferences.SimulatorWindowOrientation = 'Portrait'; + devicePreferences.SimulatorWindowRotationAngle = 0; break; } - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await this.opts.device.run(runOpts); + await /** @type {Simulator} */ (this.device).run(runOpts); } async createSim() { this.lifecycleData.createSim = true; // create sim for caps - const sim = await createSim(this.opts); + const sim = await createSim.bind(this)(); this.log.info(`Created simulator with udid '${sim.udid}'.`); return sim; } @@ -1670,15 +1664,13 @@ class XCUITestDriver extends BaseDriver { const {install, skipUninstall} = await this.checkAutInstallationState(); if (install) { if (this.isRealDevice()) { - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await installToRealDevice(this.opts.device, this.opts.app, this.opts.bundleId, { + await installToRealDevice.bind(this)(this.opts.app, this.opts.bundleId, { skipUninstall, timeout: this.opts.appPushTimeout, strategy: this.opts.appInstallStrategy, }); } else { - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await installToSimulator(this.opts.device, this.opts.app, this.opts.bundleId, { + await installToSimulator.bind(this)(this.opts.app, this.opts.bundleId, { skipUninstall, newSimulator: this.lifecycleData?.createSim, }); @@ -1693,6 +1685,10 @@ class XCUITestDriver extends BaseDriver { } } + /** + * @param {string|string[]} otherApps + * @returns {Promise} + */ async installOtherApps(otherApps) { /** @type {string[]|undefined} */ let appsList; @@ -1712,9 +1708,7 @@ class XCUITestDriver extends BaseDriver { const appIds = await B.all(appPaths.map((appPath) => extractBundleId(appPath))); for (const [appId, appPath] of _.zip(appIds, appPaths)) { if (this.isRealDevice()) { - await installToRealDevice( - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - this.opts.device, + await installToRealDevice.bind(this)( appPath, appId, { @@ -1724,10 +1718,7 @@ class XCUITestDriver extends BaseDriver { }, ); } else { - await installToSimulator( - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - this.opts.device, - // @ts-ignore the path should always be defined + await installToSimulator.bind(this)( appPath, appId, { @@ -1738,6 +1729,10 @@ class XCUITestDriver extends BaseDriver { } } + /** + * @param {string} orientation + * @returns {Promise} + */ async setInitialOrientation(orientation) { const dstOrientation = _.toUpper(orientation); if (!SUPPORTED_ORIENATIONS.includes(dstOrientation)) { @@ -1756,6 +1751,10 @@ class XCUITestDriver extends BaseDriver { } } + /** + * @param {string} [cmdName] + * @returns {number|undefined} + */ _getCommandTimeout(cmdName) { if (this.opts.commandTimeouts) { if (cmdName && _.has(this.opts.commandTimeouts, cmdName)) { @@ -1792,14 +1791,11 @@ class XCUITestDriver extends BaseDriver { `Installing prebuilt WDA at '${this.opts.prebuiltWDAPath}'. ` + `Bundle identifier: ${candidateBundleId}.` ); - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - const device = this.opts.device; // Note: The CFBundleVersion in the test bundle was always 1. // It may not be able to compare with the installed versio. if (this.isRealDevice()) { - await installToRealDevice( - device, + await installToRealDevice.bind(this)( this.opts.prebuiltWDAPath, candidateBundleId, { @@ -1809,7 +1805,10 @@ class XCUITestDriver extends BaseDriver { }, ); } else { - await installToSimulator(device, this.opts.prebuiltWDAPath, candidateBundleId); + await installToSimulator.bind(this)( + this.opts.prebuiltWDAPath, + candidateBundleId + ); } } @@ -2266,4 +2265,6 @@ export {XCUITestDriver}; * @typedef {import('./commands/types').FullContext} FullContext * @typedef {import('./types').WDACapabilities} WDACapabilities * @typedef {import('appium-xcode').XcodeVersion} XcodeVersion - */ + * @typedef {import('appium-ios-simulator').Simulator} Simulator + * @typedef {import('./real-device').RealDevice} RealDevice + */ \ No newline at end of file diff --git a/lib/real-device-management.js b/lib/real-device-management.js index a78bea43d..5d89f4788 100644 --- a/lib/real-device-management.js +++ b/lib/real-device-management.js @@ -1,9 +1,8 @@ import {utilities} from 'appium-ios-device'; import RealDevice from './real-device'; -import log from './logger'; import {SAFARI_BUNDLE_ID} from './app-utils'; -async function getConnectedDevices() { +export async function getConnectedDevices() { return await utilities.getConnectedDevices(); } @@ -11,24 +10,25 @@ async function getConnectedDevices() { * @param {string} udid * @returns {Promise} */ -async function getOSVersion(udid) { +export async function getOSVersion(udid) { return await utilities.getOSVersion(udid); } /** - * @param {RealDevice} device - * @param {import('./driver').XCUITestDriverOpts} opts + * @this {import('./driver').XCUITestDriver} * @returns {Promise} */ -async function resetRealDevice(device, opts) { - const {bundleId, fullReset} = opts; +export async function resetRealDevice() { + const {bundleId, fullReset} = this.opts; if (!bundleId) { return; } + const device = /** @type {RealDevice} */ (this.device); + if (bundleId === SAFARI_BUNDLE_ID) { - log.debug('Reset requested. About to terminate Safari'); - await device.terminateApp(bundleId, opts.platformVersion); + this.log.debug('Reset requested. About to terminate Safari'); + await device.terminateApp(bundleId, String(this.opts.platformVersion)); return; } @@ -36,33 +36,33 @@ async function resetRealDevice(device, opts) { return; } - log.debug(`Reset: fullReset requested. Will try to uninstall the app '${bundleId}'.`); + this.log.debug(`Reset: fullReset requested. Will try to uninstall the app '${bundleId}'.`); if (!(await device.isAppInstalled(bundleId))) { - log.debug('Reset: app not installed. No need to uninstall'); + this.log.debug('Reset: app not installed. No need to uninstall'); return; } + try { await device.remove(bundleId); } catch (err) { - log.error(`Reset: could not remove '${bundleId}' from device: ${err.message}`); + this.log.error(`Reset: could not remove '${bundleId}' from device: ${err.message}`); throw err; } - log.debug(`Reset: removed '${bundleId}'`); + this.log.debug(`Reset: removed '${bundleId}'`); } /** - * @param {RealDevice} device - * @param {import('./driver').XCUITestDriverOpts} opts + * @this {import('./driver').XCUITestDriver} * @returns {Promise} */ -async function runRealDeviceReset(device, opts) { - if (!opts.noReset || opts.fullReset) { - log.debug('Reset: running ios real device reset flow'); - if (!opts.noReset) { - await resetRealDevice(device, opts); +export async function runRealDeviceReset() { + if (!this.opts.noReset || this.opts.fullReset) { + this.log.debug('Reset: running ios real device reset flow'); + if (!this.opts.noReset) { + await resetRealDevice.bind(this)(); } } else { - log.debug('Reset: fullReset not set. Leaving as is'); + this.log.debug('Reset: fullReset not set. Leaving as is'); } } @@ -76,28 +76,30 @@ async function runRealDeviceReset(device, opts) { */ /** - * @param {RealDevice} device The device instance + * @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] + * @param {InstallOptions} [opts={}] */ -async function installToRealDevice(device, app, bundleId, opts) { +export async function installToRealDevice(app, bundleId, opts = {}) { + const device = /** @type {RealDevice} */ (this.device); + if (!device.udid || !app || !bundleId) { - log.debug('No device id, app or bundle id, not installing to real device.'); + this.log.debug('No device id, app or bundle id, not installing to real device.'); return; } - const {skipUninstall, strategy, timeout} = opts ?? {}; + const {skipUninstall, strategy, timeout} = opts; if (!skipUninstall) { - log.info(`Reset requested. Removing app with id '${bundleId}' from the device`); + this.log.info(`Reset requested. Removing app with id '${bundleId}' from the device`); await device.remove(bundleId); } - log.debug(`Installing '${app}' on device with UUID '${device.udid}'...`); + this.log.debug(`Installing '${app}' on device with UUID '${device.udid}'...`); try { await device.install(app, timeout, strategy); - log.debug('The app has been installed successfully.'); + this.log.debug('The app has been installed successfully.'); } catch (e) { // Want to clarify the device's application installation state in this situation. @@ -113,29 +115,20 @@ async function installToRealDevice(device, app, bundleId, opts) { // If the error was by below error case, we could recover the situation // by uninstalling the device's app bundle id explicitly regard less the app exists on the device or not (e.g. offload app). // [XCUITest] Error installing app '/path/to.app': Unexpected data: {"Error":"MismatchedApplicationIdentifierEntitlement","ErrorDescription":"Upgrade's application-identifier entitlement string (TEAM_ID.com.kazucocoa.example) does not match installed application's application-identifier string (ANOTHER_TEAM_ID.com.kazucocoa.example); rejecting upgrade."} - log.info(`The application identified by '${bundleId}' cannot be installed because it might ` + + this.log.info(`The application identified by '${bundleId}' cannot be installed because it might ` + `be already cached on the device, probably with a different signature. ` + `Will try to remove it and install a new copy. Original error: ${e.message}`); await device.remove(bundleId); await device.install(app, timeout, strategy); - log.debug('The app has been installed after one retrial.'); + this.log.debug('The app has been installed after one retrial.'); } } /** - * @param {string} udid - * @param {import('@appium/types').AppiumLogger} logger + * @this {import('./driver').XCUITestDriver} * @returns {RealDevice} */ -function getRealDeviceObj(udid, logger) { - log.debug(`Creating iDevice object with udid '${udid}'`); - return new RealDevice(udid, logger); +export function getRealDeviceObj() { + this.log.debug(`Creating iDevice object with udid '${this.opts.udid}'`); + return new RealDevice(this.opts.udid, this.log); } - -export { - getConnectedDevices, - getOSVersion, - runRealDeviceReset, - installToRealDevice, - getRealDeviceObj, -}; diff --git a/lib/real-device.js b/lib/real-device.js index c6029c4d2..2193462b9 100644 --- a/lib/real-device.js +++ b/lib/real-device.js @@ -2,7 +2,7 @@ import {fs, timing, util} from 'appium/support'; import path from 'path'; import {services, utilities, INSTRUMENT_CHANNEL} from 'appium-ios-device'; import B from 'bluebird'; -import log from './logger'; +import defaultLogger from './logger'; import _ from 'lodash'; import {exec} from 'teen_process'; import {extractBundleId} from './app-utils'; @@ -20,12 +20,27 @@ const APP_INSTALL_STRATEGY = Object.freeze({ IOS_DEPLOY, }); -class RealDevice { +export class RealDevice { + /** + * @param {string} udid + * @param {import('@appium/types').AppiumLogger} [logger] + */ constructor(udid, logger) { this.udid = udid; - this.devicectl = new Devicectl(this.udid, logger); + this._log = logger ?? defaultLogger; + this.devicectl = new Devicectl(this.udid, this._log); + } + + /** + * @returns {import('@appium/types').AppiumLogger} + */ + get log() { + return this._log; } + /** + * @param {string} bundleId + */ async remove(bundleId) { const service = await services.startInstallationProxyService(this.udid); try { @@ -35,6 +50,9 @@ class RealDevice { } } + /** + * @param {string} bundleId + */ async removeApp(bundleId) { await this.remove(bundleId); } @@ -56,7 +74,7 @@ class RealDevice { `Only the following strategies are supported: ${_.values(APP_INSTALL_STRATEGY)}`, ); } - log.debug( + this.log.debug( `Using '${strategy ?? APP_INSTALL_STRATEGY.SERIAL}' app deployment strategy. ` + `You could change it by providing another value to the 'appInstallStrategy' capability`, ); @@ -94,11 +112,11 @@ class RealDevice { await this.isAppInstalled(bundleId), ); } catch (err) { - log.warn(`Error installing app '${app}': ${err.message}`); + this.log.warn(`Error installing app '${app}': ${err.message}`); if (err instanceof B.TimeoutError) { - log.warn(`Consider increasing the value of 'appPushTimeout' capability`); + this.log.warn(`Consider increasing the value of 'appPushTimeout' capability`); } - log.warn(`Falling back to '${IOS_DEPLOY}' usage`); + this.log.warn(`Falling back to '${IOS_DEPLOY}' usage`); try { await installWithIosDeploy(); } catch (err1) { @@ -110,9 +128,13 @@ class RealDevice { afcService.close(); } } - log.info(`App installation succeeded after ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`); + this.log.info(`App installation succeeded after ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`); } + /** + * @param {string} bundlePathOnPhone + * @param {boolean} [isUpgrade=false] + */ async installOrUpgradeApplication(bundlePathOnPhone, isUpgrade = false) { const notificationService = await services.startNotificationProxyService(this.udid); const installationService = await services.startInstallationProxyService(this.udid); @@ -124,10 +146,10 @@ class RealDevice { const clientOptions = {PackageType: 'Developer'}; try { if (isUpgrade) { - log.debug(`An upgrade of the existing application is going to be performed`); + this.log.debug(`An upgrade of the existing application is going to be performed`); await installationService.upgradeApplication(bundlePathOnPhone, clientOptions); } else { - log.debug(`A new application installation is going to be performed`); + this.log.debug(`A new application installation is going to be performed`); await installationService.installApplication(bundlePathOnPhone, clientOptions); } try { @@ -137,7 +159,7 @@ class RealDevice { `${APPLICATION_NOTIFICATION_TIMEOUT_MS}ms but we will continue`, ); } catch (e) { - log.warn(`Failed to receive the notification. Error: ${e.message}`); + this.log.warn(`Failed to receive the notification. Error: ${e.message}`); } } finally { installationService.close(); @@ -194,6 +216,11 @@ class RealDevice { } } + /** + * @param {string} bundleId + * @param {string} platformVersion + * @returns {Promise} + */ async terminateApp(bundleId, platformVersion) { let instrumentService; let installProxyService; @@ -203,22 +230,22 @@ class RealDevice { returnAttributes: ['CFBundleIdentifier', 'CFBundleExecutable'] }); if (!apps[bundleId]) { - log.info(`The bundle id '${bundleId}' did not exist`); + this.log.info(`The bundle id '${bundleId}' did not exist`); return false; } const executableName = apps[bundleId].CFBundleExecutable; - log.debug(`The executable name for the bundle id '${bundleId}' was '${executableName}'`); + this.log.debug(`The executable name for the bundle id '${bundleId}' was '${executableName}'`); // 'devicectl' has overhead (generally?) than the instrument service via appium-ios-device, // so hre uses the 'devicectl' only for iOS 17+. if (util.compareVersions(platformVersion, '>=', '17.0')) { - log.debug(`Calling devicectl to kill the process`); + this.log.debug(`Calling devicectl to kill the process`); const pids = (await this.devicectl.listProcesses()) .filter(({executable}) => executable.endsWith(`/${executableName}`)) .map(({processIdentifier}) => processIdentifier); if (_.isEmpty(pids)) { - log.info(`The process of the bundle id '${bundleId}' was not running`); + this.log.info(`The process of the bundle id '${bundleId}' was not running`); return false; } await this.devicectl.sendSignalToProcess(pids[0], 2); @@ -230,7 +257,7 @@ class RealDevice { ); const process = processes.selector.find((process) => process.name === executableName); if (!process) { - log.info(`The process of the bundle id '${bundleId}' was not running`); + this.log.info(`The process of the bundle id '${bundleId}' was not running`); return false; } await instrumentService.callChannel( @@ -240,7 +267,7 @@ class RealDevice { ); } } catch (err) { - log.warn(`Failed to kill '${bundleId}'. Original error: ${err.stderr || err.message}`); + this.log.warn(`Failed to kill '${bundleId}'. Original error: ${err.stderr || err.message}`); return false; } finally { if (installProxyService) { @@ -280,6 +307,9 @@ class RealDevice { } } + /** + * @returns {Promise} + */ async getPlatformVersion() { return await utilities.getOSVersion(this.udid); } diff --git a/lib/simulator-management.js b/lib/simulator-management.js index be3f61cdb..401c31e31 100644 --- a/lib/simulator-management.js +++ b/lib/simulator-management.js @@ -2,7 +2,6 @@ import {getSimulator} from 'appium-ios-simulator'; import Simctl from 'node-simctl'; import {resetTestProcesses} from 'appium-webdriveragent'; import _ from 'lodash'; -import log from './logger'; import {util, timing} from 'appium/support'; import {UDID_AUTO, normalizePlatformName} from './utils'; @@ -16,22 +15,15 @@ const SAFARI_OPTS_ALIASES_MAP = /** @type {const} */ ({ safariOpenLinksInBackground: [['OpenLinksInBackground'], (x) => Number(Boolean(x))], }); -/** - * @typedef {Object} SimulatorCreationOptions - * @property {string} deviceName - A name for the device - * @property {string} platformVersion - The version of iOS to use - * @property {string} [simulatorDevicesSetPath] - * @property {string} [platformName] - */ /** * Create a new simulator with `appiumTest-` prefix and return the object. * - * @param {Partial} caps - Capability set by a user. The options available are: + * @this {import('./driver').XCUITestDriver} * @returns {Promise} Simulator object associated with the udid passed in. */ -async function createSim(caps) { - const {simulatorDevicesSetPath: devicesSetPath, deviceName, platformVersion} = caps; - const platform = normalizePlatformName(caps.platformName); +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'; @@ -51,12 +43,13 @@ async function createSim(caps) { } const simName = `${APPIUM_SIM_PREFIX}-${util.uuidV4().toUpperCase()}-${deviceName}`; - log.debug(`Creating a temporary Simulator device '${simName}'`); + 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, + logger: this.log, }); } @@ -72,24 +65,31 @@ async function createSim(caps) { /** * Get an existing simulator matching the provided capabilities. * - * @param {SimulatorLookupOptions} opts - * @returns {Promise} The matched Simulator instance or `null` if no matching device is found. + * @this {import('./driver').XCUITestDriver} + * @returns {Promise} The matched Simulator instance or `null` if no matching device is found. */ -async function getExistingSim(opts = /** @type {SimulatorLookupOptions} */ ({})) { - const {platformVersion, deviceName, udid, simulatorDevicesSetPath: devicesSetPath} = opts; +export async function getExistingSim() { + const { + platformVersion, + deviceName, + udid, + simulatorDevicesSetPath: devicesSetPath, + platformName, + } = this.opts; - const platform = normalizePlatformName(opts.platformName); + const platform = normalizePlatformName(platformName); const selectSim = async (/** @type {{ udid: string; platform: string; }} */ dev) => await getSimulator(dev.udid, { platform, checkExistence: false, devicesSetPath, + logger: this.log, }); const simctl = new Simctl({devicesSetPath}); let devicesMap; if (udid && _.toLower(udid) !== UDID_AUTO) { - log.debug(`Looking for an existing Simulator with UDID '${udid}'`); + 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) { @@ -100,7 +100,7 @@ async function getExistingSim(opts = /** @type {SimulatorLookupOptions} */ ({})) } if (!platformVersion) { - log.debug( + this.log.debug( `Provide 'platformVersion' capability if you prefer an existing Simulator to be selected`, ); return null; @@ -108,14 +108,14 @@ async function getExistingSim(opts = /** @type {SimulatorLookupOptions} */ ({})) const devices = devicesMap?.[platformVersion] ?? (await simctl.getDevices(platformVersion, platform)); - log.debug( + 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) { - log.debug( + this.log.debug( `The 'deviceName' capability value is empty. ` + `Selecting the first matching device '${device.name}' having the ` + `'platformVersion' set to ${platformVersion}`, @@ -127,13 +127,22 @@ async function getExistingSim(opts = /** @type {SimulatorLookupOptions} */ ({})) return null; } -async function shutdownSimulator(device) { +/** + * @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(); } -async function runSimulatorReset(device, opts) { +/** + * @this {import('./driver').XCUITestDriver} + * @property {boolean} [enforceSimulatorShutdown=false] + * @returns {Promise} + */ +export async function runSimulatorReset(enforceSimulatorShutdown = false) { const { noReset, fullReset, @@ -142,29 +151,29 @@ async function runSimulatorReset(device, opts) { bundleId, app, browserName, - enforceSimulatorShutdown, - } = opts; + } = this.opts; if (noReset && !fullReset) { // noReset === true && fullReset === false - log.debug('Reset: noReset is on. Leaving simulator as is'); + this.log.debug('Reset: noReset is on. Leaving simulator as is'); return; } - if (!device) { - log.debug('Reset: no device available. Skipping'); + const device = /** @type {import('./driver').Simulator} */ (this.device); + + if (!this.device) { + this.log.debug('Reset: no device available. Skipping'); return; } if (fullReset) { - log.debug('Reset: fullReset is on. Cleaning simulator'); - await shutdownSimulator(device); - const isKeychainsBackupSuccessful = - (keychainsExcludePatterns || keepKeyChains) && (await device.backupKeychains()); + 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 || []); - log.info(`Successfully restored keychains after full reset`); + this.log.info(`Successfully restored keychains after full reset`); } else if (keychainsExcludePatterns || keepKeyChains) { - log.warn( + this.log.warn( 'Cannot restore keychains after full reset, because ' + 'the backup operation did not succeed', ); @@ -176,32 +185,32 @@ async function runSimulatorReset(device, opts) { try { await device.terminateApp(bundleId); } catch (err) { - log.warn(`Reset: failed to terminate Simulator application with id "${bundleId}"`); + this.log.warn(`Reset: failed to terminate Simulator application with id "${bundleId}"`); } if (app) { - log.info('Not scrubbing third party app in anticipation of uninstall'); + this.log.info('Not scrubbing third party app in anticipation of uninstall'); } else { const isSafari = _.toLower(browserName) === 'safari'; try { if (isSafari) { - await device.scrubSafari(); + await device.scrubSafari(true); } else { await device.scrubApp(bundleId); } } catch (err) { - log.debug(err.stack); - log.warn(err.message); - log.warn( + this.log.debug(err.stack); + this.log.warn(err.message); + this.log.warn( `Reset: could not scrub ${ - isSafari ? 'Safari browser' : 'application with id "' + opts.bundleId + '"' + isSafari ? 'Safari browser' : 'application with id "' + this.opts.bundleId + '"' }. ` + `Leaving as is.`, ); } } if (enforceSimulatorShutdown && (await device.isRunning())) { - await shutdownSimulator(device); + await shutdownSimulator.bind(this)(device); } } } @@ -214,48 +223,52 @@ async function runSimulatorReset(device, opts) { */ /** - * @param {any} device The simulator device object + * @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 + * @param {InstallOptions} [opts={}] */ -async function installToSimulator( - device, +export async function installToSimulator( app, bundleId, - opts = /** @type {InstallOptions} */ ({}), + opts = {}, ) { if (!app) { - log.debug('No app path is given. Nothing to install.'); + 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))) { - log.debug(`Reset requested. Removing app with id '${bundleId}' from the device`); + this.log.debug(`Reset requested. Removing app with id '${bundleId}' from the device`); await device.removeApp(bundleId); } - log.debug(`Installing '${app}' on Simulator with UUID '${device.udid}'...`); + this.log.debug(`Installing '${app}' on Simulator with UUID '${device.udid}'...`); const timer = new timing.Timer().start(); await device.installApp(app); - log.info(`The app has been successfully installed in ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`); + this.log.info(`The app has been successfully installed in ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`); } -async function shutdownOtherSimulators(currentDevice) { +/** + * @this {import('./driver').XCUITestDriver} + */ +export async function shutdownOtherSimulators() { + const device = /** @type {import('./driver').Simulator} */ (this.device); const simctl = new Simctl({ - devicesSetPath: currentDevice.devicesSetPath, + devicesSetPath: device.devicesSetPath, }); const allDevices = _.flatMap(_.values(await simctl.getDevices())); const otherBootedDevices = allDevices.filter( - (device) => device.udid !== currentDevice.udid && device.state === 'Booted', + (device) => device.udid !== device.udid && device.state === 'Booted', ); if (_.isEmpty(otherBootedDevices)) { - log.info('No other running simulators have been detected'); + this.log.info('No other running simulators have been detected'); return; } - log.info( + this.log.info( `Detected ${otherBootedDevices.length} other running ${util.pluralize( 'Simulator', otherBootedDevices.length, @@ -273,40 +286,39 @@ async function shutdownOtherSimulators(currentDevice) { /** * Configures Safari options based on the given session capabilities * - * @param {any} sim Simulator instance - * @param {object} opts Session capabilities + * @this {import('./driver').XCUITestDriver} * @return {Promise} true if any preferences have been updated */ -async function setSafariPrefs(sim, opts = {}) { - const safariSettings = _.cloneDeep(opts.safariGlobalPreferences ?? {}); +export async function setSafariPrefs() { + const safariSettings = _.cloneDeep(this.opts.safariGlobalPreferences ?? {}); for (const [name, [aliases, valueConverter]] of _.toPairs(SAFARI_OPTS_ALIASES_MAP)) { - if (!_.has(opts, name)) { + if (!_.has(this.opts, name)) { continue; } for (const alias of aliases) { - safariSettings[alias] = valueConverter(opts[name]); + safariSettings[alias] = valueConverter(this.opts[name]); } } if (_.isEmpty(safariSettings)) { return false; } - log.debug(`About to update Safari preferences: ${JSON.stringify(safariSettings)}`); - await sim.updateSafariSettings(safariSettings); + this.log.debug(`About to update Safari preferences: ${JSON.stringify(safariSettings)}`); + await /** @type {import('./driver').Simulator} */ (this.device).updateSafariSettings(safariSettings); return true; } /** * Changes Simulator localization preferences * - * @param {any} sim Simulator instance - * @param {object} opts Session capabilities + * @this {import('./driver').XCUITestDriver} * @returns {Promise} True if preferences were changed */ -async function setLocalizationPrefs(sim, opts = {}) { - const {language, locale, calendarFormat, skipSyncUiDialogTranslation} = opts; +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 }; @@ -321,18 +333,7 @@ async function setLocalizationPrefs(sim, opts = {}) { return false; } - log.debug(`About to update localization preferences: ${JSON.stringify(l10nConfig)}`); - await sim.configureLocalization(l10nConfig); + this.log.debug(`About to update localization preferences: ${JSON.stringify(l10nConfig)}`); + await /** @type {import('./driver').Simulator} */ (this.device).configureLocalization(l10nConfig); return true; } - -export { - createSim, - getExistingSim, - runSimulatorReset, - installToSimulator, - shutdownSimulator, - shutdownOtherSimulators, - setSafariPrefs, - setLocalizationPrefs, -}; diff --git a/package.json b/package.json index ca733f181..9d017c424 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "@colors/colors": "^1.6.0", "appium-idb": "^1.6.13", "appium-ios-device": "^2.5.4", - "appium-ios-simulator": "^5.5.1", + "appium-ios-simulator": "^6.1.2", "appium-remote-debugger": "^11.0.0", "appium-webdriveragent": "^8.1.0", "appium-xcode": "^5.1.4", diff --git a/test/functional/driver/driver-e2e-specs.js b/test/functional/driver/driver-e2e-specs.js index 1361e194d..9f5dad08d 100644 --- a/test/functional/driver/driver-e2e-specs.js +++ b/test/functional/driver/driver-e2e-specs.js @@ -2,7 +2,7 @@ import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; import {retryInterval} from 'asyncbox'; import {getSimulator} from 'appium-ios-simulator'; -import {killAllSimulators, shutdownSimulator, deleteDeviceWithRetry} from '../helpers/simulator'; +import {killAllSimulators, deleteDeviceWithRetry, cleanupSimulator} from '../helpers/simulator'; import Simctl from 'node-simctl'; import B from 'bluebird'; import {MOCHA_TIMEOUT, initSession, deleteSession, HOST} from '../helpers/session'; @@ -55,8 +55,7 @@ describe('XCUITestDriver', function () { platform: 'iOS', checkExistence: false, }); - await shutdownSimulator(sim); - await deleteDeviceWithRetry(extractCapabilityValue(caps, 'appium:udid')); + await cleanupSimulator(sim); }); afterEach(async function () { @@ -247,9 +246,7 @@ describe('XCUITestDriver', function () { simsDuring.should.equal(simsBefore); simsAfter.should.equal(simsBefore); } finally { - // cleanup - await shutdownSimulator(sim); - await deleteDeviceWithRetry(udid); + await cleanupSimulator(sim); } }); @@ -298,9 +295,7 @@ describe('XCUITestDriver', function () { simsDuring.should.equal(simsBefore); simsAfter.should.equal(simsBefore); } finally { - // cleanup - await shutdownSimulator(sim); - await deleteDeviceWithRetry(udid); + await cleanupSimulator(sim); } }); }); diff --git a/test/functional/helpers/simulator.js b/test/functional/helpers/simulator.js index 75509bf5f..64c92c193 100644 --- a/test/functional/helpers/simulator.js +++ b/test/functional/helpers/simulator.js @@ -5,7 +5,7 @@ import {resetTestProcesses} from 'appium-webdriveragent'; import {shutdownSimulator} from '../../../lib/simulator-management'; import {killAllSimulators as simKill} from 'appium-ios-simulator'; -async function killAllSimulators() { +export async function killAllSimulators() { const simctl = new Simctl(); const allDevices = _.flatMap(_.values(await simctl.getDevices())); const bootedDevices = allDevices.filter((device) => device.state === 'Booted'); @@ -20,11 +20,26 @@ async function killAllSimulators() { await simKill(); } -async function deleteDeviceWithRetry(udid) { +/** + * @param {string} udid + */ +export async function deleteDeviceWithRetry(udid) { const simctl = new Simctl({udid}); try { await retryInterval(10, 1000, simctl.deleteDevice.bind(simctl)); } catch (ign) {} } -export {killAllSimulators, shutdownSimulator, deleteDeviceWithRetry}; +/** + * @param {import('appium-ios-simulator').Simulator} [sim] + */ +export async function cleanupSimulator(sim) { + if (!sim) { + return; + } + await resetTestProcesses(sim.udid, true); + await sim.shutdown(); + await deleteDeviceWithRetry(sim.udid); +} + +export {shutdownSimulator}; diff --git a/test/functional/tv/tvos-e2e-specs.js b/test/functional/tv/tvos-e2e-specs.js index 8f031f6fe..340ca19a4 100644 --- a/test/functional/tv/tvos-e2e-specs.js +++ b/test/functional/tv/tvos-e2e-specs.js @@ -1,7 +1,7 @@ import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; import {getSimulator} from 'appium-ios-simulator'; -import {shutdownSimulator, deleteDeviceWithRetry} from '../helpers/simulator'; +import {cleanupSimulator} from '../helpers/simulator'; import Simctl from 'node-simctl'; import {MOCHA_TIMEOUT, initSession, deleteSession} from '../helpers/session'; import {TVOS_CAPS} from '../desired'; @@ -33,8 +33,7 @@ describe('tvOS', function () { platform: TVOS_CAPS.platformName, checkExistence: false, }); - await shutdownSimulator(sim); - await deleteDeviceWithRetry(udid); + await cleanupSimulator(sim); } }); diff --git a/test/unit/driver-specs.js b/test/unit/driver-specs.js index 7d5fd2ed2..215bcc1cd 100644 --- a/test/unit/driver-specs.js +++ b/test/unit/driver-specs.js @@ -283,7 +283,6 @@ describe('XCUITestDriver', function () { expect(driver.isRealDevice).to.have.been.calledOnce; expect(driver.helpers.configureApp).to.have.been.calledOnce; expect(RealDeviceManagementModule.installToRealDevice).to.have.been.calledOnceWith( - 'some-device', '/path/to/iosApp.app', 'bundle-id', {skipUninstall: true, timeout: undefined, strategy: undefined}, @@ -307,13 +306,11 @@ describe('XCUITestDriver', function () { expect(driver.isRealDevice).to.have.been.calledTwice; expect(driver.helpers.configureApp).to.have.been.calledTwice; expect(RealDeviceManagementModule.installToRealDevice).to.have.been.calledWith( - 'some-device', '/path/to/iosApp1.app', 'bundle-id', {skipUninstall: true, timeout: undefined, strategy: undefined}, ); expect(RealDeviceManagementModule.installToRealDevice).to.have.been.calledWith( - 'some-device', '/path/to/iosApp2.app', 'bundle-id2', {skipUninstall: true, timeout: undefined, strategy: undefined}, @@ -334,7 +331,6 @@ describe('XCUITestDriver', function () { expect(driver.isRealDevice).to.have.been.calledOnce; expect(driver.helpers.configureApp).to.have.been.calledOnce; expect(SimulatorManagementModule.installToSimulator).to.have.been.calledOnceWith( - 'some-device', '/path/to/iosApp.app', 'bundle-id', {newSimulator: false}, @@ -359,13 +355,11 @@ describe('XCUITestDriver', function () { expect(driver.isRealDevice).to.have.been.calledTwice; expect(driver.helpers.configureApp).to.have.been.calledTwice; expect(SimulatorManagementModule.installToSimulator).to.have.been.calledWith( - 'some-device', '/path/to/iosApp1.app', 'bundle-id', {newSimulator: false}, ); expect(SimulatorManagementModule.installToSimulator).to.have.been.calledWith( - 'some-device', '/path/to/iosApp2.app', 'bundle-id2', {newSimulator: false}, diff --git a/test/unit/real-device-management-specs.js b/test/unit/real-device-management-specs.js index 87e15cd3e..7af56dfa1 100644 --- a/test/unit/real-device-management-specs.js +++ b/test/unit/real-device-management-specs.js @@ -4,6 +4,7 @@ import {createSandbox} from 'sinon'; import sinonChai from 'sinon-chai'; import { installToRealDevice } from '../../lib/real-device-management'; import RealDevice from '../../lib/real-device'; +import XCUITestDriver from '../../lib/driver'; chai.should(); chai.use(sinonChai).use(chaiAsPromised); @@ -17,8 +18,10 @@ describe('installToRealDevice', function () { /** @type {sinon.SinonSandbox} */ let sandbox; + let driver; beforeEach(function () { sandbox = createSandbox(); + driver = new XCUITestDriver(); }); afterEach(function () { @@ -29,8 +32,12 @@ describe('installToRealDevice', function () { const realDevice = new RealDevice(udid); sandbox.stub(realDevice, 'remove').resolves(); sandbox.stub(realDevice, 'install').resolves(); + driver.opts = { + udid, + device: realDevice, + }; - await installToRealDevice(realDevice, undefined, bundleId, {}); + await installToRealDevice.bind(driver)(undefined, bundleId, {}); expect(realDevice.remove).to.not.have.been.called; expect(realDevice.install).to.not.have.been.called; }); @@ -39,8 +46,12 @@ describe('installToRealDevice', function () { const realDevice = new RealDevice(udid); sandbox.stub(realDevice, 'remove').resolves(); sandbox.stub(realDevice, 'install').resolves(); + driver.opts = { + udid, + device: realDevice, + }; - await installToRealDevice(realDevice, app, undefined, {}); + await installToRealDevice.bind(driver)(app, undefined, {}); expect(realDevice.remove).to.not.have.been.called; expect(realDevice.install).to.not.have.been.called; }); @@ -52,8 +63,12 @@ describe('installToRealDevice', function () { const realDevice = new RealDevice(udid); sandbox.stub(realDevice, 'remove').resolves(); sandbox.stub(realDevice, 'install').resolves(); + driver.opts = { + udid, + device: realDevice, + }; - await installToRealDevice(realDevice, app, bundleId, opts); + await installToRealDevice.bind(driver)(app, bundleId, opts); expect(realDevice.remove).to.not.have.been.called; expect(realDevice.install).to.have.been.calledOnce; @@ -66,8 +81,12 @@ describe('installToRealDevice', function () { const realDevice = new RealDevice(udid); sandbox.stub(realDevice, 'remove').resolves(); sandbox.stub(realDevice, 'install').resolves(); + driver.opts = { + udid, + device: realDevice, + }; - await installToRealDevice(realDevice, app, bundleId, opts); + await installToRealDevice.bind(driver)(app, bundleId, opts); expect(realDevice.remove).to.have.been.calledOnce; expect(realDevice.install).to.have.been.calledOnce; @@ -81,8 +100,12 @@ describe('installToRealDevice', function () { const realDevice = new RealDevice(udid); sandbox.stub(realDevice, 'remove').resolves(); sandbox.stub(realDevice, 'install').throws(err_msg); + driver.opts = { + udid, + device: realDevice, + }; - await installToRealDevice(realDevice, app, bundleId, opts).should.be.rejectedWith('ApplicationVerificationFailed'); + await installToRealDevice.bind(driver)(app, bundleId, opts).should.be.rejectedWith('ApplicationVerificationFailed'); expect(realDevice.remove).to.have.been.calledOnce; expect(realDevice.install).to.have.been.calledOnce; }); @@ -98,8 +121,12 @@ describe('installToRealDevice', function () { sandbox.stub(realDevice, 'install') .onCall(0).throws(`{"Error":"MismatchedApplicationIdentifierEntitlement","ErrorDescription":"Upgrade's application-identifier entitlement string (TEAM_ID.com.kazucocoa.example) does not match installed application's application-identifier string (ANOTHER_TEAM_ID.com.kazucocoa.example); rejecting upgrade."}`) .onCall(1).resolves(); + driver.opts = { + udid, + device: realDevice, + }; - await installToRealDevice(realDevice, app, bundleId, opts); + await installToRealDevice.bind(driver)(app, bundleId, opts); expect(realDevice.remove).to.have.been.calledOnce; expect(realDevice.install).to.have.been.calledTwice; @@ -114,8 +141,12 @@ describe('installToRealDevice', function () { sandbox.stub(realDevice, 'remove').resolves(); sandbox.stub(realDevice, 'install').throws(err_msg); sandbox.stub(realDevice, 'isAppInstalled').resolves(true); + driver.opts = { + udid, + device: realDevice, + }; - await installToRealDevice(realDevice, app, bundleId, opts).should.be.rejectedWith('ApplicationVerificationFailed'); + await installToRealDevice.bind(driver)(app, bundleId, opts).should.be.rejectedWith('ApplicationVerificationFailed'); expect(realDevice.remove).to.not.have.been.called; expect(realDevice.install).to.have.been.calledOnce; }); diff --git a/test/unit/simulator-management-specs.js b/test/unit/simulator-management-specs.js index d54586943..bd13eeedc 100644 --- a/test/unit/simulator-management-specs.js +++ b/test/unit/simulator-management-specs.js @@ -1,11 +1,13 @@ import {runSimulatorReset} from '../../lib/simulator-management.js'; import chai from 'chai'; +import XCUITestDriver from '../../lib/driver'; const should = chai.should(); describe('simulator management', function () { describe('runSimulatorReset', function () { let result; + let driver; const stoppedDeviceDummy = { isRunning: () => false, scrubApp: (bundleId) => { @@ -19,78 +21,86 @@ describe('simulator management', function () { beforeEach(function () { result = undefined; + driver = new XCUITestDriver(); }); it('should call scrubApp with fastReset', async function () { - const opts = { + driver.opts = { udid: '301CD634-00A9-4042-B463-BD4E755167EA', bundleId: 'io.appium.example', noReset: false, fullReset: false, + device: stoppedDeviceDummy, }; - await runSimulatorReset(stoppedDeviceDummy, opts); + await runSimulatorReset.bind(driver)(); result.bundleId.should.eql('io.appium.example'); }); it('should return immediately with noReset', async function () { - const opts = { + driver.opts = { udid: '301CD634-00A9-4042-B463-BD4E755167EA', bundleId: 'io.appium.example', noReset: true, fullReset: false, + device: stoppedDeviceDummy, }; - await runSimulatorReset(stoppedDeviceDummy, opts); + await runSimulatorReset.bind(driver)(); should.equal(result, undefined); }); it('should call clean with fullRest', async function () { - const opts = { + driver.opts = { udid: '301CD634-00A9-4042-B463-BD4E755167EA', bundleId: 'io.appium.example', noReset: false, fullReset: true, + device: stoppedDeviceDummy, }; - await runSimulatorReset(stoppedDeviceDummy, opts); + await runSimulatorReset.bind(driver)(); result.should.eql('cleaned'); }); it('should not call scrubApp with fastReset and app', async function () { - const opts = { + driver.opts = { udid: '301CD634-00A9-4042-B463-BD4E755167EA', bundleId: 'io.appium.example', app: 'path/to/app.app', noReset: false, fullReset: false, + device: stoppedDeviceDummy, }; - await runSimulatorReset(stoppedDeviceDummy, opts); + await runSimulatorReset.bind(driver)(); should.equal(result, undefined); }); it('should return immediately with noReset and app', async function () { - const opts = { + driver.opts = { udid: '301CD634-00A9-4042-B463-BD4E755167EA', bundleId: 'io.appium.example', app: 'path/to/app.app', noReset: true, fullReset: false, + device: stoppedDeviceDummy, }; - await runSimulatorReset(stoppedDeviceDummy, opts); + await runSimulatorReset.bind(driver)(); should.equal(result, undefined); }); it('should call clean with fullRest and app', async function () { - const opts = { + driver.opts = { udid: '301CD634-00A9-4042-B463-BD4E755167EA', bundleId: 'io.appium.example', app: 'path/to/app.app', noReset: false, fullReset: true, + device: stoppedDeviceDummy, }; - await runSimulatorReset(stoppedDeviceDummy, opts); + await runSimulatorReset.bind(driver)(); result.should.eql('cleaned'); }); it('should not call scrubApp with fastReset, but no bundleid and app', async function () { - const opts = { + driver.opts = { udid: '301CD634-00A9-4042-B463-BD4E755167EA', noReset: false, fullReset: false, + device: stoppedDeviceDummy, }; - await runSimulatorReset(stoppedDeviceDummy, opts); + await runSimulatorReset.bind(driver)(); should.equal(result, undefined); }); });