appium-xcuitest-driver 7.13.0 → 7.15.0

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 +60 -51
  7. package/build/lib/app-utils.d.ts.map +1 -1
  8. package/build/lib/app-utils.js +496 -182
  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 +35 -40
  28. package/build/lib/driver.d.ts.map +1 -1
  29. package/build/lib/driver.js +15 -99
  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 +529 -193
  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 +17 -120
  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 +40 -32
  59. package/package.json +2 -2
@@ -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
@@ -12,15 +12,10 @@ import EventEmitter from 'node:events';
12
12
  import path from 'node:path';
13
13
  import url from 'node:url';
14
14
  import {
15
- APP_EXT,
16
- IPA_EXT,
15
+ SUPPORTED_EXTENSIONS,
17
16
  SAFARI_BUNDLE_ID,
18
- extractBundleId,
19
- extractBundleVersion,
20
- fetchSupportedAppPlatforms,
21
- findApps,
22
- isAppBundle,
23
- isolateAppBundle,
17
+ onPostConfigureApp,
18
+ onDownloadApp,
24
19
  verifyApplicationPlatform,
25
20
  } from './app-utils';
26
21
  import commands from './commands';
@@ -58,7 +53,6 @@ import {
58
53
  getAndCheckXcodeVersion,
59
54
  getDriverInfo,
60
55
  isLocalHost,
61
- isTvOs,
62
56
  markSystemFilesForCleanup,
63
57
  normalizeCommandTimeouts,
64
58
  normalizePlatformVersion,
@@ -66,12 +60,11 @@ import {
66
60
  removeAllSessionWebSocketHandlers,
67
61
  translateDeviceName,
68
62
  } from './utils';
63
+ import { AppInfosCache } from './app-infos-cache';
69
64
 
70
65
  const SHUTDOWN_OTHER_FEAT_NAME = 'shutdown_other_sims';
71
66
  const CUSTOMIZE_RESULT_BUNDLE_PATH = 'customize_result_bundle_path';
72
67
 
73
- const SUPPORTED_EXTENSIONS = [IPA_EXT, APP_EXT];
74
- const MAX_ARCHIVE_SCAN_DEPTH = 1;
75
68
  const defaultServerCaps = {
76
69
  webStorageEnabled: false,
77
70
  locationContextEnabled: false,
@@ -315,6 +308,7 @@ export class XCUITestDriver extends BaseDriver {
315
308
  }
316
309
  this.lifecycleData = {};
317
310
  this._audioRecorder = null;
311
+ this.appInfosCache = new AppInfosCache(this.log);
318
312
  }
319
313
 
320
314
  async onSettingsUpdate(key, value) {
@@ -553,7 +547,7 @@ export class XCUITestDriver extends BaseDriver {
553
547
  await checkAppPresent(this.opts.app);
554
548
 
555
549
  if (!this.opts.bundleId) {
556
- this.opts.bundleId = await extractBundleId(this.opts.app);
550
+ this.opts.bundleId = await this.appInfosCache.extractBundleId(this.opts.app);
557
551
  }
558
552
  }
559
553
 
@@ -1065,107 +1059,12 @@ export class XCUITestDriver extends BaseDriver {
1065
1059
  }
1066
1060
 
1067
1061
  this.opts.app = await this.helpers.configureApp(this.opts.app, {
1068
- onPostProcess: this.onPostConfigureApp.bind(this),
1062
+ onPostProcess: onPostConfigureApp.bind(this),
1063
+ onDownload: onDownloadApp.bind(this),
1069
1064
  supportedExtensions: SUPPORTED_EXTENSIONS,
1070
1065
  });
1071
1066
  }
1072
1067
 
1073
- /**
1074
- * Unzip the given archive and find a matching .app bundle in it
1075
- *
1076
- * @param {string} appPath The path to the archive.
1077
- * @param {number} depth [0] the current nesting depth. App bundles whose nesting level
1078
- * is greater than 1 are not supported.
1079
- * @returns {Promise<string>} Full path to the first matching .app bundle..
1080
- * @throws If no matching .app bundles were found in the provided archive.
1081
- */
1082
- async unzipApp(appPath, depth = 0) {
1083
- if (depth > MAX_ARCHIVE_SCAN_DEPTH) {
1084
- throw new Error('Nesting of package bundles is not supported');
1085
- }
1086
- const [rootDir, matchedPaths] = await findApps(appPath, SUPPORTED_EXTENSIONS);
1087
- if (_.isEmpty(matchedPaths)) {
1088
- this.log.debug(`'${path.basename(appPath)}' has no bundles`);
1089
- } else {
1090
- this.log.debug(
1091
- `Found ${util.pluralize('bundle', matchedPaths.length, true)} in ` +
1092
- `'${path.basename(appPath)}': ${matchedPaths}`,
1093
- );
1094
- }
1095
- try {
1096
- for (const matchedPath of matchedPaths) {
1097
- const fullPath = path.join(rootDir, matchedPath);
1098
- if (await isAppBundle(fullPath)) {
1099
- const supportedPlatforms = await fetchSupportedAppPlatforms(fullPath);
1100
- if (this.isSimulator() && !supportedPlatforms.some((p) => _.includes(p, 'Simulator'))) {
1101
- this.log.info(
1102
- `'${matchedPath}' does not have Simulator devices in the list of supported platforms ` +
1103
- `(${supportedPlatforms.join(',')}). Skipping it`,
1104
- );
1105
- continue;
1106
- }
1107
- if (this.isRealDevice() && !supportedPlatforms.some((p) => _.includes(p, 'OS'))) {
1108
- this.log.info(
1109
- `'${matchedPath}' does not have real devices in the list of supported platforms ` +
1110
- `(${supportedPlatforms.join(',')}). Skipping it`,
1111
- );
1112
- continue;
1113
- }
1114
- this.log.info(
1115
- `'${matchedPath}' is the resulting application bundle selected from '${appPath}'`,
1116
- );
1117
- return await isolateAppBundle(fullPath);
1118
- } else if (_.endsWith(_.toLower(fullPath), IPA_EXT) && (await fs.stat(fullPath)).isFile()) {
1119
- try {
1120
- return await this.unzipApp(fullPath, depth + 1);
1121
- } catch (e) {
1122
- this.log.warn(`Skipping processing of '${matchedPath}': ${e.message}`);
1123
- }
1124
- }
1125
- }
1126
- } finally {
1127
- await fs.rimraf(rootDir);
1128
- }
1129
- throw new Error(
1130
- `${this.opts.app} did not have any matching ${APP_EXT} or ${IPA_EXT} ` +
1131
- `bundles. Please make sure the provided package is valid and contains at least one matching ` +
1132
- `application bundle which is not nested.`,
1133
- );
1134
- }
1135
-
1136
- async onPostConfigureApp({cachedAppInfo, isUrl, appPath}) {
1137
- // Pick the previously cached entry if its integrity has been preserved
1138
- if (
1139
- _.isPlainObject(cachedAppInfo) &&
1140
- (await fs.stat(appPath)).isFile() &&
1141
- (await fs.hash(appPath)) === cachedAppInfo.packageHash &&
1142
- (await fs.exists(cachedAppInfo.fullPath)) &&
1143
- (
1144
- await fs.glob('**/*', {
1145
- cwd: cachedAppInfo.fullPath,
1146
- })
1147
- ).length === cachedAppInfo.integrity.folder
1148
- ) {
1149
- this.log.info(`Using '${cachedAppInfo.fullPath}' which was cached from '${appPath}'`);
1150
- return {appPath: cachedAppInfo.fullPath};
1151
- }
1152
-
1153
- // Only local .app bundles that are available in-place should not be cached
1154
- if (await isAppBundle(appPath)) {
1155
- return false;
1156
- }
1157
-
1158
- // Extract the app bundle and cache it
1159
- try {
1160
- return {appPath: await this.unzipApp(appPath)};
1161
- } finally {
1162
- // Cleanup previously downloaded archive
1163
- if (isUrl) {
1164
- await fs.rimraf(appPath);
1165
- }
1166
- }
1167
- }
1168
-
1169
1068
  async determineDevice() {
1170
1069
  // in the one case where we create a sim, we will set this state
1171
1070
  this.lifecycleData.createSim = false;
@@ -1603,7 +1502,7 @@ export class XCUITestDriver extends BaseDriver {
1603
1502
  };
1604
1503
  }
1605
1504
 
1606
- const candidateBundleVersion = await extractBundleVersion(app);
1505
+ const candidateBundleVersion = await this.appInfosCache.extractBundleVersion(app);
1607
1506
  this.log.debug(`CFBundleVersion from Info.plist: ${candidateBundleVersion}`);
1608
1507
  if (!candidateBundleVersion) {
1609
1508
  return {
@@ -1660,10 +1559,7 @@ export class XCUITestDriver extends BaseDriver {
1660
1559
  return;
1661
1560
  }
1662
1561
 
1663
- await verifyApplicationPlatform(this.opts.app, {
1664
- isSimulator: this.isSimulator(),
1665
- isTvOS: isTvOs(this.opts.platformName),
1666
- });
1562
+ await verifyApplicationPlatform.bind(this)();
1667
1563
 
1668
1564
  const {install, skipUninstall} = await this.checkAutInstallationState();
1669
1565
  if (install) {
@@ -1671,7 +1567,6 @@ export class XCUITestDriver extends BaseDriver {
1671
1567
  await installToRealDevice.bind(this)(this.opts.app, this.opts.bundleId, {
1672
1568
  skipUninstall,
1673
1569
  timeout: this.opts.appPushTimeout,
1674
- strategy: this.opts.appInstallStrategy,
1675
1570
  });
1676
1571
  } else {
1677
1572
  await installToSimulator.bind(this)(this.opts.app, this.opts.bundleId, {
@@ -1707,9 +1602,13 @@ export class XCUITestDriver extends BaseDriver {
1707
1602
  }
1708
1603
 
1709
1604
  /** @type {string[]} */
1710
- 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
+ })));
1711
1610
  /** @type {string[]} */
1712
- const appIds = await B.all(appPaths.map((appPath) => extractBundleId(appPath)));
1611
+ const appIds = await B.all(appPaths.map((appPath) => this.appInfosCache.extractBundleId(appPath)));
1713
1612
  for (const [appId, appPath] of _.zip(appIds, appPaths)) {
1714
1613
  if (this.isRealDevice()) {
1715
1614
  await installToRealDevice.bind(this)(
@@ -1718,7 +1617,6 @@ export class XCUITestDriver extends BaseDriver {
1718
1617
  {
1719
1618
  skipUninstall: true, // to make the behavior as same as UIA2
1720
1619
  timeout: this.opts.appPushTimeout,
1721
- strategy: this.opts.appInstallStrategy,
1722
1620
  },
1723
1621
  );
1724
1622
  } else {
@@ -1789,7 +1687,7 @@ export class XCUITestDriver extends BaseDriver {
1789
1687
  return;
1790
1688
  }
1791
1689
 
1792
- const candidateBundleId = await extractBundleId(this.opts.prebuiltWDAPath);
1690
+ const candidateBundleId = await this.appInfosCache.extractBundleId(this.opts.prebuiltWDAPath);
1793
1691
  this.wda.updatedWDABundleId = candidateBundleId.replace('.xctrunner', '');
1794
1692
  this.log.info(
1795
1693
  `Installing prebuilt WDA at '${this.opts.prebuiltWDAPath}'. ` +
@@ -1805,7 +1703,6 @@ export class XCUITestDriver extends BaseDriver {
1805
1703
  {
1806
1704
  skipUninstall: true,
1807
1705
  timeout: this.opts.appPushTimeout,
1808
- strategy: this.opts.appInstallStrategy,
1809
1706
  },
1810
1707
  );
1811
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
  }