appium-android-driver 7.8.2 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/build/lib/commands/app-management.d.ts +129 -5
- package/build/lib/commands/app-management.d.ts.map +1 -1
- package/build/lib/commands/app-management.js +433 -128
- package/build/lib/commands/app-management.js.map +1 -1
- package/build/lib/commands/appearance.d.ts +17 -4
- package/build/lib/commands/appearance.d.ts.map +1 -1
- package/build/lib/commands/appearance.js +32 -33
- package/build/lib/commands/appearance.js.map +1 -1
- package/build/lib/commands/context/cache.d.ts +19 -0
- package/build/lib/commands/context/cache.d.ts.map +1 -0
- package/build/lib/commands/context/cache.js +32 -0
- package/build/lib/commands/context/cache.js.map +1 -0
- package/build/lib/commands/context/exports.d.ts +141 -0
- package/build/lib/commands/context/exports.d.ts.map +1 -0
- package/build/lib/commands/context/exports.js +351 -0
- package/build/lib/commands/context/exports.js.map +1 -0
- package/build/lib/commands/context/helpers.d.ts +98 -0
- package/build/lib/commands/context/helpers.d.ts.map +1 -0
- package/build/lib/commands/context/helpers.js +715 -0
- package/build/lib/commands/context/helpers.js.map +1 -0
- package/build/lib/commands/device/common.d.ts +23 -0
- package/build/lib/commands/device/common.d.ts.map +1 -0
- package/build/lib/commands/device/common.js +230 -0
- package/build/lib/commands/device/common.js.map +1 -0
- package/build/lib/commands/device/emulator-actions.d.ts +114 -0
- package/build/lib/commands/device/emulator-actions.d.ts.map +1 -0
- package/build/lib/commands/device/emulator-actions.js +197 -0
- package/build/lib/commands/device/emulator-actions.js.map +1 -0
- package/build/lib/commands/device/emulator-console.d.ts +7 -0
- package/build/lib/commands/device/emulator-console.d.ts.map +1 -0
- package/build/lib/commands/device/emulator-console.js +24 -0
- package/build/lib/commands/device/emulator-console.js.map +1 -0
- package/build/lib/commands/device/utils.d.ts +50 -0
- package/build/lib/commands/device/utils.d.ts.map +1 -0
- package/build/lib/commands/device/utils.js +238 -0
- package/build/lib/commands/device/utils.js.map +1 -0
- package/build/lib/commands/deviceidle.d.ts +8 -5
- package/build/lib/commands/deviceidle.d.ts.map +1 -1
- package/build/lib/commands/deviceidle.js +31 -37
- package/build/lib/commands/deviceidle.js.map +1 -1
- package/build/lib/commands/element.d.ts +99 -5
- package/build/lib/commands/element.d.ts.map +1 -1
- package/build/lib/commands/element.js +152 -116
- package/build/lib/commands/element.js.map +1 -1
- package/build/lib/commands/execute.d.ts +12 -4
- package/build/lib/commands/execute.d.ts.map +1 -1
- package/build/lib/commands/execute.js +83 -78
- package/build/lib/commands/execute.js.map +1 -1
- package/build/lib/commands/file-actions.d.ts +42 -5
- package/build/lib/commands/file-actions.d.ts.map +1 -1
- package/build/lib/commands/file-actions.js +230 -194
- package/build/lib/commands/file-actions.js.map +1 -1
- package/build/lib/commands/find.d.ts +5 -4
- package/build/lib/commands/find.d.ts.map +1 -1
- package/build/lib/commands/find.js +7 -10
- package/build/lib/commands/find.js.map +1 -1
- package/build/lib/commands/geolocation.d.ts +45 -0
- package/build/lib/commands/geolocation.d.ts.map +1 -0
- package/build/lib/commands/geolocation.js +182 -0
- package/build/lib/commands/geolocation.js.map +1 -0
- package/build/lib/commands/ime.d.ts +25 -5
- package/build/lib/commands/ime.d.ts.map +1 -1
- package/build/lib/commands/ime.js +59 -42
- package/build/lib/commands/ime.js.map +1 -1
- package/build/lib/commands/intent.d.ts +56 -5
- package/build/lib/commands/intent.d.ts.map +1 -1
- package/build/lib/commands/intent.js +135 -83
- package/build/lib/commands/intent.js.map +1 -1
- package/build/lib/commands/keyboard.d.ts +58 -4
- package/build/lib/commands/keyboard.d.ts.map +1 -1
- package/build/lib/commands/keyboard.js +119 -17
- package/build/lib/commands/keyboard.js.map +1 -1
- package/build/lib/commands/lock/exports.d.ts +301 -0
- package/build/lib/commands/lock/exports.d.ts.map +1 -0
- package/build/lib/commands/lock/exports.js +121 -0
- package/build/lib/commands/lock/exports.js.map +1 -0
- package/build/lib/commands/lock/helpers.d.ts +349 -0
- package/build/lib/commands/lock/helpers.d.ts.map +1 -0
- package/build/lib/commands/lock/helpers.js +375 -0
- package/build/lib/commands/lock/helpers.js.map +1 -0
- package/build/lib/commands/log.d.ts +59 -5
- package/build/lib/commands/log.d.ts.map +1 -1
- package/build/lib/commands/log.js +150 -140
- package/build/lib/commands/log.js.map +1 -1
- package/build/lib/commands/media-projection.d.ts +16 -5
- package/build/lib/commands/media-projection.d.ts.map +1 -1
- package/build/lib/commands/media-projection.js +69 -58
- package/build/lib/commands/media-projection.js.map +1 -1
- package/build/lib/commands/memory.d.ts +9 -5
- package/build/lib/commands/memory.d.ts.map +1 -1
- package/build/lib/commands/memory.js +19 -24
- package/build/lib/commands/memory.js.map +1 -1
- package/build/lib/commands/misc.d.ts +42 -0
- package/build/lib/commands/misc.d.ts.map +1 -0
- package/build/lib/commands/misc.js +100 -0
- package/build/lib/commands/misc.js.map +1 -0
- package/build/lib/commands/network.d.ts +61 -5
- package/build/lib/commands/network.d.ts.map +1 -1
- package/build/lib/commands/network.js +196 -189
- package/build/lib/commands/network.js.map +1 -1
- package/build/lib/commands/performance.d.ts +67 -27
- package/build/lib/commands/performance.d.ts.map +1 -1
- package/build/lib/commands/performance.js +105 -80
- package/build/lib/commands/performance.js.map +1 -1
- package/build/lib/commands/permissions.d.ts +12 -6
- package/build/lib/commands/permissions.d.ts.map +1 -1
- package/build/lib/commands/permissions.js +65 -62
- package/build/lib/commands/permissions.js.map +1 -1
- package/build/lib/commands/recordscreen.d.ts +44 -5
- package/build/lib/commands/recordscreen.d.ts.map +1 -1
- package/build/lib/commands/recordscreen.js +131 -126
- package/build/lib/commands/recordscreen.js.map +1 -1
- package/build/lib/commands/resources.d.ts +16 -0
- package/build/lib/commands/resources.d.ts.map +1 -0
- package/build/lib/commands/resources.js +91 -0
- package/build/lib/commands/resources.js.map +1 -0
- package/build/lib/commands/shell.d.ts +8 -5
- package/build/lib/commands/shell.d.ts.map +1 -1
- package/build/lib/commands/shell.js +29 -33
- package/build/lib/commands/shell.js.map +1 -1
- package/build/lib/commands/streamscreen.d.ts +34 -6
- package/build/lib/commands/streamscreen.d.ts.map +1 -1
- package/build/lib/commands/streamscreen.js +166 -162
- package/build/lib/commands/streamscreen.js.map +1 -1
- package/build/lib/commands/system-bars.d.ts +18 -13
- package/build/lib/commands/system-bars.d.ts.map +1 -1
- package/build/lib/commands/system-bars.js +68 -64
- package/build/lib/commands/system-bars.js.map +1 -1
- package/build/lib/commands/time.d.ts +14 -0
- package/build/lib/commands/time.d.ts.map +1 -0
- package/build/lib/commands/time.js +39 -0
- package/build/lib/commands/time.js.map +1 -0
- package/build/lib/commands/touch.d.ts +99 -6
- package/build/lib/commands/touch.d.ts.map +1 -1
- package/build/lib/commands/touch.js +399 -280
- package/build/lib/commands/touch.js.map +1 -1
- package/build/lib/commands/types.d.ts +115 -3
- package/build/lib/commands/types.d.ts.map +1 -1
- package/build/lib/doctor/checks.d.ts.map +1 -1
- package/build/lib/doctor/checks.js +4 -4
- package/build/lib/doctor/checks.js.map +1 -1
- package/build/lib/driver.d.ts +224 -27
- package/build/lib/driver.d.ts.map +1 -1
- package/build/lib/driver.js +232 -7
- package/build/lib/driver.js.map +1 -1
- package/build/lib/index.d.ts +1 -4
- package/build/lib/index.d.ts.map +1 -1
- package/build/lib/index.js +1 -13
- package/build/lib/index.js.map +1 -1
- package/build/lib/logger.js.map +1 -1
- package/build/lib/method-map.d.ts +0 -23
- package/build/lib/method-map.d.ts.map +1 -1
- package/build/lib/method-map.js +0 -11
- package/build/lib/method-map.js.map +1 -1
- package/build/lib/utils.d.ts +12 -0
- package/build/lib/utils.d.ts.map +1 -1
- package/build/lib/utils.js +38 -2
- package/build/lib/utils.js.map +1 -1
- package/lib/commands/app-management.js +470 -145
- package/lib/commands/appearance.js +29 -36
- package/lib/commands/context/cache.js +29 -0
- package/lib/commands/context/exports.js +379 -0
- package/lib/commands/context/helpers.js +802 -0
- package/lib/commands/device/common.js +264 -0
- package/lib/commands/device/emulator-actions.js +194 -0
- package/lib/commands/device/emulator-console.js +24 -0
- package/lib/commands/device/utils.js +285 -0
- package/lib/commands/deviceidle.js +31 -44
- package/lib/commands/element.js +149 -142
- package/lib/commands/execute.js +86 -87
- package/lib/commands/file-actions.js +249 -222
- package/lib/commands/find.ts +13 -19
- package/lib/commands/geolocation.js +179 -0
- package/lib/commands/ime.js +53 -45
- package/lib/commands/intent.js +149 -91
- package/lib/commands/keyboard.js +114 -17
- package/lib/commands/lock/exports.js +139 -0
- package/lib/commands/lock/helpers.js +379 -0
- package/lib/commands/log.js +170 -166
- package/lib/commands/media-projection.js +75 -70
- package/lib/commands/memory.js +17 -29
- package/lib/commands/misc.js +94 -0
- package/lib/commands/network.js +209 -223
- package/lib/commands/performance.js +88 -73
- package/lib/commands/permissions.js +83 -84
- package/lib/commands/recordscreen.js +171 -170
- package/lib/commands/resources.js +96 -0
- package/lib/commands/shell.js +28 -42
- package/lib/commands/streamscreen.js +207 -206
- package/lib/commands/system-bars.js +76 -77
- package/lib/commands/time.js +36 -0
- package/lib/commands/touch.js +442 -346
- package/lib/commands/types.ts +142 -10
- package/lib/doctor/checks.js +24 -16
- package/lib/driver.ts +454 -12
- package/lib/index.ts +1 -13
- package/lib/logger.js +1 -1
- package/lib/method-map.js +0 -11
- package/lib/utils.js +40 -3
- package/package.json +1 -1
- package/build/lib/commands/actions.d.ts +0 -8
- package/build/lib/commands/actions.d.ts.map +0 -1
- package/build/lib/commands/actions.js +0 -207
- package/build/lib/commands/actions.js.map +0 -1
- package/build/lib/commands/alert.d.ts +0 -8
- package/build/lib/commands/alert.d.ts.map +0 -1
- package/build/lib/commands/alert.js +0 -29
- package/build/lib/commands/alert.js.map +0 -1
- package/build/lib/commands/context.d.ts +0 -10
- package/build/lib/commands/context.d.ts.map +0 -1
- package/build/lib/commands/context.js +0 -431
- package/build/lib/commands/context.js.map +0 -1
- package/build/lib/commands/emu-console.d.ts +0 -7
- package/build/lib/commands/emu-console.d.ts.map +0 -1
- package/build/lib/commands/emu-console.js +0 -27
- package/build/lib/commands/emu-console.js.map +0 -1
- package/build/lib/commands/general.d.ts +0 -9
- package/build/lib/commands/general.d.ts.map +0 -1
- package/build/lib/commands/general.js +0 -293
- package/build/lib/commands/general.js.map +0 -1
- package/build/lib/commands/index.d.ts +0 -28
- package/build/lib/commands/index.d.ts.map +0 -1
- package/build/lib/commands/index.js +0 -57
- package/build/lib/commands/index.js.map +0 -1
- package/build/lib/commands/mixins.d.ts +0 -747
- package/build/lib/commands/mixins.d.ts.map +0 -1
- package/build/lib/commands/mixins.js +0 -19
- package/build/lib/commands/mixins.js.map +0 -1
- package/build/lib/helpers/android.d.ts +0 -163
- package/build/lib/helpers/android.d.ts.map +0 -1
- package/build/lib/helpers/android.js +0 -818
- package/build/lib/helpers/android.js.map +0 -1
- package/build/lib/helpers/index.d.ts +0 -7
- package/build/lib/helpers/index.d.ts.map +0 -1
- package/build/lib/helpers/index.js +0 -29
- package/build/lib/helpers/index.js.map +0 -1
- package/build/lib/helpers/types.d.ts +0 -122
- package/build/lib/helpers/types.d.ts.map +0 -1
- package/build/lib/helpers/types.js +0 -3
- package/build/lib/helpers/types.js.map +0 -1
- package/build/lib/helpers/unlock.d.ts +0 -32
- package/build/lib/helpers/unlock.d.ts.map +0 -1
- package/build/lib/helpers/unlock.js +0 -273
- package/build/lib/helpers/unlock.js.map +0 -1
- package/build/lib/helpers/webview.d.ts +0 -74
- package/build/lib/helpers/webview.d.ts.map +0 -1
- package/build/lib/helpers/webview.js +0 -442
- package/build/lib/helpers/webview.js.map +0 -1
- package/lib/commands/actions.js +0 -244
- package/lib/commands/alert.js +0 -34
- package/lib/commands/context.js +0 -507
- package/lib/commands/emu-console.js +0 -31
- package/lib/commands/general.js +0 -343
- package/lib/commands/index.ts +0 -54
- package/lib/commands/mixins.ts +0 -976
- package/lib/helpers/android.ts +0 -1153
- package/lib/helpers/index.ts +0 -6
- package/lib/helpers/types.ts +0 -136
- package/lib/helpers/unlock.ts +0 -329
- package/lib/helpers/webview.ts +0 -604
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
|
|
3
1
|
import {fs, net, system, tempDir, timing, util} from '@appium/support';
|
|
4
2
|
import {waitForCondition} from 'asyncbox';
|
|
5
3
|
import _ from 'lodash';
|
|
6
4
|
import path from 'path';
|
|
7
5
|
import {exec} from 'teen_process';
|
|
8
|
-
import {mixin} from './mixins';
|
|
9
6
|
|
|
10
7
|
const RETRY_PAUSE = 300;
|
|
11
8
|
const RETRY_TIMEOUT = 5000;
|
|
@@ -18,6 +15,155 @@ const DEFAULT_EXT = '.mp4';
|
|
|
18
15
|
const MIN_EMULATOR_API_LEVEL = 27;
|
|
19
16
|
const FFMPEG_BINARY = `ffmpeg${system.isWindows() ? '.exe' : ''}`;
|
|
20
17
|
|
|
18
|
+
/**
|
|
19
|
+
*
|
|
20
|
+
* @this {import('../driver').AndroidDriver}
|
|
21
|
+
* @param {import('./types').StartScreenRecordingOpts} [options={}]
|
|
22
|
+
* @returns {Promise<string>}
|
|
23
|
+
*/
|
|
24
|
+
export async function startRecordingScreen(options = {}) {
|
|
25
|
+
await verifyScreenRecordIsSupported(this.adb, this.isEmulator());
|
|
26
|
+
|
|
27
|
+
let result = '';
|
|
28
|
+
const {
|
|
29
|
+
videoSize,
|
|
30
|
+
timeLimit = DEFAULT_RECORDING_TIME_SEC,
|
|
31
|
+
bugReport,
|
|
32
|
+
bitRate,
|
|
33
|
+
forceRestart,
|
|
34
|
+
} = options;
|
|
35
|
+
if (!forceRestart) {
|
|
36
|
+
result = await this.stopRecordingScreen(options);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (await terminateBackgroundScreenRecording(this.adb, true)) {
|
|
40
|
+
this.log.warn(
|
|
41
|
+
`There were some ${SCREENRECORD_BINARY} process leftovers running ` +
|
|
42
|
+
`in the background. Make sure you stop screen recording each time after it is started, ` +
|
|
43
|
+
`otherwise the recorded media might quickly exceed all the free space on the device under test.`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!_.isEmpty(this._screenRecordingProperties)) {
|
|
48
|
+
// XXX: this doesn't need to be done in serial, does it?
|
|
49
|
+
for (const record of this._screenRecordingProperties.records || []) {
|
|
50
|
+
await this.adb.rimraf(record);
|
|
51
|
+
}
|
|
52
|
+
this._screenRecordingProperties = undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const timeout = parseFloat(String(timeLimit));
|
|
56
|
+
if (isNaN(timeout) || timeout > MAX_TIME_SEC || timeout <= 0) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`The timeLimit value must be in range [1, ${MAX_TIME_SEC}] seconds. ` +
|
|
59
|
+
`The value of '${timeLimit}' has been passed instead.`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this._screenRecordingProperties = {
|
|
64
|
+
timer: new timing.Timer().start(),
|
|
65
|
+
videoSize,
|
|
66
|
+
timeLimit,
|
|
67
|
+
currentTimeLimit: timeLimit,
|
|
68
|
+
bitRate,
|
|
69
|
+
bugReport,
|
|
70
|
+
records: [],
|
|
71
|
+
recordingProcess: null,
|
|
72
|
+
stopped: false,
|
|
73
|
+
};
|
|
74
|
+
await scheduleScreenRecord.bind(this)(this._screenRecordingProperties);
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
*
|
|
80
|
+
* @this {import('../driver').AndroidDriver}
|
|
81
|
+
* @param {import('./types').StopScreenRecordingOpts} [options={}]
|
|
82
|
+
* @returns {Promise<string>}
|
|
83
|
+
*/
|
|
84
|
+
export async function stopRecordingScreen(options = {}) {
|
|
85
|
+
await verifyScreenRecordIsSupported(this.adb, this.isEmulator());
|
|
86
|
+
|
|
87
|
+
if (!_.isEmpty(this._screenRecordingProperties)) {
|
|
88
|
+
this._screenRecordingProperties.stopped = true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await terminateBackgroundScreenRecording(this.adb, false);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
this.log.warn(/** @type {Error} */ (err).message);
|
|
95
|
+
if (!_.isEmpty(this._screenRecordingProperties)) {
|
|
96
|
+
this.log.warn('The resulting video might be corrupted');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (_.isEmpty(this._screenRecordingProperties)) {
|
|
101
|
+
this.log.info(
|
|
102
|
+
`Screen recording has not been previously started by Appium. There is nothing to stop`,
|
|
103
|
+
);
|
|
104
|
+
return '';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (
|
|
108
|
+
this._screenRecordingProperties.recordingProcess &&
|
|
109
|
+
this._screenRecordingProperties.recordingProcess.isRunning
|
|
110
|
+
) {
|
|
111
|
+
try {
|
|
112
|
+
await this._screenRecordingProperties.recordingProcess.stop(
|
|
113
|
+
'SIGINT',
|
|
114
|
+
PROCESS_SHUTDOWN_TIMEOUT,
|
|
115
|
+
);
|
|
116
|
+
} catch (e) {
|
|
117
|
+
this.log.errorAndThrow(
|
|
118
|
+
`Unable to stop screen recording within ${PROCESS_SHUTDOWN_TIMEOUT}ms`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
this._screenRecordingProperties.recordingProcess = null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (_.isEmpty(this._screenRecordingProperties.records)) {
|
|
125
|
+
this.log.errorAndThrow(
|
|
126
|
+
`No screen recordings have been stored on the device so far. ` +
|
|
127
|
+
`Are you sure the ${SCREENRECORD_BINARY} utility works as expected?`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const tmpRoot = await tempDir.openDir();
|
|
132
|
+
try {
|
|
133
|
+
const localRecords = [];
|
|
134
|
+
for (const pathOnDevice of this._screenRecordingProperties.records) {
|
|
135
|
+
const relativePath = path.resolve(tmpRoot, path.posix.basename(pathOnDevice));
|
|
136
|
+
localRecords.push(relativePath);
|
|
137
|
+
await this.adb.pull(pathOnDevice, relativePath);
|
|
138
|
+
await this.adb.rimraf(pathOnDevice);
|
|
139
|
+
}
|
|
140
|
+
let resultFilePath = /** @type {string} */ (_.last(localRecords));
|
|
141
|
+
if (localRecords.length > 1) {
|
|
142
|
+
this.log.info(`Got ${localRecords.length} screen recordings. Trying to merge them`);
|
|
143
|
+
try {
|
|
144
|
+
resultFilePath = await mergeScreenRecords.bind(this)(localRecords);
|
|
145
|
+
} catch (e) {
|
|
146
|
+
this.log.warn(
|
|
147
|
+
`Cannot merge the recorded files. The most recent screen recording is going to be returned as the result. ` +
|
|
148
|
+
`Original error: ${/** @type {Error} */ (e).message}`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (_.isEmpty(options.remotePath)) {
|
|
153
|
+
const {size} = await fs.stat(resultFilePath);
|
|
154
|
+
this.log.debug(
|
|
155
|
+
`The size of the resulting screen recording is ${util.toReadableSizeString(size)}`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return await uploadRecordedMedia(resultFilePath, options.remotePath, options);
|
|
159
|
+
} finally {
|
|
160
|
+
await fs.rimraf(tmpRoot);
|
|
161
|
+
this._screenRecordingProperties = undefined;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// #region Internal helpers
|
|
166
|
+
|
|
21
167
|
/**
|
|
22
168
|
*
|
|
23
169
|
* @param {string} localFile
|
|
@@ -56,24 +202,22 @@ async function verifyScreenRecordIsSupported(adb, isEmulator) {
|
|
|
56
202
|
const apiLevel = await adb.getApiLevel();
|
|
57
203
|
if (isEmulator && apiLevel < MIN_EMULATOR_API_LEVEL) {
|
|
58
204
|
throw new Error(
|
|
59
|
-
`Screen recording does not work on emulators running Android API level less than ${MIN_EMULATOR_API_LEVEL}
|
|
205
|
+
`Screen recording does not work on emulators running Android API level less than ${MIN_EMULATOR_API_LEVEL}`,
|
|
60
206
|
);
|
|
61
207
|
}
|
|
62
208
|
if (apiLevel < 19) {
|
|
63
209
|
throw new Error(
|
|
64
|
-
`Screen recording not available on API Level ${apiLevel}. Minimum API Level is 19
|
|
210
|
+
`Screen recording not available on API Level ${apiLevel}. Minimum API Level is 19.`,
|
|
65
211
|
);
|
|
66
212
|
}
|
|
67
213
|
}
|
|
68
214
|
|
|
69
215
|
/**
|
|
70
|
-
*
|
|
71
|
-
* @param {ADB} adb
|
|
216
|
+
* @this {import('../driver').AndroidDriver}
|
|
72
217
|
* @param {import('@appium/types').StringRecord} recordingProperties
|
|
73
|
-
* @param {import('@appium/types').AppiumLogger} [log]
|
|
74
218
|
* @returns {Promise<void>}
|
|
75
219
|
*/
|
|
76
|
-
async function scheduleScreenRecord(
|
|
220
|
+
async function scheduleScreenRecord(recordingProperties) {
|
|
77
221
|
if (recordingProperties.stopped) {
|
|
78
222
|
return;
|
|
79
223
|
}
|
|
@@ -88,7 +232,7 @@ async function scheduleScreenRecord(adb, recordingProperties, log) {
|
|
|
88
232
|
}
|
|
89
233
|
}
|
|
90
234
|
const pathOnDevice = `/sdcard/${util.uuidV4().substring(0, 8)}${DEFAULT_EXT}`;
|
|
91
|
-
const recordingProc = adb.screenrecord(pathOnDevice, {
|
|
235
|
+
const recordingProc = this.adb.screenrecord(pathOnDevice, {
|
|
92
236
|
videoSize,
|
|
93
237
|
bitRate,
|
|
94
238
|
timeLimit: currentTimeLimit,
|
|
@@ -100,10 +244,10 @@ async function scheduleScreenRecord(adb, recordingProperties, log) {
|
|
|
100
244
|
return;
|
|
101
245
|
}
|
|
102
246
|
const currentDuration = timer.getDuration().asSeconds.toFixed(0);
|
|
103
|
-
log
|
|
247
|
+
this.log.debug(`The overall screen recording duration is ${currentDuration}s so far`);
|
|
104
248
|
const timeLimitInt = parseInt(timeLimit, 10);
|
|
105
249
|
if (isNaN(timeLimitInt) || currentDuration >= timeLimitInt) {
|
|
106
|
-
log
|
|
250
|
+
this.log.debug('There is no need to start the next recording chunk');
|
|
107
251
|
return;
|
|
108
252
|
}
|
|
109
253
|
|
|
@@ -112,15 +256,15 @@ async function scheduleScreenRecord(adb, recordingProperties, log) {
|
|
|
112
256
|
recordingProperties.currentTimeLimit < MAX_RECORDING_TIME_SEC
|
|
113
257
|
? recordingProperties.currentTimeLimit
|
|
114
258
|
: MAX_RECORDING_TIME_SEC;
|
|
115
|
-
log
|
|
259
|
+
this.log.debug(
|
|
116
260
|
`Starting the next ${chunkDuration}s-chunk ` +
|
|
117
|
-
`of screen recording in order to achieve ${timeLimitInt}s total duration
|
|
261
|
+
`of screen recording in order to achieve ${timeLimitInt}s total duration`,
|
|
118
262
|
);
|
|
119
263
|
(async () => {
|
|
120
264
|
try {
|
|
121
|
-
await scheduleScreenRecord(
|
|
265
|
+
await scheduleScreenRecord.bind(this)(recordingProperties);
|
|
122
266
|
} catch (e) {
|
|
123
|
-
log
|
|
267
|
+
this.log.error(/** @type {Error} */ (e).stack);
|
|
124
268
|
recordingProperties.stopped = true;
|
|
125
269
|
}
|
|
126
270
|
})();
|
|
@@ -128,14 +272,14 @@ async function scheduleScreenRecord(adb, recordingProperties, log) {
|
|
|
128
272
|
|
|
129
273
|
await recordingProc.start(0);
|
|
130
274
|
try {
|
|
131
|
-
await waitForCondition(async () => await adb.fileExists(pathOnDevice), {
|
|
275
|
+
await waitForCondition(async () => await this.adb.fileExists(pathOnDevice), {
|
|
132
276
|
waitMs: RETRY_TIMEOUT,
|
|
133
277
|
intervalMs: RETRY_PAUSE,
|
|
134
278
|
});
|
|
135
279
|
} catch (e) {
|
|
136
280
|
throw new Error(
|
|
137
281
|
`The expected screen record file '${pathOnDevice}' does not exist after ${RETRY_TIMEOUT}ms. ` +
|
|
138
|
-
`Is ${SCREENRECORD_BINARY} utility available and operational on the device under test
|
|
282
|
+
`Is ${SCREENRECORD_BINARY} utility available and operational on the device under test?`,
|
|
139
283
|
);
|
|
140
284
|
}
|
|
141
285
|
|
|
@@ -145,29 +289,29 @@ async function scheduleScreenRecord(adb, recordingProperties, log) {
|
|
|
145
289
|
|
|
146
290
|
/**
|
|
147
291
|
*
|
|
292
|
+
* @this {import('../driver').AndroidDriver}
|
|
148
293
|
* @param {string[]} mediaFiles
|
|
149
|
-
* @param {import('@appium/types').AppiumLogger} [log]
|
|
150
294
|
* @returns {Promise<string>}
|
|
151
295
|
*/
|
|
152
|
-
async function mergeScreenRecords(mediaFiles
|
|
296
|
+
async function mergeScreenRecords(mediaFiles) {
|
|
153
297
|
try {
|
|
154
298
|
await fs.which(FFMPEG_BINARY);
|
|
155
299
|
} catch (e) {
|
|
156
300
|
throw new Error(
|
|
157
|
-
`${FFMPEG_BINARY} utility is not available in PATH. Please install it from https://www.ffmpeg.org
|
|
301
|
+
`${FFMPEG_BINARY} utility is not available in PATH. Please install it from https://www.ffmpeg.org/`,
|
|
158
302
|
);
|
|
159
303
|
}
|
|
160
304
|
const configContent = mediaFiles.map((x) => `file '${x}'`).join('\n');
|
|
161
305
|
const configFile = path.resolve(path.dirname(mediaFiles[0]), 'config.txt');
|
|
162
306
|
await fs.writeFile(configFile, configContent, 'utf8');
|
|
163
|
-
log
|
|
307
|
+
this.log.debug(`Generated ffmpeg merging config '${configFile}' with items:\n${configContent}`);
|
|
164
308
|
const result = path.resolve(
|
|
165
309
|
path.dirname(mediaFiles[0]),
|
|
166
|
-
`merge_${Math.floor(+new Date())}${DEFAULT_EXT}
|
|
310
|
+
`merge_${Math.floor(+new Date())}${DEFAULT_EXT}`,
|
|
167
311
|
);
|
|
168
312
|
const args = ['-safe', '0', '-f', 'concat', '-i', configFile, '-c', 'copy', result];
|
|
169
|
-
log
|
|
170
|
-
`Initiating screen records merging using the command '${FFMPEG_BINARY} ${args.join(' ')}'
|
|
313
|
+
this.log.info(
|
|
314
|
+
`Initiating screen records merging using the command '${FFMPEG_BINARY} ${args.join(' ')}'`,
|
|
171
315
|
);
|
|
172
316
|
await exec(FFMPEG_BINARY, args);
|
|
173
317
|
return result;
|
|
@@ -194,155 +338,12 @@ async function terminateBackgroundScreenRecording(adb, force = true) {
|
|
|
194
338
|
return true;
|
|
195
339
|
} catch (err) {
|
|
196
340
|
throw new Error(
|
|
197
|
-
`Unable to stop the background screen recording: ${/** @type {Error} */ (err).message}
|
|
341
|
+
`Unable to stop the background screen recording: ${/** @type {Error} */ (err).message}`,
|
|
198
342
|
);
|
|
199
343
|
}
|
|
200
344
|
}
|
|
201
345
|
|
|
202
|
-
|
|
203
|
-
* @type {import('./mixins').RecordScreenMixin & ThisType<import('../driver').AndroidDriver>}
|
|
204
|
-
* @satisfies {import('@appium/types').ExternalDriver}
|
|
205
|
-
*/
|
|
206
|
-
const RecordScreenMixin = {
|
|
207
|
-
async startRecordingScreen(options = {}) {
|
|
208
|
-
await verifyScreenRecordIsSupported(this.adb, this.isEmulator());
|
|
209
|
-
|
|
210
|
-
let result = '';
|
|
211
|
-
const {
|
|
212
|
-
videoSize,
|
|
213
|
-
timeLimit = DEFAULT_RECORDING_TIME_SEC,
|
|
214
|
-
bugReport,
|
|
215
|
-
bitRate,
|
|
216
|
-
forceRestart,
|
|
217
|
-
} = options;
|
|
218
|
-
if (!forceRestart) {
|
|
219
|
-
result = await this.stopRecordingScreen(options);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (await terminateBackgroundScreenRecording(this.adb, true)) {
|
|
223
|
-
this.log.warn(
|
|
224
|
-
`There were some ${SCREENRECORD_BINARY} process leftovers running ` +
|
|
225
|
-
`in the background. Make sure you stop screen recording each time after it is started, ` +
|
|
226
|
-
`otherwise the recorded media might quickly exceed all the free space on the device under test.`
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (!_.isEmpty(this._screenRecordingProperties)) {
|
|
231
|
-
// XXX: this doesn't need to be done in serial, does it?
|
|
232
|
-
for (const record of this._screenRecordingProperties.records || []) {
|
|
233
|
-
await this.adb.rimraf(record);
|
|
234
|
-
}
|
|
235
|
-
this._screenRecordingProperties = undefined;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const timeout = parseFloat(String(timeLimit));
|
|
239
|
-
if (isNaN(timeout) || timeout > MAX_TIME_SEC || timeout <= 0) {
|
|
240
|
-
throw new Error(
|
|
241
|
-
`The timeLimit value must be in range [1, ${MAX_TIME_SEC}] seconds. ` +
|
|
242
|
-
`The value of '${timeLimit}' has been passed instead.`
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
this._screenRecordingProperties = {
|
|
247
|
-
timer: new timing.Timer().start(),
|
|
248
|
-
videoSize,
|
|
249
|
-
timeLimit,
|
|
250
|
-
currentTimeLimit: timeLimit,
|
|
251
|
-
bitRate,
|
|
252
|
-
bugReport,
|
|
253
|
-
records: [],
|
|
254
|
-
recordingProcess: null,
|
|
255
|
-
stopped: false,
|
|
256
|
-
};
|
|
257
|
-
await scheduleScreenRecord(this.adb, this._screenRecordingProperties, this.log);
|
|
258
|
-
return result;
|
|
259
|
-
},
|
|
260
|
-
|
|
261
|
-
async stopRecordingScreen(options = {}) {
|
|
262
|
-
await verifyScreenRecordIsSupported(this.adb, this.isEmulator());
|
|
263
|
-
|
|
264
|
-
if (!_.isEmpty(this._screenRecordingProperties)) {
|
|
265
|
-
this._screenRecordingProperties.stopped = true;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
try {
|
|
269
|
-
await terminateBackgroundScreenRecording(this.adb, false);
|
|
270
|
-
} catch (err) {
|
|
271
|
-
this.log.warn(/** @type {Error} */ (err).message);
|
|
272
|
-
if (!_.isEmpty(this._screenRecordingProperties)) {
|
|
273
|
-
this.log.warn('The resulting video might be corrupted');
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (_.isEmpty(this._screenRecordingProperties)) {
|
|
278
|
-
this.log.info(
|
|
279
|
-
`Screen recording has not been previously started by Appium. There is nothing to stop`
|
|
280
|
-
);
|
|
281
|
-
return '';
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (
|
|
285
|
-
this._screenRecordingProperties.recordingProcess &&
|
|
286
|
-
this._screenRecordingProperties.recordingProcess.isRunning
|
|
287
|
-
) {
|
|
288
|
-
try {
|
|
289
|
-
await this._screenRecordingProperties.recordingProcess.stop(
|
|
290
|
-
'SIGINT',
|
|
291
|
-
PROCESS_SHUTDOWN_TIMEOUT
|
|
292
|
-
);
|
|
293
|
-
} catch (e) {
|
|
294
|
-
this.log.errorAndThrow(
|
|
295
|
-
`Unable to stop screen recording within ${PROCESS_SHUTDOWN_TIMEOUT}ms`
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
this._screenRecordingProperties.recordingProcess = null;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (_.isEmpty(this._screenRecordingProperties.records)) {
|
|
302
|
-
this.log.errorAndThrow(
|
|
303
|
-
`No screen recordings have been stored on the device so far. ` +
|
|
304
|
-
`Are you sure the ${SCREENRECORD_BINARY} utility works as expected?`
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const tmpRoot = await tempDir.openDir();
|
|
309
|
-
try {
|
|
310
|
-
const localRecords = [];
|
|
311
|
-
for (const pathOnDevice of this._screenRecordingProperties.records) {
|
|
312
|
-
const relativePath = path.resolve(tmpRoot, path.posix.basename(pathOnDevice));
|
|
313
|
-
localRecords.push(relativePath);
|
|
314
|
-
await this.adb.pull(pathOnDevice, relativePath);
|
|
315
|
-
await this.adb.rimraf(pathOnDevice);
|
|
316
|
-
}
|
|
317
|
-
let resultFilePath = /** @type {string} */ (_.last(localRecords));
|
|
318
|
-
if (localRecords.length > 1) {
|
|
319
|
-
this.log.info(`Got ${localRecords.length} screen recordings. Trying to merge them`);
|
|
320
|
-
try {
|
|
321
|
-
resultFilePath = await mergeScreenRecords(localRecords, this.log);
|
|
322
|
-
} catch (e) {
|
|
323
|
-
this.log.warn(
|
|
324
|
-
`Cannot merge the recorded files. The most recent screen recording is going to be returned as the result. ` +
|
|
325
|
-
`Original error: ${/** @type {Error} */ (e).message}`
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
if (_.isEmpty(options.remotePath)) {
|
|
330
|
-
const {size} = await fs.stat(resultFilePath);
|
|
331
|
-
this.log.debug(
|
|
332
|
-
`The size of the resulting screen recording is ${util.toReadableSizeString(size)}`
|
|
333
|
-
);
|
|
334
|
-
}
|
|
335
|
-
return await uploadRecordedMedia(resultFilePath, options.remotePath, options);
|
|
336
|
-
} finally {
|
|
337
|
-
await fs.rimraf(tmpRoot);
|
|
338
|
-
this._screenRecordingProperties = undefined;
|
|
339
|
-
}
|
|
340
|
-
},
|
|
341
|
-
};
|
|
342
|
-
|
|
343
|
-
mixin(RecordScreenMixin);
|
|
344
|
-
|
|
345
|
-
export default RecordScreenMixin;
|
|
346
|
+
// #endregion
|
|
346
347
|
|
|
347
348
|
/**
|
|
348
349
|
* @typedef {import('appium-adb').ADB} ADB
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import _ from 'lodash';
|
|
3
|
+
import {fs, util} from '@appium/support';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @this {import('../driver').AndroidDriver}
|
|
7
|
+
* @param {string?} [language=null]
|
|
8
|
+
* @returns {Promise<import('@appium/types').StringRecord>}}
|
|
9
|
+
*/
|
|
10
|
+
export async function getStrings(language = null) {
|
|
11
|
+
if (!language) {
|
|
12
|
+
language = await this.adb.getDeviceLanguage();
|
|
13
|
+
this.log.info(`No language specified, returning strings for: ${language}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Clients require the resulting mapping to have both keys
|
|
17
|
+
// and values of type string
|
|
18
|
+
/** @param {import('@appium/types').StringRecord} mapping */
|
|
19
|
+
const preprocessStringsMap = (mapping) => {
|
|
20
|
+
/** @type {import('@appium/types').StringRecord} */
|
|
21
|
+
const result = {};
|
|
22
|
+
for (const [key, value] of _.toPairs(mapping)) {
|
|
23
|
+
result[key] = _.isString(value) ? value : JSON.stringify(value);
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
if (!this.apkStrings[language]) {
|
|
29
|
+
this.apkStrings[language] = await extractStringsFromResources.bind(this)(language);
|
|
30
|
+
}
|
|
31
|
+
const mapping = JSON.parse(await fs.readFile(this.apkStrings[language], 'utf-8'));
|
|
32
|
+
return preprocessStringsMap(mapping);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @this {import('../driver').AndroidDriver}
|
|
37
|
+
* @param {string} language
|
|
38
|
+
* @param {string} country
|
|
39
|
+
* @param {string} [script]
|
|
40
|
+
* @returns {Promise<void>}}
|
|
41
|
+
*/
|
|
42
|
+
export async function ensureDeviceLocale(language, country, script) {
|
|
43
|
+
await this.settingsApp.setDeviceLocale(language, country, script);
|
|
44
|
+
|
|
45
|
+
if (!(await this.adb.ensureCurrentLocale(language, country, script))) {
|
|
46
|
+
const message = script
|
|
47
|
+
? `language: ${language}, country: ${country} and script: ${script}`
|
|
48
|
+
: `language: ${language} and country: ${country}`;
|
|
49
|
+
throw new Error(`Failed to set ${message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// #region Internal helpers
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @this {import('../driver').AndroidDriver}
|
|
57
|
+
* @param {string?} [language]
|
|
58
|
+
* @param {import('../driver').AndroidDriverOpts?} [opts=null]
|
|
59
|
+
* @returns {Promise<string>};
|
|
60
|
+
*/
|
|
61
|
+
async function extractStringsFromResources(language, opts = null) {
|
|
62
|
+
const caps = opts ?? this.opts;
|
|
63
|
+
|
|
64
|
+
/** @type {string|undefined} */
|
|
65
|
+
let app;
|
|
66
|
+
try {
|
|
67
|
+
app =
|
|
68
|
+
caps.app ||
|
|
69
|
+
(caps.appPackage && caps.tmpDir && (await this.adb.pullApk(caps.appPackage, caps.tmpDir)));
|
|
70
|
+
} catch (err) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Failed to pull an apk from '${caps.appPackage}' to '${caps.tmpDir}'. Original error: ${err.message}`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!app || !(await fs.exists(app))) {
|
|
77
|
+
throw new Error(`Could not extract app strings, no app or package specified`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const stringsTmpDir = path.resolve(String(caps.tmpDir), util.uuidV4());
|
|
81
|
+
try {
|
|
82
|
+
this.log.debug(
|
|
83
|
+
`Extracting strings from '${app}' for the language '${language || 'default'}' into '${stringsTmpDir}'`,
|
|
84
|
+
);
|
|
85
|
+
const {localPath} = await this.adb.extractStringsFromApk(app, language ?? null, stringsTmpDir);
|
|
86
|
+
return localPath;
|
|
87
|
+
} catch (err) {
|
|
88
|
+
throw new Error(`Could not extract app strings. Original error: ${err.message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// #endregion
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @typedef {import('appium-adb').ADB} ADB
|
|
96
|
+
*/
|
package/lib/commands/shell.js
CHANGED
|
@@ -1,56 +1,42 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
|
|
3
1
|
import {util} from '@appium/support';
|
|
4
2
|
import {errors} from 'appium/driver';
|
|
5
3
|
import _ from 'lodash';
|
|
6
4
|
import {exec} from 'teen_process';
|
|
7
5
|
import {ADB_SHELL_FEATURE} from '../utils';
|
|
8
|
-
import {mixin} from './mixins';
|
|
9
6
|
|
|
10
7
|
/**
|
|
11
|
-
* @
|
|
12
|
-
* @
|
|
8
|
+
* @this {import('../driver').AndroidDriver}
|
|
9
|
+
* @param {import('./types').ShellOpts} [opts={}]
|
|
10
|
+
* @returns {Promise<string | {stderr: string; stdout: string}>};
|
|
13
11
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const {
|
|
18
|
-
command,
|
|
19
|
-
args = /** @type {string[]} */ ([]),
|
|
20
|
-
timeout = 20000,
|
|
21
|
-
includeStderr,
|
|
22
|
-
} = opts ?? {};
|
|
12
|
+
export async function mobileShell(opts) {
|
|
13
|
+
this.ensureFeatureEnabled(ADB_SHELL_FEATURE);
|
|
14
|
+
const {command, args = /** @type {string[]} */ ([]), timeout = 20000, includeStderr} = opts ?? {};
|
|
23
15
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
16
|
+
if (!_.isString(command)) {
|
|
17
|
+
throw new errors.InvalidArgumentError(`The 'command' argument is mandatory`);
|
|
18
|
+
}
|
|
27
19
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
return stdout;
|
|
39
|
-
} catch (e) {
|
|
40
|
-
const err = /** @type {import('teen_process').ExecError} */ (e);
|
|
41
|
-
this.log.errorAndThrow(
|
|
42
|
-
`Cannot execute the '${command}' shell command. ` +
|
|
43
|
-
`Original error: ${err.message}. ` +
|
|
44
|
-
`StdOut: ${err.stdout}. StdErr: ${err.stderr}`
|
|
45
|
-
);
|
|
46
|
-
throw new Error(); // unreachable; for TS
|
|
20
|
+
const adbArgs = [...this.adb.executable.defaultArgs, 'shell', command, ..._.castArray(args)];
|
|
21
|
+
this.log.debug(`Running '${this.adb.executable.path} ${util.quote(adbArgs)}'`);
|
|
22
|
+
try {
|
|
23
|
+
const {stdout, stderr} = await exec(this.adb.executable.path, adbArgs, {timeout});
|
|
24
|
+
if (includeStderr) {
|
|
25
|
+
return {
|
|
26
|
+
stdout,
|
|
27
|
+
stderr,
|
|
28
|
+
};
|
|
47
29
|
}
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
30
|
+
return stdout;
|
|
31
|
+
} catch (e) {
|
|
32
|
+
const err = /** @type {import('teen_process').ExecError} */ (e);
|
|
33
|
+
throw this.log.errorAndThrow(
|
|
34
|
+
`Cannot execute the '${command}' shell command. ` +
|
|
35
|
+
`Original error: ${err.message}. ` +
|
|
36
|
+
`StdOut: ${err.stdout}. StdErr: ${err.stderr}`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
54
40
|
|
|
55
41
|
/**
|
|
56
42
|
* @typedef {import('appium-adb').ADB} ADB
|