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
@@ -3,24 +3,23 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.onPostConfigureApp = exports.onDownloadApp = exports.buildSafariPreferences = exports.isolateAppBundle = exports.unzipStream = exports.unzipFile = exports.isAppBundle = exports.parseLocalizableStrings = exports.verifyApplicationPlatform = exports.fetchSupportedAppPlatforms = exports.extractBundleVersion = exports.extractBundleId = exports.SUPPORTED_EXTENSIONS = exports.IPA_EXT = exports.APP_EXT = exports.SAFARI_BUNDLE_ID = void 0;
6
+ exports.onPostConfigureApp = exports.onDownloadApp = exports.buildSafariPreferences = exports.unzipStream = exports.unzipFile = exports.parseLocalizableStrings = exports.verifyApplicationPlatform = exports.SUPPORTED_EXTENSIONS = exports.IPA_EXT = exports.APP_EXT = exports.SAFARI_BUNDLE_ID = void 0;
7
7
  const lodash_1 = __importDefault(require("lodash"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const support_1 = require("appium/support");
10
10
  const logger_js_1 = __importDefault(require("./logger.js"));
11
- const lru_cache_1 = require("lru-cache");
12
11
  const node_os_1 = __importDefault(require("node:os"));
13
12
  const teen_process_1 = require("teen_process");
14
13
  const bluebird_1 = __importDefault(require("bluebird"));
15
14
  const node_child_process_1 = require("node:child_process");
16
15
  const node_assert_1 = __importDefault(require("node:assert"));
16
+ const utils_js_1 = require("./utils.js");
17
17
  const STRINGSDICT_RESOURCE = '.stringsdict';
18
18
  const STRINGS_RESOURCE = '.strings';
19
19
  exports.SAFARI_BUNDLE_ID = 'com.apple.mobilesafari';
20
20
  exports.APP_EXT = '.app';
21
21
  exports.IPA_EXT = '.ipa';
22
- /** @type {LRUCache<string, import('@appium/types').StringRecord>} */
23
- const PLIST_CACHE = new lru_cache_1.LRUCache({ max: 20 });
22
+ const ZIP_EXT = '.zip';
24
23
  const SAFARI_OPTS_ALIASES_MAP = /** @type {const} */ ({
25
24
  safariAllowPopups: [
26
25
  ['WebKitJavaScriptCanOpenWindowsAutomatically', 'JavaScriptCanOpenWindowsAutomatically'],
@@ -32,129 +31,53 @@ const SAFARI_OPTS_ALIASES_MAP = /** @type {const} */ ({
32
31
  const MAX_ARCHIVE_SCAN_DEPTH = 1;
33
32
  exports.SUPPORTED_EXTENSIONS = [exports.IPA_EXT, exports.APP_EXT];
34
33
  const MACOS_RESOURCE_FOLDER = '__MACOSX';
35
- /**
36
- * Retrieves the value of the given entry name from the application's Info.plist.
37
- *
38
- * @this {Object} Optinal instance used for caching. Ususally the driver instance.
39
- * @param {string} app Full path to the app bundle root.
40
- * @param {string} entryName Key name in the plist.
41
- * @returns {Promise<any | undefined>} Either the extracted value or undefined if no such key has been found in the plist.
42
- * @throws {Error} If the application's Info.plist cannot be parsed.
43
- */
44
- async function extractPlistEntry(app, entryName) {
45
- const plistPath = path_1.default.resolve(app, 'Info.plist');
46
- const parseFile = async () => {
47
- try {
48
- return await support_1.plist.parsePlistFile(plistPath);
49
- }
50
- catch (err) {
51
- throw new Error(`Could not extract Info.plist from '${path_1.default.basename(app)}': ${err.message}`);
52
- }
53
- };
54
- let plistObj = PLIST_CACHE.get(app);
55
- if (!plistObj) {
56
- plistObj = await parseFile();
57
- PLIST_CACHE.set(app, plistObj);
58
- }
59
- return /** @type {import('@appium/types').StringRecord} */ (plistObj)[entryName];
60
- }
61
- /**
62
- *
63
- * @param {string} app
64
- * @returns {Promise<string>}
65
- */
66
- async function extractBundleId(app) {
67
- const bundleId = await extractPlistEntry(app, 'CFBundleIdentifier');
68
- logger_js_1.default.debug(`Getting bundle ID from app '${app}': '${bundleId}'`);
69
- return bundleId;
70
- }
71
- exports.extractBundleId = extractBundleId;
72
- /**
73
- *
74
- * @param {string} app
75
- * @returns {Promise<string>}
76
- */
77
- async function extractBundleVersion(app) {
78
- return await extractPlistEntry(app, 'CFBundleVersion');
79
- }
80
- exports.extractBundleVersion = extractBundleVersion;
81
- /**
82
- *
83
- * @param {string} app
84
- * @returns {Promise<string>}
85
- */
86
- async function extractExecutableName(app) {
87
- return await extractPlistEntry(app, 'CFBundleExecutable');
88
- }
89
- /**
90
- *
91
- * @param {string} app
92
- * @returns {Promise<string[]>}
93
- */
94
- async function fetchSupportedAppPlatforms(app) {
95
- try {
96
- const result = await extractPlistEntry(app, 'CFBundleSupportedPlatforms');
97
- if (!lodash_1.default.isArray(result)) {
98
- logger_js_1.default.warn(`${path_1.default.basename(app)}': CFBundleSupportedPlatforms is not a valid list`);
99
- return [];
100
- }
101
- return result;
102
- }
103
- catch (err) {
104
- logger_js_1.default.warn(`Cannot extract the list of supported platforms from '${path_1.default.basename(app)}': ${err.message}`);
105
- return [];
106
- }
107
- }
108
- exports.fetchSupportedAppPlatforms = fetchSupportedAppPlatforms;
109
- /**
110
- * @typedef {Object} PlatformOpts
111
- *
112
- * @property {boolean} isSimulator - Whether the destination platform is a Simulator
113
- * @property {boolean} isTvOS - Whether the destination platform is a Simulator
114
- */
34
+ const SANITIZE_REPLACEMENT = '-';
115
35
  /**
116
36
  * Verify whether the given application is compatible to the
117
37
  * platform where it is going to be installed and tested.
118
38
  *
119
- * @param {string} app - The actual path to the application bundle
120
- * @param {PlatformOpts} expectedPlatform
39
+ * @this {XCUITestDriver}
40
+ * @returns {Promise<void>}
121
41
  * @throws {Error} If bundle architecture does not match the expected device architecture.
122
42
  */
123
- async function verifyApplicationPlatform(app, expectedPlatform) {
124
- logger_js_1.default.debug('Verifying application platform');
125
- const supportedPlatforms = await fetchSupportedAppPlatforms(app);
126
- logger_js_1.default.debug(`CFBundleSupportedPlatforms: ${JSON.stringify(supportedPlatforms)}`);
127
- const { isSimulator, isTvOS } = expectedPlatform;
43
+ async function verifyApplicationPlatform() {
44
+ this.log.debug('Verifying application platform');
45
+ const supportedPlatforms = await this.appInfosCache.extractAppPlatforms(this.opts.app);
46
+ const isTvOS = (0, utils_js_1.isTvOs)(this.opts.platformName);
128
47
  const prefix = isTvOS ? 'AppleTV' : 'iPhone';
129
- const suffix = isSimulator ? 'Simulator' : 'OS';
48
+ const suffix = this.isSimulator() ? 'Simulator' : 'OS';
130
49
  const dstPlatform = `${prefix}${suffix}`;
131
- const appFileName = path_1.default.basename(app);
132
50
  if (!supportedPlatforms.includes(dstPlatform)) {
133
- throw new Error(`${isSimulator ? 'Simulator' : 'Real device'} architecture is not supported by the '${appFileName}' application. ` +
51
+ throw new Error(`${this.isSimulator() ? 'Simulator' : 'Real device'} architecture is not supported by the ${this.opts.bundleId} application. ` +
134
52
  `Make sure the correct deployment target has been selected for its compilation in Xcode.`);
135
53
  }
136
- if (isSimulator) {
137
- const executablePath = path_1.default.resolve(app, await extractExecutableName(app));
138
- const [resFile, resUname] = await bluebird_1.default.all([
139
- (0, teen_process_1.exec)('file', [executablePath]),
140
- (0, teen_process_1.exec)('uname', ['-m']),
141
- ]);
142
- const bundleExecutableInfo = lodash_1.default.trim(resFile.stdout);
143
- logger_js_1.default.debug(bundleExecutableInfo);
144
- const arch = lodash_1.default.trim(resUname.stdout);
145
- const isAppleSilicon = node_os_1.default.cpus()[0].model.includes('Apple');
146
- // We cannot run Simulator builds compiled for arm64 on Intel machines
147
- // Rosetta allows only to run Intel ones on arm64
148
- if (!lodash_1.default.includes(bundleExecutableInfo, `executable ${arch}`) &&
149
- !(isAppleSilicon && lodash_1.default.includes(bundleExecutableInfo, 'executable x86_64'))) {
150
- const bundleId = await extractBundleId(app);
151
- throw new Error(`The ${bundleId} application does not support the ${arch} Simulator ` +
152
- `architecture:\n${bundleExecutableInfo}\n\n` +
153
- `Please rebuild your application to support the ${arch} platform.`);
154
- }
54
+ if (this.isRealDevice()) {
55
+ return;
56
+ }
57
+ const executablePath = path_1.default.resolve(this.opts.app, await this.appInfosCache.extractExecutableName(this.opts.app));
58
+ const [resFile, resUname] = await bluebird_1.default.all([
59
+ (0, teen_process_1.exec)('file', [executablePath]),
60
+ (0, teen_process_1.exec)('uname', ['-m']),
61
+ ]);
62
+ const bundleExecutableInfo = lodash_1.default.trim(resFile.stdout);
63
+ this.log.debug(bundleExecutableInfo);
64
+ const arch = lodash_1.default.trim(resUname.stdout);
65
+ const isAppleSilicon = node_os_1.default.cpus()[0].model.includes('Apple');
66
+ // We cannot run Simulator builds compiled for arm64 on Intel machines
67
+ // Rosetta allows only to run Intel ones on arm64
68
+ if (!lodash_1.default.includes(bundleExecutableInfo, `executable ${arch}`) &&
69
+ !(isAppleSilicon && lodash_1.default.includes(bundleExecutableInfo, 'executable x86_64'))) {
70
+ throw new Error(`The ${this.opts.bundleId} application does not support the ${arch} Simulator ` +
71
+ `architecture:\n${bundleExecutableInfo}\n\n` +
72
+ `Please rebuild your application to support the ${arch} platform.`);
155
73
  }
156
74
  }
157
75
  exports.verifyApplicationPlatform = verifyApplicationPlatform;
76
+ /**
77
+ *
78
+ * @param {string} resourcePath
79
+ * @returns {Promise<import('@appium/types').StringRecord>}
80
+ */
158
81
  async function readResource(resourcePath) {
159
82
  const data = await support_1.plist.parsePlistFile(resourcePath);
160
83
  const result = {};
@@ -163,74 +86,108 @@ async function readResource(resourcePath) {
163
86
  }
164
87
  return result;
165
88
  }
166
- async function parseLocalizableStrings(opts) {
89
+ /**
90
+ * @typedef {Object} LocalizableStringsOptions
91
+ * @property {string} [app]
92
+ * @property {string} [language='en']
93
+ * @property {string} [localizableStringsDir]
94
+ * @property {string} [stringFile]
95
+ * @property {boolean} [strictMode]
96
+ */
97
+ /**
98
+ * Extracts string resources from an app
99
+ *
100
+ * @this {XCUITestDriver}
101
+ * @param {LocalizableStringsOptions} opts
102
+ * @returns {Promise<import('@appium/types').StringRecord>}
103
+ */
104
+ async function parseLocalizableStrings(opts = {}) {
167
105
  const { app, language = 'en', localizableStringsDir, stringFile, strictMode } = opts;
168
106
  if (!app) {
169
107
  const message = `Strings extraction is not supported if 'app' capability is not set`;
170
108
  if (strictMode) {
171
109
  throw new Error(message);
172
110
  }
173
- logger_js_1.default.info(message);
111
+ this.log.info(message);
174
112
  return {};
175
113
  }
176
- let lprojRoot;
177
- for (const subfolder of [`${language}.lproj`, localizableStringsDir, '']) {
178
- lprojRoot = path_1.default.resolve(app, subfolder);
179
- if (await support_1.fs.exists(lprojRoot)) {
180
- break;
181
- }
182
- const message = `No '${lprojRoot}' resources folder has been found`;
183
- if (strictMode) {
184
- throw new Error(message);
185
- }
186
- logger_js_1.default.debug(message);
187
- }
188
- logger_js_1.default.info(`Will extract resource strings from '${lprojRoot}'`);
189
- const resourcePaths = [];
190
- if (stringFile) {
191
- const dstPath = path_1.default.resolve(String(lprojRoot), stringFile);
192
- if (await support_1.fs.exists(dstPath)) {
193
- resourcePaths.push(dstPath);
114
+ let bundleRoot = app;
115
+ const isArchive = (await support_1.fs.stat(app)).isFile();
116
+ let tmpRoot;
117
+ try {
118
+ if (isArchive) {
119
+ tmpRoot = await support_1.tempDir.openDir();
120
+ this.log.info(`Extracting '${app}' into a temporary location to parse its resources`);
121
+ await support_1.zip.extractAllTo(app, tmpRoot);
122
+ const relativeBundleRoot = /** @type {string} */ (lodash_1.default.first(await findApps(tmpRoot, [exports.APP_EXT])));
123
+ this.log.info(`Selecting '${relativeBundleRoot}'`);
124
+ bundleRoot = path_1.default.join(tmpRoot, relativeBundleRoot);
194
125
  }
195
- else {
196
- const message = `No '${dstPath}' resource file has been found for '${app}'`;
126
+ /** @type {string|undefined} */
127
+ let lprojRoot;
128
+ for (const subfolder of [`${language}.lproj`, localizableStringsDir, ''].filter(lodash_1.default.isString)) {
129
+ lprojRoot = path_1.default.resolve(bundleRoot, /** @type {string} */ (subfolder));
130
+ if (await support_1.fs.exists(lprojRoot)) {
131
+ break;
132
+ }
133
+ const message = `No '${lprojRoot}' resources folder has been found`;
197
134
  if (strictMode) {
198
135
  throw new Error(message);
199
136
  }
200
- logger_js_1.default.info(message);
201
- logger_js_1.default.info(`Getting all the available strings from '${lprojRoot}'`);
137
+ this.log.debug(message);
202
138
  }
203
- }
204
- if (lodash_1.default.isEmpty(resourcePaths) && (await support_1.fs.exists(String(lprojRoot)))) {
205
- const resourceFiles = (await support_1.fs.readdir(String(lprojRoot)))
206
- .filter((name) => lodash_1.default.some([STRINGS_RESOURCE, STRINGSDICT_RESOURCE], (x) => name.endsWith(x)))
207
- .map((name) => path_1.default.resolve(lprojRoot, name));
208
- resourcePaths.push(...resourceFiles);
209
- }
210
- logger_js_1.default.info(`Got ${resourcePaths.length} resource file(s) in '${lprojRoot}'`);
211
- if (lodash_1.default.isEmpty(resourcePaths)) {
212
- return {};
213
- }
214
- const resultStrings = {};
215
- const toAbsolutePath = function (p) {
216
- return path_1.default.isAbsolute(p) ? p : path_1.default.resolve(process.cwd(), p);
217
- };
218
- for (const resourcePath of resourcePaths) {
219
- if (!support_1.util.isSubPath(toAbsolutePath(resourcePath), toAbsolutePath(app))) {
220
- // security precaution
221
- throw new Error(`'${resourcePath}' is expected to be located under '${app}'`);
139
+ if (!lprojRoot) {
140
+ return {};
222
141
  }
223
- try {
224
- const data = await readResource(resourcePath);
225
- logger_js_1.default.debug(`Parsed ${lodash_1.default.keys(data).length} string(s) from '${resourcePath}'`);
226
- lodash_1.default.merge(resultStrings, data);
142
+ this.log.info(`Retrieving resource strings from '${lprojRoot}'`);
143
+ const resourcePaths = [];
144
+ if (stringFile) {
145
+ const dstPath = path_1.default.resolve(/** @type {string} */ (lprojRoot), stringFile);
146
+ if (await support_1.fs.exists(dstPath)) {
147
+ resourcePaths.push(dstPath);
148
+ }
149
+ else {
150
+ const message = `No '${dstPath}' resource file has been found for '${app}'`;
151
+ if (strictMode) {
152
+ throw new Error(message);
153
+ }
154
+ this.log.info(message);
155
+ }
227
156
  }
228
- catch (e) {
229
- logger_js_1.default.warn(`Cannot parse '${resourcePath}' resource. Original error: ${e.message}`);
157
+ if (lodash_1.default.isEmpty(resourcePaths) && (await support_1.fs.exists(lprojRoot))) {
158
+ const resourceFiles = (await support_1.fs.readdir(lprojRoot))
159
+ .filter((name) => lodash_1.default.some([STRINGS_RESOURCE, STRINGSDICT_RESOURCE], (x) => name.endsWith(x)))
160
+ .map((name) => path_1.default.resolve(lprojRoot, name));
161
+ resourcePaths.push(...resourceFiles);
162
+ }
163
+ this.log.info(`Got ${support_1.util.pluralize('resource file', resourcePaths.length, true)} in '${lprojRoot}'`);
164
+ if (lodash_1.default.isEmpty(resourcePaths)) {
165
+ return {};
166
+ }
167
+ const resultStrings = {};
168
+ const toAbsolutePath = (/** @type {string} */ p) => path_1.default.isAbsolute(p) ? p : path_1.default.resolve(process.cwd(), p);
169
+ for (const resourcePath of resourcePaths) {
170
+ if (!support_1.util.isSubPath(toAbsolutePath(resourcePath), toAbsolutePath(bundleRoot))) {
171
+ // security precaution
172
+ throw new Error(`'${resourcePath}' is expected to be located under '${bundleRoot}'`);
173
+ }
174
+ try {
175
+ const data = await readResource(resourcePath);
176
+ this.log.debug(`Parsed ${support_1.util.pluralize('string', lodash_1.default.keys(data).length, true)} from '${resourcePath}'`);
177
+ lodash_1.default.merge(resultStrings, data);
178
+ }
179
+ catch (e) {
180
+ this.log.warn(`Cannot parse '${resourcePath}' resource. Original error: ${e.message}`);
181
+ }
182
+ }
183
+ this.log.info(`Retrieved ${support_1.util.pluralize('string', lodash_1.default.keys(resultStrings).length, true)} from '${lprojRoot}'`);
184
+ return resultStrings;
185
+ }
186
+ finally {
187
+ if (tmpRoot) {
188
+ await support_1.fs.rimraf(tmpRoot);
230
189
  }
231
190
  }
232
- logger_js_1.default.info(`Got ${lodash_1.default.keys(resultStrings).length} string(s) from '${lprojRoot}'`);
233
- return resultStrings;
234
191
  }
235
192
  exports.parseLocalizableStrings = parseLocalizableStrings;
236
193
  /**
@@ -244,7 +201,15 @@ async function isAppBundle(appPath) {
244
201
  (await support_1.fs.stat(appPath)).isDirectory() &&
245
202
  (await support_1.fs.exists(path_1.default.join(appPath, 'Info.plist'))));
246
203
  }
247
- exports.isAppBundle = isAppBundle;
204
+ /**
205
+ * Check whether the given path on the file system points to the .ipa file
206
+ *
207
+ * @param {string} appPath Possible .ipa file
208
+ * @returns {Promise<boolean>} Whether the given path points to an .ipa bundle
209
+ */
210
+ async function isIpaBundle(appPath) {
211
+ return lodash_1.default.endsWith(lodash_1.default.toLower(appPath), exports.IPA_EXT) && (await support_1.fs.stat(appPath)).isFile();
212
+ }
248
213
  /**
249
214
  * @typedef {Object} UnzipInfo
250
215
  * @property {string} rootDir
@@ -337,6 +302,104 @@ async function unzipStream(zipStream) {
337
302
  };
338
303
  }
339
304
  exports.unzipStream = unzipStream;
305
+ /**
306
+ * Used to parse the file name value from response headers
307
+ *
308
+ * @param {import('@appium/types').HTTPHeaders} headers
309
+ * @returns {string?}
310
+ */
311
+ function parseFileName(headers) {
312
+ const contentDisposition = headers['content-disposition'];
313
+ if (!lodash_1.default.isString(contentDisposition)) {
314
+ return null;
315
+ }
316
+ if (/^attachment/i.test(/** @type {string} */ (contentDisposition))) {
317
+ const match = /filename="([^"]+)/i.exec(/** @type {string} */ (contentDisposition));
318
+ if (match) {
319
+ return support_1.fs.sanitizeName(match[1], { replacement: SANITIZE_REPLACEMENT });
320
+ }
321
+ }
322
+ return null;
323
+ }
324
+ /**
325
+ * Downloads and verifies remote applications for real devices
326
+ *
327
+ * @this {XCUITestDriver}
328
+ * @param {import('node:stream').Readable} stream
329
+ * @param {import('@appium/types').HTTPHeaders} headers
330
+ * @returns {Promise<string>}
331
+ */
332
+ async function downloadIpa(stream, headers) {
333
+ const timer = new support_1.timing.Timer().start();
334
+ const logPerformance = (/** @type {string} */ dstPath, /** @type {number} */ fileSize, /** @type {string} */ action) => {
335
+ const secondsElapsed = timer.getDuration().asSeconds;
336
+ this.log.info(`The remote file (${support_1.util.toReadableSizeString(fileSize)}) ` +
337
+ `has been ${action} to '${dstPath}' in ${secondsElapsed.toFixed(3)}s`);
338
+ if (secondsElapsed >= 1) {
339
+ const bytesPerSec = Math.floor(fileSize / secondsElapsed);
340
+ this.log.debug(`Approximate speed: ${support_1.util.toReadableSizeString(bytesPerSec)}/s`);
341
+ }
342
+ };
343
+ // Check if the file to be downloaded is a .zip rather than .ipa
344
+ const fileName = parseFileName(headers) ?? `appium-app-${new Date().getTime()}${exports.IPA_EXT}`;
345
+ if (fileName.toLowerCase().endsWith(ZIP_EXT)) {
346
+ const { rootDir, archiveSize } = await unzipStream(stream);
347
+ logPerformance(rootDir, archiveSize, 'downloaded and unzipped');
348
+ try {
349
+ const matchedPaths = await findApps(rootDir, [exports.IPA_EXT]);
350
+ if (!lodash_1.default.isEmpty(matchedPaths)) {
351
+ this.log.debug(`Found ${support_1.util.pluralize(`${exports.IPA_EXT} applicaition`, matchedPaths.length, true)} in ` +
352
+ `'${path_1.default.basename(rootDir)}': ${matchedPaths}`);
353
+ }
354
+ for (const matchedPath of matchedPaths) {
355
+ try {
356
+ await this.appInfosCache.put(matchedPath);
357
+ }
358
+ catch (e) {
359
+ this.log.info(e.message);
360
+ continue;
361
+ }
362
+ this.log.debug(`Selecting the application at '${matchedPath}'`);
363
+ const isolatedPath = path_1.default.join(await support_1.tempDir.openDir(), path_1.default.basename(matchedPath));
364
+ await support_1.fs.mv(matchedPath, isolatedPath);
365
+ return isolatedPath;
366
+ }
367
+ throw new Error(`The remote archive does not contain any valid ${exports.IPA_EXT} applications`);
368
+ }
369
+ finally {
370
+ await support_1.fs.rimraf(rootDir);
371
+ }
372
+ }
373
+ const ipaPath = await support_1.tempDir.path({
374
+ prefix: fileName,
375
+ suffix: fileName.toLowerCase().endsWith(exports.IPA_EXT) ? '' : exports.IPA_EXT,
376
+ });
377
+ try {
378
+ const writer = support_1.fs.createWriteStream(ipaPath);
379
+ stream.pipe(writer);
380
+ await new bluebird_1.default((resolve, reject) => {
381
+ stream.once('error', reject);
382
+ writer.once('finish', resolve);
383
+ writer.once('error', (e) => {
384
+ stream.unpipe(writer);
385
+ reject(e);
386
+ });
387
+ });
388
+ }
389
+ catch (err) {
390
+ throw new Error(`Cannot fetch the remote file: ${err.message}`);
391
+ }
392
+ const { size } = await support_1.fs.stat(ipaPath);
393
+ logPerformance(ipaPath, size, 'downloaded');
394
+ try {
395
+ await this.appInfosCache.put(ipaPath);
396
+ }
397
+ catch (e) {
398
+ await support_1.fs.rimraf(ipaPath);
399
+ throw e;
400
+ }
401
+ return ipaPath;
402
+ }
340
403
  /**
341
404
  * Looks for items with given extensions in the given folder
342
405
  *
@@ -354,17 +417,25 @@ async function findApps(appPath, appExtensions) {
354
417
  /**
355
418
  * Moves the application bundle to a newly created temporary folder
356
419
  *
357
- * @param {string} appRoot Full path to the .app bundle
420
+ * @param {string} appPath Full path to the .app or .ipa bundle
358
421
  * @returns {Promise<string>} The new path to the app bundle.
359
- * The name of the app bundle remains though
422
+ * The name of the app bundle remains the same
360
423
  */
361
- async function isolateAppBundle(appRoot) {
424
+ async function isolateApp(appPath) {
425
+ const appFileName = path_1.default.basename(appPath);
426
+ if ((await support_1.fs.stat(appPath)).isFile()) {
427
+ const isolatedPath = await support_1.tempDir.path({
428
+ prefix: appFileName,
429
+ suffix: '',
430
+ });
431
+ await support_1.fs.mv(appPath, isolatedPath, { mkdirp: true });
432
+ return isolatedPath;
433
+ }
362
434
  const tmpRoot = await support_1.tempDir.openDir();
363
- const dstRoot = path_1.default.join(tmpRoot, path_1.default.basename(appRoot));
364
- await support_1.fs.mv(appRoot, dstRoot, { mkdirp: true });
365
- return dstRoot;
435
+ const isolatedRoot = path_1.default.join(tmpRoot, appFileName);
436
+ await support_1.fs.mv(appPath, isolatedRoot, { mkdirp: true });
437
+ return isolatedRoot;
366
438
  }
367
- exports.isolateAppBundle = isolateAppBundle;
368
439
  /**
369
440
  * Builds Safari preferences object based on the given session capabilities
370
441
  *
@@ -387,7 +458,7 @@ exports.buildSafariPreferences = buildSafariPreferences;
387
458
  /**
388
459
  * Unzip the given archive and find a matching .app bundle in it
389
460
  *
390
- * @this {import('./driver').XCUITestDriver}
461
+ * @this {XCUITestDriver}
391
462
  * @param {string|import('node:stream').Readable} appPathOrZipStream The path to the archive.
392
463
  * @param {number} depth [0] the current nesting depth. App bundles whose nesting level
393
464
  * is greater than 1 are not supported.
@@ -395,7 +466,7 @@ exports.buildSafariPreferences = buildSafariPreferences;
395
466
  * @throws If no matching .app bundles were found in the provided archive.
396
467
  */
397
468
  async function unzipApp(appPathOrZipStream, depth = 0) {
398
- const errMsg = `The archive '${this.opts.app}' did not have any matching ${exports.APP_EXT} or ${exports.IPA_EXT} ` +
469
+ const errMsg = `The archive did not have any matching ${exports.APP_EXT} or ${exports.IPA_EXT} ` +
399
470
  `bundles. Please make sure the provided package is valid and contains at least one matching ` +
400
471
  `application bundle which is not nested.`;
401
472
  if (depth > MAX_ARCHIVE_SCAN_DEPTH) {
@@ -408,21 +479,22 @@ async function unzipApp(appPathOrZipStream, depth = 0) {
408
479
  let archiveSize;
409
480
  try {
410
481
  if (lodash_1.default.isString(appPathOrZipStream)) {
411
- ({ rootDir, archiveSize } = await unzipFile(appPathOrZipStream));
482
+ ({ rootDir, archiveSize } = await unzipFile(/** @type {string} */ (appPathOrZipStream)));
412
483
  }
413
484
  else {
414
485
  if (depth > 0) {
415
486
  node_assert_1.default.fail('Streaming unzip cannot be invoked for nested archive items');
416
487
  }
417
- ({ rootDir, archiveSize } = await unzipStream(appPathOrZipStream));
488
+ ({ rootDir, archiveSize } = await unzipStream(
489
+ /** @type {import('node:stream').Readable} */ (appPathOrZipStream)));
418
490
  }
419
491
  }
420
492
  catch (e) {
421
493
  this.log.debug(e.stack);
422
- throw new Error(`Cannot prepare the application at '${this.opts.app}' for testing. Original error: ${e.message}`);
494
+ throw new Error(`Cannot prepare the application for testing. Original error: ${e.message}`);
423
495
  }
424
496
  const secondsElapsed = timer.getDuration().asSeconds;
425
- this.log.info(`The app '${this.opts.app}' (${support_1.util.toReadableSizeString(archiveSize)}) ` +
497
+ this.log.info(`The file (${support_1.util.toReadableSizeString(archiveSize)}) ` +
426
498
  `has been ${lodash_1.default.isString(appPathOrZipStream) ? 'extracted' : 'downloaded and extracted'} ` +
427
499
  `to '${rootDir}' in ${secondsElapsed.toFixed(3)}s`);
428
500
  // it does not make much sense to approximate the speed for short downloads
@@ -430,6 +502,27 @@ async function unzipApp(appPathOrZipStream, depth = 0) {
430
502
  const bytesPerSec = Math.floor(archiveSize / secondsElapsed);
431
503
  this.log.debug(`Approximate decompression speed: ${support_1.util.toReadableSizeString(bytesPerSec)}/s`);
432
504
  }
505
+ const isCompatibleWithCurrentPlatform = async (/** @type {string} */ appPath) => {
506
+ let platforms;
507
+ try {
508
+ platforms = await this.appInfosCache.extractAppPlatforms(appPath);
509
+ }
510
+ catch (e) {
511
+ this.log.info(e.message);
512
+ return false;
513
+ }
514
+ if (this.isSimulator() && !platforms.some((p) => lodash_1.default.includes(p, 'Simulator'))) {
515
+ this.log.info(`'${appPath}' does not have Simulator devices in the list of supported platforms ` +
516
+ `(${platforms.join(',')}). Skipping it`);
517
+ return false;
518
+ }
519
+ if (this.isRealDevice() && !platforms.some((p) => lodash_1.default.includes(p, 'OS'))) {
520
+ this.log.info(`'${appPath}' does not have real devices in the list of supported platforms ` +
521
+ `(${platforms.join(',')}). Skipping it`);
522
+ return false;
523
+ }
524
+ return true;
525
+ };
433
526
  const matchedPaths = await findApps(rootDir, exports.SUPPORTED_EXTENSIONS);
434
527
  if (lodash_1.default.isEmpty(matchedPaths)) {
435
528
  this.log.debug(`'${path_1.default.basename(rootDir)}' has no bundles`);
@@ -441,28 +534,10 @@ async function unzipApp(appPathOrZipStream, depth = 0) {
441
534
  try {
442
535
  for (const matchedPath of matchedPaths) {
443
536
  const fullPath = path_1.default.join(rootDir, matchedPath);
444
- if (await isAppBundle(fullPath)) {
445
- const supportedPlatforms = await fetchSupportedAppPlatforms(fullPath);
446
- if (this.isSimulator() && !supportedPlatforms.some((p) => lodash_1.default.includes(p, 'Simulator'))) {
447
- this.log.info(`'${matchedPath}' does not have Simulator devices in the list of supported platforms ` +
448
- `(${supportedPlatforms.join(',')}). Skipping it`);
449
- continue;
450
- }
451
- if (this.isRealDevice() && !supportedPlatforms.some((p) => lodash_1.default.includes(p, 'OS'))) {
452
- this.log.info(`'${matchedPath}' does not have real devices in the list of supported platforms ` +
453
- `(${supportedPlatforms.join(',')}). Skipping it`);
454
- continue;
455
- }
456
- this.log.info(`'${matchedPath}' is the resulting application bundle selected from '${rootDir}'`);
457
- return await isolateAppBundle(fullPath);
458
- }
459
- else if (lodash_1.default.endsWith(lodash_1.default.toLower(fullPath), exports.IPA_EXT) && (await support_1.fs.stat(fullPath)).isFile()) {
460
- try {
461
- return await unzipApp.bind(this)(fullPath, depth + 1);
462
- }
463
- catch (e) {
464
- this.log.warn(`Skipping processing of '${matchedPath}': ${e.message}`);
465
- }
537
+ if ((await isAppBundle(fullPath) || (this.isRealDevice() && await isIpaBundle(fullPath)))
538
+ && await isCompatibleWithCurrentPlatform(fullPath)) {
539
+ this.log.debug(`Selecting the application at '${matchedPath}'`);
540
+ return await isolateApp(fullPath);
466
541
  }
467
542
  }
468
543
  }
@@ -472,16 +547,24 @@ async function unzipApp(appPathOrZipStream, depth = 0) {
472
547
  throw new Error(errMsg);
473
548
  }
474
549
  /**
475
- * @this {import('./driver').XCUITestDriver}
550
+ * The callback invoked by configureApp helper
551
+ * when it is necessary to download the remote application.
552
+ * We assume the remote file could be anythingm, but only
553
+ * .zip and .ipa formats are supported.
554
+ * A .zip archive can contain one or more
555
+ *
556
+ * @this {XCUITestDriver}
476
557
  * @param {import('@appium/types').DownloadAppOptions} opts
477
558
  * @returns {Promise<string>}
478
559
  */
479
- async function onDownloadApp({ stream }) {
480
- return await unzipApp.bind(this)(stream);
560
+ async function onDownloadApp({ stream, headers }) {
561
+ return this.isRealDevice()
562
+ ? await downloadIpa.bind(this)(stream, headers)
563
+ : await unzipApp.bind(this)(stream);
481
564
  }
482
565
  exports.onDownloadApp = onDownloadApp;
483
566
  /**
484
- * @this {import('./driver').XCUITestDriver}
567
+ * @this {XCUITestDriver}
485
568
  * @param {import('@appium/types').PostProcessOptions} opts
486
569
  * @returns {Promise<import('@appium/types').PostProcessResult|false>}
487
570
  */
@@ -490,28 +573,53 @@ async function onPostConfigureApp({ cachedAppInfo, isUrl, appPath }) {
490
573
  /** @type {import('@appium/types').CachedAppInfo|undefined} */
491
574
  const appInfo = lodash_1.default.isPlainObject(cachedAppInfo) ? cachedAppInfo : undefined;
492
575
  const cachedPath = appInfo ? /** @type {string} */ (appInfo.fullPath) : undefined;
493
- if (
494
- // If cache is present
495
- appInfo && cachedPath
496
- // And if the path exists
497
- && await support_1.fs.exists(cachedPath)
498
- // And if hash matches to the cached one if this is a file
499
- // Or count of files >= of the cached one if this is a folder
500
- && (((await support_1.fs.stat(cachedPath)).isFile()
501
- && await support_1.fs.hash(cachedPath) === /** @type {any} */ (appInfo.integrity)?.file)
502
- || (await support_1.fs.glob('**/*', { cwd: cachedPath })).length >= /** @type {any} */ (appInfo.integrity)?.folder)) {
576
+ const shouldUseCachedApp = async () => {
577
+ if (!appInfo || !cachedPath || !await support_1.fs.exists(cachedPath)) {
578
+ return false;
579
+ }
580
+ const isCachedPathAFile = (await support_1.fs.stat(cachedPath)).isFile();
581
+ if (isCachedPathAFile) {
582
+ return await support_1.fs.hash(cachedPath) === /** @type {any} */ (appInfo.integrity)?.file;
583
+ }
584
+ // If the cached path is a folder then it is expected to be previously extracted from
585
+ // an archive located under appPath whose hash is stored as `cachedAppInfo.packageHash`
586
+ if (!isCachedPathAFile
587
+ && cachedAppInfo?.packageHash
588
+ && await support_1.fs.exists(/** @type {string} */ (appPath))
589
+ && (await support_1.fs.stat(/** @type {string} */ (appPath))).isFile()
590
+ && cachedAppInfo.packageHash === await support_1.fs.hash(/** @type {string} */ (appPath))) {
591
+ /** @type {number|undefined} */
592
+ const nestedItemsCountInCache = /** @type {any} */ (appInfo.integrity)?.folder;
593
+ if (nestedItemsCountInCache !== undefined) {
594
+ return (await support_1.fs.glob('**/*', { cwd: cachedPath })).length >= nestedItemsCountInCache;
595
+ }
596
+ }
597
+ return false;
598
+ };
599
+ if (await shouldUseCachedApp()) {
503
600
  this.log.info(`Using '${cachedPath}' which was cached from '${appPath}'`);
504
- return { appPath: cachedPath };
601
+ return { appPath: /** @type {string} */ (cachedPath) };
602
+ }
603
+ const isLocalIpa = await isIpaBundle(/** @type {string} */ (appPath));
604
+ const isLocalApp = !isLocalIpa && await isAppBundle(/** @type {string} */ (appPath));
605
+ const isPackageReadyForInstall = isLocalApp || (this.isRealDevice() && isLocalIpa);
606
+ if (isPackageReadyForInstall) {
607
+ await this.appInfosCache.put(/** @type {string} */ (appPath));
505
608
  }
506
- const isBundleAlreadyUnpacked = await isAppBundle(/** @type {string} */ (appPath));
507
- // Only local .app bundles that are available in-place should not be cached
508
- if (!isUrl && isBundleAlreadyUnpacked) {
609
+ // Only local .app bundles (real device/Simulator)
610
+ // and .ipa packages for real devices should not be cached
611
+ if (!isUrl && isPackageReadyForInstall) {
509
612
  return false;
510
613
  }
511
614
  // Cache the app while unpacking the bundle if necessary
512
615
  return {
513
- appPath: isBundleAlreadyUnpacked ? appPath : await unzipApp.bind(this)(/** @type {string} */ (appPath))
616
+ appPath: isPackageReadyForInstall
617
+ ? appPath
618
+ : await unzipApp.bind(this)(/** @type {string} */ (appPath))
514
619
  };
515
620
  }
516
621
  exports.onPostConfigureApp = onPostConfigureApp;
622
+ /**
623
+ * @typedef {import('./driver').XCUITestDriver} XCUITestDriver
624
+ */
517
625
  //# sourceMappingURL=app-utils.js.map