diff --git a/detox/src/DetoxWorker.js b/detox/src/DetoxWorker.js index ce8ead0759..4689b0f28d 100644 --- a/detox/src/DetoxWorker.js +++ b/detox/src/DetoxWorker.js @@ -97,7 +97,7 @@ class DetoxWorker { this._deviceConfig = deviceConfig; this._sessionConfig = sessionConfig; // @ts-ignore - this._sessionConfig.sessionId = sessionConfig.sessionId || uuid.UUID(); + this._sessionConfig.sessionId = uuid.UUID(); this._runtimeErrorComposer.appsConfig = this._appsConfig; this._client = new Client(sessionConfig); diff --git a/detox/src/artifacts/CloudArtifactsManager.js b/detox/src/artifacts/CloudArtifactsManager.js new file mode 100644 index 0000000000..ba9fab3278 --- /dev/null +++ b/detox/src/artifacts/CloudArtifactsManager.js @@ -0,0 +1,76 @@ +class CloudArtifactsManager { + constructor() { + this._idlePromise = Promise.resolve(); + this._artifactPlugins = {}; + } + + async onBootDevice(deviceInfo) { + return this._idlePromise; + } + + async onBeforeLaunchApp(appLaunchInfo) { + return this._idlePromise; + } + + async onLaunchApp(appLaunchInfo) { + return this._idlePromise; + } + + async onAppReady(appInfo) { + return this._idlePromise; + } + + async onBeforeTerminateApp(appInfo) { + return this._idlePromise; + } + + async onTerminateApp(appInfo) { + return this._idlePromise; + } + + async onBeforeUninstallApp(appInfo) { + return this._idlePromise; + } + + async onBeforeShutdownDevice(deviceInfo) { + return this._idlePromise; + } + + async onShutdownDevice(deviceInfo) { + return this._idlePromise; + } + + async onCreateExternalArtifact({ pluginId, artifactName, artifactPath }) { + return this._idlePromise; + } + + async onRunDescribeStart(suite) { + return this._idlePromise; + } + + async onTestStart(testSummary) { + return this._idlePromise; + } + + async onHookFailure(testSummary) { + return this._idlePromise; + } + + async onTestFnFailure(testSummary) { + return this._idlePromise; + } + + async onTestDone(testSummary) { + return this._idlePromise; + } + + async onRunDescribeFinish(suite) { + return this._idlePromise; + } + + async onBeforeCleanup() { + return this._idlePromise; + } +} + +module.exports = CloudArtifactsManager; diff --git a/detox/src/artifacts/factories/index.js b/detox/src/artifacts/factories/index.js index 36174675ea..b7d90f1c22 100644 --- a/detox/src/artifacts/factories/index.js +++ b/detox/src/artifacts/factories/index.js @@ -1,5 +1,6 @@ // @ts-nocheck const ArtifactsManager = require('../ArtifactsManager'); +const CloudArtifactsManager = require('../CloudArtifactsManager'); const { AndroidArtifactPluginsProvider, IosArtifactPluginsProvider, @@ -51,6 +52,10 @@ class Noop extends ArtifactsManagerFactory { constructor() { super(new EmptyProvider()); } + createArtifactsManager(artifactsConfig) { + const artifactsManager = new CloudArtifactsManager(); + return artifactsManager; + } } module.exports = { diff --git a/detox/src/client/Client.js b/detox/src/client/Client.js index c472544927..88ba4141bf 100644 --- a/detox/src/client/Client.js +++ b/detox/src/client/Client.js @@ -251,6 +251,22 @@ class Client { await this.sendAction(new actions.DeliverPayload(params)); } + async waitForCloudPlatform(params) { + try { + const response = await this.sendAction(new actions.CloudPlatform(params)); + if (params['method'] == 'terminateApp') { + await this.waitUntilDisconnected(); + } + // else if (params['method'] == 'launchApp') { + // this._onAppConnected(); + // } + return response; + } catch (err) { + this._successfulTestRun = false; + throw err; + } + } + async terminateApp() { /* see the property injection from Detox.js */ } diff --git a/detox/src/client/actions/actions.js b/detox/src/client/actions/actions.js index 25db10beb5..c9b7aaaf7e 100644 --- a/detox/src/client/actions/actions.js +++ b/detox/src/client/actions/actions.js @@ -41,7 +41,7 @@ class Login extends Action { } get timeout() { - return 1000; + return 240000; } async handle(response) { @@ -179,6 +179,26 @@ class Cleanup extends Action { } } +class CloudPlatform extends Action { + constructor(params) { + super('CloudPlatform', params); + } + + get isAtomic() { + return true; + } + + get timeout() { + return 90000; + } + + async handle(response) { + this.expectResponseOfType(response, 'CloudPlatform'); + return response; + } +} + + class Invoke extends Action { constructor(params) { super('invoke', params); @@ -336,4 +356,5 @@ module.exports = { SetOrientation, SetInstrumentsRecordingState, CaptureViewHierarchy, + CloudPlatform }; diff --git a/detox/src/configuration/composeBehaviorConfig.js b/detox/src/configuration/composeBehaviorConfig.js index 409a6b0bcd..280ad992aa 100644 --- a/detox/src/configuration/composeBehaviorConfig.js +++ b/detox/src/configuration/composeBehaviorConfig.js @@ -16,8 +16,7 @@ function composeBehaviorConfig({ isCloudSession }) { if (isCloudSession) { - cliConfig.reuse = false; - cliConfig.cleanup = false; + cliConfig.reuse = true; logger.warn(`[BehaviorConfig] The 'Behaviour' config section is not supported for device type android.cloud and will be ignored.`); } return _.chain({}) @@ -28,7 +27,7 @@ function composeBehaviorConfig({ reinstallApp: cliConfig.reuse ? false : undefined, }, cleanup: { - shutdownDevice: cliConfig.cleanup ? true : undefined + shutdownDevice: isCloudSession ? false : cliConfig.cleanup ? true : undefined }, launchApp: isCloudSession ? 'auto' : undefined }, diff --git a/detox/src/configuration/composeDeviceConfig.js b/detox/src/configuration/composeDeviceConfig.js index 64e65a1187..73417fb8fc 100644 --- a/detox/src/configuration/composeDeviceConfig.js +++ b/detox/src/configuration/composeDeviceConfig.js @@ -250,7 +250,7 @@ const EXPECTED_DEVICE_MATCHER_PROPS = { 'android.attached': ['adbName'], 'android.emulator': ['avdName'], 'android.genycloud': ['recipeUUID', 'recipeName'], - 'android.cloud': ['name', 'os', 'osVersion'] + 'android.cloud': ['name', 'osVersion'] }; const KNOWN_TYPES = new Set(Object.keys(EXPECTED_DEVICE_MATCHER_PROPS)); diff --git a/detox/src/configuration/composeSessionConfig.js b/detox/src/configuration/composeSessionConfig.js index 61beba6eb0..2badf96630 100644 --- a/detox/src/configuration/composeSessionConfig.js +++ b/detox/src/configuration/composeSessionConfig.js @@ -51,19 +51,19 @@ async function composeSessionConfig(options) { if (isCloudSession) { if (session.build != null) { const value = session.build; - if (typeof value !== 'string' || value.length === 0) { + if (typeof value !== 'string') { throw errorComposer.invalidCloudSessionProperty('build'); } } if (session.project != null) { const value = session.project; - if (typeof value !== 'string' || value.length === 0) { + if (typeof value !== 'string') { throw errorComposer.invalidCloudSessionProperty('project'); } } if (session.name != null) { const value = session.name; - if (typeof value !== 'string' || value.length === 0) { + if (typeof value !== 'string') { throw errorComposer.invalidCloudSessionProperty('name'); } } diff --git a/detox/src/configuration/index.js b/detox/src/configuration/index.js index 89e003c0f9..d23fc0873c 100644 --- a/detox/src/configuration/index.js +++ b/detox/src/configuration/index.js @@ -121,7 +121,6 @@ async function composeDetoxConfig({ if (isCloudSession) { const query_param = { 'device': _.get(deviceConfig, 'device.name'), - 'os': _.get(deviceConfig, 'device.os'), 'osVersion': _.get(deviceConfig, 'device.osVersion'), 'name': _.get(sessionConfig, 'name'), 'project': _.get(sessionConfig, 'project'), diff --git a/detox/src/devices/runtime/drivers/android/cloud/cloudAndroidDriver.js b/detox/src/devices/runtime/drivers/android/cloud/cloudAndroidDriver.js new file mode 100644 index 0000000000..6083fd479a --- /dev/null +++ b/detox/src/devices/runtime/drivers/android/cloud/cloudAndroidDriver.js @@ -0,0 +1,151 @@ +/* eslint @typescript-eslint/no-unused-vars: ["error", { "args": "none" }] */ +// @ts-nocheck +const _ = require('lodash'); + +const DetoxApi = require('../../../../../android/espressoapi/Detox'); +const EspressoDetoxApi = require('../../../../../android/espressoapi/EspressoDetox'); +const UiDeviceProxy = require('../../../../../android/espressoapi/UiDeviceProxy'); +const logger = require('../../../../../utils/logger'); +const DeviceDriverBase = require('../../DeviceDriverBase'); + +const log = logger.child({ cat: 'device' }); + +/** + * @typedef { DeviceDriverDeps } CloudAndroidDriverDeps + * @property invocationManager { InvocationManager } + */ + +class CloudAndroidDriver extends DeviceDriverBase { + /** + * @param deps { CloudAndroidDriverDeps } + * @param props { CloudAndroidDriverProps } + */ + constructor(deps) { + super(deps); + + this.invocationManager = deps.invocationManager; + this.instrumentation = false; + + this.uiDevice = new UiDeviceProxy(this.invocationManager).getUIDevice(); + } + + async launchApp(bundleId, launchArgs, languageAndLocale) { + return await this._handleLaunchApp({ + manually: false, + bundleId, + launchArgs, + languageAndLocale, + }); + } + + async _handleLaunchApp({ manually, bundleId, launchArgs }) { + const response = await this._launchApp( bundleId, launchArgs); + const pid = _.get(response, 'response.success'); + return pid; + } + + async deliverPayload(params) { + if (params.delayPayload) { + return; + } + + const { url } = params; + if (url) { + await this._startActivityWithUrl(url); + } + } + + async waitUntilReady() { + try { + await super.waitUntilReady(); + } catch (e) { + log.warn({ error: e }, 'An error occurred while waiting for the app to become ready. Waiting for disconnection...'); + await this.client.waitUntilDisconnected(); + log.warn('The app disconnected.'); + throw e; + } + } + + async pressBack() { + await this.uiDevice.pressBack(); + } + + async sendToHome(params) { + await this.uiDevice.pressHome(); + } + + async terminate(bundleId) { + return await this._terminateInstrumentation(); + } + + async cleanup(bundleId) { + await super.cleanup(bundleId); + } + + getPlatform() { + return 'android'; + } + + getUiDevice() { + return this.uiDevice; + } + + async enableSynchronization() { + await this.invocationManager.execute(EspressoDetoxApi.setSynchronization(true)); + } + + async disableSynchronization() { + await this.invocationManager.execute(EspressoDetoxApi.setSynchronization(false)); + } + + async takeScreenshot(screenshotName) { + + return ''; + } + + 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); + } + + async _launchApp( bundleId, launchArgs) { + if (!this.instrumentation) { + const response = await this.invocationManager.executeCloudPlatform({ + 'method': 'launchApp', + 'args': { + 'launchArgs': launchArgs + } + }); + const status = _.get(response, 'response.success'); + this.instrumentation = status && status.toString() == 'true'; + } else if (launchArgs.detoxURLOverride) { + await this._startActivityWithUrl(launchArgs.detoxURLOverride); + } else { + await this._resumeMainActivity(); + } + } + + async _terminateInstrumentation(bundleId) { + const response = await this.invocationManager.executeCloudPlatform({ + 'method': 'terminateApp', + 'args': {} + }); + const status = _.get(response, 'response.success'); + this.instrumentation = !(status && status.toString() == 'true'); + } + + _startActivityWithUrl(url) { + return this.invocationManager.execute(DetoxApi.startActivityFromUrl(url)); + } + + _resumeMainActivity() { + return this.invocationManager.execute(DetoxApi.launchMainActivity()); + } +} + +module.exports = CloudAndroidDriver; diff --git a/detox/src/devices/runtime/factories/android.js b/detox/src/devices/runtime/factories/android.js index b666f83ca1..4d83b8cb2c 100644 --- a/detox/src/devices/runtime/factories/android.js +++ b/detox/src/devices/runtime/factories/android.js @@ -66,10 +66,10 @@ class Genycloud extends RuntimeDriverFactoryAndroid { class Noop extends RuntimeDriverFactoryAndroid { _createDriver(deviceCookie, deps, configs) { const props = { - adbName: deviceCookie.adbName, + adbName: undefined, }; - const AndroidDriver = require('../drivers/android/AndroidDriver'); - return new AndroidDriver(deps, props); + const CloudAndroidDriver = require('../drivers/android/cloud/cloudAndroidDriver'); + return new CloudAndroidDriver(deps, props); } } diff --git a/detox/src/invoke.js b/detox/src/invoke.js index 5d0b9eab51..61b302d1ae 100644 --- a/detox/src/invoke.js +++ b/detox/src/invoke.js @@ -11,6 +11,10 @@ class InvocationManager { async execute(invocation) { return await this.executionHandler.execute(invocation); } + + async executeCloudPlatform(invocation) { + return await this.executionHandler.waitForCloudPlatform(invocation); + } } module.exports = {