appium-xcuitest-driver 10.10.1 → 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 +6 -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 +26 -30
- package/package.json +2 -2
- package/lib/logger.js +0 -5
|
@@ -1,43 +1,57 @@
|
|
|
1
1
|
import _ from 'lodash';
|
|
2
|
-
import path from 'path';
|
|
2
|
+
import path from 'node:path';
|
|
3
3
|
import {plist, fs, util, tempDir, zip, timing} from 'appium/support';
|
|
4
|
-
import log from './logger
|
|
4
|
+
import {log} from './logger';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import {exec} from 'teen_process';
|
|
7
7
|
import B from 'bluebird';
|
|
8
8
|
import {spawn} from 'node:child_process';
|
|
9
9
|
import assert from 'node:assert';
|
|
10
|
-
import {
|
|
10
|
+
import {isTvOs} from './utils';
|
|
11
|
+
import type {XCUITestDriver, XCUITestDriverOpts} from './driver';
|
|
12
|
+
import type {StringRecord, HTTPHeaders, DownloadAppOptions, PostProcessOptions, PostProcessResult, CachedAppInfo} from '@appium/types';
|
|
13
|
+
import type {Readable} from 'node:stream';
|
|
11
14
|
|
|
12
|
-
const STRINGSDICT_RESOURCE = '.stringsdict';
|
|
13
|
-
const STRINGS_RESOURCE = '.strings';
|
|
14
15
|
export const SAFARI_BUNDLE_ID = 'com.apple.mobilesafari';
|
|
15
16
|
export const APP_EXT = '.app';
|
|
16
17
|
export const IPA_EXT = '.ipa';
|
|
18
|
+
export const SUPPORTED_EXTENSIONS = [IPA_EXT, APP_EXT];
|
|
19
|
+
const STRINGSDICT_RESOURCE = '.stringsdict';
|
|
20
|
+
const STRINGS_RESOURCE = '.strings';
|
|
17
21
|
const ZIP_EXT = '.zip';
|
|
18
|
-
const SAFARI_OPTS_ALIASES_MAP =
|
|
22
|
+
const SAFARI_OPTS_ALIASES_MAP = {
|
|
19
23
|
safariAllowPopups: [
|
|
20
24
|
['WebKitJavaScriptCanOpenWindowsAutomatically', 'JavaScriptCanOpenWindowsAutomatically'],
|
|
21
|
-
(x) => Number(Boolean(x)),
|
|
25
|
+
(x: boolean) => Number(Boolean(x)),
|
|
22
26
|
],
|
|
23
|
-
safariIgnoreFraudWarning: [['WarnAboutFraudulentWebsites'], (x) => Number(!x)],
|
|
24
|
-
safariOpenLinksInBackground: [['OpenLinksInBackground'], (x) => Number(Boolean(x))],
|
|
25
|
-
}
|
|
27
|
+
safariIgnoreFraudWarning: [['WarnAboutFraudulentWebsites'], (x: boolean) => Number(!x)],
|
|
28
|
+
safariOpenLinksInBackground: [['OpenLinksInBackground'], (x: boolean) => Number(Boolean(x))],
|
|
29
|
+
} as const;
|
|
26
30
|
const MAX_ARCHIVE_SCAN_DEPTH = 1;
|
|
27
|
-
export const SUPPORTED_EXTENSIONS = [IPA_EXT, APP_EXT];
|
|
28
31
|
const MACOS_RESOURCE_FOLDER = '__MACOSX';
|
|
29
32
|
const SANITIZE_REPLACEMENT = '-';
|
|
30
33
|
const INTEL_ARCH = 'x86_64';
|
|
31
34
|
|
|
35
|
+
export interface LocalizableStringsOptions {
|
|
36
|
+
app?: string;
|
|
37
|
+
language?: string;
|
|
38
|
+
localizableStringsDir?: string;
|
|
39
|
+
stringFile?: string;
|
|
40
|
+
strictMode?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface UnzipInfo {
|
|
44
|
+
rootDir: string;
|
|
45
|
+
archiveSize: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
32
48
|
/**
|
|
33
49
|
* Verify whether the given application is compatible to the
|
|
34
50
|
* platform where it is going to be installed and tested.
|
|
35
51
|
*
|
|
36
|
-
* @
|
|
37
|
-
* @returns {Promise<void>}
|
|
38
|
-
* @throws {Error} If bundle architecture does not match the expected device architecture.
|
|
52
|
+
* @throws If bundle architecture does not match the expected device architecture.
|
|
39
53
|
*/
|
|
40
|
-
export async function verifyApplicationPlatform() {
|
|
54
|
+
export async function verifyApplicationPlatform(this: XCUITestDriver): Promise<void> {
|
|
41
55
|
this.log.debug('Verifying application platform');
|
|
42
56
|
|
|
43
57
|
if (!this.opts.app) {
|
|
@@ -104,37 +118,13 @@ export async function verifyApplicationPlatform() {
|
|
|
104
118
|
);
|
|
105
119
|
}
|
|
106
120
|
|
|
107
|
-
/**
|
|
108
|
-
*
|
|
109
|
-
* @param {string} resourcePath
|
|
110
|
-
* @returns {Promise<import('@appium/types').StringRecord>}
|
|
111
|
-
*/
|
|
112
|
-
async function readResource(resourcePath) {
|
|
113
|
-
const data = await plist.parsePlistFile(resourcePath);
|
|
114
|
-
const result = {};
|
|
115
|
-
for (const [key, value] of _.toPairs(data)) {
|
|
116
|
-
result[key] = _.isString(value) ? value : JSON.stringify(value);
|
|
117
|
-
}
|
|
118
|
-
return result;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* @typedef {Object} LocalizableStringsOptions
|
|
123
|
-
* @property {string} [app]
|
|
124
|
-
* @property {string} [language='en']
|
|
125
|
-
* @property {string} [localizableStringsDir]
|
|
126
|
-
* @property {string} [stringFile]
|
|
127
|
-
* @property {boolean} [strictMode]
|
|
128
|
-
*/
|
|
129
|
-
|
|
130
121
|
/**
|
|
131
122
|
* Extracts string resources from an app
|
|
132
|
-
*
|
|
133
|
-
* @this {XCUITestDriver}
|
|
134
|
-
* @param {LocalizableStringsOptions} opts
|
|
135
|
-
* @returns {Promise<import('@appium/types').StringRecord>}
|
|
136
123
|
*/
|
|
137
|
-
export async function parseLocalizableStrings(
|
|
124
|
+
export async function parseLocalizableStrings(
|
|
125
|
+
this: XCUITestDriver,
|
|
126
|
+
opts: LocalizableStringsOptions = {}
|
|
127
|
+
): Promise<StringRecord> {
|
|
138
128
|
const {app, language = 'en', localizableStringsDir, stringFile, strictMode} = opts;
|
|
139
129
|
if (!app) {
|
|
140
130
|
const message = `Strings extraction is not supported if 'app' capability is not set`;
|
|
@@ -147,21 +137,20 @@ export async function parseLocalizableStrings(opts = {}) {
|
|
|
147
137
|
|
|
148
138
|
let bundleRoot = app;
|
|
149
139
|
const isArchive = (await fs.stat(app)).isFile();
|
|
150
|
-
let tmpRoot;
|
|
140
|
+
let tmpRoot: string | undefined;
|
|
151
141
|
try {
|
|
152
142
|
if (isArchive) {
|
|
153
143
|
tmpRoot = await tempDir.openDir();
|
|
154
144
|
this.log.info(`Extracting '${app}' into a temporary location to parse its resources`);
|
|
155
145
|
await zip.extractAllTo(app, tmpRoot);
|
|
156
|
-
const relativeBundleRoot =
|
|
146
|
+
const relativeBundleRoot = _.first(await findApps(tmpRoot, [APP_EXT])) as string;
|
|
157
147
|
this.log.info(`Selecting '${relativeBundleRoot}'`);
|
|
158
148
|
bundleRoot = path.join(tmpRoot, relativeBundleRoot);
|
|
159
149
|
}
|
|
160
150
|
|
|
161
|
-
|
|
162
|
-
let lprojRoot;
|
|
151
|
+
let lprojRoot: string | undefined;
|
|
163
152
|
for (const subfolder of [`${language}.lproj`, localizableStringsDir, ''].filter(_.isString)) {
|
|
164
|
-
lprojRoot = path.resolve(bundleRoot,
|
|
153
|
+
lprojRoot = path.resolve(bundleRoot, subfolder as string);
|
|
165
154
|
if (await fs.exists(lprojRoot)) {
|
|
166
155
|
break;
|
|
167
156
|
}
|
|
@@ -176,9 +165,9 @@ export async function parseLocalizableStrings(opts = {}) {
|
|
|
176
165
|
}
|
|
177
166
|
|
|
178
167
|
this.log.info(`Retrieving resource strings from '${lprojRoot}'`);
|
|
179
|
-
const resourcePaths = [];
|
|
168
|
+
const resourcePaths: string[] = [];
|
|
180
169
|
if (stringFile) {
|
|
181
|
-
const dstPath = path.resolve(
|
|
170
|
+
const dstPath = path.resolve(lprojRoot, stringFile);
|
|
182
171
|
if (await fs.exists(dstPath)) {
|
|
183
172
|
resourcePaths.push(dstPath);
|
|
184
173
|
} else {
|
|
@@ -190,7 +179,7 @@ export async function parseLocalizableStrings(opts = {}) {
|
|
|
190
179
|
}
|
|
191
180
|
}
|
|
192
181
|
|
|
193
|
-
if (_.isEmpty(resourcePaths) && (await fs.exists(lprojRoot))) {
|
|
182
|
+
if (_.isEmpty(resourcePaths) && lprojRoot && (await fs.exists(lprojRoot))) {
|
|
194
183
|
const resourceFiles = (await fs.readdir(lprojRoot))
|
|
195
184
|
.filter((name) => _.some([STRINGS_RESOURCE, STRINGSDICT_RESOURCE], (x) => name.endsWith(x)))
|
|
196
185
|
.map((name) => path.resolve(lprojRoot, name));
|
|
@@ -202,8 +191,8 @@ export async function parseLocalizableStrings(opts = {}) {
|
|
|
202
191
|
return {};
|
|
203
192
|
}
|
|
204
193
|
|
|
205
|
-
const resultStrings = {};
|
|
206
|
-
const toAbsolutePath = (
|
|
194
|
+
const resultStrings: StringRecord = {};
|
|
195
|
+
const toAbsolutePath = (p: string) => path.isAbsolute(p) ? p : path.resolve(process.cwd(), p);
|
|
207
196
|
for (const resourcePath of resourcePaths) {
|
|
208
197
|
if (!util.isSubPath(toAbsolutePath(resourcePath), toAbsolutePath(bundleRoot))) {
|
|
209
198
|
// security precaution
|
|
@@ -213,7 +202,7 @@ export async function parseLocalizableStrings(opts = {}) {
|
|
|
213
202
|
const data = await readResource(resourcePath);
|
|
214
203
|
this.log.debug(`Parsed ${util.pluralize('string', _.keys(data).length, true)} from '${resourcePath}'`);
|
|
215
204
|
_.merge(resultStrings, data);
|
|
216
|
-
} catch (e) {
|
|
205
|
+
} catch (e: any) {
|
|
217
206
|
this.log.warn(`Cannot parse '${resourcePath}' resource. Original error: ${e.message}`);
|
|
218
207
|
}
|
|
219
208
|
}
|
|
@@ -227,43 +216,13 @@ export async function parseLocalizableStrings(opts = {}) {
|
|
|
227
216
|
}
|
|
228
217
|
}
|
|
229
218
|
|
|
230
|
-
/**
|
|
231
|
-
* Check whether the given path on the file system points to the .app bundle root
|
|
232
|
-
*
|
|
233
|
-
* @param {string} appPath Possible .app bundle root
|
|
234
|
-
* @returns {Promise<boolean>} Whether the given path points to an .app bundle
|
|
235
|
-
*/
|
|
236
|
-
async function isAppBundle(appPath) {
|
|
237
|
-
return (
|
|
238
|
-
_.endsWith(_.toLower(appPath), APP_EXT) &&
|
|
239
|
-
(await fs.stat(appPath)).isDirectory() &&
|
|
240
|
-
(await fs.exists(path.join(appPath, 'Info.plist')))
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Check whether the given path on the file system points to the .ipa file
|
|
246
|
-
*
|
|
247
|
-
* @param {string} appPath Possible .ipa file
|
|
248
|
-
* @returns {Promise<boolean>} Whether the given path points to an .ipa bundle
|
|
249
|
-
*/
|
|
250
|
-
async function isIpaBundle(appPath) {
|
|
251
|
-
return _.endsWith(_.toLower(appPath), IPA_EXT) && (await fs.stat(appPath)).isFile();
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* @typedef {Object} UnzipInfo
|
|
256
|
-
* @property {string} rootDir
|
|
257
|
-
* @property {number} archiveSize
|
|
258
|
-
*/
|
|
259
|
-
|
|
260
219
|
/**
|
|
261
220
|
* Unzips a ZIP archive on the local file system.
|
|
262
221
|
*
|
|
263
|
-
* @param
|
|
264
|
-
* @returns
|
|
222
|
+
* @param archivePath Full path to a .zip archive
|
|
223
|
+
* @returns temporary folder root where the archive has been extracted
|
|
265
224
|
*/
|
|
266
|
-
export async function unzipFile(archivePath) {
|
|
225
|
+
export async function unzipFile(archivePath: string): Promise<UnzipInfo> {
|
|
267
226
|
const useSystemUnzipEnv = process.env.APPIUM_PREFER_SYSTEM_UNZIP;
|
|
268
227
|
const useSystemUnzip =
|
|
269
228
|
_.isEmpty(useSystemUnzipEnv) || !['0', 'false'].includes(_.toLower(useSystemUnzipEnv));
|
|
@@ -289,11 +248,8 @@ export async function unzipFile(archivePath) {
|
|
|
289
248
|
* Uses bdstar tool for this purpose.
|
|
290
249
|
* This allows to optimize the time needed to prepare the app under test
|
|
291
250
|
* to MAX(download, unzip) instead of SUM(download, unzip)
|
|
292
|
-
*
|
|
293
|
-
* @param {import('node:stream').Readable} zipStream
|
|
294
|
-
* @returns {Promise<UnzipInfo>}
|
|
295
251
|
*/
|
|
296
|
-
export async function unzipStream(zipStream) {
|
|
252
|
+
export async function unzipStream(zipStream: Readable): Promise<UnzipInfo> {
|
|
297
253
|
const tmpRoot = await tempDir.openDir();
|
|
298
254
|
const bsdtarProcess = spawn(await fs.which('bsdtar'), [
|
|
299
255
|
'-x',
|
|
@@ -321,7 +277,7 @@ export async function unzipStream(zipStream) {
|
|
|
321
277
|
zipStream.unpipe(bsdtarProcess.stdin);
|
|
322
278
|
log.debug(`bsdtar process exited with code ${code}, signal ${signal}`);
|
|
323
279
|
if (code === 0) {
|
|
324
|
-
resolve();
|
|
280
|
+
resolve(undefined);
|
|
325
281
|
} else {
|
|
326
282
|
reject(new Error('Is it a valid ZIP archive?'));
|
|
327
283
|
}
|
|
@@ -331,7 +287,7 @@ export async function unzipStream(zipStream) {
|
|
|
331
287
|
reject(e);
|
|
332
288
|
});
|
|
333
289
|
});
|
|
334
|
-
} catch (err) {
|
|
290
|
+
} catch (err: any) {
|
|
335
291
|
bsdtarProcess.kill(9);
|
|
336
292
|
await fs.rimraf(tmpRoot);
|
|
337
293
|
throw new Error(`The response data cannot be unzipped: ${err.message}`);
|
|
@@ -346,19 +302,150 @@ export async function unzipStream(zipStream) {
|
|
|
346
302
|
}
|
|
347
303
|
|
|
348
304
|
/**
|
|
349
|
-
*
|
|
305
|
+
* Builds Safari preferences object based on the given session capabilities
|
|
350
306
|
*
|
|
351
|
-
* @param
|
|
352
|
-
* @
|
|
307
|
+
* @param opts
|
|
308
|
+
* @return
|
|
353
309
|
*/
|
|
354
|
-
function
|
|
310
|
+
export function buildSafariPreferences(opts: XCUITestDriverOpts): StringRecord {
|
|
311
|
+
const safariSettings = _.cloneDeep(opts?.safariGlobalPreferences ?? {});
|
|
312
|
+
|
|
313
|
+
for (const [name, [aliases, valueConverter]] of _.toPairs(SAFARI_OPTS_ALIASES_MAP)) {
|
|
314
|
+
if (!_.has(opts, name)) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
for (const alias of aliases) {
|
|
319
|
+
safariSettings[alias] = valueConverter((opts as any)[name]);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return safariSettings;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* The callback invoked by configureApp helper
|
|
327
|
+
* when it is necessary to download the remote application.
|
|
328
|
+
* We assume the remote file could be anythingm, but only
|
|
329
|
+
* .zip and .ipa formats are supported.
|
|
330
|
+
* A .zip archive can contain one or more
|
|
331
|
+
*/
|
|
332
|
+
export async function onDownloadApp(this: XCUITestDriver, opts: DownloadAppOptions): Promise<string> {
|
|
333
|
+
return this.isRealDevice()
|
|
334
|
+
? await downloadIpa.bind(this)(opts.stream, opts.headers)
|
|
335
|
+
: await unzipApp.bind(this)(opts.stream);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export async function onPostConfigureApp(
|
|
339
|
+
this: XCUITestDriver,
|
|
340
|
+
opts: PostProcessOptions
|
|
341
|
+
): Promise<PostProcessResult | false> {
|
|
342
|
+
// Pick the previously cached entry if its integrity has been preserved
|
|
343
|
+
const appInfo = _.isPlainObject(opts.cachedAppInfo) ? opts.cachedAppInfo as CachedAppInfo : undefined;
|
|
344
|
+
const cachedPath = appInfo ? appInfo.fullPath : undefined;
|
|
345
|
+
|
|
346
|
+
const shouldUseCachedApp = async () => {
|
|
347
|
+
if (!appInfo || !cachedPath || !await fs.exists(cachedPath)) {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const isCachedPathAFile = (await fs.stat(cachedPath)).isFile();
|
|
352
|
+
if (isCachedPathAFile) {
|
|
353
|
+
return await fs.hash(cachedPath) === (appInfo.integrity as any)?.file;
|
|
354
|
+
}
|
|
355
|
+
// If the cached path is a folder then it is expected to be previously extracted from
|
|
356
|
+
// an archive located under appPath whose hash is stored as `cachedAppInfo.packageHash`
|
|
357
|
+
if (
|
|
358
|
+
!isCachedPathAFile
|
|
359
|
+
&& opts.cachedAppInfo?.packageHash
|
|
360
|
+
&& opts.appPath
|
|
361
|
+
&& await fs.exists(opts.appPath)
|
|
362
|
+
&& (await fs.stat(opts.appPath)).isFile()
|
|
363
|
+
&& opts.cachedAppInfo.packageHash === await fs.hash(opts.appPath)
|
|
364
|
+
) {
|
|
365
|
+
const nestedItemsCountInCache = (appInfo.integrity as any)?.folder;
|
|
366
|
+
if (nestedItemsCountInCache !== undefined) {
|
|
367
|
+
return (await fs.glob('**/*', {cwd: cachedPath})).length >= nestedItemsCountInCache;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return false;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
if (await shouldUseCachedApp()) {
|
|
375
|
+
if (!cachedPath) {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
this.log.info(`Using '${cachedPath}' which was cached from '${opts.appPath || 'unknown'}'`);
|
|
379
|
+
return {appPath: cachedPath};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (!opts.appPath) {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const isLocalIpa = await isIpaBundle(opts.appPath);
|
|
387
|
+
const isLocalApp = !isLocalIpa && await isAppBundle(opts.appPath);
|
|
388
|
+
const isPackageReadyForInstall = isLocalApp || (this.isRealDevice() && isLocalIpa);
|
|
389
|
+
if (isPackageReadyForInstall) {
|
|
390
|
+
await this.appInfosCache.put(opts.appPath);
|
|
391
|
+
}
|
|
392
|
+
// Only local .app bundles (real device/Simulator)
|
|
393
|
+
// and .ipa packages for real devices should not be cached
|
|
394
|
+
if (!opts.isUrl && isPackageReadyForInstall) {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
// Cache the app while unpacking the bundle if necessary
|
|
398
|
+
return {
|
|
399
|
+
appPath: isPackageReadyForInstall
|
|
400
|
+
? opts.appPath
|
|
401
|
+
: await unzipApp.bind(this)(opts.appPath)
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Private functions
|
|
406
|
+
async function readResource(resourcePath: string): Promise<StringRecord> {
|
|
407
|
+
const data = await plist.parsePlistFile(resourcePath);
|
|
408
|
+
return _.toPairs(data).reduce((result, [key, value]) => {
|
|
409
|
+
result[key] = _.isString(value) ? value : JSON.stringify(value);
|
|
410
|
+
return result;
|
|
411
|
+
}, {} as StringRecord);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Check whether the given path on the file system points to the .app bundle root
|
|
416
|
+
*
|
|
417
|
+
* @param appPath Possible .app bundle root
|
|
418
|
+
* @returns Whether the given path points to an .app bundle
|
|
419
|
+
*/
|
|
420
|
+
async function isAppBundle(appPath: string): Promise<boolean> {
|
|
421
|
+
return (
|
|
422
|
+
_.endsWith(_.toLower(appPath), APP_EXT) &&
|
|
423
|
+
(await fs.stat(appPath)).isDirectory() &&
|
|
424
|
+
(await fs.exists(path.join(appPath, 'Info.plist')))
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Check whether the given path on the file system points to the .ipa file
|
|
430
|
+
*
|
|
431
|
+
* @param appPath Possible .ipa file
|
|
432
|
+
* @returns Whether the given path points to an .ipa bundle
|
|
433
|
+
*/
|
|
434
|
+
async function isIpaBundle(appPath: string): Promise<boolean> {
|
|
435
|
+
return _.endsWith(_.toLower(appPath), IPA_EXT) && (await fs.stat(appPath)).isFile();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Used to parse the file name value from response headers
|
|
440
|
+
*/
|
|
441
|
+
function parseFileName(headers: HTTPHeaders): string | null {
|
|
355
442
|
const contentDisposition = headers['content-disposition'];
|
|
356
443
|
if (!_.isString(contentDisposition)) {
|
|
357
444
|
return null;
|
|
358
445
|
}
|
|
359
446
|
|
|
360
|
-
if (/^attachment/i.test(
|
|
361
|
-
const match = /filename="([^"]+)/i.exec(
|
|
447
|
+
if (/^attachment/i.test(contentDisposition)) {
|
|
448
|
+
const match = /filename="([^"]+)/i.exec(contentDisposition);
|
|
362
449
|
if (match) {
|
|
363
450
|
return fs.sanitizeName(match[1], {replacement: SANITIZE_REPLACEMENT});
|
|
364
451
|
}
|
|
@@ -368,16 +455,11 @@ function parseFileName(headers) {
|
|
|
368
455
|
|
|
369
456
|
/**
|
|
370
457
|
* Downloads and verifies remote applications for real devices
|
|
371
|
-
*
|
|
372
|
-
* @this {XCUITestDriver}
|
|
373
|
-
* @param {import('node:stream').Readable} stream
|
|
374
|
-
* @param {import('@appium/types').HTTPHeaders} headers
|
|
375
|
-
* @returns {Promise<string>}
|
|
376
458
|
*/
|
|
377
|
-
async function downloadIpa(stream, headers) {
|
|
459
|
+
async function downloadIpa(this: XCUITestDriver, stream: Readable, headers: HTTPHeaders): Promise<string> {
|
|
378
460
|
const timer = new timing.Timer().start();
|
|
379
461
|
|
|
380
|
-
const logPerformance = (
|
|
462
|
+
const logPerformance = (dstPath: string, fileSize: number, action: string) => {
|
|
381
463
|
const secondsElapsed = timer.getDuration().asSeconds;
|
|
382
464
|
this.log.info(
|
|
383
465
|
`The remote file (${util.toReadableSizeString(fileSize)}) ` +
|
|
@@ -405,7 +487,7 @@ async function downloadIpa(stream, headers) {
|
|
|
405
487
|
for (const matchedPath of matchedPaths) {
|
|
406
488
|
try {
|
|
407
489
|
await this.appInfosCache.put(matchedPath);
|
|
408
|
-
} catch (e) {
|
|
490
|
+
} catch (e: any) {
|
|
409
491
|
this.log.info(e.message);
|
|
410
492
|
continue;
|
|
411
493
|
}
|
|
@@ -436,7 +518,7 @@ async function downloadIpa(stream, headers) {
|
|
|
436
518
|
reject(e);
|
|
437
519
|
});
|
|
438
520
|
});
|
|
439
|
-
} catch (err) {
|
|
521
|
+
} catch (err: any) {
|
|
440
522
|
throw new Error(`Cannot fetch the remote file: ${err.message}`);
|
|
441
523
|
}
|
|
442
524
|
const {size} = await fs.stat(ipaPath);
|
|
@@ -453,11 +535,11 @@ async function downloadIpa(stream, headers) {
|
|
|
453
535
|
/**
|
|
454
536
|
* Looks for items with given extensions in the given folder
|
|
455
537
|
*
|
|
456
|
-
* @param
|
|
457
|
-
* @param
|
|
458
|
-
* @returns
|
|
538
|
+
* @param appPath Full path to an app bundle
|
|
539
|
+
* @param appExtensions List of matching item extensions
|
|
540
|
+
* @returns List of relative paths to matched items
|
|
459
541
|
*/
|
|
460
|
-
async function findApps(appPath, appExtensions) {
|
|
542
|
+
async function findApps(appPath: string, appExtensions: string[]): Promise<string[]> {
|
|
461
543
|
const globPattern = `**/*.+(${appExtensions.map((ext) => ext.replace(/^\./, '')).join('|')})`;
|
|
462
544
|
const sortedBundleItems = (
|
|
463
545
|
await fs.glob(globPattern, {
|
|
@@ -470,11 +552,11 @@ async function findApps(appPath, appExtensions) {
|
|
|
470
552
|
/**
|
|
471
553
|
* Moves the application bundle to a newly created temporary folder
|
|
472
554
|
*
|
|
473
|
-
* @param
|
|
474
|
-
* @returns
|
|
555
|
+
* @param appPath Full path to the .app or .ipa bundle
|
|
556
|
+
* @returns The new path to the app bundle.
|
|
475
557
|
* The name of the app bundle remains the same
|
|
476
558
|
*/
|
|
477
|
-
async function isolateApp(appPath) {
|
|
559
|
+
async function isolateApp(appPath: string): Promise<string> {
|
|
478
560
|
const appFileName = path.basename(appPath);
|
|
479
561
|
if ((await fs.stat(appPath)).isFile()) {
|
|
480
562
|
const isolatedPath = await tempDir.path({
|
|
@@ -491,38 +573,20 @@ async function isolateApp(appPath) {
|
|
|
491
573
|
return isolatedRoot;
|
|
492
574
|
}
|
|
493
575
|
|
|
494
|
-
/**
|
|
495
|
-
* Builds Safari preferences object based on the given session capabilities
|
|
496
|
-
*
|
|
497
|
-
* @param {import('./driver').XCUITestDriverOpts} opts
|
|
498
|
-
* @return {import('@appium/types').StringRecord}
|
|
499
|
-
*/
|
|
500
|
-
export function buildSafariPreferences(opts) {
|
|
501
|
-
const safariSettings = _.cloneDeep(opts?.safariGlobalPreferences ?? {});
|
|
502
|
-
|
|
503
|
-
for (const [name, [aliases, valueConverter]] of _.toPairs(SAFARI_OPTS_ALIASES_MAP)) {
|
|
504
|
-
if (!_.has(opts, name)) {
|
|
505
|
-
continue;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
for (const alias of aliases) {
|
|
509
|
-
safariSettings[alias] = valueConverter(opts[name]);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
return safariSettings;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
576
|
/**
|
|
516
577
|
* Unzip the given archive and find a matching .app bundle in it
|
|
517
578
|
*
|
|
518
|
-
* @
|
|
519
|
-
* @param
|
|
520
|
-
* @param {number} depth [0] the current nesting depth. App bundles whose nesting level
|
|
579
|
+
* @param appPathOrZipStream The path to the archive.
|
|
580
|
+
* @param depth [0] the current nesting depth. App bundles whose nesting level
|
|
521
581
|
* is greater than 1 are not supported.
|
|
522
|
-
* @returns
|
|
582
|
+
* @returns Full path to the first matching .app bundle..
|
|
523
583
|
* @throws If no matching .app bundles were found in the provided archive.
|
|
524
584
|
*/
|
|
525
|
-
async function unzipApp(
|
|
585
|
+
async function unzipApp(
|
|
586
|
+
this: XCUITestDriver,
|
|
587
|
+
appPathOrZipStream: string | Readable,
|
|
588
|
+
depth: number = 0
|
|
589
|
+
): Promise<string> {
|
|
526
590
|
const errMsg = `The archive did not have any matching ${APP_EXT} or ${IPA_EXT} ` +
|
|
527
591
|
`bundles. Please make sure the provided package is valid and contains at least one matching ` +
|
|
528
592
|
`application bundle which is not nested.`;
|
|
@@ -531,22 +595,18 @@ async function unzipApp(appPathOrZipStream, depth = 0) {
|
|
|
531
595
|
}
|
|
532
596
|
|
|
533
597
|
const timer = new timing.Timer().start();
|
|
534
|
-
|
|
535
|
-
let
|
|
536
|
-
/** @type {number} */
|
|
537
|
-
let archiveSize;
|
|
598
|
+
let rootDir: string;
|
|
599
|
+
let archiveSize: number;
|
|
538
600
|
try {
|
|
539
601
|
if (_.isString(appPathOrZipStream)) {
|
|
540
|
-
({rootDir, archiveSize} = await unzipFile(
|
|
602
|
+
({rootDir, archiveSize} = await unzipFile(appPathOrZipStream));
|
|
541
603
|
} else {
|
|
542
604
|
if (depth > 0) {
|
|
543
605
|
assert.fail('Streaming unzip cannot be invoked for nested archive items');
|
|
544
606
|
}
|
|
545
|
-
({rootDir, archiveSize} = await unzipStream(
|
|
546
|
-
/** @type {import('node:stream').Readable} */ (appPathOrZipStream))
|
|
547
|
-
);
|
|
607
|
+
({rootDir, archiveSize} = await unzipStream(appPathOrZipStream));
|
|
548
608
|
}
|
|
549
|
-
} catch (e) {
|
|
609
|
+
} catch (e: any) {
|
|
550
610
|
this.log.debug(e.stack);
|
|
551
611
|
throw new Error(
|
|
552
612
|
`Cannot prepare the application for testing. Original error: ${e.message}`
|
|
@@ -564,11 +624,11 @@ async function unzipApp(appPathOrZipStream, depth = 0) {
|
|
|
564
624
|
this.log.debug(`Approximate decompression speed: ${util.toReadableSizeString(bytesPerSec)}/s`);
|
|
565
625
|
}
|
|
566
626
|
|
|
567
|
-
const isCompatibleWithCurrentPlatform = async (
|
|
568
|
-
let platforms;
|
|
627
|
+
const isCompatibleWithCurrentPlatform = async (appPath: string) => {
|
|
628
|
+
let platforms: string[];
|
|
569
629
|
try {
|
|
570
630
|
platforms = await this.appInfosCache.extractAppPlatforms(appPath);
|
|
571
|
-
} catch (e) {
|
|
631
|
+
} catch (e: any) {
|
|
572
632
|
this.log.info(e.message);
|
|
573
633
|
return false;
|
|
574
634
|
}
|
|
@@ -615,100 +675,10 @@ async function unzipApp(appPathOrZipStream, depth = 0) {
|
|
|
615
675
|
throw new Error(errMsg);
|
|
616
676
|
}
|
|
617
677
|
|
|
618
|
-
|
|
619
|
-
* The callback invoked by configureApp helper
|
|
620
|
-
* when it is necessary to download the remote application.
|
|
621
|
-
* We assume the remote file could be anythingm, but only
|
|
622
|
-
* .zip and .ipa formats are supported.
|
|
623
|
-
* A .zip archive can contain one or more
|
|
624
|
-
*
|
|
625
|
-
* @this {XCUITestDriver}
|
|
626
|
-
* @param {import('@appium/types').DownloadAppOptions} opts
|
|
627
|
-
* @returns {Promise<string>}
|
|
628
|
-
*/
|
|
629
|
-
export async function onDownloadApp({stream, headers}) {
|
|
630
|
-
return this.isRealDevice()
|
|
631
|
-
? await downloadIpa.bind(this)(stream, headers)
|
|
632
|
-
: await unzipApp.bind(this)(stream);
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
/**
|
|
636
|
-
* @this {XCUITestDriver}
|
|
637
|
-
* @param {import('@appium/types').PostProcessOptions} opts
|
|
638
|
-
* @returns {Promise<import('@appium/types').PostProcessResult|false>}
|
|
639
|
-
*/
|
|
640
|
-
export async function onPostConfigureApp({cachedAppInfo, isUrl, appPath}) {
|
|
641
|
-
// Pick the previously cached entry if its integrity has been preserved
|
|
642
|
-
/** @type {import('@appium/types').CachedAppInfo|undefined} */
|
|
643
|
-
const appInfo = _.isPlainObject(cachedAppInfo) ? cachedAppInfo : undefined;
|
|
644
|
-
const cachedPath = appInfo ? /** @type {string} */ (appInfo.fullPath) : undefined;
|
|
645
|
-
|
|
646
|
-
const shouldUseCachedApp = async () => {
|
|
647
|
-
if (!appInfo || !cachedPath || !await fs.exists(cachedPath)) {
|
|
648
|
-
return false;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
const isCachedPathAFile = (await fs.stat(cachedPath)).isFile();
|
|
652
|
-
if (isCachedPathAFile) {
|
|
653
|
-
return await fs.hash(cachedPath) === /** @type {any} */ (appInfo.integrity)?.file;
|
|
654
|
-
}
|
|
655
|
-
// If the cached path is a folder then it is expected to be previously extracted from
|
|
656
|
-
// an archive located under appPath whose hash is stored as `cachedAppInfo.packageHash`
|
|
657
|
-
if (
|
|
658
|
-
!isCachedPathAFile
|
|
659
|
-
&& cachedAppInfo?.packageHash
|
|
660
|
-
&& await fs.exists(/** @type {string} */ (appPath))
|
|
661
|
-
&& (await fs.stat(/** @type {string} */ (appPath))).isFile()
|
|
662
|
-
&& cachedAppInfo.packageHash === await fs.hash(/** @type {string} */ (appPath))
|
|
663
|
-
) {
|
|
664
|
-
/** @type {number|undefined} */
|
|
665
|
-
const nestedItemsCountInCache = /** @type {any} */ (appInfo.integrity)?.folder;
|
|
666
|
-
if (nestedItemsCountInCache !== undefined) {
|
|
667
|
-
return (await fs.glob('**/*', {cwd: cachedPath})).length >= nestedItemsCountInCache;
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
return false;
|
|
672
|
-
};
|
|
673
|
-
|
|
674
|
-
if (await shouldUseCachedApp()) {
|
|
675
|
-
this.log.info(`Using '${cachedPath}' which was cached from '${appPath}'`);
|
|
676
|
-
return {appPath: /** @type {string} */ (cachedPath)};
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
const isLocalIpa = await isIpaBundle(/** @type {string} */(appPath));
|
|
680
|
-
const isLocalApp = !isLocalIpa && await isAppBundle(/** @type {string} */(appPath));
|
|
681
|
-
const isPackageReadyForInstall = isLocalApp || (this.isRealDevice() && isLocalIpa);
|
|
682
|
-
if (isPackageReadyForInstall) {
|
|
683
|
-
await this.appInfosCache.put(/** @type {string} */(appPath));
|
|
684
|
-
}
|
|
685
|
-
// Only local .app bundles (real device/Simulator)
|
|
686
|
-
// and .ipa packages for real devices should not be cached
|
|
687
|
-
if (!isUrl && isPackageReadyForInstall) {
|
|
688
|
-
return false;
|
|
689
|
-
}
|
|
690
|
-
// Cache the app while unpacking the bundle if necessary
|
|
691
|
-
return {
|
|
692
|
-
appPath: isPackageReadyForInstall
|
|
693
|
-
? appPath
|
|
694
|
-
: await unzipApp.bind(this)(/** @type {string} */(appPath))
|
|
695
|
-
};
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
/**
|
|
699
|
-
* @returns {Promise<boolean>}
|
|
700
|
-
*/
|
|
701
|
-
async function isRosettaInstalled() {
|
|
678
|
+
async function isRosettaInstalled(): Promise<boolean> {
|
|
702
679
|
return await fs.exists('/Library/Apple/usr/share/rosetta/rosetta');
|
|
703
680
|
}
|
|
704
681
|
|
|
705
|
-
|
|
706
|
-
* @returns {boolean}
|
|
707
|
-
*/
|
|
708
|
-
function isAppleSilicon() {
|
|
682
|
+
function isAppleSilicon(): boolean {
|
|
709
683
|
return os.cpus()[0].model.includes('Apple');
|
|
710
684
|
}
|
|
711
|
-
|
|
712
|
-
/**
|
|
713
|
-
* @typedef {import('./driver').XCUITestDriver} XCUITestDriver
|
|
714
|
-
*/
|
package/lib/commands/battery.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { isIos18OrNewer } from '../utils.js';
|
|
1
|
+
import { isIos18OrNewer } from '../utils';
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Reads the battery information from the device under test.
|
|
@@ -22,10 +21,10 @@ export async function mobileGetBatteryInfo() {
|
|
|
22
21
|
returnRawJson: true,
|
|
23
22
|
});
|
|
24
23
|
} catch (err) {
|
|
25
|
-
log.error(`Failed to get battery info from DiagnosticsService: ${err.message}`);
|
|
24
|
+
this.log.error(`Failed to get battery info from DiagnosticsService: ${err.message}`);
|
|
26
25
|
} finally {
|
|
27
26
|
if (remoteXPCConnection) {
|
|
28
|
-
log.info(`Closing remoteXPC connection for device ${this.device.udid}`);
|
|
27
|
+
this.log.info(`Closing remoteXPC connection for device ${this.device.udid}`);
|
|
29
28
|
await remoteXPCConnection.close();
|
|
30
29
|
}
|
|
31
30
|
}
|