appium-xcuitest-driver 7.14.0 → 7.15.1

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/build/lib/app-infos-cache.d.ts +46 -0
  3. package/build/lib/app-infos-cache.d.ts.map +1 -0
  4. package/build/lib/app-infos-cache.js +156 -0
  5. package/build/lib/app-infos-cache.js.map +1 -0
  6. package/build/lib/app-utils.d.ts +32 -52
  7. package/build/lib/app-utils.d.ts.map +1 -1
  8. package/build/lib/app-utils.js +327 -219
  9. package/build/lib/app-utils.js.map +1 -1
  10. package/build/lib/commands/app-management.d.ts +5 -4
  11. package/build/lib/commands/app-management.d.ts.map +1 -1
  12. package/build/lib/commands/app-management.js +14 -7
  13. package/build/lib/commands/app-management.js.map +1 -1
  14. package/build/lib/commands/app-strings.d.ts +5 -2
  15. package/build/lib/commands/app-strings.d.ts.map +1 -1
  16. package/build/lib/commands/app-strings.js +6 -3
  17. package/build/lib/commands/app-strings.js.map +1 -1
  18. package/build/lib/commands/file-movement.js +1 -1
  19. package/build/lib/commands/file-movement.js.map +1 -1
  20. package/build/lib/commands/types.d.ts +1 -0
  21. package/build/lib/commands/types.d.ts.map +1 -1
  22. package/build/lib/commands/xctest-record-screen.d.ts +1 -1
  23. package/build/lib/desired-caps.d.ts +2 -0
  24. package/build/lib/desired-caps.d.ts.map +1 -1
  25. package/build/lib/desired-caps.js +1 -0
  26. package/build/lib/desired-caps.js.map +1 -1
  27. package/build/lib/driver.d.ts +30 -27
  28. package/build/lib/driver.d.ts.map +1 -1
  29. package/build/lib/driver.js +12 -12
  30. package/build/lib/driver.js.map +1 -1
  31. package/build/lib/execute-method-map.d.ts +1 -1
  32. package/build/lib/execute-method-map.js +1 -1
  33. package/build/lib/execute-method-map.js.map +1 -1
  34. package/build/lib/ios-fs-helpers.d.ts +30 -15
  35. package/build/lib/ios-fs-helpers.d.ts.map +1 -1
  36. package/build/lib/ios-fs-helpers.js +54 -21
  37. package/build/lib/ios-fs-helpers.js.map +1 -1
  38. package/build/lib/real-device-management.d.ts +0 -5
  39. package/build/lib/real-device-management.d.ts.map +1 -1
  40. package/build/lib/real-device-management.js +8 -5
  41. package/build/lib/real-device-management.js.map +1 -1
  42. package/build/lib/real-device.d.ts +13 -9
  43. package/build/lib/real-device.d.ts.map +1 -1
  44. package/build/lib/real-device.js +49 -75
  45. package/build/lib/real-device.js.map +1 -1
  46. package/lib/app-infos-cache.js +159 -0
  47. package/lib/app-utils.js +357 -239
  48. package/lib/commands/app-management.js +20 -9
  49. package/lib/commands/app-strings.js +6 -3
  50. package/lib/commands/file-movement.js +1 -1
  51. package/lib/commands/types.ts +1 -0
  52. package/lib/desired-caps.js +1 -0
  53. package/lib/driver.js +12 -15
  54. package/lib/execute-method-map.ts +1 -1
  55. package/lib/ios-fs-helpers.js +57 -23
  56. package/lib/real-device-management.js +7 -5
  57. package/lib/real-device.js +62 -88
  58. package/npm-shrinkwrap.json +13 -5
  59. package/package.json +1 -1
@@ -4,7 +4,11 @@ import {errors} from 'appium/driver';
4
4
  import {services} from 'appium-ios-device';
5
5
  import path from 'node:path';
6
6
  import B from 'bluebird';
7
- import { extractBundleId } from '../app-utils';
7
+ import {
8
+ SUPPORTED_EXTENSIONS,
9
+ onPostConfigureApp,
10
+ onDownloadApp,
11
+ } from '../app-utils';
8
12
 
