appium-xcuitest-driver 10.8.2 → 10.8.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.js +1 -1
- package/build/lib/commands/app-management.js.map +1 -1
- package/build/lib/commands/file-movement.d.ts.map +1 -1
- package/build/lib/commands/file-movement.js +4 -4
- package/build/lib/commands/file-movement.js.map +1 -1
- package/build/lib/commands/memory.js +1 -1
- package/build/lib/commands/memory.js.map +1 -1
- package/build/lib/commands/performance.d.ts.map +1 -1
- package/build/lib/commands/performance.js +13 -4
- package/build/lib/commands/performance.js.map +1 -1
- package/build/lib/commands/xctest-record-screen.js +1 -1
- package/build/lib/commands/xctest-record-screen.js.map +1 -1
- package/build/lib/desired-caps.d.ts +9 -7
- package/build/lib/desired-caps.d.ts.map +1 -1
- package/build/lib/desired-caps.js +1 -0
- package/build/lib/desired-caps.js.map +1 -1
- package/build/lib/device/device-connections-factory.d.ts +18 -0
- package/build/lib/device/device-connections-factory.d.ts.map +1 -0
- package/build/lib/{device-connections-factory.js → device/device-connections-factory.js} +57 -41
- package/build/lib/device/device-connections-factory.js.map +1 -0
- package/build/lib/device/real-device-management.d.ts +146 -0
- package/build/lib/device/real-device-management.d.ts.map +1 -0
- package/build/lib/device/real-device-management.js +727 -0
- package/build/lib/device/real-device-management.js.map +1 -0
- package/build/lib/device/simulator-management.d.ts +65 -0
- package/build/lib/device/simulator-management.d.ts.map +1 -0
- package/build/lib/{simulator-management.js → device/simulator-management.js} +23 -42
- package/build/lib/device/simulator-management.js.map +1 -0
- package/build/lib/driver.d.ts +4 -1
- package/build/lib/driver.d.ts.map +1 -1
- package/build/lib/driver.js +5 -6
- package/build/lib/driver.js.map +1 -1
- package/lib/commands/app-management.js +1 -1
- package/lib/commands/file-movement.js +8 -4
- package/lib/commands/memory.js +1 -1
- package/lib/commands/performance.js +12 -1
- package/lib/commands/xctest-record-screen.js +1 -1
- package/lib/desired-caps.js +1 -0
- package/lib/{device-connections-factory.js → device/device-connections-factory.ts} +96 -60
- package/lib/device/real-device-management.ts +818 -0
- package/lib/{simulator-management.js → device/simulator-management.ts} +68 -61
- package/lib/driver.js +3 -5
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/build/lib/device-connections-factory.d.ts +0 -13
- package/build/lib/device-connections-factory.d.ts.map +0 -1
- package/build/lib/device-connections-factory.js.map +0 -1
- package/build/lib/ios-fs-helpers.d.ts +0 -75
- package/build/lib/ios-fs-helpers.d.ts.map +0 -1
- package/build/lib/ios-fs-helpers.js +0 -370
- package/build/lib/ios-fs-helpers.js.map +0 -1
- package/build/lib/real-device-management.d.ts +0 -53
- package/build/lib/real-device-management.d.ts.map +0 -1
- package/build/lib/real-device-management.js +0 -128
- package/build/lib/real-device-management.js.map +0 -1
- package/build/lib/real-device.d.ts +0 -112
- package/build/lib/real-device.d.ts.map +0 -1
- package/build/lib/real-device.js +0 -352
- package/build/lib/real-device.js.map +0 -1
- package/build/lib/simulator-management.d.ts +0 -96
- package/build/lib/simulator-management.d.ts.map +0 -1
- package/build/lib/simulator-management.js.map +0 -1
- package/build/lib/xcrun.d.ts +0 -3
- package/build/lib/xcrun.d.ts.map +0 -1
- package/build/lib/xcrun.js +0 -17
- package/build/lib/xcrun.js.map +0 -1
- package/lib/ios-fs-helpers.js +0 -355
- package/lib/real-device-management.js +0 -133
- package/lib/real-device.js +0 -347
- package/lib/xcrun.js +0 -16
|
@@ -0,0 +1,818 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import B, {TimeoutError} from 'bluebird';
|
|
3
|
+
import {fs, tempDir, mkdirp, zip, util, timing} from 'appium/support';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import {services, utilities, INSTRUMENT_CHANNEL} from 'appium-ios-device';
|
|
6
|
+
import {buildSafariPreferences, SAFARI_BUNDLE_ID} from '../app-utils';
|
|
7
|
+
import defaultLogger from '../logger';
|
|
8
|
+
import { Devicectl } from 'node-devicectl';
|
|
9
|
+
import type { AppiumLogger } from '@appium/types';
|
|
10
|
+
import type { XCUITestDriver } from '../driver';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_APP_INSTALLATION_TIMEOUT_MS = 8 * 60 * 1000;
|
|
13
|
+
export const IO_TIMEOUT_MS = 4 * 60 * 1000;
|
|
14
|
+
// Mobile devices use NAND memory modules for the storage,
|
|
15
|
+
// and the parallelism there is not as performant as on regular SSDs
|
|
16
|
+
const MAX_IO_CHUNK_SIZE = 8;
|
|
17
|
+
const APPLICATION_INSTALLED_NOTIFICATION = 'com.apple.mobile.application_installed';
|
|
18
|
+
const APPLICATION_NOTIFICATION_TIMEOUT_MS = 30 * 1000;
|
|
19
|
+
const INSTALLATION_STAGING_DIR = 'PublicStaging';
|
|
20
|
+
|
|
21
|
+
//#region Public File System Functions
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Retrieve a file from a real device
|
|
25
|
+
*
|
|
26
|
+
* @param afcService Apple File Client service instance from
|
|
27
|
+
* 'appium-ios-device' module
|
|
28
|
+
* @param remotePath Relative path to the file on the device
|
|
29
|
+
* @returns The file content as a buffer
|
|
30
|
+
*/
|
|
31
|
+
export async function pullFile(afcService: any, remotePath: string): Promise<Buffer> {
|
|
32
|
+
const stream = await afcService.createReadStream(remotePath, {autoDestroy: true});
|
|
33
|
+
const pullPromise = new B((resolve, reject) => {
|
|
34
|
+
stream.on('close', resolve);
|
|
35
|
+
stream.on('error', reject);
|
|
36
|
+
}).timeout(IO_TIMEOUT_MS);
|
|
37
|
+
const buffers: Buffer[] = [];
|
|
38
|
+
stream.on('data', (data: Buffer) => buffers.push(data));
|
|
39
|
+
await pullPromise;
|
|
40
|
+
return Buffer.concat(buffers);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Retrieve a folder from a real device
|
|
45
|
+
*
|
|
46
|
+
* @param afcService Apple File Client service instance from
|
|
47
|
+
* 'appium-ios-device' module
|
|
48
|
+
* @param remoteRootPath Relative path to the folder on the device
|
|
49
|
+
* @returns The folder content as a zipped base64-encoded buffer
|
|
50
|
+
*/
|
|
51
|
+
export async function pullFolder(afcService: any, remoteRootPath: string): Promise<Buffer> {
|
|
52
|
+
const tmpFolder = await tempDir.openDir();
|
|
53
|
+
try {
|
|
54
|
+
let localTopItem: string | null = null;
|
|
55
|
+
let countFilesSuccess = 0;
|
|
56
|
+
let countFilesFail = 0;
|
|
57
|
+
let countFolders = 0;
|
|
58
|
+
const pullPromises: B<void>[] = [];
|
|
59
|
+
await afcService.walkDir(remoteRootPath, true, async (remotePath: string, isDir: boolean) => {
|
|
60
|
+
const localPath = path.join(tmpFolder, remotePath);
|
|
61
|
+
const dirname = isDir ? localPath : path.dirname(localPath);
|
|
62
|
+
if (!(await folderExists(dirname))) {
|
|
63
|
+
await mkdirp(dirname);
|
|
64
|
+
}
|
|
65
|
+
if (!localTopItem || localPath.split(path.sep).length < localTopItem.split(path.sep).length) {
|
|
66
|
+
localTopItem = localPath;
|
|
67
|
+
}
|
|
68
|
+
if (isDir) {
|
|
69
|
+
++countFolders;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const readStream = await afcService.createReadStream(remotePath, {autoDestroy: true});
|
|
74
|
+
const writeStream = fs.createWriteStream(localPath, {autoClose: true});
|
|
75
|
+
pullPromises.push(
|
|
76
|
+
new B<void>((resolve) => {
|
|
77
|
+
writeStream.on('close', () => {
|
|
78
|
+
++countFilesSuccess;
|
|
79
|
+
resolve();
|
|
80
|
+
});
|
|
81
|
+
const onStreamingError = (e: Error) => {
|
|
82
|
+
readStream.unpipe(writeStream);
|
|
83
|
+
defaultLogger.warn(
|
|
84
|
+
`Cannot pull '${remotePath}' to '${localPath}'. ` +
|
|
85
|
+
`The file will be skipped. Original error: ${e.message}`,
|
|
86
|
+
);
|
|
87
|
+
++countFilesFail;
|
|
88
|
+
resolve();
|
|
89
|
+
};
|
|
90
|
+
writeStream.on('error', onStreamingError);
|
|
91
|
+
readStream.on('error', onStreamingError);
|
|
92
|
+
}).timeout(IO_TIMEOUT_MS),
|
|
93
|
+
);
|
|
94
|
+
readStream.pipe(writeStream);
|
|
95
|
+
if (pullPromises.length >= MAX_IO_CHUNK_SIZE) {
|
|
96
|
+
await B.any(pullPromises);
|
|
97
|
+
for (let i = pullPromises.length - 1; i >= 0; i--) {
|
|
98
|
+
if (pullPromises[i].isFulfilled()) {
|
|
99
|
+
pullPromises.splice(i, 1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
// Wait for the rest of files to be pulled
|
|
105
|
+
if (!_.isEmpty(pullPromises)) {
|
|
106
|
+
await B.all(pullPromises);
|
|
107
|
+
}
|
|
108
|
+
defaultLogger.info(
|
|
109
|
+
`Pulled ${util.pluralize('file', countFilesSuccess, true)} out of ` +
|
|
110
|
+
`${countFilesSuccess + countFilesFail} and ${util.pluralize(
|
|
111
|
+
'folder',
|
|
112
|
+
countFolders,
|
|
113
|
+
true,
|
|
114
|
+
)} ` +
|
|
115
|
+
`from '${remoteRootPath}'`,
|
|
116
|
+
);
|
|
117
|
+
return await zip.toInMemoryZip(localTopItem ? path.dirname(localTopItem) : tmpFolder, {
|
|
118
|
+
encodeToBase64: true,
|
|
119
|
+
});
|
|
120
|
+
} finally {
|
|
121
|
+
await fs.rimraf(tmpFolder);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Pushes a file to a real device
|
|
127
|
+
*
|
|
128
|
+
* @param afcService afcService Apple File Client service instance from
|
|
129
|
+
* 'appium-ios-device' module
|
|
130
|
+
* @param localPathOrPayload Either full path to the source file
|
|
131
|
+
* or a buffer payload to be written into the remote destination
|
|
132
|
+
* @param remotePath Relative path to the file on the device. The remote
|
|
133
|
+
* folder structure is created automatically if necessary.
|
|
134
|
+
* @param opts Push file options
|
|
135
|
+
*/
|
|
136
|
+
export async function pushFile(
|
|
137
|
+
afcService: any,
|
|
138
|
+
localPathOrPayload: string | Buffer,
|
|
139
|
+
remotePath: string,
|
|
140
|
+
opts: PushFileOptions = {}
|
|
141
|
+
): Promise<void> {
|
|
142
|
+
const {timeoutMs = IO_TIMEOUT_MS} = opts;
|
|
143
|
+
const timer = new timing.Timer().start();
|
|
144
|
+
await remoteMkdirp(afcService, path.dirname(remotePath));
|
|
145
|
+
const source = Buffer.isBuffer(localPathOrPayload)
|
|
146
|
+
? localPathOrPayload
|
|
147
|
+
: fs.createReadStream(localPathOrPayload, {autoClose: true});
|
|
148
|
+
const writeStream = await afcService.createWriteStream(remotePath, {
|
|
149
|
+
autoDestroy: true,
|
|
150
|
+
});
|
|
151
|
+
writeStream.on('finish', writeStream.destroy);
|
|
152
|
+
let pushError: Error | null = null;
|
|
153
|
+
const filePushPromise = new B<void>((resolve, reject) => {
|
|
154
|
+
writeStream.on('close', () => {
|
|
155
|
+
if (pushError) {
|
|
156
|
+
reject(pushError);
|
|
157
|
+
} else {
|
|
158
|
+
resolve();
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
const onStreamError = (e: Error) => {
|
|
162
|
+
if (!Buffer.isBuffer(source)) {
|
|
163
|
+
source.unpipe(writeStream);
|
|
164
|
+
}
|
|
165
|
+
defaultLogger.debug(e);
|
|
166
|
+
pushError = e;
|
|
167
|
+
};
|
|
168
|
+
writeStream.on('error', onStreamError);
|
|
169
|
+
if (!Buffer.isBuffer(source)) {
|
|
170
|
+
source.on('error', onStreamError);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
if (Buffer.isBuffer(source)) {
|
|
174
|
+
writeStream.write(source);
|
|
175
|
+
writeStream.end();
|
|
176
|
+
} else {
|
|
177
|
+
source.pipe(writeStream);
|
|
178
|
+
}
|
|
179
|
+
await filePushPromise.timeout(Math.max(timeoutMs, 60000));
|
|
180
|
+
const fileSize = Buffer.isBuffer(localPathOrPayload)
|
|
181
|
+
? localPathOrPayload.length
|
|
182
|
+
: (await fs.stat(localPathOrPayload)).size;
|
|
183
|
+
defaultLogger.debug(
|
|
184
|
+
`Successfully pushed the file payload (${util.toReadableSizeString(fileSize)}) ` +
|
|
185
|
+
`to the remote location '${remotePath}' in ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Pushes a folder to a real device
|
|
191
|
+
*
|
|
192
|
+
* @param afcService Apple File Client service instance from
|
|
193
|
+
* 'appium-ios-device' module
|
|
194
|
+
* @param srcRootPath The full path to the source folder
|
|
195
|
+
* @param dstRootPath The relative path to the destination folder. The folder
|
|
196
|
+
* will be deleted if already exists.
|
|
197
|
+
* @param opts Push folder options
|
|
198
|
+
*/
|
|
199
|
+
export async function pushFolder(
|
|
200
|
+
afcService: any,
|
|
201
|
+
srcRootPath: string,
|
|
202
|
+
dstRootPath: string,
|
|
203
|
+
opts: PushFolderOptions = {}
|
|
204
|
+
): Promise<void> {
|
|
205
|
+
const {timeoutMs = IO_TIMEOUT_MS, enableParallelPush = false} = opts;
|
|
206
|
+
|
|
207
|
+
const timer = new timing.Timer().start();
|
|
208
|
+
const allItems = /** @type {import('path-scurry').Path[]} */ (
|
|
209
|
+
/** @type {unknown} */ (
|
|
210
|
+
await fs.glob('**', {
|
|
211
|
+
cwd: srcRootPath,
|
|
212
|
+
withFileTypes: true,
|
|
213
|
+
})
|
|
214
|
+
)
|
|
215
|
+
) as any[];
|
|
216
|
+
defaultLogger.debug(`Successfully scanned the tree structure of '${srcRootPath}'`);
|
|
217
|
+
// top-level folders go first
|
|
218
|
+
const foldersToPush: string[] = allItems
|
|
219
|
+
.filter((x) => x.isDirectory())
|
|
220
|
+
.map((x) => x.relative())
|
|
221
|
+
.sort((a, b) => a.split(path.sep).length - b.split(path.sep).length);
|
|
222
|
+
// larger files go first
|
|
223
|
+
const filesToPush: string[] = allItems
|
|
224
|
+
.filter((x) => !x.isDirectory())
|
|
225
|
+
.sort((a, b) => (b.size ?? 0) - (a.size ?? 0))
|
|
226
|
+
.map((x) => x.relative());
|
|
227
|
+
defaultLogger.debug(
|
|
228
|
+
`Got ${util.pluralize('folder', foldersToPush.length, true)} and ` +
|
|
229
|
+
`${util.pluralize('file', filesToPush.length, true)} to push`,
|
|
230
|
+
);
|
|
231
|
+
// create the folder structure first
|
|
232
|
+
try {
|
|
233
|
+
await afcService.deleteDirectory(dstRootPath);
|
|
234
|
+
} catch {}
|
|
235
|
+
await afcService.createDirectory(dstRootPath);
|
|
236
|
+
for (const relativeFolderPath of foldersToPush) {
|
|
237
|
+
// createDirectory does not accept folder names ending with a path separator
|
|
238
|
+
const absoluteFolderPath = _.trimEnd(path.join(dstRootPath, relativeFolderPath), path.sep);
|
|
239
|
+
if (absoluteFolderPath) {
|
|
240
|
+
await afcService.createDirectory(absoluteFolderPath);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// do not forget about the root folder
|
|
244
|
+
defaultLogger.debug(
|
|
245
|
+
`Successfully created the remote folder structure ` +
|
|
246
|
+
`(${util.pluralize('item', foldersToPush.length + 1, true)})`,
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const _pushFile = async (relativePath: string): Promise<void> => {
|
|
250
|
+
const absoluteSourcePath = path.join(srcRootPath, relativePath);
|
|
251
|
+
const readStream = fs.createReadStream(absoluteSourcePath, {autoClose: true});
|
|
252
|
+
const absoluteDestinationPath = path.join(dstRootPath, relativePath);
|
|
253
|
+
const writeStream = await afcService.createWriteStream(absoluteDestinationPath, {
|
|
254
|
+
autoDestroy: true,
|
|
255
|
+
});
|
|
256
|
+
writeStream.on('finish', writeStream.destroy);
|
|
257
|
+
let pushError: Error | null = null;
|
|
258
|
+
const filePushPromise = new B<void>((resolve, reject) => {
|
|
259
|
+
writeStream.on('close', () => {
|
|
260
|
+
if (pushError) {
|
|
261
|
+
reject(pushError);
|
|
262
|
+
} else {
|
|
263
|
+
resolve();
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
const onStreamError = (e: Error) => {
|
|
267
|
+
readStream.unpipe(writeStream);
|
|
268
|
+
defaultLogger.debug(e);
|
|
269
|
+
pushError = e;
|
|
270
|
+
};
|
|
271
|
+
writeStream.on('error', onStreamError);
|
|
272
|
+
readStream.on('error', onStreamError);
|
|
273
|
+
});
|
|
274
|
+
readStream.pipe(writeStream);
|
|
275
|
+
await filePushPromise.timeout(Math.max(timeoutMs - timer.getDuration().asMilliSeconds, 60000));
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
if (enableParallelPush) {
|
|
279
|
+
defaultLogger.debug(`Proceeding to parallel files push (max ${MAX_IO_CHUNK_SIZE} writers)`);
|
|
280
|
+
const pushPromises: B<void>[] = [];
|
|
281
|
+
for (const relativeFilePath of filesToPush) {
|
|
282
|
+
pushPromises.push(B.resolve(_pushFile(relativeFilePath)));
|
|
283
|
+
// keep the push queue filled
|
|
284
|
+
if (pushPromises.length >= MAX_IO_CHUNK_SIZE) {
|
|
285
|
+
await B.any(pushPromises);
|
|
286
|
+
const elapsedMs = timer.getDuration().asMilliSeconds;
|
|
287
|
+
if (elapsedMs > timeoutMs) {
|
|
288
|
+
throw new TimeoutError(`Timed out after ${elapsedMs} ms`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
for (let i = pushPromises.length - 1; i >= 0; i--) {
|
|
292
|
+
if (pushPromises[i].isFulfilled()) {
|
|
293
|
+
pushPromises.splice(i, 1);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (!_.isEmpty(pushPromises)) {
|
|
298
|
+
const remainingPromises = pushPromises.filter((p) => !p.isFulfilled());
|
|
299
|
+
if (remainingPromises.length > 0) {
|
|
300
|
+
await B.all(remainingPromises).timeout(
|
|
301
|
+
Math.max(timeoutMs - timer.getDuration().asMilliSeconds, 60000),
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
defaultLogger.debug(`Proceeding to serial files push`);
|
|
307
|
+
for (const relativeFilePath of filesToPush) {
|
|
308
|
+
await _pushFile(relativeFilePath);
|
|
309
|
+
const elapsedMs = timer.getDuration().asMilliSeconds;
|
|
310
|
+
if (elapsedMs > timeoutMs) {
|
|
311
|
+
throw new TimeoutError(`Timed out after ${elapsedMs} ms`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
defaultLogger.debug(
|
|
317
|
+
`Successfully pushed ${util.pluralize('folder', foldersToPush.length, true)} ` +
|
|
318
|
+
`and ${util.pluralize('file', filesToPush.length, true)} ` +
|
|
319
|
+
`within ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
//#endregion
|
|
324
|
+
|
|
325
|
+
//#region Public Device Connection Functions
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Get list of connected devices
|
|
329
|
+
*/
|
|
330
|
+
export async function getConnectedDevices(): Promise<string[]> {
|
|
331
|
+
if (['yes', 'true', '1'].includes(_.toLower(process.env.APPIUM_XCUITEST_PREFER_DEVICECTL))) {
|
|
332
|
+
return (await new Devicectl('').listDevices())
|
|
333
|
+
.map(({hardwareProperties}) => hardwareProperties?.udid)
|
|
334
|
+
.filter(Boolean);
|
|
335
|
+
}
|
|
336
|
+
return await utilities.getConnectedDevices();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
//#endregion
|
|
340
|
+
|
|
341
|
+
//#region Public Real Device Class
|
|
342
|
+
|
|
343
|
+
export class RealDevice {
|
|
344
|
+
readonly udid: string;
|
|
345
|
+
private readonly _log: AppiumLogger;
|
|
346
|
+
readonly devicectl: Devicectl;
|
|
347
|
+
|
|
348
|
+
constructor(udid: string, logger?: AppiumLogger) {
|
|
349
|
+
this.udid = udid;
|
|
350
|
+
this._log = logger ?? defaultLogger;
|
|
351
|
+
this.devicectl = new Devicectl(this.udid);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
get log(): AppiumLogger {
|
|
355
|
+
return this._log;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async remove(bundleId: string): Promise<void> {
|
|
359
|
+
const service = await services.startInstallationProxyService(this.udid);
|
|
360
|
+
try {
|
|
361
|
+
await service.uninstallApplication(bundleId);
|
|
362
|
+
} finally {
|
|
363
|
+
service.close();
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async removeApp(bundleId: string): Promise<void> {
|
|
368
|
+
await this.remove(bundleId);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async install(appPath: string, bundleId: string, opts: RealDeviceInstallOptions = {}): Promise<void> {
|
|
372
|
+
const {
|
|
373
|
+
timeoutMs = IO_TIMEOUT_MS,
|
|
374
|
+
} = opts;
|
|
375
|
+
const timer = new timing.Timer().start();
|
|
376
|
+
const afcService = await services.startAfcService(this.udid);
|
|
377
|
+
try {
|
|
378
|
+
let bundlePathOnPhone: string;
|
|
379
|
+
if ((await fs.stat(appPath)).isFile()) {
|
|
380
|
+
// https://github.com/doronz88/pymobiledevice3/blob/6ff5001f5776e03b610363254e82d7fbcad4ef5f/pymobiledevice3/services/installation_proxy.py#L75
|
|
381
|
+
bundlePathOnPhone = `/${path.basename(appPath)}`;
|
|
382
|
+
await pushFile(afcService, appPath, bundlePathOnPhone, {
|
|
383
|
+
timeoutMs,
|
|
384
|
+
});
|
|
385
|
+
} else {
|
|
386
|
+
bundlePathOnPhone = `${INSTALLATION_STAGING_DIR}/${bundleId}`;
|
|
387
|
+
await pushFolder(afcService, appPath, bundlePathOnPhone, {
|
|
388
|
+
enableParallelPush: true,
|
|
389
|
+
timeoutMs,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
await this.installOrUpgradeApplication(
|
|
393
|
+
bundlePathOnPhone,
|
|
394
|
+
{
|
|
395
|
+
timeout: Math.max(timeoutMs - timer.getDuration().asMilliSeconds, 60000),
|
|
396
|
+
isUpgrade: await this.isAppInstalled(bundleId),
|
|
397
|
+
}
|
|
398
|
+
);
|
|
399
|
+
} catch (err) {
|
|
400
|
+
this.log.debug((err as Error).stack);
|
|
401
|
+
let errMessage = `Cannot install the ${bundleId} application`;
|
|
402
|
+
if (err instanceof TimeoutError) {
|
|
403
|
+
errMessage += `. Consider increasing the value of 'appPushTimeout' capability (the current value equals to ${timeoutMs}ms)`;
|
|
404
|
+
}
|
|
405
|
+
errMessage += `. Original error: ${(err as Error).message}`;
|
|
406
|
+
throw new Error(errMessage);
|
|
407
|
+
} finally {
|
|
408
|
+
afcService.close();
|
|
409
|
+
}
|
|
410
|
+
this.log.info(
|
|
411
|
+
`The installation of '${bundleId}' succeeded after ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async installOrUpgradeApplication(bundlePathOnPhone: string, opts: InstallOrUpgradeOptions): Promise<void> {
|
|
416
|
+
const {isUpgrade, timeout} = opts;
|
|
417
|
+
const notificationService = await services.startNotificationProxyService(this.udid);
|
|
418
|
+
const installationService = await services.startInstallationProxyService(this.udid);
|
|
419
|
+
const appInstalledNotification = new B<void>((resolve) => {
|
|
420
|
+
notificationService.observeNotification(APPLICATION_INSTALLED_NOTIFICATION, {
|
|
421
|
+
notification: resolve,
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
const clientOptions = {PackageType: 'Developer'};
|
|
425
|
+
try {
|
|
426
|
+
if (isUpgrade) {
|
|
427
|
+
this.log.debug(
|
|
428
|
+
`An upgrade of the existing application is going to be performed. ` +
|
|
429
|
+
`Will timeout in ${timeout.toFixed(0)} ms`
|
|
430
|
+
);
|
|
431
|
+
await installationService.upgradeApplication(bundlePathOnPhone, clientOptions, timeout);
|
|
432
|
+
} else {
|
|
433
|
+
this.log.debug(
|
|
434
|
+
`A new application installation is going to be performed. ` +
|
|
435
|
+
`Will timeout in ${timeout.toFixed(0)} ms`
|
|
436
|
+
);
|
|
437
|
+
await installationService.installApplication(bundlePathOnPhone, clientOptions, timeout);
|
|
438
|
+
}
|
|
439
|
+
try {
|
|
440
|
+
await appInstalledNotification.timeout(
|
|
441
|
+
APPLICATION_NOTIFICATION_TIMEOUT_MS,
|
|
442
|
+
`Could not get the application installed notification within ` +
|
|
443
|
+
`${APPLICATION_NOTIFICATION_TIMEOUT_MS}ms but we will continue`,
|
|
444
|
+
);
|
|
445
|
+
} catch (e) {
|
|
446
|
+
this.log.warn((e as Error).message);
|
|
447
|
+
}
|
|
448
|
+
} finally {
|
|
449
|
+
installationService.close();
|
|
450
|
+
notificationService.close();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Alias for {@linkcode install}
|
|
456
|
+
*/
|
|
457
|
+
async installApp(appPath: string, bundleId: string, opts: RealDeviceInstallOptions = {}): Promise<void> {
|
|
458
|
+
return await this.install(appPath, bundleId, opts);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Return an application object if test app has 'bundleid'.
|
|
463
|
+
* The target bundleid can be User and System apps.
|
|
464
|
+
*
|
|
465
|
+
* @param bundleId The bundleId to ensure it is installed
|
|
466
|
+
* @returns Returns True if the app is installed on the device under test.
|
|
467
|
+
*/
|
|
468
|
+
async isAppInstalled(bundleId: string): Promise<boolean> {
|
|
469
|
+
return Boolean(await this.fetchAppInfo(bundleId));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Fetches various attributes, like bundle id, version, entitlements etc. of
|
|
474
|
+
* an installed application.
|
|
475
|
+
*
|
|
476
|
+
* @param bundleId the bundle identifier of an app to check
|
|
477
|
+
* @param returnAttributes If provided then
|
|
478
|
+
* only fetches the requested attributes of the app into the resulting object.
|
|
479
|
+
* Some apps may have too many attributes, so it makes sense to limit these
|
|
480
|
+
* by default if you don't need all of them.
|
|
481
|
+
* @returns Either app info as an object or undefined if the app is not found.
|
|
482
|
+
*/
|
|
483
|
+
async fetchAppInfo(
|
|
484
|
+
bundleId: string,
|
|
485
|
+
returnAttributes: string | string[] = ['CFBundleIdentifier', 'CFBundleVersion']
|
|
486
|
+
): Promise<Record<string, any> | undefined> {
|
|
487
|
+
const service = await services.startInstallationProxyService(this.udid);
|
|
488
|
+
try {
|
|
489
|
+
return (
|
|
490
|
+
await service.lookupApplications({
|
|
491
|
+
bundleIds: bundleId,
|
|
492
|
+
// https://github.com/appium/appium/issues/18753
|
|
493
|
+
returnAttributes,
|
|
494
|
+
})
|
|
495
|
+
)[bundleId];
|
|
496
|
+
} finally {
|
|
497
|
+
service.close();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async terminateApp(bundleId: string, platformVersion: string): Promise<boolean> {
|
|
502
|
+
let instrumentService: any;
|
|
503
|
+
let installProxyService: any;
|
|
504
|
+
try {
|
|
505
|
+
installProxyService = await services.startInstallationProxyService(this.udid);
|
|
506
|
+
const apps = await installProxyService.listApplications({
|
|
507
|
+
returnAttributes: ['CFBundleIdentifier', 'CFBundleExecutable']
|
|
508
|
+
});
|
|
509
|
+
if (!apps[bundleId]) {
|
|
510
|
+
this.log.info(`The bundle id '${bundleId}' did not exist`);
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
const executableName = apps[bundleId].CFBundleExecutable;
|
|
514
|
+
this.log.debug(`The executable name for the bundle id '${bundleId}' was '${executableName}'`);
|
|
515
|
+
|
|
516
|
+
// 'devicectl' has overhead (generally?) than the instrument service via appium-ios-device,
|
|
517
|
+
// so hre uses the 'devicectl' only for iOS 17+.
|
|
518
|
+
if (util.compareVersions(platformVersion, '>=', '17.0')) {
|
|
519
|
+
this.log.debug(`Calling devicectl to kill the process`);
|
|
520
|
+
|
|
521
|
+
const pids = (await this.devicectl.listProcesses())
|
|
522
|
+
.filter(({executable}) => executable.endsWith(`/${executableName}`))
|
|
523
|
+
.map(({processIdentifier}) => processIdentifier);
|
|
524
|
+
if (_.isEmpty(pids)) {
|
|
525
|
+
this.log.info(`The process of the bundle id '${bundleId}' was not running`);
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
await this.devicectl.sendSignalToProcess(pids[0], 2);
|
|
529
|
+
} else {
|
|
530
|
+
instrumentService = await services.startInstrumentService(this.udid);
|
|
531
|
+
|
|
532
|
+
// The result of "runningProcesses" includes `bundle_id` key in iOS 16+ (possibly a specific 16.x+)
|
|
533
|
+
// then here may not be necessary to find a process with `CFBundleExecutable`
|
|
534
|
+
// after dropping older iOS version support.
|
|
535
|
+
const processes = await instrumentService.callChannel(
|
|
536
|
+
INSTRUMENT_CHANNEL.DEVICE_INFO,
|
|
537
|
+
'runningProcesses',
|
|
538
|
+
);
|
|
539
|
+
const process = processes.selector.find((process: any) => process.name === executableName);
|
|
540
|
+
if (!process) {
|
|
541
|
+
this.log.info(`The process of the bundle id '${bundleId}' was not running`);
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
await instrumentService.callChannel(
|
|
545
|
+
INSTRUMENT_CHANNEL.PROCESS_CONTROL,
|
|
546
|
+
'killPid:',
|
|
547
|
+
`${process.pid}`,
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
} catch (err) {
|
|
551
|
+
this.log.warn(`Failed to kill '${bundleId}'. Original error: ${(err as any).stderr || (err as Error).message}`);
|
|
552
|
+
return false;
|
|
553
|
+
} finally {
|
|
554
|
+
if (installProxyService) {
|
|
555
|
+
installProxyService.close();
|
|
556
|
+
}
|
|
557
|
+
if (instrumentService) {
|
|
558
|
+
instrumentService.close();
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* @param bundleName The name of CFBundleName in Info.plist
|
|
566
|
+
*
|
|
567
|
+
* @returns A list of User level apps' bundle ids which has
|
|
568
|
+
* 'CFBundleName' attribute as 'bundleName'.
|
|
569
|
+
*/
|
|
570
|
+
async getUserInstalledBundleIdsByBundleName(bundleName: string): Promise<string[]> {
|
|
571
|
+
const service = await services.startInstallationProxyService(this.udid);
|
|
572
|
+
try {
|
|
573
|
+
const applications = await service.listApplications({
|
|
574
|
+
applicationType: 'User', returnAttributes: ['CFBundleIdentifier', 'CFBundleName']
|
|
575
|
+
});
|
|
576
|
+
return _.reduce(
|
|
577
|
+
applications,
|
|
578
|
+
(acc: string[], {CFBundleName}, key: string) => {
|
|
579
|
+
if (CFBundleName === bundleName) {
|
|
580
|
+
acc.push(key);
|
|
581
|
+
}
|
|
582
|
+
return acc;
|
|
583
|
+
},
|
|
584
|
+
[],
|
|
585
|
+
);
|
|
586
|
+
} finally {
|
|
587
|
+
service.close();
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async getPlatformVersion(): Promise<string> {
|
|
592
|
+
return await utilities.getOSVersion(this.udid);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async reset(opts: {bundleId?: string; fullReset?: boolean}): Promise<void> {
|
|
596
|
+
const {bundleId, fullReset} = opts;
|
|
597
|
+
if (!bundleId || !fullReset || bundleId === SAFARI_BUNDLE_ID) {
|
|
598
|
+
// Safari cannot be removed as system app.
|
|
599
|
+
// Safari process handling will be managed by WDA
|
|
600
|
+
// with noReset, forceAppLaunch or shouldTerminateApp capabilities.
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
this.log.debug(`Reset: fullReset requested. Will try to uninstall the app '${bundleId}'.`);
|
|
605
|
+
if (!(await this.isAppInstalled(bundleId))) {
|
|
606
|
+
this.log.debug('Reset: app not installed. No need to uninstall');
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
try {
|
|
611
|
+
await this.remove(bundleId);
|
|
612
|
+
} catch (err) {
|
|
613
|
+
this.log.error(`Reset: could not remove '${bundleId}' from device: ${(err as Error).message}`);
|
|
614
|
+
throw err;
|
|
615
|
+
}
|
|
616
|
+
this.log.debug(`Reset: removed '${bundleId}'`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
//#endregion
|
|
621
|
+
|
|
622
|
+
//#region Public Device Management Functions
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Install app to real device
|
|
626
|
+
*/
|
|
627
|
+
export async function installToRealDevice(
|
|
628
|
+
this: XCUITestDriver,
|
|
629
|
+
app: string,
|
|
630
|
+
bundleId?: string,
|
|
631
|
+
opts: ManagementInstallOptions = {}
|
|
632
|
+
): Promise<void> {
|
|
633
|
+
const device = this.device as RealDevice;
|
|
634
|
+
|
|
635
|
+
if (!device.udid || !app || !bundleId) {
|
|
636
|
+
this.log.debug('No device id, app or bundle id, not installing to real device.');
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const {
|
|
641
|
+
skipUninstall,
|
|
642
|
+
timeout = DEFAULT_APP_INSTALLATION_TIMEOUT_MS,
|
|
643
|
+
} = opts;
|
|
644
|
+
|
|
645
|
+
if (!skipUninstall) {
|
|
646
|
+
this.log.info(`Reset requested. Removing app with id '${bundleId}' from the device`);
|
|
647
|
+
await device.remove(bundleId);
|
|
648
|
+
}
|
|
649
|
+
this.log.debug(`Installing '${app}' on the device with UUID '${device.udid}'`);
|
|
650
|
+
|
|
651
|
+
try {
|
|
652
|
+
await device.install(app, bundleId, {
|
|
653
|
+
timeoutMs: timeout,
|
|
654
|
+
});
|
|
655
|
+
this.log.debug('The app has been installed successfully.');
|
|
656
|
+
} catch (e) {
|
|
657
|
+
// Want to clarify the device's application installation state in this situation.
|
|
658
|
+
|
|
659
|
+
if (!skipUninstall || !(e as Error).message.includes('MismatchedApplicationIdentifierEntitlement')) {
|
|
660
|
+
// Other error cases that could not be recoverable by here.
|
|
661
|
+
// Exact error will be in the log.
|
|
662
|
+
|
|
663
|
+
// We cannot recover 'ApplicationVerificationFailed' situation since this reason is clearly the app's provisioning profile was invalid.
|
|
664
|
+
// [XCUITest] Error installing app '/path/to.app': Unexpected data: {"Error":"ApplicationVerificationFailed","ErrorDetail":-402620395,"ErrorDescription":"Failed to verify code signature of /path/to.app : 0xe8008015 (A valid provisioning profile for this executable was not found.)"}
|
|
665
|
+
throw e;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// If the error was by below error case, we could recover the situation
|
|
669
|
+
// by uninstalling the device's app bundle id explicitly regard less the app exists on the device or not (e.g. offload app).
|
|
670
|
+
// [XCUITest] Error installing app '/path/to.app': Unexpected data: {"Error":"MismatchedApplicationIdentifierEntitlement","ErrorDescription":"Upgrade's application-identifier entitlement string (TEAM_ID.com.kazucocoa.example) does not match installed application's application-identifier string (ANOTHER_TEAM_ID.com.kazucocoa.example); rejecting upgrade."}
|
|
671
|
+
this.log.info(`The application identified by '${bundleId}' cannot be installed because it might ` +
|
|
672
|
+
`be already cached on the device, probably with a different signature. ` +
|
|
673
|
+
`Will try to remove it and install a new copy. Original error: ${(e as Error).message}`);
|
|
674
|
+
await device.remove(bundleId);
|
|
675
|
+
await device.install(app, bundleId, {
|
|
676
|
+
timeoutMs: timeout,
|
|
677
|
+
});
|
|
678
|
+
this.log.debug('The app has been installed after one retrial.');
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Run real device reset
|
|
684
|
+
*/
|
|
685
|
+
export async function runRealDeviceReset(this: XCUITestDriver): Promise<void> {
|
|
686
|
+
if (!this.opts.noReset || this.opts.fullReset) {
|
|
687
|
+
this.log.debug('Reset: running ios real device reset flow');
|
|
688
|
+
if (!this.opts.noReset) {
|
|
689
|
+
await (this.device as RealDevice).reset(this.opts);
|
|
690
|
+
}
|
|
691
|
+
} else {
|
|
692
|
+
this.log.debug('Reset: fullReset not set. Leaving as is');
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Configures Safari startup options based on the given session capabilities.
|
|
698
|
+
*
|
|
699
|
+
* !!! This method mutates driver options.
|
|
700
|
+
*
|
|
701
|
+
* @returns true if process arguments have been modified
|
|
702
|
+
*/
|
|
703
|
+
export function applySafariStartupArgs(this: XCUITestDriver): boolean {
|
|
704
|
+
const prefs = buildSafariPreferences(this.opts);
|
|
705
|
+
if (_.isEmpty(prefs)) {
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const args = _.toPairs(prefs)
|
|
710
|
+
.flatMap(([key, value]) => [_.startsWith(key, '-') ? key : `-${key}`, String(value)]);
|
|
711
|
+
defaultLogger.debug(`Generated Safari command line arguments: ${args.join(' ')}`);
|
|
712
|
+
if (_.isPlainObject(this.opts.processArguments)) {
|
|
713
|
+
this.opts.processArguments.args = [...(this.opts.processArguments.args ?? []), ...args];
|
|
714
|
+
} else {
|
|
715
|
+
this.opts.processArguments = {args};
|
|
716
|
+
}
|
|
717
|
+
return true;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Auto-detect device UDID
|
|
722
|
+
*/
|
|
723
|
+
export async function detectUdid(this: XCUITestDriver): Promise<string> {
|
|
724
|
+
this.log.debug('Auto-detecting real device udid...');
|
|
725
|
+
const udids = await getConnectedDevices();
|
|
726
|
+
if (_.isEmpty(udids)) {
|
|
727
|
+
throw new Error('No real devices are connected to the host');
|
|
728
|
+
}
|
|
729
|
+
const udid = udids[udids.length - 1];
|
|
730
|
+
if (udids.length > 1) {
|
|
731
|
+
this.log.info(`Multiple devices found: ${udids.join(', ')}`);
|
|
732
|
+
this.log.info(`Choosing '${udid}'. Consider settings the 'udid' capability if another device must be selected`);
|
|
733
|
+
}
|
|
734
|
+
this.log.debug(`Detected real device udid: '${udid}'`);
|
|
735
|
+
return udid;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
//#endregion
|
|
740
|
+
|
|
741
|
+
//#region Private Helper Functions
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Checks a presence of a local folder.
|
|
745
|
+
*
|
|
746
|
+
* @param folderPath Full path to the local folder
|
|
747
|
+
* @returns True if the folder exists and is actually a folder
|
|
748
|
+
*/
|
|
749
|
+
async function folderExists(folderPath: string): Promise<boolean> {
|
|
750
|
+
try {
|
|
751
|
+
return (await fs.stat(folderPath)).isDirectory();
|
|
752
|
+
} catch {
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Creates remote folder path recursively. Noop if the given path
|
|
759
|
+
* already exists
|
|
760
|
+
*
|
|
761
|
+
* @param afcService Apple File Client service instance from
|
|
762
|
+
* 'appium-ios-device' module
|
|
763
|
+
* @param remoteRoot The relative path to the remote folder structure
|
|
764
|
+
* to be created
|
|
765
|
+
*/
|
|
766
|
+
async function remoteMkdirp(afcService: any, remoteRoot: string): Promise<void> {
|
|
767
|
+
if (remoteRoot === '.' || remoteRoot === '/') {
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
try {
|
|
771
|
+
await afcService.listDirectory(remoteRoot);
|
|
772
|
+
return;
|
|
773
|
+
} catch {
|
|
774
|
+
// This means that the directory is missing and we got an object not found error.
|
|
775
|
+
// Therefore, we are going to the parent
|
|
776
|
+
await remoteMkdirp(afcService, path.dirname(remoteRoot));
|
|
777
|
+
}
|
|
778
|
+
await afcService.createDirectory(remoteRoot);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
//#endregion
|
|
782
|
+
|
|
783
|
+
//#region Type Definitions
|
|
784
|
+
|
|
785
|
+
export interface PushFileOptions {
|
|
786
|
+
/** The maximum count of milliceconds to wait until file push is completed. Cannot be lower than 60000ms */
|
|
787
|
+
timeoutMs?: number;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
export interface PushFolderOptions {
|
|
791
|
+
/** The maximum timeout to wait until a single file is copied */
|
|
792
|
+
timeoutMs?: number;
|
|
793
|
+
/** Whether to push files in parallel. This usually gives better performance, but might sometimes be less stable. */
|
|
794
|
+
enableParallelPush?: boolean;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
export interface RealDeviceInstallOptions {
|
|
798
|
+
/** Application installation timeout in milliseconds */
|
|
799
|
+
timeoutMs?: number;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
export interface InstallOrUpgradeOptions {
|
|
803
|
+
/** Install/upgrade timeout in milliseconds */
|
|
804
|
+
timeout: number;
|
|
805
|
+
/** Whether it is an app upgrade or a new install */
|
|
806
|
+
isUpgrade: boolean;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
export interface ManagementInstallOptions {
|
|
810
|
+
/** Whether to skip app uninstall before installing it */
|
|
811
|
+
skipUninstall?: boolean;
|
|
812
|
+
/** App install timeout */
|
|
813
|
+
timeout?: number;
|
|
814
|
+
/** Whether to enforce the app uninstallation. e.g. fullReset, or enforceAppInstall is true */
|
|
815
|
+
shouldEnforceUninstall?: boolean;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
//#endregion
|