UNPKG

detox

Version:

E2E tests and automation for mobile

436 lines (356 loc) 12.3 kB
const DetoxRuntimeError = require('../../errors/DetoxRuntimeError'); const debug = require('../../utils/debug'); // debug utils, leave here even if unused const log = require('../../utils/logger').child({ cat: 'device' }); const mapDeviceLongPressArguments = require('../../utils/mapDeviceLongPressArguments'); const traceMethods = require('../../utils/traceMethods'); const wrapWithStackTraceCutter = require('../../utils/wrapWithStackTraceCutter'); const LaunchArgsEditor = require('./utils/LaunchArgsEditor'); class RuntimeDevice { constructor({ appsConfig, behaviorConfig, deviceConfig, eventEmitter, sessionConfig, runtimeErrorComposer, }, deviceDriver) { const methodNames = [ 'captureViewHierarchy', 'clearKeychain', 'disableSynchronization', 'enableSynchronization', 'generateViewHierarchyXml', 'installApp', 'installUtilBinaries', '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', ]; traceMethods(log, this, methodNames); wrapWithStackTraceCutter(this, methodNames); this._appsConfig = appsConfig; this._behaviorConfig = behaviorConfig; this._deviceConfig = deviceConfig; this._sessionConfig = sessionConfig; this._emitter = eventEmitter; this._errorComposer = runtimeErrorComposer; this._currentApp = null; this._currentAppLaunchArgs = new LaunchArgsEditor(); this._processes = {}; 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 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) { 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 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 generateViewHierarchyXml(shouldInjectTestIds = false) { return await this.deviceDriver.generateViewHierarchyXml(shouldInjectTestIds); } 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(); } 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(); } async terminateApp(bundleId) { const _bundleId = bundleId || this._bundleId; await this.deviceDriver.terminate(_bundleId); this._processes[_bundleId] = undefined; } async installApp(binaryPath, testBinaryPath) { const currentApp = binaryPath ? { binaryPath, testBinaryPath } : this._getCurrentApp(); await this.deviceDriver.installApp(currentApp.binaryPath, currentApp.testBinaryPath); // This abstraction leaks because our drivers themselves leak, // so don't blame me - DeviceBaseDriver itself has `reverseTcpPort`, // setting a vicious downward spiral. I can't refactor everything // in a single pull request, so let's bear with it for now. if (Array.isArray(currentApp.reversePorts)) { for (const port of currentApp.reversePorts) { await this.reverseTcpPort(port); } } } async uninstallApp(bundleId) { const _bundleId = bundleId || this._bundleId; await this.deviceDriver.uninstallApp(_bundleId); } async installUtilBinaries() { const paths = this._deviceConfig.utilBinaryPaths; if (paths) { await this.deviceDriver.installUtilBinaries(paths); } } async reloadReactNative() { await 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"}`); } await this.deviceDriver.deliverPayload(params); } async setOrientation(orientation) { await this.deviceDriver.setOrientation(orientation); } async tap(point, shouldIgnoreStatusBar) { await this.deviceDriver.tap(point, shouldIgnoreStatusBar, this._bundleId); } async longPress(arg1, arg2, arg3) { let { point, duration, shouldIgnoreStatusBar } = mapDeviceLongPressArguments(arg1, arg2, arg3); await this.deviceDriver.longPress(point, duration, shouldIgnoreStatusBar, this._bundleId); } 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 unreverseTcpPort(port) { await this.deviceDriver.unreverseTcpPort(port); } async clearKeychain() { await this.deviceDriver.clearKeychain(); } async sendUserActivity(params) { await this._sendPayload('detoxUserActivityDataURL', params); } async sendUserNotification(params) { await this._sendPayload('detoxUserNotificationDataURL', params); } async setURLBlacklist(urlList) { await this.deviceDriver.setURLBlacklist(urlList); } async enableSynchronization() { await this.deviceDriver.enableSynchronization(); } async disableSynchronization() { await this.deviceDriver.disableSynchronization(); } async resetContentAndSettings() { await this.deviceDriver.resetContentAndSettings(); } getPlatform() { return this.deviceDriver.getPlatform(); } async _cleanup() { const bundleId = this._currentApp && this._currentApp.bundleId; await this.deviceDriver.cleanup(bundleId); } async pressBack() { await this.deviceDriver.pressBack(); } getUiDevice() { return this.deviceDriver.getUiDevice(); } async setStatusBar(params) { await this.deviceDriver.setStatusBar(params); } async resetStatusBar() { await this.deviceDriver.resetStatusBar(); } /** * @internal */ async _typeText(text) { await this.deviceDriver.typeText(text); } get _bundleId() { return this._getCurrentApp().bundleId; } _getCurrentApp() { if (!this._currentApp) { throw this._errorComposer.appNotSelected(); } return this._currentApp; } async _sendPayload(key, params) { const payloadFilePath = this.deviceDriver.createPayloadFile(params); const payload = { [key]: payloadFilePath, }; await this.deviceDriver.deliverPayload(payload); this.deviceDriver.cleanupRandomDirectory(payloadFilePath); } _createPayloadFileAndUpdatesParamsObject(key, launchKey, params, baseLaunchArgs) { const payloadFilePath = this.deviceDriver.createPayloadFile(params[key]); baseLaunchArgs[launchKey] = payloadFilePath; //`params` will be used later for `predeliverPayload`, so remove the actual notification and add the file URL delete params[key]; params[launchKey] = payloadFilePath; } _isAppRunning(bundleId = this._bundleId) { return this._processes[bundleId] != null; } _assertHasSingleParam(singleParams, params) { let paramsCounter = 0; singleParams.forEach((item) => { if(params[item]) { paramsCounter += 1; } }); if (paramsCounter > 1) { throw new DetoxRuntimeError(`Call to 'launchApp(${JSON.stringify(params)})' must contain only one of ${JSON.stringify(singleParams)}.`); } return (paramsCounter === 1); } _prepareLaunchArgs(additionalLaunchArgs) { return { detoxServer: this._sessionConfig.server, detoxSessionId: this._sessionConfig.sessionId, ...additionalLaunchArgs }; } async _inferBundleIdFromBinary() { const { binaryPath, bundleId } = this._currentApp; if (!bundleId) { this._currentApp.bundleId = await this.deviceDriver.getBundleIdFromBinary(binaryPath); } } } module.exports = RuntimeDevice;