appium-xcuitest-driver 10.13.2 → 10.13.3

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.
@@ -0,0 +1,414 @@
1
+ import _ from 'lodash';
2
+ import {fs, util} from 'appium/support';
3
+ import {errors} from 'appium/driver';
4
+ import {services} from 'appium-ios-device';
5
+ import path from 'node:path';
6
+ import B from 'bluebird';
7
+ import {
8
+ SUPPORTED_EXTENSIONS,
9
+ onPostConfigureApp,
10
+ onDownloadApp,
11
+ } from '../app-utils';
12
+ import type {XCUITestDriver} from '../driver';
13
+ import type {AppState} from './enum';
14
+ import type {Simulator} from 'appium-ios-simulator';
15
+ import type {RealDevice} from '../device/real-device-management';
16
+
17
+ /**
18
+ * Installs the given application to the device under test.
19
+ *
20
+ * Please ensure the app is built for a correct architecture and is signed with a proper developer signature (for real devices) prior to calling this.
21
+ * @param app - See docs for `appium:app` capability
22
+ * @param timeoutMs - The maximum time to wait until app install is finished (in ms) on real devices.
23
+ * If not provided, then the value of `appium:appPushTimeout` capability is used. If the capability is not provided then the default is 240000ms (4 minutes).
24
+ * @param checkVersion - If the application installation follows currently installed application's version status if provided.
25
+ * No checking occurs if no this option.
26
+ * @privateRemarks Link to capability docs
27
+ */
28
+ export async function mobileInstallApp(
29
+ this: XCUITestDriver,
30
+ app: string,
31
+ timeoutMs?: number,
32
+ checkVersion?: boolean,
33
+ ): Promise<void> {
34
+ const srcAppPath = await this.helpers.configureApp(app, {
35
+ onPostProcess: onPostConfigureApp.bind(this),
36
+ onDownload: onDownloadApp.bind(this),
37
+ supportedExtensions: SUPPORTED_EXTENSIONS,
38
+ });
39
+ this.log.info(
40
+ `Installing '${srcAppPath}' to the ${this.isRealDevice() ? 'real device' : 'Simulator'} ` +
41
+ `with UDID '${this.device.udid}'`,
42
+ );
43
+ if (!(await fs.exists(srcAppPath))) {
44
+ throw this.log.errorWithException(
45
+ `The application at '${srcAppPath}' does not exist or is not accessible`,
46
+ );
47
+ }
48
+
49
+ const bundleId = await this.appInfosCache.extractBundleId(srcAppPath);
50
+ if (checkVersion) {
51
+ const {install} = await this.checkAutInstallationState({
52
+ enforceAppInstall: false,
53
+ fullReset: false,
54
+ noReset: false,
55
+ bundleId,
56
+ app: srcAppPath,
57
+ });
58
+
59
+ if (!install) {
60
+ this.log.info(`Skipping the installation of '${bundleId}'`);
61
+ return;
62
+ }
63
+ }
64
+
65
+ await this.device.installApp(
66
+ srcAppPath,
67
+ bundleId,
68
+ {
69
+ timeoutMs: timeoutMs ?? this.opts.appPushTimeout,
70
+ },
71
+ );
72
+ this.log.info(`Installation of '${srcAppPath}' succeeded`);
73
+ }
74
+
75
+ /**
76
+ * Checks whether the given application is installed on the device under test.
77
+ * Offload app is handled as not installed.
78
+ *
79
+ * @param bundleId - The bundle identifier of the application to be checked
80
+ * @returns `true` if the application is installed; `false` otherwise
81
+ */
82
+ export async function mobileIsAppInstalled(
83
+ this: XCUITestDriver,
84
+ bundleId: string,
85
+ ): Promise<boolean> {
86
+ const installed = await this.device.isAppInstalled(bundleId);
87
+ this.log.info(`App '${bundleId}' is${installed ? '' : ' not'} installed`);
88
+ return installed;
89
+ }
90
+
91
+ /**
92
+ * Removes/uninstalls the given application from the device under test.
93
+ * Offload app data could also be removed.
94
+ *
95
+ * @param bundleId - The bundle identifier of the application to be removed
96
+ * @returns `true` if the application has been removed successfully; `false` otherwise
97
+ */
98
+ export async function mobileRemoveApp(
99
+ this: XCUITestDriver,
100
+ bundleId: string,
101
+ ): Promise<boolean> {
102
+ this.log.info(
103
+ `Uninstalling the application with bundle identifier '${bundleId}' ` +
104
+ `from the ${this.isRealDevice() ? 'real device' : 'Simulator'} with UDID '${this.device.udid}'`,
105
+ );
106
+ try {
107
+ await this.device.removeApp(bundleId);
108
+ this.log.info(`Removal of '${bundleId}' succeeded`);
109
+ return true;
110
+ } catch (err: any) {
111
+ this.log.warn(`Cannot remove '${bundleId}'. Original error: ${err.message}`);
112
+ return false;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Executes the given app on the device under test.
118
+ *
119
+ * If the app is already running it will be activated. If the app is not installed or cannot be launched then an exception is thrown.
120
+ * @param bundleId - The bundle identifier of the application to be launched
121
+ * @param args - One or more command line arguments for the app. If the app is already running then this argument is ignored.
122
+ * @param environment - Environment variables mapping for the app. If the app is already running then this argument is ignored.
123
+ */
124
+ export async function mobileLaunchApp(
125
+ this: XCUITestDriver,
126
+ bundleId: string,
127
+ args?: string | string[],
128
+ environment?: Record<string, any>,
129
+ ): Promise<void> {
130
+ const launchOptions: {
131
+ bundleId: string;
132
+ arguments?: any[];
133
+ environment?: any;
134
+ } = {bundleId};
135
+ if (args) {
136
+ launchOptions.arguments = Array.isArray(args) ? args : [args];
137
+ }
138
+ if (environment) {
139
+ launchOptions.environment = environment;
140
+ }
141
+ await this.proxyCommand('/wda/apps/launch', 'POST', launchOptions);
142
+ }
143
+
144
+ /**
145
+ * Terminates the given app on the device under test.
146
+ *
147
+ * This command performs termination via [XCTest's `terminate`](https://developer.apple.com/documentation/xctest/xcuiapplication/1500637-terminate) API. If the app is not installed an exception is thrown. If the app is not running then nothing is done.
148
+ * @param bundleId - The bundle identifier of the application to be terminated
149
+ * @returns `true` if the app has been terminated successfully; `false` otherwise
150
+ */
151
+ export async function mobileTerminateApp(
152
+ this: XCUITestDriver,
153
+ bundleId: string,
154
+ ): Promise<boolean> {
155
+ return (await this.proxyCommand('/wda/apps/terminate', 'POST', {bundleId})) as boolean;
156
+ }
157
+
158
+ /**
159
+ * Activate the given app on the device under test.
160
+ *
161
+ * This pushes the app to the foreground if it is running in the background. An exception is thrown if the app is not install or isn't running. Nothing is done if the app is already in the foreground.
162
+ *
163
+ * @param bundleId - The bundle identifier of the application to be activated
164
+ */
165
+ export async function mobileActivateApp(
166
+ this: XCUITestDriver,
167
+ bundleId: string,
168
+ ): Promise<void> {
169
+ await this.proxyCommand('/wda/apps/activate', 'POST', {bundleId});
170
+ }
171
+
172
+ /**
173
+ * Kill the given app on the real device under test by instruments service.
174
+ *
175
+ * If the app is not running or kill failed, then nothing is done.
176
+ *
177
+ * @remarks `appium-xcuitest-driver` v4.4 does not require `py-ios-device` to be installed.
178
+ * @privateRemarks See implementation at https://github.com/YueChen-C/py-ios-device/blob/51f4683c5c3c385a015858ada07a5f1c62d3cf57/ios_device/cli/base.py#L220
179
+ * @see https://github.com/YueChen-C/py-ios-device
180
+ * @param bundleId - The bundle identifier of the application to be killed
181
+ * @returns `true` if the app has been killed successfully; `false` otherwise
182
+ * @group Real Device Only
183
+ */
184
+ export async function mobileKillApp(
185
+ this: XCUITestDriver,
186
+ bundleId: string,
187
+ ): Promise<boolean> {
188
+ if (!this.isRealDevice()) {
189
+ throw new errors.UnsupportedOperationError('A real device is required');
190
+ }
191
+
192
+ return await (this.device as RealDevice).terminateApp(bundleId, String(this.opts.platformVersion));
193
+ }
194
+
195
+ /**
196
+ * Queries the state of an installed application from the device under test.
197
+ *
198
+ * If the app with the given `bundleId` is not installed, an exception will be thrown.
199
+ *
200
+ * @param bundleId - The bundle identifier of the application to be queried
201
+ * @returns The actual application state code
202
+ * @see https://developer.apple.com/documentation/xctest/xcuiapplicationstate?language=objc
203
+ */
204
+ export async function mobileQueryAppState(
205
+ this: XCUITestDriver,
206
+ bundleId: string,
207
+ ): Promise<AppState> {
208
+ return (await this.proxyCommand('/wda/apps/state', 'POST', {bundleId})) as AppState;
209
+ }
210
+
211
+ /**
212
+ * Installs the given application to the device under test.
213
+ *
214
+ * This is a wrapper around {@linkcode mobileInstallApp mobile: installApp}.
215
+ *
216
+ * @param appPath - Path to the application bundle or .ipa/.app file
217
+ * @param opts - Installation options
218
+ * @param opts.timeoutMs - Maximum time to wait for installation to complete (in milliseconds)
219
+ * @param opts.strategy - If `true`, checks the version before installing and skips if already installed
220
+ */
221
+ export async function installApp(
222
+ this: XCUITestDriver,
223
+ appPath: string,
224
+ opts: {timeoutMs?: number; strategy?: boolean} = {},
225
+ ): Promise<void> {
226
+ await this.mobileInstallApp(appPath, opts.timeoutMs, opts.strategy);
227
+ }
228
+
229
+ /**
230
+ * Activates the given app on the device under test.
231
+ *
232
+ * This is a wrapper around {@linkcode mobileLaunchApp mobile: launchApp}. If the app is already
233
+ * running, it will be activated (brought to foreground). If the app is not installed or cannot
234
+ * be launched, an exception is thrown.
235
+ *
236
+ * @param bundleId - The bundle identifier of the application to be activated
237
+ * @param opts - Launch options
238
+ * @param opts.environment - Environment variables mapping for the app
239
+ * @param opts.arguments - Command line arguments for the app
240
+ */
241
+ export async function activateApp(
242
+ this: XCUITestDriver,
243
+ bundleId: string,
244
+ opts: {environment?: Record<string, any>; arguments?: string[]} = {},
245
+ ): Promise<void> {
246
+ const {environment, arguments: args} = opts;
247
+ return await this.mobileLaunchApp(bundleId, args, environment);
248
+ }
249
+
250
+ /**
251
+ * Checks whether the given application is installed on the device under test.
252
+ *
253
+ * This is a wrapper around {@linkcode mobileIsAppInstalled mobile: isAppInstalled}.
254
+ * Offload apps are treated as not installed.
255
+ *
256
+ * @param bundleId - The bundle identifier of the application to be checked
257
+ * @returns `true` if the application is installed; `false` otherwise
258
+ */
259
+ export async function isAppInstalled(
260
+ this: XCUITestDriver,
261
+ bundleId: string,
262
+ ): Promise<boolean> {
263
+ return await this.mobileIsAppInstalled(bundleId);
264
+ }
265
+
266
+ /**
267
+ * Terminates the given app on the device under test.
268
+ *
269
+ * This is a wrapper around {@linkcode mobileTerminateApp mobile: terminateApp}.
270
+ * The command performs termination via XCTest's `terminate` API. If the app is not installed,
271
+ * an exception is thrown. If the app is not running, nothing is done.
272
+ *
273
+ * @param bundleId - The bundle identifier of the application to be terminated
274
+ * @returns `true` if the app has been terminated successfully; `false` otherwise
275
+ */
276
+ export async function terminateApp(
277
+ this: XCUITestDriver,
278
+ bundleId: string,
279
+ ): Promise<boolean> {
280
+ return await this.mobileTerminateApp(bundleId);
281
+ }
282
+
283
+ /**
284
+ * Queries the state of an installed application from the device under test.
285
+ *
286
+ * This is a wrapper around {@linkcode mobileQueryAppState mobile: queryAppState}.
287
+ * If the app with the given `bundleId` is not installed, an exception will be thrown.
288
+ *
289
+ * @param bundleId - The bundle identifier of the application to be queried
290
+ * @returns The actual application state code
291
+ * @see https://developer.apple.com/documentation/xctest/xcuiapplicationstate?language=objc
292
+ */
293
+ export async function queryAppState(
294
+ this: XCUITestDriver,
295
+ bundleId: string,
296
+ ): Promise<AppState> {
297
+ return await this.mobileQueryAppState(bundleId);
298
+ }
299
+
300
+ /**
301
+ * List applications installed on the real device under test
302
+ *
303
+ * Read [Pushing/Pulling files](https://appium.io/docs/en/writing-running-appium/ios/ios-xctest-file-movement/) for more details.
304
+ * @param applicationType - The type of applications to list.
305
+ * @returns A list of apps where each item is a mapping of bundle identifiers to maps of platform-specific app properties.
306
+ * @remarks Having `UIFileSharingEnabled` set to `true` in the return app properties map means this app supports file upload/download in its `documents` container.
307
+ * @group Real Device Only
308
+ */
309
+ export async function mobileListApps(
310
+ this: XCUITestDriver,
311
+ applicationType: 'User' | 'System' = 'User',
312
+ ): Promise<Record<string, any>[]> {
313
+ if (!this.isRealDevice()) {
314
+ throw new errors.NotImplementedError(`This extension is only supported on real devices`);
315
+ }
316
+
317
+ const service = await services.startInstallationProxyService(this.device.udid);
318
+ try {
319
+ return await service.listApplications({applicationType});
320
+ } finally {
321
+ service.close();
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Deletes application data files, so it could start from the clean state next time
327
+ * it is launched.
328
+ * This API only works on a Simulator.
329
+ *
330
+ * @param bundleId Application bundle identifier
331
+ * @returns true if any files from the app's data container have been deleted
332
+ */
333
+ export async function mobileClearApp(
334
+ this: XCUITestDriver,
335
+ bundleId: string,
336
+ ): Promise<boolean> {
337
+ if (this.isRealDevice()) {
338
+ throw new errors.NotImplementedError(
339
+ `This extension is only supported on simulators. ` +
340
+ `The only known way to clear app data on real devices ` +
341
+ `would be to uninstall the app then perform a fresh install of it.`,
342
+ );
343
+ }
344
+
345
+ const simctl = (this.device as Simulator).simctl;
346
+ const dataRoot = await simctl.getAppContainer(bundleId, 'data');
347
+ this.log.debug(`Got the data container root of ${bundleId} at '${dataRoot}'`);
348
+ if (!(await fs.exists(dataRoot))) {
349
+ return false;
350
+ }
351
+
352
+ await this.mobileTerminateApp(bundleId);
353
+ const items = await fs.readdir(dataRoot);
354
+ if (!items.length) {
355
+ return false;
356
+ }
357
+
358
+ await B.all(items.map((item) => fs.rimraf(path.join(dataRoot, item))));
359
+ this.log.info(
360
+ `Cleaned up ${util.pluralize('item', items.length, true)} from ${bundleId}'s data container`,
361
+ );
362
+ return true;
363
+ }
364
+
365
+ /**
366
+ * Closes the app (simulates device home button press).
367
+ *
368
+ * It is possible to restore the app after a timeout or keep it minimized based on the parameter value.
369
+ *
370
+ * @param seconds - Timeout configuration. Accepts:
371
+ * - A positive number (seconds): app will be restored after the specified number of seconds
372
+ * - A negative number or zero: app will not be restored (kept minimized)
373
+ * - `undefined` or `null`: app will not be restored (kept minimized)
374
+ * - An object with `timeout` property:
375
+ * - `{timeout: 5000}`: app will be restored after 5 seconds (timeout in milliseconds)
376
+ * - `{timeout: null}` or `{timeout: -2}`: app will not be restored
377
+ */
378
+ export async function background(
379
+ this: XCUITestDriver,
380
+ seconds?: number | {timeout?: number | null},
381
+ ): Promise<void> {
382
+ const homescreen = '/wda/homescreen';
383
+ const deactivateApp = '/wda/deactivateApp';
384
+
385
+ let endpoint: string | undefined;
386
+ let params: Record<string, any> = {};
387
+ const selectEndpoint = (timeoutSeconds?: number | null) => {
388
+ if (!util.hasValue(timeoutSeconds)) {
389
+ endpoint = homescreen;
390
+ } else if (!isNaN(Number(timeoutSeconds))) {
391
+ const duration = parseFloat(String(timeoutSeconds));
392
+ if (duration >= 0) {
393
+ params = {duration};
394
+ endpoint = deactivateApp;
395
+ } else {
396
+ endpoint = homescreen;
397
+ }
398
+ }
399
+ };
400
+ if (seconds && !_.isNumber(seconds) && _.has(seconds, 'timeout')) {
401
+ const timeout = seconds.timeout;
402
+ selectEndpoint(isNaN(Number(timeout)) ? timeout : parseFloat(String(timeout)) / 1000.0);
403
+ } else {
404
+ selectEndpoint(_.isNumber(seconds) ? seconds : undefined);
405
+ }
406
+ if (!endpoint) {
407
+ throw new errors.InvalidArgumentError(
408
+ `Argument value is expected to be a valid number. ` +
409
+ `${JSON.stringify(seconds)} has been provided instead`,
410
+ );
411
+ }
412
+ return await this.proxyCommand(endpoint, 'POST', params, endpoint !== homescreen);
413
+ }
414
+