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.
- package/CHANGELOG.md +14 -0
- package/build/lib/app-infos-cache.d.ts +46 -0
- package/build/lib/app-infos-cache.d.ts.map +1 -0
- package/build/lib/app-infos-cache.js +156 -0
- package/build/lib/app-infos-cache.js.map +1 -0
- package/build/lib/app-utils.d.ts +32 -52
- package/build/lib/app-utils.d.ts.map +1 -1
- package/build/lib/app-utils.js +327 -219
- package/build/lib/app-utils.js.map +1 -1
- package/build/lib/commands/app-management.d.ts +5 -4
- package/build/lib/commands/app-management.d.ts.map +1 -1
- package/build/lib/commands/app-management.js +14 -7
- package/build/lib/commands/app-management.js.map +1 -1
- package/build/lib/commands/app-strings.d.ts +5 -2
- package/build/lib/commands/app-strings.d.ts.map +1 -1
- package/build/lib/commands/app-strings.js +6 -3
- package/build/lib/commands/app-strings.js.map +1 -1
- package/build/lib/commands/file-movement.js +1 -1
- package/build/lib/commands/file-movement.js.map +1 -1
- package/build/lib/commands/types.d.ts +1 -0
- package/build/lib/commands/types.d.ts.map +1 -1
- package/build/lib/commands/xctest-record-screen.d.ts +1 -1
- package/build/lib/desired-caps.d.ts +2 -0
- package/build/lib/desired-caps.d.ts.map +1 -1
- package/build/lib/desired-caps.js +1 -0
- package/build/lib/desired-caps.js.map +1 -1
- package/build/lib/driver.d.ts +30 -27
- package/build/lib/driver.d.ts.map +1 -1
- package/build/lib/driver.js +12 -12
- package/build/lib/driver.js.map +1 -1
- package/build/lib/execute-method-map.d.ts +1 -1
- package/build/lib/execute-method-map.js +1 -1
- package/build/lib/execute-method-map.js.map +1 -1
- package/build/lib/ios-fs-helpers.d.ts +30 -15
- package/build/lib/ios-fs-helpers.d.ts.map +1 -1
- package/build/lib/ios-fs-helpers.js +54 -21
- package/build/lib/ios-fs-helpers.js.map +1 -1
- package/build/lib/real-device-management.d.ts +0 -5
- package/build/lib/real-device-management.d.ts.map +1 -1
- package/build/lib/real-device-management.js +8 -5
- package/build/lib/real-device-management.js.map +1 -1
- package/build/lib/real-device.d.ts +13 -9
- package/build/lib/real-device.d.ts.map +1 -1
- package/build/lib/real-device.js +49 -75
- package/build/lib/real-device.js.map +1 -1
- package/lib/app-infos-cache.js +159 -0
- package/lib/app-utils.js +357 -239
- package/lib/commands/app-management.js +20 -9
- package/lib/commands/app-strings.js +6 -3
- package/lib/commands/file-movement.js +1 -1
- package/lib/commands/types.ts +1 -0
- package/lib/desired-caps.js +1 -0
- package/lib/driver.js +12 -15
- package/lib/execute-method-map.ts +1 -1
- package/lib/ios-fs-helpers.js +57 -23
- package/lib/real-device-management.js +7 -5
- package/lib/real-device.js +62 -88
- package/npm-shrinkwrap.json +13 -5
- package/package.json +1 -1
package/build/lib/app-utils.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
* @
|
|
120
|
-
* @
|
|
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(
|
|
124
|
-
|
|
125
|
-
const supportedPlatforms = await
|
|
126
|
-
|
|
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
|
|
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 (
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
])
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
+
this.log.info(message);
|
|
174
112
|
return {};
|
|
175
113
|
}
|
|
176
|
-
let
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
196
|
-
|
|
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
|
-
|
|
201
|
-
logger_js_1.default.info(`Getting all the available strings from '${lprojRoot}'`);
|
|
137
|
+
this.log.debug(message);
|
|
202
138
|
}
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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}
|
|
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
|
|
422
|
+
* The name of the app bundle remains the same
|
|
360
423
|
*/
|
|
361
|
-
async function
|
|
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
|
|
364
|
-
await support_1.fs.mv(
|
|
365
|
-
return
|
|
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 {
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
*
|
|
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
|
|
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 {
|
|
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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
-
|
|
507
|
-
//
|
|
508
|
-
if (!isUrl &&
|
|
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:
|
|
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
|