appium-android-driver 12.4.7 → 12.4.8
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 +6 -0
- package/build/lib/commands/file-actions.d.ts +37 -19
- package/build/lib/commands/file-actions.d.ts.map +1 -1
- package/build/lib/commands/file-actions.js +44 -58
- package/build/lib/commands/file-actions.js.map +1 -1
- package/build/lib/commands/intent.d.ts +103 -107
- package/build/lib/commands/intent.d.ts.map +1 -1
- package/build/lib/commands/intent.js +103 -97
- package/build/lib/commands/intent.js.map +1 -1
- package/build/lib/commands/recordscreen.d.ts +25 -40
- package/build/lib/commands/recordscreen.d.ts.map +1 -1
- package/build/lib/commands/recordscreen.js +46 -63
- package/build/lib/commands/recordscreen.js.map +1 -1
- package/build/lib/commands/types.d.ts.map +1 -1
- package/build/lib/driver.d.ts +2 -1
- package/build/lib/driver.d.ts.map +1 -1
- package/build/lib/driver.js.map +1 -1
- package/lib/commands/{file-actions.js → file-actions.ts} +88 -74
- package/lib/commands/intent.ts +422 -0
- package/lib/commands/{recordscreen.js → recordscreen.ts} +77 -73
- package/lib/commands/types.ts +17 -0
- package/lib/driver.ts +2 -1
- package/package.json +1 -1
- package/lib/commands/intent.js +0 -409
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import {fs, net, system, tempDir, timing, util} from '@appium/support';
|
|
2
|
+
import type {NetOptions, HttpUploadOptions} from '@appium/support';
|
|
2
3
|
import {waitForCondition} from 'asyncbox';
|
|
3
4
|
import _ from 'lodash';
|
|
4
5
|
import path from 'path';
|
|
5
6
|
import {exec} from 'teen_process';
|
|
7
|
+
import type {AndroidDriver} from '../driver';
|
|
8
|
+
import type {ADB} from 'appium-adb';
|
|
9
|
+
import type {StartScreenRecordingOpts, StopScreenRecordingOpts, ScreenRecordingProperties} from './types';
|
|
6
10
|
|
|
7
11
|
const RETRY_PAUSE = 300;
|
|
8
12
|
const RETRY_TIMEOUT = 5000;
|
|
@@ -17,12 +21,22 @@ const FFMPEG_BINARY = `ffmpeg${system.isWindows() ? '.exe' : ''}`;
|
|
|
17
21
|
const ADB_PULL_TIMEOUT = 5 * 60 * 1000;
|
|
18
22
|
|
|
19
23
|
/**
|
|
24
|
+
* Starts screen recording on the Android device.
|
|
20
25
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
26
|
+
* This method uses Android's `screenrecord` command to capture the screen.
|
|
27
|
+
* The recording can be configured with various options such as video size,
|
|
28
|
+
* bit rate, time limit, and more.
|
|
29
|
+
*
|
|
30
|
+
* @param options Recording options. See {@link StartScreenRecordingOpts} for details.
|
|
31
|
+
* @returns Promise that resolves to the result of stopping any previous recording,
|
|
32
|
+
* or an empty string if no previous recording was active.
|
|
33
|
+
* @throws {Error} If screen recording is not supported on the device or emulator,
|
|
34
|
+
* or if the time limit is invalid.
|
|
24
35
|
*/
|
|
25
|
-
export async function startRecordingScreen(
|
|
36
|
+
export async function startRecordingScreen(
|
|
37
|
+
this: AndroidDriver,
|
|
38
|
+
options: StartScreenRecordingOpts = {},
|
|
39
|
+
): Promise<string> {
|
|
26
40
|
await verifyScreenRecordIsSupported(this.adb, this.isEmulator());
|
|
27
41
|
|
|
28
42
|
let result = '';
|
|
@@ -47,7 +61,8 @@ export async function startRecordingScreen(options = {}) {
|
|
|
47
61
|
|
|
48
62
|
if (!_.isEmpty(this._screenRecordingProperties)) {
|
|
49
63
|
// XXX: this doesn't need to be done in serial, does it?
|
|
50
|
-
|
|
64
|
+
const props = this._screenRecordingProperties;
|
|
65
|
+
for (const record of props.records || []) {
|
|
51
66
|
await this.adb.rimraf(record);
|
|
52
67
|
}
|
|
53
68
|
this._screenRecordingProperties = undefined;
|
|
@@ -61,7 +76,7 @@ export async function startRecordingScreen(options = {}) {
|
|
|
61
76
|
);
|
|
62
77
|
}
|
|
63
78
|
|
|
64
|
-
|
|
79
|
+
const recordingProps: ScreenRecordingProperties = {
|
|
65
80
|
timer: new timing.Timer().start(),
|
|
66
81
|
videoSize,
|
|
67
82
|
timeLimit,
|
|
@@ -72,42 +87,55 @@ export async function startRecordingScreen(options = {}) {
|
|
|
72
87
|
recordingProcess: null,
|
|
73
88
|
stopped: false,
|
|
74
89
|
};
|
|
75
|
-
|
|
90
|
+
this._screenRecordingProperties = recordingProps;
|
|
91
|
+
await scheduleScreenRecord.bind(this)(recordingProps);
|
|
76
92
|
return result;
|
|
77
93
|
}
|
|
78
94
|
|
|
79
95
|
/**
|
|
96
|
+
* Stops screen recording and returns the recorded video.
|
|
97
|
+
*
|
|
98
|
+
* This method stops any active screen recording session and returns the recorded
|
|
99
|
+
* video as a base64-encoded string or uploads it to a remote location if specified.
|
|
100
|
+
* If multiple recording chunks were created (for long recordings), they will be
|
|
101
|
+
* merged using ffmpeg if available.
|
|
80
102
|
*
|
|
81
|
-
* @
|
|
82
|
-
* @
|
|
83
|
-
*
|
|
103
|
+
* @param options Stop recording options. See {@link StopScreenRecordingOpts} for details.
|
|
104
|
+
* @returns Promise that resolves to the recorded video as a base64-encoded string
|
|
105
|
+
* if `remotePath` is not provided, or an empty string if the video was uploaded to a remote location.
|
|
106
|
+
* @throws {Error} If screen recording is not supported, no recording was active,
|
|
107
|
+
* or if the recording process cannot be stopped.
|
|
84
108
|
*/
|
|
85
|
-
export async function stopRecordingScreen(
|
|
109
|
+
export async function stopRecordingScreen(
|
|
110
|
+
this: AndroidDriver,
|
|
111
|
+
options: StopScreenRecordingOpts = {},
|
|
112
|
+
): Promise<string> {
|
|
86
113
|
await verifyScreenRecordIsSupported(this.adb, this.isEmulator());
|
|
87
114
|
|
|
88
|
-
|
|
89
|
-
|
|
115
|
+
const props = this._screenRecordingProperties;
|
|
116
|
+
if (!_.isEmpty(props)) {
|
|
117
|
+
props.stopped = true;
|
|
90
118
|
}
|
|
91
119
|
|
|
92
120
|
try {
|
|
93
121
|
await terminateBackgroundScreenRecording(this.adb, false);
|
|
94
122
|
} catch (err) {
|
|
95
|
-
this.log.warn(
|
|
96
|
-
if (!_.isEmpty(
|
|
123
|
+
this.log.warn((err as Error).message);
|
|
124
|
+
if (!_.isEmpty(props)) {
|
|
97
125
|
this.log.warn('The resulting video might be corrupted');
|
|
98
126
|
}
|
|
99
127
|
}
|
|
100
128
|
|
|
101
|
-
if (_.isEmpty(
|
|
129
|
+
if (_.isEmpty(props)) {
|
|
102
130
|
this.log.info(
|
|
103
131
|
`Screen recording has not been previously started by Appium. There is nothing to stop`,
|
|
104
132
|
);
|
|
105
133
|
return '';
|
|
106
134
|
}
|
|
107
135
|
|
|
108
|
-
if (
|
|
136
|
+
if (props.recordingProcess?.isRunning) {
|
|
109
137
|
try {
|
|
110
|
-
await
|
|
138
|
+
await props.recordingProcess.stop(
|
|
111
139
|
'SIGINT',
|
|
112
140
|
PROCESS_SHUTDOWN_TIMEOUT,
|
|
113
141
|
);
|
|
@@ -116,10 +144,10 @@ export async function stopRecordingScreen(options = {}) {
|
|
|
116
144
|
`Unable to stop screen recording within ${PROCESS_SHUTDOWN_TIMEOUT}ms`,
|
|
117
145
|
);
|
|
118
146
|
}
|
|
119
|
-
|
|
147
|
+
props.recordingProcess = null;
|
|
120
148
|
}
|
|
121
149
|
|
|
122
|
-
if (_.isEmpty(
|
|
150
|
+
if (_.isEmpty(props.records)) {
|
|
123
151
|
throw this.log.errorWithException(
|
|
124
152
|
`No screen recordings have been stored on the device so far. ` +
|
|
125
153
|
`Are you sure the ${SCREENRECORD_BINARY} utility works as expected?`,
|
|
@@ -128,14 +156,14 @@ export async function stopRecordingScreen(options = {}) {
|
|
|
128
156
|
|
|
129
157
|
const tmpRoot = await tempDir.openDir();
|
|
130
158
|
try {
|
|
131
|
-
const localRecords = [];
|
|
132
|
-
for (const pathOnDevice of
|
|
159
|
+
const localRecords: string[] = [];
|
|
160
|
+
for (const pathOnDevice of props.records) {
|
|
133
161
|
const relativePath = path.resolve(tmpRoot, path.posix.basename(pathOnDevice));
|
|
134
162
|
localRecords.push(relativePath);
|
|
135
163
|
await this.adb.pull(pathOnDevice, relativePath, { timeout: ADB_PULL_TIMEOUT });
|
|
136
164
|
await this.adb.rimraf(pathOnDevice);
|
|
137
165
|
}
|
|
138
|
-
let resultFilePath =
|
|
166
|
+
let resultFilePath = _.last(localRecords) as string;
|
|
139
167
|
if (localRecords.length > 1) {
|
|
140
168
|
this.log.info(`Got ${localRecords.length} screen recordings. Trying to merge them`);
|
|
141
169
|
try {
|
|
@@ -143,7 +171,7 @@ export async function stopRecordingScreen(options = {}) {
|
|
|
143
171
|
} catch (e) {
|
|
144
172
|
this.log.warn(
|
|
145
173
|
`Cannot merge the recorded files. The most recent screen recording is going to be returned as the result. ` +
|
|
146
|
-
`Original error: ${
|
|
174
|
+
`Original error: ${(e as Error).message}`,
|
|
147
175
|
);
|
|
148
176
|
}
|
|
149
177
|
}
|
|
@@ -162,23 +190,17 @@ export async function stopRecordingScreen(options = {}) {
|
|
|
162
190
|
|
|
163
191
|
// #region Internal helpers
|
|
164
192
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
* @returns {Promise<string>}
|
|
171
|
-
*/
|
|
172
|
-
async function uploadRecordedMedia(localFile, remotePath, uploadOptions = {}) {
|
|
193
|
+
async function uploadRecordedMedia(
|
|
194
|
+
localFile: string,
|
|
195
|
+
remotePath?: string,
|
|
196
|
+
uploadOptions: StopScreenRecordingOpts = {},
|
|
197
|
+
): Promise<string> {
|
|
173
198
|
if (_.isEmpty(remotePath)) {
|
|
174
199
|
return (await util.toInMemoryBase64(localFile)).toString();
|
|
175
200
|
}
|
|
176
201
|
|
|
177
202
|
const {user, pass, method, headers, fileFieldName, formFields} = uploadOptions;
|
|
178
|
-
|
|
179
|
-
* @type {import('@appium/support').NetOptions & import('@appium/support').HttpUploadOptions}
|
|
180
|
-
*/
|
|
181
|
-
const options = {
|
|
203
|
+
const options: NetOptions & HttpUploadOptions = {
|
|
182
204
|
method: method || 'PUT',
|
|
183
205
|
headers,
|
|
184
206
|
fileFieldName,
|
|
@@ -187,16 +209,11 @@ async function uploadRecordedMedia(localFile, remotePath, uploadOptions = {}) {
|
|
|
187
209
|
if (user && pass) {
|
|
188
210
|
options.auth = {user, pass};
|
|
189
211
|
}
|
|
190
|
-
await net.uploadFile(localFile,
|
|
212
|
+
await net.uploadFile(localFile, remotePath as string, options);
|
|
191
213
|
return '';
|
|
192
214
|
}
|
|
193
215
|
|
|
194
|
-
|
|
195
|
-
*
|
|
196
|
-
* @param {ADB} adb
|
|
197
|
-
* @param {boolean} isEmulator
|
|
198
|
-
*/
|
|
199
|
-
async function verifyScreenRecordIsSupported(adb, isEmulator) {
|
|
216
|
+
async function verifyScreenRecordIsSupported(adb: ADB, isEmulator: boolean): Promise<void> {
|
|
200
217
|
const apiLevel = await adb.getApiLevel();
|
|
201
218
|
if (isEmulator && apiLevel < MIN_EMULATOR_API_LEVEL) {
|
|
202
219
|
throw new Error(
|
|
@@ -205,12 +222,10 @@ async function verifyScreenRecordIsSupported(adb, isEmulator) {
|
|
|
205
222
|
}
|
|
206
223
|
}
|
|
207
224
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
*/
|
|
213
|
-
async function scheduleScreenRecord(recordingProperties) {
|
|
225
|
+
async function scheduleScreenRecord(
|
|
226
|
+
this: AndroidDriver,
|
|
227
|
+
recordingProperties: ScreenRecordingProperties,
|
|
228
|
+
): Promise<void> {
|
|
214
229
|
if (recordingProperties.stopped) {
|
|
215
230
|
return;
|
|
216
231
|
}
|
|
@@ -219,7 +234,7 @@ async function scheduleScreenRecord(recordingProperties) {
|
|
|
219
234
|
|
|
220
235
|
let currentTimeLimit = MAX_RECORDING_TIME_SEC;
|
|
221
236
|
if (util.hasValue(recordingProperties.currentTimeLimit)) {
|
|
222
|
-
const currentTimeLimitInt = parseInt(recordingProperties.currentTimeLimit, 10);
|
|
237
|
+
const currentTimeLimitInt = parseInt(String(recordingProperties.currentTimeLimit), 10);
|
|
223
238
|
if (!isNaN(currentTimeLimitInt) && currentTimeLimitInt < MAX_RECORDING_TIME_SEC) {
|
|
224
239
|
currentTimeLimit = currentTimeLimitInt;
|
|
225
240
|
}
|
|
@@ -238,13 +253,13 @@ async function scheduleScreenRecord(recordingProperties) {
|
|
|
238
253
|
}
|
|
239
254
|
const currentDuration = timer.getDuration().asSeconds.toFixed(0);
|
|
240
255
|
this.log.debug(`The overall screen recording duration is ${currentDuration}s so far`);
|
|
241
|
-
const timeLimitInt = parseInt(timeLimit, 10);
|
|
242
|
-
if (isNaN(timeLimitInt) || currentDuration >= timeLimitInt) {
|
|
256
|
+
const timeLimitInt = parseInt(String(timeLimit), 10);
|
|
257
|
+
if (isNaN(timeLimitInt) || Number(currentDuration) >= timeLimitInt) {
|
|
243
258
|
this.log.debug('There is no need to start the next recording chunk');
|
|
244
259
|
return;
|
|
245
260
|
}
|
|
246
261
|
|
|
247
|
-
recordingProperties.currentTimeLimit = timeLimitInt - currentDuration;
|
|
262
|
+
recordingProperties.currentTimeLimit = timeLimitInt - Number(currentDuration);
|
|
248
263
|
const chunkDuration =
|
|
249
264
|
recordingProperties.currentTimeLimit < MAX_RECORDING_TIME_SEC
|
|
250
265
|
? recordingProperties.currentTimeLimit
|
|
@@ -257,7 +272,7 @@ async function scheduleScreenRecord(recordingProperties) {
|
|
|
257
272
|
try {
|
|
258
273
|
await scheduleScreenRecord.bind(this)(recordingProperties);
|
|
259
274
|
} catch (e) {
|
|
260
|
-
this.log.error(
|
|
275
|
+
this.log.error((e as Error).stack);
|
|
261
276
|
recordingProperties.stopped = true;
|
|
262
277
|
}
|
|
263
278
|
})();
|
|
@@ -280,13 +295,10 @@ async function scheduleScreenRecord(recordingProperties) {
|
|
|
280
295
|
recordingProperties.recordingProcess = recordingProc;
|
|
281
296
|
}
|
|
282
297
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
* @returns {Promise<string>}
|
|
288
|
-
*/
|
|
289
|
-
async function mergeScreenRecords(mediaFiles) {
|
|
298
|
+
async function mergeScreenRecords(
|
|
299
|
+
this: AndroidDriver,
|
|
300
|
+
mediaFiles: string[],
|
|
301
|
+
): Promise<string> {
|
|
290
302
|
try {
|
|
291
303
|
await fs.which(FFMPEG_BINARY);
|
|
292
304
|
} catch {
|
|
@@ -310,14 +322,9 @@ async function mergeScreenRecords(mediaFiles) {
|
|
|
310
322
|
return result;
|
|
311
323
|
}
|
|
312
324
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
* @param {boolean} force
|
|
317
|
-
* @returns {Promise<boolean>}
|
|
318
|
-
*/
|
|
319
|
-
async function terminateBackgroundScreenRecording(adb, force = true) {
|
|
320
|
-
const isScreenrecordRunning = async () => _.includes(await adb.listProcessStatus(), SCREENRECORD_BINARY);
|
|
325
|
+
async function terminateBackgroundScreenRecording(adb: ADB, force = true): Promise<boolean> {
|
|
326
|
+
const isScreenrecordRunning = async (): Promise<boolean> =>
|
|
327
|
+
_.includes(await adb.listProcessStatus(), SCREENRECORD_BINARY);
|
|
321
328
|
if (!await isScreenrecordRunning()) {
|
|
322
329
|
return false;
|
|
323
330
|
}
|
|
@@ -331,13 +338,10 @@ async function terminateBackgroundScreenRecording(adb, force = true) {
|
|
|
331
338
|
return true;
|
|
332
339
|
} catch (err) {
|
|
333
340
|
throw new Error(
|
|
334
|
-
`Unable to stop the background screen recording: ${
|
|
341
|
+
`Unable to stop the background screen recording: ${(err as Error).message}`,
|
|
335
342
|
);
|
|
336
343
|
}
|
|
337
344
|
}
|
|
338
345
|
|
|
339
346
|
// #endregion
|
|
340
347
|
|
|
341
|
-
/**
|
|
342
|
-
* @typedef {import('appium-adb').ADB} ADB
|
|
343
|
-
*/
|
package/lib/commands/types.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type {HTTPMethod, StringRecord} from '@appium/types';
|
|
2
2
|
import type {AndroidDriverCaps} from '../driver';
|
|
3
|
+
import type {SubProcess} from 'teen_process';
|
|
4
|
+
import {timing} from '@appium/support';
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* @privateRemarks probably better defined in `appium-adb`
|
|
@@ -618,3 +620,18 @@ export interface InjectedImageProperties {
|
|
|
618
620
|
position?: InjectedImagePosition;
|
|
619
621
|
rotation?: InjectedImageRotation;
|
|
620
622
|
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* @internal
|
|
626
|
+
*/
|
|
627
|
+
export interface ScreenRecordingProperties {
|
|
628
|
+
timer: timing.Timer;
|
|
629
|
+
videoSize?: string;
|
|
630
|
+
timeLimit: string | number;
|
|
631
|
+
currentTimeLimit?: string | number;
|
|
632
|
+
bitRate?: string | number;
|
|
633
|
+
bugReport?: boolean;
|
|
634
|
+
records: string[];
|
|
635
|
+
recordingProcess: SubProcess | null;
|
|
636
|
+
stopped: boolean;
|
|
637
|
+
}
|
package/lib/driver.ts
CHANGED
|
@@ -210,6 +210,7 @@ import {getSystemBars, mobilePerformStatusBarCommand} from './commands/system-ba
|
|
|
210
210
|
import {getDeviceTime, mobileGetDeviceTime} from './commands/time';
|
|
211
211
|
import { executeMethodMap } from './execute-method-map';
|
|
212
212
|
import { LRUCache } from 'lru-cache';
|
|
213
|
+
import type {ScreenRecordingProperties} from './commands/types';
|
|
213
214
|
|
|
214
215
|
export type AndroidDriverCaps = DriverCaps<AndroidDriverConstraints>;
|
|
215
216
|
export type W3CAndroidDriverCaps = W3CDriverCaps<AndroidDriverConstraints>;
|
|
@@ -240,7 +241,7 @@ class AndroidDriver
|
|
|
240
241
|
_wasWindowAnimationDisabled?: boolean;
|
|
241
242
|
_cachedActivityArgs: StringRecord;
|
|
242
243
|
_screenStreamingProps?: StringRecord;
|
|
243
|
-
_screenRecordingProperties?:
|
|
244
|
+
_screenRecordingProperties?: ScreenRecordingProperties;
|
|
244
245
|
_logcatWebsocketListener?: LogcatListener;
|
|
245
246
|
_bidiServerLogListener?: (...args: any[]) => void;
|
|
246
247
|
_bidiProxyUrl: string | null = null;
|