9
13
  export default {
10
14
  /**
@@ -12,15 +16,20 @@ export default {
12
16
  *
13
17
  * 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.
14
18
  * @param {string} app - See docs for `appium:app` capability
15
- * @param {import('./types').AppInstallStrategy} [strategy] - One of possible app installation strategies on real devices. This argument is ignored on simulators. If not provided, then the value of `appium:appInstallStrategy` is used. If the latter is also not provided, then `serial` is used. See the description of `appium:appInstallStrategy` capability for more details on allowed values.
16
- * @param {number} [timeoutMs] - The maximum time to wait until app install is finished (in ms) on real devices. 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).
17
- * @param {boolean} [checkVersion] - If the application installation follows currently installed application's version status if provided. No checking occurs if no this option.
19
+ * @param {number} [timeoutMs] - The maximum time to wait until app install is finished (in ms) on real devices.
20
+ * 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).
21
+ * @param {boolean} [checkVersion] - If the application installation follows currently installed application's version status if provided.
22
+ * No checking occurs if no this option.
18
23
  * @privateRemarks Link to capability docs
19
24
  * @returns {Promise<void>}
20
25
  * @this {XCUITestDriver}
21
26
  */
22
- async mobileInstallApp(app, timeoutMs, strategy, checkVersion) {
23
- const srcAppPath = await this.helpers.configureApp(app, '.app');
27
+ async mobileInstallApp(app, timeoutMs, checkVersion) {
28
+ const srcAppPath = await this.helpers.configureApp(app, {
29
+ onPostProcess: onPostConfigureApp.bind(this),
30
+ onDownload: onDownloadApp.bind(this),
31
+ supportedExtensions: SUPPORTED_EXTENSIONS,
32
+ });
24
33
  this.log.info(
25
34
  `Installing '${srcAppPath}' to the ${this.isRealDevice() ? 'real device' : 'Simulator'} ` +
26
35
  `with UDID '${this.device.udid}'`,
@@ -31,8 +40,8 @@ export default {
31
40
  );
32
41
  }
33
42
 
43
+ const bundleId = await this.appInfosCache.extractBundleId(srcAppPath);
34
44
  if (checkVersion) {
35
- const bundleId = await extractBundleId(srcAppPath);
36
45
  const {install} = await this.checkAutInstallationState({
37
46
  enforceAppInstall: false,
38
47
  fullReset: false,
@@ -49,8 +58,10 @@ export default {
49
58
 
50
59
  await this.device.installApp(
51
60
  srcAppPath,
52
- timeoutMs ?? this.opts.appPushTimeout,
53
- strategy ?? this.opts.appInstallStrategy,
61
+ bundleId,
62
+ {
63
+ timeoutMs: timeoutMs ?? this.opts.appPushTimeout
64
+ },
54
65
  );
55
66
  this.log.info(`Installation of '${srcAppPath}' succeeded`);
56
67
  },
@@ -4,8 +4,11 @@ export default {
4
4
  /**
5
5
  * Return the language-specific strings for an app
6
6
  *
7
- * @param {string} language - The language abbreviation to fetch app strings mapping for. If no language is provided then strings for the 'en language would be returned
8
- * @param {string|null} [stringFile=null] - Relative path to the corresponding .strings file starting from the corresponding .lproj folder, e.g., `base/main.strings`. If omitted, then Appium will make its best guess where the file is.
7
+ * @param {string} language - The language abbreviation to fetch app strings mapping for.
8
+ * If no language is provided then strings for the 'en language would be returned
9
+ * @param {string|null} [stringFile=null] - Relative path to the corresponding .strings
10
+ * file starting from the corresponding .lproj folder, e.g., `base/main.strings`. If omitted,
11
+ * then Appium will make its best guess where the file is.
9
12
  *
10
13
  * @returns {Promise<import('@appium/types').StringRecord<string>>} A record of localized keys to localized text
11
14
  *
@@ -13,7 +16,7 @@ export default {
13
16
  */
14
17
  async getStrings(language, stringFile = null) {
15
18
  this.log.debug(`Gettings strings for language '${language}' and string file '${stringFile}'`);
16
- return await parseLocalizableStrings(
19
+ return await parseLocalizableStrings.bind(this)(
17
20
  Object.assign({}, this.opts, {
18
21
  language,
19
22
  stringFile,
@@ -160,7 +160,7 @@ async function pushFileToSimulator(device, remotePath, base64Data) {
160
160
  async function pushFileToRealDevice(device, remotePath, base64Data) {
161
161
  const {service, relativePath} = await createService(device.udid, remotePath);
162
162
  try {
163
- await pushFile(service, relativePath, base64Data);
163
+ await pushFile(service, Buffer.from(base64Data, 'base64'), relativePath);
164
164
  } catch (e) {
165
165
  log.debug(e.stack);
166
166
  throw new Error(`Could not push the file to '${remotePath}'. Original error: ${e.message}`);
@@ -219,6 +219,7 @@ export interface View {
219
219
  */
220
220
  export type SourceFormat = 'xml' | 'json' | 'description';
221
221
 
222
+ /** @deprecated */
222
223
  export type AppInstallStrategy = 'serial' | 'parallel' | 'ios-deploy';
223
224
 
224
225
  export interface ProfileManifest {
@@ -355,6 +355,7 @@ const desiredCapConstraints = /** @type {const} */ ({
355
355
  isBoolean: true,
356
356
  },
357
357
  appInstallStrategy: {
358
+ deprecated: true,
358
359
  isString: true,
359
360
  inclusionCaseInsensitive: ['serial', 'parallel', 'ios-deploy'],
360
361
  },
package/lib/driver.js CHANGED
@@ -14,8 +14,6 @@ import url from 'node:url';
14
14
  import {
15
15
  SUPPORTED_EXTENSIONS,
16
16
  SAFARI_BUNDLE_ID,
17
- extractBundleId,
18
- extractBundleVersion,
19
17
  onPostConfigureApp,
20
18
  onDownloadApp,
21
19
  verifyApplicationPlatform,
@@ -55,7 +53,6 @@ import {
55
53
  getAndCheckXcodeVersion,
56
54
  getDriverInfo,
57
55
  isLocalHost,
58
- isTvOs,
59
56
  markSystemFilesForCleanup,
60
57
  normalizeCommandTimeouts,
61
58
  normalizePlatformVersion,
@@ -63,6 +60,7 @@ import {
63
60
  removeAllSessionWebSocketHandlers,
64
61
  translateDeviceName,
65
62
  } from './utils';
63
+ import { AppInfosCache } from './app-infos-cache';
66
64
 
67
65
  const SHUTDOWN_OTHER_FEAT_NAME = 'shutdown_other_sims';
68
66
  const CUSTOMIZE_RESULT_BUNDLE_PATH = 'customize_result_bundle_path';
@@ -310,6 +308,7 @@ export class XCUITestDriver extends BaseDriver {
310
308
  }
311
309
  this.lifecycleData = {};
312
310
  this._audioRecorder = null;
311
+ this.appInfosCache = new AppInfosCache(this.log);
313
312
  }
314
313
 
315
314
  async onSettingsUpdate(key, value) {
@@ -548,7 +547,7 @@ export class XCUITestDriver extends BaseDriver {
548
547
  await checkAppPresent(this.opts.app);
549
548
 
550
549
  if (!this.opts.bundleId) {
551
- this.opts.bundleId = await extractBundleId(this.opts.app);
550
+ this.opts.bundleId = await this.appInfosCache.extractBundleId(this.opts.app);
552
551
  }
553
552
  }
554
553
 
@@ -1503,7 +1502,7 @@ export class XCUITestDriver extends BaseDriver {
1503
1502
  };
1504
1503
  }
1505
1504
 
1506
- const candidateBundleVersion = await extractBundleVersion(app);
1505
+ const candidateBundleVersion = await this.appInfosCache.extractBundleVersion(app);
1507
1506
  this.log.debug(`CFBundleVersion from Info.plist: ${candidateBundleVersion}`);
1508
1507
  if (!candidateBundleVersion) {
1509
1508
  return {
@@ -1560,10 +1559,7 @@ export class XCUITestDriver extends BaseDriver {
1560
1559
  return;
1561
1560
  }
1562
1561
 
1563
- await verifyApplicationPlatform(this.opts.app, {
1564
- isSimulator: this.isSimulator(),
1565
- isTvOS: isTvOs(this.opts.platformName),
1566
- });
1562
+ await verifyApplicationPlatform.bind(this)();
1567
1563
 
1568
1564
  const {install, skipUninstall} = await this.checkAutInstallationState();
1569
1565
  if (install) {
@@ -1571,7 +1567,6 @@ export class XCUITestDriver extends BaseDriver {
1571
1567
  await installToRealDevice.bind(this)(this.opts.app, this.opts.bundleId, {
1572
1568
  skipUninstall,
1573
1569
  timeout: this.opts.appPushTimeout,
1574
- strategy: this.opts.appInstallStrategy,
1575
1570
  });
1576
1571
  } else {
1577
1572
  await installToSimulator.bind(this)(this.opts.app, this.opts.bundleId, {
@@ -1607,9 +1602,13 @@ export class XCUITestDriver extends BaseDriver {
1607
1602
  }
1608
1603
 
1609
1604
  /** @type {string[]} */
1610
- const appPaths = await B.all(appsList.map((app) => this.helpers.configureApp(app, '.app')));
1605
+ const appPaths = await B.all(appsList.map((app) => this.helpers.configureApp(app, {
1606
+ onPostProcess: onPostConfigureApp.bind(this),
1607
+ onDownload: onDownloadApp.bind(this),
1608
+ supportedExtensions: SUPPORTED_EXTENSIONS,
1609
+ })));
1611
1610
  /** @type {string[]} */
1612
- const appIds = await B.all(appPaths.map((appPath) => extractBundleId(appPath)));
1611
+ const appIds = await B.all(appPaths.map((appPath) => this.appInfosCache.extractBundleId(appPath)));
1613
1612
  for (const [appId, appPath] of _.zip(appIds, appPaths)) {
1614
1613
  if (this.isRealDevice()) {
1615
1614
  await installToRealDevice.bind(this)(
@@ -1618,7 +1617,6 @@ export class XCUITestDriver extends BaseDriver {
1618
1617
  {
1619
1618
  skipUninstall: true, // to make the behavior as same as UIA2
1620
1619
  timeout: this.opts.appPushTimeout,
1621
- strategy: this.opts.appInstallStrategy,
1622
1620
  },
1623
1621
  );
1624
1622
  } else {
@@ -1689,7 +1687,7 @@ export class XCUITestDriver extends BaseDriver {
1689
1687
  return;
1690
1688
  }
1691
1689
 
1692
- const candidateBundleId = await extractBundleId(this.opts.prebuiltWDAPath);
1690
+ const candidateBundleId = await this.appInfosCache.extractBundleId(this.opts.prebuiltWDAPath);
1693
1691
  this.wda.updatedWDABundleId = candidateBundleId.replace('.xctrunner', '');
1694
1692
  this.log.info(
1695
1693
  `Installing prebuilt WDA at '${this.opts.prebuiltWDAPath}'. ` +
@@ -1705,7 +1703,6 @@ export class XCUITestDriver extends BaseDriver {
1705
1703
  {
1706
1704
  skipUninstall: true,
1707
1705
  timeout: this.opts.appPushTimeout,
1708
- strategy: this.opts.appInstallStrategy,
1709
1706
  },
1710
1707
  );
1711
1708
  } else {
@@ -150,7 +150,7 @@ export const executeMethodMap = {
150
150
  command: 'mobileInstallApp',
151
151
  params: {
152
152
  required: ['app'],
153
- optional: ['strategy', 'timeoutMs', 'checkVersion'],
153
+ optional: ['timeoutMs', 'checkVersion'],
154
154
  },
155
155
  },
156
156
  'mobile: isAppInstalled': {
@@ -4,7 +4,7 @@ import {fs, tempDir, mkdirp, zip, util, timing} from 'appium/support';
4
4
  import path from 'path';
5
5
  import log from './logger';
6
6
 
7
- const IO_TIMEOUT_MS = 4 * 60 * 1000;
7
+ export const IO_TIMEOUT_MS = 4 * 60 * 1000;
8
8
  // Mobile devices use NAND memory modules for the storage,
9
9
  // and the parallelism there is not as performant as on regular SSDs
10
10
  const MAX_IO_CHUNK_SIZE = 8;
@@ -17,7 +17,7 @@ const MAX_IO_CHUNK_SIZE = 8;
17
17
  * @param {string} remotePath Relative path to the file on the device
18
18
  * @returns {Promise<Buffer>} The file content as a buffer
19
19
  */
20
- async function pullFile(afcService, remotePath) {
20
+ export async function pullFile(afcService, remotePath) {
21
21
  const stream = await afcService.createReadStream(remotePath, {autoDestroy: true});
22
22
  const pullPromise = new B((resolve, reject) => {
23
23
  stream.on('close', resolve);
@@ -51,7 +51,7 @@ async function folderExists(folderPath) {
51
51
  * @param {string} remoteRootPath Relative path to the folder on the device
52
52
  * @returns {Promise<Buffer>} The folder content as a zipped base64-encoded buffer
53
53
  */
54
- async function pullFolder(afcService, remoteRootPath) {
54
+ export async function pullFolder(afcService, remoteRootPath) {
55
55
  const tmpFolder = await tempDir.openDir();
56
56
  try {
57
57
  let localTopItem = null;
@@ -145,35 +145,71 @@ async function remoteMkdirp(afcService, remoteRoot) {
145
145
  await afcService.createDirectory(remoteRoot);
146
146
  }
147
147
 
148
+ /**
149
+ * @typedef {Object} PushFileOptions
150
+ * @property {number} [timeoutMs=240000] The maximum count of milliceconds to wait until
151
+ * file push is completed. Cannot be lower than 60000ms
152
+ */
153
+
148
154
  /**
149
155
  * Pushes a file to a real device
150
156
  *
151
- * @param {any} afcService Apple File Client service instance from
157
+ * @param {any} afcService afcService Apple File Client service instance from
152
158
  * 'appium-ios-device' module
159
+ * @param {string|Buffer} localPathOrPayload Either full path to the source file
160
+ * or a buffer payload to be written into the remote destination
153
161
  * @param {string} remotePath Relative path to the file on the device. The remote
154
162
  * folder structure is created automatically if necessary.
155
- * @param {string} base64Data Base64-encoded content of the file to be written
163
+ * @param {PushFileOptions} [opts={}]
156
164
  */
157
- async function pushFile(afcService, remotePath, base64Data) {
165
+ export async function pushFile (afcService, localPathOrPayload, remotePath, opts = {}) {
166
+ const {
167
+ timeoutMs = IO_TIMEOUT_MS,
168
+ } = opts;
169
+ const timer = new timing.Timer().start();
158
170
  await remoteMkdirp(afcService, path.dirname(remotePath));
159
- const stream = await afcService.createWriteStream(remotePath, {autoDestroy: true});
171
+ const source = Buffer.isBuffer(localPathOrPayload)
172
+ ? localPathOrPayload
173
+ : fs.createReadStream(localPathOrPayload, {autoClose: true});
174
+ const writeStream = await afcService.createWriteStream(remotePath, {
175
+ autoDestroy: true,
176
+ });
177
+ writeStream.on('finish', writeStream.destroy);
160
178
  let pushError = null;
161
- const pushPromise = new B((resolve, reject) => {
162
- stream.on('error', (e) => {
163
- pushError = e;
164
- });
165
- stream.on('close', () => {
179
+ const filePushPromise = new B((resolve, reject) => {
180
+ writeStream.on('close', () => {
166
181
  if (pushError) {
167
182
  reject(pushError);
168
183
  } else {
169
184
  resolve();
170
185
  }
171
186
  });
172
- }).timeout(IO_TIMEOUT_MS);
173
- stream.write(Buffer.from(base64Data, 'base64'));
174
- stream.end();
175
- await pushPromise;
176
- }
187
+ const onStreamError = (e) => {
188
+ if (!Buffer.isBuffer(source)) {
189
+ source.unpipe(writeStream);
190
+ }
191
+ log.debug(e);
192
+ pushError = e;
193
+ };
194
+ writeStream.on('error', onStreamError);
195
+ if (!Buffer.isBuffer(source)) {
196
+ source.on('error', onStreamError);
197
+ }
198
+ });
199
+ if (Buffer.isBuffer(source)) {
200
+ writeStream.write(source);
201
+ } else {
202
+ source.pipe(writeStream);
203
+ }
204
+ await filePushPromise.timeout(Math.max(timeoutMs, 60000));
205
+ const fileSize = Buffer.isBuffer(localPathOrPayload)
206
+ ? localPathOrPayload.length
207
+ : (await fs.stat(localPathOrPayload)).size;
208
+ log.debug(
209
+ `Successfully pushed the file payload (${util.toReadableSizeString(fileSize)}) ` +
210
+ `to the remote location '${remotePath}' in ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`
211
+ );
212
+ };
177
213
 
178
214
  /**
179
215
  * @typedef {Object} PushFolderOptions
@@ -194,7 +230,7 @@ async function pushFile(afcService, remotePath, base64Data) {
194
230
  * will be deleted if already exists.
195
231
  * @param {PushFolderOptions} opts
196
232
  */
197
- async function pushFolder(afcService, srcRootPath, dstRootPath, opts = {}) {
233
+ export async function pushFolder(afcService, srcRootPath, dstRootPath, opts = {}) {
198
234
  const {timeoutMs = IO_TIMEOUT_MS, enableParallelPush = false} = opts;
199
235
 
200
236
  const timer = new timing.Timer().start();
@@ -239,7 +275,7 @@ async function pushFolder(afcService, srcRootPath, dstRootPath, opts = {}) {
239
275
  `(${util.pluralize('item', foldersToPush.length + 1, true)})`,
240
276
  );
241
277
 
242
- const pushFile = async (relativePath) => {
278
+ const _pushFile = async (/** @type {string} */ relativePath) => {
243
279
  const absoluteSourcePath = path.join(srcRootPath, relativePath);
244
280
  const readStream = fs.createReadStream(absoluteSourcePath, {autoClose: true});
245
281
  const absoluteDestinationPath = path.join(dstRootPath, relativePath);
@@ -272,7 +308,7 @@ async function pushFolder(afcService, srcRootPath, dstRootPath, opts = {}) {
272
308
  log.debug(`Proceeding to parallel files push (max ${MAX_IO_CHUNK_SIZE} writers)`);
273
309
  const pushPromises = [];
274
310
  for (const relativeFilePath of filesToPush) {
275
- pushPromises.push(B.resolve(pushFile(relativeFilePath)));
311
+ pushPromises.push(B.resolve(_pushFile(relativeFilePath)));
276
312
  // keep the push queue filled
277
313
  if (pushPromises.length >= MAX_IO_CHUNK_SIZE) {
278
314
  await B.any(pushPromises);
@@ -290,7 +326,7 @@ async function pushFolder(afcService, srcRootPath, dstRootPath, opts = {}) {
290
326
  } else {
291
327
  log.debug(`Proceeding to serial files push`);
292
328
  for (const relativeFilePath of filesToPush) {
293
- await pushFile(relativeFilePath);
329
+ await _pushFile(relativeFilePath);
294
330
  const elapsedMs = timer.getDuration().asMilliSeconds;
295
331
  if (elapsedMs > timeoutMs) {
296
332
  throw new B.TimeoutError(`Timed out after ${elapsedMs} ms`);
@@ -304,5 +340,3 @@ async function pushFolder(afcService, srcRootPath, dstRootPath, opts = {}) {
304
340
  `within ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`,
305
341
  );
306
342
  }
307
-
308
- export {pullFile, pullFolder, pushFile, pushFolder};
@@ -8,7 +8,6 @@ const DEFAULT_APP_INSTALLATION_TIMEOUT_MS = 8 * 60 * 1000;
8
8
  * @typedef {Object} InstallOptions
9
9
  *
10
10
  * @property {boolean} [skipUninstall] Whether to skip app uninstall before installing it
11
- * @property {'serial'|'parallel'|'ios-deploy'} [strategy='serial'] One of possible install strategies ('serial', 'parallel', 'ios-deploy')
12
11
  * @property {number} [timeout=480000] App install timeout
13
12
  * @property {boolean} [shouldEnforceUninstall] Whether to enforce the app uninstallation. e.g. fullReset, or enforceAppInstall is true
14
13
  */
@@ -29,7 +28,6 @@ export async function installToRealDevice(app, bundleId, opts = {}) {
29
28
 
30
29
  const {
31
30
  skipUninstall,
32
- strategy,
33
31
  timeout = DEFAULT_APP_INSTALLATION_TIMEOUT_MS,
34
32
  } = opts;
35
33
 
@@ -37,10 +35,12 @@ export async function installToRealDevice(app, bundleId, opts = {}) {
37
35
  this.log.info(`Reset requested. Removing app with id '${bundleId}' from the device`);
38
36
  await device.remove(bundleId);
39
37
  }
40
- this.log.debug(`Installing '${app}' on the device with UUID '${device.udid}'...`);
38
+ this.log.debug(`Installing '${app}' on the device with UUID '${device.udid}'`);
41
39
 
42
40
  try {
43
- await device.install(app, timeout, strategy);
41
+ await device.install(app, bundleId, {
42
+ timeoutMs: timeout,
43
+ });
44
44
  this.log.debug('The app has been installed successfully.');
45
45
  } catch (e) {
46
46
  // Want to clarify the device's application installation state in this situation.
@@ -61,7 +61,9 @@ export async function installToRealDevice(app, bundleId, opts = {}) {
61
61
  `be already cached on the device, probably with a different signature. ` +
62
62
  `Will try to remove it and install a new copy. Original error: ${e.message}`);
63
63
  await device.remove(bundleId);
64
- await device.install(app, timeout, strategy);
64
+ await device.install(app, bundleId, {
65
+ timeoutMs: timeout,
66
+ });
65
67
  this.log.debug('The app has been installed after one retrial.');
66
68
  }
67
69
  }
@@ -1,23 +1,16 @@
1
- import {fs, timing, util} from 'appium/support';
1
+ import {timing, util, fs} from 'appium/support';
2
2
  import path from 'path';
3
3
  import {services, utilities, INSTRUMENT_CHANNEL} from 'appium-ios-device';
4
4
  import B from 'bluebird';
5
5
  import defaultLogger from './logger';
6
6
  import _ from 'lodash';
7
- import {exec} from 'teen_process';
8
- import {extractBundleId, SAFARI_BUNDLE_ID} from './app-utils';
9
- import {pushFolder} from './ios-fs-helpers';
7
+ import {SAFARI_BUNDLE_ID} from './app-utils';
8
+ import {pushFile, pushFolder, IO_TIMEOUT_MS} from './ios-fs-helpers';
10
9
  import { Devicectl } from './devicectl';
11
10
 
12
11
  const APPLICATION_INSTALLED_NOTIFICATION = 'com.apple.mobile.application_installed';
13
- const INSTALLATION_STAGING_DIR = 'PublicStaging';
14
12
  const APPLICATION_NOTIFICATION_TIMEOUT_MS = 30 * 1000;
15
- const IOS_DEPLOY = 'ios-deploy';
16
- const APP_INSTALL_STRATEGY = Object.freeze({
17
- SERIAL: 'serial',
18
- PARALLEL: 'parallel',
19
- IOS_DEPLOY,
20
- });
13
+ const INSTALLATION_STAGING_DIR = 'PublicStaging';
21
14
 
22
15
  /**
23
16
  * @returns {Promise<string[]>}
@@ -26,6 +19,11 @@ export async function getConnectedDevices() {
26
19
  return await utilities.getConnectedDevices();
27
20
  }
28
21
 
22
+ /**
23
+ * @typedef {Object} InstallOptions
24
+ * @param {number} [timeoutMs=240000] Application installation timeout in milliseconds
25
+ */
26
+
29
27
  /**
30
28
  * @typedef {Object} InstallOrUpgradeOptions
31
29
  * @property {number} timeout Install/upgrade timeout in milliseconds
@@ -71,82 +69,52 @@ export class RealDevice {
71
69
 
72
70
  /**
73
71
  *
74
- * @param {string} app
75
- * @param {number} timeout
76
- * @param {'ios-deploy'|'serial'|'parallel'|null} strategy
77
- * @privateRemarks This really needs type guards built out
72
+ * @param {string} appPath
73
+ * @param {string} bundleId
74
+ * @param {InstallOptions} [opts={}]
78
75
  */
79
- async install(app, timeout, strategy = null) {
80
- if (
81
- strategy &&
82
- !_.values(APP_INSTALL_STRATEGY).includes(/** @type {any} */ (_.toLower(strategy)))
83
- ) {
84
- throw new Error(
85
- `App installation strategy '${strategy}' is unknown. ` +
86
- `Only the following strategies are supported: ${_.values(APP_INSTALL_STRATEGY)}`,
87
- );
88
- }
89
- this.log.debug(
90
- `Using '${strategy ?? APP_INSTALL_STRATEGY.SERIAL}' app deployment strategy. ` +
91
- `You could change it by providing another value to the 'appInstallStrategy' capability`,
92
- );
93
-
94
- const installWithIosDeploy = async () => {
95
- try {
96
- await fs.which(IOS_DEPLOY);
97
- } catch (err) {
98
- throw new Error(`'${IOS_DEPLOY}' utility has not been found in PATH. Is it installed?`);
99
- }
100
- try {
101
- await exec(IOS_DEPLOY, ['--id', this.udid, '--bundle', app], {timeout});
102
- } catch (err) {
103
- throw new Error(err.stderr || err.stdout || err.message);
104
- }
105
- };
106
-
76
+ async install(appPath, bundleId, opts = {}) {
77
+ const {
78
+ timeoutMs = IO_TIMEOUT_MS,
79
+ } = opts;
107
80
  const timer = new timing.Timer().start();
108
- if (_.toLower(/** @type {'ios-deploy'} */ (strategy)) === APP_INSTALL_STRATEGY.IOS_DEPLOY) {
109
- await installWithIosDeploy();
110
- } else {
111
- const afcService = await services.startAfcService(this.udid);
112
- const enableParallelPush = _.toLower(/** @type {'parallel'} */ (strategy)) === APP_INSTALL_STRATEGY.PARALLEL;
113
- try {
114
- const bundleId = await extractBundleId(app);
115
- const bundlePathOnPhone = path.join(INSTALLATION_STAGING_DIR, bundleId);
116
- await pushFolder(afcService, app, bundlePathOnPhone, {
117
- enableParallelPush,
118
- timeoutMs: timeout,
81
+ const afcService = await services.startAfcService(this.udid);
82
+ try {
83
+ let bundlePathOnPhone;
84
+ if ((await fs.stat(appPath)).isFile()) {
85
+ // https://github.com/doronz88/pymobiledevice3/blob/6ff5001f5776e03b610363254e82d7fbcad4ef5f/pymobiledevice3/services/installation_proxy.py#L75
86
+ bundlePathOnPhone = `/${path.basename(appPath)}`;
87
+ await pushFile(afcService, appPath, bundlePathOnPhone, {
88
+ timeoutMs,
119
89
  });
120
- await this.installOrUpgradeApplication(
121
- bundlePathOnPhone,
122
- {
123
- timeout: Math.max(timeout - timer.getDuration().asMilliSeconds, 60000),
124
- isUpgrade: await this.isAppInstalled(bundleId),
125
- }
126
- );
127
- } catch (err) {
128
- this.log.warn(`Error installing app '${app}': ${err.message}`);
129
- if (err instanceof B.TimeoutError) {
130
- this.log.info(
131
- `Consider increasing the value of 'appPushTimeout' capability (the current value equals to ${timeout}ms)`
132
- );
133
- if (!enableParallelPush) {
134
- this.log.info(`Consider setting the value of 'appInstallStrategy' capability to 'parallel'`);
135
- }
136
- }
137
- this.log.warn(`Falling back to '${IOS_DEPLOY}' usage`);
138
- try {
139
- await installWithIosDeploy();
140
- } catch (err1) {
141
- throw new Error(
142
- `Could not install '${app}':\n` + ` - ${err.message}\n` + ` - ${err1.message}`,
143
- );
90
+ } else {
91
+ bundlePathOnPhone = `${INSTALLATION_STAGING_DIR}/${bundleId}`;
92
+ await pushFolder(afcService, appPath, bundlePathOnPhone, {
93
+ enableParallelPush: true,
94
+ timeoutMs,
95
+ });
96
+ }
97
+ await this.installOrUpgradeApplication(
98
+ bundlePathOnPhone,
99
+ {
100
+ timeout: Math.max(timeoutMs - timer.getDuration().asMilliSeconds, 60000),
101
+ isUpgrade: await this.isAppInstalled(bundleId),
144
102
  }
145
- } finally {
146
- afcService.close();
103
+ );
104
+ } catch (err) {
105
+ this.log.debug(err.stack);
106
+ let errMessage = `Cannot install the ${bundleId} application`;
107
+ if (err instanceof B.TimeoutError) {
108
+ errMessage += `. Consider increasing the value of 'appPushTimeout' capability (the current value equals to ${timeoutMs}ms)`;
147
109
  }
110
+ errMessage += `. Original error: ${err.message}`;
111
+ throw new Error(errMessage);
112
+ } finally {
113
+ afcService.close();
148
114
  }
149
- this.log.info(`App installation succeeded after ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`);
115
+ this.log.info(
116
+ `The installation of '${bundleId}' succeeded after ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`
117
+ );
150
118
  }
151
119
 
152
120
  /**
@@ -164,10 +132,16 @@ export class RealDevice {
164
132
  const clientOptions = {PackageType: 'Developer'};
165
133
  try {
166
134
  if (isUpgrade) {
167
- this.log.debug(`An upgrade of the existing application is going to be performed. Will timeout in ${timeout} ms`);
135
+ this.log.debug(
136
+ `An upgrade of the existing application is going to be performed. ` +
137
+ `Will timeout in ${timeout.toFixed(0)} ms`
138
+ );
168
139
  await installationService.upgradeApplication(bundlePathOnPhone, clientOptions, timeout);
169
140
  } else {
170
- this.log.debug(`A new application installation is going to be performed. Will timeout in ${timeout} ms`);
141
+ this.log.debug(
142
+ `A new application installation is going to be performed. ` +
143
+ `Will timeout in ${timeout.toFixed(0)} ms`
144
+ );
171
145
  await installationService.installApplication(bundlePathOnPhone, clientOptions, timeout);
172
146
  }
173
147
  try {
@@ -187,12 +161,12 @@ export class RealDevice {
187
161
 
188
162
  /**
189
163
  * Alias for {@linkcode install}
190
- * @param {string} app
191
- * @param {number} timeout
192
- * @param {'ios-deploy'|'serial'|'parallel'|null} strategy
164
+ * @param {string} appPath
165
+ * @param {string} bundleId
166
+ * @param {InstallOptions} [opts={}]
193
167
  */
194
- async installApp(app, timeout, strategy) {
195
- return await this.install(app, timeout, strategy);
168
+ async installApp(appPath, bundleId, opts = {}) {
169
+ return await this.install(appPath, bundleId, opts);
196
170
  }
197
171
 
198
172
  /**