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/package.json b/detox/package.json index b51a95ed74..acc52ee7f0 100644 --- a/detox/package.json +++ b/detox/package.json @@ -64,6 +64,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/Detox.js b/detox/src/Detox.js index 0ec8832cf8..227315ea8c 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,15 @@ 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); - 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 +188,14 @@ class Detox { deviceConfig: this._deviceConfig, sessionConfig, }); - await this.device._prepare(); + await runtimeDevice.init(); + await this._artifactsManager.onDeviceCreated(runtimeDevice); + + 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 +207,9 @@ class Detox { }); } - await this.device.installUtilBinaries(); + await runtimeDevice.installUtilBinaries(); if (behaviorConfig.reinstallApp) { - await this._reinstallAppsOnDevice(); + await this._reinstallAppsOnDevice(runtimeDevice); } return this; @@ -241,26 +228,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(); - } - - for (const appName of appNames) { - await this.device.selectApp(appName); - 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/DeviceAPI.js b/detox/src/DeviceAPI.js new file mode 100644 index 0000000000..0e122d5221 --- /dev/null +++ b/detox/src/DeviceAPI.js @@ -0,0 +1,233 @@ +const _ = require('lodash'); + +const wrapWithStackTraceCutter = require('./utils/wrapWithStackTraceCutter'); + +class DeviceAPI { + + /** + * @param device { RuntimeDevice } + * @param errorComposer { DetoxRuntimeErrorComposer } + */ + constructor(device, errorComposer) { + // 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; + + 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' property + */ + getPlatform() { + return this.platform; + } + + get appLaunchArgs() { + 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(); + } + + 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); + } + + 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.selectedApp.captureViewHierarchy(name); + } + + async sendToHome() { + return this.device.selectedApp.sendToHome(); + } + + async pressBack() { + return this.device.selectedApp.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.selectedApp.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(); + } + + async setStatusBar(params) { + return this.device.setStatusBar(params); + } + + async resetStatusBar() { + return this.device.resetStatusBar(); + } +} + +module.exports = DeviceAPI; 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/AndroidExpect.test.js b/detox/src/android/AndroidExpect.test.js index 8ed8dfa035..63d56332f4 100644 --- a/detox/src/android/AndroidExpect.test.js +++ b/detox/src/android/AndroidExpect.test.js @@ -17,7 +17,7 @@ describe('AndroidExpect', () => { emitter = new Emitter(); device = { - _typeText: jest.fn(), + typeText: jest.fn(), }; const AndroidExpect = require('./AndroidExpect'); @@ -387,17 +387,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/NativeElement.js b/detox/src/android/core/NativeElement.js index aa75bcfb81..ae05311bc1 100644 --- a/detox/src/android/core/NativeElement.js +++ b/detox/src/android/core/NativeElement.js @@ -11,8 +11,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); @@ -24,6 +24,7 @@ class NativeElement { atIndex(index) { if (typeof index !== 'number') throw new DetoxRuntimeError({ message: `Element atIndex argument must be a number, got ${typeof index}` }); + const matcher = this._originalMatcher; this._originalMatcher._call = invoke.callDirectly(DetoxMatcherApi.matcherForAtIndex(index, matcher._call.value)); @@ -34,19 +35,19 @@ class NativeElement { async tap(value) { const action = new actions.TapAction(value); const traceDescription = actionDescription.tapAtPoint(value); - return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); + return await new ActionInteraction(this._device, this, action, traceDescription).execute(); } async tapAtPoint(value) { const action = new actions.TapAtPointAction(value); const traceDescription = actionDescription.tapAtPoint(value); - return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); + return await new ActionInteraction(this._device, this, action, traceDescription).execute(); } async longPress() { const action = new actions.LongPressAction(); const traceDescription = actionDescription.longPress(); - return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); + return await new ActionInteraction(this._device, this, action, traceDescription).execute(); } async multiTap(times) { @@ -55,61 +56,59 @@ class NativeElement { const action = new actions.MultiClickAction(times); const traceDescription = actionDescription.multiTap(times); - return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); + return await new ActionInteraction(this._device, this, action, traceDescription).execute(); } async tapBackspaceKey() { const action = new actions.PressKeyAction(67); const traceDescription = actionDescription.tapBackspaceKey(); - return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); + return await new ActionInteraction(this._device, this, action, traceDescription).execute(); } async tapReturnKey() { const action = new actions.TypeTextAction('\n'); const traceDescription = actionDescription.tapReturnKey(); - return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); + return await new ActionInteraction(this._device, this, action, traceDescription).execute(); } async typeText(value) { const action = new actions.TypeTextAction(value); const traceDescription = actionDescription.typeText(value); - return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); + return await new ActionInteraction(this._device, this, action, traceDescription).execute(); } async replaceText(value) { const action = new actions.ReplaceTextAction(value); const traceDescription = actionDescription.replaceText(value); - return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); + return await new ActionInteraction(this._device, this, action, traceDescription).execute(); } async clearText() { const action = new actions.ClearTextAction(); const traceDescription = actionDescription.clearText(); - return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); + return await new ActionInteraction(this._device, this, action, traceDescription).execute(); } async scroll(amount, direction = 'down', startPositionX, startPositionY) { const action = new actions.ScrollAmountAction(direction, amount, startPositionX, startPositionY); const traceDescription = actionDescription.scroll(amount, direction, startPositionX, startPositionY); - return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); + return await new ActionInteraction(this._device, this, action, traceDescription).execute(); } async scrollTo(edge) { - // override the user's element selection with an extended matcher that looks for UIScrollView children this._selectElementWithMatcher(this._originalMatcher._extendToDescendantScrollViews()); const action = new actions.ScrollEdgeAction(edge); const traceDescription = actionDescription.scrollTo(edge); - return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); + return await new ActionInteraction(this._device, this, action, traceDescription).execute(); } async scrollToIndex(index) { - // override the user's element selection with an extended matcher that looks for UIScrollView children this._selectElementWithMatcher(this._originalMatcher._extendToDescendantScrollViews()); const action = new actions.ScrollToIndex(index); const traceDescription = actionDescription.scrollToIndex(index); - return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); + return await new ActionInteraction(this._device, this, action, traceDescription).execute(); } /** @@ -127,14 +126,15 @@ class NativeElement { const action = new actions.SwipeAction(direction, speed, normalizedSwipeOffset, normalizedStartingPointX, normalizedStartingPointY); const traceDescription = actionDescription.swipe(direction, speed, normalizedSwipeOffset, normalizedStartingPointX, normalizedStartingPointY); - return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); + return await new ActionInteraction(this._device, this, action, traceDescription).execute(); } async takeScreenshot(screenshotName) { // TODO this should be moved to a lower-layer handler of this use-case const action = new actions.TakeElementScreenshot(); const traceDescription = actionDescription.takeScreenshot(screenshotName); - const resultBase64 = await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); + + const resultBase64 = await new ActionInteraction(this._device, this, action, traceDescription).execute(); const filePath = tempfile('detox.element-screenshot.png'); await fs.writeFile(filePath, resultBase64, 'base64'); @@ -149,14 +149,14 @@ class NativeElement { async getAttributes() { const action = new actions.GetAttributes(); const traceDescription = actionDescription.getAttributes(); - const result = await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); + const result = await new ActionInteraction(this._device, this, action, traceDescription).execute(); return JSON.parse(result); } async adjustSliderToPosition(newPosition) { const action = new actions.AdjustSliderToPosition(newPosition); const traceDescription = actionDescription.adjustSliderToPosition(newPosition); - return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); + return await new ActionInteraction(this._device, this, action, traceDescription).execute(); } } diff --git a/detox/src/android/core/NativeExpect.js b/detox/src/android/core/NativeExpect.js index e57fe599fa..00d1ea94a2 100644 --- a/detox/src/android/core/NativeExpect.js +++ b/detox/src/android/core/NativeExpect.js @@ -3,8 +3,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() { @@ -14,15 +14,15 @@ class NativeExpect { } class NativeExpectElement extends NativeExpect { - constructor(invocationManager, element) { - super(invocationManager); + constructor(device, element) { + super(device); this._element = element; } async toBeVisible(pct) { const matcher = new matchers.VisibleMatcher(pct); const traceDescription = expectDescription.toBeVisible(pct); - return await new MatcherAssertionInteraction(this._invocationManager, this._element, matcher, this._notCondition, traceDescription).execute(); + return await new MatcherAssertionInteraction(this._device, this._element, matcher, this._notCondition, traceDescription).execute(); } async toBeNotVisible() { @@ -32,7 +32,7 @@ class NativeExpectElement extends NativeExpect { async toExist() { const matcher = new matchers.ExistsMatcher(); const traceDescription = expectDescription.toExist(); - return await new MatcherAssertionInteraction(this._invocationManager, this._element, matcher, this._notCondition, traceDescription).execute(); + return await new MatcherAssertionInteraction(this._device, this._element, matcher, this._notCondition, traceDescription).execute(); } async toNotExist() { @@ -42,7 +42,7 @@ class NativeExpectElement extends NativeExpect { async toHaveText(text) { const matcher = new matchers.TextMatcher(text); const traceDescription = expectDescription.toHaveText(text); - return await new MatcherAssertionInteraction(this._invocationManager, this._element, matcher, this._notCondition, traceDescription).execute(); + return await new MatcherAssertionInteraction(this._device, this._element, matcher, this._notCondition, traceDescription).execute(); } async toNotHaveText(text) { @@ -52,7 +52,7 @@ class NativeExpectElement extends NativeExpect { async toHaveLabel(value) { const matcher = new matchers.LabelMatcher(value); const traceDescription = expectDescription.toHaveLabel(value); - return await new MatcherAssertionInteraction(this._invocationManager, this._element, matcher, this._notCondition, traceDescription).execute(); + return await new MatcherAssertionInteraction(this._device, this._element, matcher, this._notCondition, traceDescription).execute(); } async toNotHaveLabel(value) { @@ -62,7 +62,7 @@ class NativeExpectElement extends NativeExpect { async toHaveId(value) { const matcher = new matchers.IdMatcher(value); const traceDescription = expectDescription.toHaveId(value); - return await new MatcherAssertionInteraction(this._invocationManager, this._element, matcher, this._notCondition, traceDescription).execute(); + return await new MatcherAssertionInteraction(this._device, this._element, matcher, this._notCondition, traceDescription).execute(); } async toNotHaveId(value) { @@ -72,7 +72,7 @@ class NativeExpectElement extends NativeExpect { async toHaveValue(value) { const matcher = new matchers.ValueMatcher(value); const traceDescription = expectDescription.toHaveValue(value); - return await new MatcherAssertionInteraction(this._invocationManager, this._element, matcher, this._notCondition, traceDescription).execute(); + return await new MatcherAssertionInteraction(this._device, this._element, matcher, this._notCondition, traceDescription).execute(); } async toNotHaveValue(value) { @@ -82,19 +82,19 @@ class NativeExpectElement extends NativeExpect { async toHaveToggleValue(value) { const matcher = new matchers.ToggleMatcher(value); const traceDescription = expectDescription.toHaveToggleValue(value); - return await new MatcherAssertionInteraction(this._invocationManager, this._element, matcher, this._notCondition, traceDescription).execute(); + return await new MatcherAssertionInteraction(this._device, this._element, matcher, this._notCondition, traceDescription).execute(); } async toHaveSliderPosition(value, tolerance = 0) { const matcher = new matchers.SliderPositionMatcher(value, tolerance); const traceDescription = expectDescription.toHaveSliderPosition(value, tolerance); - return await new MatcherAssertionInteraction(this._invocationManager, this._element, matcher, this._notCondition, traceDescription).execute(); + return await new MatcherAssertionInteraction(this._device, this._element, matcher, this._notCondition, traceDescription).execute(); } async toBeFocused() { const matcher = new matchers.FocusMatcher(); const traceDescription = expectDescription.toBeFocused(); - return await new MatcherAssertionInteraction(this._invocationManager, this._element, matcher, this._notCondition, traceDescription).execute(); + return await new MatcherAssertionInteraction(this._device, this._element, matcher, this._notCondition, traceDescription).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..51279df2ef 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 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 2c0dd002f8..fa53d954cc 100644 --- a/detox/src/android/interactions/native.js +++ b/detox/src/android/interactions/native.js @@ -1,6 +1,5 @@ const DetoxRuntimeError = require('../../errors/DetoxRuntimeError'); const { expectDescription, actionDescription } = require('../../utils/invocationTraceDescriptions'); -const { traceInvocationCall } = require('../../utils/trace'); const { ScrollAmountStopAtEdgeAction } = require('../actions/native'); const { NativeMatcher } = require('../core/NativeMatcher'); const DetoxAssertionApi = require('../espressoapi/DetoxAssertion'); @@ -11,30 +10,32 @@ function call(maybeAFunction) { } class Interaction { - constructor(invocationManager, traceDescription) { + /** + * @param device { RuntimeDevice } + * @param traceDescription { String } + */ + constructor(device, traceDescription) { this._call = undefined; this._traceDescription = traceDescription; - this._invocationManager = invocationManager; + this._device = device; } async execute() { - return traceInvocationCall(this._traceDescription, this._call, - this._invocationManager.execute(this._call).then((resultObj) => resultObj ? resultObj.result : undefined)); + return this._device.selectedApp.invoke(this._call, this._traceDescription); } } class ActionInteraction extends Interaction { - constructor(invocationManager, element, action, traceDescription) { - super(invocationManager, traceDescription); + constructor(device, element, action, traceDescription) { + super(device, traceDescription); 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, notCondition, traceDescription) { - traceDescription = expectDescription.full(traceDescription, notCondition); - super(invocationManager, traceDescription); + constructor(device, element, matcher, notCondition, traceDescription) { + super(device, expectDescription.full(traceDescription, notCondition)); matcher = notCondition ? matcher.not : matcher; this._call = DetoxAssertionApi.assertMatcher(call(element._call), matcher._call.value); @@ -43,8 +44,8 @@ class MatcherAssertionInteraction extends Interaction { } class WaitForInteraction extends Interaction { - constructor(invocationManager, element, assertionMatcher, expectTraceDescription) { - super(invocationManager, expectTraceDescription); + constructor(device, element, assertionMatcher, expectTraceDescription) { + super(device, expectTraceDescription); this._element = element; this._assertionMatcher = assertionMatcher; this._element._selectElementWithMatcher(this._element._originalMatcher); @@ -60,13 +61,13 @@ 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, traceDescription) { - super(invocationManager, traceDescription); + constructor(device, element, matcher, searchMatcher, traceDescrption) { + super(device, traceDescrption); if (!(searchMatcher instanceof NativeMatcher)) throw new DetoxRuntimeError({ message: `WaitForActionInteraction ctor 3rd argument must be a valid NativeMatcher, got ${typeof searchMatcher}` }); @@ -77,6 +78,8 @@ class WaitForActionInteractionBase extends Interaction { } _prepare(searchAction) { + //if (!searchAction instanceof Action) throw new DetoxRuntimeError(`WaitForActionInteraction _execute argument must be a valid Action, got ${typeof searchAction}`); + this._call = DetoxAssertionApi.waitForAssertMatcherWithSearchAction( call(this._element._call), call(this._originalMatcher._call).value, diff --git a/detox/src/android/interactions/web.js b/detox/src/android/interactions/web.js index 3840c14a87..db32a26cce 100644 --- a/detox/src/android/interactions/web.js +++ b/detox/src/android/interactions/web.js @@ -1,25 +1,27 @@ 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); - return resultObj ? resultObj.result : undefined; + return this._device.selectedApp.invoke(this._call); } } 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/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 782a0c50b9..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, client }) { + createArtifactsManager(artifactsConfig, { eventEmitter }) { const artifactsManager = new ArtifactsManager(artifactsConfig); artifactsManager.subscribeToDeviceEvents(eventEmitter); - artifactsManager.registerArtifactPlugins(this._provider.declareArtifactPlugins({ client })); + artifactsManager.registerArtifactPlugins(this._provider.declareArtifactPlugins()); 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..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, client, devicePathBuilder }) { + constructor({ api, adb, devicePathBuilder }) { super({ api }); this.adb = adb; - this.client = client; this.devicePathBuilder = devicePathBuilder; } + async onDeviceCreated(device) { + this.device = device; + } + async onBeforeLaunchApp(event) { await super.onBeforeLaunchApp(event); @@ -31,7 +34,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/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 e95ad7a15c..c899bf6118 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() {} } class AndroidArtifactPluginsProvider extends ArtifactPluginsProvider { - declareArtifactPlugins({ client }) { + /** @override */ + declareArtifactPlugins() { 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, devicePathBuilder }), log: (api) => new ADBLogcatPlugin({ api, adb, devicePathBuilder }), screenshot: (api) => new ADBScreencapPlugin({ api, adb, devicePathBuilder }), video: (api) => new ADBScreenrecorderPlugin({ api, adb, devicePathBuilder }), @@ -25,19 +26,21 @@ class AndroidArtifactPluginsProvider extends ArtifactPluginsProvider { } class IosArtifactPluginsProvider extends ArtifactPluginsProvider { - declareArtifactPlugins({ client }) { + /** @override */ + 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 { - declareArtifactPlugins({ client }) { + /** @override */ + declareArtifactPlugins() { const serviceLocator = require('../../servicelocator/ios'); const appleSimUtils = serviceLocator.appleSimUtils; @@ -47,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/client/Client.js b/detox/src/client/Client.js index d42cdb0101..b9a67057d7 100644 --- a/detox/src/client/Client.js +++ b/detox/src/client/Client.js @@ -65,6 +65,10 @@ class Client { return this._serverUrl; } + get sessionId() { + return this._sessionId; + } + async open() { return this._asyncWebSocket.open(); } 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 9b0b346a5a..f4e57b2caa 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) { @@ -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(() => ''); @@ -298,19 +298,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._instrumentationRunnerForPackageId(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'); + _instrumentationRunnerForPackageId(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 173a3a2b99..07d93f552a 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 fc18d4d675..8beea88b0c 100644 --- a/detox/src/devices/runtime/RuntimeDevice.js +++ b/detox/src/devices/runtime/RuntimeDevice.js @@ -1,419 +1,182 @@ 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; - this.deviceDriver = deviceDriver; - this.deviceDriver.validateDeviceConfig(deviceConfig); this.debug = debug; } - get id() { - return this.deviceDriver.getExternalId(); - } - - get name() { - return this.deviceDriver.getDeviceName(); - } - - get type() { - return this._deviceConfig.type; - } - - get appLaunchArgs() { - return this._currentAppLaunchArgs; - } + async init() { + await this._initApps(); - async _prepare() { - const appAliases = Object.keys(this._appsConfig); + const appAliases = Object.keys(this._predefinedApps); if (appAliases.length === 1) { - await this.selectApp(appAliases[0]); + const appAlias = appAliases[0]; + await this.selectPredefinedApp(appAlias); } } - async selectApp(name) { - if (name === undefined) { - throw this._errorComposer.cantSelectEmptyApp(); - } - - if (this._currentApp) { - await this.terminateApp(); - } - - if (name === null) { // Internal use to unselect the app - this._currentApp = null; - return; - } - - const appConfig = this._appsConfig[name]; - if (!appConfig) { - throw this._errorComposer.cantFindApp(name); - } - - this._currentApp = appConfig; - this._currentAppLaunchArgs.reset(); - this._currentAppLaunchArgs.modify(this._currentApp.launchArgs); - await this._inferBundleIdFromBinary(); - } - - async launchApp(params = {}, bundleId = this._bundleId) { - return traceCall('launch app', this._doLaunchApp(params, bundleId)); + async cleanup() { + await traceCall('deviceCleanup', async () => { + await this._driver.cleanup(); + await forEachSeries(this._allRunnableApps(), (app) => app.cleanup(), this); + }); } /** - * @deprecated + * @returns { RunnableTestApp } */ - async relaunchApp(params = {}, bundleId) { - if (params.newInstance === undefined) { - params['newInstance'] = true; - } - await this.launchApp(params, bundleId); - } - - async takeScreenshot(name) { - if (!name) { - throw new DetoxRuntimeError('Cannot take a screenshot with an empty name.'); - } - - return this.deviceDriver.takeScreenshot(name); - } - - async captureViewHierarchy(name = 'capture') { - return this.deviceDriver.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(); - } - - async unmatchFace() { - await this.deviceDriver.unmatchFace(); - await this.deviceDriver.waitForActive(); + get selectedApp() { + return this._selectedApp; } - async matchFinger() { - await this.deviceDriver.matchFinger(); - await this.deviceDriver.waitForActive(); - } - - async unmatchFinger() { - await this.deviceDriver.unmatchFinger(); - await this.deviceDriver.waitForActive(); - } - - async shake() { - await this.deviceDriver.shake(); + get id() { + return this._driver.externalId; } - async terminateApp(bundleId) { - const _bundleId = bundleId || this._bundleId; - await this.deviceDriver.terminate(_bundleId); - this._processes[_bundleId] = undefined; + get name() { + return this._driver.deviceName; } - async installApp(binaryPath, testBinaryPath) { - const currentApp = binaryPath ? { binaryPath, testBinaryPath } : this._getCurrentApp(); - await traceCall('appInstall', - this.deviceDriver.installApp(currentApp.binaryPath, currentApp.testBinaryPath)); + get platform() { + return this._driver.platform; } - async uninstallApp(bundleId) { - const _bundleId = bundleId || this._bundleId; - await traceCall('appUninstall', this.deviceDriver.uninstallApp(_bundleId)); + get type() { + return this._deviceConfig.type; } - async installUtilBinaries() { - const paths = this._deviceConfig.utilBinaryPaths; - if (paths) { - await traceCall('installUtilBinaries', this.deviceDriver.installUtilBinaries(paths)); + async selectPredefinedApp(appAlias) { + const app = this._predefinedApps[appAlias]; + if (!app) { + throw this._errorComposer.cantFindApp(app); } - } - async reloadReactNative() { - await traceCall('reload React Native', this.deviceDriver.reloadReactNative()); - } - - 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"}`); + if (this._selectedApp) { + await this._selectedApp.deselect(); } - await this.deviceDriver.deliverPayload(params); + this._selectedApp = app; + await this._selectedApp.select(); } - async setOrientation(orientation) { - await this.deviceDriver.setOrientation(orientation); - } - - async setLocation(lat, lon) { - lat = String(lat); - lon = String(lon); - await this.deviceDriver.setLocation(lat, lon); - } - - async reverseTcpPort(port) { - await this.deviceDriver.reverseTcpPort(port); - } + async selectUnspecifiedApp(appConfig) { + if (this._selectedApp) { + await this._selectedApp.deselect(); + } - async unreverseTcpPort(port) { - await this.deviceDriver.unreverseTcpPort(port); + this._selectedApp = this._unspecifiedApp; + await this._selectedApp.select(appConfig); } - async clearKeychain() { - await this.deviceDriver.clearKeychain(); + async installUtilBinaries() { + await traceCall('installUtilBinaries', + forEachSeries(this._utilApps, (app) => app.install(), this)); } - async sendUserActivity(params) { - await this._sendPayload('detoxUserActivityDataURL', params); - } + async reinstallApps(appAliases) { + const selectedApp = this._selectedApp; - async sendUserNotification(params) { - await this._sendPayload('detoxUserNotificationDataURL', params); - } + for (const appAlias of appAliases) { + const app = this._predefinedApps[appAlias]; + await app.select(); + await app.uninstall(); + await app.deselect(); + } - async setURLBlacklist(urlList) { - await this.deviceDriver.setURLBlacklist(urlList); - } + for (const appAlias of appAliases) { + const app = this._predefinedApps[appAlias]; + await app.select(); + await app.install(); + await app.deselect(); + } - async enableSynchronization() { - await this.deviceDriver.enableSynchronization(); + if (selectedApp) { + await selectedApp.select(); + } } - async disableSynchronization() { - await this.deviceDriver.disableSynchronization(); + setInvokeFailuresListener(handler) { + this._allRunnableApps().forEach((app) => app.setInvokeFailuresListener(handler)); } - async resetContentAndSettings() { - await this.deviceDriver.resetContentAndSettings(); + async startInstrumentsRecording({ recordingPath, samplingInterval }) { + const promises = this._allRunnableApps().map((app) => app.startInstrumentsRecording({ recordingPath, samplingInterval })); + return Promise.all(promises); } - getPlatform() { - return this.deviceDriver.getPlatform(); + async stopInstrumentsRecording() { + const promises = this._allRunnableApps().map((app) => app.stopInstrumentsRecording()); + return Promise.all(promises); } - async _cleanup() { - const bundleId = this._currentApp && this._currentApp.bundleId; - await this.deviceDriver.cleanup(bundleId); + async takeScreenshot(name) { + if (!name) { + throw new DetoxRuntimeError({ message: 'Cannot take a screenshot with an empty name.' }); + } + return this._driver.takeScreenshot(name); } - async pressBack() { - await this.deviceDriver.pressBack(); + async setBiometricEnrollment(yesOrNo) { + await this._driver.setBiometricEnrollment(yesOrNo); } - getUiDevice() { - return this.deviceDriver.getUiDevice(); + async shake() { + await this._driver.shake(); } async setStatusBar(params) { - await this.deviceDriver.setStatusBar(params); - } - - async resetStatusBar() { - await this.deviceDriver.resetStatusBar(); + return this._driver.setStatusBar(params); } - /** - * @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 resetStatusBar(params) { + return this._driver.resetStatusBar(params); } - 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 reverseTcpPort(port) { + await this._driver.reverseTcpPort(port); } - async _sendPayload(key, params) { - const payloadFilePath = this.deviceDriver.createPayloadFile(params); - const payload = { - [key]: payloadFilePath, - }; - await this.deviceDriver.deliverPayload(payload); - this.deviceDriver.cleanupRandomDirectory(payloadFilePath); + async unreverseTcpPort(port) { + await this._driver.unreverseTcpPort(port); } - _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; + async clearKeychain() { + await this._driver.clearKeychain(); } - _isAppRunning(bundleId = this._bundleId) { - return this._processes[bundleId] != null; + async typeText(text) { + await this._driver.typeText(text); } - _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); + async _initApps() { + await forEachSeries(this._allApps(), (app) => app.init(), this); } - _prepareLaunchArgs(additionalLaunchArgs) { - return { - detoxServer: this._sessionConfig.server, - detoxSessionId: this._sessionConfig.sessionId, - ...additionalLaunchArgs - }; + _allApps() { + return [...Object.values(this._predefinedApps), this._unspecifiedApp, ...this._utilApps]; } - async _inferBundleIdFromBinary() { - const { binaryPath, bundleId } = this._currentApp; - - if (!bundleId) { - this._currentApp.bundleId = await this.deviceDriver.getBundleIdFromBinary(binaryPath); - } + _allRunnableApps() { + return [...Object.values(this._predefinedApps), this._unspecifiedApp]; } } 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..587f0f17af --- /dev/null +++ b/detox/src/devices/runtime/TestApp.js @@ -0,0 +1,281 @@ +const _ = require('lodash'); + +const DetoxRuntimeError = require('../../errors/DetoxRuntimeError'); +const { traceCall, traceInvocationCall } = 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 launchArgs() { + return this._launchArgs; + } + + 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(); + } + + get alias() { + return null; + } + + get uiDevice() { + return this._driver.uiDevice; + } + + async select() { + this._launchArgs.reset(); + this._launchArgs.modify(this.appConfig.launchArgs); + } + + async deselect() { + await this._driver.deselect(); + } + + async launch(launchInfo) { + return traceCall('launch app', () => this._launch(launchInfo)); + } + + async reloadReactNative() { + return traceCall('reload React Native', () => this._driver.reloadReactNative()); + } + + async enableSynchronization() { + await this._driver.enableSynchronization(); + } + + async disableSynchronization() { + 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` }); + } + await this._driver.openURL(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 sendToHome() { + await this._driver.sendToHome(); + } + + async pressBack() { + await this._driver.pressBack(); + } + + async setOrientation(orientation) { + 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()); + } + + 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 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 + // the platform (iOS / Android). + async invoke(action, traceDescription = 'Unspecified trace section') { + return traceInvocationCall(traceDescription, action, 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 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 _onDisconnect() { + if (this._driver.isRunning()) { + await this.terminate(); + } + } + + async _launch(launchParams) { + const passthroughParams = ['url', 'sourceApp', 'userNotification', 'userActivity', 'disableTouchIndicators', 'languageAndLocale']; + + 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 userLaunchArgs = this._mergeUserLaunchArgs(launchParams); + const launchInfo = { + ..._.pick(launchParams, passthroughParams), + userLaunchArgs, + }; + + if (this.behaviorConfig.launchApp === 'manual') { + await this._driver.waitForLaunch(launchInfo); + } else { + await this._driver.launch(launchInfo); + } + } + + _mergeUserLaunchArgs(launchInfo) { + return { + ...this._launchArgs.get(), + ...launchInfo.launchArgs, + }; + } +} + +class PredefinedTestApp extends RunnableTestApp { + constructor(driver, configs, alias) { + super(driver, configs); + + this._alias = alias; + } + + /** @override */ + get alias() { + return this._alias; + } + + async select() { + await super.select(); + await this._driver.select(this.appConfig); + } +} + +class UnspecifiedTestApp extends RunnableTestApp { + constructor(driver, { behaviorConfig }) { + super(driver, { behaviorConfig, appConfig: {} }); + } + + 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); + } +} + +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..32938dff23 --- /dev/null +++ b/detox/src/devices/runtime/drivers/BaseDrivers.js @@ -0,0 +1,246 @@ +const fs = require('fs-extra'); + +const { DetoxInternalError } = require('../../../errors'); +const tempFile = require('../../../utils/tempFile'); + +function throwNotImplemented(func) { + throw new DetoxInternalError(`Oops! Function '${func.name}' was left unimplemented!`); +} + +/** + * @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 ''; + } + + // TODO (multiapps) Where should this be called from? + validateDeviceConfig(_deviceConfig) {} + + async takeScreenshot(_screenshotName) {} + async setBiometricEnrollment() {} + async setStatusBar(_params) {} + async resetStatusBar() {} + async reverseTcpPort(_port) {} + async unreverseTcpPort(_port) {} + async clearKeychain() {} + async typeText(_text) {} + async cleanup() {} +} + +/** + * @typedef TestAppDriverDeps + * @property client { Client } + * @property invocationManager { InvocationManager } + * @property eventEmitter { AsyncEmitter } + */ + +/** + * @typedef AppInfo + * @property binaryPath { String } + */ + +/** + * @typedef { Object } LaunchArgs + * @property [detoxURLOverride] { String } + * @property [detoxUserNotificationDataURL] { Object } + * @property [detoxUserActivityDataURL] { Object } + */ + +/** + * @typedef { Object } LaunchInfo + * @property userLaunchArgs { LaunchArgs } + */ + +class TestAppDriver { + + /** + * @param deps { TestAppDriverDeps } + */ + constructor({ client, invocationManager, eventEmitter }) { + this.client = client; + this.invocationManager = invocationManager; + this.emitter = eventEmitter; + + this._pid = null; + this._appInfo = null; + } + + async init() { + await this.client.connect(); + } + + get uiDevice() { + return null; + } + + /** + * @returns {boolean} Whether the app is currently running + */ + isRunning() { + return !!this._pid; + } + + setDisconnectListener(listener) { + this.client.terminateApp = listener; + } + + /** + * @param appInfo { AppInfo } + */ + async select(appInfo) { + this._appInfo = appInfo; + } + + async deselect() { + this._appInfo = null; + } + + /** + * @param _launchInfo { LaunchInfo } + */ + async launch(_launchInfo) { throwNotImplemented(this.launch); } + + /** + * @param _launchInfo { LaunchInfo } + */ + async waitForLaunch(_launchInfo) { throwNotImplemented(this.waitForLaunch); } + + /** + * @param _params {{ url: String, sourceApp: (String|undefined) }} + */ + async openURL(_params) { throwNotImplemented(this.openURL); } + async reloadReactNative() { throwNotImplemented(this.reloadReactNative); } + async resetContentAndSettings() {} + + async sendUserActivity(payload) { + await this._sendPayload('detoxUserActivityDataURL', payload); + } + + async sendUserNotification(payload) { + await this._sendPayload('detoxUserNotificationDataURL', payload); + } + + async terminate() { + this._pid = null; + } + + async invoke(_action) { + throwNotImplemented(this.invoke); + } + + async install() {} + async uninstall() {} + + async setOrientation(_orientation) {} + async setLocation(_lat, _lon) {} + async setPermissions(_permissions) {} + async sendToHome() {} + async pressBack() {} + async matchFace() {} + async unmatchFace() {} + async matchFinger() {} + async unmatchFinger() {} + async shake() {} + async setURLBlacklist(_urlList) {} + async enableSynchronization() {} + async disableSynchronization() {} + async captureViewHierarchy(_name) {} + async cleanup() { + this.client.dumpPendingRequests(); + await this.client.cleanup(); + 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 */ + async _sendPayload(name, payload) { + const payloadFile = this._createPayloadFile(payload); + + 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 _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 }); + } +} + +module.exports = { + TestAppDriver, + DeviceDriver, +}; 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 55% rename from detox/src/devices/runtime/drivers/android/AndroidDriver.js rename to detox/src/devices/runtime/drivers/android/AndroidDrivers.js index a915663996..12040015b1 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,232 +15,383 @@ 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 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 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); + } +} + /** - * @typedef { DeviceDriverDeps } AndroidDriverDeps - * @property invocationManager { InvocationManager } + * @typedef { AppInfo } AndroidAppInfo + * @property testBinaryPath { String } + */ + +/** + * @typedef { LaunchInfo } LaunchInfoAndroid + * @property [userNotification] { Object } + */ + +/** + * @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(); - } + this._uiDevice = deps.uiDevice; + this._instrumentation = deps.instrumentation; - getExternalId() { - return this.adbName; - } + this.adbName = adbName; + this._packageId = null; - async getBundleIdFromBinary(apkPath) { - const binaryPath = getAbsoluteBinaryPath(apkPath); - return await this.aapt.getPackageName(binaryPath); + this._inferPackageIdFromApk = _.memoize(this._inferPackageIdFromApk.bind(this), (appInfo) => appInfo.binaryPath); } - async installApp(_appBinaryPath, _testBinaryPath) { - const { - appBinaryPath, - testBinaryPath, - } = this._getAppInstallPaths(_appBinaryPath, _testBinaryPath); - await this._validateAppBinaries(appBinaryPath, testBinaryPath); - await this._installAppBinaries(appBinaryPath, testBinaryPath); + /** @override */ + get uiDevice() { + return this._uiDevice; } - async uninstallApp(bundleId) { - await this.emitter.emit('beforeUninstallApp', { deviceId: this.adbName, bundleId }); - await this.appUninstallHelper.uninstall(this.adbName, bundleId); - } + /** + * @override + * @param appInfo { AndroidAppInfo } + */ + async select(appInfo) { + await super.select(appInfo); - 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); - } - } + this._packageId = await this._inferPackageIdFromApk(appInfo.binaryPath); } - async launchApp(bundleId, launchArgs, languageAndLocale) { - return await this._handleLaunchApp({ + /** + * @override + * @param launchInfo { LaunchInfoAndroid } + */ + async launch(launchInfo) { + await this._handleLaunchApp({ manually: false, - bundleId, - launchArgs, - languageAndLocale, + launchInfo, }); } - async waitForAppLaunch(bundleId, launchArgs, languageAndLocale) { - return await this._handleLaunchApp({ + /** + * @override + * @param launchInfo { LaunchInfoAndroid } + */ + async waitForLaunch(launchInfo) { + await this._handleLaunchApp({ manually: true, - bundleId, - launchArgs, - languageAndLocale, + launchInfo, }); } - async _handleLaunchApp({ manually, bundleId, launchArgs }) { - const { adbName } = this; + /** @override */ + async openURL(params) { + return this._deliverPayload(params); + } - await this.emitter.emit('beforeLaunchApp', { deviceId: adbName, bundleId, launchArgs }); + /** @override */ + async reloadReactNative() { + return this.client.reloadReactNative(); + } - launchArgs = await this._modifyArgsForNotificationHandling(adbName, bundleId, launchArgs); + /** @override */ + async terminate() { + const { adbName, _packageId } = this; - if (manually) { - await this._waitForAppLaunch(adbName, bundleId, launchArgs); - } else { - await this._launchApp(adbName, bundleId, launchArgs); - } + await this.emitter.emit('beforeTerminateApp', { deviceId: adbName, bundleId: _packageId }); + await this._terminateInstrumentation(); + await this.adb.terminate(adbName, _packageId); + await this.emitter.emit('terminateApp', { deviceId: adbName, bundleId: _packageId }); + await super.terminate(); + } - const pid = await this._waitForProcess(adbName, bundleId); - if (manually) { - log.info({}, `Found the app (${bundleId}) with process ID = ${pid}. Proceeding...`); - } + /** @override */ + async invoke(invocation) { + const resultObj = await this.invocationManager.execute(invocation); + return resultObj ? resultObj.result : undefined; + } - await this.emitter.emit('launchApp', { deviceId: adbName, bundleId, launchArgs, pid }); - return pid; + /** @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); } - async deliverPayload(params) { - if (params.delayPayload) { - return; - } + /** @override */ + async uninstall() { + const { _packageId } = this; - 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); + if (_packageId) { + await this.emitter.emit('beforeUninstallApp', { deviceId: this.adbName, bundleId: _packageId }); + await this.appUninstallHelper.uninstall(this.adbName, _packageId); } } - async waitUntilReady() { - try { - await Promise.race([ - super.waitUntilReady(), - this.instrumentation.waitForCrash() - ]); - } catch (e) { - console.warn('An error occurred while waiting for the app to become ready. Waiting for disconnection... Error:\n', e); - await this.client.waitUntilDisconnected(); - console.warn('...app disconnected.'); - throw e; - } finally { - this.instrumentation.abortWaitForCrash(); - } + /** @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); } - async pressBack() { // eslint-disable-line no-unused-vars - await this.uiDevice.pressBack(); + /** @override */ + async setURLBlacklist(urlList) { + await this.invoke(EspressoDetoxApi.setURLBlacklist(urlList)); } - async sendToHome(params) { // eslint-disable-line no-unused-vars - await this.uiDevice.pressHome(); + /** @override */ + async enableSynchronization() { + await this.invoke(EspressoDetoxApi.setSynchronization(true)); } - async typeText(text) { - await this.adb.typeText(this.adbName, text); + /** @override */ + async disableSynchronization() { + await this.invoke(EspressoDetoxApi.setSynchronization(false)); } - async terminate(bundleId) { - const { adbName } = this; - await this.emitter.emit('beforeTerminateApp', { deviceId: adbName, bundleId }); - await this._terminateInstrumentation(); - await this.adb.terminate(adbName, bundleId); - await this.emitter.emit('terminateApp', { deviceId: adbName, bundleId }); + /** @override */ + async sendToHome() { + await this.uiDevice.pressHome(); } - async cleanup(bundleId) { + /** @override */ + async pressBack() { + await this.uiDevice.pressBack(); + } + + /** @override */ + async cleanup() { await this._terminateInstrumentation(); - await super.cleanup(bundleId); } - getPlatform() { - return 'android'; + async _inferPackageIdFromApk(apkPath) { + const binaryPath = getAbsoluteBinaryPath(apkPath); + return await this.aapt.getPackageName(binaryPath); } - getUiDevice() { - return this.uiDevice; + async _handleLaunchApp({ manually, launchInfo }) { + const { adbName, _packageId } = this; + + const userLaunchArgs = { ...launchInfo.userLaunchArgs }; + const notificationLaunchArgs = await this._getNotificationLaunchArgs(launchInfo); + const sessionLaunchArgs = this._getAppSessionArgs(); + const launchArgs = { + ...userLaunchArgs, + ...notificationLaunchArgs, + ...sessionLaunchArgs, + }; + const _launchApp = (manually ? this.__waitForAppLaunch : this.__launchApp).bind(this); + + await this._notifyBeforeAppLaunch(adbName, _packageId, launchArgs); + await _launchApp(launchArgs); + + const pid = this._pid = await this._waitForProcess(); + + if (manually) { + log.info({}, `Found the app (${_packageId}) with process ID = ${pid}. Proceeding...`); + } + + await this._notifyAppLaunch(adbName, _packageId, launchArgs, pid); + await this._waitUntilReady(); + await this._notifyAppReady(adbName, _packageId, pid); } - async reverseTcpPort(port) { - await this.adb.reverse(this.adbName, port); + async __launchApp(launchArgs) { + if (!this._instrumentation.isRunning()) { + await this._launchInstrumentationProcess(launchArgs); + await sleep(500); + } else if (launchArgs.detoxURLOverride) { + await this._startActivityWithUrl(launchArgs.detoxURLOverride); + } else if (launchArgs.detoxUserNotificationDataURL) { + await this._startActivityFromNotification(launchArgs.detoxUserNotificationDataURL); + } else { + await this._resumeMainActivity(); + } } - async unreverseTcpPort(port) { - await this.adb.reverseRemove(this.adbName, port); + 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 setURLBlacklist(urlList) { - await this.invocationManager.execute(EspressoDetoxApi.setURLBlacklist(urlList)); + async _launchInstrumentationProcess(userLaunchArgs) { + const { adbName, _packageId } = this; + const serverPort = await this._reverseServerPort(adbName); + this._instrumentation.setTerminationFn(async () => { + await this._terminateInstrumentation(); + await this.adb.reverseRemove(adbName, serverPort); + }); + await this._instrumentation.launch(adbName, _packageId, userLaunchArgs); } - async enableSynchronization() { - await this.invocationManager.execute(EspressoDetoxApi.setSynchronization(true)); + /** @override */ + async _deliverPayload({ url, detoxUserNotificationDataURL }) { + if (url) { + await this._startActivityWithUrl(url); + } else if (detoxUserNotificationDataURL) { + const payloadPathOnDevice = await this._sendNotificationFileToDevice(detoxUserNotificationDataURL, this.adbName); + await this._startActivityFromNotification(payloadPathOnDevice); + } } - async disableSynchronization() { - await this.invocationManager.execute(EspressoDetoxApi.setSynchronization(false)); + _startActivityWithUrl(url) { + return this.invocationManager.execute(DetoxApi.startActivityFromUrl(url)); } - async takeScreenshot(screenshotName) { - const { adbName } = this; + _startActivityFromNotification(dataFilePath) { + return this.invocationManager.execute(DetoxApi.startActivityFromNotification(dataFilePath)); + } - const pathOnDevice = this.devicePathBuilder.buildTemporaryArtifactPath('.png'); - await this.adb.screencap(adbName, pathOnDevice); + _resumeMainActivity() { + return this.invocationManager.execute(DetoxApi.launchMainActivity()); + } - const tempPath = temporaryPath.for.png(); - await this.adb.pull(adbName, pathOnDevice, tempPath); - await this.adb.rm(adbName, pathOnDevice); + async _getNotificationLaunchArgs(launchInfo) { + const launchArgs = {}; - await this.emitter.emit('createExternalArtifact', { - pluginId: 'screenshot', - artifactName: screenshotName || path.basename(tempPath, '.png'), - artifactPath: tempPath, - }); + if (launchInfo.userNotification) { + const notificationLocalFile = this._createPayloadFile(launchInfo.userNotification); + const notificationTargetPath = await this._sendNotificationFileToDevice(notificationLocalFile.path, this.adbName); + notificationLocalFile.cleanup(); - return tempPath; + launchArgs.detoxUserNotificationDataURL = notificationTargetPath; + } + return launchArgs; } - async setOrientation(orientation) { - const orientationMapping = { - landscape: 1, // top at left side landscape - portrait: 0 // non-reversed portrait. + _getAppSessionArgs() { + return { + detoxServer: this.client.serverUrl, + detoxSessionId: this.client.sessionId, }; + } - const call = EspressoDetoxApi.changeOrientation(orientationMapping[orientation]); - await this.invocationManager.execute(call); + /** @override */ + async _waitUntilReady() { + try { + await Promise.race([ + super._waitUntilReady(), + this._instrumentation.waitForCrash() + ]); + } catch (e) { + console.warn('An error occurred while waiting for the app to become ready. Waiting for disconnection... Error:\n', e); + await this.client.waitUntilDisconnected(); + console.warn('...app disconnected.'); + throw e; + } finally { + this._instrumentation.abortWaitForCrash(); + } + } + + async _sendNotificationFileToDevice(dataFileLocalPath, adbName) { + await this.fileXfer.prepareDestinationDir(adbName); + return await this.fileXfer.send(adbName, dataFileLocalPath, 'notification.json'); + } + + async _reverseServerPort(adbName) { + const serverPort = new URL(this.client.serverUrl).port; + await this.adb.reverse(adbName, serverPort); + return serverPort; } _getAppInstallPaths(_appBinaryPath, _testBinaryPath) { @@ -253,25 +403,6 @@ class AndroidDriver extends DeviceDriverBase { }; } - 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 _installAppBinaries(appBinaryPath, testBinaryPath) { - await this.adb.install(this.adbName, appBinaryPath); - await this.adb.install(this.adbName, testBinaryPath); - } - _getTestApkPath(originalApkPath) { const testApkPath = apkUtils.getTestApkPath(originalApkPath); @@ -285,72 +416,30 @@ class AndroidDriver extends DeviceDriverBase { 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, - }; + async _validateAppBinaries(appBinaryPath, testBinaryPath) { + try { + await this.apkValidator.validateAppApk(appBinaryPath); + } catch (e) { + log.warn(e.toString()); } - return _launchArgs; - } - async _launchApp(adbName, bundleId, launchArgs) { - if (!this.instrumentation.isRunning()) { - await this._launchInstrumentationProcess(adbName, bundleId, launchArgs); - await sleep(500); - } else if (launchArgs.detoxURLOverride) { - await this._startActivityWithUrl(launchArgs.detoxURLOverride); - } else if (launchArgs.detoxUserNotificationDataURL) { - await this._startActivityFromNotification(launchArgs.detoxUserNotificationDataURL); - } else { - await this._resumeMainActivity(); + try { + await this.apkValidator.validateTestApk(testBinaryPath); + } catch (e) { + log.warn(e.toString()); } } - async _launchInstrumentationProcess(adbName, bundleId, userLaunchArgs) { - const serverPort = await this._reverseServerPort(adbName); - this.instrumentation.setTerminationFn(async () => { - await this._terminateInstrumentation(); - await this.adb.reverseRemove(adbName, serverPort); - }); - await this.instrumentation.launch(adbName, bundleId, userLaunchArgs); - } - - async _reverseServerPort(adbName) { - const serverPort = new URL(this.client.serverUrl).port; - await this.adb.reverse(adbName, serverPort); - return serverPort; - } - - async _terminateInstrumentation() { - await this.instrumentation.terminate(); - await this.instrumentation.setTerminationFn(null); - } - - 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)); - } - - _startActivityFromNotification(dataFilePath) { - return this.invocationManager.execute(DetoxApi.startActivityFromNotification(dataFilePath)); - } - - _resumeMainActivity() { - return this.invocationManager.execute(DetoxApi.launchMainActivity()); + async _installAppBinaries(appBinaryPath, testBinaryPath) { + await this.adb.install(this.adbName, appBinaryPath); + await this.adb.install(this.adbName, testBinaryPath); } - async _waitForProcess(adbName, bundleId) { + 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(); @@ -361,19 +450,17 @@ class AndroidDriver extends DeviceDriverBase { return pid; } - async _queryPID(adbName, bundleId) { - const pid = await this.adb.pidof(adbName, bundleId); + 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); + async _terminateInstrumentation() { + await this._instrumentation.terminate(); + await this._instrumentation.setTerminationFn(null); } _printInstrumentationHint({ instrumentationClass, launchArgs }) { @@ -404,4 +491,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..1f352e20a2 100644 --- a/detox/src/devices/runtime/drivers/android/attached/AttachedAndroidDriver.js +++ b/detox/src/devices/runtime/drivers/android/attached/AttachedAndroidDriver.js @@ -1,9 +1,15 @@ -const AndroidDriver = require('../AndroidDriver'); +// TODO (multiapps) -class AttachedAndroidDriver extends AndroidDriver { - getDeviceName() { +const { AndroidDeviceDriver, AndroidAppDriver } = require('../AndroidDrivers'); + +class AttachedAndroidDriver extends AndroidDeviceDriver { + /** @override */ + get deviceName() { return `AttachedDevice:${this.adbName}`; } } -module.exports = AttachedAndroidDriver; +module.exports = { + AttachedAndroidDriver, + +}; diff --git a/detox/src/devices/runtime/drivers/android/emulator/EmulatorDriver.js b/detox/src/devices/runtime/drivers/android/emulator/EmulatorDriver.js index da73dcfcf3..d3b44e2c4d 100644 --- a/detox/src/devices/runtime/drivers/android/emulator/EmulatorDriver.js +++ b/detox/src/devices/runtime/drivers/android/emulator/EmulatorDriver.js @@ -1,33 +1,44 @@ // @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; } +} +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 setLocation(lat, lon) { + await this.adb.setLocation(this.adbName, lat, lon); + } + + /** @override */ async _installAppBinaries(appBinaryPath, testBinaryPath) { if (this._forceAdbInstall) { await super._installAppBinaries(appBinaryPath, testBinaryPath); @@ -36,13 +47,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/GenycloudDrivers.js similarity index 51% rename from detox/src/devices/runtime/drivers/android/genycloud/GenyCloudDriver.js rename to detox/src/devices/runtime/drivers/android/genycloud/GenycloudDrivers.js index 92e4395705..2629dcd438 100644 --- a/detox/src/devices/runtime/drivers/android/genycloud/GenyCloudDriver.js +++ b/detox/src/devices/runtime/drivers/android/genycloud/GenycloudDrivers.js @@ -1,37 +1,49 @@ // @ts-nocheck const DetoxGenymotionManager = require('../../../../../android/espressoapi/DetoxGenymotionManager'); -const AndroidDriver = require('../AndroidDriver'); +const { AndroidDeviceDriver, AndroidAppDriver } = require('../AndroidDrivers'); /** - * @typedef { AndroidDriverDeps } 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 }); this.instance = instance; } - getDeviceName() { + /** @override */ + get deviceName() { return this.instance.toString(); } +} + +class GenycloudAppDriver extends AndroidAppDriver { + constructor(deps, { instance }) { + 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); } } -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/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..30145eda12 --- /dev/null +++ b/detox/src/devices/runtime/drivers/ios/IosDrivers.js @@ -0,0 +1,102 @@ +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 { + /** @override */ + get platform() { + return 'ios'; + } +} + +/** + * @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, + // we must terminate if we're not the selected ones. + if (this.isRunning()) { + await this.terminate(); + } + } + + /** @override */ + async openURL(params) { + return this._deliverPayload(params); + } + + /** @override */ + async reloadReactNative() { + return this.client.reloadReactNative(); + } + + /** @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 _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); + } + + async _waitForActive() { + return await this.client.waitForActive(); + } +} + + +module.exports = { + IosDeviceDriver, + IosAppDriver, +}; diff --git a/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js b/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js new file mode 100644 index 0000000000..d2052bc7bb --- /dev/null +++ b/detox/src/devices/runtime/drivers/ios/IosSimulatorDrivers.js @@ -0,0 +1,366 @@ +// @ts-nocheck +const path = require('path'); + +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 { IosDeviceDriver, IosAppDriver } = require('./IosDrivers'); + +/** + * @typedef SimulatorDriverDeps + * @property simulatorLauncher { SimulatorLauncher } + * @property applesimutils { AppleSimUtils } + */ + +/** + * @typedef SimulatorDriverProps + * @property udid { String } The unique cross-OS identifier of the simulator + * @property type { String } + * @property bootArgs { Object } + */ + +class IosSimulatorDeviceDriver extends IosDeviceDriver { + /** + * @param deps { SimulatorDriverDeps } + * @param props { SimulatorDriverProps } + */ + constructor(deps, { udid, type, bootArgs }) { + super(deps); + + this.udid = udid; + this._type = type; + this._bootArgs = bootArgs; + this._deviceName = `${udid} (${this._type})`; + + this._simulatorLauncher = deps.simulatorLauncher; + this._applesimutils = deps.applesimutils; + } + + /** @override */ + get externalId() { + return this.udid; + } + + /** @override */ + get deviceName() { + return this._deviceName; + } + + /** @override */ + async setBiometricEnrollment(yesOrNo) { + await this._applesimutils.setBiometricEnrollment(this.udid, yesOrNo); + } + + /** @override */ + async clearKeychain() { + await this._applesimutils.clearKeychain(this.udid); + } + + /** @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); + } +} + +/** + * @typedef { LaunchInfo } LaunchInfoIosSim + * @property [url] { String } + * @property [sourceApp] { String } + * @property [userNotification] { Object } + * @property [userActivity] { Object } + * @property [disableTouchIndicators] { Boolean } + * @property [languageAndLocale] { String } + */ + +/** + * @typedef { TestAppDriverDeps } IosSimulatorAppDriverDeps + * @property applesimutils { AppleSimUtils } + */ + +class IosSimulatorAppDriver extends IosAppDriver { + + /** + * @param deps { IosSimulatorAppDriverDeps } + * @param props {{ udid: String }} + */ + constructor(deps, { udid }) { + super(deps); + this.udid = udid; + + this._applesimutils = deps.applesimutils; + } + + /** + * @override + * @param launchInfo { LaunchInfoIosSim } + */ + async launch(launchInfo) { + await this._handleLaunchApp({ manually: false, launchInfo }); + } + + /** + * @override + * @param launchInfo { LaunchInfoIosSim } + */ + async waitForLaunch(launchInfo) { + await this._handleLaunchApp({ manually: true, launchInfo }); + } + + /** @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)); + } + + /** @override */ + async uninstall() { + const { udid, bundleId } = this; + await this.emitter.emit('beforeUninstallApp', { deviceId: udid, bundleId }); + 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; + await this._applesimutils.setPermissions(udid, bundleId, permissions); + } + + /** @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(); + } + + /** @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(); + await this.client.captureViewHierarchy({ viewHierarchyURL }); + + await this.emitter.emit('createExternalArtifact', { + pluginId: 'uiHierarchy', + artifactName: artifactName, + artifactPath: viewHierarchyURL, + }); + + return viewHierarchyURL; + } + + async _waitUntilReady() { + return super._waitUntilReady(); // Just for clarity + } + + async _waitForActive() { + return this.client.waitForActive(); + } + + async _waitForBackground() { + return await this.client.waitForBackground(); + } + + async _handleLaunchApp({ manually, launchInfo }) { + const { udid, bundleId } = this; + + const fundamentalLaunchArgs = this._getFundamentalLaunchArgs(launchInfo); + const payloadLaunchArgsHandle = await this._getPayloadFileOrUrlLaunchArgs(launchInfo); + const sessionLaunchArgs = this._getAppSessionArgs(); + const launchArgs = { + ...fundamentalLaunchArgs, + ...payloadLaunchArgsHandle.args, + ...sessionLaunchArgs, + }; + + await this._notifyBeforeAppLaunch(udid, bundleId, launchArgs); + + const _launchApp = (manually ? this.__waitForAppLaunch : this.__launchApp).bind(this); + const pid = this._pid = await _launchApp(launchArgs, launchInfo.languageAndLocale); + + payloadLaunchArgsHandle.cleanup(); + + await this._notifyAppLaunch(udid, bundleId, launchArgs, pid); + 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; + + const pid = await this._applesimutils.launch(udid, bundleId, launchArgs, languageAndLocale); + return pid; + } + + _getFundamentalLaunchArgs(launchInfo) { + const launchArgs = { + ...launchInfo.userLaunchArgs, + }; + + if (launchInfo.disableTouchIndicators) { + launchArgs.detoxDisableTouchIndicators = true; + } + return 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) { + // TODO Are 'url' and 'detoxURLOverride' both required? + args.url = launchInfo.url; + args.detoxURLOverride = launchInfo.url; + + if (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) { + await deliverPayloadAndSetupLaunchArg('detoxUserNotificationDataURL', launchInfo.userNotification); + } else if (launchInfo.userActivity) { + await deliverPayloadAndSetupLaunchArg('detoxUserActivityDataURL', launchInfo.userActivity); + } + + return { + args, + cleanup, + }; + } + + _getAppSessionArgs() { + return { + detoxServer: this.client.serverUrl, + detoxSessionId: this.client.sessionId, + }; + } +} + +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/drivers/ios/SimulatorDriver.js b/detox/src/devices/runtime/drivers/ios/SimulatorDriver.js deleted file mode 100644 index 878d483869..0000000000 --- a/detox/src/devices/runtime/drivers/ios/SimulatorDriver.js +++ /dev/null @@ -1,204 +0,0 @@ -// @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 IosDriver = require('./IosDriver'); - -/** - * @typedef SimulatorDriverDeps { DeviceDriverDeps } - * @property simulatorLauncher { SimulatorLauncher } - * @property applesimutils { AppleSimUtils } - */ - -/** - * @typedef SimulatorDriverProps - * @property udid { String } The unique cross-OS identifier of the simulator - * @property type { String } - * @property bootArgs { Object } - */ - -class SimulatorDriver extends IosDriver { - /** - * @param deps { SimulatorDriverDeps } - * @param props { SimulatorDriverProps } - */ - constructor(deps, { udid, type, bootArgs }) { - super(deps); - - this.udid = udid; - this._type = type; - this._bootArgs = bootArgs; - this._deviceName = `${udid} (${this._type})`; - this._simulatorLauncher = deps.simulatorLauncher; - this._applesimutils = deps.applesimutils; - } - - getExternalId() { - return this.udid; - } - - getDeviceName() { - 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}`); - } - } - - async installApp(binaryPath) { - await this._applesimutils.install(this.udid, getAbsoluteBinaryPath(binaryPath)); - } - - async uninstallApp(bundleId) { - const { udid } = this; - await this.emitter.emit('beforeUninstallApp', { deviceId: udid, bundleId }); - await this._applesimutils.uninstall(udid, bundleId); - } - - async launchApp(bundleId, launchArgs, languageAndLocale) { - const { udid } = 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; - } - - async waitForAppLaunch(bundleId, launchArgs, languageAndLocale) { - const { udid } = this; - - await this.emitter.emit('beforeLaunchApp', { bundleId, deviceId: udid, launchArgs }); - - 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...`); - } - - await this.emitter.emit('launchApp', { bundleId, deviceId: udid, launchArgs, pid }); - 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 }); - } - - async setBiometricEnrollment(yesOrNo) { - await this._applesimutils.setBiometricEnrollment(this.udid, yesOrNo); - } - - async matchFace() { - await this._applesimutils.matchBiometric(this.udid, 'Face'); - } - - async unmatchFace() { - await this._applesimutils.unmatchBiometric(this.udid, 'Face'); - } - - async matchFinger() { - await this._applesimutils.matchBiometric(this.udid, 'Finger'); - } - - async unmatchFinger() { - await this._applesimutils.unmatchBiometric(this.udid, 'Finger'); - } - - 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; - } - - async captureViewHierarchy(artifactName) { - const viewHierarchyURL = temporaryPath.for.viewhierarchy(); - await this.client.captureViewHierarchy({ viewHierarchyURL }); - - await this.emitter.emit('createExternalArtifact', { - pluginId: 'uiHierarchy', - artifactName: artifactName, - artifactPath: viewHierarchyURL, - }); - - return viewHierarchyURL; - } - - async setStatusBar(flags) { - await this._applesimutils.statusBarOverride(this.udid, flags); - } - - async resetStatusBar() { - await this._applesimutils.statusBarReset(this.udid); - } -} - -module.exports = SimulatorDriver; diff --git a/detox/src/devices/runtime/factories/android.js b/detox/src/devices/runtime/factories/android.js index e025ecf4c2..3044051446 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) { +class RuntimeDeviceFactoryAndroid extends RuntimeDeviceFactory { + _createFundamentalDriverDeps(commonDeps) { const serviceLocator = require('../../../servicelocator/android'); const adb = serviceLocator.adb; const aapt = serviceLocator.aapt; @@ -25,22 +25,63 @@ 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, + }; + } } -class AndroidEmulator extends RuntimeDriverFactoryAndroid { - _createDriver(deviceCookie, deps, { deviceConfig }) { +class AndroidEmulator extends RuntimeDeviceFactoryAndroid { + /** @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); } } -class AndroidAttached extends RuntimeDriverFactoryAndroid { +class AndroidAttached extends RuntimeDeviceFactoryAndroid { _createDriver(deviceCookie, deps, configs) { // eslint-disable-line no-unused-vars const props = { adbName: deviceCookie.adbName, @@ -51,14 +92,31 @@ class AndroidAttached extends RuntimeDriverFactoryAndroid { } } -class Genycloud extends RuntimeDriverFactoryAndroid { - _createDriver(deviceCookie, deps, configs) { // eslint-disable-line no-unused-vars +class Genycloud extends RuntimeDeviceFactoryAndroid { + _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); } } diff --git a/detox/src/devices/runtime/factories/base.js b/detox/src/devices/runtime/factories/base.js index 626b736054..9e0ede7b02 100644 --- a/detox/src/devices/runtime/factories/base.js +++ b/detox/src/devices/runtime/factories/base.js @@ -1,14 +1,65 @@ +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 }); + }); + } + + /** @protected */ + _createAppSessionConfig(sessionConfig, alias) { + const { sessionId } = sessionConfig; + + if (alias) { + return { + ...sessionConfig, + sessionId: `${sessionId}:${alias}`, + }; + } + return sessionConfig; } - _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/devices/runtime/factories/ios.js b/detox/src/devices/runtime/factories/ios.js index d606c94d92..1fefc18465 100644 --- a/detox/src/devices/runtime/factories/ios.js +++ b/detox/src/devices/runtime/factories/ios.js @@ -1,37 +1,88 @@ const RuntimeDeviceFactory = require('./base'); class RuntimeDriverFactoryIos extends RuntimeDeviceFactory { - _createDriverDependencies(commonDeps) { - const serviceLocator = require('../../../servicelocator/ios'); - const applesimutils = serviceLocator.appleSimUtils; - const { eventEmitter } = commonDeps; + _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 client = new Client(appSessionConfig); + + const { InvocationManager } = require('../../../invoke'); + const invocationManager = new InvocationManager(client); - const SimulatorLauncher = require('../../allocation/drivers/ios/SimulatorLauncher'); 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 }, alias); + 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, { sessionConfig }, alias) { + const simulatorDeps = this.__createIosSimulatorDriverDeps(commonDeps); + const appDeps = this._createAppDriverDeps({ sessionConfig }, alias); + 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 }), + }; } } 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) { diff --git a/detox/src/ios/expectTwo.js b/detox/src/ios/expectTwo.js index 93f7b198dc..ac2fb0548d 100644 --- a/detox/src/ios/expectTwo.js +++ b/detox/src/ios/expectTwo.js @@ -8,14 +8,17 @@ const tempfile = require('tempfile'); const { assertEnum, assertNormalized } = require('../utils/assertArgument'); const { actionDescription, expectDescription } = require('../utils/invocationTraceDescriptions'); -const { traceInvocationCall } = require('../utils/trace'); 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 = []; } @@ -119,7 +122,7 @@ class Expect { const invocation = this.createInvocation(expectation, ...params); traceDescription = expectDescription.full(traceDescription, this.modifiers.includes('not')); - return _executeInvocation(this._invocationManager, invocation, traceDescription); + return this._device.selectedApp.invoke(invocation, traceDescription); } } @@ -130,8 +133,14 @@ class InternalExpect extends Expect { } 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; @@ -342,12 +351,12 @@ class Element { withAction(action, traceDescription, ...params) { const invocation = this.createInvocation(action, null, ...params); - return _executeInvocation(this._invocationManager, invocation, traceDescription); + return this._device.selectedApp.invoke(invocation, traceDescription); } withActionAndTargetElement(action, targetElement, traceDescription, ...params) { const invocation = this.createInvocation(action, targetElement, ...params); - return _executeInvocation(this._invocationManager, invocation, traceDescription); + return this._device.selectedApp.invoke(invocation, traceDescription); } } @@ -392,6 +401,7 @@ class By { } class Matcher { + accessibilityLabel(label) { return this.label(label); } @@ -465,10 +475,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; } @@ -548,7 +563,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; } @@ -656,16 +671,7 @@ class WaitFor { const invocation = this.createWaitForWithActionInvocation(expectation, action); const traceDescription = expectDescription.waitFor(actionTraceDescription); - return _executeInvocation(this._invocationManager, invocation, traceDescription); - } - - createWaitForWithActionInvocation(expectation, action) { - return { - ...action, - while: { - ...expectation - } - }; + return this._device.selectedApp.invoke(invocation, traceDescription); } waitForWithTimeout(expectTraceDescription) { @@ -673,10 +679,9 @@ class WaitFor { const action = this.action; const timeout = this.timeout; - const invocation = this.createWaitForWithTimeoutInvocation(expectation, action, timeout); - const traceDescription = expectDescription.waitForWithTimeout(expectTraceDescription, timeout); - return _executeInvocation(this._invocationManager, invocation, traceDescription); + const invocation = this.createWaitForWithTimeoutInvocation(expectation, action, timeout); + return this._device.selectedApp.invoke(invocation, traceDescription); } createWaitForWithTimeoutInvocation(expectation, action, timeout) { @@ -688,31 +693,35 @@ class WaitFor { } } -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); @@ -722,15 +731,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) { @@ -746,8 +755,4 @@ function throwElementError(param) { throw new Error(`${param} is not a Detox element. More about Detox elements here: https://wix.github.io/Detox/docs/api/matchers`); } -function _executeInvocation(invocationManager, invocation, traceDescription) { - return traceInvocationCall(traceDescription, invocation, invocationManager.execute(invocation)); -} - module.exports = IosExpect; diff --git a/detox/src/matchers/factories/index.js b/detox/src/matchers/factories/index.js index a756c9f39e..dcc0dee4b0 100644 --- a/detox/src/matchers/factories/index.js +++ b/detox/src/matchers/factories/index.js @@ -5,16 +5,16 @@ 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 }); } } 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 }); } } 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 } 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