diff --git a/detox/src/artifacts/instruments/android/AndroidInstrumentsRecording.js b/detox/src/artifacts/instruments/android/AndroidInstrumentsRecording.js index e0e629679c..38b6a2fcf0 100644 --- a/detox/src/artifacts/instruments/android/AndroidInstrumentsRecording.js +++ b/detox/src/artifacts/instruments/android/AndroidInstrumentsRecording.js @@ -4,18 +4,17 @@ const InstrumentsArtifactRecording = require('../InstrumentsArtifactRecording'); class AndroidInstrumentsRecording extends InstrumentsArtifactRecording { constructor({ adb, pluginContext, client, deviceId, userConfig, temporaryRecordingPath }) { super({ pluginContext, client, userConfig, temporaryRecordingPath }); - this.adb = adb; - this.deviceId = deviceId; + this.adb = adb.bind({ deviceId }); } async doSave(artifactPath) { await super.doSave(artifactPath); - await this.adb.pull(this.deviceId, this.temporaryRecordingPath, artifactPath); - await this.adb.rm(this.deviceId, this.temporaryRecordingPath, true); + await this.adb.pull(this.temporaryRecordingPath, artifactPath); + await this.adb.rm(this.temporaryRecordingPath, true); } async doDiscard() { - await this.adb.rm(this.deviceId, this.temporaryRecordingPath, true); + await this.adb.rm(this.temporaryRecordingPath, true); } } diff --git a/detox/src/artifacts/log/android/ADBLogcatPlugin.js b/detox/src/artifacts/log/android/ADBLogcatPlugin.js index e80b37ea04..dfe1184556 100644 --- a/detox/src/artifacts/log/android/ADBLogcatPlugin.js +++ b/detox/src/artifacts/log/android/ADBLogcatPlugin.js @@ -14,7 +14,7 @@ class ADBLogcatPlugin extends LogArtifactPlugin { async onBeforeLaunchApp(event) { await super.onBeforeLaunchApp(event); - this._lastTimestamp = await this._adb.now(event.deviceId); + this._lastTimestamp = await this._adb.bind({ deviceId: event.deviceId }).now(); } async onLaunchApp(event) { diff --git a/detox/src/artifacts/log/android/ADBLogcatRecording.js b/detox/src/artifacts/log/android/ADBLogcatRecording.js index 72022c87cf..be732d1fde 100644 --- a/detox/src/artifacts/log/android/ADBLogcatRecording.js +++ b/detox/src/artifacts/log/android/ADBLogcatRecording.js @@ -13,7 +13,7 @@ class ADBLogcatRecording extends Artifact { pathToLogOnDevice, }) { super(); - this.adb = adb; + this.adb = adb.bind({ deviceId }); this.deviceId = deviceId; this.pid = pid; @@ -28,7 +28,7 @@ class ADBLogcatRecording extends Artifact { async doStart() { const pid = this.pid.get(); - this.processPromise = this.adb.logcat(this.deviceId, { + this.processPromise = this.adb.logcat({ file: this.pathToLogOnDevice, time: this.since.get(), pid: pid > 0 ? pid : 0, @@ -42,7 +42,7 @@ class ADBLogcatRecording extends Artifact { async doStop() { try { await this._waitUntilLogFileIsCreated; - this.since.set(await this.adb.now(this.deviceId)); + this.since.set(await this.adb.now()); } finally { if (this.processPromise) { await interruptProcess(this.processPromise); @@ -52,16 +52,16 @@ class ADBLogcatRecording extends Artifact { } async doSave(artifactPath) { - await this.adb.pull(this.deviceId, this.pathToLogOnDevice, artifactPath); - await this.adb.rm(this.deviceId, this.pathToLogOnDevice); + await this.adb.pull(this.pathToLogOnDevice, artifactPath); + await this.adb.rm(this.pathToLogOnDevice); } async doDiscard() { - await this.adb.rm(this.deviceId, this.pathToLogOnDevice); + await this.adb.rm(this.pathToLogOnDevice); } async _assertLogIsCreated() { - const size = await this.adb.getFileSize(this.deviceId, this.pathToLogOnDevice); + const size = await this.adb.getFileSize(this.pathToLogOnDevice); if (size < 0) { throw new DetoxRuntimeError({ diff --git a/detox/src/artifacts/screenshot/ADBScreencapPlugin.js b/detox/src/artifacts/screenshot/ADBScreencapPlugin.js index 19b59d787f..c9adb268d7 100644 --- a/detox/src/artifacts/screenshot/ADBScreencapPlugin.js +++ b/detox/src/artifacts/screenshot/ADBScreencapPlugin.js @@ -11,27 +11,27 @@ class ADBScreencapPlugin extends ScreenshotArtifactPlugin { } createTestArtifact() { - const adb = this._adb; const deviceId = this.context.deviceId; + const adb = this._adb.bind({ deviceId }); const pathToScreenshotOnDevice = this._devicePathBuilder.buildTemporaryArtifactPath('.png'); return new Artifact({ name: 'ADBScreencapRecording', async start() { - await adb.screencap(deviceId, pathToScreenshotOnDevice); + await adb.screencap(pathToScreenshotOnDevice); }, async save(artifactPath) { - await adb.pull(deviceId, pathToScreenshotOnDevice, artifactPath); - await adb.rm(deviceId, pathToScreenshotOnDevice); + await adb.pull(pathToScreenshotOnDevice, artifactPath); + await adb.rm(pathToScreenshotOnDevice); }, async discard() { - await adb.rm(deviceId, pathToScreenshotOnDevice); + await adb.rm(pathToScreenshotOnDevice); }, }); } } -module.exports = ADBScreencapPlugin; \ No newline at end of file +module.exports = ADBScreencapPlugin; diff --git a/detox/src/artifacts/video/ADBScreenrecorderArtifact.js b/detox/src/artifacts/video/ADBScreenrecorderArtifact.js index c5e6b2cc57..fed81903b9 100644 --- a/detox/src/artifacts/video/ADBScreenrecorderArtifact.js +++ b/detox/src/artifacts/video/ADBScreenrecorderArtifact.js @@ -8,7 +8,7 @@ class ADBVideoRecording extends Artifact { constructor(config) { super(config); - this.adb = config.adb; + this.adb = config.adb.bind({ deviceId: config.deviceId }); this.deviceId = config.deviceId; this.pathToVideoOnDevice = config.pathToVideoOnDevice; this.screenRecordOptions = config.screenRecordOptions || {}; @@ -18,7 +18,7 @@ class ADBVideoRecording extends Artifact { } async doStart() { - this.processPromise = this.adb.screenrecord(this.deviceId, { + this.processPromise = this.adb.screenrecord({ ...this.screenRecordOptions, path: this.pathToVideoOnDevice }); @@ -40,17 +40,17 @@ class ADBVideoRecording extends Artifact { async doSave(artifactPath) { await this._waitWhileVideoIsBusy; - await this.adb.pull(this.deviceId, this.pathToVideoOnDevice, artifactPath); - await this.adb.rm(this.deviceId, this.pathToVideoOnDevice); + await this.adb.pull(this.pathToVideoOnDevice, artifactPath); + await this.adb.rm(this.pathToVideoOnDevice); } async doDiscard() { await this._waitWhileVideoIsBusy; - await this.adb.rm(this.deviceId, this.pathToVideoOnDevice); + await this.adb.rm(this.pathToVideoOnDevice); } async _assertVideoIsBeingRecorded() { - const size = await this.adb.getFileSize(this.deviceId, this.pathToVideoOnDevice); + const size = await this.adb.getFileSize(this.pathToVideoOnDevice); if (size < 1) { throw new DetoxRuntimeError({ @@ -60,7 +60,7 @@ class ADBVideoRecording extends Artifact { } async _assertVideoIsNotOpenedByProcesses() { - const size = await this.adb.getFileSize(this.deviceId, this.pathToVideoOnDevice); + const size = await this.adb.getFileSize(this.pathToVideoOnDevice); if (size < 1) { throw new DetoxRuntimeError({ diff --git a/detox/src/devices/allocation/drivers/android/genycloud/services/GenyInstanceLifecycleService.js b/detox/src/devices/allocation/drivers/android/genycloud/services/GenyInstanceLifecycleService.js index 2fd91b00e1..35a842354e 100644 --- a/detox/src/devices/allocation/drivers/android/genycloud/services/GenyInstanceLifecycleService.js +++ b/detox/src/devices/allocation/drivers/android/genycloud/services/GenyInstanceLifecycleService.js @@ -36,7 +36,9 @@ class GenyInstanceLifecycleService { }; const result = await retry(options, doAdbConnect); - return new Instance(result.instance); + const instance = new Instance(result.instance); + this._adb = this._adb.bind({ deviceId: instance.adbName }); + return instance; } async deleteInstance(instanceUUID) { diff --git a/detox/src/devices/common/drivers/android/exec/ADB.js b/detox/src/devices/common/drivers/android/exec/ADB.js index ab269f5ba3..07b6b9f61c 100644 --- a/detox/src/devices/common/drivers/android/exec/ADB.js +++ b/detox/src/devices/common/drivers/android/exec/ADB.js @@ -8,6 +8,8 @@ const { escape } = require('../../../../../utils/pipeCommands'); const DeviceHandle = require('../tools/DeviceHandle'); const EmulatorHandle = require('../tools/EmulatorHandle'); +const ADBCache = require('./ADBCache'); + const DEFAULT_EXEC_OPTIONS = { retries: 1, }; @@ -17,11 +19,25 @@ const DEFAULT_INSTALL_OPTIONS = { }; class ADB { - constructor() { - this._cachedApiLevels = new Map(); - this.defaultExecOptions = DEFAULT_EXEC_OPTIONS; - this.installOptions = DEFAULT_INSTALL_OPTIONS; - this.adbBin = getAdbPath(); + constructor(options = {}) { + this.adbBin = options.adbBin || getAdbPath(); + this.deviceId = options.deviceId; + this.defaultExecOptions = options.defaultExecOptions || DEFAULT_EXEC_OPTIONS; + this.installOptions = options.installOptions || DEFAULT_INSTALL_OPTIONS; + this._cache = options.cache || ADBCache.instance; + } + + get _cachedApiLevels() { + return this._cache.apiLevels; + } + + bind(options) { + return new ADB({ + adbBin: this.adbBin, + cache: this._cache, + deviceId: this.deviceId, + ...options, + }); } async startDaemon() { @@ -43,9 +59,9 @@ class ADB { return { devices, stdout }; } - async getState(deviceId) { + async getState() { try { - const output = await this.adbCmd(deviceId, `get-state`, { + const output = await this.adbCmd(this.deviceId, `get-state`, { verbosity: 'low' }); @@ -60,23 +76,23 @@ class ADB { } } - async unlockScreen(deviceId) { + async unlockScreen() { const { mWakefulness, mUserActivityTimeoutOverrideFromWindowManager, - } = await this._getPowerStatus(deviceId); + } = await this._getPowerStatus(); if (mWakefulness === 'Asleep' || mWakefulness === 'Dozing') { - await this.pressPowerDevice(deviceId); + await this.pressPowerDevice(); } if (mUserActivityTimeoutOverrideFromWindowManager === '10000') { // screen is locked - await this.pressOptionsMenu(deviceId); + await this.pressOptionsMenu(); } } - async _getPowerStatus(deviceId) { - const stdout = await this.shell(deviceId, `dumpsys power | grep "^[ ]*m[UW].*="`, { retries: 5 }); + async _getPowerStatus() { + const stdout = await this.shell(`dumpsys power | grep "^[ ]*m[UW].*="`, { retries: 5 }); return stdout .split('\n') .map(s => s.trim().split('=')) @@ -86,69 +102,69 @@ class ADB { }), {}); } - async pressOptionsMenu(deviceId) { - await this._sendKeyEvent(deviceId, 'KEYCODE_MENU'); + async pressOptionsMenu() { + await this._sendKeyEvent('KEYCODE_MENU'); } - async pressPowerDevice(deviceId) { - await this._sendKeyEvent(deviceId, 'KEYCODE_POWER'); + async pressPowerDevice() { + await this._sendKeyEvent('KEYCODE_POWER'); } - async typeText(deviceId, text) { + async typeText(text) { const actualText = text.replace(/ /g, '%s'); - await this.shell(deviceId, `input text ${actualText}`); + await this.shell(`input text ${actualText}`); } - async _sendKeyEvent(deviceId, keyevent) { - await this.shell(deviceId, `input keyevent ${keyevent}`); + async _sendKeyEvent(keyevent) { + await this.shell(`input keyevent ${keyevent}`); } - async now(deviceId) { - return this.shell(deviceId, `date +"%m-%d %T.000"`); + async now() { + return this.shell(`date +"%m-%d %T.000"`); } - async isPackageInstalled(deviceId, packageId) { - const output = await this.shell(deviceId, `pm list packages ${packageId}`); + async isPackageInstalled(packageId) { + const output = await this.shell(`pm list packages ${packageId}`); const packageRegexp = new RegExp(`^package:${escape.inQuotedRegexp(packageId)}$`, 'm'); const isInstalled = packageRegexp.test(output); return isInstalled; } - async install(deviceId, _apkPath) { + async install(_apkPath) { const apkPath = escape.inQuotedString(_apkPath); - const apiLvl = await this.apiLevel(deviceId); + const apiLvl = await this.apiLevel(); const command = (apiLvl >= 23) ? `install -r -g -t ${apkPath}` : `install -rg ${apkPath}`; - const result = await this.adbCmdSpawned(deviceId, command, this.installOptions); + const result = await this.adbCmdSpawned(this.deviceId, command, this.installOptions); const [failure] = (result.stdout || '').match(/^Failure \[.*\]$/m) || []; if (failure) { throw new DetoxRuntimeError({ - message: `Failed to install app on ${deviceId}: ${apkPath}`, + message: `Failed to install app on ${this.deviceId}: ${apkPath}`, debugInfo: failure, }); } } - async remoteInstall(deviceId, path) { - const apiLvl = await this.apiLevel(deviceId); + async remoteInstall(path) { + const apiLvl = await this.apiLevel(); const command = (apiLvl >= 23) ? `pm install -r -g -t ${path}` : `pm install -rg ${path}`; - return this.shellSpawned(deviceId, command, this.installOptions); + return this.shellSpawned(command, this.installOptions); } - async uninstall(deviceId, appId) { - await this.adbCmd(deviceId, `uninstall ${appId}`); + async uninstall(appId) { + await this.adbCmd(this.deviceId, `uninstall ${appId}`); } - async terminate(deviceId, appId) { - await this.shell(deviceId, `am force-stop ${appId}`); + async terminate(appId) { + await this.shell(`am force-stop ${appId}`); } - async setLocation(deviceId, lat, lon) { + async setLocation(lat, lon) { // NOTE: QEMU for Android for the telnet part relies on C stdlib // function `strtod` which is locale-sensitive, meaning that depending // on user environment you'll have to send either comma-separated @@ -166,16 +182,16 @@ class ADB { const dot = `${lon} ${lat}`; const comma = dot.replace(/\./g, ','); - await this.emu(deviceId, `geo fix ${dot}`); - await this.emu(deviceId, `geo fix ${comma}`); + await this.emu(`geo fix ${dot}`); + await this.emu(`geo fix ${comma}`); } - async pidof(deviceId, bundleId) { + async pidof(bundleId) { const bundleIdRegex = escape.inQuotedRegexp(bundleId) + '$'; const command = `ps | grep "${bundleIdRegex}"`; const options = { silent: true }; - const processes = await this.shell(deviceId, command, options).catch(() => ''); + const processes = await this.shell(command, options).catch(() => ''); if (!processes) { return NaN; } @@ -183,8 +199,8 @@ class ADB { return parseInt(processes.split(' ').filter(Boolean)[1], 10); } - async getFileSize(deviceId, filename) { - const { stdout, stderr } = await this.adbCmd(deviceId, 'shell du ' + filename).catch(e => e); + async getFileSize(filename) { + const { stdout, stderr } = await this.adbCmd(this.deviceId, 'shell du ' + filename).catch(e => e); if (stderr.includes('No such file or directory')) { return -1; @@ -193,49 +209,49 @@ class ADB { return Number(stdout.slice(0, stdout.indexOf(' '))); } - async isBootComplete(deviceId) { + async isBootComplete() { try { - const bootComplete = await this.shell(deviceId, `getprop dev.bootcomplete`, { retries: 0, silent: true }); + const bootComplete = await this.shell(`getprop dev.bootcomplete`, { retries: 0, silent: true }); return (bootComplete === '1'); } catch (ex) { return false; } } - async waitForDevice(deviceId) { - return await this.adbCmd(deviceId, 'wait-for-device'); + async waitForDevice() { + return await this.adbCmd(this.deviceId, 'wait-for-device'); } - async apiLevel(deviceId) { - if (this._cachedApiLevels.has(deviceId)) { - return this._cachedApiLevels.get(deviceId); + async apiLevel() { + if (this._cachedApiLevels.has(this.deviceId)) { + return this._cachedApiLevels.get(this.deviceId); } - const lvl = Number(await this.shell(deviceId, `getprop ro.build.version.sdk`, { retries: 5 })); - this._cachedApiLevels.set(deviceId, lvl); + const lvl = Number(await this.shell(`getprop ro.build.version.sdk`, { retries: 5 })); + this._cachedApiLevels.set(this.deviceId, lvl); return lvl; } - async disableAndroidAnimations(deviceId) { - await this.shell(deviceId, `settings put global animator_duration_scale 0`); - await this.shell(deviceId, `settings put global window_animation_scale 0`); - await this.shell(deviceId, `settings put global transition_animation_scale 0`); + async disableAndroidAnimations() { + await this.shell(`settings put global animator_duration_scale 0`); + await this.shell(`settings put global window_animation_scale 0`); + await this.shell(`settings put global transition_animation_scale 0`); } - async setWiFiToggle(deviceId, state) { + async setWiFiToggle(state) { const value = (state === true ? 'enable' : 'disable'); - await this.shell(deviceId, `svc wifi ${value}`); + await this.shell(`svc wifi ${value}`); } - async screencap(deviceId, path) { - await this.shell(deviceId, `screencap ${path}`); + async screencap(path) { + await this.shell(`screencap ${path}`); } /*** * @returns ChildProcessPromise */ - screenrecord(deviceId, { path, size, bitRate, timeLimit, verbose }) { + screenrecord({ path, size, bitRate, timeLimit, verbose }) { const [width = 0, height = 0] = size || []; const _size = (width > 0) && (height > 0) @@ -253,18 +269,18 @@ class ADB { const _verbose = verbose ? ['--verbose'] : []; const screenRecordArgs = [..._size, ..._bitRate, ..._timeLimit, ..._verbose, path]; - return this.spawn(deviceId, ['shell', 'screenrecord', ...screenRecordArgs]); + return this.spawn(this.deviceId, ['shell', 'screenrecord', ...screenRecordArgs]); } /*** * @returns ChildProcessPromise */ - logcat(deviceId, { file, pid, time }) { + logcat({ file, pid, time }) { let shellCommand = 'logcat'; // HACK: cannot make this function async, otherwise ChildProcessPromise.childProcess field will get lost, // and this will break interruptProcess() call for any logcat promise. - const apiLevel = this._cachedApiLevels.get(deviceId); + const apiLevel = this._cachedApiLevels.get(this.deviceId); if (time && apiLevel >= 21) { shellCommand += ` -T "${time}"`; } @@ -288,38 +304,38 @@ class ADB { } } - return this.spawn(deviceId, ['shell', shellCommand]); + return this.spawn(this.deviceId, ['shell', shellCommand]); } - async push(deviceId, src, dst) { - await this.adbCmd(deviceId, `push "${src}" "${dst}"`); + async push(src, dst) { + await this.adbCmd(this.deviceId, `push "${src}" "${dst}"`); } - async pull(deviceId, src, dst = '') { - await this.adbCmd(deviceId, `pull "${src}" "${dst}"`); + async pull(src, dst = '') { + await this.adbCmd(this.deviceId, `pull "${src}" "${dst}"`); } - async rm(deviceId, path, force = false) { - await this.shell(deviceId, `rm ${force ? '-f' : ''} "${path}"`); + async rm(path, force = false) { + await this.shell(`rm ${force ? '-f' : ''} "${path}"`); } /*** * @returns {ChildProcessPromise} */ - spawnInstrumentation(deviceId, userArgs, testRunner) { + spawnInstrumentation(userArgs, testRunner) { const spawnArgs = ['shell', 'am', 'instrument', '-w', '-r', ...userArgs, testRunner]; - return this.spawn(deviceId, spawnArgs, { detached: false }); + return this.spawn(this.deviceId, spawnArgs, { detached: false }); } - async listInstrumentation(deviceId) { - return this.shell(deviceId, 'pm list instrumentation'); + async listInstrumentation() { + return this.shell('pm list instrumentation'); } - async getInstrumentationRunner(deviceId, bundleId) { - const instrumentationRunners = await this.listInstrumentation(deviceId); + async getInstrumentationRunner(bundleId) { + const instrumentationRunners = await this.listInstrumentation(); const instrumentationRunner = this._instrumentationRunnerForBundleId(instrumentationRunners, bundleId); 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 ${this.deviceId} for package ${bundleId}`); } return instrumentationRunner; @@ -330,31 +346,35 @@ class ADB { return _.get(runnerForBundleRegEx.exec(instrumentationRunners), [1], 'undefined'); } - async shell(deviceId, command, options) { - const result = await this.adbCmd(deviceId, `shell "${escape.inQuotedString(command)}"`, options); + async shell(command, options) { + const result = await this.adbCmd(this.deviceId, `shell "${escape.inQuotedString(command)}"`, options); return result.stdout.trim(); } - async shellSpawned(deviceId, command, options) { + async shellSpawned(command, options) { const _command = `shell ${command}`; - const result = await this.adbCmdSpawned(deviceId, _command, options); + const result = await this.adbCmdSpawned(this.deviceId, _command, options); return result.stdout.trim(); } - async emu(deviceId, cmd, options) { - return (await this.adbCmd(deviceId, `emu "${escape.inQuotedString(cmd)}"`, options)).stdout.trim(); + async emu(cmd, options) { + return (await this.adbCmd(this.deviceId, `emu "${escape.inQuotedString(cmd)}"`, options)).stdout.trim(); + } + + async reverse(port) { + await this.adbCmd(this.deviceId, `reverse tcp:${port} tcp:${port}`); } - async reverse(deviceId, port) { - return this.adbCmd(deviceId, `reverse tcp:${port} tcp:${port}`); + async reverseRemove(port) { + await this.adbCmd(this.deviceId, `reverse --remove tcp:${port}`); } - async reverseRemove(deviceId, port) { - return this.adbCmd(deviceId, `reverse --remove tcp:${port}`); + async reverseList() { + const { stdout } = await this.adbCmd(this.deviceId, `reverse --list`); } - async emuKill(deviceId) { - return this.adbCmd(deviceId, `emu kill`); + async emuKill() { + return this.adbCmd(this.deviceId, `emu kill`); } async adbCmd(deviceId, params, options = {}) { diff --git a/detox/src/devices/common/drivers/android/exec/ADB.test.js b/detox/src/devices/common/drivers/android/exec/ADB.test.js index 85f21d49d4..8a2dea3f5e 100644 --- a/detox/src/devices/common/drivers/android/exec/ADB.test.js +++ b/detox/src/devices/common/drivers/android/exec/ADB.test.js @@ -36,7 +36,7 @@ describe('ADB', () => { spawnWithRetriesAndLogs = require('../../../../../utils/childProcess').spawnWithRetriesAndLogs; ADB = require('./ADB'); - adb = new ADB(); + adb = new ADB({ deviceId }); }); describe('devices', () => { @@ -101,7 +101,7 @@ describe('ADB', () => { }); it('should await device boot', async () => { - await adb.waitForDevice(deviceId); + await adb.waitForDevice(); expect(execWithRetriesAndLogs).toHaveBeenCalledWith( expect.stringContaining(`"${adbBinPath}" -s ${deviceId} wait-for-device`), @@ -110,7 +110,7 @@ describe('ADB', () => { it('should install an APK (api≤22)', async () => { jest.spyOn(adb, 'apiLevel').mockImplementation(async () => 22); - await adb.install(deviceId, 'path/to/bin.apk'); + await adb.install('path/to/bin.apk'); expect(spawnWithRetriesAndLogs).toHaveBeenCalledWith( adbBinPath, @@ -120,7 +120,7 @@ describe('ADB', () => { it('should install an APK (api≥23)', async () => { jest.spyOn(adb, 'apiLevel').mockImplementation(async () => 23); - await adb.install(deviceId, 'path/to/bin.apk'); + await adb.install('path/to/bin.apk'); expect(spawnWithRetriesAndLogs).toHaveBeenCalledWith( adbBinPath, @@ -135,7 +135,7 @@ describe('ADB', () => { capture: ['stdout'], encoding: 'utf8', }; - await adb.install(deviceId, 'path/to/bin.apk'); + await adb.install('path/to/bin.apk'); expect(spawnWithRetriesAndLogs).toHaveBeenCalledWith(expect.any(String), expect.any(Array), expectedOptions); }); @@ -150,7 +150,7 @@ describe('ADB', () => { retries: 99, timeout: 1337, }; - await adb.install(deviceId, 'dont-care/path'); + await adb.install('dont-care/path'); expect(spawnWithRetriesAndLogs).toHaveBeenCalledWith(expect.any(String), expect.any(Array), expect.objectContaining({ retries: 99, interval: 123123, @@ -189,7 +189,7 @@ describe('ADB', () => { const lat = 30.5; const lon = -70.5; - await adb.setLocation(deviceId, lat, lon); + await adb.setLocation(lat, lon); expect(execWithRetriesAndLogs).toHaveBeenCalledWith( expect.stringContaining(`-s mockEmulator emu "geo fix -70.5 30.5"`), @@ -204,18 +204,18 @@ describe('ADB', () => { jest.spyOn(adb, 'shell').mockImplementation(async () => `u0_a19 2199 1701 3554600 70264 0 0 s com.google.android.ext.services `); - expect(await adb.pidof('', 'com.google.android.ext.services')).toBe(2199); + expect(await adb.pidof('com.google.android.ext.services')).toBe(2199); }); it(`pidof (failure)`, async () => { jest.spyOn(adb, 'shell').mockImplementation(async () => ''); - expect(await adb.pidof('', 'com.google.android.ext.services')).toBe(NaN); + expect(await adb.pidof('com.google.android.ext.services')).toBe(NaN); }); it('push', async () => { const sourceFile = '/mock-source/file.xyz'; const destFile = '/sdcard/file.abc'; - await adb.push(deviceId, sourceFile, destFile); + await adb.push(sourceFile, destFile); expect(execWithRetriesAndLogs).toHaveBeenCalledWith( expect.stringContaining(`-s mockEmulator push "${sourceFile}" "${destFile}"`), @@ -225,7 +225,7 @@ describe('ADB', () => { it('should remote-install (api≤22)', async () => { jest.spyOn(adb, 'apiLevel').mockImplementation(async () => 22); const binaryPath = '/mock-path/filename.mock'; - await adb.remoteInstall(deviceId, binaryPath); + await adb.remoteInstall(binaryPath); expect(spawnWithRetriesAndLogs).toHaveBeenCalledWith( adbBinPath, @@ -236,7 +236,7 @@ describe('ADB', () => { it('should remote-install (api≥22)', async () => { jest.spyOn(adb, 'apiLevel').mockImplementation(async () => 23); const binaryPath = '/mock-path/filename.mock'; - await adb.remoteInstall(deviceId, binaryPath); + await adb.remoteInstall(binaryPath); expect(spawnWithRetriesAndLogs).toHaveBeenCalledWith( adbBinPath, @@ -252,7 +252,7 @@ describe('ADB', () => { encoding: 'utf8', }; const binaryPath = '/mock-path/filename.mock'; - await adb.install(deviceId, binaryPath); + await adb.install(binaryPath); expect(spawnWithRetriesAndLogs).toHaveBeenCalledWith(expect.any(String), expect.any(Array), expectedOptions); }); @@ -260,7 +260,7 @@ describe('ADB', () => { it('global text-typing', async () => { const text = 'some-text-with spaces'; const expectedText = 'some-text-with%sspaces'; - await adb.typeText(deviceId, text); + await adb.typeText(text); expect(execWithRetriesAndLogs).toHaveBeenCalledWith( expect.stringContaining(`-s mockEmulator shell "input text ${expectedText}"`), expect.anything()); @@ -279,47 +279,47 @@ describe('ADB', () => { mUserInactiveOverrideFromWindowManager=false `); - await adb.unlockScreen(deviceId); + await adb.unlockScreen(); } describe('when unlocking an awake and unlocked device', function() { beforeEach(async () => unlockScreenWithPowerStatus('Awake', '-1')); it('should not press power button', () => - expect(adb.shell).not.toHaveBeenCalledWith(deviceId, 'input keyevent KEYCODE_POWER')); + expect(adb.shell).not.toHaveBeenCalledWith('input keyevent KEYCODE_POWER')); it('should not press menu button', () => - expect(adb.shell).not.toHaveBeenCalledWith(deviceId, 'input keyevent KEYCODE_MENU')); + expect(adb.shell).not.toHaveBeenCalledWith('input keyevent KEYCODE_MENU')); }); describe('when unlocking a sleeping and locked device', function() { beforeEach(async () => unlockScreenWithPowerStatus('Asleep', '10000')); it('should press power button first', () => - expect(adb.shell.mock.calls[1]).toEqual([deviceId, 'input keyevent KEYCODE_POWER'])); + expect(adb.shell.mock.calls[1]).toEqual(['input keyevent KEYCODE_POWER'])); it('should press menu afterwards', () => - expect(adb.shell.mock.calls[2]).toEqual([deviceId, 'input keyevent KEYCODE_MENU'])); + expect(adb.shell.mock.calls[2]).toEqual(['input keyevent KEYCODE_MENU'])); }); describe('when unlocking an awake but locked device', function() { beforeEach(async () => unlockScreenWithPowerStatus('Awake', '10000')); it('should not press power button', () => - expect(adb.shell).not.toHaveBeenCalledWith(deviceId, 'input keyevent KEYCODE_POWER')); + expect(adb.shell).not.toHaveBeenCalledWith('input keyevent KEYCODE_POWER')); it('should press menu button', () => - expect(adb.shell).toHaveBeenCalledWith(deviceId, 'input keyevent KEYCODE_MENU')); + expect(adb.shell).toHaveBeenCalledWith('input keyevent KEYCODE_MENU')); }); describe('when unlocking a sleeping but unlocked device', function() { beforeEach(async () => unlockScreenWithPowerStatus('Asleep', '-1')); it('should press power button', () => - expect(adb.shell).toHaveBeenCalledWith(deviceId, 'input keyevent KEYCODE_POWER')); + expect(adb.shell).toHaveBeenCalledWith('input keyevent KEYCODE_POWER')); it('should not press menu button', () => - expect(adb.shell).not.toHaveBeenCalledWith(deviceId, 'input keyevent KEYCODE_MENU')); + expect(adb.shell).not.toHaveBeenCalledWith('input keyevent KEYCODE_MENU')); }); }); @@ -329,13 +329,13 @@ describe('ADB', () => { it('should spawn instrumentation', async () => { const userArgs = []; const expectedArgs = ['-s', deviceId, 'shell', 'am', 'instrument', '-w', '-r', testRunner]; - await adb.spawnInstrumentation(deviceId, userArgs, testRunner); + await adb.spawnInstrumentation(userArgs, testRunner); expect(spawnAndLog).toHaveBeenCalledWith(adbBinPath, expectedArgs, expect.any(Object)); }); it('should pass through additional args', async () => { const userArgs = ['-mock', '-args']; - await adb.spawnInstrumentation(deviceId, userArgs, testRunner); + await adb.spawnInstrumentation(userArgs, testRunner); expect(spawnAndLog).toHaveBeenCalledWith(adbBinPath, expect.arrayContaining([...userArgs, testRunner]), expect.any(Object)); }); @@ -345,7 +345,7 @@ describe('ADB', () => { }; const userArgs = []; - await adb.spawnInstrumentation(deviceId, userArgs, testRunner); + await adb.spawnInstrumentation(userArgs, testRunner); expect(spawnAndLog).toHaveBeenCalledWith(adbBinPath, expect.any(Array), expectedOptions); }); @@ -355,18 +355,17 @@ describe('ADB', () => { const mockPromise = Promise.resolve('mock'); spawnAndLog.mockReturnValue(mockPromise); - const childProcessPromise = adb.spawnInstrumentation(deviceId, userArgs, testRunner); + const childProcessPromise = adb.spawnInstrumentation(userArgs, testRunner); expect(childProcessPromise).toEqual(mockPromise); }); }); it(`listInstrumentation passes the right deviceId`, async () => { - const deviceId = 'aDeviceId'; jest.spyOn(adb, 'shell'); - await adb.listInstrumentation(deviceId); + await adb.listInstrumentation(); - expect(adb.shell).toHaveBeenCalledWith(deviceId, 'pm list instrumentation'); + expect(adb.shell).toHaveBeenCalledWith('pm list instrumentation'); }); it(`getInstrumentationRunner parses the correct runner for the package`, async () => { @@ -380,37 +379,37 @@ describe('ADB', () => { jest.spyOn(adb, 'shell').mockImplementation(async () => instrumentationRunnersShellOutput); - const result = await adb.getInstrumentationRunner('aDeviceId', expectedPackage); + const result = await adb.getInstrumentationRunner(expectedPackage); - expect(adb.shell).toHaveBeenCalledWith('aDeviceId', 'pm list instrumentation'); + expect(adb.shell).toHaveBeenCalledWith('pm list instrumentation'); expect(result).toEqual(expectedRunner); }); describe('animation disabling', () => { it('should disable animator (e.g. ObjectAnimator) animations', async () => { - await adb.disableAndroidAnimations(deviceId); + await adb.disableAndroidAnimations(); expect(execWithRetriesAndLogs).toHaveBeenCalledWith(`"${adbBinPath}" -s ${deviceId} shell "settings put global animator_duration_scale 0"`, { retries: 1 }); }); it('should disable window animations', async () => { - await adb.disableAndroidAnimations(deviceId); + await adb.disableAndroidAnimations(); expect(execWithRetriesAndLogs).toHaveBeenCalledWith(`"${adbBinPath}" -s ${deviceId} shell "settings put global window_animation_scale 0"`, { retries: 1 }); }); it('should disable transition (e.g. activity launch) animations', async () => { - await adb.disableAndroidAnimations(deviceId); + await adb.disableAndroidAnimations(); expect(execWithRetriesAndLogs).toHaveBeenCalledWith(`"${adbBinPath}" -s ${deviceId} shell "settings put global transition_animation_scale 0"`, { retries: 1 }); }); }); describe('WiFi toggle', () => { it('should enable wifi', async () => { - await adb.setWiFiToggle(deviceId, true); + await adb.setWiFiToggle(true); expect(execWithRetriesAndLogs).toHaveBeenCalledWith(`"${adbBinPath}" -s ${deviceId} shell "svc wifi enable"`, { retries: 1 }); }); it('should disable wifi', async () => { - await adb.setWiFiToggle(deviceId, false); + await adb.setWiFiToggle(false); expect(execWithRetriesAndLogs).toHaveBeenCalledWith(`"${adbBinPath}" -s ${deviceId} shell "svc wifi disable"`, { retries: 1 }); }); }); diff --git a/detox/src/devices/common/drivers/android/exec/ADBCache.js b/detox/src/devices/common/drivers/android/exec/ADBCache.js new file mode 100644 index 0000000000..2e88224e9f --- /dev/null +++ b/detox/src/devices/common/drivers/android/exec/ADBCache.js @@ -0,0 +1,9 @@ +class ADBCache { + constructor(initialCaches = {}) { + this.apiLevels = initialCaches.apiLevels || new Map(); + } + + static instance = new ADBCache(); +} + +module.exports = ADBCache; diff --git a/detox/src/devices/common/drivers/android/tools/AppInstallHelper.js b/detox/src/devices/common/drivers/android/tools/AppInstallHelper.js index ba621d322d..1999cb4369 100644 --- a/detox/src/devices/common/drivers/android/tools/AppInstallHelper.js +++ b/detox/src/devices/common/drivers/android/tools/AppInstallHelper.js @@ -1,20 +1,21 @@ +// @ts-nocheck class AppInstallHelper { constructor(adb, fileTransfer) { this._adb = adb; this._fileTransfer = fileTransfer; } - async install(deviceId, appBinaryPath, testBinaryPath) { - await this._fileTransfer.prepareDestinationDir(deviceId); - await this._pushAndInstallBinary(deviceId, appBinaryPath, 'Application.apk'); + async install(appBinaryPath, testBinaryPath) { + await this._fileTransfer.prepareDestinationDir(); + await this._pushAndInstallBinary(appBinaryPath, 'Application.apk'); if (testBinaryPath) { - await this._pushAndInstallBinary(deviceId, testBinaryPath, 'Test.apk'); + await this._pushAndInstallBinary(testBinaryPath, 'Test.apk'); } } - async _pushAndInstallBinary(deviceId, binaryPath, binaryFilenameOnTarget) { - const binaryPathOnTarget = await this._fileTransfer.send(deviceId, binaryPath, binaryFilenameOnTarget); - await this._adb.remoteInstall(deviceId, binaryPathOnTarget); + async _pushAndInstallBinary(binaryPath, binaryFilenameOnTarget) { + const binaryPathOnTarget = await this._fileTransfer.send(binaryPath, binaryFilenameOnTarget); + await this._adb.remoteInstall(binaryPathOnTarget); } } diff --git a/detox/src/devices/common/drivers/android/tools/AppInstallHelper.test.js b/detox/src/devices/common/drivers/android/tools/AppInstallHelper.test.js index c3905c5fa4..faad67581c 100644 --- a/detox/src/devices/common/drivers/android/tools/AppInstallHelper.test.js +++ b/detox/src/devices/common/drivers/android/tools/AppInstallHelper.test.js @@ -1,3 +1,4 @@ +// @ts-nocheck const deviceId = 'mock-device-id'; const appBinaryPath = '/mock-app-binary-path/binary.apk'; const testBinaryPath = '/mock-test-binary-path/test/binary.apk'; @@ -21,30 +22,30 @@ describe('Android app installation helper', () => { }); it('should recreate the transient dir on the device', async () => { - await uut.install(deviceId, appBinaryPath, testBinaryPath); - expect(fileTransfer.prepareDestinationDir).toHaveBeenCalledWith(deviceId); + await uut.install(appBinaryPath, testBinaryPath); + expect(fileTransfer.prepareDestinationDir).toHaveBeenCalledWith(); }); it('should throw if transient dir prep fails', async () => { fileTransfer.prepareDestinationDir.mockRejectedValue(new Error('mocked error in adb-shell')); - await expect(uut.install(deviceId, appBinaryPath, testBinaryPath)).rejects.toThrow(); + await expect(uut.install(appBinaryPath, testBinaryPath)).rejects.toThrow(); }); it('should push app-binary file to the device', async () => { - await uut.install(deviceId, appBinaryPath, testBinaryPath); - expect(fileTransfer.send).toHaveBeenCalledWith(deviceId, appBinaryPath, 'Application.apk'); + await uut.install(appBinaryPath, testBinaryPath); + expect(fileTransfer.send).toHaveBeenCalledWith(appBinaryPath, 'Application.apk'); }); it('should push test-binary file to the device', async () => { - await uut.install(deviceId, appBinaryPath, testBinaryPath); - expect(fileTransfer.send).toHaveBeenCalledWith(deviceId, testBinaryPath, 'Test.apk'); + await uut.install(appBinaryPath, testBinaryPath); + expect(fileTransfer.send).toHaveBeenCalledWith(testBinaryPath, 'Test.apk'); }); it('should break if file push fails', async () => { fileTransfer.send.mockRejectedValue(new Error('mocked error in adb-push')); - await expect(uut.install(deviceId, appBinaryPath, testBinaryPath)).rejects.toThrow(); + await expect(uut.install(appBinaryPath, testBinaryPath)).rejects.toThrow(); }); it('should remote-install both binaries via shell', async () => { @@ -52,15 +53,15 @@ describe('Android app installation helper', () => { .mockReturnValueOnce('/mocked-final-dir/first.apk') .mockReturnValueOnce('/mocked-final-dir/second.apk'); - await uut.install(deviceId, appBinaryPath, testBinaryPath); - expect(adb.remoteInstall).toHaveBeenCalledWith(deviceId, '/mocked-final-dir/first.apk'); - expect(adb.remoteInstall).toHaveBeenCalledWith(deviceId, '/mocked-final-dir/second.apk'); + await uut.install(appBinaryPath, testBinaryPath); + expect(adb.remoteInstall).toHaveBeenCalledWith('/mocked-final-dir/first.apk'); + expect(adb.remoteInstall).toHaveBeenCalledWith('/mocked-final-dir/second.apk'); }); it('should break if remote-install fails', async () => { adb.remoteInstall.mockRejectedValue(new Error('mocked error in remote-install')); - await expect(uut.install(deviceId, appBinaryPath, testBinaryPath)).rejects.toThrow(); + await expect(uut.install(appBinaryPath, testBinaryPath)).rejects.toThrow(); }); it('should allow for an install with no test binary', async () => { @@ -68,9 +69,9 @@ describe('Android app installation helper', () => { .mockReturnValueOnce('/mocked-final-dir/first.apk') .mockReturnValueOnce('/mocked-final-dir/second.apk'); - await uut.install(deviceId, appBinaryPath, undefined); + await uut.install(appBinaryPath, undefined); expect(fileTransfer.send).toHaveBeenCalledTimes(1); - expect(adb.remoteInstall).toHaveBeenCalledWith(deviceId, '/mocked-final-dir/first.apk'); + expect(adb.remoteInstall).toHaveBeenCalledWith('/mocked-final-dir/first.apk'); expect(adb.remoteInstall).toHaveBeenCalledTimes(1); }); }); diff --git a/detox/src/devices/common/drivers/android/tools/AppUninstallHelper.js b/detox/src/devices/common/drivers/android/tools/AppUninstallHelper.js index 345885899c..d53c5d193d 100644 --- a/detox/src/devices/common/drivers/android/tools/AppUninstallHelper.js +++ b/detox/src/devices/common/drivers/android/tools/AppUninstallHelper.js @@ -3,14 +3,14 @@ class AppUninstallHelper { this._adb = adb; } - async uninstall(deviceId, bundleId) { - if (await this._adb.isPackageInstalled(deviceId, bundleId)) { - await this._adb.uninstall(deviceId, bundleId); + async uninstall(bundleId) { + if (await this._adb.isPackageInstalled(bundleId)) { + await this._adb.uninstall(bundleId); } const testBundleId = `${bundleId}.test`; - if (await this._adb.isPackageInstalled(deviceId, testBundleId)) { - await this._adb.uninstall(deviceId, testBundleId); + if (await this._adb.isPackageInstalled(testBundleId)) { + await this._adb.uninstall(testBundleId); } } } diff --git a/detox/src/devices/common/drivers/android/tools/AppUninstallHelper.test.js b/detox/src/devices/common/drivers/android/tools/AppUninstallHelper.test.js index af347144c3..159bf5e801 100644 --- a/detox/src/devices/common/drivers/android/tools/AppUninstallHelper.test.js +++ b/detox/src/devices/common/drivers/android/tools/AppUninstallHelper.test.js @@ -1,3 +1,4 @@ +// @ts-nocheck const deviceId = 'mock-device-id'; const bundleId = 'mock-bundle-id'; const testBundleId = 'mock-bundle-id.test'; @@ -17,27 +18,27 @@ describe('Android app uninstall helper', () => { }); it('should uninstall the app\'s binary using adb', async () => { - await uut.uninstall(deviceId, bundleId); - expect(adb.uninstall).toHaveBeenCalledWith(deviceId, bundleId); + await uut.uninstall(bundleId); + expect(adb.uninstall).toHaveBeenCalledWith(bundleId); }); it('should fail if app uninstall fails', async () => { adb.uninstall.mockRejectedValue(new Error('mocked error in adb.uninstall')); - await expect(uut.uninstall(deviceId, bundleId)).rejects.toThrow(); + await expect(uut.uninstall(bundleId)).rejects.toThrow(); }); it('should avoid uninstalling app if not already installed', async () => { adb.isPackageInstalled.mockResolvedValue(false); - await uut.uninstall(deviceId, bundleId); + await uut.uninstall(bundleId); - expect(adb.isPackageInstalled).toHaveBeenCalledWith(deviceId, bundleId); - expect(adb.uninstall).not.toHaveBeenCalledWith(deviceId, bundleId); + expect(adb.isPackageInstalled).toHaveBeenCalledWith(bundleId); + expect(adb.uninstall).not.toHaveBeenCalledWith(bundleId); }); it('should uninstall the test binary using adb', async () => { - await uut.uninstall(deviceId, bundleId); - expect(adb.uninstall).toHaveBeenCalledWith(deviceId, testBundleId); + await uut.uninstall(bundleId); + expect(adb.uninstall).toHaveBeenCalledWith(testBundleId); }); it('should fail if test binary uninstall fails', async () => { @@ -45,7 +46,7 @@ describe('Android app uninstall helper', () => { .mockResolvedValueOnce(true) .mockRejectedValueOnce(new Error('mocked error in adb.uninstall')); - await expect(uut.uninstall(deviceId, bundleId)).rejects.toThrow(); + await expect(uut.uninstall(bundleId)).rejects.toThrow(); }); it('should avoid uninstalling test binary if not already installed', async () => { @@ -53,9 +54,9 @@ describe('Android app uninstall helper', () => { .mockResolvedValueOnce(true) .mockResolvedValueOnce(false); - await uut.uninstall(deviceId, bundleId); + await uut.uninstall(bundleId); - expect(adb.isPackageInstalled).toHaveBeenCalledWith(deviceId, testBundleId); - expect(adb.uninstall).not.toHaveBeenCalledWith(deviceId, testBundleId); + expect(adb.isPackageInstalled).toHaveBeenCalledWith(testBundleId); + expect(adb.uninstall).not.toHaveBeenCalledWith(testBundleId); }); }); diff --git a/detox/src/devices/common/drivers/android/tools/FileTransfer.js b/detox/src/devices/common/drivers/android/tools/FileTransfer.js index 551b9eab10..f320e6a5fe 100644 --- a/detox/src/devices/common/drivers/android/tools/FileTransfer.js +++ b/detox/src/devices/common/drivers/android/tools/FileTransfer.js @@ -6,14 +6,14 @@ class FileTransfer { this._dir = destinationDir; } - async prepareDestinationDir(deviceId) { - await this._adb.shell(deviceId, `rm -fr ${this._dir}`); - await this._adb.shell(deviceId, `mkdir -p ${this._dir}`); + async prepareDestinationDir() { + await this._adb.shell(`rm -fr ${this._dir}`); + await this._adb.shell(`mkdir -p ${this._dir}`); } - async send(deviceId, sourcePath, destinationFilename) { + async send(sourcePath, destinationFilename) { const destinationPath = path.posix.join(this._dir, destinationFilename); - await this._adb.push(deviceId, sourcePath, destinationPath); + await this._adb.push(sourcePath, destinationPath); return destinationPath; } } diff --git a/detox/src/devices/common/drivers/android/tools/FileTransfer.test.js b/detox/src/devices/common/drivers/android/tools/FileTransfer.test.js index f8ab8bb5da..a7161b1a94 100644 --- a/detox/src/devices/common/drivers/android/tools/FileTransfer.test.js +++ b/detox/src/devices/common/drivers/android/tools/FileTransfer.test.js @@ -1,3 +1,4 @@ +// @ts-nocheck const deviceId = 'mock-device-id'; const deviceDestinationDir = '/mock-tmp-dir'; @@ -14,26 +15,26 @@ describe('File-transfer util', () => { }); it('should create the destination directory on the device', async () => { - await uut.prepareDestinationDir(deviceId); + await uut.prepareDestinationDir(); - expect(adb.shell).toHaveBeenCalledWith(deviceId, `rm -fr ${deviceDestinationDir}`); - expect(adb.shell).toHaveBeenCalledWith(deviceId, `mkdir -p ${deviceDestinationDir}`); + expect(adb.shell).toHaveBeenCalledWith(`rm -fr ${deviceDestinationDir}`); + expect(adb.shell).toHaveBeenCalledWith(`mkdir -p ${deviceDestinationDir}`); }); it('should send a file by path', async () => { const sourcePath = '/source/path/source-file.src'; const destFilename = 'dest-file.dst'; - await uut.send(deviceId, sourcePath, destFilename); + await uut.send(sourcePath, destFilename); - expect(adb.push).toHaveBeenCalledWith(deviceId, sourcePath, '/mock-tmp-dir/dest-file.dst'); + expect(adb.push).toHaveBeenCalledWith(sourcePath, '/mock-tmp-dir/dest-file.dst'); }); it('should return final destination path', async () => { const sourcePath = '/source/path/source-file.src'; const destFilename = 'dest-file.dst'; - const destPath = await uut.send(deviceId, sourcePath, destFilename); + const destPath = await uut.send(sourcePath, destFilename); expect(destPath).toEqual(`${deviceDestinationDir}/${destFilename}`); }); diff --git a/detox/src/devices/common/drivers/android/tools/TempFileTransfer.test.js b/detox/src/devices/common/drivers/android/tools/TempFileTransfer.test.js index cb027a555e..65bf004949 100644 --- a/detox/src/devices/common/drivers/android/tools/TempFileTransfer.test.js +++ b/detox/src/devices/common/drivers/android/tools/TempFileTransfer.test.js @@ -1,3 +1,4 @@ +// @ts-nocheck describe('Temp file transfer', () => { let adb; let uut; @@ -10,8 +11,8 @@ describe('Temp file transfer', () => { }); it('should use the default temp-dir', async () => { - await uut.prepareDestinationDir('device-id'); + await uut.prepareDestinationDir(); - expect(adb.shell).toHaveBeenCalledWith('device-id', `rm -fr /data/local/tmp/detox`); + expect(adb.shell).toHaveBeenCalledWith(`rm -fr /data/local/tmp/detox`); }); }); diff --git a/detox/src/devices/runtime/drivers/android/AndroidDriver.js b/detox/src/devices/runtime/drivers/android/AndroidDriver.js index 01b146ae74..d2d52ed0d5 100644 --- a/detox/src/devices/runtime/drivers/android/AndroidDriver.js +++ b/detox/src/devices/runtime/drivers/android/AndroidDriver.js @@ -81,14 +81,14 @@ class AndroidDriver extends DeviceDriverBase { async uninstallApp(bundleId) { await this.emitter.emit('beforeUninstallApp', { deviceId: this.adbName, bundleId }); - await this.appUninstallHelper.uninstall(this.adbName, bundleId); + await this.appUninstallHelper.uninstall(bundleId); } 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); + if (!await this.adb.isPackageInstalled(packageId)) { + await this.appInstallHelper.install(path); } } } @@ -112,24 +112,22 @@ class AndroidDriver extends DeviceDriverBase { } async _handleLaunchApp({ manually, bundleId, launchArgs }) { - const { adbName } = this; + await this.emitter.emit('beforeLaunchApp', { deviceId: this.adbName, bundleId, launchArgs }); - await this.emitter.emit('beforeLaunchApp', { deviceId: adbName, bundleId, launchArgs }); - - launchArgs = await this._modifyArgsForNotificationHandling(adbName, bundleId, launchArgs); + launchArgs = await this._modifyArgsForNotificationHandling(bundleId, launchArgs); if (manually) { - await this._waitForAppLaunch(adbName, bundleId, launchArgs); + await this._waitForAppLaunch(bundleId, launchArgs); } else { - await this._launchApp(adbName, bundleId, launchArgs); + await this._launchApp(bundleId, launchArgs); } - const pid = await this._waitForProcess(adbName, bundleId); + const pid = await this._waitForProcess(bundleId); if (manually) { log.info({}, `Found the app (${bundleId}) with process ID = ${pid}. Proceeding...`); } - await this.emitter.emit('launchApp', { deviceId: adbName, bundleId, launchArgs, pid }); + await this.emitter.emit('launchApp', { deviceId: this.adbName, bundleId, launchArgs, pid }); return pid; } @@ -142,7 +140,7 @@ class AndroidDriver extends DeviceDriverBase { if (url) { await this._startActivityWithUrl(url); } else if (detoxUserNotificationDataURL) { - const payloadPathOnDevice = await this._sendNotificationDataToDevice(detoxUserNotificationDataURL, this.adbName); + const payloadPathOnDevice = await this._sendNotificationDataToDevice(detoxUserNotificationDataURL); await this._startActivityFromNotification(payloadPathOnDevice); } } @@ -172,15 +170,14 @@ class AndroidDriver extends DeviceDriverBase { } async typeText(text) { - await this.adb.typeText(this.adbName, text); + await this.adb.typeText(text); } async terminate(bundleId) { - const { adbName } = this; - await this.emitter.emit('beforeTerminateApp', { deviceId: adbName, bundleId }); + await this.emitter.emit('beforeTerminateApp', { deviceId: this.adbName, bundleId }); await this._terminateInstrumentation(); - await this.adb.terminate(adbName, bundleId); - await this.emitter.emit('terminateApp', { deviceId: adbName, bundleId }); + await this.adb.terminate(bundleId); + await this.emitter.emit('terminateApp', { deviceId: this.adbName, bundleId }); } async cleanup(bundleId) { @@ -197,11 +194,11 @@ class AndroidDriver extends DeviceDriverBase { } async reverseTcpPort(port) { - await this.adb.reverse(this.adbName, port); + await this.adb.reverse(port); } async unreverseTcpPort(port) { - await this.adb.reverseRemove(this.adbName, port); + await this.adb.reverseRemove(port); } async setURLBlacklist(urlList) { @@ -217,14 +214,12 @@ class AndroidDriver extends DeviceDriverBase { } async takeScreenshot(screenshotName) { - const { adbName } = this; - const pathOnDevice = this.devicePathBuilder.buildTemporaryArtifactPath('.png'); - await this.adb.screencap(adbName, pathOnDevice); + await this.adb.screencap(pathOnDevice); const tempPath = temporaryPath.for.png(); - await this.adb.pull(adbName, pathOnDevice, tempPath); - await this.adb.rm(adbName, pathOnDevice); + await this.adb.pull(pathOnDevice, tempPath); + await this.adb.rm(pathOnDevice); await this.emitter.emit('createExternalArtifact', { pluginId: 'screenshot', @@ -260,7 +255,7 @@ class AndroidDriver extends DeviceDriverBase { const call = duration ? EspressoDetoxApi.longPress(x, y, duration, _shouldIgnoreStatusBar): EspressoDetoxApi.longPress(x, y, _shouldIgnoreStatusBar); await this.invocationManager.execute(call); } - + async generateViewHierarchyXml(shouldInjectTestIds) { const hierarchy = await this.invocationManager.execute(DetoxApi.generateViewHierarchyXml(shouldInjectTestIds)); return hierarchy.result; @@ -290,8 +285,8 @@ class AndroidDriver extends DeviceDriverBase { } async _installAppBinaries(appBinaryPath, testBinaryPath) { - await this.adb.install(this.adbName, appBinaryPath); - await this.adb.install(this.adbName, testBinaryPath); + await this.adb.install(appBinaryPath); + await this.adb.install(testBinaryPath); } _getTestApkPath(originalApkPath) { @@ -307,10 +302,10 @@ class AndroidDriver extends DeviceDriverBase { return testApkPath; } - async _modifyArgsForNotificationHandling(adbName, bundleId, launchArgs) { + async _modifyArgsForNotificationHandling(bundleId, launchArgs) { let _launchArgs = launchArgs; if (launchArgs.detoxUserNotificationDataURL) { - const notificationPayloadTargetPath = await this._sendNotificationDataToDevice(launchArgs.detoxUserNotificationDataURL, adbName); + const notificationPayloadTargetPath = await this._sendNotificationDataToDevice(launchArgs.detoxUserNotificationDataURL); _launchArgs = { ...launchArgs, detoxUserNotificationDataURL: notificationPayloadTargetPath, @@ -319,9 +314,9 @@ class AndroidDriver extends DeviceDriverBase { return _launchArgs; } - async _launchApp(adbName, bundleId, launchArgs) { + async _launchApp(bundleId, launchArgs) { if (!this.instrumentation.isRunning()) { - await this._launchInstrumentationProcess(adbName, bundleId, launchArgs); + await this._launchInstrumentationProcess(bundleId, launchArgs); await sleep(500); } else if (launchArgs.detoxURLOverride) { await this._startActivityWithUrl(launchArgs.detoxURLOverride); @@ -332,18 +327,17 @@ class AndroidDriver extends DeviceDriverBase { } } - async _launchInstrumentationProcess(adbName, bundleId, userLaunchArgs) { - const serverPort = await this._reverseServerPort(adbName); + async _launchInstrumentationProcess(bundleId, userLaunchArgs) { + const serverPort = await this._reverseServerPort(); this.instrumentation.setTerminationFn(async () => { - await this._terminateInstrumentation(); - await this.adb.reverseRemove(adbName, serverPort); + await this.adb.reverseRemove(serverPort); }); - await this.instrumentation.launch(adbName, bundleId, userLaunchArgs); + await this.instrumentation.launch(this.adbName, bundleId, userLaunchArgs); } - async _reverseServerPort(adbName) { + async _reverseServerPort() { const serverPort = new URL(this.client.serverUrl).port; - await this.adb.reverse(adbName, serverPort); + await this.adb.reverse(serverPort); return serverPort; } @@ -352,9 +346,9 @@ class AndroidDriver extends DeviceDriverBase { await this.instrumentation.setTerminationFn(null); } - async _sendNotificationDataToDevice(dataFileLocalPath, adbName) { - await this.fileTransfer.prepareDestinationDir(adbName); - return await this.fileTransfer.send(adbName, dataFileLocalPath, 'notification.json'); + async _sendNotificationDataToDevice(dataFileLocalPath) { + await this.fileTransfer.prepareDestinationDir(); + return await this.fileTransfer.send(dataFileLocalPath, 'notification.json'); } _startActivityWithUrl(url) { @@ -369,33 +363,33 @@ class AndroidDriver extends DeviceDriverBase { return this.invocationManager.execute(DetoxApi.launchMainActivity()); } - async _waitForProcess(adbName, bundleId) { + async _waitForProcess(bundleId) { let pid = NaN; try { - const queryPid = () => this._queryPID(adbName, bundleId); + const queryPid = () => this._queryPID(bundleId); const retryQueryPid = () => retry({ backoff: 'none', retries: 4 }, queryPid); const retryQueryPidMultiple = () => retry({ backoff: 'linear' }, retryQueryPid); pid = await retryQueryPidMultiple(); } catch (e) { - log.warn(await this.adb.shell(adbName, 'ps')); + log.warn(await this.adb.shell('ps')); throw e; } return pid; } - async _queryPID(adbName, bundleId) { - const pid = await this.adb.pidof(adbName, bundleId); + async _queryPID(bundleId) { + const pid = await this.adb.pidof(bundleId); if (!pid) { throw new DetoxRuntimeError('PID still not available'); } return pid; } - async _waitForAppLaunch(adbName, bundleId, launchArgs) { - const instrumentationClass = await this.adb.getInstrumentationRunner(adbName, bundleId); + async _waitForAppLaunch(bundleId, launchArgs) { + const instrumentationClass = await this.adb.getInstrumentationRunner(bundleId); this._printInstrumentationHint({ instrumentationClass, launchArgs }); await pressAnyKey(); - await this._reverseServerPort(adbName); + await this._reverseServerPort(); } _printInstrumentationHint({ instrumentationClass, launchArgs }) { diff --git a/detox/src/devices/runtime/drivers/android/AndroidDriver.test.js b/detox/src/devices/runtime/drivers/android/AndroidDriver.test.js index 745c491ab1..a784c250b7 100644 --- a/detox/src/devices/runtime/drivers/android/AndroidDriver.test.js +++ b/detox/src/devices/runtime/drivers/android/AndroidDriver.test.js @@ -65,7 +65,7 @@ describe('Android driver', () => { it('should adb-reverse the detox server port', async () => { await uut.launchApp(bundleId, {}, ''); - await expect(adb.reverse).toHaveBeenCalledWith(adbName, detoxServerPort.toString()); + await expect(adb.reverse).toHaveBeenCalledWith(detoxServerPort.toString()); }); }); @@ -75,13 +75,13 @@ describe('Android driver', () => { await invokeTerminationCallbackFn(); }); - it('should clear out the termination callback function', () => - expect(instrumentation.setTerminationFn).toHaveBeenCalledWith(null)); - - it('should adb-unreverse the detox server port', () => - expect(adb.reverseRemove).toHaveBeenCalledWith(adbName, detoxServerPort.toString())); + it('should adb-unreverse the detox server port', () => { + expect(adb.reverseRemove).toHaveBeenCalledWith(detoxServerPort.toString()); + }); - const extractTerminationCallbackFn = () => instrumentation.setTerminationFn.mock.calls[0][0]; + const extractTerminationCallbackFn = () => { + return instrumentation.setTerminationFn.mock.calls[0][0]; + }; const invokeTerminationCallbackFn = async () => { const fn = extractTerminationCallbackFn(); await fn(); @@ -97,8 +97,10 @@ describe('Android driver', () => { it('should terminate instrumentation', () => expect(instrumentation.terminate).toHaveBeenCalled()); - it('should clear out the termination callback function', () => - expect(instrumentation.setTerminationFn).toHaveBeenCalledWith(null)); + it('should clean up and un-reverse the detox server port', () => { + expect(instrumentation.setTerminationFn).toHaveBeenCalledWith(null); + expect(adb.reverseRemove).toHaveBeenCalledWith(detoxServerPort.toString()); + }); it('should terminate ADB altogether', () => expect(adb.terminate).toHaveBeenCalled()); @@ -210,12 +212,12 @@ describe('Android driver', () => { describe('in app launch (with dedicated arg)', () => { it('should prepare the device for receiving notification data file', async () => { await uut.launchApp(bundleId, notificationArgs, ''); - expect(fileTransfer.prepareDestinationDir).toHaveBeenCalledWith(adbName); + expect(fileTransfer.prepareDestinationDir).toHaveBeenCalledWith(); }); it('should transfer the notification data file to the device', async () => { await uut.launchApp(bundleId, notificationArgs, ''); - expect(fileTransfer.send).toHaveBeenCalledWith(adbName, notificationArgs.detoxUserNotificationDataURL, 'notification.json'); + expect(fileTransfer.send).toHaveBeenCalledWith(notificationArgs.detoxUserNotificationDataURL, 'notification.json'); }); it('should not send the data if device prep fails', async () => { @@ -249,8 +251,8 @@ describe('Android driver', () => { it('should pre-transfer notification data to device', async () => { await spec.applyFn(); - expect(fileTransfer.prepareDestinationDir).toHaveBeenCalledWith(adbName); - expect(fileTransfer.send).toHaveBeenCalledWith(adbName, notificationArgs.detoxUserNotificationDataURL, 'notification.json'); + expect(fileTransfer.prepareDestinationDir).toHaveBeenCalledWith(); + expect(fileTransfer.send).toHaveBeenCalledWith(notificationArgs.detoxUserNotificationDataURL, 'notification.json'); }); it('should start the app with notification data using invocation-manager', async () => { @@ -368,14 +370,14 @@ describe('Android driver', () => { await uut.installApp(binaryPath, testBinaryPath); expect(getAbsoluteBinaryPath).toHaveBeenCalledWith(binaryPath); - expect(adb.install).toHaveBeenCalledWith(adbName, mockGetAbsoluteBinaryPathImpl(binaryPath)); + expect(adb.install).toHaveBeenCalledWith(mockGetAbsoluteBinaryPathImpl(binaryPath)); }); it('should adb-install the test binary', async () => { await uut.installApp(binaryPath, testBinaryPath); expect(getAbsoluteBinaryPath).toHaveBeenCalledWith(binaryPath); - expect(adb.install).toHaveBeenCalledWith(adbName, mockGetAbsoluteBinaryPathImpl(testBinaryPath)); + expect(adb.install).toHaveBeenCalledWith(mockGetAbsoluteBinaryPathImpl(testBinaryPath)); }); it('should resort to auto test-binary path resolution, if not specific', async () => { @@ -386,7 +388,7 @@ describe('Android driver', () => { await uut.installApp(binaryPath, undefined); expect(fs.existsSync).toHaveBeenCalledWith(expectedTestBinPath); - expect(adb.install).toHaveBeenCalledWith(adbName, expectedTestBinPath); + expect(adb.install).toHaveBeenCalledWith(expectedTestBinPath); }); it('should throw if auto test-binary path resolves an invalid file', async () => { @@ -423,8 +425,8 @@ describe('Android driver', () => { it('should install using an app-install helper', async () => { await uut.installUtilBinaries(binaryPaths); - expect(appInstallHelper.install).toHaveBeenCalledWith(adbName, binaryPaths[0]); - expect(appInstallHelper.install).toHaveBeenCalledWith(adbName, binaryPaths[1]); + expect(appInstallHelper.install).toHaveBeenCalledWith(binaryPaths[0]); + expect(appInstallHelper.install).toHaveBeenCalledWith(binaryPaths[1]); }); it('should break if one installation fails', async () => { @@ -434,25 +436,27 @@ describe('Android driver', () => { .mockResolvedValueOnce(); await expect(uut.installUtilBinaries(binaryPaths)).rejects.toThrow(); - expect(appInstallHelper.install).toHaveBeenCalledWith(adbName, binaryPaths[0]); - expect(appInstallHelper.install).toHaveBeenCalledWith(adbName, binaryPaths[1]); + expect(appInstallHelper.install).toHaveBeenCalledWith(binaryPaths[0]); + expect(appInstallHelper.install).toHaveBeenCalledWith(binaryPaths[1]); expect(appInstallHelper.install).toHaveBeenCalledTimes(2); }); it('should not install if already installed', async () => { adb.isPackageInstalled.mockResolvedValueOnce(false).mockResolvedValueOnce(true); await uut.installUtilBinaries(binaryPaths); - expect(appInstallHelper.install).toHaveBeenCalledWith(adbName, binaryPaths[0]); - expect(appInstallHelper.install).not.toHaveBeenCalledWith(adbName, binaryPaths[1]); + expect(appInstallHelper.install).toHaveBeenCalledWith(binaryPaths[0]); + expect(appInstallHelper.install).not.toHaveBeenCalledWith(binaryPaths[1]); }); it('should properly check for preinstallation', async () => { const packageId = 'mockPackageId'; - const binaryPath = 'some/path/file.apk'; + const binaryPath = binaryPaths[0]; + + adb.isPackageInstalled.mockResolvedValue(false); aapt.getPackageName.mockResolvedValue(packageId); await uut.installUtilBinaries([binaryPath]); - expect(adb.isPackageInstalled).toHaveBeenCalledWith(adbName, packageId); + expect(adb.isPackageInstalled).toHaveBeenCalledWith(packageId); expect(aapt.getPackageName).toHaveBeenCalledWith(mockGetAbsoluteBinaryPathImpl(binaryPath)); }); }); @@ -462,22 +466,22 @@ describe('Android driver', () => { it(`should invoke ADB's reverse`, async () => { await uut.reverseTcpPort(port); - expect(adb.reverse).toHaveBeenCalledWith(adbName, port); + expect(adb.reverse).toHaveBeenCalledWith(port); }); it(`should invoke ADB's reverse, given a device handle`, async () => { await uut.reverseTcpPort(port); - expect(adb.reverse).toHaveBeenCalledWith(adbName, port); + expect(adb.reverse).toHaveBeenCalledWith(port); }); it(`should invoke ADB's reverse-remove`, async () => { await uut.unreverseTcpPort(port); - expect(adb.reverseRemove).toHaveBeenCalledWith(adbName, port); + expect(adb.reverseRemove).toHaveBeenCalledWith(port); }); it(`should invoke ADB's reverse-remove, given a device handle`, async () => { await uut.unreverseTcpPort(port); - expect(adb.reverseRemove).toHaveBeenCalledWith(adbName, port); + expect(adb.reverseRemove).toHaveBeenCalledWith(port); }); }); @@ -485,13 +489,13 @@ describe('Android driver', () => { const text = 'text to type'; it(`should invoke ADB's text typing`, async () => { - await uut.typeText( text); - expect(adb.typeText).toHaveBeenCalledWith(adbName, text); + await uut.typeText(text); + expect(adb.typeText).toHaveBeenCalledWith(text); }); it(`should invoke ADB's text typing, given a device handle`, async () => { await uut.typeText(text); - expect(adb.typeText).toHaveBeenCalledWith(adbName, text); + expect(adb.typeText).toHaveBeenCalledWith(text); }); }); @@ -566,6 +570,17 @@ describe('Android driver', () => { instrumentation = new MonitoredInstrumentation(); mockInstrumentationDead(); + let terminationFn = null; + instrumentation.setTerminationFn.mockImplementation((fn) => { + terminationFn = fn; + }); + + instrumentation.terminate.mockImplementation(async () => { + if (terminationFn) { + await terminationFn(); + } + }); + jest.mock('../../../common/drivers/android/exec/ADB'); const ADB = jest.requireMock('../../../common/drivers/android/exec/ADB'); adb = new ADB();