appium-xcuitest-driver 10.13.2 → 10.13.4
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 +12 -0
- package/build/lib/commands/app-management.d.ts +99 -76
- package/build/lib/commands/app-management.d.ts.map +1 -1
- package/build/lib/commands/app-management.js +83 -73
- package/build/lib/commands/app-management.js.map +1 -1
- package/build/lib/commands/general.d.ts +84 -80
- package/build/lib/commands/general.d.ts.map +1 -1
- package/build/lib/commands/general.js +66 -53
- package/build/lib/commands/general.js.map +1 -1
- package/build/lib/commands/log.d.ts +42 -44
- package/build/lib/commands/log.d.ts.map +1 -1
- package/build/lib/commands/log.js +32 -53
- package/build/lib/commands/log.js.map +1 -1
- package/build/lib/commands/navigation.d.ts +1 -1
- package/build/lib/commands/navigation.d.ts.map +1 -1
- package/build/lib/commands/performance.d.ts +36 -55
- package/build/lib/commands/performance.d.ts.map +1 -1
- package/build/lib/commands/performance.js +93 -86
- package/build/lib/commands/performance.js.map +1 -1
- package/build/lib/commands/recordscreen.d.ts +31 -63
- package/build/lib/commands/recordscreen.d.ts.map +1 -1
- package/build/lib/commands/recordscreen.js +29 -28
- package/build/lib/commands/recordscreen.js.map +1 -1
- package/build/lib/commands/xctest.d.ts +37 -37
- package/build/lib/commands/xctest.d.ts.map +1 -1
- package/build/lib/commands/xctest.js +38 -50
- package/build/lib/commands/xctest.js.map +1 -1
- package/build/lib/execute-method-map.d.ts.map +1 -1
- package/build/lib/execute-method-map.js +0 -2
- package/build/lib/execute-method-map.js.map +1 -1
- package/lib/commands/app-management.ts +414 -0
- package/lib/commands/{general.js → general.ts} +101 -76
- package/lib/commands/{log.js → log.ts} +68 -68
- package/lib/commands/{performance.js → performance.ts} +133 -114
- package/lib/commands/{recordscreen.js → recordscreen.ts} +78 -50
- package/lib/commands/{xctest.js → xctest.ts} +78 -71
- package/lib/execute-method-map.ts +0 -2
- package/npm-shrinkwrap.json +8 -8
- package/package.json +1 -1
- package/lib/commands/app-management.js +0 -346
|
@@ -5,6 +5,9 @@ import {encodeBase64OrUpload} from '../utils';
|
|
|
5
5
|
import {WDA_BASE_URL} from 'appium-webdriveragent';
|
|
6
6
|
import {waitForCondition} from 'asyncbox';
|
|
7
7
|
import url from 'url';
|
|
8
|
+
import type {XCUITestDriver} from '../driver';
|
|
9
|
+
import type {StartRecordingScreenOptions, StopRecordingScreenOptions} from './types';
|
|
10
|
+
import type {WDASettings} from 'appium-webdriveragent';
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* Set max timeout for 'reconnect_delay_max' ffmpeg argument usage.
|
|
@@ -20,14 +23,22 @@ const DEFAULT_VCODEC = 'mjpeg';
|
|
|
20
23
|
const MP4_EXT = '.mp4';
|
|
21
24
|
const FFMPEG_BINARY = 'ffmpeg';
|
|
22
25
|
const ffmpegLogger = logger.getLogger(FFMPEG_BINARY);
|
|
23
|
-
const QUALITY_MAPPING = {
|
|
26
|
+
const QUALITY_MAPPING: Record<string, number> = {
|
|
24
27
|
low: 10,
|
|
25
28
|
medium: 25,
|
|
26
29
|
high: 75,
|
|
27
30
|
photo: 100,
|
|
28
31
|
};
|
|
29
32
|
|
|
30
|
-
const HARDWARE_ACCELERATION_PARAMETERS
|
|
33
|
+
const HARDWARE_ACCELERATION_PARAMETERS: Record<
|
|
34
|
+
string,
|
|
35
|
+
{
|
|
36
|
+
hwaccel: string;
|
|
37
|
+
hwaccelOutputFormat: string;
|
|
38
|
+
scaleFilterHWAccel: string;
|
|
39
|
+
videoTypeHWAccel: string;
|
|
40
|
+
}
|
|
41
|
+
> = {
|
|
31
42
|
/* https://trac.ffmpeg.org/wiki/HWAccelIntro#VideoToolbox */
|
|
32
43
|
videoToolbox: {
|
|
33
44
|
hwaccel: 'videotoolbox',
|
|
@@ -54,21 +65,28 @@ const HARDWARE_ACCELERATION_PARAMETERS = {
|
|
|
54
65
|
hwaccel: 'qsv',
|
|
55
66
|
hwaccelOutputFormat: '',
|
|
56
67
|
scaleFilterHWAccel: 'scale_qsv',
|
|
57
|
-
videoTypeHWAccel: 'h264_qsv'
|
|
68
|
+
videoTypeHWAccel: 'h264_qsv',
|
|
58
69
|
},
|
|
59
70
|
/* https://trac.ffmpeg.org/wiki/Hardware/VAAPI */
|
|
60
71
|
vaapi: {
|
|
61
72
|
hwaccel: 'vaapi',
|
|
62
73
|
hwaccelOutputFormat: 'vaapi',
|
|
63
74
|
scaleFilterHWAccel: 'scale_vaapi',
|
|
64
|
-
videoTypeHWAccel: 'h264_vaapi'
|
|
65
|
-
}
|
|
75
|
+
videoTypeHWAccel: 'h264_vaapi',
|
|
76
|
+
},
|
|
66
77
|
};
|
|
67
78
|
|
|
68
79
|
const CAPTURE_START_MARKER = /^\s*frame=/;
|
|
69
80
|
|
|
70
81
|
export class ScreenRecorder {
|
|
71
|
-
|
|
82
|
+
private readonly videoPath: string;
|
|
83
|
+
private readonly log: any;
|
|
84
|
+
private readonly opts: ScreenRecorderOptions;
|
|
85
|
+
private readonly udid: string;
|
|
86
|
+
private mainProcess: SubProcess | null;
|
|
87
|
+
private timeoutHandler: NodeJS.Timeout | null;
|
|
88
|
+
|
|
89
|
+
constructor(udid: string, log: any, videoPath: string, opts: ScreenRecorderOptions = {}) {
|
|
72
90
|
this.videoPath = videoPath;
|
|
73
91
|
this.log = log;
|
|
74
92
|
this.opts = opts;
|
|
@@ -77,7 +95,7 @@ export class ScreenRecorder {
|
|
|
77
95
|
this.timeoutHandler = null;
|
|
78
96
|
}
|
|
79
97
|
|
|
80
|
-
async start(timeoutMs) {
|
|
98
|
+
async start(timeoutMs: number): Promise<void> {
|
|
81
99
|
try {
|
|
82
100
|
await fs.which(FFMPEG_BINARY);
|
|
83
101
|
} catch {
|
|
@@ -98,7 +116,7 @@ export class ScreenRecorder {
|
|
|
98
116
|
pixelFormat,
|
|
99
117
|
} = this.opts;
|
|
100
118
|
|
|
101
|
-
const args = [
|
|
119
|
+
const args: string[] = [
|
|
102
120
|
'-f',
|
|
103
121
|
'mjpeg',
|
|
104
122
|
// https://github.com/appium/appium/issues/16294
|
|
@@ -115,8 +133,8 @@ export class ScreenRecorder {
|
|
|
115
133
|
hwaccel,
|
|
116
134
|
hwaccelOutputFormat,
|
|
117
135
|
scaleFilterHWAccel,
|
|
118
|
-
videoTypeHWAccel
|
|
119
|
-
} = HARDWARE_ACCELERATION_PARAMETERS[hardwareAcceleration] ?? {};
|
|
136
|
+
videoTypeHWAccel,
|
|
137
|
+
} = HARDWARE_ACCELERATION_PARAMETERS[hardwareAcceleration || ''] ?? {};
|
|
120
138
|
|
|
121
139
|
if (hwaccel) {
|
|
122
140
|
args.push('-hwaccel', hwaccel);
|
|
@@ -127,10 +145,10 @@ export class ScreenRecorder {
|
|
|
127
145
|
}
|
|
128
146
|
|
|
129
147
|
//Parameter `-r` is optional. See details: https://github.com/appium/appium/issues/12067
|
|
130
|
-
if (videoFps && videoType === 'libx264' || videoTypeHWAccel) {
|
|
131
|
-
args.push('-r', videoFps);
|
|
148
|
+
if ((videoFps && videoType === 'libx264') || videoTypeHWAccel) {
|
|
149
|
+
args.push('-r', String(videoFps));
|
|
132
150
|
}
|
|
133
|
-
const {protocol, hostname} = url.parse(remoteUrl);
|
|
151
|
+
const {protocol, hostname} = url.parse(remoteUrl || '');
|
|
134
152
|
args.push('-i', `${protocol}//${hostname}:${remotePort}`);
|
|
135
153
|
|
|
136
154
|
if (videoFilters || videoScale) {
|
|
@@ -141,13 +159,13 @@ export class ScreenRecorder {
|
|
|
141
159
|
if (pixelFormat) {
|
|
142
160
|
args.push('-pix_fmt', pixelFormat);
|
|
143
161
|
}
|
|
144
|
-
args.push('-vcodec', videoTypeHWAccel || videoType);
|
|
162
|
+
args.push('-vcodec', videoTypeHWAccel || videoType || DEFAULT_VCODEC);
|
|
145
163
|
args.push('-y');
|
|
146
164
|
args.push(this.videoPath);
|
|
147
165
|
|
|
148
166
|
this.mainProcess = new SubProcess(FFMPEG_BINARY, args);
|
|
149
167
|
let isCaptureStarted = false;
|
|
150
|
-
this.mainProcess.on('line-stderr', (line) => {
|
|
168
|
+
this.mainProcess.on('line-stderr', (line: string) => {
|
|
151
169
|
if (CAPTURE_START_MARKER.test(line)) {
|
|
152
170
|
if (!isCaptureStarted) {
|
|
153
171
|
isCaptureStarted = true;
|
|
@@ -175,9 +193,8 @@ export class ScreenRecorder {
|
|
|
175
193
|
);
|
|
176
194
|
}
|
|
177
195
|
this.log.info(
|
|
178
|
-
`Starting screen capture on the device '${
|
|
179
|
-
|
|
180
|
-
}' with command: '${FFMPEG_BINARY} ${args.join(' ')}'. ` + `Will timeout in ${timeoutMs}ms`,
|
|
196
|
+
`Starting screen capture on the device '${this.udid}' with command: '${FFMPEG_BINARY} ${args.join(' ')}'. ` +
|
|
197
|
+
`Will timeout in ${timeoutMs}ms`,
|
|
181
198
|
);
|
|
182
199
|
|
|
183
200
|
this.timeoutHandler = setTimeout(async () => {
|
|
@@ -189,7 +206,7 @@ export class ScreenRecorder {
|
|
|
189
206
|
}, timeoutMs);
|
|
190
207
|
}
|
|
191
208
|
|
|
192
|
-
async interrupt(force = false) {
|
|
209
|
+
async interrupt(force = false): Promise<boolean> {
|
|
193
210
|
let result = true;
|
|
194
211
|
|
|
195
212
|
if (this.timeoutHandler) {
|
|
@@ -202,7 +219,7 @@ export class ScreenRecorder {
|
|
|
202
219
|
this.mainProcess = null;
|
|
203
220
|
try {
|
|
204
221
|
await interruptPromise;
|
|
205
|
-
} catch (e) {
|
|
222
|
+
} catch (e: any) {
|
|
206
223
|
this.log.warn(
|
|
207
224
|
`Cannot ${force ? 'terminate' : 'interrupt'} ${FFMPEG_BINARY}. ` +
|
|
208
225
|
`Original error: ${e.message}`,
|
|
@@ -214,12 +231,12 @@ export class ScreenRecorder {
|
|
|
214
231
|
return result;
|
|
215
232
|
}
|
|
216
233
|
|
|
217
|
-
async finish() {
|
|
234
|
+
async finish(): Promise<string> {
|
|
218
235
|
await this.interrupt();
|
|
219
236
|
return this.videoPath;
|
|
220
237
|
}
|
|
221
238
|
|
|
222
|
-
async cleanup() {
|
|
239
|
+
async cleanup(): Promise<void> {
|
|
223
240
|
if (await fs.exists(this.videoPath)) {
|
|
224
241
|
await fs.rimraf(this.videoPath);
|
|
225
242
|
}
|
|
@@ -235,13 +252,15 @@ export class ScreenRecorder {
|
|
|
235
252
|
* If screen recording has been already started then the command will stop it forcefully and start a new one.
|
|
236
253
|
* The previously recorded video file will be deleted.
|
|
237
254
|
*
|
|
238
|
-
* @param
|
|
239
|
-
* @returns
|
|
255
|
+
* @param options - The available options.
|
|
256
|
+
* @returns Base64-encoded content of the recorded media file if
|
|
240
257
|
* any screen recording is currently running or an empty string.
|
|
241
258
|
* @throws {Error} If screen recording has failed to start.
|
|
242
|
-
* @this {XCUITestDriver}
|
|
243
259
|
*/
|
|
244
|
-
export async function startRecordingScreen(
|
|
260
|
+
export async function startRecordingScreen(
|
|
261
|
+
this: XCUITestDriver,
|
|
262
|
+
options: StartRecordingScreenOptions = {},
|
|
263
|
+
): Promise<string> {
|
|
245
264
|
const {
|
|
246
265
|
videoType = DEFAULT_VCODEC,
|
|
247
266
|
timeLimit = DEFAULT_RECORDING_TIME_SEC,
|
|
@@ -251,21 +270,21 @@ export async function startRecordingScreen(options = {}) {
|
|
|
251
270
|
videoScale,
|
|
252
271
|
forceRestart,
|
|
253
272
|
pixelFormat,
|
|
254
|
-
hardwareAcceleration
|
|
273
|
+
hardwareAcceleration,
|
|
255
274
|
} = options;
|
|
256
275
|
|
|
257
276
|
let result = '';
|
|
258
277
|
if (!forceRestart) {
|
|
259
278
|
this.log.info(
|
|
260
279
|
`Checking if there is/was a previous screen recording. ` +
|
|
261
|
-
|
|
280
|
+
`Set 'forceRestart' option to 'true' if you'd like to skip this step.`,
|
|
262
281
|
);
|
|
263
282
|
result = (await this.stopRecordingScreen(options)) ?? result;
|
|
264
283
|
}
|
|
265
284
|
|
|
266
285
|
const videoPath = await tempDir.path({
|
|
267
286
|
prefix: `appium_${Math.random().toString(16).substring(2, 8)}`,
|
|
268
|
-
suffix: MP4_EXT
|
|
287
|
+
suffix: MP4_EXT,
|
|
269
288
|
});
|
|
270
289
|
|
|
271
290
|
const wdaBaseUrl = this.opts.wdaBaseUrl || WDA_BASE_URL;
|
|
@@ -275,9 +294,9 @@ export async function startRecordingScreen(options = {}) {
|
|
|
275
294
|
videoType,
|
|
276
295
|
videoFilters,
|
|
277
296
|
videoScale,
|
|
278
|
-
videoFps,
|
|
297
|
+
videoFps: typeof videoFps === 'string' ? parseInt(videoFps, 10) : videoFps,
|
|
279
298
|
pixelFormat,
|
|
280
|
-
hardwareAcceleration
|
|
299
|
+
hardwareAcceleration,
|
|
281
300
|
});
|
|
282
301
|
if (!(await screenRecorder.interrupt(true))) {
|
|
283
302
|
throw this.log.errorWithException('Unable to stop screen recording process');
|
|
@@ -295,10 +314,10 @@ export async function startRecordingScreen(options = {}) {
|
|
|
295
314
|
);
|
|
296
315
|
}
|
|
297
316
|
|
|
298
|
-
let {mjpegServerScreenshotQuality, mjpegServerFramerate} =
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
);
|
|
317
|
+
let {mjpegServerScreenshotQuality, mjpegServerFramerate} = (await this.proxyCommand(
|
|
318
|
+
'/appium/settings',
|
|
319
|
+
'GET',
|
|
320
|
+
)) as WDASettings;
|
|
302
321
|
if (videoQuality) {
|
|
303
322
|
const quality = _.isInteger(videoQuality)
|
|
304
323
|
? videoQuality
|
|
@@ -306,11 +325,12 @@ export async function startRecordingScreen(options = {}) {
|
|
|
306
325
|
if (!quality) {
|
|
307
326
|
throw new Error(
|
|
308
327
|
`videoQuality value should be one of ${JSON.stringify(
|
|
309
|
-
_.keys(QUALITY_MAPPING)
|
|
310
|
-
)} or a number in range 1..100. ` + `'${videoQuality}' is given instead
|
|
328
|
+
_.keys(QUALITY_MAPPING),
|
|
329
|
+
)} or a number in range 1..100. ` + `'${videoQuality}' is given instead`,
|
|
311
330
|
);
|
|
312
331
|
}
|
|
313
|
-
mjpegServerScreenshotQuality =
|
|
332
|
+
mjpegServerScreenshotQuality =
|
|
333
|
+
mjpegServerScreenshotQuality !== quality ? (quality as number) : undefined;
|
|
314
334
|
} else {
|
|
315
335
|
mjpegServerScreenshotQuality = undefined;
|
|
316
336
|
}
|
|
@@ -319,7 +339,7 @@ export async function startRecordingScreen(options = {}) {
|
|
|
319
339
|
if (isNaN(fps)) {
|
|
320
340
|
throw new Error(
|
|
321
341
|
`videoFps value should be a valid number in range 1..60. ` +
|
|
322
|
-
|
|
342
|
+
`'${videoFps}' is given instead`,
|
|
323
343
|
);
|
|
324
344
|
}
|
|
325
345
|
mjpegServerFramerate = mjpegServerFramerate !== fps ? fps : undefined;
|
|
@@ -330,8 +350,8 @@ export async function startRecordingScreen(options = {}) {
|
|
|
330
350
|
await this.proxyCommand('/appium/settings', 'POST', {
|
|
331
351
|
settings: {
|
|
332
352
|
mjpegServerScreenshotQuality,
|
|
333
|
-
mjpegServerFramerate
|
|
334
|
-
}
|
|
353
|
+
mjpegServerFramerate,
|
|
354
|
+
},
|
|
335
355
|
});
|
|
336
356
|
}
|
|
337
357
|
|
|
@@ -355,16 +375,17 @@ export async function startRecordingScreen(options = {}) {
|
|
|
355
375
|
* active screen recording processes are running then the method returns an
|
|
356
376
|
* empty string.
|
|
357
377
|
*
|
|
358
|
-
* @param
|
|
359
|
-
*
|
|
360
|
-
* @returns {Promise<string?>} Base64-encoded content of the recorded media
|
|
378
|
+
* @param options - The available options.
|
|
379
|
+
* @returns Base64-encoded content of the recorded media
|
|
361
380
|
* file if `remotePath` parameter is empty or null or an empty string.
|
|
362
381
|
* @throws {Error} If there was an error while getting the name of a media
|
|
363
382
|
* file or the file content cannot be uploaded to the remote
|
|
364
383
|
* location.
|
|
365
|
-
* @this {XCUITestDriver}
|
|
366
384
|
*/
|
|
367
|
-
export async function stopRecordingScreen(
|
|
385
|
+
export async function stopRecordingScreen(
|
|
386
|
+
this: XCUITestDriver,
|
|
387
|
+
options: StopRecordingScreenOptions = {},
|
|
388
|
+
): Promise<string | null> {
|
|
368
389
|
if (!this._recentScreenRecorder) {
|
|
369
390
|
this.log.info('Screen recording is not running. There is nothing to stop.');
|
|
370
391
|
return '';
|
|
@@ -375,7 +396,7 @@ export async function stopRecordingScreen(options = {}) {
|
|
|
375
396
|
if (!(await fs.exists(videoPath))) {
|
|
376
397
|
throw this.log.errorWithException(
|
|
377
398
|
`The screen recorder utility has failed ` +
|
|
378
|
-
|
|
399
|
+
`to store the actual screen recording at '${videoPath}'`,
|
|
379
400
|
);
|
|
380
401
|
}
|
|
381
402
|
return await encodeBase64OrUpload(videoPath, options.remotePath, options);
|
|
@@ -386,6 +407,13 @@ export async function stopRecordingScreen(options = {}) {
|
|
|
386
407
|
}
|
|
387
408
|
}
|
|
388
409
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
410
|
+
interface ScreenRecorderOptions {
|
|
411
|
+
hardwareAcceleration?: string;
|
|
412
|
+
remotePort?: number;
|
|
413
|
+
remoteUrl?: string;
|
|
414
|
+
videoFps?: number;
|
|
415
|
+
videoType?: string;
|
|
416
|
+
videoScale?: string;
|
|
417
|
+
videoFilters?: string;
|
|
418
|
+
pixelFormat?: string;
|
|
419
|
+
}
|
|
@@ -2,34 +2,42 @@ import B from 'bluebird';
|
|
|
2
2
|
import {logger} from 'appium/support';
|
|
3
3
|
import _ from 'lodash';
|
|
4
4
|
import {errors} from 'appium/driver';
|
|
5
|
+
import type {XCUITestDriver} from '../driver';
|
|
6
|
+
import type {XCTestResult, RunXCTestResult} from './types';
|
|
7
|
+
import type {StringRecord} from '@appium/types';
|
|
8
|
+
import type IDB from 'appium-idb';
|
|
5
9
|
|
|
6
10
|
const XCTEST_TIMEOUT = 360000; // 60 minute timeout
|
|
7
11
|
|
|
8
12
|
const xctestLog = logger.getLogger('XCTest');
|
|
9
13
|
|
|
10
14
|
/**
|
|
11
|
-
* Asserts that IDB is present and that launchWithIDB was used
|
|
15
|
+
* Asserts that IDB is present and that launchWithIDB was used.
|
|
12
16
|
*
|
|
13
|
-
* @param
|
|
17
|
+
* @param opts - Opts object from the driver instance
|
|
18
|
+
* @returns The IDB instance
|
|
19
|
+
* @throws {Error} If IDB is not available or launchWithIDB is not enabled
|
|
14
20
|
*/
|
|
15
|
-
export function assertIDB(opts) {
|
|
16
|
-
|
|
21
|
+
export function assertIDB(this: XCUITestDriver, opts: XCUITestDriver['opts']): IDB {
|
|
22
|
+
const device = this.device as any;
|
|
23
|
+
if (!device?.idb || !opts.launchWithIDB) {
|
|
17
24
|
throw new Error(
|
|
18
25
|
`To use XCTest runner, IDB (https://github.com/facebook/idb) must be installed ` +
|
|
19
26
|
`and sessions must be run with the "launchWithIDB" capability`,
|
|
20
27
|
);
|
|
21
28
|
}
|
|
22
|
-
return
|
|
29
|
+
return device.idb;
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
/**
|
|
26
|
-
* Parse the stdout of XC test log
|
|
27
|
-
*
|
|
28
|
-
* @
|
|
33
|
+
* Parse the stdout of XC test log.
|
|
34
|
+
*
|
|
35
|
+
* @param stdout - A line of standard out from `idb xctest run ...`
|
|
36
|
+
* @returns The final output of the XCTest run
|
|
29
37
|
*/
|
|
30
|
-
export function parseXCTestStdout(stdout) {
|
|
38
|
+
export function parseXCTestStdout(stdout: string): XCTestResult[] | string[] {
|
|
31
39
|
// Parses a 'key' into JSON format
|
|
32
|
-
function parseKey(name) {
|
|
40
|
+
function parseKey(name: string): string {
|
|
33
41
|
const words = name.split(' ');
|
|
34
42
|
let out = '';
|
|
35
43
|
for (const word of words) {
|
|
@@ -39,7 +47,7 @@ export function parseXCTestStdout(stdout) {
|
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
// Parses a 'value' into JSON format
|
|
42
|
-
function parseValue(value) {
|
|
50
|
+
function parseValue(value: string): any {
|
|
43
51
|
value = value || '';
|
|
44
52
|
switch (value.toLowerCase()) {
|
|
45
53
|
case 'true':
|
|
@@ -51,7 +59,7 @@ export function parseXCTestStdout(stdout) {
|
|
|
51
59
|
default:
|
|
52
60
|
break;
|
|
53
61
|
}
|
|
54
|
-
if (!isNaN(value)) {
|
|
62
|
+
if (!isNaN(Number(value))) {
|
|
55
63
|
if (!_.isString(value)) {
|
|
56
64
|
return 0;
|
|
57
65
|
} else if (value.indexOf('.') > 0) {
|
|
@@ -73,16 +81,14 @@ export function parseXCTestStdout(stdout) {
|
|
|
73
81
|
return [lines[0]];
|
|
74
82
|
}
|
|
75
83
|
|
|
76
|
-
|
|
77
|
-
const results = [];
|
|
84
|
+
const results: XCTestResult[] = [];
|
|
78
85
|
for (const line of lines) {
|
|
79
86
|
// The properties are split up by pipes and each property
|
|
80
87
|
// has the format "Some Key : Some Value"
|
|
81
88
|
const properties = line.split('|');
|
|
82
89
|
|
|
83
90
|
// Parse each property
|
|
84
|
-
|
|
85
|
-
const output = /** @type {any} */ ({});
|
|
91
|
+
const output: any = {};
|
|
86
92
|
let entryIndex = 0;
|
|
87
93
|
for (const prop of properties) {
|
|
88
94
|
if (entryIndex === 0) {
|
|
@@ -94,7 +100,7 @@ export function parseXCTestStdout(stdout) {
|
|
|
94
100
|
// e.g. Location /path/to/XCTesterAppUITests/XCTesterAppUITests.swift:36
|
|
95
101
|
output.location = prop.substring(prop.indexOf('Location') + 8).trim();
|
|
96
102
|
} else {
|
|
97
|
-
|
|
103
|
+
const [key, value] = prop.split(':');
|
|
98
104
|
output[parseKey(key.trim())] = parseValue(value ? value.trim() : '');
|
|
99
105
|
}
|
|
100
106
|
entryIndex++;
|
|
@@ -123,12 +129,13 @@ export function parseXCTestStdout(stdout) {
|
|
|
123
129
|
}
|
|
124
130
|
|
|
125
131
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* @property {number} code Subprocess exit code
|
|
129
|
-
* @property {string} signal The signal (SIG*) that caused the process to fail
|
|
130
|
-
* @property {XCTestResult[]} results The output of the failed test (if there is output)
|
|
132
|
+
* Error thrown when XCTest subprocess returns non-zero exit code.
|
|
131
133
|
*/
|
|
134
|
+
export interface XCUITestError extends Error {
|
|
135
|
+
code: number;
|
|
136
|
+
signal?: string;
|
|
137
|
+
result?: XCTestResult[];
|
|
138
|
+
}
|
|
132
139
|
|
|
133
140
|
/**
|
|
134
141
|
* Run a native XCTest script.
|
|
@@ -137,36 +144,36 @@ export function parseXCTestStdout(stdout) {
|
|
|
137
144
|
*
|
|
138
145
|
* **Facebook's [IDB](https://github.com/facebook/idb) tool is required** to run such tests; see [the idb docs](https://fbidb.io/docs/test-execution/) for reference.
|
|
139
146
|
*
|
|
140
|
-
* @param
|
|
141
|
-
* @param
|
|
142
|
-
* @param
|
|
143
|
-
* @param
|
|
144
|
-
* @param
|
|
145
|
-
* @param
|
|
146
|
-
* @param
|
|
147
|
+
* @param testRunnerBundleId - Test app bundle (e.g.: `io.appium.XCTesterAppUITests.xctrunner`)
|
|
148
|
+
* @param appUnderTestBundleId - App-under-test bundle
|
|
149
|
+
* @param xcTestBundleId - XCTest bundle ID
|
|
150
|
+
* @param args - Launch arguments to start the test with (see [reference documentation](https://developer.apple.com/documentation/xctest/xcuiapplication/1500477-launcharguments))
|
|
151
|
+
* @param testType - XC test type
|
|
152
|
+
* @param env - Environment variables passed to test
|
|
153
|
+
* @param timeout - Timeout (in ms) for session completion
|
|
154
|
+
* @returns The array of test results
|
|
147
155
|
* @throws {XCUITestError} Error thrown if subprocess returns non-zero exit code
|
|
148
|
-
* @returns {Promise<import('./types').RunXCTestResult>} The array of test results
|
|
149
|
-
* @this {XCUITestDriver}
|
|
150
156
|
*/
|
|
151
157
|
export async function mobileRunXCTest(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
+
this: XCUITestDriver,
|
|
159
|
+
testRunnerBundleId: string,
|
|
160
|
+
appUnderTestBundleId: string,
|
|
161
|
+
xcTestBundleId: string,
|
|
162
|
+
args: string[] = [],
|
|
163
|
+
testType: 'app' | 'ui' | 'logic' = 'ui',
|
|
164
|
+
env?: StringRecord,
|
|
158
165
|
timeout = XCTEST_TIMEOUT,
|
|
159
|
-
) {
|
|
160
|
-
const subproc = await assertIDB(this.opts).runXCUITest(
|
|
166
|
+
): Promise<RunXCTestResult> {
|
|
167
|
+
const subproc = await assertIDB.call(this, this.opts).runXCUITest(
|
|
161
168
|
testRunnerBundleId,
|
|
162
169
|
appUnderTestBundleId,
|
|
163
170
|
xcTestBundleId,
|
|
164
171
|
{env, args, testType},
|
|
165
172
|
);
|
|
166
173
|
return await new B((resolve, reject) => {
|
|
167
|
-
let mostRecentLogObject = null;
|
|
168
|
-
let xctestTimeout;
|
|
169
|
-
let lastErrorMessage = null;
|
|
174
|
+
let mostRecentLogObject: XCTestResult[] | string[] | null = null;
|
|
175
|
+
let xctestTimeout: NodeJS.Timeout | undefined;
|
|
176
|
+
let lastErrorMessage: string | null = null;
|
|
170
177
|
if (timeout > 0) {
|
|
171
178
|
xctestTimeout = setTimeout(
|
|
172
179
|
() =>
|
|
@@ -179,11 +186,11 @@ export async function mobileRunXCTest(
|
|
|
179
186
|
);
|
|
180
187
|
}
|
|
181
188
|
|
|
182
|
-
subproc.on('output', (stdout, stderr) => {
|
|
189
|
+
subproc.on('output', (stdout: string, stderr: string) => {
|
|
183
190
|
if (stdout) {
|
|
184
191
|
try {
|
|
185
192
|
mostRecentLogObject = parseXCTestStdout(stdout);
|
|
186
|
-
} catch (err) {
|
|
193
|
+
} catch (err: any) {
|
|
187
194
|
// Fails if log parsing fails.
|
|
188
195
|
// This is in case IDB changes the way that logs are formatted and
|
|
189
196
|
// it breaks 'parseXCTestStdout'. If that happens we still want the process
|
|
@@ -202,23 +209,25 @@ export async function mobileRunXCTest(
|
|
|
202
209
|
}
|
|
203
210
|
});
|
|
204
211
|
|
|
205
|
-
subproc.on('exit', (code, signal) => {
|
|
206
|
-
|
|
212
|
+
subproc.on('exit', (code: number | null, signal: string | null) => {
|
|
213
|
+
if (xctestTimeout) {
|
|
214
|
+
clearTimeout(xctestTimeout);
|
|
215
|
+
}
|
|
207
216
|
if (code !== 0) {
|
|
208
|
-
const err =
|
|
209
|
-
err.code = code;
|
|
217
|
+
const err = new Error(lastErrorMessage || String(mostRecentLogObject)) as XCUITestError;
|
|
218
|
+
err.code = code ?? -1;
|
|
210
219
|
if (signal != null) {
|
|
211
220
|
err.signal = signal;
|
|
212
221
|
}
|
|
213
222
|
if (mostRecentLogObject) {
|
|
214
|
-
err.result = mostRecentLogObject;
|
|
223
|
+
err.result = mostRecentLogObject as XCTestResult[];
|
|
215
224
|
}
|
|
216
225
|
return reject(err);
|
|
217
226
|
}
|
|
218
227
|
resolve({
|
|
219
|
-
code,
|
|
220
|
-
signal,
|
|
221
|
-
results: mostRecentLogObject,
|
|
228
|
+
code: code ?? 0,
|
|
229
|
+
signal: signal ?? null,
|
|
230
|
+
results: mostRecentLogObject as XCTestResult[],
|
|
222
231
|
passed: true,
|
|
223
232
|
});
|
|
224
233
|
});
|
|
@@ -230,11 +239,12 @@ export async function mobileRunXCTest(
|
|
|
230
239
|
*
|
|
231
240
|
* **Facebook's [IDB](https://github.com/facebook/idb) tool is required** for this command to work.
|
|
232
241
|
*
|
|
233
|
-
* @param
|
|
234
|
-
* @returns {Promise<void>}
|
|
235
|
-
* @this {XCUITestDriver}
|
|
242
|
+
* @param xctestApp - Path of the XCTest app (URL or filename with extension `.app`)
|
|
236
243
|
*/
|
|
237
|
-
export async function mobileInstallXCTestBundle(
|
|
244
|
+
export async function mobileInstallXCTestBundle(
|
|
245
|
+
this: XCUITestDriver,
|
|
246
|
+
xctestApp: string,
|
|
247
|
+
): Promise<void> {
|
|
238
248
|
if (!_.isString(xctestApp)) {
|
|
239
249
|
throw new errors.InvalidArgumentError(
|
|
240
250
|
`'xctestApp' is a required parameter for 'installXCTestBundle' and ` +
|
|
@@ -242,7 +252,7 @@ export async function mobileInstallXCTestBundle(xctestApp) {
|
|
|
242
252
|
);
|
|
243
253
|
}
|
|
244
254
|
xctestLog.info(`Installing bundle '${xctestApp}'`);
|
|
245
|
-
const idb = assertIDB(this.opts);
|
|
255
|
+
const idb = assertIDB.call(this, this.opts);
|
|
246
256
|
const res = await this.helpers.configureApp(xctestApp, '.xctest');
|
|
247
257
|
await idb.installXCTestBundle(res);
|
|
248
258
|
}
|
|
@@ -252,34 +262,31 @@ export async function mobileInstallXCTestBundle(xctestApp) {
|
|
|
252
262
|
*
|
|
253
263
|
* **Facebook's [IDB](https://github.com/facebook/idb) tool is required** for this command to work.
|
|
254
264
|
*
|
|
255
|
-
* @returns
|
|
256
|
-
* @this {XCUITestDriver}
|
|
265
|
+
* @returns List of XCTest bundles (e.g.: `XCTesterAppUITests.XCTesterAppUITests/testLaunchPerformance`)
|
|
257
266
|
*/
|
|
258
|
-
export async function mobileListXCTestBundles() {
|
|
259
|
-
return await assertIDB(this.opts).listXCTestBundles();
|
|
267
|
+
export async function mobileListXCTestBundles(this: XCUITestDriver): Promise<string[]> {
|
|
268
|
+
return await assertIDB.call(this, this.opts).listXCTestBundles();
|
|
260
269
|
}
|
|
261
270
|
|
|
262
271
|
/**
|
|
263
|
-
* List XCTests in a test bundle
|
|
272
|
+
* List XCTests in a test bundle.
|
|
264
273
|
*
|
|
265
274
|
* **Facebook's [IDB](https://github.com/facebook/idb) tool is required** for this command to work.
|
|
266
|
-
* @param {string} bundle - Bundle ID of the XCTest
|
|
267
275
|
*
|
|
268
|
-
* @
|
|
269
|
-
* @
|
|
276
|
+
* @param bundle - Bundle ID of the XCTest
|
|
277
|
+
* @returns The list of xctests in the test bundle (e.g., `['XCTesterAppUITests.XCTesterAppUITests/testExample', 'XCTesterAppUITests.XCTesterAppUITests/testLaunchPerformance']`)
|
|
270
278
|
*/
|
|
271
|
-
export async function mobileListXCTestsInTestBundle(
|
|
279
|
+
export async function mobileListXCTestsInTestBundle(
|
|
280
|
+
this: XCUITestDriver,
|
|
281
|
+
bundle: string,
|
|
282
|
+
): Promise<string[]> {
|
|
272
283
|
if (!_.isString(bundle)) {
|
|
273
284
|
throw new errors.InvalidArgumentError(
|
|
274
285
|
`'bundle' is a required parameter for 'listXCTestsInTestBundle' and ` +
|
|
275
286
|
`must be a string. Found '${bundle}'`,
|
|
276
287
|
);
|
|
277
288
|
}
|
|
278
|
-
const idb = assertIDB(this.opts);
|
|
289
|
+
const idb = assertIDB.call(this, this.opts);
|
|
279
290
|
return await idb.listXCTestsInTestBundle(bundle);
|
|
280
291
|
}
|
|
281
292
|
|
|
282
|
-
/**
|
|
283
|
-
* @typedef {import('../driver').XCUITestDriver} XCUITestDriver
|
|
284
|
-
* @typedef {import('./types').XCTestResult} XCTestResult
|
|
285
|
-
*/
|
|
@@ -215,7 +215,6 @@ export const executeMethodMap = {
|
|
|
215
215
|
command: 'getViewportRect',
|
|
216
216
|
},
|
|
217
217
|
'mobile: startPerfRecord': {
|
|
218
|
-
// @ts-expect-error Class field assignment - method exists on XCUITestDriver
|
|
219
218
|
command: 'mobileStartPerfRecord',
|
|
220
219
|
params: {
|
|
221
220
|
optional: ['timeout', 'profileName', 'pid'],
|
|
@@ -253,7 +252,6 @@ export const executeMethodMap = {
|
|
|
253
252
|
command: 'mobileListCertificates',
|
|
254
253
|
},
|
|
255
254
|
'mobile: startLogsBroadcast': {
|
|
256
|
-
// @ts-expect-error Class field assignment - method exists on XCUITestDriver
|
|
257
255
|
command: 'mobileStartLogsBroadcast',
|
|
258
256
|
},
|
|
259
257
|
'mobile: stopLogsBroadcast': {
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appium-xcuitest-driver",
|
|
3
|
-
"version": "10.13.
|
|
3
|
+
"version": "10.13.4",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "appium-xcuitest-driver",
|
|
9
|
-
"version": "10.13.
|
|
9
|
+
"version": "10.13.4",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@appium/strongbox": "^1.0.0-rc.1",
|
|
@@ -773,9 +773,9 @@
|
|
|
773
773
|
}
|
|
774
774
|
},
|
|
775
775
|
"node_modules/appium-webdriveragent": {
|
|
776
|
-
"version": "11.0
|
|
777
|
-
"resolved": "https://registry.npmjs.org/appium-webdriveragent/-/appium-webdriveragent-11.0.
|
|
778
|
-
"integrity": "sha512-
|
|
776
|
+
"version": "11.1.0",
|
|
777
|
+
"resolved": "https://registry.npmjs.org/appium-webdriveragent/-/appium-webdriveragent-11.1.0.tgz",
|
|
778
|
+
"integrity": "sha512-Ve9n+QIM4JUwTvKKeuaVvyra1k02JAcNsmgTtbx3fNItEzuC/s4LjEvqoxdr5KAjGAIz/Np5Efqn6S1WyvEiZQ==",
|
|
779
779
|
"license": "Apache-2.0",
|
|
780
780
|
"dependencies": {
|
|
781
781
|
"@appium/base-driver": "^10.0.0-rc.1",
|
|
@@ -3550,9 +3550,9 @@
|
|
|
3550
3550
|
"license": "MIT"
|
|
3551
3551
|
},
|
|
3552
3552
|
"node_modules/qs": {
|
|
3553
|
-
"version": "6.14.
|
|
3554
|
-
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.
|
|
3555
|
-
"integrity": "sha512-
|
|
3553
|
+
"version": "6.14.1",
|
|
3554
|
+
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
|
3555
|
+
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
|
3556
3556
|
"license": "BSD-3-Clause",
|
|
3557
3557
|
"dependencies": {
|
|
3558
3558
|
"side-channel": "^1.1.0"
|