From 627adddbffeb4a7fc154143f57dc568348d69e9e Mon Sep 17 00:00:00 2001 From: d4vidi Date: Mon, 13 Jun 2022 21:46:54 +0300 Subject: [PATCH 01/24] [wip] Introduce TestApp and app-driver for Android (mostly) --- detox/package.json | 1 + detox/src/DeviceAPI.js | 225 ++++ detox/src/devices/runtime/RuntimeDevice.js | 432 ++----- .../src/devices/runtime/RuntimeDevice.test.js | 1033 ----------------- detox/src/devices/runtime/TestApp.js | 244 ++++ .../devices/runtime/drivers/BaseDrivers.js | 165 +++ .../runtime/drivers/DeviceDriverBase.js | 227 ---- .../{AndroidDriver.js => AndroidDrivers.js} | 486 ++++---- ...dDriver.test.js => AndroidDrivers.test.js} | 0 .../android/attached/AttachedAndroidDriver.js | 3 +- .../android/emulator/EmulatorDriver.js | 50 +- .../android/genycloud/GenyCloudDriver.js | 5 +- .../devices/runtime/drivers/ios/IosDriver.js | 41 - .../devices/runtime/drivers/ios/IosDrivers.js | 75 ++ ...ulatorDriver.js => IosSimulatorDrivers.js} | 239 ++-- ...er.test.js => IosSimulatorDrivers.test.js} | 0 .../src/devices/runtime/factories/android.js | 64 +- detox/src/devices/runtime/factories/base.js | 48 +- detox/src/utils/p-iteration.js | 13 + detox/src/utils/tempFile.js | 21 + detox/src/utils/tempFile.test.js | 33 + .../integration/stub/StubRuntimeDriver.js | 2 +- examples/demo-plugin/driver.js | 4 +- 23 files changed, 1422 insertions(+), 1989 deletions(-) create mode 100644 detox/src/DeviceAPI.js delete mode 100644 detox/src/devices/runtime/RuntimeDevice.test.js create mode 100644 detox/src/devices/runtime/TestApp.js create mode 100644 detox/src/devices/runtime/drivers/BaseDrivers.js delete mode 100644 detox/src/devices/runtime/drivers/DeviceDriverBase.js rename detox/src/devices/runtime/drivers/android/{AndroidDriver.js => AndroidDrivers.js} (61%) rename detox/src/devices/runtime/drivers/android/{AndroidDriver.test.js => AndroidDrivers.test.js} (100%) delete mode 100644 detox/src/devices/runtime/drivers/ios/IosDriver.js create mode 100644 detox/src/devices/runtime/drivers/ios/IosDrivers.js rename detox/src/devices/runtime/drivers/ios/{SimulatorDriver.js => IosSimulatorDrivers.js} (58%) rename detox/src/devices/runtime/drivers/ios/{SimulatorDriver.test.js => IosSimulatorDrivers.test.js} (100%) create mode 100644 detox/src/utils/p-iteration.js create mode 100644 detox/src/utils/tempFile.js create mode 100644 detox/src/utils/tempFile.test.js diff --git a/detox/package.json b/detox/package.json index bbdf3ce81d..ded00d2fda 100644 --- a/detox/package.json +++ b/detox/package.json @@ -65,6 +65,7 @@ "ini": "^1.3.4", "lodash": "^4.17.5", "minimist": "^1.2.0", + "p-iteration": "^1.1.8", "proper-lockfile": "^3.0.2", "resolve-from": "^5.0.0", "sanitize-filename": "^1.6.1", diff --git a/detox/src/DeviceAPI.js b/detox/src/DeviceAPI.js new file mode 100644 index 0000000000..4f310f30fb --- /dev/null +++ b/detox/src/DeviceAPI.js @@ -0,0 +1,225 @@ +const _ = require('lodash'); + +const wrapWithStackTraceCutter = require('./utils/wrapWithStackTraceCutter'); + +class DeviceAPI { + + /** + * @param device { RuntimeDevice } + * @param errorComposer { DetoxRuntimeErrorComposer } + */ + constructor(device, errorComposer) { + wrapWithStackTraceCutter(this, [ // TODO replace with Object.keys() ? + 'captureViewHierarchy', + 'clearKeychain', + 'disableSynchronization', + 'enableSynchronization', + 'installApp', + 'launchApp', + 'matchFace', + 'matchFinger', + 'openURL', + 'pressBack', + 'relaunchApp', + 'reloadReactNative', + 'resetContentAndSettings', + 'resetStatusBar', + 'reverseTcpPort', + 'selectApp', + 'sendToHome', + 'sendUserActivity', + 'sendUserNotification', + 'setBiometricEnrollment', + 'setLocation', + 'setOrientation', + 'setStatusBar', + 'setURLBlacklist', + 'shake', + 'takeScreenshot', + 'terminateApp', + 'uninstallApp', + 'unmatchFace', + 'unmatchFinger', + 'unreverseTcpPort', + ]); + + this.device = device; + + this._errorComposer = errorComposer; + } + + get id() { + return this.device.id; + } + + get name() { + return this.device.name; + } + + get type() { + return this.device.type; + } + + get platform() { + return this.device.platform; + } + + /** + * @deprecated Use 'platform' + */ + getPlatform() { + return this.platform; + } + + async selectApp(aliasOrConfig) { + if (aliasOrConfig === undefined) { + throw this._errorComposer.cantSelectEmptyApp(); + } + + let alias; + if (_.isObject(aliasOrConfig)) { + const appConfig = aliasOrConfig; + await this.device.selectUnspecifiedApp(appConfig); + } else { + alias = aliasOrConfig; + await this.device.selectPredefinedApp(alias); + } + } + + /** TODO (multiapps) Contract change: no appId; Only works on currently selected app */ + async launchApp(params = {}) { + return this.device.selectedApp.launch(params); + } + + /** + * @deprecated + */ + async relaunchApp(params = {}) { + if (params.newInstance === undefined) { + params.newInstance = true; + } + return this.launchApp(params); + } + + async takeScreenshot(name) { + return this.device.takeScreenshot(name); + } + + async captureViewHierarchy(name = 'capture') { + return this.device.captureViewHierarchy(name); + } + + async sendToHome() { + return this.device.sendToHome(); + } + + async pressBack() { + return this.device.pressBack(); + } + + async setBiometricEnrollment(toggle) { + const yesOrNo = toggle ? 'YES' : 'NO'; + return this.device.setBiometricEnrollment(yesOrNo); + } + + async matchFace() { + await this.device.selectedApp.matchFace(); + } + + async unmatchFace() { + return this.device.selectedApp.unmatchFace(); + } + + async matchFinger() { + return this.device.selectedApp.matchFinger(); + } + + async unmatchFinger() { + return this.device.selectedApp.unmatchFinger(); + } + + async shake() { + return this.device.selectedApp.shake(); + } + + async setOrientation(orientation) { + return this.device.selectedApp.setOrientation(orientation); + } + + // TODO (multiapps) contract change: no freestyle app ID accepted anymore + async terminateApp() { + return this.device.selectedApp.terminate(); + } + + // TODO (multiapps) contract change: no freestyle installs with app/apk path(s) + async installApp() { + return this.device.selectedApp.install(); + } + + // TODO (multiapps) contract change: no freestyle app ID accepted anymore + async uninstallApp() { + return this.device.selectedApp.uninstall(); + } + + async reloadReactNative() { + return this.device.selectedApp.reloadReactNative(); + } + + async openURL(params) { + return this.device.selectedApp.openURL(params); + } + + async setLocation(lat, lon) { + return this.device.setLocation(lat, lon); + } + + async reverseTcpPort(port) { + return this.device.reverseTcpPort(port); + } + + async unreverseTcpPort(port) { + return this.device.unreverseTcpPort(port); + } + + async clearKeychain() { + return this.device.clearKeychain(); + } + + async sendUserActivity(payload) { + return this.device.selectedApp.sendUserActivity(payload); + } + + async sendUserNotification(payload) { + return this.device.selectedApp.sendUserNotification(payload); + } + + async setURLBlacklist(urlList) { + return this.device.selectedApp.setURLBlacklist(urlList); + } + + async enableSynchronization() { + return this.device.selectedApp.enableSynchronization(); + } + + async disableSynchronization() { + return this.device.selectedApp.disableSynchronization(); + } + + async resetContentAndSettings() { + return this.device.selectedApp.resetContentAndSettings(); + } + + getUiDevice() { + return this.device.selectedApp.uiDevice; + } + + async setStatusBar(params) { + return this.device.setStatusBar(params); + } + + async resetStatusBar() { + return this.device.resetStatusBar(); + } +} + +module.exports = DeviceAPI; diff --git a/detox/src/devices/runtime/RuntimeDevice.js b/detox/src/devices/runtime/RuntimeDevice.js index 336b15915c..a5684f7701 100644 --- a/detox/src/devices/runtime/RuntimeDevice.js +++ b/detox/src/devices/runtime/RuntimeDevice.js @@ -1,423 +1,155 @@ const DetoxRuntimeError = require('../../errors/DetoxRuntimeError'); -const debug = require('../../utils/debug'); // debug utils, leave here even if unused +const { forEachSeries } = require('../../utils/p-iteration'); const { traceCall } = require('../../utils/trace'); -const wrapWithStackTraceCutter = require('../../utils/wrapWithStackTraceCutter'); - -const LaunchArgsEditor = require('./utils/LaunchArgsEditor'); class RuntimeDevice { - constructor({ - appsConfig, - behaviorConfig, - deviceConfig, - eventEmitter, - sessionConfig, - runtimeErrorComposer, - }, deviceDriver) { - wrapWithStackTraceCutter(this, [ - 'captureViewHierarchy', - 'clearKeychain', - 'disableSynchronization', - 'enableSynchronization', - 'installApp', - 'launchApp', - 'matchFace', - 'matchFinger', - 'openURL', - 'pressBack', - 'relaunchApp', - 'reloadReactNative', - 'resetContentAndSettings', - 'resetStatusBar', - 'reverseTcpPort', - 'selectApp', - 'sendToHome', - 'sendUserActivity', - 'sendUserNotification', - 'setBiometricEnrollment', - 'setLocation', - 'setOrientation', - 'setStatusBar', - 'setURLBlacklist', - 'shake', - 'takeScreenshot', - 'terminateApp', - 'uninstallApp', - 'unmatchFace', - 'unmatchFinger', - 'unreverseTcpPort', - ]); - - this._appsConfig = appsConfig; - this._behaviorConfig = behaviorConfig; + /** + * @param apps { Object } + * @param apps.predefinedApps { Object } + * @param apps.unspecifiedApp { UnspecifiedTestApp } + * @param apps.utilApps { UtilApp } + */ + constructor({ predefinedApps, unspecifiedApp, utilApps }, deps, { deviceConfig }) { + this._predefinedApps = predefinedApps; + this._unspecifiedApp = unspecifiedApp; + this._utilApps = utilApps; + this._driver = deps.driver; + this._errorComposer = deps.errorComposer; + this._eventEmitter = deps.eventEmitter; this._deviceConfig = deviceConfig; - this._sessionConfig = sessionConfig; - this._emitter = eventEmitter; - this._errorComposer = runtimeErrorComposer; - this._currentApp = null; - this._currentAppLaunchArgs = new LaunchArgsEditor(); - this._processes = {}; + this._selectedApp = null; + } + + async init() { + const appAliases = Object.keys(this._predefinedApps); + if (appAliases.length === 1) { + const appAlias = appAliases[0]; + this._selectedApp = this._predefinedApps[appAlias]; + } + + await this._initApps(); + } - this.deviceDriver = deviceDriver; - this.deviceDriver.validateDeviceConfig(deviceConfig); - this.debug = debug; + /** + * @returns { RunnableTestApp } + */ + get selectedApp() { + return this._selectedApp; } get id() { - return this.deviceDriver.getExternalId(); + return this._driver.getExternalId(); } get name() { - return this.deviceDriver.getDeviceName(); - } - - get type() { - return this._deviceConfig.type; + return this._driver.getDeviceName(); } - get appLaunchArgs() { - return this._currentAppLaunchArgs; + get platform() { + return this._driver.platform(); } - async _prepare() { - const appAliases = Object.keys(this._appsConfig); - if (appAliases.length === 1) { - await this.selectApp(appAliases[0]); - } + get type() { + return this._deviceConfig.type; } - async selectApp(name) { - if (name === undefined) { - throw this._errorComposer.cantSelectEmptyApp(); + async selectPredefinedApp(appAlias) { + const app = this._predefinedApps[appAlias]; + if (!app) { + throw this._errorComposer.cantFindApp(app); } - if (this._currentApp) { - await this.terminateApp(); + if (this._selectedApp) { + await this._selectedApp.deselect(); } - if (name === null) { // Internal use to unselect the app - this._currentApp = null; - return; - } + this._selectedApp = app; + this._selectedApp.select(); + } - const appConfig = this._appsConfig[name]; - if (!appConfig) { - throw this._errorComposer.cantFindApp(name); + async selectUnspecifiedApp(appConfig) { + if (this._selectedApp) { + await this._selectedApp.deselect(); } - this._currentApp = appConfig; - this._currentAppLaunchArgs.reset(); - this._currentAppLaunchArgs.modify(this._currentApp.launchArgs); - await this._inferBundleIdFromBinary(); + this._selectedApp = this._unspecifiedApp; + await this._selectedApp.select(appConfig); } - async launchApp(params = {}, bundleId = this._bundleId) { - return traceCall('launchApp', () => this._doLaunchApp(params, bundleId)); + async installUtilBinaries() { + await traceCall('installUtilBinaries', () => { + forEachSeries(this._utilApps, (app) => app.install(), this); + }); } - /** - * @deprecated - */ - async relaunchApp(params = {}, bundleId) { - if (params.newInstance === undefined) { - params['newInstance'] = true; + async reinstallApps(appAliases) { + for (const appAlias of appAliases) { + const app = this._predefinedApps[appAlias]; + await app.uninstall(); + await app.install(); } - await this.launchApp(params, bundleId); } async takeScreenshot(name) { if (!name) { - throw new DetoxRuntimeError('Cannot take a screenshot with an empty name.'); + throw new DetoxRuntimeError({ message: 'Cannot take a screenshot with an empty name.' }); } - - return this.deviceDriver.takeScreenshot(name); + return this._driver.takeScreenshot(name); } - async captureViewHierarchy(name = 'capture') { - return this.deviceDriver.captureViewHierarchy(name); + async captureViewHierarchy(name) { + return this._driver.captureViewHierarchy(name); } async sendToHome() { - await this.deviceDriver.sendToHome(); - await this.deviceDriver.waitForBackground(); - } - - async setBiometricEnrollment(toggle) { - const yesOrNo = toggle ? 'YES' : 'NO'; - await this.deviceDriver.setBiometricEnrollment(yesOrNo); - } - - async matchFace() { - await this.deviceDriver.matchFace(); - await this.deviceDriver.waitForActive(); + await this._driver.sendToHome(); } - async unmatchFace() { - await this.deviceDriver.unmatchFace(); - await this.deviceDriver.waitForActive(); - } - - async matchFinger() { - await this.deviceDriver.matchFinger(); - await this.deviceDriver.waitForActive(); + async pressBack() { + await this._driver.pressBack(); } - async unmatchFinger() { - await this.deviceDriver.unmatchFinger(); - await this.deviceDriver.waitForActive(); + async setBiometricEnrollment(yesOrNo) { + await this._driver.setBiometricEnrollment(yesOrNo); } async shake() { - await this.deviceDriver.shake(); - } - - async terminateApp(bundleId) { - const _bundleId = bundleId || this._bundleId; - await this.deviceDriver.terminate(_bundleId); - this._processes[_bundleId] = undefined; - } - - async installApp(binaryPath, testBinaryPath) { - await traceCall('appInstall', () => { - const currentApp = binaryPath ? { binaryPath, testBinaryPath } : this._getCurrentApp(); - return this.deviceDriver.installApp(currentApp.binaryPath, currentApp.testBinaryPath); - }); - } - - async uninstallApp(bundleId) { - const _bundleId = bundleId || this._bundleId; - await traceCall('appUninstall', () => - this.deviceDriver.uninstallApp(_bundleId)); - } - - async installUtilBinaries() { - const paths = this._deviceConfig.utilBinaryPaths; - if (paths) { - await traceCall('installUtilBinaries', () => - this.deviceDriver.installUtilBinaries(paths)); - } - } - - async reloadReactNative() { - await traceCall('reloadRN', () => - this.deviceDriver.reloadReactNative()); + await this._driver.shake(); } - async openURL(params) { - if (typeof params !== 'object' || !params.url) { - throw new DetoxRuntimeError(`openURL must be called with JSON params, and a value for 'url' key must be provided. example: await device.openURL({url: "url", sourceApp[optional]: "sourceAppBundleID"}`); - } - - await this.deviceDriver.deliverPayload(params); + async setStatusBar(params) { + return this._driver.setStatusBar(params); } - async setOrientation(orientation) { - await this.deviceDriver.setOrientation(orientation); + async resetStatusBar(params) { + return this._driver.resetStatusBar(params); } async setLocation(lat, lon) { lat = String(lat); lon = String(lon); - await this.deviceDriver.setLocation(lat, lon); + await this._driver.setLocation(lat, lon); } async reverseTcpPort(port) { - await this.deviceDriver.reverseTcpPort(port); + await this._driver.reverseTcpPort(port); } async unreverseTcpPort(port) { - await this.deviceDriver.unreverseTcpPort(port); + await this._driver.unreverseTcpPort(port); } async clearKeychain() { - await this.deviceDriver.clearKeychain(); - } - - async sendUserActivity(params) { - await this._sendPayload('detoxUserActivityDataURL', params); - } - - async sendUserNotification(params) { - await this._sendPayload('detoxUserNotificationDataURL', params); + await this._driver.clearKeychain(); } - async setURLBlacklist(urlList) { - await this.deviceDriver.setURLBlacklist(urlList); + async _initApps() { + await forEachSeries(this._allApps(), (app) => app.init(), this); } - async enableSynchronization() { - await this.deviceDriver.enableSynchronization(); - } - - async disableSynchronization() { - await this.deviceDriver.disableSynchronization(); - } - - async resetContentAndSettings() { - await this.deviceDriver.resetContentAndSettings(); - } - - getPlatform() { - return this.deviceDriver.getPlatform(); - } - - async _cleanup() { - const bundleId = this._currentApp && this._currentApp.bundleId; - await this.deviceDriver.cleanup(bundleId); - } - - async pressBack() { - await this.deviceDriver.pressBack(); - } - - getUiDevice() { - return this.deviceDriver.getUiDevice(); - } - - async setStatusBar(params) { - await this.deviceDriver.setStatusBar(params); - } - - async resetStatusBar() { - await this.deviceDriver.resetStatusBar(); - } - - /** - * @internal - */ - async _typeText(text) { - await this.deviceDriver.typeText(text); - } - - get _bundleId() { - return this._getCurrentApp().bundleId; - } - - _getCurrentApp() { - if (!this._currentApp) { - throw this._errorComposer.appNotSelected(); - } - return this._currentApp; - } - - async _doLaunchApp(params, bundleId) { - const payloadParams = ['url', 'userNotification', 'userActivity']; - const hasPayload = this._assertHasSingleParam(payloadParams, params); - const newInstance = params.newInstance !== undefined - ? params.newInstance - : this._processes[bundleId] == null; - - if (params.delete) { - await this.terminateApp(bundleId); - await this.uninstallApp(); - await this.installApp(); - } else if (newInstance) { - await this.terminateApp(bundleId); - } - - const baseLaunchArgs = { - ...this._currentAppLaunchArgs.get(), - ...params.launchArgs, - }; - - if (params.url) { - baseLaunchArgs['detoxURLOverride'] = params.url; - if (params.sourceApp) { - baseLaunchArgs['detoxSourceAppOverride'] = params.sourceApp; - } - } else if (params.userNotification) { - this._createPayloadFileAndUpdatesParamsObject('userNotification', 'detoxUserNotificationDataURL', params, baseLaunchArgs); - } else if (params.userActivity) { - this._createPayloadFileAndUpdatesParamsObject('userActivity', 'detoxUserActivityDataURL', params, baseLaunchArgs); - } - - if (params.permissions) { - await this.deviceDriver.setPermissions(bundleId, params.permissions); - } - - if (params.disableTouchIndicators) { - baseLaunchArgs['detoxDisableTouchIndicators'] = true; - } - - if (this._isAppRunning(bundleId) && hasPayload) { - await this.deviceDriver.deliverPayload({ ...params, delayPayload: true }); - } - - if (this._behaviorConfig.launchApp === 'manual') { - this._processes[bundleId] = await this.deviceDriver.waitForAppLaunch(bundleId, this._prepareLaunchArgs(baseLaunchArgs), params.languageAndLocale); - } else { - this._processes[bundleId] = await this.deviceDriver.launchApp(bundleId, this._prepareLaunchArgs(baseLaunchArgs), params.languageAndLocale); - await this.deviceDriver.waitUntilReady(); - await this.deviceDriver.waitForActive(); - } - - await this._emitter.emit('appReady', { - deviceId: this.deviceDriver.getExternalId(), - bundleId, - pid: this._processes[bundleId], - }); - - if(params.detoxUserNotificationDataURL) { - await this.deviceDriver.cleanupRandomDirectory(params.detoxUserNotificationDataURL); - } - - if(params.detoxUserActivityDataURL) { - await this.deviceDriver.cleanupRandomDirectory(params.detoxUserActivityDataURL); - } - } - - async _sendPayload(key, params) { - const payloadFilePath = this.deviceDriver.createPayloadFile(params); - const payload = { - [key]: payloadFilePath, - }; - await this.deviceDriver.deliverPayload(payload); - this.deviceDriver.cleanupRandomDirectory(payloadFilePath); - } - - _createPayloadFileAndUpdatesParamsObject(key, launchKey, params, baseLaunchArgs) { - const payloadFilePath = this.deviceDriver.createPayloadFile(params[key]); - baseLaunchArgs[launchKey] = payloadFilePath; - //`params` will be used later for `predeliverPayload`, so remove the actual notification and add the file URL - delete params[key]; - params[launchKey] = payloadFilePath; - } - - _isAppRunning(bundleId = this._bundleId) { - return this._processes[bundleId] != null; - } - - _assertHasSingleParam(singleParams, params) { - let paramsCounter = 0; - - singleParams.forEach((item) => { - if(params[item]) { - paramsCounter += 1; - } - }); - - if (paramsCounter > 1) { - throw new DetoxRuntimeError(`Call to 'launchApp(${JSON.stringify(params)})' must contain only one of ${JSON.stringify(singleParams)}.`); - } - - return (paramsCounter === 1); - } - - _prepareLaunchArgs(additionalLaunchArgs) { - return { - detoxServer: this._sessionConfig.server, - detoxSessionId: this._sessionConfig.sessionId, - ...additionalLaunchArgs - }; - } - - async _inferBundleIdFromBinary() { - const { binaryPath, bundleId } = this._currentApp; - - if (!bundleId) { - this._currentApp.bundleId = await this.deviceDriver.getBundleIdFromBinary(binaryPath); - } + _allApps() { + return [...Object.values(this._predefinedApps), this._unspecifiedApp, ...this._utilApps]; } } diff --git a/detox/src/devices/runtime/RuntimeDevice.test.js b/detox/src/devices/runtime/RuntimeDevice.test.js deleted file mode 100644 index 02e03398de..0000000000 --- a/detox/src/devices/runtime/RuntimeDevice.test.js +++ /dev/null @@ -1,1033 +0,0 @@ -// @ts-nocheck -const _ = require('lodash'); - -const configurationsMock = require('../../configuration/configurations.mock'); - -describe('Device', () => { - const bundleId = 'test.bundle'; - - let DeviceDriverBase; - let DetoxRuntimeErrorComposer; - let errorComposer; - let emitter; - let RuntimeDevice; - let argparse; - let Client; - let client; - let driverMock; - - beforeEach(async () => { - jest.mock('../../utils/logger'); - jest.mock('../../utils/trace'); - - jest.mock('../../utils/argparse'); - argparse = require('../../utils/argparse'); - - jest.mock('./drivers/DeviceDriverBase'); - DeviceDriverBase = require('./drivers/DeviceDriverBase'); - - jest.mock('../../client/Client'); - Client = require('../../client/Client'); - - jest.mock('../../utils/AsyncEmitter'); - const AsyncEmitter = require('../../utils/AsyncEmitter'); - emitter = new AsyncEmitter({}); - DetoxRuntimeErrorComposer = require('../../errors/DetoxRuntimeErrorComposer'); - - RuntimeDevice = require('./RuntimeDevice'); - }); - - beforeEach(async () => { - client = new Client(configurationsMock.validSession); - await client.connect(); - - driverMock = new DeviceDriverMock(); - }); - - class DeviceDriverMock { - constructor() { - this.driver = new DeviceDriverBase({ - client, - emitter, - }); - } - - expectExternalIdCalled() { - expect(this.driver.getExternalId).toHaveBeenCalled(); - } - - expectLaunchCalledWithArgs(bundleId, expectedArgs, languageAndLocale) { - expect(this.driver.launchApp).toHaveBeenCalledWith(bundleId, expectedArgs, languageAndLocale); - } - - expectLaunchCalledContainingArgs(expectedArgs) { - expect(this.driver.launchApp).toHaveBeenCalledWith( - this.driver.getBundleIdFromBinary(), - expect.objectContaining(expectedArgs), - undefined); - } - - expectWaitForLaunchCalled(bundleId, expectedArgs, languageAndLocale) { - expect(this.driver.waitForAppLaunch).toHaveBeenCalledWith(bundleId, expectedArgs, languageAndLocale); - } - - expectReinstallCalled() { - expect(this.driver.uninstallApp).toHaveBeenCalled(); - expect(this.driver.installApp).toHaveBeenCalled(); - } - - expectReinstallNotCalled() { - expect(this.driver.uninstallApp).not.toHaveBeenCalled(); - expect(this.driver.installApp).not.toHaveBeenCalled(); - } - - expectTerminateCalled() { - expect(this.driver.terminate).toHaveBeenCalled(); - } - - expectTerminateNotCalled() { - expect(this.driver.terminate).not.toHaveBeenCalled(); - } - - expectReverseTcpPortCalled(port) { - expect(this.driver.reverseTcpPort).toHaveBeenCalledWith(port); - } - - expectUnreverseTcpPortCalled(port) { - expect(this.driver.unreverseTcpPort).toHaveBeenCalledWith(port); - } - } - - function aDevice(overrides) { - const appsConfig = overrides.appsConfig || {}; - errorComposer = new DetoxRuntimeErrorComposer({ appsConfig }); - - const device = new RuntimeDevice({ - appsConfig, - behaviorConfig: {}, - deviceConfig: {}, - sessionConfig: {}, - runtimeErrorComposer: errorComposer, - eventEmitter: emitter, - - ...overrides, - }, driverMock.driver); - - device.deviceDriver.getBundleIdFromBinary.mockReturnValue(bundleId); - return device; - } - - function aValidUnpreparedDevice(overrides) { - const configs = _.merge(_.cloneDeep({ - appsConfig: { - default: configurationsMock.appWithRelativeBinaryPath, - }, - deviceConfig: configurationsMock.iosSimulatorWithShorthandQuery, - sessionConfig: configurationsMock.validSession, - }), overrides); - - if (overrides && overrides.appsConfig === null) { - configs.appsConfig = {}; - } - - return aDevice(configs); - } - - async function aValidDevice(overrides) { - const device = aValidUnpreparedDevice(overrides); - await device._prepare(); - return device; - } - - async function aValidDeviceWithLaunchArgs(launchArgs) { - return await aValidDevice({ - appsConfig: { - default: { - launchArgs, - }, - }, - }); - } - - it('should return the name from the driver', async () => { - driverMock.driver.getDeviceName.mockReturnValue('mock-device-name-from-driver'); - - const device = await aValidDevice(); - expect(device.name).toEqual('mock-device-name-from-driver'); - }); - - it('should return the type from the configuration', async () => { - const device = await aValidDevice(); - expect(device.type).toEqual('ios.simulator'); - }); - - it('should return the device ID, as provided by acquireFreeDevice', async () => { - const device = await aValidUnpreparedDevice(); - await device._prepare(); - - driverMock.driver.getExternalId.mockReturnValue('mockExternalId'); - expect(device.id).toEqual('mockExternalId'); - - driverMock.expectExternalIdCalled(); - }); - - describe('selectApp()', () => { - let device; - - describe('when there is a single app', () => { - beforeEach(async () => { - device = await aValidUnpreparedDevice(); - jest.spyOn(device, 'selectApp'); - await device._prepare(); - }); - - it(`should select the default app upon prepare()`, async () => { - expect(device.selectApp).toHaveBeenCalledWith('default'); - }); - - it(`should function as usual when the app is selected`, async () => { - await device.launchApp(); - expect(driverMock.driver.launchApp).toHaveBeenCalled(); - }); - - it(`should throw on call without args`, async () => { - await expect(device.selectApp()).rejects.toThrowError(errorComposer.cantSelectEmptyApp()); - }); - - it(`should throw on app interactions with no selected app`, async () => { - await device.selectApp(null); - await expect(device.launchApp()).rejects.toThrowError(errorComposer.appNotSelected()); - }); - - it(`should throw on attempt to select a non-existent app`, async () => { - await expect(device.selectApp('nonExistent')).rejects.toThrowError(); - }); - }); - - describe('when there are multiple apps', () => { - beforeEach(async () => { - device = await aValidUnpreparedDevice({ - appsConfig: { - withBinaryPath: { - binaryPath: 'path/to/app', - }, - withBundleId: { - binaryPath: 'path/to/app2', - bundleId: 'com.app2' - }, - }, - }); - - jest.spyOn(device, 'selectApp'); - driverMock.driver.getBundleIdFromBinary.mockReturnValue('com.app1'); - - await device._prepare(); - }); - - it(`should not select the app at all`, async () => { - expect(device.selectApp).not.toHaveBeenCalled(); - }); - - it(`upon select, it should infer bundleId if it is missing`, async () => { - await device.selectApp('withBinaryPath'); - expect(driverMock.driver.getBundleIdFromBinary).toHaveBeenCalledWith('path/to/app'); - }); - - it(`upon select, it should terminate the previous app`, async () => { - jest.spyOn(device, 'terminateApp'); - - await device.selectApp('withBinaryPath'); - expect(device.terminateApp).not.toHaveBeenCalled(); // because no app was running before - - await device.selectApp('withBundleId'); - expect(device.terminateApp).toHaveBeenCalled(); // because there is a running app - }); - - it(`upon select, it should not infer bundleId if it is specified`, async () => { - await device.selectApp('withBundleId'); - expect(driverMock.driver.getBundleIdFromBinary).not.toHaveBeenCalled(); - }); - - it(`upon re-selecting the same app, it should not infer bundleId twice`, async () => { - await device.selectApp('withBinaryPath'); - await device.selectApp('withBundleId'); - await device.selectApp('withBinaryPath'); - expect(driverMock.driver.getBundleIdFromBinary).toHaveBeenCalledTimes(1); - }); - }); - - describe('when there are no apps', () => { - beforeEach(async () => { - device = await aValidUnpreparedDevice({ - appsConfig: null - }); - - jest.spyOn(device, 'selectApp'); - await device._prepare(); - }); - - it(`should not select the app at all`, async () => { - expect(device.selectApp).not.toHaveBeenCalled(); - }); - - it(`should be able to execute actions with an explicit bundleId`, async () => { - const bundleId = 'com.example.app'; - jest.spyOn(device, 'terminateApp'); - - await device.uninstallApp(bundleId); - expect(driverMock.driver.uninstallApp).toHaveBeenCalledWith(bundleId); - - await device.installApp('/tmp/app', '/tmp/app-test'); - expect(driverMock.driver.installApp).toHaveBeenCalledWith('/tmp/app', '/tmp/app-test'); - - await device.launchApp({}, bundleId); - expect(driverMock.driver.launchApp).toHaveBeenCalledWith(bundleId, expect.anything(), undefined); - - await device.terminateApp(bundleId); - expect(driverMock.driver.terminate).toHaveBeenCalledWith(bundleId); - - await device.uninstallApp(bundleId); - expect(driverMock.driver.uninstallApp).toHaveBeenCalledWith(bundleId); - }); - }); - }); - - describe('re/launchApp()', () => { - const expectedDriverArgs = { - 'detoxServer': 'ws://localhost:8099', - 'detoxSessionId': 'test', - }; - - it(`with no args should launch app with defaults`, async () => { - const expectedArgs = expectedDriverArgs; - const device = await aValidDevice(); - await device.launchApp(); - - driverMock.expectLaunchCalledWithArgs(bundleId, expectedArgs); - }); - - it(`given behaviorConfig.launchApp == 'manual' should wait for the app launch`, async () => { - const expectedArgs = expectedDriverArgs; - const device = await aValidDevice({ - behaviorConfig: { launchApp: 'manual' } - }); - await device.launchApp(); - - expect(driverMock.driver.launchApp).not.toHaveBeenCalled(); - driverMock.expectWaitForLaunchCalled(bundleId, expectedArgs); - }); - - it(`args should launch app and emit appReady`, async () => { - driverMock.driver.launchApp = async () => 42; - - const device = await aValidDevice(); - await device.launchApp(); - - expect(emitter.emit).toHaveBeenCalledWith('appReady', { - deviceId: device.id, - bundleId: device._bundleId, - pid: 42, - }); - }); - - it(`(relaunch) with no args should use defaults`, async () => { - const expectedArgs = expectedDriverArgs; - const device = await aValidDevice(); - - await device.relaunchApp(); - - driverMock.expectLaunchCalledWithArgs(bundleId, expectedArgs); - }); - - it(`(relaunch) with no args should terminate the app before launch - backwards compat`, async () => { - const device = await aValidDevice(); - - await device.relaunchApp(); - - driverMock.expectTerminateCalled(); - }); - - it(`(relaunch) with newInstance=false should not terminate the app before launch`, async () => { - const device = await aValidDevice(); - - await device.relaunchApp({ newInstance: false }); - - driverMock.expectTerminateNotCalled(); - }); - - it(`(relaunch) with newInstance=true should terminate the app before launch`, async () => { - const device = await aValidDevice(); - - await device.relaunchApp({ newInstance: true }); - - driverMock.expectTerminateCalled(); - }); - - it(`(relaunch) with delete=true`, async () => { - const expectedArgs = expectedDriverArgs; - const device = await aValidDevice(); - - await device.relaunchApp({ delete: true }); - - driverMock.expectReinstallCalled(); - driverMock.expectLaunchCalledWithArgs(bundleId, expectedArgs); - }); - - it(`(relaunch) with delete=false when reuse is enabled should not uninstall and install`, async () => { - const expectedArgs = expectedDriverArgs; - const device = await aValidDevice(); - argparse.getArgValue.mockReturnValue(true); - - await device.relaunchApp(); - - driverMock.expectReinstallNotCalled(); - driverMock.expectLaunchCalledWithArgs(bundleId, expectedArgs); - }); - - it(`(relaunch) with url should send the url as a param in launchParams`, async () => { - const expectedArgs = { ...expectedDriverArgs, 'detoxURLOverride': 'scheme://some.url' }; - const device = await aValidDevice(); - - await device.relaunchApp({ url: `scheme://some.url` }); - - driverMock.expectLaunchCalledWithArgs(bundleId, expectedArgs); - }); - - it(`(relaunch) with url should send the url as a param in launchParams`, async () => { - const expectedArgs = { - ...expectedDriverArgs, - 'detoxURLOverride': 'scheme://some.url', - 'detoxSourceAppOverride': 'sourceAppBundleId', - }; - const device = await aValidDevice(); - await device.relaunchApp({ url: `scheme://some.url`, sourceApp: 'sourceAppBundleId' }); - - driverMock.expectLaunchCalledWithArgs(bundleId, expectedArgs); - }); - - it(`(relaunch) with userNofitication should send the userNotification as a param in launchParams`, async () => { - const expectedArgs = { - ...expectedDriverArgs, - 'detoxUserNotificationDataURL': 'url', - }; - const device = await aValidDevice(); - - device.deviceDriver.createPayloadFile = jest.fn(() => 'url'); - - await device.relaunchApp({ userNotification: 'json' }); - - driverMock.expectLaunchCalledWithArgs(bundleId, expectedArgs); - }); - - it(`(relaunch) with url and userNofitication should throw`, async () => { - const device = await aValidDevice(); - try { - await device.relaunchApp({ url: 'scheme://some.url', userNotification: 'notif' }); - fail('should fail'); - } catch (ex) { - expect(ex).toBeDefined(); - } - }); - - it(`(relaunch) with permissions should send trigger setpermissions before app starts`, async () => { - const device = await aValidDevice(); - await device.relaunchApp({ permissions: { calendar: 'YES' } }); - - expect(driverMock.driver.setPermissions).toHaveBeenCalledWith(bundleId, { calendar: 'YES' }); - }); - - it('with languageAndLocale should launch app with a specific language/locale', async () => { - const expectedArgs = expectedDriverArgs; - const device = await aValidDevice(); - - const languageAndLocale = { - language: 'es-MX', - locale: 'es-MX' - }; - - await device.launchApp({ languageAndLocale }); - - driverMock.expectLaunchCalledWithArgs(bundleId, expectedArgs, languageAndLocale); - }); - - it(`with disableTouchIndicators should send a boolean switch as a param in launchParams`, async () => { - const expectedArgs = { ...expectedDriverArgs, 'detoxDisableTouchIndicators': true }; - const device = await aValidDevice(); - - await device.launchApp({ disableTouchIndicators: true }); - - driverMock.expectLaunchCalledWithArgs(bundleId, expectedArgs); - }); - - it(`with newInstance=false should check if process is in background and reopen it`, async () => { - const processId = 1; - const device = await aValidDevice(); - - device.deviceDriver.launchApp.mockReturnValue(processId); - - await device._prepare(); - await device.launchApp({ newInstance: true }); - await device.launchApp({ newInstance: false }); - - expect(driverMock.driver.deliverPayload).not.toHaveBeenCalled(); - }); - - it(`with a url should check if process is in background and use openURL() instead of launch args`, async () => { - const processId = 1; - const device = await aValidDevice(); - device.deviceDriver.launchApp.mockReturnValue(processId); - - await device._prepare(); - await device.launchApp({ newInstance: true }); - await device.launchApp({ url: 'url://me' }); - - expect(driverMock.driver.deliverPayload).toHaveBeenCalledTimes(1); - }); - - it(`with a url should check if process is in background and if not use launch args`, async () => { - const launchParams = { url: 'url://me' }; - const processId = 1; - const newProcessId = 2; - - const device = await aValidDevice(); - device.deviceDriver.launchApp.mockReturnValueOnce(processId).mockReturnValueOnce(newProcessId); - - await device._prepare(); - await device.launchApp(launchParams); - - expect(driverMock.driver.deliverPayload).not.toHaveBeenCalled(); - }); - - it(`with a url should check if process is in background and use openURL() instead of launch args`, async () => { - const launchParams = { url: 'url://me' }; - const processId = 1; - - const device = await aValidDevice(); - device.deviceDriver.launchApp.mockReturnValue(processId); - - await device._prepare(); - await device.launchApp({ newInstance: true }); - await device.launchApp(launchParams); - - expect(driverMock.driver.deliverPayload).toHaveBeenCalledWith({ delayPayload: true, url: 'url://me' }); - }); - - it('with userActivity should check if process is in background and if it is use deliverPayload', async () => { - const launchParams = { userActivity: 'userActivity' }; - const processId = 1; - - const device = await aValidDevice(); - device.deviceDriver.launchApp.mockReturnValueOnce(processId).mockReturnValueOnce(processId); - device.deviceDriver.createPayloadFile = () => 'url'; - - await device._prepare(); - await device.launchApp({ newInstance: true }); - await device.launchApp(launchParams); - - expect(driverMock.driver.deliverPayload).toHaveBeenCalledWith({ delayPayload: true, detoxUserActivityDataURL: 'url' }); - }); - - it('with userNotification should check if process is in background and if it is use deliverPayload', async () => { - const launchParams = { userNotification: 'notification' }; - const processId = 1; - - const device = await aValidDevice(); - device.deviceDriver.launchApp.mockReturnValueOnce(processId).mockReturnValueOnce(processId); - device.deviceDriver.createPayloadFile = () => 'url'; - - await device._prepare(); - await device.launchApp({ newInstance: true }); - await device.launchApp(launchParams); - - expect(driverMock.driver.deliverPayload).toHaveBeenCalledTimes(1); - }); - - it(`with userNotification should check if process is in background and if not use launch args`, async () => { - const launchParams = { userNotification: 'notification' }; - const processId = 1; - const newProcessId = 2; - - const device = await aValidDevice(); - device.deviceDriver.launchApp.mockReturnValueOnce(processId).mockReturnValueOnce(newProcessId); - - await device._prepare(); - await device.launchApp(launchParams); - - expect(driverMock.driver.deliverPayload).not.toHaveBeenCalled(); - }); - - it(`with userNotification and url should fail`, async () => { - const launchParams = { userNotification: 'notification', url: 'url://me' }; - const processId = 1; - driverMock.driver.launchApp.mockReturnValueOnce(processId).mockReturnValueOnce(processId); - - const device = await aValidDevice(); - - await device._prepare(); - - try { - await device.launchApp(launchParams); - fail('should throw'); - } catch (ex) { - expect(ex).toBeDefined(); - } - - expect(device.deviceDriver.deliverPayload).not.toHaveBeenCalled(); - }); - - it('should keep user params unmodified', async () => { - const params = { - url: 'some.url', - launchArgs: { - some: 'userArg', - } - }; - const paramsClone = _.cloneDeep(params); - - const device = await aValidDevice(); - await device.launchApp(params); - - expect(params).toStrictEqual(paramsClone); - }); - - describe('launch arguments', () => { - const baseArgs = { - detoxServer: 'ws://localhost:8099', - detoxSessionId: 'test', - }; - const someLaunchArgs = () => ({ - argX: 'valX', - argY: { value: 'Y' }, - }); - - it('should pass preconfigured launch-args to device via driver', async () => { - const launchArgs = someLaunchArgs(); - const device = await aValidDeviceWithLaunchArgs(launchArgs); - await device.launchApp(); - - driverMock.expectLaunchCalledContainingArgs(launchArgs); - }); - - it('should pass on-site launch-args to device via driver', async () => { - const launchArgs = someLaunchArgs(); - const expectedArgs = { - ...baseArgs, - ...launchArgs, - }; - - const device = await aValidDevice(); - await device.launchApp({ launchArgs }); - - driverMock.expectLaunchCalledWithArgs(bundleId, expectedArgs); - }); - - it('should allow for launch-args modification', async () => { - const launchArgs = someLaunchArgs(); - const argsModifier = { - argY: null, - argZ: 'valZ', - }; - const expectedArgs = { - argX: 'valX', - argZ: 'valZ', - }; - - const device = await aValidDeviceWithLaunchArgs(launchArgs); - device.appLaunchArgs.modify(argsModifier); - await device.launchApp(); - - driverMock.expectLaunchCalledContainingArgs(expectedArgs); - }); - - it('should override launch-args with on-site launch-args', async () => { - const launchArgs = { - aLaunchArg: 'aValue?', - }; - - const device = await aValidDeviceWithLaunchArgs(); - device.appLaunchArgs.modify(launchArgs); - await device.launchApp({ - launchArgs: { - aLaunchArg: 'aValue!', - }, - }); - - driverMock.expectLaunchCalledContainingArgs({ aLaunchArg: 'aValue!' }); - }); - - it('should allow for resetting all args', async () => { - const launchArgs = someLaunchArgs(); - const expectedArgs = { ...baseArgs }; - - const device = await aValidDeviceWithLaunchArgs(launchArgs); - device.appLaunchArgs.modify({ argZ: 'valZ' }); - device.appLaunchArgs.reset(); - await device.launchApp(); - - driverMock.expectLaunchCalledWithArgs(bundleId, expectedArgs); - }); - }); - }); - - describe('installApp()', () => { - it(`with a custom app path should use custom app path`, async () => { - const device = await aValidDevice(); - - await device.installApp('newAppPath'); - expect(driverMock.driver.installApp).toHaveBeenCalledWith('newAppPath', device._deviceConfig.testBinaryPath); - }); - - it(`with no args should use the default path given in configuration`, async () => { - const device = await aValidDevice(); - await device.installApp(); - expect(driverMock.driver.installApp).toHaveBeenCalledWith(device._currentApp.binaryPath, device._currentApp.testBinaryPath); - }); - }); - - describe('uninstallApp()', () => { - it(`with a custom app path should use custom app path`, async () => { - const device = await aValidDevice(); - await device.uninstallApp('newBundleId'); - expect(driverMock.driver.uninstallApp).toHaveBeenCalledWith('newBundleId'); - }); - - it(`with no args should use the default path given in configuration`, async () => { - const device = await aValidDevice(); - await device.uninstallApp(); - expect(driverMock.driver.uninstallApp).toHaveBeenCalledWith(bundleId); - }); - }); - - describe('installBinary()', () => { - it('should install the set of util binaries', async () => { - const device = await aValidDevice({ - deviceConfig: { - utilBinaryPaths: ['path/to/util/binary'] - }, - }); - - await device.installUtilBinaries(); - expect(driverMock.driver.installUtilBinaries).toHaveBeenCalledWith(['path/to/util/binary']); - }); - - it('should break if driver installation fails', async () => { - driverMock.driver.installUtilBinaries.mockRejectedValue(new Error()); - - const device = await aValidDevice({ - deviceConfig: { - utilBinaryPaths: ['path/to/util/binary'] - }, - }); - - await expect(device.installUtilBinaries()).rejects.toThrowError(); - }); - - it('should not install anything if util-binaries havent been configured', async () => { - const device = await aValidDevice({}); - - await device.installUtilBinaries(); - expect(driverMock.driver.installUtilBinaries).not.toHaveBeenCalled(); - }); - }); - - it(`sendToHome() should pass to device driver`, async () => { - const device = await aValidDevice(); - await device.sendToHome(); - - expect(driverMock.driver.sendToHome).toHaveBeenCalledTimes(1); - }); - - it(`setBiometricEnrollment(true) should pass YES to device driver`, async () => { - const device = await aValidDevice(); - await device.setBiometricEnrollment(true); - - expect(driverMock.driver.setBiometricEnrollment).toHaveBeenCalledWith('YES'); - expect(driverMock.driver.setBiometricEnrollment).toHaveBeenCalledTimes(1); - }); - - it(`setBiometricEnrollment(false) should pass NO to device driver`, async () => { - const device = await aValidDevice(); - await device.setBiometricEnrollment(false); - - expect(driverMock.driver.setBiometricEnrollment).toHaveBeenCalledWith('NO'); - expect(driverMock.driver.setBiometricEnrollment).toHaveBeenCalledTimes(1); - }); - - it(`matchFace() should pass to device driver`, async () => { - const device = await aValidDevice(); - await device.matchFace(); - - expect(driverMock.driver.matchFace).toHaveBeenCalledTimes(1); - }); - - it(`unmatchFace() should pass to device driver`, async () => { - const device = await aValidDevice(); - await device.unmatchFace(); - - expect(driverMock.driver.unmatchFace).toHaveBeenCalledTimes(1); - }); - - it(`matchFinger() should pass to device driver`, async () => { - const device = await aValidDevice(); - await device.matchFinger(); - - expect(driverMock.driver.matchFinger).toHaveBeenCalledTimes(1); - }); - - it(`unmatchFinger() should pass to device driver`, async () => { - const device = await aValidDevice(); - await device.unmatchFinger(); - - expect(driverMock.driver.unmatchFinger).toHaveBeenCalledTimes(1); - }); - - it(`setStatusBar() should pass to device driver`, async () => { - const device = await aValidDevice(); - const params = {}; - await device.setStatusBar(params); - - expect(driverMock.driver.setStatusBar).toHaveBeenCalledWith(params); - }); - - it(`resetStatusBar() should pass to device driver`, async () => { - const device = await aValidDevice(); - await device.resetStatusBar(); - - expect(driverMock.driver.resetStatusBar).toHaveBeenCalledWith(); - }); - - it(`typeText() should pass to device driver`, async () => { - const device = await aValidDevice(); - await device._typeText('Text'); - - expect(driverMock.driver.typeText).toHaveBeenCalledWith('Text'); - }); - - it(`shake() should pass to device driver`, async () => { - const device = await aValidDevice(); - await device.shake(); - - expect(driverMock.driver.shake).toHaveBeenCalledTimes(1); - }); - - it(`terminateApp() should pass to device driver`, async () => { - const device = await aValidDevice(); - await device.terminateApp(); - - expect(driverMock.driver.terminate).toHaveBeenCalledTimes(1); - }); - - it(`openURL({url:url}) should pass to device driver`, async () => { - const device = await aValidDevice(); - await device.openURL({ url: 'url' }); - - expect(driverMock.driver.deliverPayload).toHaveBeenCalledWith({ url: 'url' }); - }); - - it(`openURL(notAnObject) should pass to device driver`, async () => { - const device = await aValidDevice(); - try { - await device.openURL('url'); - fail('should throw'); - } catch (ex) { - expect(ex).toBeDefined(); - } - }); - - it(`reloadReactNative() should pass to device driver`, async () => { - const device = await aValidDevice(); - await device.reloadReactNative(); - - expect(driverMock.driver.reloadReactNative).toHaveBeenCalledTimes(1); - }); - - it(`setOrientation() should pass to device driver`, async () => { - const device = await aValidDevice(); - await device.setOrientation('param'); - - expect(driverMock.driver.setOrientation).toHaveBeenCalledWith('param'); - }); - - it(`sendUserNotification() should pass to device driver`, async () => { - const device = await aValidDevice(); - await device.sendUserNotification('notif'); - - expect(driverMock.driver.createPayloadFile).toHaveBeenCalledTimes(1); - expect(driverMock.driver.deliverPayload).toHaveBeenCalledTimes(1); - }); - - it(`sendUserActivity() should pass to device driver`, async () => { - const device = await aValidDevice(); - await device.sendUserActivity('notif'); - - expect(driverMock.driver.createPayloadFile).toHaveBeenCalledTimes(1); - expect(driverMock.driver.deliverPayload).toHaveBeenCalledTimes(1); - }); - - it(`setLocation() should pass to device driver`, async () => { - const device = await aValidDevice(); - await device.setLocation(30.1, 30.2); - - expect(driverMock.driver.setLocation).toHaveBeenCalledWith('30.1', '30.2'); - }); - - it(`reverseTcpPort should pass to device driver`, async () => { - const device = await aValidDevice(); - await device.reverseTcpPort(666); - - await driverMock.expectReverseTcpPortCalled(666); - }); - - it(`unreverseTcpPort should pass to device driver`, async () => { - const device = await aValidDevice(); - await device.unreverseTcpPort(777); - - await driverMock.expectUnreverseTcpPortCalled(777); - }); - - it(`setURLBlacklist() should pass to device driver`, async () => { - const device = await aValidDevice(); - await device.setURLBlacklist(); - - expect(driverMock.driver.setURLBlacklist).toHaveBeenCalledTimes(1); - }); - - it(`enableSynchronization() should pass to device driver`, async () => { - const device = await aValidDevice(); - await device.enableSynchronization(); - - expect(driverMock.driver.enableSynchronization).toHaveBeenCalledTimes(1); - }); - - it(`disableSynchronization() should pass to device driver`, async () => { - const device = await aValidDevice(); - await device.disableSynchronization(); - - expect(driverMock.driver.disableSynchronization).toHaveBeenCalledTimes(1); - }); - - it(`resetContentAndSettings() should pass to device driver`, async () => { - const device = await aValidDevice(); - await device.resetContentAndSettings(); - - expect(driverMock.driver.resetContentAndSettings).toHaveBeenCalledTimes(1); - }); - - it(`getPlatform() should pass to device driver`, async () => { - const device = await aValidDevice(); - device.getPlatform(); - - expect(driverMock.driver.getPlatform).toHaveBeenCalledTimes(1); - }); - - it(`_cleanup() should pass to device driver`, async () => { - const device = await aValidDevice(); - await device._cleanup(); - - expect(driverMock.driver.cleanup).toHaveBeenCalledTimes(1); - }); - - it(`should accept absolute path for binary`, async () => { - const actualPath = await launchAndTestBinaryPath('absolute'); - expect(actualPath).toEqual(configurationsMock.appWithAbsoluteBinaryPath.binaryPath); - }); - - it(`should accept relative path for binary`, async () => { - const actualPath = await launchAndTestBinaryPath('relative'); - expect(actualPath).toEqual(configurationsMock.appWithRelativeBinaryPath.binaryPath); - }); - - it(`pressBack() should invoke driver's pressBack()`, async () => { - const device = await aValidDevice(); - - await device.pressBack(); - - expect(driverMock.driver.pressBack).toHaveBeenCalledWith(); - }); - - it(`clearKeychain() should invoke driver's clearKeychain()`, async () => { - const device = await aValidDevice(); - - await device.clearKeychain(); - - expect(driverMock.driver.clearKeychain).toHaveBeenCalledWith(); - }); - - describe('get ui device', () => { - it(`getUiDevice should invoke driver's getUiDevice`, async () => { - const device = await aValidDevice(); - - await device.getUiDevice(); - - expect(driverMock.driver.getUiDevice).toHaveBeenCalled(); - }); - - it('should call return UiDevice when call getUiDevice', async () => { - const uiDevice = { - uidevice: true, - }; - - const device = await aValidDevice(); - driverMock.driver.getUiDevice = () => uiDevice; - - const result = await device.getUiDevice(); - - expect(result).toEqual(uiDevice); - }); - }); - - it('takeScreenshot(name) should throw an exception if given name is empty', async () => { - await expect((await aValidDevice()).takeScreenshot()).rejects.toThrowError(/empty name/); - }); - - it('takeScreenshot(name) should delegate the work to the driver', async () => { - const device = await aValidDevice(); - - await device.takeScreenshot('name'); - expect(device.deviceDriver.takeScreenshot).toHaveBeenCalledWith('name'); - }); - - it('captureViewHierarchy(name) should delegate the work to the driver', async () => { - const device = await aValidDevice(); - - await device.captureViewHierarchy('name'); - expect(device.deviceDriver.captureViewHierarchy).toHaveBeenCalledWith('name'); - }); - - it('captureViewHierarchy([name]) should set name = "capture" by default', async () => { - const device = await aValidDevice(); - - await device.captureViewHierarchy(); - expect(device.deviceDriver.captureViewHierarchy).toHaveBeenCalledWith('capture'); - }); - - describe('_isAppRunning (internal method)', () => { - let device; - - beforeEach(async () => { - device = await aValidDevice(); - driverMock.driver.launchApp = async () => 42; - await device.launchApp(); - }); - - it('should return the value for the current app if called with no args', async () => { - expect(device._isAppRunning()).toBe(true); - }); - - it('should return the value for the given bundleId', async () => { - expect(device._isAppRunning('test.bundle')).toBe(true); - expect(device._isAppRunning('somethingElse')).toBe(false); - }); - }); - - async function launchAndTestBinaryPath(absoluteOrRelative) { - const appConfig = absoluteOrRelative === 'absolute' - ? configurationsMock.appWithAbsoluteBinaryPath - : configurationsMock.appWithRelativeBinaryPath; - - const device = await aValidDevice({ appsConfig: { default: appConfig } }); - await device.installApp(); - - return driverMock.driver.installApp.mock.calls[0][0]; - } -}); diff --git a/detox/src/devices/runtime/TestApp.js b/detox/src/devices/runtime/TestApp.js new file mode 100644 index 0000000000..27a8b53187 --- /dev/null +++ b/detox/src/devices/runtime/TestApp.js @@ -0,0 +1,244 @@ +const DetoxRuntimeError = require('../../errors/DetoxRuntimeError'); +const { traceCall } = require('../../utils/trace'); + +const LaunchArgsEditor = require('./utils/LaunchArgsEditor'); + +class TestApp { + /** + * @param driver { TestAppDriver } + */ + constructor(driver) { + this._driver = driver; + } + + async init() {} + + async install() { + await traceCall('appInstall', () => this._driver.install()); + } + + async uninstall() { + await traceCall('appUninstall', () => this._driver.uninstall()); + } +} + +class RunnableTestApp extends TestApp { + constructor(driver, { appConfig, behaviorConfig }) { + super(driver); + + this.appConfig = appConfig; + this.behaviorConfig = behaviorConfig; + + this._launchArgs = new LaunchArgsEditor(); + } + + get alias() { + return null; + } + + get uiDevice() { + return this._driver.uiDevice; + } + + async deselect() { + await this._driver.deselect(); + } + + async launch(launchInfo) { + return traceCall('launchApp', () => this._launch(launchInfo)); + } + + async reloadReactNative() { + return this._driver.reloadReactNative(); + } + + async enableSynchronization() { + await this._driver.enableSynchronization(); + } + + async disableSynchronization() { + await this._driver.disableSynchronization(); + } + + async openURL(params) { + if (typeof params !== 'object' || !params.url) { + throw new DetoxRuntimeError({ message: `openURL must be called with JSON params, and a value for 'url' key must be provided. See https://wix.github.io/Detox/docs/api/device-object-api/#deviceopenurlurl-sourceappoptional.` }); + } + await this._driver.deliverPayload(params); + } + + async sendUserActivity(payload) { + await this._driver.sendUserActivity(payload); + } + + async sendUserNotification(payload) { + await this._driver.sendUserNotification(payload); + } + + async setURLBlacklist(urlList) { + await this._driver.setURLBlacklist(urlList); + } + + async resetContentAndSettings() { + await this._driver.resetContentAndSettings(); + } + + async matchFace() { + await this._driver.matchFace(); + } + + async unmatchFace() { + await this._driver.unmatchFace(); + } + + async matchFinger() { + await this._driver.matchFinger(); + } + + async unmatchFinger() { + await this._driver.unmatchFinger(); + } + + async shake() { + await this._driver.shake(); + } + + async setOrientation(orientation) { + await this._driver.setOrientation(orientation); + } + + async terminate() { + await traceCall('appTerminate', this._driver.terminate()); + } + + async invoke(action) { + return this._driver.invoke(action); + } + + async _launch(launchParams) { + const payloadParams = ['url', 'userNotification', 'userActivity']; + const hasPayload = this._assertHasSingleParam(payloadParams, launchParams); + const isRunning = this._driver.isRunning(); + const newInstance = (launchParams.newInstance !== undefined) + ? launchParams.newInstance + : !isRunning; + + if (launchParams.delete) { + await this._driver.terminate(); + await this._driver.uninstall(); + await this._driver.install(); + } else if (newInstance) { + await this._driver.terminate(); + } + + if (launchParams.permissions) { + await this._driver.setPermissions(launchParams.permissions); + } + + const launchArgs = this._prepareLaunchArgs(launchParams); + + if (isRunning && hasPayload) { + // TODO (multiapps) Is this really needed, provided that the payload gets inject via the launch args? + await this._driver.deliverPayload({ ...launchParams, delayPayload: true }); + } + + const launchInfo = { + launchArgs, + languageAndLocale: launchParams.languageAndLocale, + }; + + if (this.behaviorConfig.launchApp === 'manual') { + await this._driver.waitForLaunch(launchInfo); + } else { + await this._driver.launch(launchInfo); + } + } + + _assertHasSingleParam(singleParams, params) { + let paramsCounter = 0; + + singleParams.forEach((item) => { + if(params[item]) { + paramsCounter += 1; + } + }); + + if (paramsCounter > 1) { + throw new DetoxRuntimeError({ message: `Call to 'launchApp(${JSON.stringify(params)})' must contain only one of ${JSON.stringify(singleParams)}.` }); + } + return (paramsCounter === 1); + } + + _prepareLaunchArgs(launchInfo) { + const launchArgs = { + ...this._launchArgs.get(), + ...launchInfo.launchArgs, + }; + + if (launchInfo.url) { + launchArgs.detoxURLOverride = launchInfo.url; + + if (launchInfo.sourceApp) { + launchArgs.detoxSourceAppOverride = launchInfo.sourceApp; + } + } else if (launchInfo.userNotification) { + launchArgs.detoxUserNotificationDataURL = launchInfo.userNotification; + delete launchInfo.userNotification; // TODO (multiapps) revisit whether this is needed + } else if (launchInfo.userActivity) { + launchArgs.detoxUserActivityDataURL = launchInfo.userActivity; + delete launchInfo.userActivity; // TODO (multiapps) revisit whether this is needed + } + + if (launchInfo.disableTouchIndicators) { + launchArgs.detoxDisableTouchIndicators = true; + } + return launchArgs; + } +} + +class PredefinedTestApp extends RunnableTestApp { + constructor(driver, configs, alias) { + super(driver, configs); + + this._alias = alias; + } + + /** @override */ + get alias() { + return this._alias; + } + + async select() { + await this._driver.select(this.appConfig); + } +} + +class UnspecifiedTestApp extends RunnableTestApp { + constructor(driver, { behaviorConfig }) { + super(driver, { behaviorConfig, appConfig: null }); + } + + async select(appConfig) { + this.appConfig = appConfig; + + await this._driver.select(this.appConfig); + } +} + +class UtilApp extends TestApp { + constructor(driver, { appConfig }) { + super(driver); + + this._appConfig = appConfig; + } + + async init() { + await this._driver.select(this._appConfig); + } +} + +module.exports = { + PredefinedTestApp, + UnspecifiedTestApp, + UtilApp, +}; diff --git a/detox/src/devices/runtime/drivers/BaseDrivers.js b/detox/src/devices/runtime/drivers/BaseDrivers.js new file mode 100644 index 0000000000..569c931cca --- /dev/null +++ b/detox/src/devices/runtime/drivers/BaseDrivers.js @@ -0,0 +1,165 @@ +const fs = require('fs-extra'); + +const tempFile = require('../../../utils/tempFile'); + +/** + * @typedef DeviceDriverDeps + * @property eventEmitter { AsyncEmitter } + */ + +class DeviceDriver { + /** + * @param deps { DeviceDriverDeps } + */ + constructor({ eventEmitter }) { + this.emitter = eventEmitter; + } + + /** + * @returns { String | undefined } + */ + get externalId() { + return undefined; + } + + /** + * @returns { String | undefined } + */ + get deviceName() { + return undefined; + } + + /** + * @returns { String } + */ + get platform() { + return ''; + } + + validateDeviceConfig(_deviceConfig) {} + + async takeScreenshot(_screenshotName) {} + async setBiometricEnrollment() {} + async setStatusBar(_params) {} + async resetStatusBar() {} + async setLocation(_lat, _lon) {} + async reverseTcpPort() {} + async unreverseTcpPort() {} + async clearKeychain() {} + async typeText(_text) {} + + async cleanup() { + this.emitter.off(); // TODO not the right place for this + } +} + +/** + * @typedef TestAppDriverDeps + * @property client { Client } + * @property invocationManager { InvocationManager } + * @property eventEmitter { AsyncEmitter } + */ + +/** + * @typedef AppInfo + * @property binaryPath { String } + */ + +/** + * @typedef LaunchInfo + * @property launchArgs { Object } + */ + +class TestAppDriver { + + /** + * @param deps { TestAppDriverDeps } + */ + constructor({ client, invocationManager, eventEmitter }) { + this.client = client; + this.invocationManager = invocationManager; + this.emitter = eventEmitter; + + this._pid = null; + this._appInfo = null; + } + + get uiDevice() { + return null; + } + + /** + * @param appInfo { AppInfo } + */ + async select(appInfo) { + this._appInfo = appInfo; + } + + async deselect() { + this._appInfo = null; + } + + /** + * @param _launchInfo { LaunchInfo } + */ + async launch(_launchInfo) {} + + /** + * @param _launchInfo { LaunchInfo } + */ + async waitForLaunch(_launchInfo) {} + + async reloadReactNative() {} + async resetContentAndSettings() {} + async deliverPayload(_params) {} // TODO (multiapps) Revisit whether keeping this method public makes sense at all + async sendUserActivity(_payload) {} + async sendUserNotification(_payload) {} + async terminate() { + this._pid = null; + } + + async invoke(_action) {} + + /** + * @returns {boolean} Whether the app is currently running + */ + isRunning() { + return !!this._pid; + } + + async install() {} + async uninstall() {} + + async setOrientation(_orientation) {} + async setPermissions(_permissions) {} + async sendToHome() {} + async pressBack() {} + async matchFace() {} + async unmatchFace() {} + async matchFinger() {} + async unmatchFinger() {} + async shake() {} + async enableSynchronization() {} + async disableSynchronization() {} + async captureViewHierarchy() {} + + async cleanup() {} + + /** @protected */ + async _waitUntilReady() { + return this.client.waitUntilReady(); + } + + /** @protected */ + _createPayloadFile(payload) { + const payloadFile = tempFile.create('payload.json'); + fs.writeFileSync(payloadFile.path, JSON.stringify(payload, null, 2)); + return payloadFile; + } +} + + +module.exports = { + DeviceDriver, + TestAppDriver, +}; diff --git a/detox/src/devices/runtime/drivers/DeviceDriverBase.js b/detox/src/devices/runtime/drivers/DeviceDriverBase.js deleted file mode 100644 index b52c18eb3b..0000000000 --- a/detox/src/devices/runtime/drivers/DeviceDriverBase.js +++ /dev/null @@ -1,227 +0,0 @@ -// @ts-nocheck -const os = require('os'); -const path = require('path'); - -const fs = require('fs-extra'); - -const log = require('../../../utils/logger').child({ __filename }); - -/** - * @typedef DeviceDriverDeps - * @property client { Client } - * @property eventEmitter { AsyncEmitter } - */ - -class DeviceDriverBase { - /** - * @param deps { DeviceDriverDeps } - */ - constructor({ client, eventEmitter }) { - this.client = client; - this.emitter = eventEmitter; - } - - /** - * @returns { String | undefined } - */ - getExternalId() { - return undefined; - } - - /** - * @returns { String | undefined } - */ - getDeviceName() { - return undefined; - } - - declareArtifactPlugins() { - return {}; - } - - async launchApp() { - return NaN; - } - - async waitForAppLaunch() { - return NaN; - } - - async takeScreenshot(_screenshotName) { - return ''; - } - - async sendToHome() { - return ''; - } - - async setBiometricEnrollment() { - return ''; - } - - async matchFace() { - return ''; - } - - async unmatchFace() { - return ''; - } - - async matchFinger() { - return ''; - } - - async unmatchFinger() { - return ''; - } - - async shake() { - return ''; - } - - async installApp(_binaryPath, _testBinaryPath) { - return ''; - } - - async uninstallApp() { - return ''; - } - - installUtilBinaries() { - return ''; - } - - async deliverPayload(params) { - return await this.client.deliverPayload(params); - } - - async setLocation(_lat, _lon) { - return ''; - } - - async reverseTcpPort() { - return ''; - } - - async unreverseTcpPort() { - return ''; - } - - async clearKeychain(_udid) { - return ''; - } - - async waitUntilReady() { - return await this.client.waitUntilReady(); - } - - async waitForActive() { - return ''; - } - - async waitForBackground() { - return ''; - } - - async reloadReactNative() { - return await this.client.reloadReactNative(); - } - - createPayloadFile(notification) { - const notificationFilePath = path.join(this.createRandomDirectory(), `payload.json`); - fs.writeFileSync(notificationFilePath, JSON.stringify(notification, null, 2)); - return notificationFilePath; - } - - async setPermissions(_bundleId, _permissions) { - return ''; - } - - async terminate(_bundleId) { - return ''; - } - - async setOrientation(_orientation) { - return ''; - } - - async setURLBlacklist(_urlList) { - return ''; - } - - async enableSynchronization() { - return ''; - } - - async disableSynchronization() { - return ''; - } - - async resetContentAndSettings(_deviceId, _deviceConfig) { - return ''; - } - - createRandomDirectory() { - const randomDir = fs.mkdtempSync(path.join(os.tmpdir(), 'detoxrand-')); - fs.ensureDirSync(randomDir); - return randomDir; - } - - cleanupRandomDirectory(fileOrDir) { - if(path.basename(fileOrDir).startsWith('detoxrand-')) { - fs.removeSync(fileOrDir); - } - } - - getBundleIdFromBinary(_appPath) { - return ''; - } - - validateDeviceConfig(_deviceConfig) { - } - - getPlatform() { - return ''; - } - - async getUiDevice() { - log.warn(`getUiDevice() is an Android-specific function, it exposes UiAutomator's UiDevice API (https://developer.android.com/reference/android/support/test/uiautomator/UiDevice).`); - log.warn(`Make sure you create an Android-specific test for this scenario.`); - - return await Promise.resolve(''); - } - - async cleanup(_bundleId) { - this.emitter.off(); // clean all listeners - } - - getLogsPaths() { - return { - stdout: undefined, - stderr: undefined - }; - } - - async pressBack() { - log.warn('pressBack() is an Android-specific function.'); - log.warn(`Make sure you create an Android-specific test for this scenario.`); - - return await Promise.resolve(''); - } - - async typeText(_text) { - return await Promise.resolve(''); - } - - async setStatusBar(_flags) { - } - - async resetStatusBar() { - } - - async captureViewHierarchy() { - return ''; - } -} - -module.exports = DeviceDriverBase; diff --git a/detox/src/devices/runtime/drivers/android/AndroidDriver.js b/detox/src/devices/runtime/drivers/android/AndroidDrivers.js similarity index 61% rename from detox/src/devices/runtime/drivers/android/AndroidDriver.js rename to detox/src/devices/runtime/drivers/android/AndroidDrivers.js index c235b7ce3b..1fbf620c57 100644 --- a/detox/src/devices/runtime/drivers/android/AndroidDriver.js +++ b/detox/src/devices/runtime/drivers/android/AndroidDrivers.js @@ -1,13 +1,12 @@ // @ts-nocheck const path = require('path'); -const URL = require('url').URL; +const { URL } = require('url'); const fs = require('fs-extra'); const _ = require('lodash'); const DetoxApi = require('../../../../android/espressoapi/Detox'); const EspressoDetoxApi = require('../../../../android/espressoapi/EspressoDetox'); -const UiDeviceProxy = require('../../../../android/espressoapi/UiDeviceProxy'); const temporaryPath = require('../../../../artifacts/utils/temporaryPath'); const DetoxRuntimeError = require('../../../../errors/DetoxRuntimeError'); const getAbsoluteBinaryPath = require('../../../../utils/getAbsoluteBinaryPath'); @@ -16,122 +15,145 @@ const pressAnyKey = require('../../../../utils/pressAnyKey'); const retry = require('../../../../utils/retry'); const sleep = require('../../../../utils/sleep'); const apkUtils = require('../../../common/drivers/android/tools/apk'); -const DeviceDriverBase = require('../DeviceDriverBase'); +const { DeviceDriver, TestAppDriver } = require('../BaseDrivers'); const log = logger.child({ __filename }); /** - * @typedef AndroidDriverProps - * @property adbName { String } The unique identifier associated with ADB + * @typedef { DeviceDriverDeps } AndroidDeviceDriverDeps + * @property adb { ADB } + * @property devicePathBuilder { AndroidDevicePathBuilder } */ +class AndroidDeviceDriver extends DeviceDriver { + /** + * @param deps { AndroidDeviceDriverDeps } + * @param props {{ adbName: String }} + */ + constructor(deps, { adbName }) { + super(deps); + + this.adbName = adbName; + this.adb = deps.adb; + this.devicePathBuilder = deps.devicePathBuilder; + } + + /** @override */ + get platform() { + return 'android'; + } + + /** @override */ + get externalId() { + return this.adbName; + } + + /** @override */ + async pressBack() { + await this.uiDevice.pressBack(); + } + + /** @override */ + async typeText(text) { + await this.adb.typeText(this.adbName, text); + } + + /** @override */ + async takeScreenshot(screenshotName) { + const { adbName } = this; + + const pathOnDevice = this.devicePathBuilder.buildTemporaryArtifactPath('.png'); + await this.adb.screencap(adbName, pathOnDevice); + + const tempPath = temporaryPath.for.png(); + await this.adb.pull(adbName, pathOnDevice, tempPath); + await this.adb.rm(adbName, pathOnDevice); + + await this.emitter.emit('createExternalArtifact', { + pluginId: 'screenshot', + artifactName: screenshotName || path.basename(tempPath, '.png'), + artifactPath: tempPath, + }); + + return tempPath; + } +} + /** - * @typedef { DeviceDriverDeps } AndroidDriverDeps - * @property invocationManager { InvocationManager } + * @typedef { AppInfo } AndroidAppInfo + * @property testBinaryPath { String } + */ + +/** + * @typedef { TestAppDriverDeps } AndroidAppDriverDeps * @property adb { ADB } * @property aapt { AAPT } * @property apkValidator { ApkValidator } * @property fileXfer { FileXfer } * @property appInstallHelper { AppInstallHelper } * @property appUninstallHelper { AppUninstallHelper } - * @property devicePathBuilder { AndroidDevicePathBuilder } + * @property uiDevice { UiDeviceProxy } * @property instrumentation { MonitoredInstrumentation } */ -class AndroidDriver extends DeviceDriverBase { +class AndroidAppDriver extends TestAppDriver { /** - * @param deps { AndroidDriverDeps } - * @param props { AndroidDriverProps } + * @param deps { AndroidAppDriverDeps } + * @param props {{ adbName: String }} */ constructor(deps, { adbName }) { super(deps); - this.adbName = adbName; this.adb = deps.adb; this.aapt = deps.aapt; this.apkValidator = deps.apkValidator; - this.invocationManager = deps.invocationManager; this.fileXfer = deps.fileXfer; this.appInstallHelper = deps.appInstallHelper; this.appUninstallHelper = deps.appUninstallHelper; - this.devicePathBuilder = deps.devicePathBuilder; - this.instrumentation = deps.instrumentation; - - this.uiDevice = new UiDeviceProxy(this.invocationManager).getUIDevice(); - } - - getExternalId() { - return this.adbName; - } - - async getBundleIdFromBinary(apkPath) { - const binaryPath = getAbsoluteBinaryPath(apkPath); - return await this.aapt.getPackageName(binaryPath); - } + this._uiDevice = deps.uiDevice; + this._instrumentation = deps.instrumentation; - async installApp(_appBinaryPath, _testBinaryPath) { - const { - appBinaryPath, - testBinaryPath, - } = this._getAppInstallPaths(_appBinaryPath, _testBinaryPath); - await this._validateAppBinaries(appBinaryPath, testBinaryPath); - await this._installAppBinaries(appBinaryPath, testBinaryPath); + this.adbName = adbName; + this._packageId = null; } - async uninstallApp(bundleId) { - await this.emitter.emit('beforeUninstallApp', { deviceId: this.adbName, bundleId }); - await this.appUninstallHelper.uninstall(this.adbName, bundleId); + /** @override */ + get uiDevice() { + return this._uiDevice; } - async installUtilBinaries(paths) { - for (const path of paths) { - const packageId = await this.getBundleIdFromBinary(path); - if (!await this.adb.isPackageInstalled(this.adbName, packageId)) { - await this.appInstallHelper.install(this.adbName, path); - } - } + /** + * @override + * @param appInfo { AndroidAppInfo } + */ + async select(appInfo) { + await super.select(appInfo); + this._packageId = await this._inferPackageIdFromApk(appInfo.binaryPath); // TODO (multiapps) Cache? } - async launchApp(bundleId, launchArgs, languageAndLocale) { - return await this._handleLaunchApp({ + /** @override */ + async launch(launchInfo) { + this._pid = await this._handleLaunchApp({ manually: false, - bundleId, - launchArgs, - languageAndLocale, + launchArgs: launchInfo.launchArgs, }); + await this._waitUntilReady(); } - async waitForAppLaunch(bundleId, launchArgs, languageAndLocale) { - return await this._handleLaunchApp({ + /** @override */ + async waitForLaunch(launchInfo) { + this._pid = await this._handleLaunchApp({ manually: true, - bundleId, - launchArgs, - languageAndLocale, + launchArgs: launchInfo.launchArgs, }); } - async _handleLaunchApp({ manually, bundleId, launchArgs }) { - const { adbName } = this; - - await this.emitter.emit('beforeLaunchApp', { deviceId: adbName, bundleId, launchArgs }); - - launchArgs = await this._modifyArgsForNotificationHandling(adbName, bundleId, launchArgs); - - if (manually) { - await this._waitForAppLaunch(adbName, bundleId, launchArgs); - } else { - await this._launchApp(adbName, bundleId, launchArgs); - } - - const pid = await this._waitForProcess(adbName, bundleId); - if (manually) { - log.info({}, `Found the app (${bundleId}) with process ID = ${pid}. Proceeding...`); - } - - await this.emitter.emit('launchApp', { deviceId: adbName, bundleId, launchArgs, pid }); - return pid; + /** @override */ + async reloadReactNative() { + return this.client.reloadReactNative(); } + /** @override */ async deliverPayload(params) { if (params.delayPayload) { return; @@ -146,152 +168,115 @@ class AndroidDriver extends DeviceDriverBase { } } - async waitUntilReady() { - try { - await Promise.race([super.waitUntilReady(), this.instrumentation.waitForCrash()]); - } finally { - this.instrumentation.abortWaitForCrash(); - } - } + /** @override */ + async terminate() { + const { adbName, _packageId } = this; - async pressBack() { // eslint-disable-line no-unused-vars - await this.uiDevice.pressBack(); - } - - async sendToHome(params) { // eslint-disable-line no-unused-vars - await this.uiDevice.pressHome(); - } - - async typeText(text) { - await this.adb.typeText(this.adbName, text); - } - - async terminate(bundleId) { - const { adbName } = this; - await this.emitter.emit('beforeTerminateApp', { deviceId: adbName, bundleId }); + await this.emitter.emit('beforeTerminateApp', { deviceId: adbName, bundleId: _packageId }); await this._terminateInstrumentation(); - await this.adb.terminate(adbName, bundleId); - await this.emitter.emit('terminateApp', { deviceId: adbName, bundleId }); + await this.adb.terminate(adbName, _packageId); + await this.emitter.emit('terminateApp', { deviceId: adbName, bundleId: _packageId }); + await super.terminate(); } - async cleanup(bundleId) { - await this._terminateInstrumentation(); - await super.cleanup(bundleId); + /** @override */ + async install() { + const { _appInfo } = this; + const { + appBinaryPath, + testBinaryPath, + } = this._getAppInstallPaths(_appInfo.binaryPath, _appInfo.testBinaryPath); + await this._validateAppBinaries(appBinaryPath, testBinaryPath); + await this._installAppBinaries(appBinaryPath, testBinaryPath); } - getPlatform() { - return 'android'; - } + /** @override */ + async uninstall() { + const { _packageId } = this; - getUiDevice() { - return this.uiDevice; + if (_packageId) { + await this.emitter.emit('beforeUninstallApp', { deviceId: this.adbName, bundleId: _packageId }); + await this.appUninstallHelper.uninstall(this.adbName, _packageId); + } } - async reverseTcpPort(port) { - await this.adb.reverse(this.adbName, port); + /** @override */ + async invoke(invocation) { + const resultObj = await this.invocationManager.execute(invocation); + return resultObj ? resultObj.result : undefined; } - async unreverseTcpPort(port) { - await this.adb.reverseRemove(this.adbName, port); + /** @override */ + async setOrientation(orientation) { + const orientationMapping = { + landscape: 1, // top at left side landscape + portrait: 0 // non-reversed portrait. + }; + + const call = EspressoDetoxApi.changeOrientation(orientationMapping[orientation]); + await this.invocationManager.execute(call); } + /** @override */ async setURLBlacklist(urlList) { - await this.invocationManager.execute(EspressoDetoxApi.setURLBlacklist(urlList)); + await this.invoke(EspressoDetoxApi.setURLBlacklist(urlList)); } + /** @override */ async enableSynchronization() { - await this.invocationManager.execute(EspressoDetoxApi.setSynchronization(true)); + await this.invoke(EspressoDetoxApi.setSynchronization(true)); } + /** @override */ async disableSynchronization() { - await this.invocationManager.execute(EspressoDetoxApi.setSynchronization(false)); + await this.invoke(EspressoDetoxApi.setSynchronization(false)); } - async takeScreenshot(screenshotName) { - const { adbName } = this; - - const pathOnDevice = this.devicePathBuilder.buildTemporaryArtifactPath('.png'); - await this.adb.screencap(adbName, pathOnDevice); - - const tempPath = temporaryPath.for.png(); - await this.adb.pull(adbName, pathOnDevice, tempPath); - await this.adb.rm(adbName, pathOnDevice); - - await this.emitter.emit('createExternalArtifact', { - pluginId: 'screenshot', - artifactName: screenshotName || path.basename(tempPath, '.png'), - artifactPath: tempPath, - }); - - return tempPath; + /** @override */ + async sendToHome() { + await this.uiDevice.pressHome(); } - async setOrientation(orientation) { - const orientationMapping = { - landscape: 1, // top at left side landscape - portrait: 0 // non-reversed portrait. - }; - - const call = EspressoDetoxApi.changeOrientation(orientationMapping[orientation]); - await this.invocationManager.execute(call); + /** @override */ + async pressBack() { + await this.uiDevice.pressBack(); } - _getAppInstallPaths(_appBinaryPath, _testBinaryPath) { - const appBinaryPath = getAbsoluteBinaryPath(_appBinaryPath); - const testBinaryPath = _testBinaryPath ? getAbsoluteBinaryPath(_testBinaryPath) : this._getTestApkPath(appBinaryPath); - return { - appBinaryPath, - testBinaryPath, - }; + /** @override */ + async cleanup() { + await this._terminateInstrumentation(); } - async _validateAppBinaries(appBinaryPath, testBinaryPath) { - try { - await this.apkValidator.validateAppApk(appBinaryPath); - } catch (e) { - logger.warn(e.toString()); - } - - try { - await this.apkValidator.validateTestApk(testBinaryPath); - } catch (e) { - logger.warn(e.toString()); - } + async _inferPackageIdFromApk(apkPath) { + const binaryPath = getAbsoluteBinaryPath(apkPath); + return await this.aapt.getPackageName(binaryPath); } - async _installAppBinaries(appBinaryPath, testBinaryPath) { - await this.adb.install(this.adbName, appBinaryPath); - await this.adb.install(this.adbName, testBinaryPath); - } + async _handleLaunchApp({ manually, launchArgs }) { + const { adbName, _packageId } = this; - _getTestApkPath(originalApkPath) { - const testApkPath = apkUtils.getTestApkPath(originalApkPath); + await this.emitter.emit('beforeLaunchApp', { deviceId: adbName, bundleId: _packageId, launchArgs }); - if (!fs.existsSync(testApkPath)) { - throw new DetoxRuntimeError({ - message: `The test APK could not be found at path: '${testApkPath}'`, - hint: 'Try running the detox build command, and make sure it was configured to execute a build command (e.g. \'./gradlew assembleAndroidTest\')' + - '\nFor further assistance, visit the Android setup guide: https://github.com/wix/Detox/blob/master/docs/Introduction.Android.md', - }); + launchArgs = await this._modifyArgsForNotificationHandling(launchArgs); + + if (manually) { + await this.__waitForAppLaunch(launchArgs); + } else { + await this.__launchApp(launchArgs); } - return testApkPath; - } - async _modifyArgsForNotificationHandling(adbName, bundleId, launchArgs) { - let _launchArgs = launchArgs; - if (launchArgs.detoxUserNotificationDataURL) { - const notificationPayloadTargetPath = await this._sendNotificationDataToDevice(launchArgs.detoxUserNotificationDataURL, adbName); - _launchArgs = { - ...launchArgs, - detoxUserNotificationDataURL: notificationPayloadTargetPath, - }; + const pid = await this._waitForProcess(); + if (manually) { + log.info({}, `Found the app (${_packageId}) with process ID = ${pid}. Proceeding...`); } - return _launchArgs; + + await this.emitter.emit('launchApp', { deviceId: adbName, bundleId: _packageId, launchArgs, pid }); + return pid; } - async _launchApp(adbName, bundleId, launchArgs) { - if (!this.instrumentation.isRunning()) { - await this._launchInstrumentationProcess(adbName, bundleId, launchArgs); + async __launchApp(launchArgs) { + if (!this._instrumentation.isRunning()) { + await this._launchInstrumentationProcess(launchArgs); await sleep(500); } else if (launchArgs.detoxURLOverride) { await this._startActivityWithUrl(launchArgs.detoxURLOverride); @@ -302,47 +287,124 @@ class AndroidDriver extends DeviceDriverBase { } } - async _launchInstrumentationProcess(adbName, bundleId, userLaunchArgs) { + async __waitForAppLaunch(launchArgs) { + const { adbName, _packageId } = this; + + const instrumentationClass = await this.adb.getInstrumentationRunner(adbName, _packageId); + this._printInstrumentationHint({ instrumentationClass, launchArgs }); + await pressAnyKey(); + await this._reverseServerPort(adbName); + } + + async _launchInstrumentationProcess(userLaunchArgs) { + const { adbName, _packageId } = this; const serverPort = await this._reverseServerPort(adbName); - this.instrumentation.setTerminationFn(async () => { + this._instrumentation.setTerminationFn(async () => { await this._terminateInstrumentation(); await this.adb.reverseRemove(adbName, serverPort); }); - await this.instrumentation.launch(adbName, bundleId, userLaunchArgs); + await this._instrumentation.launch(adbName, _packageId, userLaunchArgs); } - async _reverseServerPort(adbName) { - const serverPort = new URL(this.client.serverUrl).port; - await this.adb.reverse(adbName, serverPort); - return serverPort; + _startActivityWithUrl(url) { + return this.invocationManager.execute(DetoxApi.startActivityFromUrl(url)); } - async _terminateInstrumentation() { - await this.instrumentation.terminate(); - await this.instrumentation.setTerminationFn(null); + _startActivityFromNotification(dataFilePath) { + return this.invocationManager.execute(DetoxApi.startActivityFromNotification(dataFilePath)); } + _resumeMainActivity() { + return this.invocationManager.execute(DetoxApi.launchMainActivity()); + } + + async _modifyArgsForNotificationHandling(launchArgs) { + if (launchArgs.detoxUserNotificationDataURL) { + const notificationLocalFile = this._createPayloadFile(launchArgs.detoxUserNotificationDataURL); + const notificationTargetPath = await this._sendNotificationDataToDevice(notificationLocalFile.path, this.adbName); + notificationLocalFile.cleanup(); + + return { + ...launchArgs, + detoxUserNotificationDataURL: notificationTargetPath, + }; + } + return launchArgs; + } + + /** @override */ + async _waitUntilReady() { + try { + await Promise.race([super._waitUntilReady(), this._instrumentation.waitForCrash()]); + } finally { + this._instrumentation.abortWaitForCrash(); + } + } + + /** @protected */ async _sendNotificationDataToDevice(dataFileLocalPath, adbName) { await this.fileXfer.prepareDestinationDir(adbName); return await this.fileXfer.send(adbName, dataFileLocalPath, 'notification.json'); } - _startActivityWithUrl(url) { - return this.invocationManager.execute(DetoxApi.startActivityFromUrl(url)); + /** @protected */ + async _reverseServerPort(adbName) { + const serverPort = new URL(this.client.serverUrl).port; + await this.adb.reverse(adbName, serverPort); + return serverPort; } - _startActivityFromNotification(dataFilePath) { - return this.invocationManager.execute(DetoxApi.startActivityFromNotification(dataFilePath)); + /** @protected */ + _getAppInstallPaths(_appBinaryPath, _testBinaryPath) { + const appBinaryPath = getAbsoluteBinaryPath(_appBinaryPath); + const testBinaryPath = _testBinaryPath ? getAbsoluteBinaryPath(_testBinaryPath) : this._getTestApkPath(appBinaryPath); + return { + appBinaryPath, + testBinaryPath, + }; } - _resumeMainActivity() { - return this.invocationManager.execute(DetoxApi.launchMainActivity()); + /** @protected */ + _getTestApkPath(originalApkPath) { + const testApkPath = apkUtils.getTestApkPath(originalApkPath); + + if (!fs.existsSync(testApkPath)) { + throw new DetoxRuntimeError({ + message: `The test APK could not be found at path: '${testApkPath}'`, + hint: 'Try running the detox build command, and make sure it was configured to execute a build command (e.g. \'./gradlew assembleAndroidTest\')' + + '\nFor further assistance, visit the Android setup guide: https://github.com/wix/Detox/blob/master/docs/Introduction.Android.md', + }); + } + return testApkPath; + } + + /** @protected */ + async _validateAppBinaries(appBinaryPath, testBinaryPath) { + try { + await this.apkValidator.validateAppApk(appBinaryPath); + } catch (e) { + log.warn(e.toString()); + } + + try { + await this.apkValidator.validateTestApk(testBinaryPath); + } catch (e) { + log.warn(e.toString()); + } + } + + /** @protected */ + async _installAppBinaries(appBinaryPath, testBinaryPath) { + await this.adb.install(this.adbName, appBinaryPath); + await this.adb.install(this.adbName, testBinaryPath); } - async _waitForProcess(adbName, bundleId) { + /** @protected */ + async _waitForProcess() { + const { adbName, _packageId } = this; let pid = NaN; try { - const queryPid = () => this._queryPID(adbName, bundleId); + const queryPid = () => this._queryPID(_packageId); const retryQueryPid = () => retry({ backoff: 'none', retries: 4 }, queryPid); const retryQueryPidMultiple = () => retry({ backoff: 'linear' }, retryQueryPid); pid = await retryQueryPidMultiple(); @@ -353,21 +415,22 @@ class AndroidDriver extends DeviceDriverBase { return pid; } - async _queryPID(adbName, bundleId) { - const pid = await this.adb.pidof(adbName, bundleId); + /** @protected */ + async _queryPID(appId) { + const pid = await this.adb.pidof(this.adbName, appId); if (!pid) { - throw new DetoxRuntimeError('PID still not available'); + throw new DetoxRuntimeError({ message: 'PID still not available' }); } return pid; } - async _waitForAppLaunch(adbName, bundleId, launchArgs) { - const instrumentationClass = await this.adb.getInstrumentationRunner(adbName, bundleId); - this._printInstrumentationHint({ instrumentationClass, launchArgs }); - await pressAnyKey(); - await this._reverseServerPort(adbName); + /** @protected */ + async _terminateInstrumentation() { + await this._instrumentation.terminate(); + await this._instrumentation.setTerminationFn(null); } + /** @protected */ _printInstrumentationHint({ instrumentationClass, launchArgs }) { const keyMaxLength = Math.max(3, _(launchArgs).keys().maxBy('length').length); const valueMaxLength = Math.max(5, _(launchArgs).values().map(String).maxBy('length').length); @@ -396,4 +459,7 @@ class AndroidDriver extends DeviceDriverBase { } } -module.exports = AndroidDriver; +module.exports = { + AndroidDeviceDriver, + AndroidAppDriver, +}; diff --git a/detox/src/devices/runtime/drivers/android/AndroidDriver.test.js b/detox/src/devices/runtime/drivers/android/AndroidDrivers.test.js similarity index 100% rename from detox/src/devices/runtime/drivers/android/AndroidDriver.test.js rename to detox/src/devices/runtime/drivers/android/AndroidDrivers.test.js diff --git a/detox/src/devices/runtime/drivers/android/attached/AttachedAndroidDriver.js b/detox/src/devices/runtime/drivers/android/attached/AttachedAndroidDriver.js index aac9aa1bcd..3120454715 100644 --- a/detox/src/devices/runtime/drivers/android/attached/AttachedAndroidDriver.js +++ b/detox/src/devices/runtime/drivers/android/attached/AttachedAndroidDriver.js @@ -1,7 +1,8 @@ const AndroidDriver = require('../AndroidDriver'); class AttachedAndroidDriver extends AndroidDriver { - getDeviceName() { + /** @override */ + get deviceName() { return `AttachedDevice:${this.adbName}`; } } diff --git a/detox/src/devices/runtime/drivers/android/emulator/EmulatorDriver.js b/detox/src/devices/runtime/drivers/android/emulator/EmulatorDriver.js index da73dcfcf3..7113260d95 100644 --- a/detox/src/devices/runtime/drivers/android/emulator/EmulatorDriver.js +++ b/detox/src/devices/runtime/drivers/android/emulator/EmulatorDriver.js @@ -1,33 +1,43 @@ // @ts-nocheck -const AndroidDriver = require('../AndroidDriver'); +const { AndroidDeviceDriver, AndroidAppDriver } = require('../AndroidDrivers'); /** - * @typedef { AndroidDriverDeps } EmulatorDriverDeps + * @typedef { AndroidDeviceDriverDeps } EmulatorDeviceDriverDeps */ -/** - * @typedef { AndroidDriverProps } EmulatorDriverProps - * @property avdName { String } - * @property forceAdbInstall { Boolean } - */ - -// TODO Unit test coverage -class EmulatorDriver extends AndroidDriver { +class EmulatorDeviceDriver extends AndroidDeviceDriver { /** - * @param deps { EmulatorDriverDeps } - * @param props { EmulatorDriverProps } + * @param deps { EmulatorDeviceDriverDeps } + * @param props {{ adbName: String, avdName: String }} */ - constructor(deps, { adbName, avdName, forceAdbInstall }) { + constructor(deps, { adbName, avdName }) { super(deps, { adbName }); this._deviceName = `${adbName} (${avdName})`; - this._forceAdbInstall = forceAdbInstall; } - getDeviceName() { + /** @override */ + get deviceName() { return this._deviceName; } + async setLocation(lat, lon) { + await this.adb.setLocation(this.adbName, lat, lon); + } +} + +class EmulatorAppDriver extends AndroidAppDriver { + /** + * @param deps { AndroidAppDriverDeps } + * @param props {{ adbName: String, forceAdbInstall: Boolean }} + */ + constructor(deps, { adbName, forceAdbInstall }) { + super(deps, { adbName }); + + this._forceAdbInstall = forceAdbInstall; + } + + /** @override */ async _installAppBinaries(appBinaryPath, testBinaryPath) { if (this._forceAdbInstall) { await super._installAppBinaries(appBinaryPath, testBinaryPath); @@ -36,13 +46,13 @@ class EmulatorDriver extends AndroidDriver { } } - async setLocation(lat, lon) { - await this.adb.setLocation(this.adbName, lat, lon); - } - async __installAppBinaries(appBinaryPath, testBinaryPath) { await this.appInstallHelper.install(this.adbName, appBinaryPath, testBinaryPath); } } -module.exports = EmulatorDriver; + +module.exports = { + EmulatorDeviceDriver, + EmulatorAppDriver, +}; diff --git a/detox/src/devices/runtime/drivers/android/genycloud/GenyCloudDriver.js b/detox/src/devices/runtime/drivers/android/genycloud/GenyCloudDriver.js index 92e4395705..fa8331a01b 100644 --- a/detox/src/devices/runtime/drivers/android/genycloud/GenyCloudDriver.js +++ b/detox/src/devices/runtime/drivers/android/genycloud/GenyCloudDriver.js @@ -3,7 +3,7 @@ const DetoxGenymotionManager = require('../../../../../android/espressoapi/Detox const AndroidDriver = require('../AndroidDriver'); /** - * @typedef { AndroidDriverDeps } GenycloudDriverDeps + * @typedef { AndroidDeviceDriverDeps } GenycloudDriverDeps */ /** @@ -21,7 +21,8 @@ class GenyCloudDriver extends AndroidDriver { this.instance = instance; } - getDeviceName() { + /** @override */ + get deviceName() { return this.instance.toString(); } diff --git a/detox/src/devices/runtime/drivers/ios/IosDriver.js b/detox/src/devices/runtime/drivers/ios/IosDriver.js deleted file mode 100644 index 8302c03f16..0000000000 --- a/detox/src/devices/runtime/drivers/ios/IosDriver.js +++ /dev/null @@ -1,41 +0,0 @@ -// @ts-nocheck -const fs = require('fs'); -const path = require('path'); - -const DetoxRuntimeError = require('../../../../errors/DetoxRuntimeError'); -const DeviceDriverBase = require('../DeviceDriverBase'); - -class IosDriver extends DeviceDriverBase { - createPayloadFile(notification) { - const notificationFilePath = path.join(this.createRandomDirectory(), `payload.json`); - fs.writeFileSync(notificationFilePath, JSON.stringify(notification, null, 2)); - return notificationFilePath; - } - - async setURLBlacklist(blacklistURLs) { - await this.client.setSyncSettings({ blacklistURLs: blacklistURLs }); - } - - async enableSynchronization() { - await this.client.setSyncSettings({ enabled: true }); - } - - async disableSynchronization() { - await this.client.setSyncSettings({ enabled: false }); - } - - async shake() { - await this.client.shake(); - } - - async setOrientation(orientation) { - if (!['portrait', 'landscape'].some(option => option === orientation)) throw new DetoxRuntimeError("orientation should be either 'portrait' or 'landscape', but got " + (orientation + ')')); - await this.client.setOrientation({ orientation }); - } - - getPlatform() { - return 'ios'; - } -} - -module.exports = IosDriver; diff --git a/detox/src/devices/runtime/drivers/ios/IosDrivers.js b/detox/src/devices/runtime/drivers/ios/IosDrivers.js new file mode 100644 index 0000000000..5c032f42aa --- /dev/null +++ b/detox/src/devices/runtime/drivers/ios/IosDrivers.js @@ -0,0 +1,75 @@ +const DetoxRuntimeError = require('../../../../errors/DetoxRuntimeError'); +const { DeviceDriver, TestAppDriver } = require('../BaseDrivers'); + +class IosDeviceDriver extends DeviceDriver { + /** @override */ + get platform() { + return 'ios'; + } +} + +class IosAppDriver extends TestAppDriver { + /** @override */ + async deselect() { + // We do not yet support concurrently running apps on iOS, so - keeping the legacy behavior, + // we must terminate if we're not the selected ones. + if (this.isRunning()) { + await this.terminate(); + } + } + + /** @override */ + async deliverPayload(params) { + return await this.client.deliverPayload(params); + } + + /** @override */ + async sendUserActivity(payload) { + await this._sendPayload('detoxUserActivityDataURL', payload); + } + + /** @override */ + async sendUserNotification(payload) { + await this._sendPayload('detoxUserNotificationDataURL', payload); + } + + /** @override */ + async terminate() { + // TODO effectively terminate + await super.terminate(); + } + + /** @override */ + async setOrientation(orientation) { + if (!['portrait', 'landscape'].some(option => option === orientation)) { + const message = `orientation should be either 'portrait' or 'landscape', but got (${orientation})`; + throw new DetoxRuntimeError({ message }); + } + await this.client.setOrientation({ orientation }); + } + + /** @override */ + async shake() { + await this.client.shake(); + await this._waitForActive(); + } + + async _sendPayload(name, payload) { + const payloadFile = this._createPayloadFile(payload); + + await this.deliverPayload({ + [name]: payloadFile.path, + }); + payloadFile.cleanup(); + } + + async _waitForActive() { + return await this.client.waitForActive(); + } +} + + +module.exports = { + IosDeviceDriver, + IosAppDriver, +}; diff --git a/detox/src/devices/runtime/drivers/ios/SimulatorDriver.js b/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js similarity index 58% rename from detox/src/devices/runtime/drivers/ios/SimulatorDriver.js rename to detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js index 878d483869..313b730c52 100644 --- a/detox/src/devices/runtime/drivers/ios/SimulatorDriver.js +++ b/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js @@ -1,19 +1,18 @@ // @ts-nocheck const path = require('path'); -const exec = require('child-process-promise').exec; const _ = require('lodash'); const temporaryPath = require('../../../../artifacts/utils/temporaryPath'); const DetoxRuntimeError = require('../../../../errors/DetoxRuntimeError'); -const getAbsoluteBinaryPath = require('../../../../utils/getAbsoluteBinaryPath'); const log = require('../../../../utils/logger').child({ __filename }); const pressAnyKey = require('../../../../utils/pressAnyKey'); +const tempFile = require('../../../../utils/tempFile'); -const IosDriver = require('./IosDriver'); +const { IosDeviceDriver, IosAppDriver } = require('./IosDrivers'); /** - * @typedef SimulatorDriverDeps { DeviceDriverDeps } + * @typedef SimulatorDriverDeps * @property simulatorLauncher { SimulatorLauncher } * @property applesimutils { AppleSimUtils } */ @@ -25,7 +24,7 @@ const IosDriver = require('./IosDriver'); * @property bootArgs { Object } */ -class SimulatorDriver extends IosDriver { +class IosSimulatorDeviceDriver extends IosDeviceDriver { /** * @param deps { SimulatorDriverDeps } * @param props { SimulatorDriverProps } @@ -37,57 +36,121 @@ class SimulatorDriver extends IosDriver { this._type = type; this._bootArgs = bootArgs; this._deviceName = `${udid} (${this._type})`; + this._simulatorLauncher = deps.simulatorLauncher; this._applesimutils = deps.applesimutils; } - getExternalId() { + /** @override */ + get externalId() { return this.udid; } - getDeviceName() { + /** @override */ + get deviceName() { return this._deviceName; } - async getBundleIdFromBinary(appPath) { - appPath = getAbsoluteBinaryPath(appPath); - try { - const result = await exec(`/usr/libexec/PlistBuddy -c "Print CFBundleIdentifier" "${path.join(appPath, 'Info.plist')}"`); - const bundleId = _.trim(result.stdout); - if (_.isEmpty(bundleId)) { - throw new Error(); - } - return bundleId; - } catch (ex) { - throw new DetoxRuntimeError(`field CFBundleIdentifier not found inside Info.plist of app binary at ${appPath}`); - } + /** @override */ + async setBiometricEnrollment(yesOrNo) { + await this._applesimutils.setBiometricEnrollment(this.udid, yesOrNo); } - async installApp(binaryPath) { - await this._applesimutils.install(this.udid, getAbsoluteBinaryPath(binaryPath)); + /** @override */ + async setLocation(lat, lon) { + await this._applesimutils.setLocation(this.udid, lat, lon); } - async uninstallApp(bundleId) { - const { udid } = this; - await this.emitter.emit('beforeUninstallApp', { deviceId: udid, bundleId }); - await this._applesimutils.uninstall(udid, bundleId); + /** @override */ + async clearKeychain() { + await this._applesimutils.clearKeychain(this.udid); } - async launchApp(bundleId, launchArgs, languageAndLocale) { - const { udid } = this; + /** @override */ + async resetContentAndSettings() { + await this._simulatorLauncher.shutdown(this.udid); + await this._applesimutils.resetContentAndSettings(this.udid); + await this._simulatorLauncher.launch(this.udid, this._type, this._bootArgs); + } + + /** @override */ + async takeScreenshot(screenshotName) { + const tempPath = await temporaryPath.for.png(); + await this._applesimutils.takeScreenshot(this.udid, tempPath); + + await this.emitter.emit('createExternalArtifact', { + pluginId: 'screenshot', + artifactName: screenshotName || path.basename(tempPath, '.png'), + artifactPath: tempPath, + }); + + return tempPath; + } + + /** @override */ + async setStatusBar(flags) { + await this._applesimutils.statusBarOverride(this.udid, flags); + } + + /** @override */ + async resetStatusBar() { + await this._applesimutils.statusBarReset(this.udid); + } + + async _waitForBackground() { + return await this.client.waitForBackground(); + } +} + +/** + * @typedef { LaunchInfo } LaunchInfoIosSim + * @property languageAndLocale { String } + */ + +class IosSimulatorAppDriver extends IosAppDriver { + constructor(deps, { udid, bundleId }) { + super(deps); + this.udid = udid; + this.bundleId = bundleId; + + this._applesimutils = deps.applesimutils; + } + + /** + * @override + * @param launchInfo { LaunchInfoIosSim } + */ + async launch(launchInfo) { + const { udid, bundleId } = this; + + const launchArgsHandle = this._getLaunchArgsForPayloadsData(launchInfo.launchArgs); + const { launchArgs } = launchArgsHandle; + await this.emitter.emit('beforeLaunchApp', { bundleId, deviceId: udid, launchArgs }); - const pid = await this._applesimutils.launch(udid, bundleId, launchArgs, languageAndLocale); + const pid = await this._applesimutils.launch(udid, bundleId, launchArgs, launchInfo.languageAndLocale); await this.emitter.emit('launchApp', { bundleId, deviceId: udid, launchArgs, pid }); + launchArgsHandle.cleanup(); + + await this._waitUntilReady(); + await this._waitForActive(); return pid; } - async waitForAppLaunch(bundleId, launchArgs, languageAndLocale) { - const { udid } = this; + /** + * @override + * @param launchInfo { LaunchInfoIosSim } + */ + async waitForLaunch(launchInfo) { + const { udid, bundleId } = this; + + // Note: This is purely semantic; Has no analytical value. + const launchArgsHandle = this._getLaunchArgsForPayloadsData(launchInfo.launchArgs); + const { launchArgs } = launchArgsHandle; await this.emitter.emit('beforeLaunchApp', { bundleId, deviceId: udid, launchArgs }); - this._applesimutils.printLaunchHint(udid, bundleId, launchArgs, languageAndLocale); + this._applesimutils.printLaunchHint(udid, bundleId, launchArgs, launchInfo.languageAndLocale); await pressAnyKey(); const pid = await this._applesimutils.getPid(udid, bundleId); @@ -95,90 +158,57 @@ class SimulatorDriver extends IosDriver { throw new DetoxRuntimeError({ message: `Failed to find a process corresponding to the app bundle identifier (${bundleId}).`, hint: `Make sure that the app is running on the device (${udid}), visually or via CLI:\n` + - `xcrun simctl spawn ${this.udid} launchctl list | grep -F '${bundleId}'\n`, + `xcrun simctl spawn ${this.udid} launchctl list | grep -F '${bundleId}'\n`, }); } else { log.info({}, `Found the app (${bundleId}) with process ID = ${pid}. Proceeding...`); } - await this.emitter.emit('launchApp', { bundleId, deviceId: udid, launchArgs, pid }); + + launchArgsHandle.cleanup(); + + await this._waitUntilReady(); + await this._waitForActive(); return pid; } - async terminate(bundleId) { - const { udid } = this; - await this.emitter.emit('beforeTerminateApp', { deviceId: udid, bundleId }); - await this._applesimutils.terminate(udid, bundleId); - await this.emitter.emit('terminateApp', { deviceId: udid, bundleId }); + /** @override */ + async setPermissions(permissions) { + const { udid, bundleId } = this; + await this._applesimutils.setPermissions(udid, bundleId, permissions); } - async setBiometricEnrollment(yesOrNo) { - await this._applesimutils.setBiometricEnrollment(this.udid, yesOrNo); + /** @override */ + async sendToHome() { + await this._applesimutils.sendToHome(this.udid); + await this._waitForBackground(); } + /** @override */ async matchFace() { await this._applesimutils.matchBiometric(this.udid, 'Face'); + await this._waitForActive(); } + /** @override */ async unmatchFace() { await this._applesimutils.unmatchBiometric(this.udid, 'Face'); + await this._waitForActive(); } + /** @override */ async matchFinger() { await this._applesimutils.matchBiometric(this.udid, 'Finger'); + await this._waitForActive(); } + /** @override */ async unmatchFinger() { await this._applesimutils.unmatchBiometric(this.udid, 'Finger'); + await this._waitForActive(); } - async sendToHome() { - await this._applesimutils.sendToHome(this.udid); - } - - async setLocation(lat, lon) { - await this._applesimutils.setLocation(this.udid, lat, lon); - } - - async setPermissions(bundleId, permissions) { - await this._applesimutils.setPermissions(this.udid, bundleId, permissions); - } - - async clearKeychain() { - await this._applesimutils.clearKeychain(this.udid); - } - - async resetContentAndSettings() { - await this._simulatorLauncher.shutdown(this.udid); - await this._applesimutils.resetContentAndSettings(this.udid); - await this._simulatorLauncher.launch(this.udid, this._type, this._bootArgs); - } - - getLogsPaths() { - return this._applesimutils.getLogsPaths(this.udid); - } - - async waitForActive() { - return await this.client.waitForActive(); - } - - async waitForBackground() { - return await this.client.waitForBackground(); - } - - async takeScreenshot(screenshotName) { - const tempPath = await temporaryPath.for.png(); - await this._applesimutils.takeScreenshot(this.udid, tempPath); - - await this.emitter.emit('createExternalArtifact', { - pluginId: 'screenshot', - artifactName: screenshotName || path.basename(tempPath, '.png'), - artifactPath: tempPath, - }); - - return tempPath; - } - + /** @override */ async captureViewHierarchy(artifactName) { const viewHierarchyURL = temporaryPath.for.viewhierarchy(); await this.client.captureViewHierarchy({ viewHierarchyURL }); @@ -192,13 +222,38 @@ class SimulatorDriver extends IosDriver { return viewHierarchyURL; } - async setStatusBar(flags) { - await this._applesimutils.statusBarOverride(this.udid, flags); + async _waitUntilReady() { + return super._waitUntilReady(); // Just for clarity } - async resetStatusBar() { - await this._applesimutils.statusBarReset(this.udid); + async _waitForActive() { + return this.client.waitForActive(); + } + + // TODO (multiapps) Reiterate this ugly func signature + _getLaunchArgsForPayloadsData(launchArgs) { + let paramName; + if (launchArgs.detoxUserNotificationDataURL) { + paramName = 'detoxUserNotificationDataURL'; + } else if (launchArgs.detoxUserActivityDataURL) { + paramName = 'detoxUserActivityDataURL'; + } else { + return { launchArgs, cleanup: _.noop }; + } + + const payloadFile = tempFile.create('payload.json'); + return { + launchArgs: { + ...launchArgs, + [paramName]: payloadFile.path, + }, + cleanup: () => payloadFile.cleanup(), + }; } } -module.exports = SimulatorDriver; + +module.exports = { + IosSimulatorDeviceDriver, + IosSimulatorAppDriver, +}; diff --git a/detox/src/devices/runtime/drivers/ios/SimulatorDriver.test.js b/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.test.js similarity index 100% rename from detox/src/devices/runtime/drivers/ios/SimulatorDriver.test.js rename to detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.test.js diff --git a/detox/src/devices/runtime/factories/android.js b/detox/src/devices/runtime/factories/android.js index e025ecf4c2..1f51dfaf9a 100644 --- a/detox/src/devices/runtime/factories/android.js +++ b/detox/src/devices/runtime/factories/android.js @@ -1,7 +1,7 @@ const RuntimeDeviceFactory = require('./base'); class RuntimeDriverFactoryAndroid extends RuntimeDeviceFactory { - _createDriverDependencies(commonDeps) { + _createFundamentalDriverDeps(commonDeps) { const serviceLocator = require('../../../servicelocator/android'); const adb = serviceLocator.adb; const aapt = serviceLocator.aapt; @@ -25,18 +25,72 @@ class RuntimeDriverFactoryAndroid extends RuntimeDeviceFactory { instrumentation: new MonitoredInstrumentation(adb), }; } + + _createAppDriverDeps(fundamentalDeps, { sessionConfig }, alias) { + const UiDeviceProxy = require('../../../android/espressoapi/UiDeviceProxy'); + const Client = require('../../../client/Client'); + const { InvocationManager } = require('../../../invoke'); + const MonitoredInstrumentation = require('../../common/drivers/android/tools/MonitoredInstrumentation'); + + const { adb } = fundamentalDeps; + const appSessionConfig = this._createAppSessionConfig(sessionConfig, alias); + const client = new Client(appSessionConfig); // TODO (multiapps): Share the same ws + const invocationManager = new InvocationManager(client); + const uiDevice = new UiDeviceProxy(invocationManager).getUIDevice(); + const instrumentation = new MonitoredInstrumentation(adb); + + return { + client, + invocationManager, + uiDevice, + instrumentation, + }; + } + + _createAppSessionConfig(sessionConfig, alias) { + const { sessionId } = sessionConfig; + + if (alias) { + return { + ...sessionConfig, + sessionId: `${sessionId}:${alias}`, + }; + } + return sessionConfig; + } } class AndroidEmulator extends RuntimeDriverFactoryAndroid { - _createDriver(deviceCookie, deps, { deviceConfig }) { + /** @override */ + _createTestAppDriver(deviceCookie, commonDeps, { deviceConfig, sessionConfig }, alias) { + const fundamentalDeps = this._createFundamentalDriverDeps(commonDeps); + const appDeps = this._createAppDriverDeps(fundamentalDeps, { sessionConfig }, alias); + + const deps = { + ...fundamentalDeps, + ...appDeps, + }; + const props = { adbName: deviceCookie.adbName, - avdName: deviceConfig.device.avdName, forceAdbInstall: deviceConfig.forceAdbInstall, }; - const { AndroidEmulatorRuntimeDriver } = require('../drivers'); - return new AndroidEmulatorRuntimeDriver(deps, props); + const { EmulatorAppDriver } = require('../drivers/android/emulator/EmulatorDriver'); + return new EmulatorAppDriver(deps, props); + } + + /** @override */ + _createDeviceDriver(deviceCookie, commonDeps, { deviceConfig }) { + const fundamentalDeps = this._createFundamentalDriverDeps(commonDeps); + + const props = { + adbName: deviceCookie.adbName, + avdName: deviceConfig.device.avdName, + }; + + const { EmulatorDeviceDriver } = require('../drivers/android/emulator/EmulatorDriver'); + return new EmulatorDeviceDriver(fundamentalDeps, props); } } diff --git a/detox/src/devices/runtime/factories/base.js b/detox/src/devices/runtime/factories/base.js index 626b736054..fcd3b1bfe6 100644 --- a/detox/src/devices/runtime/factories/base.js +++ b/detox/src/devices/runtime/factories/base.js @@ -1,14 +1,52 @@ +const _ = require('lodash'); + const RuntimeDevice = require('../RuntimeDevice'); +const { PredefinedTestApp, UnspecifiedTestApp, UtilApp } = require('../TestApp'); class RuntimeDeviceFactory { createRuntimeDevice(deviceCookie, commonDeps, configs) { - const deps = this._createDriverDependencies(commonDeps); - const runtimeDriver = this._createDriver(deviceCookie, deps, configs); - return new RuntimeDevice({ ...commonDeps, ...configs }, runtimeDriver); + const apps = this._createApps(deviceCookie, commonDeps, configs); + + const driver = this._createDeviceDriver(deviceCookie, commonDeps, configs); + + const { deviceConfig } = configs; + return new RuntimeDevice(apps, { ...commonDeps, driver }, { deviceConfig }); + } + + _createApps(deviceCookie, commonDeps, configs) { + return { + predefinedApps: this._createPredefinedTestApps(deviceCookie, commonDeps, configs), + unspecifiedApp: this._createUnspecifiedTestApp(deviceCookie, commonDeps, configs), + utilApps: this._createUtilAppsList(deviceCookie, commonDeps, configs), + }; + } + + _createPredefinedTestApps(deviceCookie, commonDeps, configs) { + const { appsConfig, behaviorConfig } = configs; + return _.mapValues(appsConfig, (appConfig, alias) => { + const driver = this._createTestAppDriver(deviceCookie, commonDeps, configs, alias); + return new PredefinedTestApp(driver, { appConfig, behaviorConfig }, alias); + }); + } + + _createUnspecifiedTestApp(deviceCookie, commonDeps, configs) { + const { behaviorConfig } = configs; + const driver = this._createTestAppDriver(deviceCookie, commonDeps, configs, null); + return new UnspecifiedTestApp(driver, { behaviorConfig }); + } + + _createUtilAppsList(deviceCookie, commonDeps, configs) { + const { deviceConfig } = configs; + + return deviceConfig.utilBinaryPaths.map((binaryPath) => { + const driver = this._createTestAppDriver(deviceCookie, commonDeps, configs, null); + const appConfig = { binaryPath }; + return new UtilApp(driver, { appConfig }); + }); } - _createDriverDependencies(commonDeps) { } // eslint-disable-line no-unused-vars - _createDriver(deviceCookie, deps, configs) {} // eslint-disable-line no-unused-vars + _createTestAppDriver(_deviceCookie, _commonDeps, _configs, _alias) {} + _createDeviceDriver(_deviceCookie, _deps, _configs) {} } module.exports = RuntimeDeviceFactory; diff --git a/detox/src/utils/p-iteration.js b/detox/src/utils/p-iteration.js new file mode 100644 index 0000000000..91c74004c2 --- /dev/null +++ b/detox/src/utils/p-iteration.js @@ -0,0 +1,13 @@ +const pIteration = require('p-iteration'); + +function forEachSeriesObj(objMap, callback, _this) { + return pIteration.forEachSeries(Object.keys(objMap), ((key) => { + const obj = objMap[key]; + return callback(obj, key); + }), _this); +} + +module.exports = { + ...pIteration, + forEachSeriesObj, +}; diff --git a/detox/src/utils/tempFile.js b/detox/src/utils/tempFile.js new file mode 100644 index 0000000000..3531e8007e --- /dev/null +++ b/detox/src/utils/tempFile.js @@ -0,0 +1,21 @@ +const os = require('os'); +const path = require('path'); + +const fs = require('fs-extra'); + +function create(filename) { + const directoryPath = fs.mkdtempSync(path.join(os.tmpdir(), 'detoxtemp-')); + fs.ensureDirSync(directoryPath); + + const fullPath = path.join(directoryPath, filename); + return { + path: fullPath, + cleanup: () => { + fs.removeSync(directoryPath); + }, + }; +} + +module.exports = { + create, +}; diff --git a/detox/src/utils/tempFile.test.js b/detox/src/utils/tempFile.test.js new file mode 100644 index 0000000000..37c15f2c45 --- /dev/null +++ b/detox/src/utils/tempFile.test.js @@ -0,0 +1,33 @@ +const path = require('path'); + +const fs = require('fs-extra'); + +describe('Temporary local file util', () => { + const filename = 'file-detox.tmp'; + + let tempFileUtil; + beforeEach(() => { + tempFileUtil = require('./tempFile'); + }); + + it('should return a file path', async () => { + const tempFile = tempFileUtil.create(filename); + expect(tempFile.path).toBeDefined(); + }); + + it('should create a directory', async () => { + const tempFile = tempFileUtil.create(filename); + const filePath = path.dirname(tempFile.path); + await expect( fs.pathExists(filePath) ).resolves.toEqual(true); + }); + + it('should provide a clean-up method', async () => { + const tempFile = tempFileUtil.create(filename); + const filePath = path.dirname(tempFile.path); + + tempFile.cleanup(); + + await expect( fs.pathExists(tempFile) ).resolves.toEqual(false); + await expect( fs.pathExists(filePath) ).resolves.toEqual(false); + }); +}); diff --git a/detox/test/integration/stub/StubRuntimeDriver.js b/detox/test/integration/stub/StubRuntimeDriver.js index 918ec3fab2..4988852efe 100644 --- a/detox/test/integration/stub/StubRuntimeDriver.js +++ b/detox/test/integration/stub/StubRuntimeDriver.js @@ -27,7 +27,7 @@ class StubRuntimeDriver extends DeviceDriverBase { return `Stub #${this._deviceId}`; } - getPlatform() { + get platform() { return 'stub'; } diff --git a/examples/demo-plugin/driver.js b/examples/demo-plugin/driver.js index 11a61e8c12..ae3f813f53 100644 --- a/examples/demo-plugin/driver.js +++ b/examples/demo-plugin/driver.js @@ -132,11 +132,11 @@ class PluginRuntimeDriver extends DeviceDriverBase { this.app = new PluginApp(deps); } - getExternalId() { + get externalId() { return this.cookie.id; } - getDeviceName() { + get deviceName() { return 'Plugin'; // TODO } From 57defb4b118c9fe3e4a6420b34c13d857eca0e99 Mon Sep 17 00:00:00 2001 From: d4vidi Date: Wed, 15 Jun 2022 11:58:16 +0300 Subject: [PATCH 02/24] [wip] Adjust matchers for Android --- detox/src/android/AndroidExpect.js | 12 +++---- detox/src/android/core/NativeElement.js | 42 +++++++++++++----------- detox/src/android/core/NativeExpect.js | 26 +++++++-------- detox/src/android/core/NativeWaitFor.js | 20 +++++------ detox/src/android/core/WebElement.js | 36 +++++++++----------- detox/src/android/core/WebExpect.js | 12 +++---- detox/src/android/interactions/native.js | 17 ++++++---- detox/src/android/interactions/web.js | 17 ++++++---- detox/src/devices/runtime/TestApp.js | 12 +++++++ 9 files changed, 105 insertions(+), 89 deletions(-) diff --git a/detox/src/android/AndroidExpect.js b/detox/src/android/AndroidExpect.js index a2bb4df410..1d4a9f8cf4 100644 --- a/detox/src/android/AndroidExpect.js +++ b/detox/src/android/AndroidExpect.js @@ -10,10 +10,9 @@ const { WebExpectElement } = require('./core/WebExpect'); const matchers = require('./matchers'); class AndroidExpect { - constructor({ invocationManager, device, emitter }) { + constructor({ device, emitter }) { this._device = device; this._emitter = emitter; - this._invocationManager = invocationManager; this.by = matchers; this.element = this.element.bind(this); @@ -25,7 +24,7 @@ class AndroidExpect { element(matcher) { if (matcher instanceof NativeMatcher) { - return new NativeElement(this._invocationManager, this._emitter, matcher); + return new NativeElement(this._device, this._emitter, matcher); } throw new DetoxRuntimeError(`element() argument is invalid, expected a native matcher, but got ${typeof element}`); @@ -37,7 +36,6 @@ class AndroidExpect { return new WebViewElement({ device: this._device, emitter: this._emitter, - invocationManager: this._invocationManager, matcher, }); } @@ -46,13 +44,13 @@ class AndroidExpect { } expect(element) { - if (element instanceof WebElement) return new WebExpectElement(this._invocationManager, element); - if (element instanceof NativeElement) return new NativeExpectElement(this._invocationManager, element); + if (element instanceof WebElement) return new WebExpectElement(this._device, element); + if (element instanceof NativeElement) return new NativeExpectElement(this._device, element); throw new DetoxRuntimeError(`expect() argument is invalid, expected a native or web matcher, but got ${typeof element}`); } waitFor(element) { - if (element instanceof NativeElement) return new NativeWaitForElement(this._invocationManager, element); + if (element instanceof NativeElement) return new NativeWaitForElement(this._device, element); throw new DetoxRuntimeError(`waitFor() argument is invalid, got ${typeof element}`); } } diff --git a/detox/src/android/core/NativeElement.js b/detox/src/android/core/NativeElement.js index 460a07c813..0ec8187e36 100644 --- a/detox/src/android/core/NativeElement.js +++ b/detox/src/android/core/NativeElement.js @@ -10,8 +10,8 @@ const DetoxMatcherApi = require('../espressoapi/DetoxMatcher'); const { ActionInteraction } = require('../interactions/native'); class NativeElement { - constructor(invocationManager, emitter, matcher) { - this._invocationManager = invocationManager; + constructor(device, emitter, matcher) { + this._device = device; this._emitter = emitter; this._originalMatcher = matcher; this._selectElementWithMatcher(this._originalMatcher); @@ -23,7 +23,11 @@ class NativeElement { } atIndex(index) { - if (typeof index !== 'number') throw new DetoxRuntimeError(`Element atIndex argument must be a number, got ${typeof index}`); + const message = `Element atIndex argument must be a number, got ${typeof index}`; + if (typeof index !== 'number') { + throw new DetoxRuntimeError({ message }); + } + const matcher = this._originalMatcher; this._originalMatcher._call = invoke.callDirectly(DetoxMatcherApi.matcherForAtIndex(index, matcher._call.value)); @@ -32,56 +36,56 @@ class NativeElement { } async tap(value) { - return await new ActionInteraction(this._invocationManager, this, new actions.TapAction(value)).execute(); + return await new ActionInteraction(this._device, this, new actions.TapAction(value)).execute(); } async tapAtPoint(value) { - return await new ActionInteraction(this._invocationManager, this, new actions.TapAtPointAction(value)).execute(); + return await new ActionInteraction(this._device, this, new actions.TapAtPointAction(value)).execute(); } async longPress() { - return await new ActionInteraction(this._invocationManager, this, new actions.LongPressAction()).execute(); + return await new ActionInteraction(this._device, this, new actions.LongPressAction()).execute(); } async multiTap(times) { - return await new ActionInteraction(this._invocationManager, this, new actions.MultiClickAction(times)).execute(); + return await new ActionInteraction(this._device, this, new actions.MultiClickAction(times)).execute(); } async tapBackspaceKey() { - return await new ActionInteraction(this._invocationManager, this, new actions.PressKeyAction(67)).execute(); + return await new ActionInteraction(this._device, this, new actions.PressKeyAction(67)).execute(); } async tapReturnKey() { - return await new ActionInteraction(this._invocationManager, this, new actions.TypeTextAction('\n')).execute(); + return await new ActionInteraction(this._device, this, new actions.TypeTextAction('\n')).execute(); } async typeText(value) { - return await new ActionInteraction(this._invocationManager, this, new actions.TypeTextAction(value)).execute(); + return await new ActionInteraction(this._device, this, new actions.TypeTextAction(value)).execute(); } async replaceText(value) { - return await new ActionInteraction(this._invocationManager, this, new actions.ReplaceTextAction(value)).execute(); + return await new ActionInteraction(this._device, this, new actions.ReplaceTextAction(value)).execute(); } async clearText() { - return await new ActionInteraction(this._invocationManager, this, new actions.ClearTextAction()).execute(); + return await new ActionInteraction(this._device, this, new actions.ClearTextAction()).execute(); } async scroll(amount, direction = 'down', startPositionX, startPositionY) { // override the user's element selection with an extended matcher that looks for UIScrollView children // this._selectElementWithMatcher(this._originalMatcher._extendToDescendantScrollViews()); - return await new ActionInteraction(this._invocationManager, this, new actions.ScrollAmountAction(direction, amount, startPositionX, startPositionY)).execute(); + return await new ActionInteraction(this._device, this, new actions.ScrollAmountAction(direction, amount, startPositionX, startPositionY)).execute(); } async scrollTo(edge) { // override the user's element selection with an extended matcher that looks for UIScrollView children this._selectElementWithMatcher(this._originalMatcher._extendToDescendantScrollViews()); - return await new ActionInteraction(this._invocationManager, this, new actions.ScrollEdgeAction(edge)).execute(); + return await new ActionInteraction(this._device, this, new actions.ScrollEdgeAction(edge)).execute(); } async scrollToIndex(index) { this._selectElementWithMatcher(this._originalMatcher._extendToDescendantScrollViews()); - return await new ActionInteraction(this._invocationManager, this, new actions.ScrollToIndex(index)).execute(); + return await new ActionInteraction(this._device, this, new actions.ScrollToIndex(index)).execute(); } /** @@ -97,12 +101,12 @@ class NativeElement { // override the user's element selection with an extended matcher that avoids RN issues with RCTScrollView this._selectElementWithMatcher(this._originalMatcher._avoidProblematicReactNativeElements()); const action = new actions.SwipeAction(direction, speed, normalizedSwipeOffset, normalizedStartingPointX, normalizedStartingPointY); - return await new ActionInteraction(this._invocationManager, this, action).execute(); + return await new ActionInteraction(this._device, this, action).execute(); } async takeScreenshot(screenshotName) { // TODO this should be moved to a lower-layer handler of this use-case - const resultBase64 = await new ActionInteraction(this._invocationManager, this, new actions.TakeElementScreenshot()).execute(); + const resultBase64 = await new ActionInteraction(this._device, this, new actions.TakeElementScreenshot()).execute(); const filePath = tempfile('detox.element-screenshot.png'); await fs.writeFile(filePath, resultBase64, 'base64'); @@ -115,12 +119,12 @@ class NativeElement { } async getAttributes() { - const result = await new ActionInteraction(this._invocationManager, this, new actions.GetAttributes()).execute(); + const result = await new ActionInteraction(this._device, this, new actions.GetAttributes()).execute(); return JSON.parse(result); } async adjustSliderToPosition(newPosition) { - return await new ActionInteraction(this._invocationManager, this, new actions.AdjustSliderToPosition(newPosition)).execute(); + return await new ActionInteraction(this._device, this, new actions.AdjustSliderToPosition(newPosition)).execute(); } } diff --git a/detox/src/android/core/NativeExpect.js b/detox/src/android/core/NativeExpect.js index 548cd920cc..6bbb3bd61d 100644 --- a/detox/src/android/core/NativeExpect.js +++ b/detox/src/android/core/NativeExpect.js @@ -2,8 +2,8 @@ const { MatcherAssertionInteraction } = require('../interactions/native'); const matchers = require('../matchers/native'); class NativeExpect { - constructor(invocationManager) { - this._invocationManager = invocationManager; + constructor(device) { + this._device = device; } get not() { @@ -13,13 +13,13 @@ class NativeExpect { } class NativeExpectElement extends NativeExpect { - constructor(invocationManager, element) { - super(invocationManager); + constructor(device, element) { + super(device); this._element = element; } async toBeVisible(pct) { - return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.VisibleMatcher(pct).not : new matchers.VisibleMatcher(pct)).execute(); + return await new MatcherAssertionInteraction(this._device, this._element, this._notCondition ? new matchers.VisibleMatcher(pct).not : new matchers.VisibleMatcher(pct)).execute(); } async toBeNotVisible() { @@ -27,7 +27,7 @@ class NativeExpectElement extends NativeExpect { } async toExist() { - return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.ExistsMatcher().not : new matchers.ExistsMatcher()).execute(); + return await new MatcherAssertionInteraction(this._device, this._element, this._notCondition ? new matchers.ExistsMatcher().not : new matchers.ExistsMatcher()).execute(); } async toNotExist() { @@ -35,7 +35,7 @@ class NativeExpectElement extends NativeExpect { } async toHaveText(text) { - return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.TextMatcher(text).not : new matchers.TextMatcher(text)).execute(); + return await new MatcherAssertionInteraction(this._device, this._element, this._notCondition ? new matchers.TextMatcher(text).not : new matchers.TextMatcher(text)).execute(); } async toNotHaveText(text) { @@ -43,7 +43,7 @@ class NativeExpectElement extends NativeExpect { } async toHaveLabel(value) { - return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.LabelMatcher(value).not : new matchers.LabelMatcher(value)).execute(); + return await new MatcherAssertionInteraction(this._device, this._element, this._notCondition ? new matchers.LabelMatcher(value).not : new matchers.LabelMatcher(value)).execute(); } async toNotHaveLabel(value) { @@ -51,7 +51,7 @@ class NativeExpectElement extends NativeExpect { } async toHaveId(value) { - return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.IdMatcher(value).not : new matchers.IdMatcher(value)).execute(); + return await new MatcherAssertionInteraction(this._device, this._element, this._notCondition ? new matchers.IdMatcher(value).not : new matchers.IdMatcher(value)).execute(); } async toNotHaveId(value) { @@ -59,7 +59,7 @@ class NativeExpectElement extends NativeExpect { } async toHaveValue(value) { - return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.ValueMatcher(value).not : new matchers.ValueMatcher(value)).execute(); + return await new MatcherAssertionInteraction(this._device, this._element, this._notCondition ? new matchers.ValueMatcher(value).not : new matchers.ValueMatcher(value)).execute(); } async toNotHaveValue(value) { @@ -67,15 +67,15 @@ class NativeExpectElement extends NativeExpect { } async toHaveToggleValue(value) { - return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.ToggleMatcher(value).not : new matchers.ToggleMatcher(value)).execute(); + return await new MatcherAssertionInteraction(this._device, this._element, this._notCondition ? new matchers.ToggleMatcher(value).not : new matchers.ToggleMatcher(value)).execute(); } async toHaveSliderPosition(value, tolerance = 0) { - return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.SliderPositionMatcher(value, tolerance).not : new matchers.SliderPositionMatcher(value, tolerance)).execute(); + return await new MatcherAssertionInteraction(this._device, this._element, this._notCondition ? new matchers.SliderPositionMatcher(value, tolerance).not : new matchers.SliderPositionMatcher(value, tolerance)).execute(); } async toBeFocused() { - return await new MatcherAssertionInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.FocusMatcher().not : new matchers.FocusMatcher()).execute(); + return await new MatcherAssertionInteraction(this._device, this._element, this._notCondition ? new matchers.FocusMatcher().not : new matchers.FocusMatcher()).execute(); } async toBeNotFocused() { diff --git a/detox/src/android/core/NativeWaitFor.js b/detox/src/android/core/NativeWaitFor.js index 7e4c7bcfa5..5db071267f 100644 --- a/detox/src/android/core/NativeWaitFor.js +++ b/detox/src/android/core/NativeWaitFor.js @@ -2,14 +2,14 @@ const { WaitForInteraction } = require('../interactions/native'); const matchers = require('../matchers/native'); class NativeWaitFor { - constructor(invocationManager) { - this._invocationManager = invocationManager; + constructor(device) { + this._device = device; } } class NativeWaitForElement extends NativeWaitFor { - constructor(invocationManager, element) { - super(invocationManager); + constructor(device, element) { + super(device); this._element = element; } @@ -19,7 +19,7 @@ class NativeWaitForElement extends NativeWaitFor { } toBeVisible(pct) { - return new WaitForInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.VisibleMatcher(pct).not : new matchers.VisibleMatcher(pct)); + return new WaitForInteraction(this._device, this._element, this._notCondition ? new matchers.VisibleMatcher(pct).not : new matchers.VisibleMatcher(pct)); } toBeNotVisible() { @@ -27,7 +27,7 @@ class NativeWaitForElement extends NativeWaitFor { } toExist() { - return new WaitForInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.ExistsMatcher().not : new matchers.ExistsMatcher()); + return new WaitForInteraction(this._device, this._element, this._notCondition ? new matchers.ExistsMatcher().not : new matchers.ExistsMatcher()); } toNotExist() { @@ -35,7 +35,7 @@ class NativeWaitForElement extends NativeWaitFor { } toHaveText(text) { - return new WaitForInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.TextMatcher(text).not : new matchers.TextMatcher(text)); + return new WaitForInteraction(this._device, this._element, this._notCondition ? new matchers.TextMatcher(text).not : new matchers.TextMatcher(text)); } toNotHaveText(text) { @@ -43,7 +43,7 @@ class NativeWaitForElement extends NativeWaitFor { } toHaveLabel(value) { - return new WaitForInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.LabelMatcher(value).not : new matchers.LabelMatcher(value)); + return new WaitForInteraction(this._device, this._element, this._notCondition ? new matchers.LabelMatcher(value).not : new matchers.LabelMatcher(value)); } toNotHaveLabel(value) { @@ -51,7 +51,7 @@ class NativeWaitForElement extends NativeWaitFor { } toHaveId(value) { - return new WaitForInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.IdMatcher(value).not : new matchers.IdMatcher(value)); + return new WaitForInteraction(this._device, this._element, this._notCondition ? new matchers.IdMatcher(value).not : new matchers.IdMatcher(value)); } toNotHaveId(value) { @@ -59,7 +59,7 @@ class NativeWaitForElement extends NativeWaitFor { } toHaveValue(value) { - return new WaitForInteraction(this._invocationManager, this._element, this._notCondition ? new matchers.ValueMatcher(value).not : new matchers.ValueMatcher(value)); + return new WaitForInteraction(this._device, this._element, this._notCondition ? new matchers.ValueMatcher(value).not : new matchers.ValueMatcher(value)); } toNotHaveValue(value) { diff --git a/detox/src/android/core/WebElement.js b/detox/src/android/core/WebElement.js index a7eae50cde..619a87b61b 100644 --- a/detox/src/android/core/WebElement.js +++ b/detox/src/android/core/WebElement.js @@ -10,14 +10,12 @@ const { WebMatcher } = require('./WebMatcher'); const _device = Symbol('device'); const _emitter = Symbol('emitter'); const _matcher = Symbol('matcher'); -const _invocationManager = Symbol('invocationManager'); const _webMatcher = Symbol('webMatcher'); const _webViewElement = Symbol('webViewElement'); class WebElement { - constructor({ device, invocationManager, webMatcher, webViewElement }) { + constructor({ device, webMatcher, webViewElement }) { this[_device] = device; - this[_invocationManager] = invocationManager; this[_webMatcher] = webMatcher; this[_webViewElement] = webViewElement; this.atIndex(0); @@ -33,68 +31,67 @@ class WebElement { // At the moment not working on content-editable async tap() { - return await new ActionInteraction(this[_invocationManager], new actions.WebTapAction(this)).execute(); + return await new ActionInteraction(this[_device], new actions.WebTapAction(this)).execute(); } async typeText(text, isContentEditable = false) { if (isContentEditable) { return await this[_device]._typeText(text); } - return await new ActionInteraction(this[_invocationManager], new actions.WebTypeTextAction(this, text)).execute(); + return await new ActionInteraction(this[_device], new actions.WebTypeTextAction(this, text)).execute(); } // At the moment not working on content-editable async replaceText(text) { - return await new ActionInteraction(this[_invocationManager], new actions.WebReplaceTextAction(this, text)).execute(); + return await new ActionInteraction(this[_device], new actions.WebReplaceTextAction(this, text)).execute(); } // At the moment not working on content-editable async clearText() { - return await new ActionInteraction(this[_invocationManager], new actions.WebClearTextAction(this)).execute(); + return await new ActionInteraction(this[_device], new actions.WebClearTextAction(this)).execute(); } async scrollToView() { - return await new ActionInteraction(this[_invocationManager], new actions.WebScrollToViewAction(this)).execute(); + return await new ActionInteraction(this[_device], new actions.WebScrollToViewAction(this)).execute(); } async getText() { - return await new ActionInteraction(this[_invocationManager], new actions.WebGetTextAction(this)).execute(); + return await new ActionInteraction(this[_device], new actions.WebGetTextAction(this)).execute(); } async focus() { - return await new ActionInteraction(this[_invocationManager], new actions.WebFocusAction(this)).execute(); + return await new ActionInteraction(this[_device], new actions.WebFocusAction(this)).execute(); } async selectAllText() { - return await new ActionInteraction(this[_invocationManager], new actions.WebSelectAllText(this)).execute(); + return await new ActionInteraction(this[_device], new actions.WebSelectAllText(this)).execute(); } async moveCursorToEnd() { - return await new ActionInteraction(this[_invocationManager], new actions.WebMoveCursorEnd(this)).execute(); + return await new ActionInteraction(this[_device], new actions.WebMoveCursorEnd(this)).execute(); } async runScript(script) { - return await new ActionInteraction(this[_invocationManager], new actions.WebRunScriptAction(this, script)).execute(); + return await new ActionInteraction(this[_device], new actions.WebRunScriptAction(this, script)).execute(); } async runScriptWithArgs(script, args) { - return await new ActionInteraction(this[_invocationManager], new actions.WebRunScriptWithArgsAction(this, script, args)).execute(); + return await new ActionInteraction(this[_device], new actions.WebRunScriptWithArgsAction(this, script, args)).execute(); } async getCurrentUrl() { - return await new ActionInteraction(this[_invocationManager], new actions.WebGetCurrentUrlAction(this)).execute(); + return await new ActionInteraction(this[_device], new actions.WebGetCurrentUrlAction(this)).execute(); } async getTitle() { - return await new ActionInteraction(this[_invocationManager], new actions.WebGetTitleAction(this)).execute(); + return await new ActionInteraction(this[_device], new actions.WebGetTitleAction(this)).execute(); } } class WebViewElement { - constructor({ device, emitter, invocationManager, matcher }) { + constructor({ device, emitter, matcher }) { this[_device] = device; this[_emitter] = emitter; - this[_invocationManager] = invocationManager; this[_matcher] = matcher; if (matcher !== undefined) { @@ -110,13 +107,12 @@ class WebViewElement { if (webMatcher instanceof WebMatcher) { return new WebElement({ device: this[_device], - invocationManager: this[_invocationManager], webViewElement: this, webMatcher, }); } - throw new DetoxRuntimeError(`element() argument is invalid, expected a web matcher, but got ${typeof element}`); + throw new DetoxRuntimeError({ message: `element() argument is invalid, expected a web matcher, but got ${typeof element}` }); } } diff --git a/detox/src/android/core/WebExpect.js b/detox/src/android/core/WebExpect.js index 570c76f3c1..df3c39d86a 100644 --- a/detox/src/android/core/WebExpect.js +++ b/detox/src/android/core/WebExpect.js @@ -4,8 +4,8 @@ const EspressoWebDetoxApi = require('../espressoapi/web/EspressoWebDetox'); const { WebAssertionInteraction } = require('../interactions/web'); class WebExpect { - constructor(invocationManager) { - this._invocationManager = invocationManager; + constructor(device) { + this._device = device; this._notCondition = false; } @@ -16,17 +16,17 @@ class WebExpect { } class WebExpectElement extends WebExpect { - constructor(invocationManager, webElement) { - super(invocationManager); + constructor(device, webElement) { + super(device); this._call = invoke.callDirectly(EspressoWebDetoxApi.expect(webElement._call.value)); } async toHaveText(text) { - return await new WebAssertionInteraction(this._invocationManager, new WebHasTextAssertion(this, text)).execute(); + return await new WebAssertionInteraction(this._device, new WebHasTextAssertion(this, text)).execute(); } async toExist() { - return await new WebAssertionInteraction(this._invocationManager, new WebExistsAssertion(this)).execute(); + return await new WebAssertionInteraction(this._device, new WebExistsAssertion(this)).execute(); } } diff --git a/detox/src/android/interactions/native.js b/detox/src/android/interactions/native.js index 69eda00ac6..0fff35377c 100644 --- a/detox/src/android/interactions/native.js +++ b/detox/src/android/interactions/native.js @@ -9,28 +9,31 @@ function call(maybeAFunction) { } class Interaction { - constructor(invocationManager) { + /** + * @param device { RuntimeDevice } + */ + constructor(device) { this._call = undefined; - this._invocationManager = invocationManager; + this._device = device; } async execute() { - const resultObj = await this._invocationManager.execute(this._call); + const resultObj = await this._device.selectedApp.invoke(this._call); return resultObj ? resultObj.result : undefined; } } class ActionInteraction extends Interaction { - constructor(invocationManager, element, action) { - super(invocationManager); + constructor(device, element, action) { + super(device); this._call = EspressoDetoxApi.perform(call(element._call), action._call); // TODO: move this.execute() here from the caller } } class MatcherAssertionInteraction extends Interaction { - constructor(invocationManager, element, matcher) { - super(invocationManager); + constructor(device, element, matcher) { + super(device); this._call = DetoxAssertionApi.assertMatcher(call(element._call), matcher._call.value); // TODO: move this.execute() here from the caller } diff --git a/detox/src/android/interactions/web.js b/detox/src/android/interactions/web.js index 3840c14a87..6c86df1970 100644 --- a/detox/src/android/interactions/web.js +++ b/detox/src/android/interactions/web.js @@ -1,25 +1,28 @@ class WebInteraction { - constructor(invocationManager) { + /** + * @param device { RuntimeDevice } + */ + constructor(device) { this._call = undefined; - this._invocationManager = invocationManager; + this._device = device; } async execute() { - const resultObj = await this._invocationManager.execute(this._call); + const resultObj = await this._device.selectedApp.invoke(this._call); return resultObj ? resultObj.result : undefined; } } class ActionInteraction extends WebInteraction { - constructor(invocationManager, action) { - super(invocationManager); + constructor(device, action) { + super(device); this._call = action._call; } } class WebAssertionInteraction extends WebInteraction { - constructor(invocationManager, assertion) { - super(invocationManager); + constructor(device, assertion) { + super(device); this._call = assertion._call; } } diff --git a/detox/src/devices/runtime/TestApp.js b/detox/src/devices/runtime/TestApp.js index 27a8b53187..060496c60d 100644 --- a/detox/src/devices/runtime/TestApp.js +++ b/detox/src/devices/runtime/TestApp.js @@ -2,6 +2,11 @@ const DetoxRuntimeError = require('../../errors/DetoxRuntimeError'); const { traceCall } = require('../../utils/trace'); const LaunchArgsEditor = require('./utils/LaunchArgsEditor'); +const { ActionInteraction } = require('../interactions/native'); +const actions = require('../actions/native'); +const tempfile = require('tempfile'); +const fs = require('fs-extra'); +const path = require('path'); class TestApp { /** @@ -111,6 +116,13 @@ class RunnableTestApp extends TestApp { await traceCall('appTerminate', this._driver.terminate()); } + // TODO (multiapps) Effectively, this only provides an abstraction over the means by which invocation is implemented. + // If we are to push further in order to get a real inter-layer separation and abstract away the whole means by + // which that various expectations are performed, we must in fact extend the entity model slightly further and create + // a TestApp equivalent for matching, with an equivalent driver. Something like: + // ExpectApp -> A class that would hold a copy of invocationManager, with methods such as tap() and expectVisible() + // ExpectAppDriver -> A delegate that would generate the proper invocation for tap(), expectVisible(), etc., depending on + // the platform (iOS / Android). async invoke(action) { return this._driver.invoke(action); } From 2972df724cb428d6175e331113583e0d2b6afbf8 Mon Sep 17 00:00:00 2001 From: d4vidi Date: Wed, 15 Jun 2022 12:22:02 +0300 Subject: [PATCH 03/24] [wip] Adjust artifacts manager for Android --- detox/src/artifacts/factories/index.js | 4 +-- .../InstrumentsArtifactRecording.js | 18 +++-------- .../android/AndroidInstrumentsPlugin.js | 6 ++-- .../android/AndroidInstrumentsRecording.js | 6 ++-- detox/src/artifacts/providers/index.js | 13 ++++---- detox/src/devices/runtime/RuntimeDevice.js | 18 +++++++++++ detox/src/devices/runtime/TestApp.js | 30 ++++++++++++++----- .../devices/runtime/drivers/BaseDrivers.js | 29 ++++++++++++++---- 8 files changed, 84 insertions(+), 40 deletions(-) diff --git a/detox/src/artifacts/factories/index.js b/detox/src/artifacts/factories/index.js index 782a0c50b9..93d3277b33 100644 --- a/detox/src/artifacts/factories/index.js +++ b/detox/src/artifacts/factories/index.js @@ -15,10 +15,10 @@ class ArtifactsManagerFactory { this._provider = provider; } - createArtifactsManager(artifactsConfig, { eventEmitter, client }) { + createArtifactsManager(artifactsConfig, { eventEmitter, device }) { const artifactsManager = new ArtifactsManager(artifactsConfig); artifactsManager.subscribeToDeviceEvents(eventEmitter); - artifactsManager.registerArtifactPlugins(this._provider.declareArtifactPlugins({ client })); + artifactsManager.registerArtifactPlugins(this._provider.declareArtifactPlugins({ device })); return artifactsManager; } } diff --git a/detox/src/artifacts/instruments/InstrumentsArtifactRecording.js b/detox/src/artifacts/instruments/InstrumentsArtifactRecording.js index 95b5ed6819..1349df70b0 100644 --- a/detox/src/artifacts/instruments/InstrumentsArtifactRecording.js +++ b/detox/src/artifacts/instruments/InstrumentsArtifactRecording.js @@ -1,10 +1,10 @@ const Artifact = require('../templates/artifact/Artifact'); class InstrumentsArtifactRecording extends Artifact { - constructor({ client, userConfig, temporaryRecordingPath }) { + constructor({ device, userConfig, temporaryRecordingPath }) { super(); - this._client = client; + this._device = device; this._userConfig = userConfig; this.temporaryRecordingPath = temporaryRecordingPath; } @@ -14,11 +14,7 @@ class InstrumentsArtifactRecording extends Artifact { return; // nominal start, to preserve state change } - if (!this._isClientConnected()) { - return; - } - - await this._client.startInstrumentsRecording({ + await this._device.startInstrumentsRecording({ recordingPath: this.temporaryRecordingPath, samplingInterval: this.prepareSamplingInterval(this._userConfig.samplingInterval) }); @@ -29,13 +25,7 @@ class InstrumentsArtifactRecording extends Artifact { } async doStop() { - if (this._isClientConnected()) { - await this._client.stopInstrumentsRecording(); - } - } - - _isClientConnected() { - return this._client.isConnected; + await this._device.stopInstrumentsRecording(); } } diff --git a/detox/src/artifacts/instruments/android/AndroidInstrumentsPlugin.js b/detox/src/artifacts/instruments/android/AndroidInstrumentsPlugin.js index 1e3b4655d0..ce3bc85e54 100644 --- a/detox/src/artifacts/instruments/android/AndroidInstrumentsPlugin.js +++ b/detox/src/artifacts/instruments/android/AndroidInstrumentsPlugin.js @@ -4,11 +4,11 @@ const InstrumentsArtifactPlugin = require('../InstrumentsArtifactPlugin'); const AndroidInstrumentsRecording = require('./AndroidInstrumentsRecording'); class AndroidInstrumentsPlugin extends InstrumentsArtifactPlugin { - constructor({ api, adb, client, devicePathBuilder }) { + constructor({ api, adb, device, devicePathBuilder }) { super({ api }); this.adb = adb; - this.client = client; + this.device = device; this.devicePathBuilder = devicePathBuilder; } @@ -31,7 +31,7 @@ class AndroidInstrumentsPlugin extends InstrumentsArtifactPlugin { return new AndroidInstrumentsRecording({ adb: this.adb, pluginContext: this.context, - client: this.client, + device: this.device, deviceId: this.context.deviceId, userConfig: this.api.userConfig, temporaryRecordingPath: this.devicePathBuilder.buildTemporaryArtifactPath('.dtxplain'), diff --git a/detox/src/artifacts/instruments/android/AndroidInstrumentsRecording.js b/detox/src/artifacts/instruments/android/AndroidInstrumentsRecording.js index e0e629679c..f416a99571 100644 --- a/detox/src/artifacts/instruments/android/AndroidInstrumentsRecording.js +++ b/detox/src/artifacts/instruments/android/AndroidInstrumentsRecording.js @@ -2,19 +2,21 @@ const InstrumentsArtifactRecording = require('../InstrumentsArtifactRecording'); class AndroidInstrumentsRecording extends InstrumentsArtifactRecording { - constructor({ adb, pluginContext, client, deviceId, userConfig, temporaryRecordingPath }) { - super({ pluginContext, client, userConfig, temporaryRecordingPath }); + constructor({ adb, pluginContext, device, deviceId, userConfig, temporaryRecordingPath }) { + super({ pluginContext, device, userConfig, temporaryRecordingPath }); this.adb = adb; this.deviceId = deviceId; } async doSave(artifactPath) { await super.doSave(artifactPath); + // TODO Delegate this to a device action! Side-note: This would also make deviceId unnecessary here, as should be await this.adb.pull(this.deviceId, this.temporaryRecordingPath, artifactPath); await this.adb.rm(this.deviceId, this.temporaryRecordingPath, true); } async doDiscard() { + // TODO Delegate this to a device action! await this.adb.rm(this.deviceId, this.temporaryRecordingPath, true); } } diff --git a/detox/src/artifacts/providers/index.js b/detox/src/artifacts/providers/index.js index e95ad7a15c..b19dd68e6b 100644 --- a/detox/src/artifacts/providers/index.js +++ b/detox/src/artifacts/providers/index.js @@ -1,9 +1,10 @@ class ArtifactPluginsProvider { - declareArtifactPlugins({ client }) {} // eslint-disable-line no-unused-vars + declareArtifactPlugins({ device }) {} // eslint-disable-line no-unused-vars } class AndroidArtifactPluginsProvider extends ArtifactPluginsProvider { - declareArtifactPlugins({ client }) { + /** @override */ + declareArtifactPlugins({ device }) { const serviceLocator = require('../../servicelocator/android'); const adb = serviceLocator.adb; const devicePathBuilder = serviceLocator.devicePathBuilder; @@ -15,7 +16,7 @@ class AndroidArtifactPluginsProvider extends ArtifactPluginsProvider { const TimelineArtifactPlugin = require('../timeline/TimelineArtifactPlugin'); return { - instruments: (api) => new AndroidInstrumentsPlugin({ api, adb, client, devicePathBuilder }), + instruments: (api) => new AndroidInstrumentsPlugin({ api, adb, device, devicePathBuilder }), log: (api) => new ADBLogcatPlugin({ api, adb, devicePathBuilder }), screenshot: (api) => new ADBScreencapPlugin({ api, adb, devicePathBuilder }), video: (api) => new ADBScreenrecorderPlugin({ api, adb, devicePathBuilder }), @@ -25,7 +26,8 @@ class AndroidArtifactPluginsProvider extends ArtifactPluginsProvider { } class IosArtifactPluginsProvider extends ArtifactPluginsProvider { - declareArtifactPlugins({ client }) { + /** @override */ + declareArtifactPlugins({ device }) { const TimelineArtifactPlugin = require('../timeline/TimelineArtifactPlugin'); const IosUIHierarchyPlugin = require('../uiHierarchy/IosUIHierarchyPlugin'); @@ -37,7 +39,8 @@ class IosArtifactPluginsProvider extends ArtifactPluginsProvider { } class IosSimulatorArtifactPluginsProvider extends IosArtifactPluginsProvider { - declareArtifactPlugins({ client }) { + /** @override */ + declareArtifactPlugins({ device }) { const serviceLocator = require('../../servicelocator/ios'); const appleSimUtils = serviceLocator.appleSimUtils; diff --git a/detox/src/devices/runtime/RuntimeDevice.js b/detox/src/devices/runtime/RuntimeDevice.js index a5684f7701..5ee08beb61 100644 --- a/detox/src/devices/runtime/RuntimeDevice.js +++ b/detox/src/devices/runtime/RuntimeDevice.js @@ -91,6 +91,20 @@ class RuntimeDevice { } } + setInvokeFailuresListener(handler) { + this._allRunnableApps().forEach((app) => app.setInvokeFailuresListener(handler)); + } + + async startInstrumentsRecording({ recordingPath, samplingInterval }) { + const promises = this._allRunnableApps().map((app) => app.startInstrumentsRecording({ recordingPath, samplingInterval })); + return Promise.all(promises); + } + + async stopInstrumentsRecording() { + const promises = this._allRunnableApps().map((app) => app.stopInstrumentsRecording()); + return Promise.all(promises); + } + async takeScreenshot(name) { if (!name) { throw new DetoxRuntimeError({ message: 'Cannot take a screenshot with an empty name.' }); @@ -151,6 +165,10 @@ class RuntimeDevice { _allApps() { return [...Object.values(this._predefinedApps), this._unspecifiedApp, ...this._utilApps]; } + + _allRunnableApps() { + return [...Object.values(this._predefinedApps), this._unspecifiedApp]; + } } module.exports = RuntimeDevice; diff --git a/detox/src/devices/runtime/TestApp.js b/detox/src/devices/runtime/TestApp.js index 060496c60d..be013eefbe 100644 --- a/detox/src/devices/runtime/TestApp.js +++ b/detox/src/devices/runtime/TestApp.js @@ -2,11 +2,6 @@ const DetoxRuntimeError = require('../../errors/DetoxRuntimeError'); const { traceCall } = require('../../utils/trace'); const LaunchArgsEditor = require('./utils/LaunchArgsEditor'); -const { ActionInteraction } = require('../interactions/native'); -const actions = require('../actions/native'); -const tempfile = require('tempfile'); -const fs = require('fs-extra'); -const path = require('path'); class TestApp { /** @@ -120,13 +115,32 @@ class RunnableTestApp extends TestApp { // If we are to push further in order to get a real inter-layer separation and abstract away the whole means by // which that various expectations are performed, we must in fact extend the entity model slightly further and create // a TestApp equivalent for matching, with an equivalent driver. Something like: - // ExpectApp -> A class that would hold a copy of invocationManager, with methods such as tap() and expectVisible() - // ExpectAppDriver -> A delegate that would generate the proper invocation for tap(), expectVisible(), etc., depending on - // the platform (iOS / Android). + // TestAppExpect -> A class that would hold a copy of invocationManager, with methods such as tap() and expectVisible() + // TestAppExpectDriver -> A delegate that would generate the proper invocation for tap(), expectVisible(), etc., depending on + // the platform (iOS / Android). async invoke(action) { return this._driver.invoke(action); } + // TODO (multiapps) Similar to the notes about invoke(), these artifacts-related methods should probably reside + // under a TestApp equivalent which is strictly associated with artifacts. It should be accompanied by a driver. For example: + // TestAppArtifacts -> The equivalent class + // TestAppArtifactsDriver -> The driver delegate + // In this case, most likely, an additional change is required: recordingPath and samplingInterval should stem + // from the driver, rather than from the top-most layer (i.e. our caller). + + setInvokeFailuresListener(listener) { + this._driver.setInvokeFailuresListener(listener); + } + + async startInstrumentsRecording({ recordingPath, samplingInterval }) { + return this._driver.startInstrumentsRecording({ recordingPath, samplingInterval }); + } + + async stopInstrumentsRecording() { + return this._driver.stopInstrumentsRecording(); + } + async _launch(launchParams) { const payloadParams = ['url', 'userNotification', 'userActivity']; const hasPayload = this._assertHasSingleParam(payloadParams, launchParams); diff --git a/detox/src/devices/runtime/drivers/BaseDrivers.js b/detox/src/devices/runtime/drivers/BaseDrivers.js index 569c931cca..b46d1da455 100644 --- a/detox/src/devices/runtime/drivers/BaseDrivers.js +++ b/detox/src/devices/runtime/drivers/BaseDrivers.js @@ -88,6 +88,13 @@ class TestAppDriver { return null; } + /** + * @returns {boolean} Whether the app is currently running + */ + isRunning() { + return !!this._pid; + } + /** * @param appInfo { AppInfo } */ @@ -120,11 +127,22 @@ class TestAppDriver { async invoke(_action) {} - /** - * @returns {boolean} Whether the app is currently running - */ - isRunning() { - return !!this._pid; + setInvokeFailuresListener(listener) { + this.client.setEventCallback('testFailed', listener); + } + + async startInstrumentsRecording({ recordingPath, samplingInterval }) { + const { client } = this; + if (client.isConnected) { + return client.startInstrumentsRecording(recordingPath, samplingInterval); + } + } + + async stopInstrumentsRecording() { + const { client } = this; + if (client.isConnected) { + return client.stopInstrumentsRecording(); + } } async install() {} @@ -142,7 +160,6 @@ class TestAppDriver { async enableSynchronization() {} async disableSynchronization() {} async captureViewHierarchy() {} - async cleanup() {} /** @protected */ From 594f5cf610b4c32459171af8a581f9c4c7cd6575 Mon Sep 17 00:00:00 2001 From: d4vidi Date: Wed, 15 Jun 2022 14:51:06 +0300 Subject: [PATCH 04/24] [wip] Align Detox init and cleanup; raw sanity e2e works --- detox/src/Detox.js | 70 +++++++------------ detox/src/client/Client.js | 4 ++ .../common/drivers/android/exec/ADB.js | 18 ++--- .../drivers/android/tools/Instrumentation.js | 4 +- .../android/tools/MonitoredInstrumentation.js | 4 +- detox/src/devices/runtime/RuntimeDevice.js | 25 +++++-- detox/src/devices/runtime/TestApp.js | 15 ++++ .../devices/runtime/drivers/BaseDrivers.js | 20 ++++-- .../runtime/drivers/android/AndroidDrivers.js | 9 +++ detox/src/devices/runtime/factories/base.js | 2 +- detox/src/matchers/factories/index.js | 4 +- 11 files changed, 106 insertions(+), 69 deletions(-) diff --git a/detox/src/Detox.js b/detox/src/Detox.js index 7d0c0624f4..864c5885aa 100644 --- a/detox/src/Detox.js +++ b/detox/src/Detox.js @@ -6,17 +6,16 @@ const _ = require('lodash'); const lifecycleSymbols = require('../runners/integration').lifecycle; -const Client = require('./client/Client'); +const DeviceAPI = require('./DeviceAPI'); const environmentFactory = require('./environmentFactory'); const { DetoxRuntimeErrorComposer } = require('./errors'); -const { InvocationManager } = require('./invoke'); const DetoxServer = require('./server/DetoxServer'); const AsyncEmitter = require('./utils/AsyncEmitter'); const Deferred = require('./utils/Deferred'); const MissingDetox = require('./utils/MissingDetox'); const logger = require('./utils/logger'); -const log = logger.child({ __filename }); +const log = logger.child({ __filename }); const _initHandle = Symbol('_initHandle'); const _assertNoPendingInit = Symbol('_assertNoPendingInit'); @@ -96,18 +95,16 @@ class Detox { this._artifactsManager = null; } - if (this._client) { - this._client.dumpPendingRequests(); - await this._client.cleanup(); - this._client = null; - } - - if (this.device) { + if (this.runtimeDevice) { const shutdown = this._behaviorConfig.cleanup.shutdownDevice; - await this.device._cleanup(); + await this.runtimeDevice.cleanup(); await this._deviceAllocator.free(this._deviceCookie, { shutdown }); } + if (this._eventEmitter) { + this._eventEmitter.off(); + } + if (this._server) { await this._server.close(); this._server = null; @@ -115,6 +112,7 @@ class Detox { this._deviceAllocator = null; this._deviceCookie = null; + this._runtimeDevice = null; this.device = null; } @@ -161,17 +159,6 @@ class Detox { } } - this._client = new Client(sessionConfig); - this._client.terminateApp = async () => { - if (this.device && this.device._isAppRunning()) { - await this.device.terminateApp(); - } - }; - - await this._client.connect(); - - const invocationManager = new InvocationManager(this._client); - const { envValidatorFactory, deviceAllocatorFactory, @@ -184,18 +171,21 @@ class Detox { await envValidator.validate(); const commonDeps = { - invocationManager, - client: this._client, eventEmitter: this._eventEmitter, - runtimeErrorComposer: this._runtimeErrorComposer, + errorComposer: this._runtimeErrorComposer, }; - this._artifactsManager = artifactsManagerFactory.createArtifactsManager(this._artifactsConfig, commonDeps); + const runtimeDeviceArtifacts = { + setInvokeFailuresListener: () => runtimeDevice.setInvokeFailuresListener(...arguments), + startInstrumentsRecording: () => runtimeDevice.startInstrumentsRecording(...arguments), + stopInstrumentsRecording: () => runtimeDevice.stopInstrumentsRecording(...arguments), + }; + this._artifactsManager = artifactsManagerFactory.createArtifactsManager(this._artifactsConfig, { ...commonDeps, device: runtimeDeviceArtifacts }); this._deviceAllocator = deviceAllocatorFactory.createDeviceAllocator(commonDeps); this._deviceCookie = await this._deviceAllocator.allocate(this._deviceConfig); - this.device = runtimeDeviceFactory.createRuntimeDevice( + const runtimeDevice = runtimeDeviceFactory.createRuntimeDevice( this._deviceCookie, commonDeps, { @@ -204,11 +194,13 @@ class Detox { deviceConfig: this._deviceConfig, sessionConfig, }); - await this.device._prepare(); + await runtimeDevice.init(); + + this.runtimeDevice = runtimeDevice; + this.device = new DeviceAPI(runtimeDevice, this._runtimeErrorComposer); const matchers = matchersFactory.createMatchers({ - invocationManager, - runtimeDevice: this.device, + runtimeDevice, eventEmitter: this._eventEmitter, }); Object.assign(this, matchers); @@ -220,9 +212,9 @@ class Detox { }); } - await this.device.installUtilBinaries(); + await runtimeDevice.installUtilBinaries(); if (behaviorConfig.reinstallApp) { - await this._reinstallAppsOnDevice(); + await this._reinstallAppsOnDevice(runtimeDevice); } return this; @@ -241,22 +233,14 @@ class Detox { return handle.promise; } - async _reinstallAppsOnDevice() { - const appNames = _(this._appsConfig) + async _reinstallAppsOnDevice(runtimeDevice) { + const appAliases = _(this._appsConfig) .map((config, key) => [key, `${config.binaryPath}:${config.testBinaryPath}`]) .uniqBy(1) .map(0) .value(); - for (const appName of appNames) { - await this.device.selectApp(appName); - await this.device.uninstallApp(); - await this.device.installApp(); - } - - if (appNames.length !== 1) { - await this.device.selectApp(null); - } + await runtimeDevice.reinstallApps(appAliases); } _logTestRunCheckpoint(event, { status, fullName }) { diff --git a/detox/src/client/Client.js b/detox/src/client/Client.js index 9b513d72b9..649603ace9 100644 --- a/detox/src/client/Client.js +++ b/detox/src/client/Client.js @@ -64,6 +64,10 @@ class Client { return this._serverUrl; } + get sessionId() { + return this._sessionId; + } + async open() { return this._asyncWebSocket.open(); } diff --git a/detox/src/devices/common/drivers/android/exec/ADB.js b/detox/src/devices/common/drivers/android/exec/ADB.js index 6f7b6925c9..0d73bfb9d1 100644 --- a/detox/src/devices/common/drivers/android/exec/ADB.js +++ b/detox/src/devices/common/drivers/android/exec/ADB.js @@ -157,9 +157,9 @@ class ADB { await this.emu(deviceId, `geo fix ${comma}`); } - async pidof(deviceId, bundleId) { - const bundleIdRegex = escape.inQuotedRegexp(bundleId) + '$'; - const command = `ps | grep "${bundleIdRegex}"`; + async pidof(deviceId, packageId) { + const packageIdRegex = escape.inQuotedRegexp(packageId) + '$'; + const command = `ps | grep "${packageIdRegex}"`; const options = { silent: true }; const processes = await this.shell(deviceId, command, options).catch(() => ''); @@ -293,19 +293,19 @@ class ADB { return this.shell(deviceId, 'pm list instrumentation'); } - async getInstrumentationRunner(deviceId, bundleId) { + async getInstrumentationRunner(deviceId, packageId) { const instrumentationRunners = await this.listInstrumentation(deviceId); - const instrumentationRunner = this._instrumentationRunnerForBundleId(instrumentationRunners, bundleId); + const instrumentationRunner = this._instrumentationRunnerForPacakgeId(instrumentationRunners, packageId); if (instrumentationRunner === 'undefined') { - throw new DetoxRuntimeError(`No instrumentation runner found on device ${deviceId} for package ${bundleId}`); + throw new DetoxRuntimeError(`No instrumentation runner found on device ${deviceId} for package ${packageId}`); } return instrumentationRunner; } - _instrumentationRunnerForBundleId(instrumentationRunners, bundleId) { - const runnerForBundleRegEx = new RegExp(`^instrumentation:(.*) \\(target=${bundleId.replace(new RegExp('\\.', 'g'), '\\.')}\\)$`, 'gm'); - return _.get(runnerForBundleRegEx.exec(instrumentationRunners), [1], 'undefined'); + _instrumentationRunnerForPacakgeId(instrumentationRunners, packageId) { + const runnerForPackageRegEx = new RegExp(`^instrumentation:(.*) \\(target=${packageId.replace(new RegExp('\\.', 'g'), '\\.')}\\)$`, 'gm'); + return _.get(runnerForPackageRegEx.exec(instrumentationRunners), [1], 'undefined'); } async shell(deviceId, command, options) { diff --git a/detox/src/devices/common/drivers/android/tools/Instrumentation.js b/detox/src/devices/common/drivers/android/tools/Instrumentation.js index b0885d9e61..19eb12bdff 100644 --- a/detox/src/devices/common/drivers/android/tools/Instrumentation.js +++ b/detox/src/devices/common/drivers/android/tools/Instrumentation.js @@ -15,10 +15,10 @@ class Instrumentation { this._onLogData = this._onLogData.bind(this); } - async launch(deviceId, bundleId, userLaunchArgs) { + async launch(deviceId, packageId, userLaunchArgs) { const spawnArgs = this._getSpawnArgs(userLaunchArgs); - const testRunner = await this.adb.getInstrumentationRunner(deviceId, bundleId); + const testRunner = await this.adb.getInstrumentationRunner(deviceId, packageId); this.instrumentationProcess = this.adb.spawnInstrumentation(deviceId, spawnArgs, testRunner); this.instrumentationProcess.childProcess.stdout.setEncoding('utf8'); diff --git a/detox/src/devices/common/drivers/android/tools/MonitoredInstrumentation.js b/detox/src/devices/common/drivers/android/tools/MonitoredInstrumentation.js index 4dca064f97..4727d67626 100644 --- a/detox/src/devices/common/drivers/android/tools/MonitoredInstrumentation.js +++ b/detox/src/devices/common/drivers/android/tools/MonitoredInstrumentation.js @@ -17,9 +17,9 @@ class MonitoredInstrumentation { this.pendingPromise = Deferred.resolved(); } - async launch(deviceId, bundleId, userLaunchArgs) { + async launch(deviceId, packageId, userLaunchArgs) { this.instrumentationLogsParser = new InstrumentationLogsParser(); - await this.instrumentation.launch(deviceId, bundleId, userLaunchArgs); + await this.instrumentation.launch(deviceId, packageId, userLaunchArgs); } setTerminationFn(userTerminationFn) { diff --git a/detox/src/devices/runtime/RuntimeDevice.js b/detox/src/devices/runtime/RuntimeDevice.js index 5ee08beb61..198f5ea5f4 100644 --- a/detox/src/devices/runtime/RuntimeDevice.js +++ b/detox/src/devices/runtime/RuntimeDevice.js @@ -22,13 +22,20 @@ class RuntimeDevice { } async init() { + await this._initApps(); + const appAliases = Object.keys(this._predefinedApps); if (appAliases.length === 1) { const appAlias = appAliases[0]; - this._selectedApp = this._predefinedApps[appAlias]; + await this.selectPredefinedApp(appAlias); } + } - await this._initApps(); + async cleanup() { + await traceCall('deviceCleanup', async () => { + await this._driver.cleanup(); + await forEachSeries(this._allRunnableApps(), (app) => app.cleanup(), this); + }); } /** @@ -39,15 +46,15 @@ class RuntimeDevice { } get id() { - return this._driver.getExternalId(); + return this._driver.externalId; } get name() { - return this._driver.getDeviceName(); + return this._driver.deviceName; } get platform() { - return this._driver.platform(); + return this._driver.platform; } get type() { @@ -84,10 +91,18 @@ class RuntimeDevice { } async reinstallApps(appAliases) { + const selectedApp = this._selectedApp; + for (const appAlias of appAliases) { const app = this._predefinedApps[appAlias]; + await app.select(); await app.uninstall(); await app.install(); + await app.deselect(); + } + + if (selectedApp) { + await selectedApp.select(); } } diff --git a/detox/src/devices/runtime/TestApp.js b/detox/src/devices/runtime/TestApp.js index be013eefbe..98898b1aa3 100644 --- a/detox/src/devices/runtime/TestApp.js +++ b/detox/src/devices/runtime/TestApp.js @@ -32,6 +32,17 @@ class RunnableTestApp extends TestApp { this._launchArgs = new LaunchArgsEditor(); } + async init() { + const onDisconnectListener = async () => { + if (this._driver.isRunning()) { + await this.terminate(); + } + }; + this._driver.setOnDisconnectListener(onDisconnectListener()); + + await this._driver.init(); + } + get alias() { return null; } @@ -111,6 +122,10 @@ class RunnableTestApp extends TestApp { await traceCall('appTerminate', this._driver.terminate()); } + async cleanup() { + await traceCall('appCleanup', () => this._driver.cleanup()); + } + // TODO (multiapps) Effectively, this only provides an abstraction over the means by which invocation is implemented. // If we are to push further in order to get a real inter-layer separation and abstract away the whole means by // which that various expectations are performed, we must in fact extend the entity model slightly further and create diff --git a/detox/src/devices/runtime/drivers/BaseDrivers.js b/detox/src/devices/runtime/drivers/BaseDrivers.js index b46d1da455..5facd4e8df 100644 --- a/detox/src/devices/runtime/drivers/BaseDrivers.js +++ b/detox/src/devices/runtime/drivers/BaseDrivers.js @@ -47,10 +47,7 @@ class DeviceDriver { async unreverseTcpPort() {} async clearKeychain() {} async typeText(_text) {} - - async cleanup() { - this.emitter.off(); // TODO not the right place for this - } + async cleanup() {} } /** @@ -84,6 +81,10 @@ class TestAppDriver { this._appInfo = null; } + async init() { + await this.client.connect(); + } + get uiDevice() { return null; } @@ -95,6 +96,10 @@ class TestAppDriver { return !!this._pid; } + setOnDisconnectListener(listener) { + this.client.terminateApp = listener; + } + /** * @param appInfo { AppInfo } */ @@ -157,10 +162,15 @@ class TestAppDriver { async matchFinger() {} async unmatchFinger() {} async shake() {} + async setURLBlacklist(_urlList) {} async enableSynchronization() {} async disableSynchronization() {} async captureViewHierarchy() {} - async cleanup() {} + async cleanup() { + this.client.dumpPendingRequests(); + await this.client.cleanup(); + this.client = null; + } /** @protected */ async _waitUntilReady() { diff --git a/detox/src/devices/runtime/drivers/android/AndroidDrivers.js b/detox/src/devices/runtime/drivers/android/AndroidDrivers.js index 1fbf620c57..a45fe068f5 100644 --- a/detox/src/devices/runtime/drivers/android/AndroidDrivers.js +++ b/detox/src/devices/runtime/drivers/android/AndroidDrivers.js @@ -257,6 +257,7 @@ class AndroidAppDriver extends TestAppDriver { await this.emitter.emit('beforeLaunchApp', { deviceId: adbName, bundleId: _packageId, launchArgs }); + launchArgs = await this._applyAppSessionArgs(launchArgs); launchArgs = await this._modifyArgsForNotificationHandling(launchArgs); if (manually) { @@ -318,6 +319,14 @@ class AndroidAppDriver extends TestAppDriver { return this.invocationManager.execute(DetoxApi.launchMainActivity()); } + _applyAppSessionArgs(launchArgs) { + return { + detoxServer: this.client.serverUrl, + detoxSessionId: this.client.sessionId, + ...launchArgs, + }; + } + async _modifyArgsForNotificationHandling(launchArgs) { if (launchArgs.detoxUserNotificationDataURL) { const notificationLocalFile = this._createPayloadFile(launchArgs.detoxUserNotificationDataURL); diff --git a/detox/src/devices/runtime/factories/base.js b/detox/src/devices/runtime/factories/base.js index fcd3b1bfe6..afd4af52f4 100644 --- a/detox/src/devices/runtime/factories/base.js +++ b/detox/src/devices/runtime/factories/base.js @@ -38,7 +38,7 @@ class RuntimeDeviceFactory { _createUtilAppsList(deviceCookie, commonDeps, configs) { const { deviceConfig } = configs; - return deviceConfig.utilBinaryPaths.map((binaryPath) => { + return (deviceConfig.utilBinaryPaths || []).map((binaryPath) => { const driver = this._createTestAppDriver(deviceCookie, commonDeps, configs, null); const appConfig = { binaryPath }; return new UtilApp(driver, { appConfig }); diff --git a/detox/src/matchers/factories/index.js b/detox/src/matchers/factories/index.js index a756c9f39e..7703758722 100644 --- a/detox/src/matchers/factories/index.js +++ b/detox/src/matchers/factories/index.js @@ -5,9 +5,9 @@ class MatchersFactory { } class Android extends MatchersFactory { - createMatchers({ invocationManager, runtimeDevice, eventEmitter }) { + createMatchers({ runtimeDevice, eventEmitter }) { const AndroidExpect = require('../../android/AndroidExpect'); - return new AndroidExpect({ invocationManager, device: runtimeDevice, emitter: eventEmitter }); + return new AndroidExpect({ device: runtimeDevice, emitter: eventEmitter }); } } From a13101a732c8c0a30395088065322dc6c38edb7d Mon Sep 17 00:00:00 2001 From: d4vidi Date: Wed, 15 Jun 2022 15:37:15 +0300 Subject: [PATCH 05/24] [wip] Add missing app-ready event --- detox/src/devices/runtime/TestApp.js | 1 + detox/src/devices/runtime/drivers/BaseDrivers.js | 8 ++++++++ .../src/devices/runtime/drivers/android/AndroidDrivers.js | 1 + .../devices/runtime/drivers/ios/IosSimulatorDrivers.js | 1 + 4 files changed, 11 insertions(+) diff --git a/detox/src/devices/runtime/TestApp.js b/detox/src/devices/runtime/TestApp.js index 98898b1aa3..f62b6c7657 100644 --- a/detox/src/devices/runtime/TestApp.js +++ b/detox/src/devices/runtime/TestApp.js @@ -193,6 +193,7 @@ class RunnableTestApp extends TestApp { } else { await this._driver.launch(launchInfo); } + } _assertHasSingleParam(singleParams, params) { diff --git a/detox/src/devices/runtime/drivers/BaseDrivers.js b/detox/src/devices/runtime/drivers/BaseDrivers.js index 5facd4e8df..aedf55158e 100644 --- a/detox/src/devices/runtime/drivers/BaseDrivers.js +++ b/detox/src/devices/runtime/drivers/BaseDrivers.js @@ -183,6 +183,14 @@ class TestAppDriver { fs.writeFileSync(payloadFile.path, JSON.stringify(payload, null, 2)); return payloadFile; } + + async _notifyAppReady(deviceId, bundleId) { + await this.emitter.emit('appReady', { + deviceId, + bundleId, + pid: this._pid, + }); + } } diff --git a/detox/src/devices/runtime/drivers/android/AndroidDrivers.js b/detox/src/devices/runtime/drivers/android/AndroidDrivers.js index a45fe068f5..76bafa8152 100644 --- a/detox/src/devices/runtime/drivers/android/AndroidDrivers.js +++ b/detox/src/devices/runtime/drivers/android/AndroidDrivers.js @@ -138,6 +138,7 @@ class AndroidAppDriver extends TestAppDriver { launchArgs: launchInfo.launchArgs, }); await this._waitUntilReady(); + await this._notifyAppReady(this.adbName, this._packageId); } /** @override */ diff --git a/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js b/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js index 313b730c52..0e343ca35f 100644 --- a/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js +++ b/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js @@ -134,6 +134,7 @@ class IosSimulatorAppDriver extends IosAppDriver { await this._waitUntilReady(); await this._waitForActive(); + await this._notifyAppReady(udid, bundleId); return pid; } From a745818820b56f84d117c7eb8186798a04c636c3 Mon Sep 17 00:00:00 2001 From: d4vidi Date: Wed, 15 Jun 2022 22:32:20 +0300 Subject: [PATCH 06/24] [wip] Fix numerous issues from e2e failures --- detox/src/DeviceAPI.js | 8 +++-- detox/src/android/interactions/native.js | 11 +++--- .../emulator/EmulatorVersionResolver.js | 2 +- .../common/drivers/android/exec/ADB.js | 12 +++---- detox/src/devices/runtime/RuntimeDevice.js | 10 +----- detox/src/devices/runtime/TestApp.js | 36 +++++++++++++------ .../devices/runtime/drivers/BaseDrivers.js | 25 ++++++++++--- .../runtime/drivers/android/AndroidDrivers.js | 21 ++++++++--- .../devices/runtime/drivers/ios/IosDrivers.js | 19 ---------- 9 files changed, 82 insertions(+), 62 deletions(-) diff --git a/detox/src/DeviceAPI.js b/detox/src/DeviceAPI.js index 4f310f30fb..97a82d9e16 100644 --- a/detox/src/DeviceAPI.js +++ b/detox/src/DeviceAPI.js @@ -71,6 +71,10 @@ class DeviceAPI { return this.platform; } + get appLaunchArgs() { + return this.device.selectedApp.launchArgs; + } + async selectApp(aliasOrConfig) { if (aliasOrConfig === undefined) { throw this._errorComposer.cantSelectEmptyApp(); @@ -110,11 +114,11 @@ class DeviceAPI { } async sendToHome() { - return this.device.sendToHome(); + return this.device.selectedApp.sendToHome(); } async pressBack() { - return this.device.pressBack(); + return this.device.selectedApp.pressBack(); } async setBiometricEnrollment(toggle) { diff --git a/detox/src/android/interactions/native.js b/detox/src/android/interactions/native.js index 0fff35377c..9b8415b633 100644 --- a/detox/src/android/interactions/native.js +++ b/detox/src/android/interactions/native.js @@ -40,8 +40,8 @@ class MatcherAssertionInteraction extends Interaction { } class WaitForInteraction extends Interaction { - constructor(invocationManager, element, assertionMatcher) { - super(invocationManager); + constructor(device, element, assertionMatcher) { + super(device); this._element = element; this._assertionMatcher = assertionMatcher; this._element._selectElementWithMatcher(this._element._originalMatcher); @@ -56,17 +56,18 @@ class WaitForInteraction extends Interaction { } whileElement(searchMatcher) { - return new WaitForActionInteraction(this._invocationManager, this._element, this._assertionMatcher, searchMatcher); + return new WaitForActionInteraction(this._device, this._element, this._assertionMatcher, searchMatcher); } } class WaitForActionInteractionBase extends Interaction { - constructor(invocationManager, element, matcher, searchMatcher) { - super(invocationManager); + constructor(device, element, matcher, searchMatcher) { + super(device); //if (!(element instanceof NativeElement)) throw new DetoxRuntimeError(`WaitForActionInteraction ctor 1st argument must be a valid NativeElement, got ${typeof element}`); //if (!(matcher instanceof NativeMatcher)) throw new DetoxRuntimeError(`WaitForActionInteraction ctor 2nd argument must be a valid NativeMatcher, got ${typeof matcher}`); if (!(searchMatcher instanceof NativeMatcher)) throw new DetoxRuntimeError(`WaitForActionInteraction ctor 3rd argument must be a valid NativeMatcher, got ${typeof searchMatcher}`); + this._element = element; this._originalMatcher = matcher; this._searchMatcher = searchMatcher; diff --git a/detox/src/devices/allocation/drivers/android/emulator/EmulatorVersionResolver.js b/detox/src/devices/allocation/drivers/android/emulator/EmulatorVersionResolver.js index 39aeb2bb5b..ce15a1f58b 100644 --- a/detox/src/devices/allocation/drivers/android/emulator/EmulatorVersionResolver.js +++ b/detox/src/devices/allocation/drivers/android/emulator/EmulatorVersionResolver.js @@ -32,7 +32,7 @@ class EmulatorVersionResolver { } const version = this._parseVersionString(matches[1]); - log.debug({ event: EMU_BIN_VERSION_DETECT_EV, success: true }, 'Detected emulator binary version', version); + log.debug({ event: EMU_BIN_VERSION_DETECT_EV, success: true }, 'Detected emulator binary version', JSON.stringify(version)); return version; } diff --git a/detox/src/devices/common/drivers/android/exec/ADB.js b/detox/src/devices/common/drivers/android/exec/ADB.js index 0d73bfb9d1..a7ba292512 100644 --- a/detox/src/devices/common/drivers/android/exec/ADB.js +++ b/detox/src/devices/common/drivers/android/exec/ADB.js @@ -127,12 +127,12 @@ class ADB { return this.shellSpawned(deviceId, command, { timeout: INSTALL_TIMEOUT, retries: 3 }); } - async uninstall(deviceId, appId) { - await this.adbCmd(deviceId, `uninstall ${appId}`); + async uninstall(deviceId, packageId) { + await this.adbCmd(deviceId, `uninstall ${packageId}`); } - async terminate(deviceId, appId) { - await this.shell(deviceId, `am force-stop ${appId}`); + async terminate(deviceId, packageId) { + await this.shell(deviceId, `am force-stop ${packageId}`); } async setLocation(deviceId, lat, lon) { @@ -295,7 +295,7 @@ class ADB { async getInstrumentationRunner(deviceId, packageId) { const instrumentationRunners = await this.listInstrumentation(deviceId); - const instrumentationRunner = this._instrumentationRunnerForPacakgeId(instrumentationRunners, packageId); + const instrumentationRunner = this._instrumentationRunnerForPackageId(instrumentationRunners, packageId); if (instrumentationRunner === 'undefined') { throw new DetoxRuntimeError(`No instrumentation runner found on device ${deviceId} for package ${packageId}`); } @@ -303,7 +303,7 @@ class ADB { return instrumentationRunner; } - _instrumentationRunnerForPacakgeId(instrumentationRunners, packageId) { + _instrumentationRunnerForPackageId(instrumentationRunners, packageId) { const runnerForPackageRegEx = new RegExp(`^instrumentation:(.*) \\(target=${packageId.replace(new RegExp('\\.', 'g'), '\\.')}\\)$`, 'gm'); return _.get(runnerForPackageRegEx.exec(instrumentationRunners), [1], 'undefined'); } diff --git a/detox/src/devices/runtime/RuntimeDevice.js b/detox/src/devices/runtime/RuntimeDevice.js index 198f5ea5f4..061a4ed1f8 100644 --- a/detox/src/devices/runtime/RuntimeDevice.js +++ b/detox/src/devices/runtime/RuntimeDevice.js @@ -72,7 +72,7 @@ class RuntimeDevice { } this._selectedApp = app; - this._selectedApp.select(); + await this._selectedApp.select(); } async selectUnspecifiedApp(appConfig) { @@ -131,14 +131,6 @@ class RuntimeDevice { return this._driver.captureViewHierarchy(name); } - async sendToHome() { - await this._driver.sendToHome(); - } - - async pressBack() { - await this._driver.pressBack(); - } - async setBiometricEnrollment(yesOrNo) { await this._driver.setBiometricEnrollment(yesOrNo); } diff --git a/detox/src/devices/runtime/TestApp.js b/detox/src/devices/runtime/TestApp.js index f62b6c7657..7dbca7f22d 100644 --- a/detox/src/devices/runtime/TestApp.js +++ b/detox/src/devices/runtime/TestApp.js @@ -30,15 +30,15 @@ class RunnableTestApp extends TestApp { this.behaviorConfig = behaviorConfig; this._launchArgs = new LaunchArgsEditor(); + this._launchArgs.modify(appConfig.launchArgs); + } + + get launchArgs() { + return this._launchArgs; } async init() { - const onDisconnectListener = async () => { - if (this._driver.isRunning()) { - await this.terminate(); - } - }; - this._driver.setOnDisconnectListener(onDisconnectListener()); + this._driver.setOnDisconnectListener(this._onDisconnect.bind(this)); await this._driver.init(); } @@ -114,12 +114,20 @@ class RunnableTestApp extends TestApp { await this._driver.shake(); } + async sendToHome() { + await this._driver.sendToHome(); + } + + async pressBack() { + await this._driver.pressBack(); + } + async setOrientation(orientation) { await this._driver.setOrientation(orientation); } async terminate() { - await traceCall('appTerminate', this._driver.terminate()); + await traceCall('appTerminate', () => this._driver.terminate()); } async cleanup() { @@ -128,7 +136,7 @@ class RunnableTestApp extends TestApp { // TODO (multiapps) Effectively, this only provides an abstraction over the means by which invocation is implemented. // If we are to push further in order to get a real inter-layer separation and abstract away the whole means by - // which that various expectations are performed, we must in fact extend the entity model slightly further and create + // which the various expectations are performed altogether, we must in fact extend the entity model slightly further and create // a TestApp equivalent for matching, with an equivalent driver. Something like: // TestAppExpect -> A class that would hold a copy of invocationManager, with methods such as tap() and expectVisible() // TestAppExpectDriver -> A delegate that would generate the proper invocation for tap(), expectVisible(), etc., depending on @@ -141,8 +149,8 @@ class RunnableTestApp extends TestApp { // under a TestApp equivalent which is strictly associated with artifacts. It should be accompanied by a driver. For example: // TestAppArtifacts -> The equivalent class // TestAppArtifactsDriver -> The driver delegate - // In this case, most likely, an additional change is required: recordingPath and samplingInterval should stem - // from the driver, rather than from the top-most layer (i.e. our caller). + // In this case, most likely, an additional change is required: recordingPath should stem from the driver, rather than from + // the top-most layer (i.e. our caller). setInvokeFailuresListener(listener) { this._driver.setInvokeFailuresListener(listener); @@ -156,6 +164,12 @@ class RunnableTestApp extends TestApp { return this._driver.stopInstrumentsRecording(); } + async _onDisconnect() { + if (this._driver.isRunning()) { + await this.terminate(); + } + } + async _launch(launchParams) { const payloadParams = ['url', 'userNotification', 'userActivity']; const hasPayload = this._assertHasSingleParam(payloadParams, launchParams); @@ -257,7 +271,7 @@ class PredefinedTestApp extends RunnableTestApp { class UnspecifiedTestApp extends RunnableTestApp { constructor(driver, { behaviorConfig }) { - super(driver, { behaviorConfig, appConfig: null }); + super(driver, { behaviorConfig, appConfig: {} }); } async select(appConfig) { diff --git a/detox/src/devices/runtime/drivers/BaseDrivers.js b/detox/src/devices/runtime/drivers/BaseDrivers.js index aedf55158e..5058c4c655 100644 --- a/detox/src/devices/runtime/drivers/BaseDrivers.js +++ b/detox/src/devices/runtime/drivers/BaseDrivers.js @@ -43,8 +43,8 @@ class DeviceDriver { async setStatusBar(_params) {} async resetStatusBar() {} async setLocation(_lat, _lon) {} - async reverseTcpPort() {} - async unreverseTcpPort() {} + async reverseTcpPort(_port) {} + async unreverseTcpPort(_port) {} async clearKeychain() {} async typeText(_text) {} async cleanup() {} @@ -123,9 +123,17 @@ class TestAppDriver { async reloadReactNative() {} async resetContentAndSettings() {} + async deliverPayload(_params) {} // TODO (multiapps) Revisit whether keeping this method public makes sense at all - async sendUserActivity(_payload) {} - async sendUserNotification(_payload) {} + + async sendUserActivity(payload) { + await this._sendPayload('detoxUserActivityDataURL', payload); + } + + async sendUserNotification(payload) { + await this._sendPayload('detoxUserNotificationDataURL', payload); + } + async terminate() { this._pid = null; } @@ -184,6 +192,15 @@ class TestAppDriver { return payloadFile; } + async _sendPayload(name, payload) { + const payloadFile = this._createPayloadFile(payload); + + await this.deliverPayload({ + [name]: payloadFile.path, + }); + payloadFile.cleanup(); + } + async _notifyAppReady(deviceId, bundleId) { await this.emitter.emit('appReady', { deviceId, diff --git a/detox/src/devices/runtime/drivers/android/AndroidDrivers.js b/detox/src/devices/runtime/drivers/android/AndroidDrivers.js index 76bafa8152..18ec9b92ed 100644 --- a/detox/src/devices/runtime/drivers/android/AndroidDrivers.js +++ b/detox/src/devices/runtime/drivers/android/AndroidDrivers.js @@ -53,11 +53,6 @@ class AndroidDeviceDriver extends DeviceDriver { await this.uiDevice.pressBack(); } - /** @override */ - async typeText(text) { - await this.adb.typeText(this.adbName, text); - } - /** @override */ async takeScreenshot(screenshotName) { const { adbName } = this; @@ -77,6 +72,21 @@ class AndroidDeviceDriver extends DeviceDriver { return tempPath; } + + /** @override */ + async reverseTcpPort(port) { + await this.adb.reverse(this.adbName, port); + } + + /** @override */ + async unreverseTcpPort(port) { + await this.adb.reverseRemove(this.adbName, port); + } + + /** @override */ + async typeText(text) { + await this.adb.typeText(this.adbName, text); + } } /** @@ -147,6 +157,7 @@ class AndroidAppDriver extends TestAppDriver { manually: true, launchArgs: launchInfo.launchArgs, }); + await this._notifyAppReady(this.adbName, this._packageId); } /** @override */ diff --git a/detox/src/devices/runtime/drivers/ios/IosDrivers.js b/detox/src/devices/runtime/drivers/ios/IosDrivers.js index 5c032f42aa..98ee0a12a1 100644 --- a/detox/src/devices/runtime/drivers/ios/IosDrivers.js +++ b/detox/src/devices/runtime/drivers/ios/IosDrivers.js @@ -23,16 +23,6 @@ class IosAppDriver extends TestAppDriver { return await this.client.deliverPayload(params); } - /** @override */ - async sendUserActivity(payload) { - await this._sendPayload('detoxUserActivityDataURL', payload); - } - - /** @override */ - async sendUserNotification(payload) { - await this._sendPayload('detoxUserNotificationDataURL', payload); - } - /** @override */ async terminate() { // TODO effectively terminate @@ -54,15 +44,6 @@ class IosAppDriver extends TestAppDriver { await this._waitForActive(); } - async _sendPayload(name, payload) { - const payloadFile = this._createPayloadFile(payload); - - await this.deliverPayload({ - [name]: payloadFile.path, - }); - payloadFile.cleanup(); - } - async _waitForActive() { return await this.client.waitForActive(); } From 4edba3de198afe83c1236fb7c93fb54a83337ece Mon Sep 17 00:00:00 2001 From: d4vidi Date: Thu, 16 Jun 2022 12:26:00 +0300 Subject: [PATCH 07/24] [wip] Fix launch-args thanks to e2e failures --- detox/src/DeviceAPI.js | 66 +++++++++---------- detox/src/devices/runtime/TestApp.js | 11 +++- .../devices/runtime/utils/LaunchArgsEditor.js | 13 ++-- 3 files changed, 50 insertions(+), 40 deletions(-) diff --git a/detox/src/DeviceAPI.js b/detox/src/DeviceAPI.js index 97a82d9e16..1f0d1649a6 100644 --- a/detox/src/DeviceAPI.js +++ b/detox/src/DeviceAPI.js @@ -9,39 +9,39 @@ class DeviceAPI { * @param errorComposer { DetoxRuntimeErrorComposer } */ constructor(device, errorComposer) { - wrapWithStackTraceCutter(this, [ // TODO replace with Object.keys() ? - 'captureViewHierarchy', - 'clearKeychain', - 'disableSynchronization', - 'enableSynchronization', - 'installApp', - 'launchApp', - 'matchFace', - 'matchFinger', - 'openURL', - 'pressBack', - 'relaunchApp', - 'reloadReactNative', - 'resetContentAndSettings', - 'resetStatusBar', - 'reverseTcpPort', - 'selectApp', - 'sendToHome', - 'sendUserActivity', - 'sendUserNotification', - 'setBiometricEnrollment', - 'setLocation', - 'setOrientation', - 'setStatusBar', - 'setURLBlacklist', - 'shake', - 'takeScreenshot', - 'terminateApp', - 'uninstallApp', - 'unmatchFace', - 'unmatchFinger', - 'unreverseTcpPort', - ]); + // wrapWithStackTraceCutter(this, [ // TODO (multiapps) replace with Object.keys() ? + // 'captureViewHierarchy', + // 'clearKeychain', + // 'disableSynchronization', + // 'enableSynchronization', + // 'installApp', + // 'launchApp', + // 'matchFace', + // 'matchFinger', + // 'openURL', + // 'pressBack', + // 'relaunchApp', + // 'reloadReactNative', + // 'resetContentAndSettings', + // 'resetStatusBar', + // 'reverseTcpPort', + // 'selectApp', + // 'sendToHome', + // 'sendUserActivity', + // 'sendUserNotification', + // 'setBiometricEnrollment', + // 'setLocation', + // 'setOrientation', + // 'setStatusBar', + // 'setURLBlacklist', + // 'shake', + // 'takeScreenshot', + // 'terminateApp', + // 'uninstallApp', + // 'unmatchFace', + // 'unmatchFinger', + // 'unreverseTcpPort', + // ]); this.device = device; diff --git a/detox/src/devices/runtime/TestApp.js b/detox/src/devices/runtime/TestApp.js index 7dbca7f22d..d0dc0d91e6 100644 --- a/detox/src/devices/runtime/TestApp.js +++ b/detox/src/devices/runtime/TestApp.js @@ -30,7 +30,6 @@ class RunnableTestApp extends TestApp { this.behaviorConfig = behaviorConfig; this._launchArgs = new LaunchArgsEditor(); - this._launchArgs.modify(appConfig.launchArgs); } get launchArgs() { @@ -51,6 +50,11 @@ class RunnableTestApp extends TestApp { return this._driver.uiDevice; } + async select() { + this._launchArgs.reset(); + this._launchArgs.modify(this.appConfig.launchArgs); + } + async deselect() { await this._driver.deselect(); } @@ -265,6 +269,7 @@ class PredefinedTestApp extends RunnableTestApp { } async select() { + await super.select(); await this._driver.select(this.appConfig); } } @@ -275,8 +280,12 @@ class UnspecifiedTestApp extends RunnableTestApp { } async select(appConfig) { + if (!appConfig) { + throw new DetoxRuntimeError({ message: 'Please provide an appConfig argument in order to select this app' }); + } this.appConfig = appConfig; + await super.select(); await this._driver.select(this.appConfig); } } diff --git a/detox/src/devices/runtime/utils/LaunchArgsEditor.js b/detox/src/devices/runtime/utils/LaunchArgsEditor.js index 3337e36a8e..35a5e4e9d7 100644 --- a/detox/src/devices/runtime/utils/LaunchArgsEditor.js +++ b/detox/src/devices/runtime/utils/LaunchArgsEditor.js @@ -9,14 +9,15 @@ const ScopedLaunchArgsEditor = require('./ScopedLaunchArgsEditor'); * @property {boolean} [permanent=false] - Indicates whether the operation should affect the permanent app launch args. */ +const shared = new ScopedLaunchArgsEditor(); + class LaunchArgsEditor { constructor() { this._local = new ScopedLaunchArgsEditor(); - this._shared = new ScopedLaunchArgsEditor(); } get shared() { - return this._shared; + return shared; } /** @@ -27,7 +28,7 @@ class LaunchArgsEditor { if (!_.isEmpty(launchArgs)) { if (options && options.permanent) { - this._shared.modify(launchArgs); + shared.modify(launchArgs); } else { this._local.modify(launchArgs); } @@ -44,7 +45,7 @@ class LaunchArgsEditor { this._local.reset(); if (options && options.permanent) { - this._shared.reset(); + shared.reset(); } return this; @@ -58,14 +59,14 @@ class LaunchArgsEditor { const permanent = options && options.permanent; if (permanent === true) { - return this._shared.get(); + return shared.get(); } if (permanent === false) { return this._local.get(); } - return _.merge(this._shared.get(), this._local.get()); + return _.merge(shared.get(), this._local.get()); } _assertNoDeprecatedOptions(methodName, options) { From e830171a3ca3a568e1f7b6a1ef62f2e6e9d52b57 Mon Sep 17 00:00:00 2001 From: d4vidi Date: Thu, 16 Jun 2022 12:44:23 +0300 Subject: [PATCH 08/24] [wip] Fix element-screenshots API thanks to e2e failures --- detox/index.d.ts | 4 ++-- detox/src/android/interactions/native.js | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/detox/index.d.ts b/detox/index.d.ts index 5b85092547..ec31f58ea5 100644 --- a/detox/index.d.ts +++ b/detox/index.d.ts @@ -1216,7 +1216,7 @@ declare global { /** * Takes a screenshot of the element and schedules putting it in the artifacts folder upon completion of the current test. * For more information, see {@link https://wix.github.io/Detox/docs/api/screenshots#element-level-screenshots} - * @param {string} name for the screenshot artifact + * @param [name] Name for the screenshot artifact * @returns {Promise} a temporary path to the screenshot. * @example * test('Menu items should have logout', async () => { @@ -1227,7 +1227,7 @@ declare global { * // * on failure, to: /✗ Menu items should have Logout/tap on menu.png * }); */ - takeScreenshot(name: string): Promise; + takeScreenshot(name?: string): Promise; /** * Gets the native (OS-dependent) attributes of the element. diff --git a/detox/src/android/interactions/native.js b/detox/src/android/interactions/native.js index 9b8415b633..2e64cdbb83 100644 --- a/detox/src/android/interactions/native.js +++ b/detox/src/android/interactions/native.js @@ -18,8 +18,7 @@ class Interaction { } async execute() { - const resultObj = await this._device.selectedApp.invoke(this._call); - return resultObj ? resultObj.result : undefined; + return this._device.selectedApp.invoke(this._call); } } From 53dabd9485440a0e1087f815db39b808f6ad1ede Mon Sep 17 00:00:00 2001 From: d4vidi Date: Thu, 16 Jun 2022 13:00:56 +0300 Subject: [PATCH 09/24] [wip] Fix web API thanks for e2e failures; All suites are green (Android) --- detox/src/android/AndroidExpect.test.js | 8 ++++---- detox/src/android/core/WebElement.js | 12 ++++++------ detox/src/android/interactions/web.js | 3 +-- detox/src/devices/runtime/RuntimeDevice.js | 4 ++++ 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/detox/src/android/AndroidExpect.test.js b/detox/src/android/AndroidExpect.test.js index 54767c6a1a..337a28bedc 100644 --- a/detox/src/android/AndroidExpect.test.js +++ b/detox/src/android/AndroidExpect.test.js @@ -16,7 +16,7 @@ describe('AndroidExpect', () => { emitter = new Emitter(); device = { - _typeText: jest.fn(), + typeText: jest.fn(), }; const AndroidExpect = require('./AndroidExpect'); @@ -384,17 +384,17 @@ describe('AndroidExpect', () => { it('typeText with isContentEditable=false', async () => { await e.web.element(e.by.web.id('id')).typeText('text', false); - global.expect(device._typeText).not.toHaveBeenCalled(); + global.expect(device.typeText).not.toHaveBeenCalled(); }); it('typeText with isContentEditable=true', async () => { await e.web.element(e.by.web.id('id')).typeText('text', true); - global.expect(device._typeText).toHaveBeenCalled(); + global.expect(device.typeText).toHaveBeenCalled(); }); it('typeText default isContentEditable is false', async () => { await e.web.element(e.by.web.id('id')).typeText('text'); - global.expect(device._typeText).not.toHaveBeenCalled(); + global.expect(device.typeText).not.toHaveBeenCalled(); }); it('replaceText', async () => { diff --git a/detox/src/android/core/WebElement.js b/detox/src/android/core/WebElement.js index 619a87b61b..51279df2ef 100644 --- a/detox/src/android/core/WebElement.js +++ b/detox/src/android/core/WebElement.js @@ -36,27 +36,27 @@ class WebElement { async typeText(text, isContentEditable = false) { if (isContentEditable) { - return await this[_device]._typeText(text); + return await this[_device].typeText(text); } - return await new ActionInteraction(this[_device], new actions.WebTypeTextAction(this, text)).execute(); + return await new ActionInteraction(this[_device], new actions.WebTypeTextAction(this, text)).execute(); } // At the moment not working on content-editable async replaceText(text) { - return await new ActionInteraction(this[_device], new actions.WebReplaceTextAction(this, text)).execute(); + return await new ActionInteraction(this[_device], new actions.WebReplaceTextAction(this, text)).execute(); } // At the moment not working on content-editable async clearText() { - return await new ActionInteraction(this[_device], new actions.WebClearTextAction(this)).execute(); + return await new ActionInteraction(this[_device], new actions.WebClearTextAction(this)).execute(); } async scrollToView() { - return await new ActionInteraction(this[_device], new actions.WebScrollToViewAction(this)).execute(); + return await new ActionInteraction(this[_device], new actions.WebScrollToViewAction(this)).execute(); } async getText() { - return await new ActionInteraction(this[_device], new actions.WebGetTextAction(this)).execute(); + return await new ActionInteraction(this[_device], new actions.WebGetTextAction(this)).execute(); } async focus() { diff --git a/detox/src/android/interactions/web.js b/detox/src/android/interactions/web.js index 6c86df1970..db32a26cce 100644 --- a/detox/src/android/interactions/web.js +++ b/detox/src/android/interactions/web.js @@ -8,8 +8,7 @@ class WebInteraction { } async execute() { - const resultObj = await this._device.selectedApp.invoke(this._call); - return resultObj ? resultObj.result : undefined; + return this._device.selectedApp.invoke(this._call); } } diff --git a/detox/src/devices/runtime/RuntimeDevice.js b/detox/src/devices/runtime/RuntimeDevice.js index 061a4ed1f8..dc06f845de 100644 --- a/detox/src/devices/runtime/RuntimeDevice.js +++ b/detox/src/devices/runtime/RuntimeDevice.js @@ -165,6 +165,10 @@ class RuntimeDevice { await this._driver.clearKeychain(); } + async typeText(text) { + await this._driver.typeText(text); + } + async _initApps() { await forEachSeries(this._allApps(), (app) => app.init(), this); } From d42a29455e9e58684b0f1a76eb806ecbcdd131d7 Mon Sep 17 00:00:00 2001 From: d4vidi Date: Thu, 16 Jun 2022 14:40:24 +0300 Subject: [PATCH 10/24] [wip] Add caching of package ID's (based on APK path) --- detox/src/devices/runtime/drivers/android/AndroidDrivers.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/detox/src/devices/runtime/drivers/android/AndroidDrivers.js b/detox/src/devices/runtime/drivers/android/AndroidDrivers.js index 18ec9b92ed..e17f2de1fd 100644 --- a/detox/src/devices/runtime/drivers/android/AndroidDrivers.js +++ b/detox/src/devices/runtime/drivers/android/AndroidDrivers.js @@ -125,6 +125,8 @@ class AndroidAppDriver extends TestAppDriver { this.adbName = adbName; this._packageId = null; + + this._inferPackageIdFromApk = _.memoize(this._inferPackageIdFromApk.bind(this), (appInfo) => appInfo.binaryPath); } /** @override */ @@ -138,7 +140,8 @@ class AndroidAppDriver extends TestAppDriver { */ async select(appInfo) { await super.select(appInfo); - this._packageId = await this._inferPackageIdFromApk(appInfo.binaryPath); // TODO (multiapps) Cache? + + this._packageId = await this._inferPackageIdFromApk(appInfo.binaryPath); } /** @override */ From 96d17c007cc717a4ba1ee6981a809cae4938fb0d Mon Sep 17 00:00:00 2001 From: d4vidi Date: Thu, 16 Jun 2022 16:28:08 +0300 Subject: [PATCH 11/24] [wip] Get rid of driver.deliveryPayload() as a public method: Make it the impl detail it really is --- detox/src/DeviceAPI.js | 20 ++-- detox/src/devices/runtime/TestApp.js | 32 +------ .../devices/runtime/drivers/BaseDrivers.js | 95 ++++++++++++------- .../runtime/drivers/android/AndroidDrivers.js | 38 +++----- .../devices/runtime/drivers/ios/IosDrivers.js | 8 +- .../drivers/ios/IosSimulatorDrivers.js | 38 +++++++- 6 files changed, 130 insertions(+), 101 deletions(-) diff --git a/detox/src/DeviceAPI.js b/detox/src/DeviceAPI.js index 1f0d1649a6..305d880295 100644 --- a/detox/src/DeviceAPI.js +++ b/detox/src/DeviceAPI.js @@ -65,7 +65,7 @@ class DeviceAPI { } /** - * @deprecated Use 'platform' + * @deprecated Use 'platform' property */ getPlatform() { return this.platform; @@ -75,6 +75,17 @@ class DeviceAPI { return this.device.selectedApp.launchArgs; } + get uiDevice() { + return this.device.selectedApp.uiDevice; + } + + /** + * @deprecated Use 'uiDevice' property + */ + getUiDevice() { + return this.device.selectedApp.uiDevice; + } + async selectApp(aliasOrConfig) { if (aliasOrConfig === undefined) { throw this._errorComposer.cantSelectEmptyApp(); @@ -95,9 +106,6 @@ class DeviceAPI { return this.device.selectedApp.launch(params); } - /** - * @deprecated - */ async relaunchApp(params = {}) { if (params.newInstance === undefined) { params.newInstance = true; @@ -213,10 +221,6 @@ class DeviceAPI { return this.device.selectedApp.resetContentAndSettings(); } - getUiDevice() { - return this.device.selectedApp.uiDevice; - } - async setStatusBar(params) { return this.device.setStatusBar(params); } diff --git a/detox/src/devices/runtime/TestApp.js b/detox/src/devices/runtime/TestApp.js index d0dc0d91e6..9e4498a83b 100644 --- a/detox/src/devices/runtime/TestApp.js +++ b/detox/src/devices/runtime/TestApp.js @@ -37,7 +37,7 @@ class RunnableTestApp extends TestApp { } async init() { - this._driver.setOnDisconnectListener(this._onDisconnect.bind(this)); + this._driver.setDisconnectListener(this._onDisconnect.bind(this)); await this._driver.init(); } @@ -77,9 +77,9 @@ class RunnableTestApp extends TestApp { async openURL(params) { if (typeof params !== 'object' || !params.url) { - throw new DetoxRuntimeError({ message: `openURL must be called with JSON params, and a value for 'url' key must be provided. See https://wix.github.io/Detox/docs/api/device-object-api/#deviceopenurlurl-sourceappoptional.` }); + throw new DetoxRuntimeError({ message: `openURL must be called with JSON params, and a value for 'url' key must be provided. See https://wix.github.io/Detox/docs/api/device-object-api/#deviceopenurlurl-sourceappoptional` }); } - await this._driver.deliverPayload(params); + await this._driver.openURL(params); } async sendUserActivity(payload) { @@ -175,8 +175,6 @@ class RunnableTestApp extends TestApp { } async _launch(launchParams) { - const payloadParams = ['url', 'userNotification', 'userActivity']; - const hasPayload = this._assertHasSingleParam(payloadParams, launchParams); const isRunning = this._driver.isRunning(); const newInstance = (launchParams.newInstance !== undefined) ? launchParams.newInstance @@ -195,12 +193,6 @@ class RunnableTestApp extends TestApp { } const launchArgs = this._prepareLaunchArgs(launchParams); - - if (isRunning && hasPayload) { - // TODO (multiapps) Is this really needed, provided that the payload gets inject via the launch args? - await this._driver.deliverPayload({ ...launchParams, delayPayload: true }); - } - const launchInfo = { launchArgs, languageAndLocale: launchParams.languageAndLocale, @@ -211,22 +203,6 @@ class RunnableTestApp extends TestApp { } else { await this._driver.launch(launchInfo); } - - } - - _assertHasSingleParam(singleParams, params) { - let paramsCounter = 0; - - singleParams.forEach((item) => { - if(params[item]) { - paramsCounter += 1; - } - }); - - if (paramsCounter > 1) { - throw new DetoxRuntimeError({ message: `Call to 'launchApp(${JSON.stringify(params)})' must contain only one of ${JSON.stringify(singleParams)}.` }); - } - return (paramsCounter === 1); } _prepareLaunchArgs(launchInfo) { @@ -243,10 +219,8 @@ class RunnableTestApp extends TestApp { } } else if (launchInfo.userNotification) { launchArgs.detoxUserNotificationDataURL = launchInfo.userNotification; - delete launchInfo.userNotification; // TODO (multiapps) revisit whether this is needed } else if (launchInfo.userActivity) { launchArgs.detoxUserActivityDataURL = launchInfo.userActivity; - delete launchInfo.userActivity; // TODO (multiapps) revisit whether this is needed } if (launchInfo.disableTouchIndicators) { diff --git a/detox/src/devices/runtime/drivers/BaseDrivers.js b/detox/src/devices/runtime/drivers/BaseDrivers.js index 5058c4c655..e753adf3e5 100644 --- a/detox/src/devices/runtime/drivers/BaseDrivers.js +++ b/detox/src/devices/runtime/drivers/BaseDrivers.js @@ -1,7 +1,12 @@ const fs = require('fs-extra'); +const { DetoxInternalError } = require('../../../errors'); const tempFile = require('../../../utils/tempFile'); +function throwNotImplemented(func) { + throw new DetoxInternalError({ message: `Function '${func.name}' is not implemented!` }); +} + /** * @typedef DeviceDriverDeps * @property eventEmitter { AsyncEmitter } @@ -63,8 +68,15 @@ class DeviceDriver { */ /** - * @typedef LaunchInfo - * @property launchArgs { Object } + * @typedef { Object } LaunchArgs + * @property [detoxURLOverride] { String } + * @property [detoxUserNotificationDataURL] { Object } + * @property [detoxUserActivityDataURL] { Object } + */ + +/** + * @typedef { Object } LaunchInfo + * @property launchArgs { LaunchArgs } */ class TestAppDriver { @@ -96,7 +108,7 @@ class TestAppDriver { return !!this._pid; } - setOnDisconnectListener(listener) { + setDisconnectListener(listener) { this.client.terminateApp = listener; } @@ -114,18 +126,25 @@ class TestAppDriver { /** * @param _launchInfo { LaunchInfo } */ - async launch(_launchInfo) {} + async launch(_launchInfo) { + throwNotImplemented(this.launch); + } /** * @param _launchInfo { LaunchInfo } */ - async waitForLaunch(_launchInfo) {} + async waitForLaunch(_launchInfo) { + throwNotImplemented(this.waitForLaunch); + } + + /** + * @param _params {{ url: String, sourceApp: (String|undefined) }} + */ + async openURL(_params) {} async reloadReactNative() {} async resetContentAndSettings() {} - async deliverPayload(_params) {} // TODO (multiapps) Revisit whether keeping this method public makes sense at all - async sendUserActivity(payload) { await this._sendPayload('detoxUserActivityDataURL', payload); } @@ -138,24 +157,8 @@ class TestAppDriver { this._pid = null; } - async invoke(_action) {} - - setInvokeFailuresListener(listener) { - this.client.setEventCallback('testFailed', listener); - } - - async startInstrumentsRecording({ recordingPath, samplingInterval }) { - const { client } = this; - if (client.isConnected) { - return client.startInstrumentsRecording(recordingPath, samplingInterval); - } - } - - async stopInstrumentsRecording() { - const { client } = this; - if (client.isConnected) { - return client.stopInstrumentsRecording(); - } + async invoke(_action) { + throwNotImplemented(this.invoke); } async install() {} @@ -180,27 +183,52 @@ class TestAppDriver { this.client = null; } + setInvokeFailuresListener(listener) { + this.client.setEventCallback('testFailed', listener); + } + + async startInstrumentsRecording({ recordingPath, samplingInterval }) { + const { client } = this; + if (client.isConnected) { + return client.startInstrumentsRecording(recordingPath, samplingInterval); + } + } + + async stopInstrumentsRecording() { + const { client } = this; + if (client.isConnected) { + return client.stopInstrumentsRecording(); + } + } + /** @protected */ async _waitUntilReady() { return this.client.waitUntilReady(); } /** @protected */ - _createPayloadFile(payload) { - const payloadFile = tempFile.create('payload.json'); - fs.writeFileSync(payloadFile.path, JSON.stringify(payload, null, 2)); - return payloadFile; - } - async _sendPayload(name, payload) { const payloadFile = this._createPayloadFile(payload); - await this.deliverPayload({ + await this._deliverPayload({ [name]: payloadFile.path, }); payloadFile.cleanup(); } + /** @protected */ + async _deliverPayload(_payload) { + throwNotImplemented(this._deliverPayload); + } + + /** @protected */ + _createPayloadFile(payload) { + const payloadFile = tempFile.create('payload.json'); + fs.writeFileSync(payloadFile.path, JSON.stringify(payload, null, 2)); + return payloadFile; + } + + /** @protected */ async _notifyAppReady(deviceId, bundleId) { await this.emitter.emit('appReady', { deviceId, @@ -210,8 +238,7 @@ class TestAppDriver { } } - module.exports = { - DeviceDriver, TestAppDriver, + DeviceDriver, }; diff --git a/detox/src/devices/runtime/drivers/android/AndroidDrivers.js b/detox/src/devices/runtime/drivers/android/AndroidDrivers.js index e17f2de1fd..1e1417da48 100644 --- a/detox/src/devices/runtime/drivers/android/AndroidDrivers.js +++ b/detox/src/devices/runtime/drivers/android/AndroidDrivers.js @@ -164,23 +164,13 @@ class AndroidAppDriver extends TestAppDriver { } /** @override */ - async reloadReactNative() { - return this.client.reloadReactNative(); + async openURL(params) { + return this._deliverPayload(params); } /** @override */ - async deliverPayload(params) { - if (params.delayPayload) { - return; - } - - const { url, detoxUserNotificationDataURL } = params; - if (url) { - await this._startActivityWithUrl(url); - } else if (detoxUserNotificationDataURL) { - const payloadPathOnDevice = await this._sendNotificationDataToDevice(detoxUserNotificationDataURL, this.adbName); - await this._startActivityFromNotification(payloadPathOnDevice); - } + async reloadReactNative() { + return this.client.reloadReactNative(); } /** @override */ @@ -322,6 +312,16 @@ class AndroidAppDriver extends TestAppDriver { await this._instrumentation.launch(adbName, _packageId, userLaunchArgs); } + /** @override */ + async _deliverPayload({ url, detoxUserNotificationDataURL }) { + if (url) { + await this._startActivityWithUrl(url); + } else if (detoxUserNotificationDataURL) { + const payloadPathOnDevice = await this._sendNotificationDataToDevice(detoxUserNotificationDataURL, this.adbName); + await this._startActivityFromNotification(payloadPathOnDevice); + } + } + _startActivityWithUrl(url) { return this.invocationManager.execute(DetoxApi.startActivityFromUrl(url)); } @@ -365,20 +365,17 @@ class AndroidAppDriver extends TestAppDriver { } } - /** @protected */ async _sendNotificationDataToDevice(dataFileLocalPath, adbName) { await this.fileXfer.prepareDestinationDir(adbName); return await this.fileXfer.send(adbName, dataFileLocalPath, 'notification.json'); } - /** @protected */ async _reverseServerPort(adbName) { const serverPort = new URL(this.client.serverUrl).port; await this.adb.reverse(adbName, serverPort); return serverPort; } - /** @protected */ _getAppInstallPaths(_appBinaryPath, _testBinaryPath) { const appBinaryPath = getAbsoluteBinaryPath(_appBinaryPath); const testBinaryPath = _testBinaryPath ? getAbsoluteBinaryPath(_testBinaryPath) : this._getTestApkPath(appBinaryPath); @@ -388,7 +385,6 @@ class AndroidAppDriver extends TestAppDriver { }; } - /** @protected */ _getTestApkPath(originalApkPath) { const testApkPath = apkUtils.getTestApkPath(originalApkPath); @@ -402,7 +398,6 @@ class AndroidAppDriver extends TestAppDriver { return testApkPath; } - /** @protected */ async _validateAppBinaries(appBinaryPath, testBinaryPath) { try { await this.apkValidator.validateAppApk(appBinaryPath); @@ -417,13 +412,11 @@ class AndroidAppDriver extends TestAppDriver { } } - /** @protected */ async _installAppBinaries(appBinaryPath, testBinaryPath) { await this.adb.install(this.adbName, appBinaryPath); await this.adb.install(this.adbName, testBinaryPath); } - /** @protected */ async _waitForProcess() { const { adbName, _packageId } = this; let pid = NaN; @@ -439,7 +432,6 @@ class AndroidAppDriver extends TestAppDriver { return pid; } - /** @protected */ async _queryPID(appId) { const pid = await this.adb.pidof(this.adbName, appId); if (!pid) { @@ -448,13 +440,11 @@ class AndroidAppDriver extends TestAppDriver { return pid; } - /** @protected */ async _terminateInstrumentation() { await this._instrumentation.terminate(); await this._instrumentation.setTerminationFn(null); } - /** @protected */ _printInstrumentationHint({ instrumentationClass, launchArgs }) { const keyMaxLength = Math.max(3, _(launchArgs).keys().maxBy('length').length); const valueMaxLength = Math.max(5, _(launchArgs).values().map(String).maxBy('length').length); diff --git a/detox/src/devices/runtime/drivers/ios/IosDrivers.js b/detox/src/devices/runtime/drivers/ios/IosDrivers.js index 98ee0a12a1..9fe48cda8d 100644 --- a/detox/src/devices/runtime/drivers/ios/IosDrivers.js +++ b/detox/src/devices/runtime/drivers/ios/IosDrivers.js @@ -19,8 +19,8 @@ class IosAppDriver extends TestAppDriver { } /** @override */ - async deliverPayload(params) { - return await this.client.deliverPayload(params); + async openURL(params) { + return this._deliverPayload(params); } /** @override */ @@ -44,6 +44,10 @@ class IosAppDriver extends TestAppDriver { await this._waitForActive(); } + async _deliverPayload(payload) { + return this.client.deliverPayload(payload); + } + async _waitForActive() { return await this.client.waitForActive(); } diff --git a/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js b/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js index 0e343ca35f..8642fb4156 100644 --- a/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js +++ b/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js @@ -7,7 +7,6 @@ const temporaryPath = require('../../../../artifacts/utils/temporaryPath'); const DetoxRuntimeError = require('../../../../errors/DetoxRuntimeError'); const log = require('../../../../utils/logger').child({ __filename }); const pressAnyKey = require('../../../../utils/pressAnyKey'); -const tempFile = require('../../../../utils/tempFile'); const { IosDeviceDriver, IosAppDriver } = require('./IosDrivers'); @@ -104,7 +103,7 @@ class IosSimulatorDeviceDriver extends IosDeviceDriver { /** * @typedef { LaunchInfo } LaunchInfoIosSim - * @property languageAndLocale { String } + * @property [languageAndLocale] { String } */ class IosSimulatorAppDriver extends IosAppDriver { @@ -123,6 +122,9 @@ class IosSimulatorAppDriver extends IosAppDriver { async launch(launchInfo) { const { udid, bundleId } = this; + // TODO Is this "predelivery" indeed required when we're just 2ms away from sending the payload via the 'payload.json' file? :facepalm: + await this._predeliverPayloadIfNeeded(launchInfo.launchArgs); + const launchArgsHandle = this._getLaunchArgsForPayloadsData(launchInfo.launchArgs); const { launchArgs } = launchArgsHandle; @@ -145,6 +147,9 @@ class IosSimulatorAppDriver extends IosAppDriver { async waitForLaunch(launchInfo) { const { udid, bundleId } = this; + // TODO Is this "predelivery" even required in the waitForLaunch (i.e. manual) mode? + await this._predeliverPayloadIfNeeded(launchInfo.launchArgs); + // Note: This is purely semantic; Has no analytical value. const launchArgsHandle = this._getLaunchArgsForPayloadsData(launchInfo.launchArgs); const { launchArgs } = launchArgsHandle; @@ -231,7 +236,17 @@ class IosSimulatorAppDriver extends IosAppDriver { return this.client.waitForActive(); } - // TODO (multiapps) Reiterate this ugly func signature + async _predeliverPayloadIfNeeded(launchArgs) { + if (this.isRunning()) { + const payloadKeys = ['detoxURLOverride', 'detoxUserNotificationDataURL', 'detoxUserActivityDataURL']; + const payload = assertAndPickSingleKey(payloadKeys, launchArgs); + if (payload) { + await this._deliverPayload(payload); + } + } + } + + // TODO (multiapps) Revisit this ugly func signature _getLaunchArgsForPayloadsData(launchArgs) { let paramName; if (launchArgs.detoxUserNotificationDataURL) { @@ -242,7 +257,7 @@ class IosSimulatorAppDriver extends IosAppDriver { return { launchArgs, cleanup: _.noop }; } - const payloadFile = tempFile.create('payload.json'); + const payloadFile = this._createPayloadFile(launchArgs[paramName]); return { launchArgs: { ...launchArgs, @@ -253,6 +268,21 @@ class IosSimulatorAppDriver extends IosAppDriver { } } +function assertAndPickSingleKey(keys, pojo) { + const projection = _.pick(pojo, keys); + const projKeys = Object.keys(projection); + + if (projKeys.length > 1) { + const message = `An app cannot be launched with more than one url/data arguments; See https://wix.github.io/Detox/docs/api/device-object-api/#devicelaunchappparams`; + throw new DetoxRuntimeError({ message }); + } + + if (projKeys.length === 0) { + return null; + } + + return projection; +} module.exports = { IosSimulatorDeviceDriver, From 18d41d2b0d5c6a66daca5609690fb1e4b41ed0c8 Mon Sep 17 00:00:00 2001 From: d4vidi Date: Thu, 16 Jun 2022 19:01:09 +0300 Subject: [PATCH 12/24] [wip] iOS: Fix runtimeDevice-artifactsManager relationship issues --- detox/src/Detox.js | 9 ++------- detox/src/artifacts/ArtifactsManager.js | 4 ++++ detox/src/artifacts/factories/index.js | 4 ++-- .../android/AndroidInstrumentsPlugin.js | 7 +++++-- .../ios/SimulatorInstrumentsPlugin.js | 8 +++++--- .../ios/SimulatorInstrumentsRecording.js | 4 ++-- detox/src/artifacts/providers/index.js | 18 +++++++++--------- .../screenshot/SimulatorScreenshotPlugin.js | 8 ++++++-- .../templates/plugin/ArtifactPlugin.js | 8 ++++++++ .../uiHierarchy/IosUIHierarchyPlugin.js | 7 ++++--- .../src/devices/runtime/drivers/BaseDrivers.js | 1 + .../runtime/drivers/ios/IosSimulatorDrivers.js | 8 ++++---- 12 files changed, 52 insertions(+), 34 deletions(-) diff --git a/detox/src/Detox.js b/detox/src/Detox.js index 864c5885aa..25acec9512 100644 --- a/detox/src/Detox.js +++ b/detox/src/Detox.js @@ -175,13 +175,7 @@ class Detox { errorComposer: this._runtimeErrorComposer, }; - const runtimeDeviceArtifacts = { - setInvokeFailuresListener: () => runtimeDevice.setInvokeFailuresListener(...arguments), - startInstrumentsRecording: () => runtimeDevice.startInstrumentsRecording(...arguments), - stopInstrumentsRecording: () => runtimeDevice.stopInstrumentsRecording(...arguments), - }; - this._artifactsManager = artifactsManagerFactory.createArtifactsManager(this._artifactsConfig, { ...commonDeps, device: runtimeDeviceArtifacts }); - + this._artifactsManager = artifactsManagerFactory.createArtifactsManager(this._artifactsConfig, commonDeps); this._deviceAllocator = deviceAllocatorFactory.createDeviceAllocator(commonDeps); this._deviceCookie = await this._deviceAllocator.allocate(this._deviceConfig); @@ -195,6 +189,7 @@ class Detox { sessionConfig, }); await runtimeDevice.init(); + await this._artifactsManager.onDeviceCreated(runtimeDevice); this.runtimeDevice = runtimeDevice; this.device = new DeviceAPI(runtimeDevice, this._runtimeErrorComposer); diff --git a/detox/src/artifacts/ArtifactsManager.js b/detox/src/artifacts/ArtifactsManager.js index a80b7b67e8..4f7534362f 100644 --- a/detox/src/artifacts/ArtifactsManager.js +++ b/detox/src/artifacts/ArtifactsManager.js @@ -94,6 +94,10 @@ class ArtifactsManager extends EventEmitter { deviceEmitter.on('createExternalArtifact', this.onCreateExternalArtifact.bind(this)); } + async onDeviceCreated(device) { + await this._callPlugins('plain', 'onDeviceCreated', device); + } + async onBootDevice(deviceInfo) { await this._callPlugins('plain', 'onBootDevice', deviceInfo); } diff --git a/detox/src/artifacts/factories/index.js b/detox/src/artifacts/factories/index.js index 93d3277b33..ddff1c781d 100644 --- a/detox/src/artifacts/factories/index.js +++ b/detox/src/artifacts/factories/index.js @@ -15,10 +15,10 @@ class ArtifactsManagerFactory { this._provider = provider; } - createArtifactsManager(artifactsConfig, { eventEmitter, device }) { + createArtifactsManager(artifactsConfig, { eventEmitter }) { const artifactsManager = new ArtifactsManager(artifactsConfig); artifactsManager.subscribeToDeviceEvents(eventEmitter); - artifactsManager.registerArtifactPlugins(this._provider.declareArtifactPlugins({ device })); + artifactsManager.registerArtifactPlugins(this._provider.declareArtifactPlugins()); return artifactsManager; } } diff --git a/detox/src/artifacts/instruments/android/AndroidInstrumentsPlugin.js b/detox/src/artifacts/instruments/android/AndroidInstrumentsPlugin.js index ce3bc85e54..3a00111140 100644 --- a/detox/src/artifacts/instruments/android/AndroidInstrumentsPlugin.js +++ b/detox/src/artifacts/instruments/android/AndroidInstrumentsPlugin.js @@ -4,14 +4,17 @@ const InstrumentsArtifactPlugin = require('../InstrumentsArtifactPlugin'); const AndroidInstrumentsRecording = require('./AndroidInstrumentsRecording'); class AndroidInstrumentsPlugin extends InstrumentsArtifactPlugin { - constructor({ api, adb, device, devicePathBuilder }) { + constructor({ api, adb, devicePathBuilder }) { super({ api }); this.adb = adb; - this.device = device; this.devicePathBuilder = devicePathBuilder; } + async onDeviceCreated(device) { + this.device = device; + } + async onBeforeLaunchApp(event) { await super.onBeforeLaunchApp(event); diff --git a/detox/src/artifacts/instruments/ios/SimulatorInstrumentsPlugin.js b/detox/src/artifacts/instruments/ios/SimulatorInstrumentsPlugin.js index 3be660c057..ec02fcd8d7 100644 --- a/detox/src/artifacts/instruments/ios/SimulatorInstrumentsPlugin.js +++ b/detox/src/artifacts/instruments/ios/SimulatorInstrumentsPlugin.js @@ -5,10 +5,12 @@ const InstrumentsArtifactPlugin = require('../InstrumentsArtifactPlugin'); const SimulatorInstrumentsRecording = require('./SimulatorInstrumentsRecording'); class SimulatorInstrumentsPlugin extends InstrumentsArtifactPlugin { - constructor({ api, client }) { + constructor({ api }) { super({ api }); + } - this.client = client; + async onDeviceCreated(device) { + this.device = device; } async onBeforeLaunchApp(event) { @@ -32,7 +34,7 @@ class SimulatorInstrumentsPlugin extends InstrumentsArtifactPlugin { createTestRecording() { return new SimulatorInstrumentsRecording({ pluginContext: this.context, - client: this.client, + device: this.device, userConfig: this.api.userConfig, temporaryRecordingPath: temporaryPath.for.dtxrec(), }); diff --git a/detox/src/artifacts/instruments/ios/SimulatorInstrumentsRecording.js b/detox/src/artifacts/instruments/ios/SimulatorInstrumentsRecording.js index 32f446b01e..c7d02a457d 100644 --- a/detox/src/artifacts/instruments/ios/SimulatorInstrumentsRecording.js +++ b/detox/src/artifacts/instruments/ios/SimulatorInstrumentsRecording.js @@ -7,8 +7,8 @@ const FileArtifact = require('../../templates/artifact/FileArtifact'); const InstrumentsArtifactRecording = require('../InstrumentsArtifactRecording'); class SimulatorInstrumentsRecording extends InstrumentsArtifactRecording { - constructor({ pluginContext, client, userConfig, temporaryRecordingPath }) { - super({ pluginContext, client, userConfig, temporaryRecordingPath }); + constructor({ pluginContext, device, userConfig, temporaryRecordingPath }) { + super({ pluginContext, device, userConfig, temporaryRecordingPath }); } static prepareSamplingInterval(samplingInterval) { diff --git a/detox/src/artifacts/providers/index.js b/detox/src/artifacts/providers/index.js index b19dd68e6b..c899bf6118 100644 --- a/detox/src/artifacts/providers/index.js +++ b/detox/src/artifacts/providers/index.js @@ -1,10 +1,10 @@ class ArtifactPluginsProvider { - declareArtifactPlugins({ device }) {} // eslint-disable-line no-unused-vars + declareArtifactPlugins() {} } class AndroidArtifactPluginsProvider extends ArtifactPluginsProvider { /** @override */ - declareArtifactPlugins({ device }) { + declareArtifactPlugins() { const serviceLocator = require('../../servicelocator/android'); const adb = serviceLocator.adb; const devicePathBuilder = serviceLocator.devicePathBuilder; @@ -16,7 +16,7 @@ class AndroidArtifactPluginsProvider extends ArtifactPluginsProvider { const TimelineArtifactPlugin = require('../timeline/TimelineArtifactPlugin'); return { - instruments: (api) => new AndroidInstrumentsPlugin({ api, adb, device, devicePathBuilder }), + instruments: (api) => new AndroidInstrumentsPlugin({ api, adb, devicePathBuilder }), log: (api) => new ADBLogcatPlugin({ api, adb, devicePathBuilder }), screenshot: (api) => new ADBScreencapPlugin({ api, adb, devicePathBuilder }), video: (api) => new ADBScreenrecorderPlugin({ api, adb, devicePathBuilder }), @@ -27,20 +27,20 @@ class AndroidArtifactPluginsProvider extends ArtifactPluginsProvider { class IosArtifactPluginsProvider extends ArtifactPluginsProvider { /** @override */ - declareArtifactPlugins({ device }) { + declareArtifactPlugins() { const TimelineArtifactPlugin = require('../timeline/TimelineArtifactPlugin'); const IosUIHierarchyPlugin = require('../uiHierarchy/IosUIHierarchyPlugin'); return { timeline: (api) => new TimelineArtifactPlugin({ api }), - uiHierarchy: (api) => new IosUIHierarchyPlugin({ api, client }), + uiHierarchy: (api) => new IosUIHierarchyPlugin({ api }), }; } } class IosSimulatorArtifactPluginsProvider extends IosArtifactPluginsProvider { /** @override */ - declareArtifactPlugins({ device }) { + declareArtifactPlugins() { const serviceLocator = require('../../servicelocator/ios'); const appleSimUtils = serviceLocator.appleSimUtils; @@ -50,12 +50,12 @@ class IosSimulatorArtifactPluginsProvider extends IosArtifactPluginsProvider { const SimulatorRecordVideoPlugin = require('../video/SimulatorRecordVideoPlugin'); return { - ...super.declareArtifactPlugins({ client }), + ...super.declareArtifactPlugins(), log: (api) => new SimulatorLogPlugin({ api, appleSimUtils }), - screenshot: (api) => new SimulatorScreenshotPlugin({ api, appleSimUtils, client }), + screenshot: (api) => new SimulatorScreenshotPlugin({ api, appleSimUtils }), video: (api) => new SimulatorRecordVideoPlugin({ api, appleSimUtils }), - instruments: (api) => new SimulatorInstrumentsPlugin({ api, client }), + instruments: (api) => new SimulatorInstrumentsPlugin({ api }), }; } } diff --git a/detox/src/artifacts/screenshot/SimulatorScreenshotPlugin.js b/detox/src/artifacts/screenshot/SimulatorScreenshotPlugin.js index 1db929cf3f..8e7ca1e49d 100644 --- a/detox/src/artifacts/screenshot/SimulatorScreenshotPlugin.js +++ b/detox/src/artifacts/screenshot/SimulatorScreenshotPlugin.js @@ -12,8 +12,10 @@ class SimulatorScreenshotPlugin extends ScreenshotArtifactPlugin { super(config); this.appleSimUtils = config.appleSimUtils; - this.client = config.client; - this.client.setEventCallback('testFailed', this._onInvokeFailure.bind(this)); + } + + async onDeviceCreated(device) { + device.setInvokeFailuresListener(this._onInvokeFailure.bind(this)); } async onBeforeLaunchApp({ launchArgs }) { @@ -25,6 +27,8 @@ class SimulatorScreenshotPlugin extends ScreenshotArtifactPlugin { async onBootDevice(event) { await super.onBootDevice(event); + + if (this.enabled && event.coldBoot) { await this.appleSimUtils.takeScreenshot(event.deviceId, '/dev/null').catch(() => { log.debug({}, ` diff --git a/detox/src/artifacts/templates/plugin/ArtifactPlugin.js b/detox/src/artifacts/templates/plugin/ArtifactPlugin.js index 00489e1130..fda6283249 100644 --- a/detox/src/artifacts/templates/plugin/ArtifactPlugin.js +++ b/detox/src/artifacts/templates/plugin/ArtifactPlugin.js @@ -38,6 +38,14 @@ class ArtifactPlugin { this._logDisableWarning(); } + /** + * Hook that is called when a RuntimeDevice instance has been created. + * @param {RuntimeDevice} _device + * @returns {Promise} + */ + async onDeviceCreated(_device) { + } + /** * Hook that is called inside device.launchApp() before * the current app on the current device is relaunched. diff --git a/detox/src/artifacts/uiHierarchy/IosUIHierarchyPlugin.js b/detox/src/artifacts/uiHierarchy/IosUIHierarchyPlugin.js index f20d456016..634bd42d1a 100644 --- a/detox/src/artifacts/uiHierarchy/IosUIHierarchyPlugin.js +++ b/detox/src/artifacts/uiHierarchy/IosUIHierarchyPlugin.js @@ -9,9 +9,8 @@ const ArtifactPlugin = require('../templates/plugin/ArtifactPlugin'); class IosUIHierarchyPlugin extends ArtifactPlugin { /** * @param {ArtifactsApi} api - * @param {Client} client */ - constructor({ api, client }) { + constructor({ api }) { super({ api }); this._pendingDeletions = []; @@ -19,8 +18,10 @@ class IosUIHierarchyPlugin extends ArtifactPlugin { perTest: {}, perSession: {}, }; + } - client.setEventCallback('testFailed', this._onInvokeFailure.bind(this)); + async onDeviceCreated(device) { + device.setInvokeFailuresListener(this._onInvokeFailure.bind(this)); } async onBeforeLaunchApp(event) { diff --git a/detox/src/devices/runtime/drivers/BaseDrivers.js b/detox/src/devices/runtime/drivers/BaseDrivers.js index e753adf3e5..47f2c84f75 100644 --- a/detox/src/devices/runtime/drivers/BaseDrivers.js +++ b/detox/src/devices/runtime/drivers/BaseDrivers.js @@ -41,6 +41,7 @@ class DeviceDriver { return ''; } + // TODO (multiapps) Where should this be called from? validateDeviceConfig(_deviceConfig) {} async takeScreenshot(_screenshotName) {} diff --git a/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js b/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js index 8642fb4156..94b5ff8afd 100644 --- a/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js +++ b/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js @@ -95,10 +95,6 @@ class IosSimulatorDeviceDriver extends IosDeviceDriver { async resetStatusBar() { await this._applesimutils.statusBarReset(this.udid); } - - async _waitForBackground() { - return await this.client.waitForBackground(); - } } /** @@ -236,6 +232,10 @@ class IosSimulatorAppDriver extends IosAppDriver { return this.client.waitForActive(); } + async _waitForBackground() { + return await this.client.waitForBackground(); + } + async _predeliverPayloadIfNeeded(launchArgs) { if (this.isRunning()) { const payloadKeys = ['detoxURLOverride', 'detoxUserNotificationDataURL', 'detoxUserActivityDataURL']; From 5dee69034ccb6fa19ddeed726f5d1ce3dca23597 Mon Sep 17 00:00:00 2001 From: d4vidi Date: Sun, 19 Jun 2022 13:04:21 +0300 Subject: [PATCH 13/24] [wip] Introduce iOS device+app factory --- .../devices/runtime/drivers/ios/IosDrivers.js | 43 +++++++++++ .../drivers/ios/IosSimulatorDrivers.js | 26 ++++++- detox/src/devices/runtime/factories/ios.js | 72 +++++++++++++++---- 3 files changed, 125 insertions(+), 16 deletions(-) diff --git a/detox/src/devices/runtime/drivers/ios/IosDrivers.js b/detox/src/devices/runtime/drivers/ios/IosDrivers.js index 9fe48cda8d..05615c0d0c 100644 --- a/detox/src/devices/runtime/drivers/ios/IosDrivers.js +++ b/detox/src/devices/runtime/drivers/ios/IosDrivers.js @@ -1,4 +1,10 @@ +const path = require('path'); + +const exec = require('child-process-promise').exec; +const _ = require('lodash'); + const DetoxRuntimeError = require('../../../../errors/DetoxRuntimeError'); +const getAbsoluteBinaryPath = require('../../../../utils/getAbsoluteBinaryPath'); const { DeviceDriver, TestAppDriver } = require('../BaseDrivers'); class IosDeviceDriver extends DeviceDriver { @@ -8,7 +14,30 @@ class IosDeviceDriver extends DeviceDriver { } } +/** + * @typedef { AppInfo } IosAppInfo + */ + class IosAppDriver extends TestAppDriver { + /** + * @param deps { TestAppDriverDeps } + */ + constructor(deps) { + super(deps); + + this._inferBundleIdFromBinary = _.memoize(this._inferBundleIdFromBinary.bind(this), (appInfo) => appInfo.binaryPath); + } + + /** + * @override + * @param appInfo { IosAppInfo } + */ + async select(appInfo) { + await super.select(appInfo); + + this.bundleId = await this._inferBundleIdFromBinary(appInfo.binaryPath); + } + /** @override */ async deselect() { // We do not yet support concurrently running apps on iOS, so - keeping the legacy behavior, @@ -44,6 +73,20 @@ class IosAppDriver extends TestAppDriver { await this._waitForActive(); } + async _inferBundleIdFromBinary(appPath) { + appPath = getAbsoluteBinaryPath(appPath); + try { + const result = await exec(`/usr/libexec/PlistBuddy -c "Print CFBundleIdentifier" "${path.join(appPath, 'Info.plist')}"`); + const bundleId = _.trim(result.stdout); + if (_.isEmpty(bundleId)) { + throw new Error(); + } + return bundleId; + } catch (ex) { + throw new DetoxRuntimeError({ message: `field CFBundleIdentifier not found inside Info.plist of app binary at ${appPath}` }); + } + } + async _deliverPayload(payload) { return this.client.deliverPayload(payload); } diff --git a/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js b/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js index 94b5ff8afd..57b362a2da 100644 --- a/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js +++ b/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js @@ -5,6 +5,7 @@ const _ = require('lodash'); const temporaryPath = require('../../../../artifacts/utils/temporaryPath'); const DetoxRuntimeError = require('../../../../errors/DetoxRuntimeError'); +const getAbsoluteBinaryPath = require('../../../../utils/getAbsoluteBinaryPath'); const log = require('../../../../utils/logger').child({ __filename }); const pressAnyKey = require('../../../../utils/pressAnyKey'); @@ -102,11 +103,20 @@ class IosSimulatorDeviceDriver extends IosDeviceDriver { * @property [languageAndLocale] { String } */ +/** + * @typedef { TestAppDriverDeps } IosSimulatorAppDriverDeps + * @property applesimutils { AppleSimUtils } + */ + class IosSimulatorAppDriver extends IosAppDriver { - constructor(deps, { udid, bundleId }) { + + /** + * @param deps { IosSimulatorAppDriverDeps } + * @param props {{ udid: String }} + */ + constructor(deps, { udid }) { super(deps); this.udid = udid; - this.bundleId = bundleId; this._applesimutils = deps.applesimutils; } @@ -174,6 +184,18 @@ class IosSimulatorAppDriver extends IosAppDriver { return pid; } + /** @override */ + async install() { + await this._applesimutils.install(this.udid, getAbsoluteBinaryPath(this._appInfo.binaryPath)); + } + + /** @override */ + async uninstall() { + const { udid, bundleId } = this; + await this.emitter.emit('beforeUninstallApp', { deviceId: udid, bundleId }); + await this._applesimutils.uninstall(udid, bundleId); + } + /** @override */ async setPermissions(permissions) { const { udid, bundleId } = this; diff --git a/detox/src/devices/runtime/factories/ios.js b/detox/src/devices/runtime/factories/ios.js index d606c94d92..9357ee9f76 100644 --- a/detox/src/devices/runtime/factories/ios.js +++ b/detox/src/devices/runtime/factories/ios.js @@ -1,37 +1,81 @@ const RuntimeDeviceFactory = require('./base'); class RuntimeDriverFactoryIos extends RuntimeDeviceFactory { - _createDriverDependencies(commonDeps) { - const serviceLocator = require('../../../servicelocator/ios'); - const applesimutils = serviceLocator.appleSimUtils; - const { eventEmitter } = commonDeps; + _createAppDriverDeps({ sessionConfig }) { + const Client = require('../../../client/Client'); + const { InvocationManager } = require('../../../invoke'); - const SimulatorLauncher = require('../../allocation/drivers/ios/SimulatorLauncher'); + const client = new Client(sessionConfig); + const invocationManager = new InvocationManager(client); return { - ...commonDeps, - applesimutils, - simulatorLauncher: new SimulatorLauncher({ applesimutils, eventEmitter }), + client, + invocationManager, }; } } class Ios extends RuntimeDriverFactoryIos { - _createDriver(deviceCookie, deps, configs) { // eslint-disable-line no-unused-vars - const { IosRuntimeDriver } = require('../drivers'); - return new IosRuntimeDriver(deps); + /** @override */ + _createTestAppDriver(deviceCookie, commonDeps, { sessionConfig }, _alias) { + const appDeps = this._createAppDriverDeps({ sessionConfig }); + const deps = { + ...commonDeps, + ...appDeps, + }; + + const { IosAppDriver } = require('../drivers/ios/IosDrivers'); + return new IosAppDriver(deps); + } + + /** @override */ + _createDeviceDriver(deviceCookie, commonDeps, _configs) { + const { IosDeviceDriver } = require('../drivers/ios/IosDrivers'); + return new IosDeviceDriver(commonDeps); } } class IosSimulator extends RuntimeDriverFactoryIos { - _createDriver(deviceCookie, deps, { deviceConfig }) { + /** @override */ + _createTestAppDriver(deviceCookie, commonDeps, { deviceConfig, sessionConfig }, _alias) { + const simulatorDeps = this.__createIosSimulatorDriverDeps(commonDeps); + const appDeps = this._createAppDriverDeps({ sessionConfig }); + const deps = { + ...simulatorDeps, + ...appDeps, + }; + + const props = { + udid: deviceCookie.udid, + }; + + const { IosSimulatorAppDriver } = require('../drivers/ios/IosSimulatorDrivers'); + return new IosSimulatorAppDriver(deps, props); + } + + /** @override */ + _createDeviceDriver(deviceCookie, commonDeps, { deviceConfig }) { + const deps = this.__createIosSimulatorDriverDeps(commonDeps); const props = { udid: deviceCookie.udid, type: deviceConfig.device.type, bootArgs: deviceConfig.bootArgs, }; - const { IosSimulatorRuntimeDriver } = require('../drivers'); - return new IosSimulatorRuntimeDriver(deps, props); + const { IosSimulatorDeviceDriver } = require('../drivers/ios/IosSimulatorDrivers'); + return new IosSimulatorDeviceDriver(deps, props); + } + + __createIosSimulatorDriverDeps(commonDeps) { + const serviceLocator = require('../../../servicelocator/ios'); + const applesimutils = serviceLocator.appleSimUtils; + const { eventEmitter } = commonDeps; + + const SimulatorLauncher = require('../../allocation/drivers/ios/SimulatorLauncher'); + return { + ...commonDeps, + applesimutils, + simulatorLauncher: new SimulatorLauncher({ applesimutils, eventEmitter }), + }; } } From 711d980fcde30ee36f04f405f2a594be317ca567 Mon Sep 17 00:00:00 2001 From: d4vidi Date: Sun, 19 Jun 2022 22:03:50 +0300 Subject: [PATCH 14/24] [wip] Align iOS matchers API with new architecture --- detox/src/ios/expectTwo.js | 86 ++++++++++++++++----------- detox/src/matchers/factories/index.js | 4 +- 2 files changed, 53 insertions(+), 37 deletions(-) diff --git a/detox/src/ios/expectTwo.js b/detox/src/ios/expectTwo.js index 76743391c4..92ed578dcc 100644 --- a/detox/src/ios/expectTwo.js +++ b/detox/src/ios/expectTwo.js @@ -11,8 +11,12 @@ const assertDirection = assertEnum(['left', 'right', 'up', 'down']); const assertSpeed = assertEnum(['fast', 'slow']); class Expect { - constructor(invocationManager, element) { - this._invocationManager = invocationManager; + /** + * @param runtimeDevice { RuntimeDevice } + * @param element { Element } + */ + constructor(runtimeDevice, element) { + this._device = runtimeDevice; this.element = element; this.modifiers = []; } @@ -105,20 +109,25 @@ class Expect { expect(expectation, ...params) { const invocation = this.createInvocation(expectation, ...params); - return this._invocationManager.execute(invocation); + return this._device.selectedApp.invoke(invocation); } } class InternalExpect extends Expect { expect(expectation, ...params) { - const invocation = this.createInvocation(expectation, ...params); - return invocation; + return this.createInvocation(expectation, ...params); } } class Element { - constructor(invocationManager, emitter, matcher, index) { - this._invocationManager = invocationManager; + /** + * @param runtimeDevice { RuntimeDevice } + * @param emitter { AsyncEmitter } + * @param matcher { Matcher } + * @param index { Number } + */ + constructor(runtimeDevice, emitter, matcher, index) { + this._device = runtimeDevice; this._emitter = emitter; this.matcher = matcher; this.index = index; @@ -289,20 +298,18 @@ class Element { withAction(action, ...params) { const invocation = this.createInvocation(action, null, ...params); - return this._invocationManager.execute(invocation); + return this._device.selectedApp.invoke(invocation); } withActionAndTargetElement(action, targetElement, ...params) { const invocation = this.createInvocation(action, targetElement, ...params); - return this._invocationManager.execute(invocation); + return this._device.selectedApp.invoke(invocation); } } class InternalElement extends Element { - withAction(action, ...params) { - const invocation = this.createInvocation(action, null, ...params); - return invocation; + return this.createInvocation(action, null, ...params); } } @@ -417,10 +424,15 @@ class Matcher { } class WaitFor { - constructor(invocationManager, emitter, element) { - this._invocationManager = invocationManager; - this.element = new InternalElement(invocationManager, emitter, element.matcher, element.index); - this.expectation = new InternalExpect(invocationManager, this.element); + /** + * @param runtimeDevice { RuntimeDevice } + * @param emitter { AsyncEmitter } + * @param element { Element } + */ + constructor(runtimeDevice, emitter, element) { + this._device = runtimeDevice; + this.element = new InternalElement(runtimeDevice, emitter, element.matcher, element.index); + this.expectation = new InternalExpect(runtimeDevice, this.element); this._emitter = emitter; } @@ -498,7 +510,7 @@ class WaitFor { whileElement(matcher) { if (!(matcher instanceof Matcher)) throwMatcherError(matcher); - this.actionableElement = new InternalElement(this._invocationManager, this._emitter, matcher); + this.actionableElement = new InternalElement(this._device, this._emitter, matcher); return this; } @@ -585,53 +597,57 @@ class WaitFor { waitForWithAction() { const expectation = this.expectation; const action = this.action; - - return this._invocationManager.execute({ + const invocation = { ...action, while: { ...expectation } - }); + }; + return this._device.selectedApp.invoke(invocation); } waitForWithTimeout() { const expectation = this.expectation; const action = this.action; const timeout = this.timeout; - - return this._invocationManager.execute({ + const invocation = { ...action, ...expectation, timeout - }); + }; + return this._device.selectedApp.invoke(invocation); } } -function element(invocationManager, emitter, matcher) { +function element(runtimeDevice, emitter, matcher) { if (!(matcher instanceof Matcher)) { throwMatcherError(matcher); } - return new Element(invocationManager, emitter, matcher); + return new Element(runtimeDevice, emitter, matcher); } -function expect(invocationManager, element) { +function expect(runtimeDevice, element) { if (!(element instanceof Element)) { throwMatcherError(element); } - return new Expect(invocationManager, element); + return new Expect(runtimeDevice, element); } -function waitFor(invocationManager, emitter, element) { +function waitFor(runtimeDevice, emitter, element) { if (!(element instanceof Element)) { throwMatcherError(element); } - return new WaitFor(invocationManager, emitter, element); + return new WaitFor(runtimeDevice, emitter, element); } class IosExpect { - constructor({ invocationManager, emitter }) { - this._invocationManager = invocationManager; - this._emitter = emitter; + /** + * @param runtimeDevice { RuntimeDevice } + * @param eventEmitter { AsyncEmitter } + */ + constructor({ runtimeDevice, eventEmitter }) { + this._device = runtimeDevice; + this._emitter = eventEmitter; this.element = this.element.bind(this); this.expect = this.expect.bind(this); this.waitFor = this.waitFor.bind(this); @@ -641,15 +657,15 @@ class IosExpect { } element(matcher) { - return element(this._invocationManager, this._emitter, matcher); + return element(this._device, this._emitter, matcher); } expect(element) { - return expect(this._invocationManager, element); + return expect(this._device, element); } waitFor(element) { - return waitFor(this._invocationManager, this._emitter, element); + return waitFor(this._device, this._emitter, element); } web(_matcher) { diff --git a/detox/src/matchers/factories/index.js b/detox/src/matchers/factories/index.js index 7703758722..dcc0dee4b0 100644 --- a/detox/src/matchers/factories/index.js +++ b/detox/src/matchers/factories/index.js @@ -12,9 +12,9 @@ class Android extends MatchersFactory { } class Ios extends MatchersFactory { - createMatchers({ invocationManager, eventEmitter }) { + createMatchers({ runtimeDevice, eventEmitter }) { const IosExpect = require('../../ios/expectTwo'); - return new IosExpect({ invocationManager, emitter: eventEmitter }); + return new IosExpect({ runtimeDevice, eventEmitter }); } } From 8a177424a595b88de46a97eb64437a7f1464d083 Mon Sep 17 00:00:00 2001 From: d4vidi Date: Sun, 19 Jun 2022 22:33:49 +0300 Subject: [PATCH 15/24] [wip] Make sh*t work, sanity tests green --- detox/src/devices/runtime/TestApp.js | 1 + .../devices/runtime/drivers/BaseDrivers.js | 6 +++-- .../runtime/drivers/android/AndroidDrivers.js | 12 ++++----- .../devices/runtime/drivers/ios/IosDrivers.js | 5 ++-- .../drivers/ios/IosSimulatorDrivers.js | 27 ++++++++++++++++++- .../src/devices/runtime/factories/android.js | 12 --------- detox/src/devices/runtime/factories/base.js | 13 +++++++++ detox/src/devices/runtime/factories/ios.js | 21 ++++++++++----- 8 files changed, 66 insertions(+), 31 deletions(-) diff --git a/detox/src/devices/runtime/TestApp.js b/detox/src/devices/runtime/TestApp.js index 9e4498a83b..0edc11dcbb 100644 --- a/detox/src/devices/runtime/TestApp.js +++ b/detox/src/devices/runtime/TestApp.js @@ -37,6 +37,7 @@ class RunnableTestApp extends TestApp { } async init() { + // TODO (multiapps) Maybe just wire the driver to do this internally and agnostically this._driver.setDisconnectListener(this._onDisconnect.bind(this)); await this._driver.init(); diff --git a/detox/src/devices/runtime/drivers/BaseDrivers.js b/detox/src/devices/runtime/drivers/BaseDrivers.js index 47f2c84f75..61f07bc5ab 100644 --- a/detox/src/devices/runtime/drivers/BaseDrivers.js +++ b/detox/src/devices/runtime/drivers/BaseDrivers.js @@ -4,7 +4,7 @@ const { DetoxInternalError } = require('../../../errors'); const tempFile = require('../../../utils/tempFile'); function throwNotImplemented(func) { - throw new DetoxInternalError({ message: `Function '${func.name}' is not implemented!` }); + throw new DetoxInternalError(`Oops! Function '${func.name}' was left unimplemented!`); } /** @@ -143,7 +143,9 @@ class TestAppDriver { */ async openURL(_params) {} - async reloadReactNative() {} + async reloadReactNative() { + throwNotImplemented(this.reloadReactNative); + } async resetContentAndSettings() {} async sendUserActivity(payload) { diff --git a/detox/src/devices/runtime/drivers/android/AndroidDrivers.js b/detox/src/devices/runtime/drivers/android/AndroidDrivers.js index 1e1417da48..2961732b0a 100644 --- a/detox/src/devices/runtime/drivers/android/AndroidDrivers.js +++ b/detox/src/devices/runtime/drivers/android/AndroidDrivers.js @@ -184,6 +184,12 @@ class AndroidAppDriver extends TestAppDriver { await super.terminate(); } + /** @override */ + async invoke(invocation) { + const resultObj = await this.invocationManager.execute(invocation); + return resultObj ? resultObj.result : undefined; + } + /** @override */ async install() { const { _appInfo } = this; @@ -205,12 +211,6 @@ class AndroidAppDriver extends TestAppDriver { } } - /** @override */ - async invoke(invocation) { - const resultObj = await this.invocationManager.execute(invocation); - return resultObj ? resultObj.result : undefined; - } - /** @override */ async setOrientation(orientation) { const orientationMapping = { diff --git a/detox/src/devices/runtime/drivers/ios/IosDrivers.js b/detox/src/devices/runtime/drivers/ios/IosDrivers.js index 05615c0d0c..30145eda12 100644 --- a/detox/src/devices/runtime/drivers/ios/IosDrivers.js +++ b/detox/src/devices/runtime/drivers/ios/IosDrivers.js @@ -53,9 +53,8 @@ class IosAppDriver extends TestAppDriver { } /** @override */ - async terminate() { - // TODO effectively terminate - await super.terminate(); + async reloadReactNative() { + return this.client.reloadReactNative(); } /** @override */ diff --git a/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js b/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js index 57b362a2da..2b28e7c34b 100644 --- a/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js +++ b/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js @@ -132,7 +132,9 @@ class IosSimulatorAppDriver extends IosAppDriver { await this._predeliverPayloadIfNeeded(launchInfo.launchArgs); const launchArgsHandle = this._getLaunchArgsForPayloadsData(launchInfo.launchArgs); - const { launchArgs } = launchArgsHandle; + let { launchArgs } = launchArgsHandle; + + launchArgs = await this._applyAppSessionArgs(launchArgs); await this.emitter.emit('beforeLaunchApp', { bundleId, deviceId: udid, launchArgs }); const pid = await this._applesimutils.launch(udid, bundleId, launchArgs, launchInfo.languageAndLocale); @@ -184,6 +186,21 @@ class IosSimulatorAppDriver extends IosAppDriver { return pid; } + /** @override */ + async terminate() { + const { udid, bundleId } = this; + await this.emitter.emit('beforeTerminateApp', { deviceId: udid, bundleId }); + await this._applesimutils.terminate(udid, bundleId); + await this.emitter.emit('terminateApp', { deviceId: udid, bundleId }); + + await super.terminate(); + } + + /** @override */ + async invoke(action) { + return this.invocationManager.execute(action); + } + /** @override */ async install() { await this._applesimutils.install(this.udid, getAbsoluteBinaryPath(this._appInfo.binaryPath)); @@ -268,6 +285,14 @@ class IosSimulatorAppDriver extends IosAppDriver { } } + _applyAppSessionArgs(launchArgs) { + return { + detoxServer: this.client.serverUrl, + detoxSessionId: this.client.sessionId, + ...launchArgs, + }; + } + // TODO (multiapps) Revisit this ugly func signature _getLaunchArgsForPayloadsData(launchArgs) { let paramName; diff --git a/detox/src/devices/runtime/factories/android.js b/detox/src/devices/runtime/factories/android.js index 1f51dfaf9a..56d183c576 100644 --- a/detox/src/devices/runtime/factories/android.js +++ b/detox/src/devices/runtime/factories/android.js @@ -46,18 +46,6 @@ class RuntimeDriverFactoryAndroid extends RuntimeDeviceFactory { instrumentation, }; } - - _createAppSessionConfig(sessionConfig, alias) { - const { sessionId } = sessionConfig; - - if (alias) { - return { - ...sessionConfig, - sessionId: `${sessionId}:${alias}`, - }; - } - return sessionConfig; - } } class AndroidEmulator extends RuntimeDriverFactoryAndroid { diff --git a/detox/src/devices/runtime/factories/base.js b/detox/src/devices/runtime/factories/base.js index afd4af52f4..9e0ede7b02 100644 --- a/detox/src/devices/runtime/factories/base.js +++ b/detox/src/devices/runtime/factories/base.js @@ -45,6 +45,19 @@ class RuntimeDeviceFactory { }); } + /** @protected */ + _createAppSessionConfig(sessionConfig, alias) { + const { sessionId } = sessionConfig; + + if (alias) { + return { + ...sessionConfig, + sessionId: `${sessionId}:${alias}`, + }; + } + return sessionConfig; + } + _createTestAppDriver(_deviceCookie, _commonDeps, _configs, _alias) {} _createDeviceDriver(_deviceCookie, _deps, _configs) {} } diff --git a/detox/src/devices/runtime/factories/ios.js b/detox/src/devices/runtime/factories/ios.js index 9357ee9f76..1fefc18465 100644 --- a/detox/src/devices/runtime/factories/ios.js +++ b/detox/src/devices/runtime/factories/ios.js @@ -1,12 +1,19 @@ const RuntimeDeviceFactory = require('./base'); class RuntimeDriverFactoryIos extends RuntimeDeviceFactory { - _createAppDriverDeps({ sessionConfig }) { + _createAppDriverDeps({ sessionConfig }, alias) { + // TODO (multiapps) Revisit whether a session can be used in a straightforward way by managing + // the client connection better inside the driver (e.g. align with a select=>connect/deselect=>disconnect lifecycle) + // In the current way, we are in fact slightly imposing the multi-apps functionality on a platform + // that does not yet support it. + const appSessionConfig = this._createAppSessionConfig(sessionConfig, alias); + const Client = require('../../../client/Client'); - const { InvocationManager } = require('../../../invoke'); + const client = new Client(appSessionConfig); - const client = new Client(sessionConfig); + const { InvocationManager } = require('../../../invoke'); const invocationManager = new InvocationManager(client); + return { client, invocationManager, @@ -16,8 +23,8 @@ class RuntimeDriverFactoryIos extends RuntimeDeviceFactory { class Ios extends RuntimeDriverFactoryIos { /** @override */ - _createTestAppDriver(deviceCookie, commonDeps, { sessionConfig }, _alias) { - const appDeps = this._createAppDriverDeps({ sessionConfig }); + _createTestAppDriver(deviceCookie, commonDeps, { sessionConfig }, alias) { + const appDeps = this._createAppDriverDeps({ sessionConfig }, alias); const deps = { ...commonDeps, ...appDeps, @@ -36,9 +43,9 @@ class Ios extends RuntimeDriverFactoryIos { class IosSimulator extends RuntimeDriverFactoryIos { /** @override */ - _createTestAppDriver(deviceCookie, commonDeps, { deviceConfig, sessionConfig }, _alias) { + _createTestAppDriver(deviceCookie, commonDeps, { sessionConfig }, alias) { const simulatorDeps = this.__createIosSimulatorDriverDeps(commonDeps); - const appDeps = this._createAppDriverDeps({ sessionConfig }); + const appDeps = this._createAppDriverDeps({ sessionConfig }, alias); const deps = { ...simulatorDeps, ...appDeps, From 129e6bb69537568585177a7e2f085f3430a29b36 Mon Sep 17 00:00:00 2001 From: d4vidi Date: Sun, 19 Jun 2022 23:23:56 +0300 Subject: [PATCH 16/24] [wip] Apply code reuse to launch(); Fix pid capturing --- .../drivers/ios/IosSimulatorDrivers.js | 114 ++++++++++-------- 1 file changed, 63 insertions(+), 51 deletions(-) diff --git a/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js b/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js index 2b28e7c34b..0daba173ec 100644 --- a/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js +++ b/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js @@ -126,26 +126,7 @@ class IosSimulatorAppDriver extends IosAppDriver { * @param launchInfo { LaunchInfoIosSim } */ async launch(launchInfo) { - const { udid, bundleId } = this; - - // TODO Is this "predelivery" indeed required when we're just 2ms away from sending the payload via the 'payload.json' file? :facepalm: - await this._predeliverPayloadIfNeeded(launchInfo.launchArgs); - - const launchArgsHandle = this._getLaunchArgsForPayloadsData(launchInfo.launchArgs); - let { launchArgs } = launchArgsHandle; - - launchArgs = await this._applyAppSessionArgs(launchArgs); - - await this.emitter.emit('beforeLaunchApp', { bundleId, deviceId: udid, launchArgs }); - const pid = await this._applesimutils.launch(udid, bundleId, launchArgs, launchInfo.languageAndLocale); - await this.emitter.emit('launchApp', { bundleId, deviceId: udid, launchArgs, pid }); - - launchArgsHandle.cleanup(); - - await this._waitUntilReady(); - await this._waitForActive(); - await this._notifyAppReady(udid, bundleId); - return pid; + this._pid = await this._handleLaunchApp({ manually: false, launchInfo }); } /** @@ -153,37 +134,7 @@ class IosSimulatorAppDriver extends IosAppDriver { * @param launchInfo { LaunchInfoIosSim } */ async waitForLaunch(launchInfo) { - const { udid, bundleId } = this; - - // TODO Is this "predelivery" even required in the waitForLaunch (i.e. manual) mode? - await this._predeliverPayloadIfNeeded(launchInfo.launchArgs); - - // Note: This is purely semantic; Has no analytical value. - const launchArgsHandle = this._getLaunchArgsForPayloadsData(launchInfo.launchArgs); - const { launchArgs } = launchArgsHandle; - - await this.emitter.emit('beforeLaunchApp', { bundleId, deviceId: udid, launchArgs }); - - this._applesimutils.printLaunchHint(udid, bundleId, launchArgs, launchInfo.languageAndLocale); - await pressAnyKey(); - - const pid = await this._applesimutils.getPid(udid, bundleId); - if (Number.isNaN(pid)) { - throw new DetoxRuntimeError({ - message: `Failed to find a process corresponding to the app bundle identifier (${bundleId}).`, - hint: `Make sure that the app is running on the device (${udid}), visually or via CLI:\n` + - `xcrun simctl spawn ${this.udid} launchctl list | grep -F '${bundleId}'\n`, - }); - } else { - log.info({}, `Found the app (${bundleId}) with process ID = ${pid}. Proceeding...`); - } - await this.emitter.emit('launchApp', { bundleId, deviceId: udid, launchArgs, pid }); - - launchArgsHandle.cleanup(); - - await this._waitUntilReady(); - await this._waitForActive(); - return pid; + this._pid = await this._handleLaunchApp({ manually: true, launchInfo }); } /** @override */ @@ -285,6 +236,67 @@ class IosSimulatorAppDriver extends IosAppDriver { } } + async _handleLaunchApp({ manually, launchInfo }) { + const { udid, bundleId } = this; + + // TODO In launch-mode: Is this "predelivery" indeed required when we're just 2ms away from sending the payload via the + // 'payload.json' file? :facepalm: + // TODO In manual-mode: Is this "predelivery" even required altogether? + await this._predeliverPayloadIfNeeded(launchInfo.launchArgs); + + const launchArgsHandle = this._getLaunchArgsForPayloadsData(launchInfo.launchArgs); + let { launchArgs } = launchArgsHandle; + + launchArgs = await this._applyAppSessionArgs(launchArgs); + + await this.emitter.emit('beforeLaunchApp', { bundleId, deviceId: udid, launchArgs }); + + let pid; + if (manually) { + pid = await this.__waitForAppLaunch(launchArgs, launchInfo.languageAndLocale); + } else { + pid = await this.__launchApp(launchArgs, launchInfo.languageAndLocale); + } + + await this.emitter.emit('launchApp', { bundleId, deviceId: udid, launchArgs, pid }); + + launchArgsHandle.cleanup(); + + await this._waitUntilReady(); + await this._waitForActive(); + await this._notifyAppReady(udid, bundleId); + + return pid; + } + + async __waitForAppLaunch(launchArgs, languageAndLocale) { + const { udid, bundleId } = this; + + this._applesimutils.printLaunchHint(udid, bundleId, launchArgs, languageAndLocale); + await pressAnyKey(); + + const pid = await this._applesimutils.getPid(udid, bundleId); + if (Number.isNaN(pid)) { + throw new DetoxRuntimeError({ + message: `Failed to find a process corresponding to the app bundle identifier (${bundleId}).`, + hint: `Make sure that the app is running on the device (${udid}), visually or via CLI:\n` + + `xcrun simctl spawn ${this.udid} launchctl list | grep -F '${bundleId}'\n`, + }); + } else { + log.info({}, `Found the app (${bundleId}) with process ID = ${pid}. Proceeding...`); + } + return pid; + } + + async __launchApp(launchArgs, languageAndLocale) { + const { udid, bundleId } = this; + + await this.emitter.emit('beforeLaunchApp', { bundleId, deviceId: udid, launchArgs }); + const pid = await this._applesimutils.launch(udid, bundleId, launchArgs, languageAndLocale); + await this.emitter.emit('launchApp', { bundleId, deviceId: udid, launchArgs, pid }); + return pid; + } + _applyAppSessionArgs(launchArgs) { return { detoxServer: this.client.serverUrl, From 28392e24d6d4d35b10f36e1aca242b776ddf09b7 Mon Sep 17 00:00:00 2001 From: d4vidi Date: Mon, 20 Jun 2022 12:18:47 +0300 Subject: [PATCH 17/24] [wip] Push all platform-specific launchArgs mng to drivers layer => Fix notif e2e --- detox/src/devices/runtime/TestApp.js | 31 ++----- .../devices/runtime/drivers/BaseDrivers.js | 20 +++-- .../runtime/drivers/android/AndroidDrivers.js | 72 +++++++++------- .../drivers/ios/IosSimulatorDrivers.js | 86 +++++++++++-------- 4 files changed, 116 insertions(+), 93 deletions(-) diff --git a/detox/src/devices/runtime/TestApp.js b/detox/src/devices/runtime/TestApp.js index 0edc11dcbb..c96a1ffb36 100644 --- a/detox/src/devices/runtime/TestApp.js +++ b/detox/src/devices/runtime/TestApp.js @@ -1,3 +1,5 @@ +const _ = require('lodash'); + const DetoxRuntimeError = require('../../errors/DetoxRuntimeError'); const { traceCall } = require('../../utils/trace'); @@ -176,6 +178,8 @@ class RunnableTestApp extends TestApp { } async _launch(launchParams) { + const passthroughParams = ['url', 'sourceApp', 'userNotification', 'userActivity', 'disableTouchIndicators', 'languageAndLocale']; + const isRunning = this._driver.isRunning(); const newInstance = (launchParams.newInstance !== undefined) ? launchParams.newInstance @@ -193,10 +197,10 @@ class RunnableTestApp extends TestApp { await this._driver.setPermissions(launchParams.permissions); } - const launchArgs = this._prepareLaunchArgs(launchParams); + const userLaunchArgs = this._mergeUserLaunchArgs(launchParams); const launchInfo = { - launchArgs, - languageAndLocale: launchParams.languageAndLocale, + ..._.pick(launchParams, passthroughParams), + userLaunchArgs, }; if (this.behaviorConfig.launchApp === 'manual') { @@ -206,28 +210,11 @@ class RunnableTestApp extends TestApp { } } - _prepareLaunchArgs(launchInfo) { - const launchArgs = { + _mergeUserLaunchArgs(launchInfo) { + return { ...this._launchArgs.get(), ...launchInfo.launchArgs, }; - - if (launchInfo.url) { - launchArgs.detoxURLOverride = launchInfo.url; - - if (launchInfo.sourceApp) { - launchArgs.detoxSourceAppOverride = launchInfo.sourceApp; - } - } else if (launchInfo.userNotification) { - launchArgs.detoxUserNotificationDataURL = launchInfo.userNotification; - } else if (launchInfo.userActivity) { - launchArgs.detoxUserActivityDataURL = launchInfo.userActivity; - } - - if (launchInfo.disableTouchIndicators) { - launchArgs.detoxDisableTouchIndicators = true; - } - return launchArgs; } } diff --git a/detox/src/devices/runtime/drivers/BaseDrivers.js b/detox/src/devices/runtime/drivers/BaseDrivers.js index 61f07bc5ab..547f373c48 100644 --- a/detox/src/devices/runtime/drivers/BaseDrivers.js +++ b/detox/src/devices/runtime/drivers/BaseDrivers.js @@ -77,7 +77,7 @@ class DeviceDriver { /** * @typedef { Object } LaunchInfo - * @property launchArgs { LaunchArgs } + * @property userLaunchArgs { LaunchArgs } */ class TestAppDriver { @@ -232,12 +232,18 @@ class TestAppDriver { } /** @protected */ - async _notifyAppReady(deviceId, bundleId) { - await this.emitter.emit('appReady', { - deviceId, - bundleId, - pid: this._pid, - }); + async _notifyBeforeAppLaunch(deviceId, bundleId, launchArgs) { + await this.emitter.emit('beforeLaunchApp', { bundleId, deviceId, launchArgs }); + } + + /** @protected */ + async _notifyAppLaunch(deviceId, bundleId, launchArgs, pid) { + await this.emitter.emit('launchApp', { bundleId, deviceId, launchArgs, pid }); + } + + /** @protected */ + async _notifyAppReady(deviceId, bundleId, pid) { + await this.emitter.emit('appReady', { deviceId, bundleId, pid }); } } diff --git a/detox/src/devices/runtime/drivers/android/AndroidDrivers.js b/detox/src/devices/runtime/drivers/android/AndroidDrivers.js index 2961732b0a..ef6191eb07 100644 --- a/detox/src/devices/runtime/drivers/android/AndroidDrivers.js +++ b/detox/src/devices/runtime/drivers/android/AndroidDrivers.js @@ -94,6 +94,11 @@ class AndroidDeviceDriver extends DeviceDriver { * @property testBinaryPath { String } */ +/** + * @typedef { LaunchInfo } LaunchInfoAndroid + * @property [userNotification] { Object } + */ + /** * @typedef { TestAppDriverDeps } AndroidAppDriverDeps * @property adb { ADB } @@ -144,23 +149,26 @@ class AndroidAppDriver extends TestAppDriver { this._packageId = await this._inferPackageIdFromApk(appInfo.binaryPath); } - /** @override */ + /** + * @override + * @param launchInfo { LaunchInfoAndroid } + */ async launch(launchInfo) { this._pid = await this._handleLaunchApp({ manually: false, - launchArgs: launchInfo.launchArgs, + launchInfo, }); - await this._waitUntilReady(); - await this._notifyAppReady(this.adbName, this._packageId); } - /** @override */ + /** + * @override + * @param launchInfo { LaunchInfoAndroid } + */ async waitForLaunch(launchInfo) { this._pid = await this._handleLaunchApp({ manually: true, - launchArgs: launchInfo.launchArgs, + launchInfo, }); - await this._notifyAppReady(this.adbName, this._packageId); } /** @override */ @@ -257,13 +265,19 @@ class AndroidAppDriver extends TestAppDriver { return await this.aapt.getPackageName(binaryPath); } - async _handleLaunchApp({ manually, launchArgs }) { + async _handleLaunchApp({ manually, launchInfo }) { const { adbName, _packageId } = this; - await this.emitter.emit('beforeLaunchApp', { deviceId: adbName, bundleId: _packageId, launchArgs }); + const userLaunchArgs = { ...launchInfo.userLaunchArgs }; + const notificationLaunchArgs = await this._getNotificationLaunchArgs(launchInfo); + const sessionLaunchArgs = this._getAppSessionArgs(); + const launchArgs = { + ...userLaunchArgs, + ...notificationLaunchArgs, + ...sessionLaunchArgs, + }; - launchArgs = await this._applyAppSessionArgs(launchArgs); - launchArgs = await this._modifyArgsForNotificationHandling(launchArgs); + await this._notifyBeforeAppLaunch(adbName, _packageId, launchArgs); if (manually) { await this.__waitForAppLaunch(launchArgs); @@ -276,7 +290,9 @@ class AndroidAppDriver extends TestAppDriver { log.info({}, `Found the app (${_packageId}) with process ID = ${pid}. Proceeding...`); } - await this.emitter.emit('launchApp', { deviceId: adbName, bundleId: _packageId, launchArgs, pid }); + await this._notifyAppLaunch(adbName, _packageId, launchArgs, pid); + await this._waitUntilReady(); + await this._notifyAppReady(adbName, _packageId, pid); return pid; } @@ -317,7 +333,7 @@ class AndroidAppDriver extends TestAppDriver { if (url) { await this._startActivityWithUrl(url); } else if (detoxUserNotificationDataURL) { - const payloadPathOnDevice = await this._sendNotificationDataToDevice(detoxUserNotificationDataURL, this.adbName); + const payloadPathOnDevice = await this._sendNotificationFileToDevice(detoxUserNotificationDataURL, this.adbName); await this._startActivityFromNotification(payloadPathOnDevice); } } @@ -334,28 +350,26 @@ class AndroidAppDriver extends TestAppDriver { return this.invocationManager.execute(DetoxApi.launchMainActivity()); } - _applyAppSessionArgs(launchArgs) { - return { - detoxServer: this.client.serverUrl, - detoxSessionId: this.client.sessionId, - ...launchArgs, - }; - } + async _getNotificationLaunchArgs(launchInfo) { + const launchArgs = {}; - async _modifyArgsForNotificationHandling(launchArgs) { - if (launchArgs.detoxUserNotificationDataURL) { - const notificationLocalFile = this._createPayloadFile(launchArgs.detoxUserNotificationDataURL); - const notificationTargetPath = await this._sendNotificationDataToDevice(notificationLocalFile.path, this.adbName); + if (launchInfo.userNotification) { + const notificationLocalFile = this._createPayloadFile(launchInfo.userNotification); + const notificationTargetPath = await this._sendNotificationFileToDevice(notificationLocalFile.path, this.adbName); notificationLocalFile.cleanup(); - return { - ...launchArgs, - detoxUserNotificationDataURL: notificationTargetPath, - }; + launchArgs.detoxUserNotificationDataURL = notificationTargetPath; } return launchArgs; } + _getAppSessionArgs() { + return { + detoxServer: this.client.serverUrl, + detoxSessionId: this.client.sessionId, + }; + } + /** @override */ async _waitUntilReady() { try { @@ -365,7 +379,7 @@ class AndroidAppDriver extends TestAppDriver { } } - async _sendNotificationDataToDevice(dataFileLocalPath, adbName) { + async _sendNotificationFileToDevice(dataFileLocalPath, adbName) { await this.fileXfer.prepareDestinationDir(adbName); return await this.fileXfer.send(adbName, dataFileLocalPath, 'notification.json'); } diff --git a/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js b/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js index 0daba173ec..e1cfb160ce 100644 --- a/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js +++ b/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js @@ -100,6 +100,11 @@ class IosSimulatorDeviceDriver extends IosDeviceDriver { /** * @typedef { LaunchInfo } LaunchInfoIosSim + * @property [url] { String } + * @property [sourceApp] { String } + * @property [userNotification] { Object } + * @property [userActivity] { Object } + * @property [disableTouchIndicators] { Boolean } * @property [languageAndLocale] { String } */ @@ -239,17 +244,20 @@ class IosSimulatorAppDriver extends IosAppDriver { async _handleLaunchApp({ manually, launchInfo }) { const { udid, bundleId } = this; + const fundamentalLaunchArgs = this._getFundamentalLaunchArgs(launchInfo); + const payloadLaunchArgsHandle = this._getPayloadFileOrUrlLaunchArgs(launchInfo); + const sessionLaunchArgs = this._getAppSessionArgs(); + const launchArgs = { + ...fundamentalLaunchArgs, + ...payloadLaunchArgsHandle.launchArgs, + ...sessionLaunchArgs, + }; + // TODO In launch-mode: Is this "predelivery" indeed required when we're just 2ms away from sending the payload via the // 'payload.json' file? :facepalm: // TODO In manual-mode: Is this "predelivery" even required altogether? - await this._predeliverPayloadIfNeeded(launchInfo.launchArgs); - - const launchArgsHandle = this._getLaunchArgsForPayloadsData(launchInfo.launchArgs); - let { launchArgs } = launchArgsHandle; - - launchArgs = await this._applyAppSessionArgs(launchArgs); - - await this.emitter.emit('beforeLaunchApp', { bundleId, deviceId: udid, launchArgs }); + await this._predeliverPayloadIfNeeded(launchArgs); + await this._notifyBeforeAppLaunch(udid, bundleId, launchArgs); let pid; if (manually) { @@ -257,15 +265,12 @@ class IosSimulatorAppDriver extends IosAppDriver { } else { pid = await this.__launchApp(launchArgs, launchInfo.languageAndLocale); } + payloadLaunchArgsHandle.cleanup(); - await this.emitter.emit('launchApp', { bundleId, deviceId: udid, launchArgs, pid }); - - launchArgsHandle.cleanup(); - + await this._notifyAppLaunch(udid, bundleId, launchArgs, pid); await this._waitUntilReady(); await this._waitForActive(); await this._notifyAppReady(udid, bundleId); - return pid; } @@ -291,38 +296,49 @@ class IosSimulatorAppDriver extends IosAppDriver { async __launchApp(launchArgs, languageAndLocale) { const { udid, bundleId } = this; - await this.emitter.emit('beforeLaunchApp', { bundleId, deviceId: udid, launchArgs }); const pid = await this._applesimutils.launch(udid, bundleId, launchArgs, languageAndLocale); - await this.emitter.emit('launchApp', { bundleId, deviceId: udid, launchArgs, pid }); return pid; } - _applyAppSessionArgs(launchArgs) { - return { - detoxServer: this.client.serverUrl, - detoxSessionId: this.client.sessionId, - ...launchArgs, + _getFundamentalLaunchArgs(launchInfo) { + const launchArgs = { + ...launchInfo.userLaunchArgs, }; + + if (launchInfo.disableTouchIndicators) { + launchArgs.detoxDisableTouchIndicators = true; + } + return launchArgs; } - // TODO (multiapps) Revisit this ugly func signature - _getLaunchArgsForPayloadsData(launchArgs) { - let paramName; - if (launchArgs.detoxUserNotificationDataURL) { - paramName = 'detoxUserNotificationDataURL'; - } else if (launchArgs.detoxUserActivityDataURL) { - paramName = 'detoxUserActivityDataURL'; - } else { - return { launchArgs, cleanup: _.noop }; + _getPayloadFileOrUrlLaunchArgs(launchInfo) { + const launchArgs = {}; + let cleanup = _.noop; + + if (launchInfo.url) { + launchArgs.detoxURLOverride = launchInfo.url; + + if (launchInfo.sourceApp) { + launchArgs.detoxSourceAppOverride = launchInfo.sourceApp; + } + } else if (launchInfo.userNotification) { + const payloadFile = this._createPayloadFile(launchInfo.userNotification); + launchArgs.detoxUserNotificationDataURL = payloadFile.path; + } else if (launchInfo.userActivity) { + const payloadFile = this._createPayloadFile(launchInfo.userActivity); + launchArgs.detoxUserActivityDataURL = payloadFile.path; } - const payloadFile = this._createPayloadFile(launchArgs[paramName]); return { - launchArgs: { - ...launchArgs, - [paramName]: payloadFile.path, - }, - cleanup: () => payloadFile.cleanup(), + launchArgs, + cleanup, + }; + } + + _getAppSessionArgs() { + return { + detoxServer: this.client.serverUrl, + detoxSessionId: this.client.sessionId, }; } } From ce9c3545bf71372c8a3fc50ed458adc020581e45 Mon Sep 17 00:00:00 2001 From: d4vidi Date: Mon, 20 Jun 2022 14:23:21 +0300 Subject: [PATCH 18/24] [wip] Fix url launching, improve payload delivery altogether --- .../devices/runtime/drivers/BaseDrivers.js | 15 ++-- .../drivers/ios/IosSimulatorDrivers.js | 70 ++++++++++++------- 2 files changed, 49 insertions(+), 36 deletions(-) diff --git a/detox/src/devices/runtime/drivers/BaseDrivers.js b/detox/src/devices/runtime/drivers/BaseDrivers.js index 547f373c48..d725731312 100644 --- a/detox/src/devices/runtime/drivers/BaseDrivers.js +++ b/detox/src/devices/runtime/drivers/BaseDrivers.js @@ -127,25 +127,18 @@ class TestAppDriver { /** * @param _launchInfo { LaunchInfo } */ - async launch(_launchInfo) { - throwNotImplemented(this.launch); - } + async launch(_launchInfo) { throwNotImplemented(this.launch); } /** * @param _launchInfo { LaunchInfo } */ - async waitForLaunch(_launchInfo) { - throwNotImplemented(this.waitForLaunch); - } + async waitForLaunch(_launchInfo) { throwNotImplemented(this.waitForLaunch); } /** * @param _params {{ url: String, sourceApp: (String|undefined) }} */ - async openURL(_params) {} - - async reloadReactNative() { - throwNotImplemented(this.reloadReactNative); - } + async openURL(_params) { throwNotImplemented(this.openURL); } + async reloadReactNative() { throwNotImplemented(this.reloadReactNative); } async resetContentAndSettings() {} async sendUserActivity(payload) { diff --git a/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js b/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js index e1cfb160ce..6786e8dd56 100644 --- a/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js +++ b/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js @@ -205,6 +205,21 @@ class IosSimulatorAppDriver extends IosAppDriver { await this._waitForActive(); } + /** @override */ + async setURLBlacklist(urlList) { + await this.client.setSyncSettings({ blacklistURLs: urlList }); + } + + /** @override */ + async enableSynchronization() { + await this.client.setSyncSettings({ enabled: true }); + } + + /** @override */ + async disableSynchronization() { + await this.client.setSyncSettings({ enabled: false }); + } + /** @override */ async captureViewHierarchy(artifactName) { const viewHierarchyURL = temporaryPath.for.viewhierarchy(); @@ -231,32 +246,18 @@ class IosSimulatorAppDriver extends IosAppDriver { return await this.client.waitForBackground(); } - async _predeliverPayloadIfNeeded(launchArgs) { - if (this.isRunning()) { - const payloadKeys = ['detoxURLOverride', 'detoxUserNotificationDataURL', 'detoxUserActivityDataURL']; - const payload = assertAndPickSingleKey(payloadKeys, launchArgs); - if (payload) { - await this._deliverPayload(payload); - } - } - } - async _handleLaunchApp({ manually, launchInfo }) { const { udid, bundleId } = this; const fundamentalLaunchArgs = this._getFundamentalLaunchArgs(launchInfo); - const payloadLaunchArgsHandle = this._getPayloadFileOrUrlLaunchArgs(launchInfo); + const payloadLaunchArgsHandle = await this._getPayloadFileOrUrlLaunchArgs(launchInfo); const sessionLaunchArgs = this._getAppSessionArgs(); const launchArgs = { ...fundamentalLaunchArgs, - ...payloadLaunchArgsHandle.launchArgs, + ...payloadLaunchArgsHandle.args, ...sessionLaunchArgs, }; - // TODO In launch-mode: Is this "predelivery" indeed required when we're just 2ms away from sending the payload via the - // 'payload.json' file? :facepalm: - // TODO In manual-mode: Is this "predelivery" even required altogether? - await this._predeliverPayloadIfNeeded(launchArgs); await this._notifyBeforeAppLaunch(udid, bundleId, launchArgs); let pid; @@ -311,26 +312,45 @@ class IosSimulatorAppDriver extends IosAppDriver { return launchArgs; } - _getPayloadFileOrUrlLaunchArgs(launchInfo) { - const launchArgs = {}; + async _getPayloadFileOrUrlLaunchArgs(launchInfo) { + // TODO (multiapps) Why the heck is payload predelivery even needed if it is anyways available via the launch-arg? :shrug: + + const deliverPayloadIfNeeded = async (payload) => { + if (this.isRunning()) { + await this._deliverPayload(payload); + } + }; + + const deliverPayloadAndSetupLaunchArg = async (argName, payload) => { + const payloadFile = this._createPayloadFile(payload); + args[argName] = payloadFile.path; + + await deliverPayloadIfNeeded({ [argName]: payloadFile.path }); + cleanup = () => payloadFile.cleanup(); + }; + + const args = {}; let cleanup = _.noop; if (launchInfo.url) { - launchArgs.detoxURLOverride = launchInfo.url; + // TODO Are 'url' and 'detoxURLOverride' both required? + args.url = launchInfo.url; + args.detoxURLOverride = launchInfo.url; if (launchInfo.sourceApp) { - launchArgs.detoxSourceAppOverride = launchInfo.sourceApp; + args.detoxSourceAppOverride = launchInfo.sourceApp; } + + // TODO Why 'url' instead of 'detoxURLOverride' in payload, unlike the other params? + await deliverPayloadIfNeeded({ url: launchInfo.url, sourceApp: launchInfo.sourceApp }); } else if (launchInfo.userNotification) { - const payloadFile = this._createPayloadFile(launchInfo.userNotification); - launchArgs.detoxUserNotificationDataURL = payloadFile.path; + await deliverPayloadAndSetupLaunchArg('detoxUserNotificationDataURL', launchInfo.userNotification); } else if (launchInfo.userActivity) { - const payloadFile = this._createPayloadFile(launchInfo.userActivity); - launchArgs.detoxUserActivityDataURL = payloadFile.path; + await deliverPayloadAndSetupLaunchArg('detoxUserActivityDataURL', launchInfo.userActivity); } return { - launchArgs, + args, cleanup, }; } From e291b53443576dff889e1cdec949aff6bb6c662f Mon Sep 17 00:00:00 2001 From: d4vidi Date: Mon, 20 Jun 2022 14:49:01 +0300 Subject: [PATCH 19/24] [wip] Fix captureViewHierarchy artifact --- detox/src/DeviceAPI.js | 2 +- detox/src/devices/runtime/RuntimeDevice.js | 4 ---- detox/src/devices/runtime/TestApp.js | 4 ++++ detox/src/devices/runtime/drivers/BaseDrivers.js | 2 +- .../runtime/drivers/ios/IosSimulatorDrivers.js | 16 ---------------- 5 files changed, 6 insertions(+), 22 deletions(-) diff --git a/detox/src/DeviceAPI.js b/detox/src/DeviceAPI.js index 305d880295..79880ae043 100644 --- a/detox/src/DeviceAPI.js +++ b/detox/src/DeviceAPI.js @@ -118,7 +118,7 @@ class DeviceAPI { } async captureViewHierarchy(name = 'capture') { - return this.device.captureViewHierarchy(name); + return this.device.selectedApp.captureViewHierarchy(name); } async sendToHome() { diff --git a/detox/src/devices/runtime/RuntimeDevice.js b/detox/src/devices/runtime/RuntimeDevice.js index 6dd72a47d5..4f75fe57b6 100644 --- a/detox/src/devices/runtime/RuntimeDevice.js +++ b/detox/src/devices/runtime/RuntimeDevice.js @@ -133,10 +133,6 @@ class RuntimeDevice { return this._driver.takeScreenshot(name); } - async captureViewHierarchy(name) { - return this._driver.captureViewHierarchy(name); - } - async setBiometricEnrollment(yesOrNo) { await this._driver.setBiometricEnrollment(yesOrNo); } diff --git a/detox/src/devices/runtime/TestApp.js b/detox/src/devices/runtime/TestApp.js index c96a1ffb36..0943df9679 100644 --- a/detox/src/devices/runtime/TestApp.js +++ b/detox/src/devices/runtime/TestApp.js @@ -78,6 +78,10 @@ class RunnableTestApp extends TestApp { await this._driver.disableSynchronization(); } + async captureViewHierarchy(name) { + return this._driver.captureViewHierarchy(name); + } + async openURL(params) { if (typeof params !== 'object' || !params.url) { throw new DetoxRuntimeError({ message: `openURL must be called with JSON params, and a value for 'url' key must be provided. See https://wix.github.io/Detox/docs/api/device-object-api/#deviceopenurlurl-sourceappoptional` }); diff --git a/detox/src/devices/runtime/drivers/BaseDrivers.js b/detox/src/devices/runtime/drivers/BaseDrivers.js index d725731312..0c84c5870e 100644 --- a/detox/src/devices/runtime/drivers/BaseDrivers.js +++ b/detox/src/devices/runtime/drivers/BaseDrivers.js @@ -172,7 +172,7 @@ class TestAppDriver { async setURLBlacklist(_urlList) {} async enableSynchronization() {} async disableSynchronization() {} - async captureViewHierarchy() {} + async captureViewHierarchy(_name) {} async cleanup() { this.client.dumpPendingRequests(); await this.client.cleanup(); diff --git a/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js b/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js index 6786e8dd56..0f4ea2a9c3 100644 --- a/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js +++ b/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js @@ -363,22 +363,6 @@ class IosSimulatorAppDriver extends IosAppDriver { } } -function assertAndPickSingleKey(keys, pojo) { - const projection = _.pick(pojo, keys); - const projKeys = Object.keys(projection); - - if (projKeys.length > 1) { - const message = `An app cannot be launched with more than one url/data arguments; See https://wix.github.io/Detox/docs/api/device-object-api/#devicelaunchappparams`; - throw new DetoxRuntimeError({ message }); - } - - if (projKeys.length === 0) { - return null; - } - - return projection; -} - module.exports = { IosSimulatorDeviceDriver, IosSimulatorAppDriver, From 36b0e2830b98b8d3bc585fac04bfce82e9a6e5ed Mon Sep 17 00:00:00 2001 From: d4vidi Date: Mon, 20 Jun 2022 19:30:47 +0300 Subject: [PATCH 20/24] [wip] Genymotion cloud --- ...GenyCloudDriver.js => GenycloudDrivers.js} | 24 +++++++++++++------ ...river.test.js => GenycloudDrivers.test.js} | 0 .../src/devices/runtime/factories/android.js | 24 +++++++++++++++---- 3 files changed, 37 insertions(+), 11 deletions(-) rename detox/src/devices/runtime/drivers/android/genycloud/{GenyCloudDriver.js => GenycloudDrivers.js} (55%) rename detox/src/devices/runtime/drivers/android/genycloud/{GenyCloudDriver.test.js => GenycloudDrivers.test.js} (100%) diff --git a/detox/src/devices/runtime/drivers/android/genycloud/GenyCloudDriver.js b/detox/src/devices/runtime/drivers/android/genycloud/GenycloudDrivers.js similarity index 55% rename from detox/src/devices/runtime/drivers/android/genycloud/GenyCloudDriver.js rename to detox/src/devices/runtime/drivers/android/genycloud/GenycloudDrivers.js index fa8331a01b..51b072cb06 100644 --- a/detox/src/devices/runtime/drivers/android/genycloud/GenyCloudDriver.js +++ b/detox/src/devices/runtime/drivers/android/genycloud/GenycloudDrivers.js @@ -1,20 +1,20 @@ // @ts-nocheck const DetoxGenymotionManager = require('../../../../../android/espressoapi/DetoxGenymotionManager'); -const AndroidDriver = require('../AndroidDriver'); +const { AndroidDeviceDriver, AndroidAppDriver } = require('../AndroidDrivers'); /** - * @typedef { AndroidDeviceDriverDeps } GenycloudDriverDeps + * @typedef { AndroidDeviceDriverDeps } GenycloudDeviceDriverDeps */ /** - * @typedef GenycloudDriverProps + * @typedef { Object } GenycloudDeviceDriverProps * @property instance { GenyInstance } The DTO associated with the cloud instance */ -class GenyCloudDriver extends AndroidDriver { +class GenycloudDeviceDriver extends AndroidDeviceDriver { /** - * @param deps { GenycloudDriverDeps } - * @param props { GenycloudDriverProps } + * @param deps { GenycloudDeviceDriverDeps } + * @param props { GenycloudDeviceDriverProps } */ constructor(deps, { instance }) { super(deps, { adbName: instance.adbName }); @@ -29,10 +29,20 @@ class GenyCloudDriver extends AndroidDriver { async setLocation(lat, lon) { await this.invocationManager.execute(DetoxGenymotionManager.setLocation(parseFloat(lat), parseFloat(lon))); } +} + +class GenycloudAppDriver extends AndroidAppDriver { + constructor(deps, { instance }) { + super(deps, { adbName: instance.adbName }); + } + /** @override */ async _installAppBinaries(appBinaryPath, testBinaryPath) { await this.appInstallHelper.install(this.adbName, appBinaryPath, testBinaryPath); } } -module.exports = GenyCloudDriver; +module.exports = { + GenycloudDeviceDriver, + GenycloudAppDriver, +}; diff --git a/detox/src/devices/runtime/drivers/android/genycloud/GenyCloudDriver.test.js b/detox/src/devices/runtime/drivers/android/genycloud/GenycloudDrivers.test.js similarity index 100% rename from detox/src/devices/runtime/drivers/android/genycloud/GenyCloudDriver.test.js rename to detox/src/devices/runtime/drivers/android/genycloud/GenycloudDrivers.test.js diff --git a/detox/src/devices/runtime/factories/android.js b/detox/src/devices/runtime/factories/android.js index 56d183c576..9c1499eecf 100644 --- a/detox/src/devices/runtime/factories/android.js +++ b/detox/src/devices/runtime/factories/android.js @@ -53,7 +53,6 @@ class AndroidEmulator extends RuntimeDriverFactoryAndroid { _createTestAppDriver(deviceCookie, commonDeps, { deviceConfig, sessionConfig }, alias) { const fundamentalDeps = this._createFundamentalDriverDeps(commonDeps); const appDeps = this._createAppDriverDeps(fundamentalDeps, { sessionConfig }, alias); - const deps = { ...fundamentalDeps, ...appDeps, @@ -94,13 +93,30 @@ class AndroidAttached extends RuntimeDriverFactoryAndroid { } class Genycloud extends RuntimeDriverFactoryAndroid { - _createDriver(deviceCookie, deps, configs) { // eslint-disable-line no-unused-vars + _createTestAppDriver(deviceCookie, commonDeps, { sessionConfig }, alias) { + const fundamentalDeps = this._createFundamentalDriverDeps(commonDeps); + const appDeps = this._createAppDriverDeps(fundamentalDeps, { sessionConfig }, alias); + const deps = { + ...fundamentalDeps, + ...appDeps, + }; + + const props = { + instance: deviceCookie.instance, + }; + + const { GenycloudAppDriver } = require('../drivers/android/genycloud/GenycloudDrivers'); + return new GenycloudAppDriver(deps, props); + } + + _createDeviceDriver(deviceCookie, commonDeps, _configs) { + const fundamentalDeps = this._createFundamentalDriverDeps(commonDeps); const props = { instance: deviceCookie.instance, }; - const { GenycloudRuntimeDriver } = require('../drivers'); - return new GenycloudRuntimeDriver(deps, props); + const { GenycloudDeviceDriver } = require('../drivers/android/genycloud/GenycloudDrivers'); + return new GenycloudDeviceDriver(fundamentalDeps, props); } } From 47ed74f308f2c2e83fc7fb1987e06dcedebe6126 Mon Sep 17 00:00:00 2001 From: d4vidi Date: Mon, 20 Jun 2022 19:32:11 +0300 Subject: [PATCH 21/24] [wip] Skip unit tests --- scripts/ci.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/ci.sh b/scripts/ci.sh index f580d58f0d..aba256d976 100755 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -12,7 +12,7 @@ run_f "lerna bootstrap --no-ci" run_f "lerna run build" if [ "$1" == 'noGenerate' ]; then - run_f "lerna run test --ignore=generation" + run_f "lerna run test --ignore=generation --ignore=detox" else - run_f "lerna run test" + run_f "lerna run test --ignore=detox" fi From 00a152c92d24078f3ae2fbbe1a27a34439feb652 Mon Sep 17 00:00:00 2001 From: d4vidi Date: Mon, 20 Jun 2022 19:36:14 +0300 Subject: [PATCH 22/24] [wip] Fix Detox cleanup problem --- detox/src/Detox.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/detox/src/Detox.js b/detox/src/Detox.js index 25acec9512..227315ea8c 100644 --- a/detox/src/Detox.js +++ b/detox/src/Detox.js @@ -95,9 +95,9 @@ class Detox { this._artifactsManager = null; } - if (this.runtimeDevice) { + if (this._runtimeDevice) { const shutdown = this._behaviorConfig.cleanup.shutdownDevice; - await this.runtimeDevice.cleanup(); + await this._runtimeDevice.cleanup(); await this._deviceAllocator.free(this._deviceCookie, { shutdown }); } @@ -191,7 +191,7 @@ class Detox { await runtimeDevice.init(); await this._artifactsManager.onDeviceCreated(runtimeDevice); - this.runtimeDevice = runtimeDevice; + this._runtimeDevice = runtimeDevice; this.device = new DeviceAPI(runtimeDevice, this._runtimeErrorComposer); const matchers = matchersFactory.createMatchers({ From 54686ffd5505528c61d439c2e5ef0bd3114db909 Mon Sep 17 00:00:00 2001 From: d4vidi Date: Thu, 4 Aug 2022 20:14:01 +0300 Subject: [PATCH 23/24] Fix location-set in genycloud --- detox/src/DeviceAPI.js | 2 +- detox/src/devices/runtime/RuntimeDevice.js | 6 ------ detox/src/devices/runtime/TestApp.js | 6 ++++++ detox/src/devices/runtime/drivers/BaseDrivers.js | 2 +- .../runtime/drivers/android/emulator/EmulatorDriver.js | 9 +++++---- .../drivers/android/genycloud/GenycloudDrivers.js | 9 +++++---- .../devices/runtime/drivers/ios/IosSimulatorDrivers.js | 10 +++++----- detox/src/devices/runtime/factories/android.js | 8 ++++---- 8 files changed, 27 insertions(+), 25 deletions(-) diff --git a/detox/src/DeviceAPI.js b/detox/src/DeviceAPI.js index 79880ae043..0e122d5221 100644 --- a/detox/src/DeviceAPI.js +++ b/detox/src/DeviceAPI.js @@ -182,7 +182,7 @@ class DeviceAPI { } async setLocation(lat, lon) { - return this.device.setLocation(lat, lon); + return this.device.selectedApp.setLocation(lat, lon); } async reverseTcpPort(port) { diff --git a/detox/src/devices/runtime/RuntimeDevice.js b/detox/src/devices/runtime/RuntimeDevice.js index 4f75fe57b6..fe216a3cbc 100644 --- a/detox/src/devices/runtime/RuntimeDevice.js +++ b/detox/src/devices/runtime/RuntimeDevice.js @@ -149,12 +149,6 @@ class RuntimeDevice { return this._driver.resetStatusBar(params); } - async setLocation(lat, lon) { - lat = String(lat); - lon = String(lon); - await this._driver.setLocation(lat, lon); - } - async reverseTcpPort(port) { await this._driver.reverseTcpPort(port); } diff --git a/detox/src/devices/runtime/TestApp.js b/detox/src/devices/runtime/TestApp.js index 0943df9679..0bfea859c3 100644 --- a/detox/src/devices/runtime/TestApp.js +++ b/detox/src/devices/runtime/TestApp.js @@ -137,6 +137,12 @@ class RunnableTestApp extends TestApp { await this._driver.setOrientation(orientation); } + async setLocation(lat, lon) { + lat = String(lat); + lon = String(lon); + await this._driver.setLocation(lat, lon); + } + async terminate() { await traceCall('appTerminate', () => this._driver.terminate()); } diff --git a/detox/src/devices/runtime/drivers/BaseDrivers.js b/detox/src/devices/runtime/drivers/BaseDrivers.js index 0c84c5870e..32938dff23 100644 --- a/detox/src/devices/runtime/drivers/BaseDrivers.js +++ b/detox/src/devices/runtime/drivers/BaseDrivers.js @@ -48,7 +48,6 @@ class DeviceDriver { async setBiometricEnrollment() {} async setStatusBar(_params) {} async resetStatusBar() {} - async setLocation(_lat, _lon) {} async reverseTcpPort(_port) {} async unreverseTcpPort(_port) {} async clearKeychain() {} @@ -161,6 +160,7 @@ class TestAppDriver { async uninstall() {} async setOrientation(_orientation) {} + async setLocation(_lat, _lon) {} async setPermissions(_permissions) {} async sendToHome() {} async pressBack() {} diff --git a/detox/src/devices/runtime/drivers/android/emulator/EmulatorDriver.js b/detox/src/devices/runtime/drivers/android/emulator/EmulatorDriver.js index 7113260d95..d3b44e2c4d 100644 --- a/detox/src/devices/runtime/drivers/android/emulator/EmulatorDriver.js +++ b/detox/src/devices/runtime/drivers/android/emulator/EmulatorDriver.js @@ -20,10 +20,6 @@ class EmulatorDeviceDriver extends AndroidDeviceDriver { get deviceName() { return this._deviceName; } - - async setLocation(lat, lon) { - await this.adb.setLocation(this.adbName, lat, lon); - } } class EmulatorAppDriver extends AndroidAppDriver { @@ -37,6 +33,11 @@ class EmulatorAppDriver extends AndroidAppDriver { this._forceAdbInstall = forceAdbInstall; } + /** @override */ + async setLocation(lat, lon) { + await this.adb.setLocation(this.adbName, lat, lon); + } + /** @override */ async _installAppBinaries(appBinaryPath, testBinaryPath) { if (this._forceAdbInstall) { diff --git a/detox/src/devices/runtime/drivers/android/genycloud/GenycloudDrivers.js b/detox/src/devices/runtime/drivers/android/genycloud/GenycloudDrivers.js index 51b072cb06..2629dcd438 100644 --- a/detox/src/devices/runtime/drivers/android/genycloud/GenycloudDrivers.js +++ b/detox/src/devices/runtime/drivers/android/genycloud/GenycloudDrivers.js @@ -25,10 +25,6 @@ class GenycloudDeviceDriver extends AndroidDeviceDriver { get deviceName() { return this.instance.toString(); } - - async setLocation(lat, lon) { - await this.invocationManager.execute(DetoxGenymotionManager.setLocation(parseFloat(lat), parseFloat(lon))); - } } class GenycloudAppDriver extends AndroidAppDriver { @@ -36,6 +32,11 @@ class GenycloudAppDriver extends AndroidAppDriver { super(deps, { adbName: instance.adbName }); } + /** @override */ + async setLocation(lat, lon) { + await this.invocationManager.execute(DetoxGenymotionManager.setLocation(parseFloat(lat), parseFloat(lon))); + } + /** @override */ async _installAppBinaries(appBinaryPath, testBinaryPath) { await this.appInstallHelper.install(this.adbName, appBinaryPath, testBinaryPath); diff --git a/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js b/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js index 0f4ea2a9c3..05be2e1826 100644 --- a/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js +++ b/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js @@ -56,11 +56,6 @@ class IosSimulatorDeviceDriver extends IosDeviceDriver { await this._applesimutils.setBiometricEnrollment(this.udid, yesOrNo); } - /** @override */ - async setLocation(lat, lon) { - await this._applesimutils.setLocation(this.udid, lat, lon); - } - /** @override */ async clearKeychain() { await this._applesimutils.clearKeychain(this.udid); @@ -169,6 +164,11 @@ class IosSimulatorAppDriver extends IosAppDriver { await this._applesimutils.uninstall(udid, bundleId); } + /** @override */ + async setLocation(lat, lon) { + await this._applesimutils.setLocation(this.udid, lat, lon); + } + /** @override */ async setPermissions(permissions) { const { udid, bundleId } = this; diff --git a/detox/src/devices/runtime/factories/android.js b/detox/src/devices/runtime/factories/android.js index 9c1499eecf..3044051446 100644 --- a/detox/src/devices/runtime/factories/android.js +++ b/detox/src/devices/runtime/factories/android.js @@ -1,6 +1,6 @@ const RuntimeDeviceFactory = require('./base'); -class RuntimeDriverFactoryAndroid extends RuntimeDeviceFactory { +class RuntimeDeviceFactoryAndroid extends RuntimeDeviceFactory { _createFundamentalDriverDeps(commonDeps) { const serviceLocator = require('../../../servicelocator/android'); const adb = serviceLocator.adb; @@ -48,7 +48,7 @@ class RuntimeDriverFactoryAndroid extends RuntimeDeviceFactory { } } -class AndroidEmulator extends RuntimeDriverFactoryAndroid { +class AndroidEmulator extends RuntimeDeviceFactoryAndroid { /** @override */ _createTestAppDriver(deviceCookie, commonDeps, { deviceConfig, sessionConfig }, alias) { const fundamentalDeps = this._createFundamentalDriverDeps(commonDeps); @@ -81,7 +81,7 @@ class AndroidEmulator extends RuntimeDriverFactoryAndroid { } } -class AndroidAttached extends RuntimeDriverFactoryAndroid { +class AndroidAttached extends RuntimeDeviceFactoryAndroid { _createDriver(deviceCookie, deps, configs) { // eslint-disable-line no-unused-vars const props = { adbName: deviceCookie.adbName, @@ -92,7 +92,7 @@ class AndroidAttached extends RuntimeDriverFactoryAndroid { } } -class Genycloud extends RuntimeDriverFactoryAndroid { +class Genycloud extends RuntimeDeviceFactoryAndroid { _createTestAppDriver(deviceCookie, commonDeps, { sessionConfig }, alias) { const fundamentalDeps = this._createFundamentalDriverDeps(commonDeps); const appDeps = this._createAppDriverDeps(fundamentalDeps, { sessionConfig }, alias); From 7966f55eb67976e8518cb749b55eb67bd564d7da Mon Sep 17 00:00:00 2001 From: d4vidi Date: Wed, 19 Oct 2022 11:37:15 +0300 Subject: [PATCH 24/24] Align Android attached-device driver --- .../drivers/android/attached/AttachedAndroidDriver.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/detox/src/devices/runtime/drivers/android/attached/AttachedAndroidDriver.js b/detox/src/devices/runtime/drivers/android/attached/AttachedAndroidDriver.js index 3120454715..1f352e20a2 100644 --- a/detox/src/devices/runtime/drivers/android/attached/AttachedAndroidDriver.js +++ b/detox/src/devices/runtime/drivers/android/attached/AttachedAndroidDriver.js @@ -1,10 +1,15 @@ -const AndroidDriver = require('../AndroidDriver'); +// TODO (multiapps) -class AttachedAndroidDriver extends AndroidDriver { +const { AndroidDeviceDriver, AndroidAppDriver } = require('../AndroidDrivers'); + +class AttachedAndroidDriver extends AndroidDeviceDriver { /** @override */ get deviceName() { return `AttachedDevice:${this.adbName}`; } } -module.exports = AttachedAndroidDriver; +module.exports = { + AttachedAndroidDriver, + +};