appium-xcuitest-driver 10.10.0 → 10.11.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.
- package/CHANGELOG.md +12 -0
- package/build/lib/app-infos-cache.d.ts +29 -31
- package/build/lib/app-infos-cache.d.ts.map +1 -1
- package/build/lib/app-infos-cache.js +29 -33
- package/build/lib/app-infos-cache.js.map +1 -1
- package/build/lib/app-utils.d.ts +30 -59
- package/build/lib/app-utils.d.ts.map +1 -1
- package/build/lib/app-utils.js +158 -211
- package/build/lib/app-utils.js.map +1 -1
- package/build/lib/commands/battery.d.ts.map +1 -1
- package/build/lib/commands/battery.js +4 -8
- package/build/lib/commands/battery.js.map +1 -1
- package/build/lib/commands/biometric.d.ts.map +1 -1
- package/build/lib/commands/biometric.js +1 -5
- package/build/lib/commands/biometric.js.map +1 -1
- package/build/lib/commands/condition.js +4 -4
- package/build/lib/commands/condition.js.map +1 -1
- package/build/lib/commands/content-size.js +1 -1
- package/build/lib/commands/content-size.js.map +1 -1
- package/build/lib/commands/find.js +2 -2
- package/build/lib/commands/find.js.map +1 -1
- package/build/lib/commands/increase-contrast.js +1 -1
- package/build/lib/commands/increase-contrast.js.map +1 -1
- package/build/lib/commands/keychains.d.ts.map +1 -1
- package/build/lib/commands/keychains.js +1 -5
- package/build/lib/commands/keychains.js.map +1 -1
- package/build/lib/commands/localization.d.ts.map +1 -1
- package/build/lib/commands/localization.js +1 -5
- package/build/lib/commands/localization.js.map +1 -1
- package/build/lib/commands/pasteboard.d.ts.map +1 -1
- package/build/lib/commands/pasteboard.js +10 -8
- package/build/lib/commands/pasteboard.js.map +1 -1
- package/build/lib/commands/permissions.js +1 -1
- package/build/lib/commands/permissions.js.map +1 -1
- package/build/lib/css-converter.d.ts +3 -9
- package/build/lib/css-converter.d.ts.map +1 -1
- package/build/lib/css-converter.js +41 -52
- package/build/lib/css-converter.js.map +1 -1
- package/build/lib/device/real-device-management.js +14 -14
- package/build/lib/device/real-device-management.js.map +1 -1
- package/build/lib/device/simulator-management.d.ts.map +1 -1
- package/build/lib/device/simulator-management.js +8 -4
- package/build/lib/device/simulator-management.js.map +1 -1
- package/build/lib/driver.d.ts.map +1 -1
- package/build/lib/driver.js +3 -3
- package/build/lib/driver.js.map +1 -1
- package/build/lib/logger.d.ts +1 -2
- package/build/lib/logger.d.ts.map +1 -1
- package/build/lib/logger.js +2 -2
- package/build/lib/logger.js.map +1 -1
- package/build/lib/utils.d.ts +76 -134
- package/build/lib/utils.d.ts.map +1 -1
- package/build/lib/utils.js +80 -141
- package/build/lib/utils.js.map +1 -1
- package/lib/{app-infos-cache.js → app-infos-cache.ts} +44 -46
- package/lib/{app-utils.js → app-utils.ts} +215 -245
- package/lib/commands/battery.js +3 -4
- package/lib/commands/biometric.js +1 -2
- package/lib/commands/condition.js +1 -1
- package/lib/commands/content-size.js +1 -1
- package/lib/commands/find.js +1 -1
- package/lib/commands/increase-contrast.js +1 -1
- package/lib/commands/keychains.js +1 -2
- package/lib/commands/localization.js +1 -2
- package/lib/commands/pasteboard.js +9 -8
- package/lib/commands/permissions.js +1 -1
- package/lib/{css-converter.js → css-converter.ts} +75 -88
- package/lib/device/real-device-management.ts +1 -1
- package/lib/device/simulator-management.ts +9 -4
- package/lib/driver.ts +6 -4
- package/lib/logger.ts +3 -0
- package/lib/{utils.js → utils.ts} +102 -139
- package/npm-shrinkwrap.json +38 -32
- package/package.json +3 -3
- package/lib/logger.js +0 -5
package/build/lib/app-utils.js
CHANGED
|
@@ -12,31 +12,31 @@ exports.buildSafariPreferences = buildSafariPreferences;
|
|
|
12
12
|
exports.onDownloadApp = onDownloadApp;
|
|
13
13
|
exports.onPostConfigureApp = onPostConfigureApp;
|
|
14
14
|
const lodash_1 = __importDefault(require("lodash"));
|
|
15
|
-
const
|
|
15
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
16
16
|
const support_1 = require("appium/support");
|
|
17
|
-
const
|
|
17
|
+
const logger_1 = require("./logger");
|
|
18
18
|
const node_os_1 = __importDefault(require("node:os"));
|
|
19
19
|
const teen_process_1 = require("teen_process");
|
|
20
20
|
const bluebird_1 = __importDefault(require("bluebird"));
|
|
21
21
|
const node_child_process_1 = require("node:child_process");
|
|
22
22
|
const node_assert_1 = __importDefault(require("node:assert"));
|
|
23
|
-
const
|
|
24
|
-
const STRINGSDICT_RESOURCE = '.stringsdict';
|
|
25
|
-
const STRINGS_RESOURCE = '.strings';
|
|
23
|
+
const utils_1 = require("./utils");
|
|
26
24
|
exports.SAFARI_BUNDLE_ID = 'com.apple.mobilesafari';
|
|
27
25
|
exports.APP_EXT = '.app';
|
|
28
26
|
exports.IPA_EXT = '.ipa';
|
|
27
|
+
exports.SUPPORTED_EXTENSIONS = [exports.IPA_EXT, exports.APP_EXT];
|
|
28
|
+
const STRINGSDICT_RESOURCE = '.stringsdict';
|
|
29
|
+
const STRINGS_RESOURCE = '.strings';
|
|
29
30
|
const ZIP_EXT = '.zip';
|
|
30
|
-
const SAFARI_OPTS_ALIASES_MAP =
|
|
31
|
+
const SAFARI_OPTS_ALIASES_MAP = {
|
|
31
32
|
safariAllowPopups: [
|
|
32
33
|
['WebKitJavaScriptCanOpenWindowsAutomatically', 'JavaScriptCanOpenWindowsAutomatically'],
|
|
33
34
|
(x) => Number(Boolean(x)),
|
|
34
35
|
],
|
|
35
36
|
safariIgnoreFraudWarning: [['WarnAboutFraudulentWebsites'], (x) => Number(!x)],
|
|
36
37
|
safariOpenLinksInBackground: [['OpenLinksInBackground'], (x) => Number(Boolean(x))],
|
|
37
|
-
}
|
|
38
|
+
};
|
|
38
39
|
const MAX_ARCHIVE_SCAN_DEPTH = 1;
|
|
39
|
-
exports.SUPPORTED_EXTENSIONS = [exports.IPA_EXT, exports.APP_EXT];
|
|
40
40
|
const MACOS_RESOURCE_FOLDER = '__MACOSX';
|
|
41
41
|
const SANITIZE_REPLACEMENT = '-';
|
|
42
42
|
const INTEL_ARCH = 'x86_64';
|
|
@@ -44,9 +44,7 @@ const INTEL_ARCH = 'x86_64';
|
|
|
44
44
|
* Verify whether the given application is compatible to the
|
|
45
45
|
* platform where it is going to be installed and tested.
|
|
46
46
|
*
|
|
47
|
-
* @
|
|
48
|
-
* @returns {Promise<void>}
|
|
49
|
-
* @throws {Error} If bundle architecture does not match the expected device architecture.
|
|
47
|
+
* @throws If bundle architecture does not match the expected device architecture.
|
|
50
48
|
*/
|
|
51
49
|
async function verifyApplicationPlatform() {
|
|
52
50
|
this.log.debug('Verifying application platform');
|
|
@@ -54,7 +52,7 @@ async function verifyApplicationPlatform() {
|
|
|
54
52
|
return;
|
|
55
53
|
}
|
|
56
54
|
const supportedPlatforms = await this.appInfosCache.extractAppPlatforms(this.opts.app);
|
|
57
|
-
const isTvOS = (0,
|
|
55
|
+
const isTvOS = (0, utils_1.isTvOs)(this.opts.platformName);
|
|
58
56
|
const prefix = isTvOS ? 'AppleTV' : 'iPhone';
|
|
59
57
|
const suffix = this.isSimulator() ? 'Simulator' : 'OS';
|
|
60
58
|
const dstPlatform = `${prefix}${suffix}`;
|
|
@@ -65,7 +63,7 @@ async function verifyApplicationPlatform() {
|
|
|
65
63
|
if (this.isRealDevice()) {
|
|
66
64
|
return;
|
|
67
65
|
}
|
|
68
|
-
const executablePath =
|
|
66
|
+
const executablePath = node_path_1.default.resolve(this.opts.app, await this.appInfosCache.extractExecutableName(this.opts.app));
|
|
69
67
|
const [resFile, resUname] = await bluebird_1.default.all([
|
|
70
68
|
(0, teen_process_1.exec)('lipo', ['-info', executablePath]),
|
|
71
69
|
(0, teen_process_1.exec)('uname', ['-m']),
|
|
@@ -98,33 +96,8 @@ async function verifyApplicationPlatform() {
|
|
|
98
96
|
throw new Error(`The ${this.opts.bundleId} application does not support the ${processArch} Simulator ` +
|
|
99
97
|
`architecture:\n${bundleExecutableInfo}\n\n${advice}`);
|
|
100
98
|
}
|
|
101
|
-
/**
|
|
102
|
-
*
|
|
103
|
-
* @param {string} resourcePath
|
|
104
|
-
* @returns {Promise<import('@appium/types').StringRecord>}
|
|
105
|
-
*/
|
|
106
|
-
async function readResource(resourcePath) {
|
|
107
|
-
const data = await support_1.plist.parsePlistFile(resourcePath);
|
|
108
|
-
const result = {};
|
|
109
|
-
for (const [key, value] of lodash_1.default.toPairs(data)) {
|
|
110
|
-
result[key] = lodash_1.default.isString(value) ? value : JSON.stringify(value);
|
|
111
|
-
}
|
|
112
|
-
return result;
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* @typedef {Object} LocalizableStringsOptions
|
|
116
|
-
* @property {string} [app]
|
|
117
|
-
* @property {string} [language='en']
|
|
118
|
-
* @property {string} [localizableStringsDir]
|
|
119
|
-
* @property {string} [stringFile]
|
|
120
|
-
* @property {boolean} [strictMode]
|
|
121
|
-
*/
|
|
122
99
|
/**
|
|
123
100
|
* Extracts string resources from an app
|
|
124
|
-
*
|
|
125
|
-
* @this {XCUITestDriver}
|
|
126
|
-
* @param {LocalizableStringsOptions} opts
|
|
127
|
-
* @returns {Promise<import('@appium/types').StringRecord>}
|
|
128
101
|
*/
|
|
129
102
|
async function parseLocalizableStrings(opts = {}) {
|
|
130
103
|
const { app, language = 'en', localizableStringsDir, stringFile, strictMode } = opts;
|
|
@@ -144,14 +117,13 @@ async function parseLocalizableStrings(opts = {}) {
|
|
|
144
117
|
tmpRoot = await support_1.tempDir.openDir();
|
|
145
118
|
this.log.info(`Extracting '${app}' into a temporary location to parse its resources`);
|
|
146
119
|
await support_1.zip.extractAllTo(app, tmpRoot);
|
|
147
|
-
const relativeBundleRoot =
|
|
120
|
+
const relativeBundleRoot = lodash_1.default.first(await findApps(tmpRoot, [exports.APP_EXT]));
|
|
148
121
|
this.log.info(`Selecting '${relativeBundleRoot}'`);
|
|
149
|
-
bundleRoot =
|
|
122
|
+
bundleRoot = node_path_1.default.join(tmpRoot, relativeBundleRoot);
|
|
150
123
|
}
|
|
151
|
-
/** @type {string|undefined} */
|
|
152
124
|
let lprojRoot;
|
|
153
125
|
for (const subfolder of [`${language}.lproj`, localizableStringsDir, ''].filter(lodash_1.default.isString)) {
|
|
154
|
-
lprojRoot =
|
|
126
|
+
lprojRoot = node_path_1.default.resolve(bundleRoot, subfolder);
|
|
155
127
|
if (await support_1.fs.exists(lprojRoot)) {
|
|
156
128
|
break;
|
|
157
129
|
}
|
|
@@ -167,7 +139,7 @@ async function parseLocalizableStrings(opts = {}) {
|
|
|
167
139
|
this.log.info(`Retrieving resource strings from '${lprojRoot}'`);
|
|
168
140
|
const resourcePaths = [];
|
|
169
141
|
if (stringFile) {
|
|
170
|
-
const dstPath =
|
|
142
|
+
const dstPath = node_path_1.default.resolve(lprojRoot, stringFile);
|
|
171
143
|
if (await support_1.fs.exists(dstPath)) {
|
|
172
144
|
resourcePaths.push(dstPath);
|
|
173
145
|
}
|
|
@@ -179,10 +151,10 @@ async function parseLocalizableStrings(opts = {}) {
|
|
|
179
151
|
this.log.info(message);
|
|
180
152
|
}
|
|
181
153
|
}
|
|
182
|
-
if (lodash_1.default.isEmpty(resourcePaths) && (await support_1.fs.exists(lprojRoot))) {
|
|
154
|
+
if (lodash_1.default.isEmpty(resourcePaths) && lprojRoot && (await support_1.fs.exists(lprojRoot))) {
|
|
183
155
|
const resourceFiles = (await support_1.fs.readdir(lprojRoot))
|
|
184
156
|
.filter((name) => lodash_1.default.some([STRINGS_RESOURCE, STRINGSDICT_RESOURCE], (x) => name.endsWith(x)))
|
|
185
|
-
.map((name) =>
|
|
157
|
+
.map((name) => node_path_1.default.resolve(lprojRoot, name));
|
|
186
158
|
resourcePaths.push(...resourceFiles);
|
|
187
159
|
}
|
|
188
160
|
this.log.info(`Got ${support_1.util.pluralize('resource file', resourcePaths.length, true)} in '${lprojRoot}'`);
|
|
@@ -190,7 +162,7 @@ async function parseLocalizableStrings(opts = {}) {
|
|
|
190
162
|
return {};
|
|
191
163
|
}
|
|
192
164
|
const resultStrings = {};
|
|
193
|
-
const toAbsolutePath = (
|
|
165
|
+
const toAbsolutePath = (p) => node_path_1.default.isAbsolute(p) ? p : node_path_1.default.resolve(process.cwd(), p);
|
|
194
166
|
for (const resourcePath of resourcePaths) {
|
|
195
167
|
if (!support_1.util.isSubPath(toAbsolutePath(resourcePath), toAbsolutePath(bundleRoot))) {
|
|
196
168
|
// security precaution
|
|
@@ -214,36 +186,11 @@ async function parseLocalizableStrings(opts = {}) {
|
|
|
214
186
|
}
|
|
215
187
|
}
|
|
216
188
|
}
|
|
217
|
-
/**
|
|
218
|
-
* Check whether the given path on the file system points to the .app bundle root
|
|
219
|
-
*
|
|
220
|
-
* @param {string} appPath Possible .app bundle root
|
|
221
|
-
* @returns {Promise<boolean>} Whether the given path points to an .app bundle
|
|
222
|
-
*/
|
|
223
|
-
async function isAppBundle(appPath) {
|
|
224
|
-
return (lodash_1.default.endsWith(lodash_1.default.toLower(appPath), exports.APP_EXT) &&
|
|
225
|
-
(await support_1.fs.stat(appPath)).isDirectory() &&
|
|
226
|
-
(await support_1.fs.exists(path_1.default.join(appPath, 'Info.plist'))));
|
|
227
|
-
}
|
|
228
|
-
/**
|
|
229
|
-
* Check whether the given path on the file system points to the .ipa file
|
|
230
|
-
*
|
|
231
|
-
* @param {string} appPath Possible .ipa file
|
|
232
|
-
* @returns {Promise<boolean>} Whether the given path points to an .ipa bundle
|
|
233
|
-
*/
|
|
234
|
-
async function isIpaBundle(appPath) {
|
|
235
|
-
return lodash_1.default.endsWith(lodash_1.default.toLower(appPath), exports.IPA_EXT) && (await support_1.fs.stat(appPath)).isFile();
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* @typedef {Object} UnzipInfo
|
|
239
|
-
* @property {string} rootDir
|
|
240
|
-
* @property {number} archiveSize
|
|
241
|
-
*/
|
|
242
189
|
/**
|
|
243
190
|
* Unzips a ZIP archive on the local file system.
|
|
244
191
|
*
|
|
245
|
-
* @param
|
|
246
|
-
* @returns
|
|
192
|
+
* @param archivePath Full path to a .zip archive
|
|
193
|
+
* @returns temporary folder root where the archive has been extracted
|
|
247
194
|
*/
|
|
248
195
|
async function unzipFile(archivePath) {
|
|
249
196
|
const useSystemUnzipEnv = process.env.APPIUM_PREFER_SYSTEM_UNZIP;
|
|
@@ -270,9 +217,6 @@ async function unzipFile(archivePath) {
|
|
|
270
217
|
* Uses bdstar tool for this purpose.
|
|
271
218
|
* This allows to optimize the time needed to prepare the app under test
|
|
272
219
|
* to MAX(download, unzip) instead of SUM(download, unzip)
|
|
273
|
-
*
|
|
274
|
-
* @param {import('node:stream').Readable} zipStream
|
|
275
|
-
* @returns {Promise<UnzipInfo>}
|
|
276
220
|
*/
|
|
277
221
|
async function unzipStream(zipStream) {
|
|
278
222
|
const tmpRoot = await support_1.tempDir.openDir();
|
|
@@ -288,7 +232,7 @@ async function unzipStream(zipStream) {
|
|
|
288
232
|
bsdtarProcess.stderr.on('data', (chunk) => {
|
|
289
233
|
const stderr = chunk.toString();
|
|
290
234
|
if (lodash_1.default.trim(stderr)) {
|
|
291
|
-
|
|
235
|
+
logger_1.log.warn(stderr);
|
|
292
236
|
}
|
|
293
237
|
});
|
|
294
238
|
zipStream.on('data', (chunk) => {
|
|
@@ -300,9 +244,9 @@ async function unzipStream(zipStream) {
|
|
|
300
244
|
zipStream.once('error', reject);
|
|
301
245
|
bsdtarProcess.once('exit', (code, signal) => {
|
|
302
246
|
zipStream.unpipe(bsdtarProcess.stdin);
|
|
303
|
-
|
|
247
|
+
logger_1.log.debug(`bsdtar process exited with code ${code}, signal ${signal}`);
|
|
304
248
|
if (code === 0) {
|
|
305
|
-
resolve();
|
|
249
|
+
resolve(undefined);
|
|
306
250
|
}
|
|
307
251
|
else {
|
|
308
252
|
reject(new Error('Is it a valid ZIP archive?'));
|
|
@@ -329,18 +273,128 @@ async function unzipStream(zipStream) {
|
|
|
329
273
|
};
|
|
330
274
|
}
|
|
331
275
|
/**
|
|
332
|
-
*
|
|
276
|
+
* Builds Safari preferences object based on the given session capabilities
|
|
277
|
+
*
|
|
278
|
+
* @param opts
|
|
279
|
+
* @return
|
|
280
|
+
*/
|
|
281
|
+
function buildSafariPreferences(opts) {
|
|
282
|
+
const safariSettings = lodash_1.default.cloneDeep(opts?.safariGlobalPreferences ?? {});
|
|
283
|
+
for (const [name, [aliases, valueConverter]] of lodash_1.default.toPairs(SAFARI_OPTS_ALIASES_MAP)) {
|
|
284
|
+
if (!lodash_1.default.has(opts, name)) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
for (const alias of aliases) {
|
|
288
|
+
safariSettings[alias] = valueConverter(opts[name]);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return safariSettings;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* The callback invoked by configureApp helper
|
|
295
|
+
* when it is necessary to download the remote application.
|
|
296
|
+
* We assume the remote file could be anythingm, but only
|
|
297
|
+
* .zip and .ipa formats are supported.
|
|
298
|
+
* A .zip archive can contain one or more
|
|
299
|
+
*/
|
|
300
|
+
async function onDownloadApp(opts) {
|
|
301
|
+
return this.isRealDevice()
|
|
302
|
+
? await downloadIpa.bind(this)(opts.stream, opts.headers)
|
|
303
|
+
: await unzipApp.bind(this)(opts.stream);
|
|
304
|
+
}
|
|
305
|
+
async function onPostConfigureApp(opts) {
|
|
306
|
+
// Pick the previously cached entry if its integrity has been preserved
|
|
307
|
+
const appInfo = lodash_1.default.isPlainObject(opts.cachedAppInfo) ? opts.cachedAppInfo : undefined;
|
|
308
|
+
const cachedPath = appInfo ? appInfo.fullPath : undefined;
|
|
309
|
+
const shouldUseCachedApp = async () => {
|
|
310
|
+
if (!appInfo || !cachedPath || !await support_1.fs.exists(cachedPath)) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
const isCachedPathAFile = (await support_1.fs.stat(cachedPath)).isFile();
|
|
314
|
+
if (isCachedPathAFile) {
|
|
315
|
+
return await support_1.fs.hash(cachedPath) === appInfo.integrity?.file;
|
|
316
|
+
}
|
|
317
|
+
// If the cached path is a folder then it is expected to be previously extracted from
|
|
318
|
+
// an archive located under appPath whose hash is stored as `cachedAppInfo.packageHash`
|
|
319
|
+
if (!isCachedPathAFile
|
|
320
|
+
&& opts.cachedAppInfo?.packageHash
|
|
321
|
+
&& opts.appPath
|
|
322
|
+
&& await support_1.fs.exists(opts.appPath)
|
|
323
|
+
&& (await support_1.fs.stat(opts.appPath)).isFile()
|
|
324
|
+
&& opts.cachedAppInfo.packageHash === await support_1.fs.hash(opts.appPath)) {
|
|
325
|
+
const nestedItemsCountInCache = appInfo.integrity?.folder;
|
|
326
|
+
if (nestedItemsCountInCache !== undefined) {
|
|
327
|
+
return (await support_1.fs.glob('**/*', { cwd: cachedPath })).length >= nestedItemsCountInCache;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return false;
|
|
331
|
+
};
|
|
332
|
+
if (await shouldUseCachedApp()) {
|
|
333
|
+
if (!cachedPath) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
this.log.info(`Using '${cachedPath}' which was cached from '${opts.appPath || 'unknown'}'`);
|
|
337
|
+
return { appPath: cachedPath };
|
|
338
|
+
}
|
|
339
|
+
if (!opts.appPath) {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
const isLocalIpa = await isIpaBundle(opts.appPath);
|
|
343
|
+
const isLocalApp = !isLocalIpa && await isAppBundle(opts.appPath);
|
|
344
|
+
const isPackageReadyForInstall = isLocalApp || (this.isRealDevice() && isLocalIpa);
|
|
345
|
+
if (isPackageReadyForInstall) {
|
|
346
|
+
await this.appInfosCache.put(opts.appPath);
|
|
347
|
+
}
|
|
348
|
+
// Only local .app bundles (real device/Simulator)
|
|
349
|
+
// and .ipa packages for real devices should not be cached
|
|
350
|
+
if (!opts.isUrl && isPackageReadyForInstall) {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
// Cache the app while unpacking the bundle if necessary
|
|
354
|
+
return {
|
|
355
|
+
appPath: isPackageReadyForInstall
|
|
356
|
+
? opts.appPath
|
|
357
|
+
: await unzipApp.bind(this)(opts.appPath)
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
// Private functions
|
|
361
|
+
async function readResource(resourcePath) {
|
|
362
|
+
const data = await support_1.plist.parsePlistFile(resourcePath);
|
|
363
|
+
return lodash_1.default.toPairs(data).reduce((result, [key, value]) => {
|
|
364
|
+
result[key] = lodash_1.default.isString(value) ? value : JSON.stringify(value);
|
|
365
|
+
return result;
|
|
366
|
+
}, {});
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Check whether the given path on the file system points to the .app bundle root
|
|
333
370
|
*
|
|
334
|
-
* @param
|
|
335
|
-
* @returns
|
|
371
|
+
* @param appPath Possible .app bundle root
|
|
372
|
+
* @returns Whether the given path points to an .app bundle
|
|
373
|
+
*/
|
|
374
|
+
async function isAppBundle(appPath) {
|
|
375
|
+
return (lodash_1.default.endsWith(lodash_1.default.toLower(appPath), exports.APP_EXT) &&
|
|
376
|
+
(await support_1.fs.stat(appPath)).isDirectory() &&
|
|
377
|
+
(await support_1.fs.exists(node_path_1.default.join(appPath, 'Info.plist'))));
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Check whether the given path on the file system points to the .ipa file
|
|
381
|
+
*
|
|
382
|
+
* @param appPath Possible .ipa file
|
|
383
|
+
* @returns Whether the given path points to an .ipa bundle
|
|
384
|
+
*/
|
|
385
|
+
async function isIpaBundle(appPath) {
|
|
386
|
+
return lodash_1.default.endsWith(lodash_1.default.toLower(appPath), exports.IPA_EXT) && (await support_1.fs.stat(appPath)).isFile();
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Used to parse the file name value from response headers
|
|
336
390
|
*/
|
|
337
391
|
function parseFileName(headers) {
|
|
338
392
|
const contentDisposition = headers['content-disposition'];
|
|
339
393
|
if (!lodash_1.default.isString(contentDisposition)) {
|
|
340
394
|
return null;
|
|
341
395
|
}
|
|
342
|
-
if (/^attachment/i.test(
|
|
343
|
-
const match = /filename="([^"]+)/i.exec(
|
|
396
|
+
if (/^attachment/i.test(contentDisposition)) {
|
|
397
|
+
const match = /filename="([^"]+)/i.exec(contentDisposition);
|
|
344
398
|
if (match) {
|
|
345
399
|
return support_1.fs.sanitizeName(match[1], { replacement: SANITIZE_REPLACEMENT });
|
|
346
400
|
}
|
|
@@ -349,15 +403,10 @@ function parseFileName(headers) {
|
|
|
349
403
|
}
|
|
350
404
|
/**
|
|
351
405
|
* Downloads and verifies remote applications for real devices
|
|
352
|
-
*
|
|
353
|
-
* @this {XCUITestDriver}
|
|
354
|
-
* @param {import('node:stream').Readable} stream
|
|
355
|
-
* @param {import('@appium/types').HTTPHeaders} headers
|
|
356
|
-
* @returns {Promise<string>}
|
|
357
406
|
*/
|
|
358
407
|
async function downloadIpa(stream, headers) {
|
|
359
408
|
const timer = new support_1.timing.Timer().start();
|
|
360
|
-
const logPerformance = (
|
|
409
|
+
const logPerformance = (dstPath, fileSize, action) => {
|
|
361
410
|
const secondsElapsed = timer.getDuration().asSeconds;
|
|
362
411
|
this.log.info(`The remote file (${support_1.util.toReadableSizeString(fileSize)}) ` +
|
|
363
412
|
`has been ${action} to '${dstPath}' in ${secondsElapsed.toFixed(3)}s`);
|
|
@@ -375,7 +424,7 @@ async function downloadIpa(stream, headers) {
|
|
|
375
424
|
const matchedPaths = await findApps(rootDir, [exports.IPA_EXT]);
|
|
376
425
|
if (!lodash_1.default.isEmpty(matchedPaths)) {
|
|
377
426
|
this.log.debug(`Found ${support_1.util.pluralize(`${exports.IPA_EXT} application`, matchedPaths.length, true)} in ` +
|
|
378
|
-
`'${
|
|
427
|
+
`'${node_path_1.default.basename(rootDir)}': ${matchedPaths}`);
|
|
379
428
|
}
|
|
380
429
|
for (const matchedPath of matchedPaths) {
|
|
381
430
|
try {
|
|
@@ -386,7 +435,7 @@ async function downloadIpa(stream, headers) {
|
|
|
386
435
|
continue;
|
|
387
436
|
}
|
|
388
437
|
this.log.debug(`Selecting the application at '${matchedPath}'`);
|
|
389
|
-
const isolatedPath =
|
|
438
|
+
const isolatedPath = node_path_1.default.join(await support_1.tempDir.openDir(), node_path_1.default.basename(matchedPath));
|
|
390
439
|
await support_1.fs.mv(matchedPath, isolatedPath);
|
|
391
440
|
return isolatedPath;
|
|
392
441
|
}
|
|
@@ -429,26 +478,26 @@ async function downloadIpa(stream, headers) {
|
|
|
429
478
|
/**
|
|
430
479
|
* Looks for items with given extensions in the given folder
|
|
431
480
|
*
|
|
432
|
-
* @param
|
|
433
|
-
* @param
|
|
434
|
-
* @returns
|
|
481
|
+
* @param appPath Full path to an app bundle
|
|
482
|
+
* @param appExtensions List of matching item extensions
|
|
483
|
+
* @returns List of relative paths to matched items
|
|
435
484
|
*/
|
|
436
485
|
async function findApps(appPath, appExtensions) {
|
|
437
486
|
const globPattern = `**/*.+(${appExtensions.map((ext) => ext.replace(/^\./, '')).join('|')})`;
|
|
438
487
|
const sortedBundleItems = (await support_1.fs.glob(globPattern, {
|
|
439
488
|
cwd: appPath,
|
|
440
|
-
})).sort((a, b) => a.split(
|
|
489
|
+
})).sort((a, b) => a.split(node_path_1.default.sep).length - b.split(node_path_1.default.sep).length);
|
|
441
490
|
return sortedBundleItems;
|
|
442
491
|
}
|
|
443
492
|
/**
|
|
444
493
|
* Moves the application bundle to a newly created temporary folder
|
|
445
494
|
*
|
|
446
|
-
* @param
|
|
447
|
-
* @returns
|
|
495
|
+
* @param appPath Full path to the .app or .ipa bundle
|
|
496
|
+
* @returns The new path to the app bundle.
|
|
448
497
|
* The name of the app bundle remains the same
|
|
449
498
|
*/
|
|
450
499
|
async function isolateApp(appPath) {
|
|
451
|
-
const appFileName =
|
|
500
|
+
const appFileName = node_path_1.default.basename(appPath);
|
|
452
501
|
if ((await support_1.fs.stat(appPath)).isFile()) {
|
|
453
502
|
const isolatedPath = await support_1.tempDir.path({
|
|
454
503
|
prefix: appFileName,
|
|
@@ -458,36 +507,17 @@ async function isolateApp(appPath) {
|
|
|
458
507
|
return isolatedPath;
|
|
459
508
|
}
|
|
460
509
|
const tmpRoot = await support_1.tempDir.openDir();
|
|
461
|
-
const isolatedRoot =
|
|
510
|
+
const isolatedRoot = node_path_1.default.join(tmpRoot, appFileName);
|
|
462
511
|
await support_1.fs.mv(appPath, isolatedRoot, { mkdirp: true });
|
|
463
512
|
return isolatedRoot;
|
|
464
513
|
}
|
|
465
|
-
/**
|
|
466
|
-
* Builds Safari preferences object based on the given session capabilities
|
|
467
|
-
*
|
|
468
|
-
* @param {import('./driver').XCUITestDriverOpts} opts
|
|
469
|
-
* @return {import('@appium/types').StringRecord}
|
|
470
|
-
*/
|
|
471
|
-
function buildSafariPreferences(opts) {
|
|
472
|
-
const safariSettings = lodash_1.default.cloneDeep(opts?.safariGlobalPreferences ?? {});
|
|
473
|
-
for (const [name, [aliases, valueConverter]] of lodash_1.default.toPairs(SAFARI_OPTS_ALIASES_MAP)) {
|
|
474
|
-
if (!lodash_1.default.has(opts, name)) {
|
|
475
|
-
continue;
|
|
476
|
-
}
|
|
477
|
-
for (const alias of aliases) {
|
|
478
|
-
safariSettings[alias] = valueConverter(opts[name]);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
return safariSettings;
|
|
482
|
-
}
|
|
483
514
|
/**
|
|
484
515
|
* Unzip the given archive and find a matching .app bundle in it
|
|
485
516
|
*
|
|
486
|
-
* @
|
|
487
|
-
* @param
|
|
488
|
-
* @param {number} depth [0] the current nesting depth. App bundles whose nesting level
|
|
517
|
+
* @param appPathOrZipStream The path to the archive.
|
|
518
|
+
* @param depth [0] the current nesting depth. App bundles whose nesting level
|
|
489
519
|
* is greater than 1 are not supported.
|
|
490
|
-
* @returns
|
|
520
|
+
* @returns Full path to the first matching .app bundle..
|
|
491
521
|
* @throws If no matching .app bundles were found in the provided archive.
|
|
492
522
|
*/
|
|
493
523
|
async function unzipApp(appPathOrZipStream, depth = 0) {
|
|
@@ -498,20 +528,17 @@ async function unzipApp(appPathOrZipStream, depth = 0) {
|
|
|
498
528
|
throw new Error(errMsg);
|
|
499
529
|
}
|
|
500
530
|
const timer = new support_1.timing.Timer().start();
|
|
501
|
-
/** @type {string} */
|
|
502
531
|
let rootDir;
|
|
503
|
-
/** @type {number} */
|
|
504
532
|
let archiveSize;
|
|
505
533
|
try {
|
|
506
534
|
if (lodash_1.default.isString(appPathOrZipStream)) {
|
|
507
|
-
({ rootDir, archiveSize } = await unzipFile(
|
|
535
|
+
({ rootDir, archiveSize } = await unzipFile(appPathOrZipStream));
|
|
508
536
|
}
|
|
509
537
|
else {
|
|
510
538
|
if (depth > 0) {
|
|
511
539
|
node_assert_1.default.fail('Streaming unzip cannot be invoked for nested archive items');
|
|
512
540
|
}
|
|
513
|
-
({ rootDir, archiveSize } = await unzipStream(
|
|
514
|
-
/** @type {import('node:stream').Readable} */ (appPathOrZipStream)));
|
|
541
|
+
({ rootDir, archiveSize } = await unzipStream(appPathOrZipStream));
|
|
515
542
|
}
|
|
516
543
|
}
|
|
517
544
|
catch (e) {
|
|
@@ -527,7 +554,7 @@ async function unzipApp(appPathOrZipStream, depth = 0) {
|
|
|
527
554
|
const bytesPerSec = Math.floor(archiveSize / secondsElapsed);
|
|
528
555
|
this.log.debug(`Approximate decompression speed: ${support_1.util.toReadableSizeString(bytesPerSec)}/s`);
|
|
529
556
|
}
|
|
530
|
-
const isCompatibleWithCurrentPlatform = async (
|
|
557
|
+
const isCompatibleWithCurrentPlatform = async (appPath) => {
|
|
531
558
|
let platforms;
|
|
532
559
|
try {
|
|
533
560
|
platforms = await this.appInfosCache.extractAppPlatforms(appPath);
|
|
@@ -550,15 +577,15 @@ async function unzipApp(appPathOrZipStream, depth = 0) {
|
|
|
550
577
|
};
|
|
551
578
|
const matchedPaths = await findApps(rootDir, exports.SUPPORTED_EXTENSIONS);
|
|
552
579
|
if (lodash_1.default.isEmpty(matchedPaths)) {
|
|
553
|
-
this.log.debug(`'${
|
|
580
|
+
this.log.debug(`'${node_path_1.default.basename(rootDir)}' has no bundles`);
|
|
554
581
|
}
|
|
555
582
|
else {
|
|
556
583
|
this.log.debug(`Found ${support_1.util.pluralize('bundle', matchedPaths.length, true)} in ` +
|
|
557
|
-
`'${
|
|
584
|
+
`'${node_path_1.default.basename(rootDir)}': ${matchedPaths}`);
|
|
558
585
|
}
|
|
559
586
|
try {
|
|
560
587
|
for (const matchedPath of matchedPaths) {
|
|
561
|
-
const fullPath =
|
|
588
|
+
const fullPath = node_path_1.default.join(rootDir, matchedPath);
|
|
562
589
|
if ((await isAppBundle(fullPath) || (this.isRealDevice() && await isIpaBundle(fullPath)))
|
|
563
590
|
&& await isCompatibleWithCurrentPlatform(fullPath)) {
|
|
564
591
|
this.log.debug(`Selecting the application at '${matchedPath}'`);
|
|
@@ -571,90 +598,10 @@ async function unzipApp(appPathOrZipStream, depth = 0) {
|
|
|
571
598
|
}
|
|
572
599
|
throw new Error(errMsg);
|
|
573
600
|
}
|
|
574
|
-
/**
|
|
575
|
-
* The callback invoked by configureApp helper
|
|
576
|
-
* when it is necessary to download the remote application.
|
|
577
|
-
* We assume the remote file could be anythingm, but only
|
|
578
|
-
* .zip and .ipa formats are supported.
|
|
579
|
-
* A .zip archive can contain one or more
|
|
580
|
-
*
|
|
581
|
-
* @this {XCUITestDriver}
|
|
582
|
-
* @param {import('@appium/types').DownloadAppOptions} opts
|
|
583
|
-
* @returns {Promise<string>}
|
|
584
|
-
*/
|
|
585
|
-
async function onDownloadApp({ stream, headers }) {
|
|
586
|
-
return this.isRealDevice()
|
|
587
|
-
? await downloadIpa.bind(this)(stream, headers)
|
|
588
|
-
: await unzipApp.bind(this)(stream);
|
|
589
|
-
}
|
|
590
|
-
/**
|
|
591
|
-
* @this {XCUITestDriver}
|
|
592
|
-
* @param {import('@appium/types').PostProcessOptions} opts
|
|
593
|
-
* @returns {Promise<import('@appium/types').PostProcessResult|false>}
|
|
594
|
-
*/
|
|
595
|
-
async function onPostConfigureApp({ cachedAppInfo, isUrl, appPath }) {
|
|
596
|
-
// Pick the previously cached entry if its integrity has been preserved
|
|
597
|
-
/** @type {import('@appium/types').CachedAppInfo|undefined} */
|
|
598
|
-
const appInfo = lodash_1.default.isPlainObject(cachedAppInfo) ? cachedAppInfo : undefined;
|
|
599
|
-
const cachedPath = appInfo ? /** @type {string} */ (appInfo.fullPath) : undefined;
|
|
600
|
-
const shouldUseCachedApp = async () => {
|
|
601
|
-
if (!appInfo || !cachedPath || !await support_1.fs.exists(cachedPath)) {
|
|
602
|
-
return false;
|
|
603
|
-
}
|
|
604
|
-
const isCachedPathAFile = (await support_1.fs.stat(cachedPath)).isFile();
|
|
605
|
-
if (isCachedPathAFile) {
|
|
606
|
-
return await support_1.fs.hash(cachedPath) === /** @type {any} */ (appInfo.integrity)?.file;
|
|
607
|
-
}
|
|
608
|
-
// If the cached path is a folder then it is expected to be previously extracted from
|
|
609
|
-
// an archive located under appPath whose hash is stored as `cachedAppInfo.packageHash`
|
|
610
|
-
if (!isCachedPathAFile
|
|
611
|
-
&& cachedAppInfo?.packageHash
|
|
612
|
-
&& await support_1.fs.exists(/** @type {string} */ (appPath))
|
|
613
|
-
&& (await support_1.fs.stat(/** @type {string} */ (appPath))).isFile()
|
|
614
|
-
&& cachedAppInfo.packageHash === await support_1.fs.hash(/** @type {string} */ (appPath))) {
|
|
615
|
-
/** @type {number|undefined} */
|
|
616
|
-
const nestedItemsCountInCache = /** @type {any} */ (appInfo.integrity)?.folder;
|
|
617
|
-
if (nestedItemsCountInCache !== undefined) {
|
|
618
|
-
return (await support_1.fs.glob('**/*', { cwd: cachedPath })).length >= nestedItemsCountInCache;
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
return false;
|
|
622
|
-
};
|
|
623
|
-
if (await shouldUseCachedApp()) {
|
|
624
|
-
this.log.info(`Using '${cachedPath}' which was cached from '${appPath}'`);
|
|
625
|
-
return { appPath: /** @type {string} */ (cachedPath) };
|
|
626
|
-
}
|
|
627
|
-
const isLocalIpa = await isIpaBundle(/** @type {string} */ (appPath));
|
|
628
|
-
const isLocalApp = !isLocalIpa && await isAppBundle(/** @type {string} */ (appPath));
|
|
629
|
-
const isPackageReadyForInstall = isLocalApp || (this.isRealDevice() && isLocalIpa);
|
|
630
|
-
if (isPackageReadyForInstall) {
|
|
631
|
-
await this.appInfosCache.put(/** @type {string} */ (appPath));
|
|
632
|
-
}
|
|
633
|
-
// Only local .app bundles (real device/Simulator)
|
|
634
|
-
// and .ipa packages for real devices should not be cached
|
|
635
|
-
if (!isUrl && isPackageReadyForInstall) {
|
|
636
|
-
return false;
|
|
637
|
-
}
|
|
638
|
-
// Cache the app while unpacking the bundle if necessary
|
|
639
|
-
return {
|
|
640
|
-
appPath: isPackageReadyForInstall
|
|
641
|
-
? appPath
|
|
642
|
-
: await unzipApp.bind(this)(/** @type {string} */ (appPath))
|
|
643
|
-
};
|
|
644
|
-
}
|
|
645
|
-
/**
|
|
646
|
-
* @returns {Promise<boolean>}
|
|
647
|
-
*/
|
|
648
601
|
async function isRosettaInstalled() {
|
|
649
602
|
return await support_1.fs.exists('/Library/Apple/usr/share/rosetta/rosetta');
|
|
650
603
|
}
|
|
651
|
-
/**
|
|
652
|
-
* @returns {boolean}
|
|
653
|
-
*/
|
|
654
604
|
function isAppleSilicon() {
|
|
655
605
|
return node_os_1.default.cpus()[0].model.includes('Apple');
|
|
656
606
|
}
|
|
657
|
-
/**
|
|
658
|
-
* @typedef {import('./driver').XCUITestDriver} XCUITestDriver
|
|
659
|
-
*/
|
|
660
607
|
//# sourceMappingURL=app-utils.js.map
|