@appium/base-driver 10.1.2 → 10.2.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/build/lib/basedriver/capabilities.d.ts.map +1 -1
- package/build/lib/basedriver/capabilities.js +7 -7
- package/build/lib/basedriver/capabilities.js.map +1 -1
- package/build/lib/basedriver/commands/event.d.ts +1 -1
- package/build/lib/basedriver/commands/event.d.ts.map +1 -1
- package/build/lib/basedriver/commands/execute.d.ts +1 -1
- package/build/lib/basedriver/commands/execute.d.ts.map +1 -1
- package/build/lib/basedriver/commands/find.d.ts +1 -1
- package/build/lib/basedriver/commands/find.d.ts.map +1 -1
- package/build/lib/basedriver/commands/mixin.d.ts +1 -1
- package/build/lib/basedriver/commands/mixin.d.ts.map +1 -1
- package/build/lib/basedriver/commands/timeout.d.ts +1 -1
- package/build/lib/basedriver/commands/timeout.d.ts.map +1 -1
- package/build/lib/basedriver/device-settings.d.ts +14 -23
- package/build/lib/basedriver/device-settings.d.ts.map +1 -1
- package/build/lib/basedriver/device-settings.js +11 -26
- package/build/lib/basedriver/device-settings.js.map +1 -1
- package/build/lib/basedriver/helpers.d.ts +36 -57
- package/build/lib/basedriver/helpers.d.ts.map +1 -1
- package/build/lib/basedriver/helpers.js +160 -248
- package/build/lib/basedriver/helpers.js.map +1 -1
- package/build/lib/basedriver/logger.d.ts +1 -2
- package/build/lib/basedriver/logger.d.ts.map +1 -1
- package/build/lib/basedriver/logger.js +2 -2
- package/build/lib/basedriver/logger.js.map +1 -1
- package/build/lib/basedriver/validation.d.ts.map +1 -1
- package/build/lib/basedriver/validation.js +3 -3
- package/build/lib/basedriver/validation.js.map +1 -1
- package/build/lib/constants.d.ts +1 -1
- package/build/lib/constants.d.ts.map +1 -1
- package/build/lib/express/crash.d.ts +8 -2
- package/build/lib/express/crash.d.ts.map +1 -1
- package/build/lib/express/crash.js +6 -0
- package/build/lib/express/crash.js.map +1 -1
- package/build/lib/express/express-logging.d.ts +12 -2
- package/build/lib/express/express-logging.d.ts.map +1 -1
- package/build/lib/express/express-logging.js +34 -26
- package/build/lib/express/express-logging.js.map +1 -1
- package/build/lib/express/idempotency.d.ts +4 -10
- package/build/lib/express/idempotency.d.ts.map +1 -1
- package/build/lib/express/idempotency.js +71 -75
- package/build/lib/express/idempotency.js.map +1 -1
- package/build/lib/express/logger.d.ts +1 -2
- package/build/lib/express/logger.d.ts.map +1 -1
- package/build/lib/express/logger.js +2 -2
- package/build/lib/express/logger.js.map +1 -1
- package/build/lib/express/middleware.d.ts +37 -41
- package/build/lib/express/middleware.d.ts.map +1 -1
- package/build/lib/express/middleware.js +48 -60
- package/build/lib/express/middleware.js.map +1 -1
- package/build/lib/express/server.d.ts +57 -101
- package/build/lib/express/server.d.ts.map +1 -1
- package/build/lib/express/server.js +55 -133
- package/build/lib/express/server.js.map +1 -1
- package/build/lib/express/static.d.ts +10 -5
- package/build/lib/express/static.d.ts.map +1 -1
- package/build/lib/express/static.js +33 -43
- package/build/lib/express/static.js.map +1 -1
- package/build/lib/express/websocket.d.ts +22 -6
- package/build/lib/express/websocket.d.ts.map +1 -1
- package/build/lib/express/websocket.js +10 -15
- package/build/lib/express/websocket.js.map +1 -1
- package/build/lib/helpers/capabilities.d.ts +4 -16
- package/build/lib/helpers/capabilities.d.ts.map +1 -1
- package/build/lib/helpers/capabilities.js +36 -48
- package/build/lib/helpers/capabilities.js.map +1 -1
- package/build/lib/jsonwp-proxy/proxy.d.ts +4 -2
- package/build/lib/jsonwp-proxy/proxy.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/proxy.js +9 -4
- package/build/lib/jsonwp-proxy/proxy.js.map +1 -1
- package/build/lib/jsonwp-status/status.d.ts +113 -158
- package/build/lib/jsonwp-status/status.d.ts.map +1 -1
- package/build/lib/jsonwp-status/status.js +10 -14
- package/build/lib/jsonwp-status/status.js.map +1 -1
- package/build/lib/protocol/bidi-commands.d.ts +31 -36
- package/build/lib/protocol/bidi-commands.d.ts.map +1 -1
- package/build/lib/protocol/bidi-commands.js +5 -5
- package/build/lib/protocol/bidi-commands.js.map +1 -1
- package/build/lib/protocol/errors.d.ts.map +1 -1
- package/build/lib/protocol/errors.js.map +1 -1
- package/build/lib/protocol/helpers.d.ts +7 -11
- package/build/lib/protocol/helpers.d.ts.map +1 -1
- package/build/lib/protocol/helpers.js +5 -9
- package/build/lib/protocol/helpers.js.map +1 -1
- package/build/lib/protocol/index.d.ts +4 -21
- package/build/lib/protocol/index.d.ts.map +1 -1
- package/build/lib/protocol/index.js.map +1 -1
- package/build/lib/protocol/protocol.d.ts +15 -1
- package/build/lib/protocol/protocol.d.ts.map +1 -1
- package/build/lib/protocol/protocol.js +50 -20
- package/build/lib/protocol/protocol.js.map +1 -1
- package/build/lib/protocol/routes.d.ts +8 -15
- package/build/lib/protocol/routes.d.ts.map +1 -1
- package/build/lib/protocol/routes.js +18 -33
- package/build/lib/protocol/routes.js.map +1 -1
- package/lib/basedriver/capabilities.ts +2 -2
- package/lib/basedriver/commands/event.ts +2 -2
- package/lib/basedriver/commands/execute.ts +2 -2
- package/lib/basedriver/commands/find.ts +2 -2
- package/lib/basedriver/commands/mixin.ts +1 -1
- package/lib/basedriver/commands/timeout.ts +2 -2
- package/lib/basedriver/{device-settings.js → device-settings.ts} +24 -35
- package/lib/basedriver/{helpers.js → helpers.ts} +215 -270
- package/lib/basedriver/logger.ts +3 -0
- package/lib/basedriver/validation.ts +2 -2
- package/lib/constants.ts +1 -1
- package/lib/express/crash.ts +15 -0
- package/lib/express/express-logging.ts +84 -0
- package/lib/express/{idempotency.js → idempotency.ts} +106 -90
- package/lib/express/logger.ts +3 -0
- package/lib/express/middleware.ts +187 -0
- package/lib/express/{server.js → server.ts} +177 -170
- package/lib/express/static.ts +77 -0
- package/lib/express/websocket.ts +81 -0
- package/lib/helpers/capabilities.ts +83 -0
- package/lib/jsonwp-proxy/proxy.js +7 -2
- package/lib/jsonwp-status/{status.js → status.ts} +12 -15
- package/lib/protocol/{bidi-commands.js → bidi-commands.ts} +7 -5
- package/lib/protocol/errors.ts +4 -4
- package/lib/protocol/{helpers.js → helpers.ts} +8 -11
- package/lib/protocol/protocol.ts +57 -26
- package/lib/protocol/{routes.js → routes.ts} +29 -40
- package/package.json +15 -15
- package/tsconfig.json +3 -1
- package/lib/basedriver/logger.js +0 -4
- package/lib/express/crash.js +0 -11
- package/lib/express/express-logging.js +0 -60
- package/lib/express/logger.js +0 -4
- package/lib/express/middleware.js +0 -171
- package/lib/express/static.js +0 -76
- package/lib/express/websocket.js +0 -79
- package/lib/helpers/capabilities.js +0 -93
- /package/lib/protocol/{index.js → index.ts} +0 -0
|
@@ -10,9 +10,8 @@ exports.duplicateKeys = duplicateKeys;
|
|
|
10
10
|
exports.parseCapsArray = parseCapsArray;
|
|
11
11
|
exports.generateDriverLogPrefix = generateDriverLogPrefix;
|
|
12
12
|
const lodash_1 = __importDefault(require("lodash"));
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const logger_1 = __importDefault(require("./logger"));
|
|
13
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
14
|
+
const logger_1 = require("./logger");
|
|
16
15
|
const support_1 = require("@appium/support");
|
|
17
16
|
const lru_cache_1 = require("lru-cache");
|
|
18
17
|
const async_lock_1 = __importDefault(require("async-lock"));
|
|
@@ -27,13 +26,12 @@ const DEFAULT_REQ_HEADERS = Object.freeze({
|
|
|
27
26
|
'user-agent': `Appium (BaseDriver v${exports.BASEDRIVER_VER})`,
|
|
28
27
|
});
|
|
29
28
|
const AVG_DOWNLOAD_SPEED_MEASUREMENT_THRESHOLD_SEC = 2;
|
|
30
|
-
/** @type {LRUCache<string, import('@appium/types').CachedAppInfo>} */
|
|
31
29
|
const APPLICATIONS_CACHE = new lru_cache_1.LRUCache({
|
|
32
30
|
max: MAX_CACHED_APPS,
|
|
33
31
|
ttl: CACHED_APPS_MAX_AGE_MS, // expire after 24 hours
|
|
34
32
|
updateAgeOnGet: true,
|
|
35
33
|
dispose: ({ fullPath }, app) => {
|
|
36
|
-
logger_1.
|
|
34
|
+
logger_1.log.info(`The application '${app}' cached at '${fullPath}' has ` +
|
|
37
35
|
`expired after ${CACHED_APPS_MAX_AGE_MS}ms`);
|
|
38
36
|
if (fullPath) {
|
|
39
37
|
support_1.fs.rimraf(fullPath);
|
|
@@ -50,40 +48,40 @@ process.on('exit', () => {
|
|
|
50
48
|
return;
|
|
51
49
|
}
|
|
52
50
|
const appPaths = [...APPLICATIONS_CACHE.values()].map(({ fullPath }) => fullPath);
|
|
53
|
-
logger_1.
|
|
54
|
-
support_1.util.pluralize('application', appPaths.length));
|
|
51
|
+
logger_1.log.debug(`Performing cleanup of ${support_1.util.pluralize('cached application', appPaths.length, true)}`);
|
|
55
52
|
for (const appPath of appPaths) {
|
|
53
|
+
if (!appPath) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
56
|
try {
|
|
57
|
-
// @ts-ignore it's defined
|
|
58
57
|
support_1.fs.rimrafSync(appPath);
|
|
59
58
|
}
|
|
60
59
|
catch (e) {
|
|
61
|
-
logger_1.
|
|
60
|
+
logger_1.log.warn(e.message);
|
|
62
61
|
}
|
|
63
62
|
}
|
|
64
63
|
});
|
|
65
64
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* - Manages caching logic
|
|
70
|
-
* - Downloads the app from a remote URL to the local filesystem
|
|
71
|
-
* - Determines package name
|
|
72
|
-
* - Checks basic requirements on the application package
|
|
65
|
+
* Performs initial application package configuration so the app is ready for driver use.
|
|
66
|
+
* Resolves local paths, downloads remote apps (http/https) with optional caching, and
|
|
67
|
+
* runs optional post-process or custom download hooks.
|
|
73
68
|
*
|
|
74
|
-
* @param
|
|
75
|
-
* @param
|
|
76
|
-
* @
|
|
69
|
+
* @param app - Path to a local app or URL of a downloadable app (http/https).
|
|
70
|
+
* @param options - Supported extensions and optional hooks. Either a single extension
|
|
71
|
+
* string, an array of extension strings, or {@link ConfigureAppOptions} (e.g.
|
|
72
|
+
* `supportedExtensions`, `onPostProcess`, `onDownload`).
|
|
73
|
+
* @returns Resolved path to the application (local path or path to downloaded/cached app).
|
|
74
|
+
* @throws {Error} If supported extensions are missing, the app path/URL is invalid, or download fails.
|
|
77
75
|
*/
|
|
78
|
-
async function configureApp(app, options =
|
|
76
|
+
async function configureApp(app, options = {}) {
|
|
79
77
|
if (!lodash_1.default.isString(app)) {
|
|
80
78
|
// immediately shortcircuit if not given an app
|
|
81
79
|
return '';
|
|
82
80
|
}
|
|
83
|
-
/** @type {string[]} */
|
|
84
81
|
let supportedAppExtensions;
|
|
85
|
-
const
|
|
86
|
-
const
|
|
82
|
+
const opts = !lodash_1.default.isString(options) && !lodash_1.default.isArray(options) ? options : undefined;
|
|
83
|
+
const onPostProcess = opts?.onPostProcess;
|
|
84
|
+
const onDownload = opts?.onDownload;
|
|
87
85
|
if (lodash_1.default.isString(options)) {
|
|
88
86
|
supportedAppExtensions = [options];
|
|
89
87
|
}
|
|
@@ -91,18 +89,18 @@ async function configureApp(app, options = /** @type {import('@appium/types').Co
|
|
|
91
89
|
supportedAppExtensions = options;
|
|
92
90
|
}
|
|
93
91
|
else if (lodash_1.default.isPlainObject(options)) {
|
|
94
|
-
supportedAppExtensions = options.supportedExtensions;
|
|
92
|
+
supportedAppExtensions = options.supportedExtensions ?? [];
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
supportedAppExtensions = [];
|
|
95
96
|
}
|
|
96
|
-
// @ts-ignore this is OK
|
|
97
97
|
if (lodash_1.default.isEmpty(supportedAppExtensions)) {
|
|
98
98
|
throw new Error(`One or more supported app extensions must be provided`);
|
|
99
99
|
}
|
|
100
100
|
let newApp = app;
|
|
101
101
|
const originalAppLink = app;
|
|
102
102
|
let packageHash = null;
|
|
103
|
-
/** @type {import('axios').AxiosResponse['headers']|undefined} */
|
|
104
103
|
let headers;
|
|
105
|
-
/** @type {RemoteAppProps} */
|
|
106
104
|
const remoteAppProps = {
|
|
107
105
|
lastModified: null,
|
|
108
106
|
immutable: false,
|
|
@@ -111,9 +109,9 @@ async function configureApp(app, options = /** @type {import('@appium/types').Co
|
|
|
111
109
|
};
|
|
112
110
|
const { protocol, pathname } = parseAppLink(app);
|
|
113
111
|
const isUrl = isSupportedUrl(app);
|
|
114
|
-
if (!isUrl && !
|
|
115
|
-
newApp =
|
|
116
|
-
logger_1.
|
|
112
|
+
if (!isUrl && !node_path_1.default.isAbsolute(newApp)) {
|
|
113
|
+
newApp = node_path_1.default.resolve(process.cwd(), newApp);
|
|
114
|
+
logger_1.log.warn(`The current application path '${app}' is not absolute ` +
|
|
117
115
|
`and has been rewritten to '${newApp}'. Consider using absolute paths rather than relative`);
|
|
118
116
|
app = newApp;
|
|
119
117
|
}
|
|
@@ -121,64 +119,68 @@ async function configureApp(app, options = /** @type {import('@appium/types').Co
|
|
|
121
119
|
return await APPLICATIONS_CACHE_GUARD.acquire(appCacheKey, async () => {
|
|
122
120
|
const cachedAppInfo = APPLICATIONS_CACHE.get(appCacheKey);
|
|
123
121
|
if (cachedAppInfo) {
|
|
124
|
-
logger_1.
|
|
122
|
+
logger_1.log.debug(`Cached app data: ${JSON.stringify(cachedAppInfo, null, 2)}`);
|
|
125
123
|
}
|
|
126
124
|
if (isUrl) {
|
|
127
125
|
// Use the app from remote URL
|
|
128
|
-
logger_1.
|
|
129
|
-
const reqHeaders = {
|
|
130
|
-
...DEFAULT_REQ_HEADERS,
|
|
131
|
-
};
|
|
126
|
+
logger_1.log.info(`Using downloadable app '${newApp}'`);
|
|
127
|
+
const reqHeaders = { ...DEFAULT_REQ_HEADERS };
|
|
132
128
|
if (cachedAppInfo?.etag) {
|
|
133
129
|
reqHeaders['if-none-match'] = cachedAppInfo.etag;
|
|
134
130
|
}
|
|
135
131
|
else if (cachedAppInfo?.lastModified) {
|
|
136
132
|
reqHeaders['if-modified-since'] = cachedAppInfo.lastModified.toUTCString();
|
|
137
133
|
}
|
|
138
|
-
logger_1.
|
|
139
|
-
let
|
|
140
|
-
|
|
134
|
+
logger_1.log.debug(`Request headers: ${JSON.stringify(reqHeaders)}`);
|
|
135
|
+
let result = await queryAppLink(newApp, reqHeaders);
|
|
136
|
+
headers = result.headers;
|
|
137
|
+
let { stream, status } = result;
|
|
138
|
+
logger_1.log.debug(`Response status: ${status}`);
|
|
141
139
|
try {
|
|
142
140
|
if (!lodash_1.default.isEmpty(headers)) {
|
|
143
141
|
if (headers.etag) {
|
|
144
|
-
logger_1.
|
|
142
|
+
logger_1.log.debug(`Etag: ${headers.etag}`);
|
|
145
143
|
remoteAppProps.etag = headers.etag;
|
|
146
144
|
}
|
|
147
145
|
if (headers['last-modified']) {
|
|
148
|
-
logger_1.
|
|
146
|
+
logger_1.log.debug(`Last-Modified: ${headers['last-modified']}`);
|
|
149
147
|
remoteAppProps.lastModified = new Date(headers['last-modified']);
|
|
150
148
|
}
|
|
151
149
|
if (headers['cache-control']) {
|
|
152
|
-
logger_1.
|
|
153
|
-
remoteAppProps.immutable = /\bimmutable\b/i.test(headers['cache-control']);
|
|
154
|
-
const maxAgeMatch = /\bmax-age=(\d+)\b/i.exec(headers['cache-control']);
|
|
150
|
+
logger_1.log.debug(`Cache-Control: ${headers['cache-control']}`);
|
|
151
|
+
remoteAppProps.immutable = /\bimmutable\b/i.test(String(headers['cache-control']));
|
|
152
|
+
const maxAgeMatch = /\bmax-age=(\d+)\b/i.exec(String(headers['cache-control']));
|
|
155
153
|
if (maxAgeMatch) {
|
|
156
154
|
remoteAppProps.maxAge = parseInt(maxAgeMatch[1], 10);
|
|
157
155
|
}
|
|
158
156
|
}
|
|
159
157
|
}
|
|
160
158
|
if (cachedAppInfo && status === HTTP_STATUS_NOT_MODIFIED) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
159
|
+
const cachedPath = cachedAppInfo.fullPath ?? '';
|
|
160
|
+
if (cachedPath && (await isAppIntegrityOk(cachedPath, cachedAppInfo.integrity))) {
|
|
161
|
+
logger_1.log.info(`Reusing previously downloaded application at '${cachedPath}'`);
|
|
162
|
+
return verifyAppExtension(cachedPath, supportedAppExtensions);
|
|
164
163
|
}
|
|
165
|
-
logger_1.
|
|
164
|
+
logger_1.log.info(`The application at '${cachedAppInfo.fullPath}' does not exist anymore ` +
|
|
166
165
|
`or its integrity has been damaged. Deleting it from the internal cache`);
|
|
167
166
|
APPLICATIONS_CACHE.delete(appCacheKey);
|
|
168
167
|
if (!stream.closed) {
|
|
169
168
|
stream.destroy();
|
|
170
169
|
}
|
|
171
|
-
|
|
170
|
+
result = await queryAppLink(newApp, { ...DEFAULT_REQ_HEADERS });
|
|
171
|
+
stream = result.stream;
|
|
172
|
+
headers = result.headers;
|
|
173
|
+
status = result.status;
|
|
172
174
|
}
|
|
173
175
|
if (onDownload) {
|
|
174
176
|
newApp = await onDownload({
|
|
175
177
|
url: originalAppLink,
|
|
176
|
-
headers:
|
|
178
|
+
headers: lodash_1.default.clone(headers),
|
|
177
179
|
stream,
|
|
178
180
|
});
|
|
179
181
|
}
|
|
180
182
|
else {
|
|
181
|
-
const fileName = determineFilename(headers, pathname, supportedAppExtensions);
|
|
183
|
+
const fileName = determineFilename(headers, pathname ?? '', supportedAppExtensions);
|
|
182
184
|
newApp = await fetchApp(stream, await support_1.tempDir.path({
|
|
183
185
|
prefix: fileName,
|
|
184
186
|
suffix: '',
|
|
@@ -193,7 +195,7 @@ async function configureApp(app, options = /** @type {import('@appium/types').Co
|
|
|
193
195
|
}
|
|
194
196
|
else if (await support_1.fs.exists(newApp)) {
|
|
195
197
|
// Use the local app
|
|
196
|
-
logger_1.
|
|
198
|
+
logger_1.log.info(`Using local app '${newApp}'`);
|
|
197
199
|
}
|
|
198
200
|
else {
|
|
199
201
|
let errorMessage = `The application at '${newApp}' does not exist or is not accessible`;
|
|
@@ -209,7 +211,6 @@ async function configureApp(app, options = /** @type {import('@appium/types').Co
|
|
|
209
211
|
if (isPackageAFile) {
|
|
210
212
|
packageHash = await calculateFileIntegrity(newApp);
|
|
211
213
|
}
|
|
212
|
-
/** @type {(appPathToCache: string) => Promise<string>} */
|
|
213
214
|
const storeAppInCache = async (appPathToCache) => {
|
|
214
215
|
const cachedFullPath = cachedAppInfo?.fullPath;
|
|
215
216
|
if (cachedFullPath && cachedFullPath !== appPathToCache) {
|
|
@@ -232,14 +233,14 @@ async function configureApp(app, options = /** @type {import('@appium/types').Co
|
|
|
232
233
|
return appPathToCache;
|
|
233
234
|
};
|
|
234
235
|
if (lodash_1.default.isFunction(onPostProcess)) {
|
|
235
|
-
const
|
|
236
|
-
/** @type {import('@appium/types').PostProcessOptions<import('axios').AxiosResponseHeaders>} */ ({
|
|
236
|
+
const postProcessArg = {
|
|
237
237
|
cachedAppInfo: lodash_1.default.clone(cachedAppInfo),
|
|
238
238
|
isUrl,
|
|
239
239
|
originalAppLink,
|
|
240
240
|
headers: lodash_1.default.clone(headers),
|
|
241
241
|
appPath: newApp,
|
|
242
|
-
}
|
|
242
|
+
};
|
|
243
|
+
const result = await onPostProcess(postProcessArg);
|
|
243
244
|
return !result?.appPath || app === result?.appPath || !(await support_1.fs.exists(result?.appPath))
|
|
244
245
|
? newApp
|
|
245
246
|
: await storeAppInCache(result.appPath);
|
|
@@ -251,21 +252,23 @@ async function configureApp(app, options = /** @type {import('@appium/types').Co
|
|
|
251
252
|
});
|
|
252
253
|
}
|
|
253
254
|
/**
|
|
254
|
-
*
|
|
255
|
-
*
|
|
255
|
+
* Returns whether the given string looks like a package or bundle identifier
|
|
256
|
+
* (e.g. `com.example.app` or `org.company.AnotherApp`).
|
|
257
|
+
*
|
|
258
|
+
* @param app - Value to check (e.g. app path or bundle id).
|
|
259
|
+
* @returns `true` if the value matches a dot-separated identifier pattern.
|
|
256
260
|
*/
|
|
257
261
|
function isPackageOrBundle(app) {
|
|
258
262
|
return /^([a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+)+$/.test(app);
|
|
259
263
|
}
|
|
260
264
|
/**
|
|
261
|
-
*
|
|
262
|
-
*
|
|
265
|
+
* Recursively ensures both keys exist with the same value in objects and arrays.
|
|
266
|
+
* For each object, if `firstKey` exists its value is also set at `secondKey`, and vice versa.
|
|
263
267
|
*
|
|
264
|
-
*
|
|
265
|
-
|
|
266
|
-
* @param
|
|
267
|
-
* @
|
|
268
|
-
* @param {String} secondKey The second key to duplicate
|
|
268
|
+
* @param input - Object, array, or primitive to process (arrays/objects traversed recursively).
|
|
269
|
+
* @param firstKey - First key name to mirror.
|
|
270
|
+
* @param secondKey - Second key name to mirror.
|
|
271
|
+
* @returns A deep copy of `input` with both keys present where objects had either key.
|
|
269
272
|
*/
|
|
270
273
|
function duplicateKeys(input, firstKey, secondKey) {
|
|
271
274
|
// If array provided, recursively call on all elements
|
|
@@ -275,7 +278,7 @@ function duplicateKeys(input, firstKey, secondKey) {
|
|
|
275
278
|
// If object, create duplicates for keys and then recursively call on values
|
|
276
279
|
if (lodash_1.default.isPlainObject(input)) {
|
|
277
280
|
const resultObj = {};
|
|
278
|
-
for (
|
|
281
|
+
for (const [key, value] of lodash_1.default.toPairs(input)) {
|
|
279
282
|
const recursivelyCalledValue = duplicateKeys(value, firstKey, secondKey);
|
|
280
283
|
if (key === firstKey) {
|
|
281
284
|
resultObj[secondKey] = recursivelyCalledValue;
|
|
@@ -291,11 +294,12 @@ function duplicateKeys(input, firstKey, secondKey) {
|
|
|
291
294
|
return input;
|
|
292
295
|
}
|
|
293
296
|
/**
|
|
294
|
-
*
|
|
295
|
-
*
|
|
297
|
+
* Normalizes a capability value to a string array. If already an array, returns it;
|
|
298
|
+
* if a string, parses as JSON array when possible, otherwise returns a single-element array.
|
|
296
299
|
*
|
|
297
|
-
* @param
|
|
298
|
-
* @returns
|
|
300
|
+
* @param capValue - Capability value: string (including JSON array like `"[\"a\",\"b\"]"`) or string[].
|
|
301
|
+
* @returns Array of strings.
|
|
302
|
+
* @throws {TypeError} If value is not a string/array or JSON parsing fails for array-like input.
|
|
299
303
|
*/
|
|
300
304
|
function parseCapsArray(capValue) {
|
|
301
305
|
if (lodash_1.default.isArray(capValue)) {
|
|
@@ -312,7 +316,7 @@ function parseCapsArray(capValue) {
|
|
|
312
316
|
if (lodash_1.default.isString(capValue) && lodash_1.default.startsWith(lodash_1.default.trimStart(capValue), '[')) {
|
|
313
317
|
throw new TypeError(message);
|
|
314
318
|
}
|
|
315
|
-
logger_1.
|
|
319
|
+
logger_1.log.warn(message);
|
|
316
320
|
}
|
|
317
321
|
if (lodash_1.default.isString(capValue)) {
|
|
318
322
|
return [capValue];
|
|
@@ -320,36 +324,77 @@ function parseCapsArray(capValue) {
|
|
|
320
324
|
throw new TypeError(`Expected a string or a valid JSON array; received '${capValue}'`);
|
|
321
325
|
}
|
|
322
326
|
/**
|
|
323
|
-
*
|
|
327
|
+
* Builds a short log prefix for a driver instance (e.g. `UiAutomator2@a1b2`).
|
|
324
328
|
*
|
|
325
|
-
* @param
|
|
326
|
-
* @param
|
|
327
|
-
*
|
|
328
|
-
* @returns {string}
|
|
329
|
+
* @param obj - Driver or other object; its constructor name and a short id are used.
|
|
330
|
+
* @param _sessionId - Deprecated and unused; kept for {@link DriverHelpers} interface compatibility.
|
|
331
|
+
* @returns Prefix string like `DriverName@xxxx`, or `UnknownDriver@????` if `obj` is null.
|
|
329
332
|
*/
|
|
330
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
331
|
-
function generateDriverLogPrefix(obj,
|
|
333
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- DriverHelpers interface
|
|
334
|
+
function generateDriverLogPrefix(obj, _sessionId) {
|
|
335
|
+
if (!obj) {
|
|
336
|
+
// This should not happen
|
|
337
|
+
return 'UnknownDriver@????';
|
|
338
|
+
}
|
|
332
339
|
return `${obj.constructor.name}@${support_1.node.getObjectId(obj).substring(0, 4)}`;
|
|
333
340
|
}
|
|
341
|
+
function parseAppLink(appLink) {
|
|
342
|
+
try {
|
|
343
|
+
return new URL(appLink);
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return {};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function isEnvOptionEnabled(optionName, defaultValue = null) {
|
|
350
|
+
const value = process.env[optionName];
|
|
351
|
+
if (!lodash_1.default.isNull(defaultValue) && lodash_1.default.isEmpty(value)) {
|
|
352
|
+
return defaultValue;
|
|
353
|
+
}
|
|
354
|
+
return !lodash_1.default.isEmpty(value) && !['0', 'false', 'no'].includes(lodash_1.default.toLower(value));
|
|
355
|
+
}
|
|
356
|
+
function isSupportedUrl(app) {
|
|
357
|
+
try {
|
|
358
|
+
const { protocol } = parseAppLink(app);
|
|
359
|
+
return ['http:', 'https:'].includes(protocol ?? '');
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
334
365
|
/**
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
* @param {string} appLink The URL to download an app from
|
|
339
|
-
* @param {import('axios').RawAxiosRequestHeaders} reqHeaders Additional HTTP request headers
|
|
340
|
-
* @returns {Promise<RemoteAppData>}
|
|
366
|
+
* Transforms the given app link to the cache key.
|
|
367
|
+
* Necessary to properly cache apps having the same address but different query strings,
|
|
368
|
+
* e.g. ones stored in S3 using presigned URLs.
|
|
341
369
|
*/
|
|
370
|
+
function toCacheKey(app) {
|
|
371
|
+
if (!isEnvOptionEnabled('APPIUM_APPS_CACHE_IGNORE_URL_QUERY') || !isSupportedUrl(app)) {
|
|
372
|
+
return app;
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
const parsed = parseAppLink(app);
|
|
376
|
+
const href = 'href' in parsed ? parsed.href : undefined;
|
|
377
|
+
const search = 'search' in parsed ? parsed.search : undefined;
|
|
378
|
+
if (href && search) {
|
|
379
|
+
return href.replace(search, '');
|
|
380
|
+
}
|
|
381
|
+
if (href) {
|
|
382
|
+
return href;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
// ignore
|
|
387
|
+
}
|
|
388
|
+
return app;
|
|
389
|
+
}
|
|
342
390
|
async function queryAppLink(appLink, reqHeaders) {
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
} : undefined;
|
|
350
|
-
/**
|
|
351
|
-
* @type {import('axios').RawAxiosRequestConfig}
|
|
352
|
-
*/
|
|
391
|
+
const url = new URL(appLink);
|
|
392
|
+
// Extract credentials, then remove them from the URL for axios
|
|
393
|
+
const { username, password } = url;
|
|
394
|
+
url.username = '';
|
|
395
|
+
url.password = '';
|
|
396
|
+
const axiosUrl = url.href;
|
|
397
|
+
const axiosAuth = username ? { username, password } : undefined;
|
|
353
398
|
const requestOpts = {
|
|
354
399
|
url: axiosUrl,
|
|
355
400
|
auth: axiosAuth,
|
|
@@ -360,24 +405,12 @@ async function queryAppLink(appLink, reqHeaders) {
|
|
|
360
405
|
};
|
|
361
406
|
try {
|
|
362
407
|
const { data: stream, headers, status } = await (0, axios_1.default)(requestOpts);
|
|
363
|
-
return {
|
|
364
|
-
stream,
|
|
365
|
-
headers,
|
|
366
|
-
status,
|
|
367
|
-
};
|
|
408
|
+
return { stream, headers, status };
|
|
368
409
|
}
|
|
369
410
|
catch (err) {
|
|
370
411
|
throw new Error(`Cannot download the app from ${axiosUrl}: ${err.message}`);
|
|
371
412
|
}
|
|
372
413
|
}
|
|
373
|
-
/**
|
|
374
|
-
* Retrieves app payload from the given stream. Also meters the download performance.
|
|
375
|
-
*
|
|
376
|
-
* @param {import('stream').Readable} srcStream The incoming stream
|
|
377
|
-
* @param {string} dstPath The target file path to be written
|
|
378
|
-
* @returns {Promise<string>} The same dstPath
|
|
379
|
-
* @throws {Error} If there was a failure while downloading the file
|
|
380
|
-
*/
|
|
381
414
|
async function fetchApp(srcStream, dstPath) {
|
|
382
415
|
const timer = new support_1.timing.Timer().start();
|
|
383
416
|
try {
|
|
@@ -385,7 +418,7 @@ async function fetchApp(srcStream, dstPath) {
|
|
|
385
418
|
srcStream.pipe(writer);
|
|
386
419
|
await new bluebird_1.default((resolve, reject) => {
|
|
387
420
|
srcStream.once('error', reject);
|
|
388
|
-
writer.once('finish', resolve);
|
|
421
|
+
writer.once('finish', () => resolve());
|
|
389
422
|
writer.once('error', (e) => {
|
|
390
423
|
srcStream.unpipe(writer);
|
|
391
424
|
reject(e);
|
|
@@ -397,78 +430,25 @@ async function fetchApp(srcStream, dstPath) {
|
|
|
397
430
|
}
|
|
398
431
|
const secondsElapsed = timer.getDuration().asSeconds;
|
|
399
432
|
const { size } = await support_1.fs.stat(dstPath);
|
|
400
|
-
logger_1.
|
|
433
|
+
logger_1.log.debug(`The application (${support_1.util.toReadableSizeString(size)}) ` +
|
|
401
434
|
`has been downloaded to '${dstPath}' in ${secondsElapsed.toFixed(3)}s`);
|
|
402
435
|
// it does not make much sense to approximate the speed for short downloads
|
|
403
436
|
if (secondsElapsed >= AVG_DOWNLOAD_SPEED_MEASUREMENT_THRESHOLD_SEC) {
|
|
404
437
|
const bytesPerSec = Math.floor(size / secondsElapsed);
|
|
405
|
-
logger_1.
|
|
438
|
+
logger_1.log.debug(`Approximate download speed: ${support_1.util.toReadableSizeString(bytesPerSec)}/s`);
|
|
406
439
|
}
|
|
407
440
|
return dstPath;
|
|
408
441
|
}
|
|
409
|
-
/**
|
|
410
|
-
* Transforms the given app link to the cache key.
|
|
411
|
-
* This is necessary to properly cache apps
|
|
412
|
-
* having the same address, but different query strings,
|
|
413
|
-
* for example ones stored in S3 using presigned URLs.
|
|
414
|
-
*
|
|
415
|
-
* @param {string} app App link.
|
|
416
|
-
* @returns {string} Transformed app link or the original arg if
|
|
417
|
-
* no transformation is needed.
|
|
418
|
-
*/
|
|
419
|
-
function toCacheKey(app) {
|
|
420
|
-
if (!isEnvOptionEnabled('APPIUM_APPS_CACHE_IGNORE_URL_QUERY') || !isSupportedUrl(app)) {
|
|
421
|
-
return app;
|
|
422
|
-
}
|
|
423
|
-
try {
|
|
424
|
-
const { href, search } = parseAppLink(app);
|
|
425
|
-
if (href && search) {
|
|
426
|
-
return href.replace(search, '');
|
|
427
|
-
}
|
|
428
|
-
if (href) {
|
|
429
|
-
return href;
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
catch { }
|
|
433
|
-
return app;
|
|
434
|
-
}
|
|
435
|
-
/**
|
|
436
|
-
* Safely parses the given app link to a URL object
|
|
437
|
-
*
|
|
438
|
-
* @param {string} appLink
|
|
439
|
-
* @returns {URL|import('@appium/types').StringRecord} Parsed URL object
|
|
440
|
-
* or an empty object if the parsing has failed
|
|
441
|
-
*/
|
|
442
|
-
function parseAppLink(appLink) {
|
|
443
|
-
try {
|
|
444
|
-
return new URL(appLink);
|
|
445
|
-
}
|
|
446
|
-
catch {
|
|
447
|
-
return {};
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
/**
|
|
451
|
-
* Tries to determine the file name of the payload that is going
|
|
452
|
-
* to be downloaded from an URL
|
|
453
|
-
*
|
|
454
|
-
* @param {import('axios').RawAxiosRequestHeaders} headers
|
|
455
|
-
* @param {string} pathname
|
|
456
|
-
* @param {string[]} supportedAppExtensions
|
|
457
|
-
* @returns {string}
|
|
458
|
-
*/
|
|
459
442
|
function determineFilename(headers, pathname, supportedAppExtensions) {
|
|
460
|
-
const basename = support_1.fs.sanitizeName(
|
|
443
|
+
const basename = support_1.fs.sanitizeName(node_path_1.default.basename(decodeURIComponent(pathname ?? '')), {
|
|
461
444
|
replacement: SANITIZE_REPLACEMENT,
|
|
462
445
|
});
|
|
463
|
-
const extname =
|
|
464
|
-
if (headers['content-disposition'] && /^attachment/i.test(
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const match = /filename="([^"]+)/i.exec(/** @type {string} */ (headers['content-disposition']));
|
|
446
|
+
const extname = node_path_1.default.extname(basename);
|
|
447
|
+
if (headers['content-disposition'] && /^attachment/i.test(String(headers['content-disposition']))) {
|
|
448
|
+
logger_1.log.debug(`Content-Disposition: ${headers['content-disposition']}`);
|
|
449
|
+
const match = /filename="([^"]+)/i.exec(String(headers['content-disposition']));
|
|
468
450
|
if (match) {
|
|
469
|
-
return support_1.fs.sanitizeName(match[1], {
|
|
470
|
-
replacement: SANITIZE_REPLACEMENT,
|
|
471
|
-
});
|
|
451
|
+
return support_1.fs.sanitizeName(match[1], { replacement: SANITIZE_REPLACEMENT });
|
|
472
452
|
}
|
|
473
453
|
}
|
|
474
454
|
// assign the default file name and the extension if none has been detected
|
|
@@ -477,88 +457,26 @@ function determineFilename(headers, pathname, supportedAppExtensions) {
|
|
|
477
457
|
: DEFAULT_BASENAME;
|
|
478
458
|
let resultingExt = extname;
|
|
479
459
|
if (!supportedAppExtensions.map(lodash_1.default.toLower).includes(lodash_1.default.toLower(resultingExt))) {
|
|
480
|
-
logger_1.
|
|
460
|
+
logger_1.log.info(`The current file extension '${resultingExt}' is not supported. ` +
|
|
481
461
|
`Defaulting to '${lodash_1.default.first(supportedAppExtensions)}'`);
|
|
482
|
-
resultingExt =
|
|
462
|
+
resultingExt = lodash_1.default.first(supportedAppExtensions);
|
|
483
463
|
}
|
|
484
464
|
return `${resultingName}${resultingExt}`;
|
|
485
465
|
}
|
|
486
|
-
/**
|
|
487
|
-
* Checks whether we can threat the given app link
|
|
488
|
-
* as a URL,
|
|
489
|
-
*
|
|
490
|
-
* @param {string} app
|
|
491
|
-
* @returns {boolean} True if app is a supported URL
|
|
492
|
-
*/
|
|
493
|
-
function isSupportedUrl(app) {
|
|
494
|
-
try {
|
|
495
|
-
const { protocol } = parseAppLink(app);
|
|
496
|
-
return ['http:', 'https:'].includes(protocol);
|
|
497
|
-
}
|
|
498
|
-
catch {
|
|
499
|
-
return false;
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
/**
|
|
503
|
-
* Check if the given environment option is enabled
|
|
504
|
-
*
|
|
505
|
-
* @param {string} optionName Option name
|
|
506
|
-
* @param {boolean|null} [defaultValue=null] The value to return if the given env value
|
|
507
|
-
* is not set explicitly
|
|
508
|
-
* @returns {boolean} True if the option is enabled
|
|
509
|
-
*/
|
|
510
|
-
function isEnvOptionEnabled(optionName, defaultValue = null) {
|
|
511
|
-
const value = process.env[optionName];
|
|
512
|
-
if (!lodash_1.default.isNull(defaultValue) && lodash_1.default.isEmpty(value)) {
|
|
513
|
-
return defaultValue;
|
|
514
|
-
}
|
|
515
|
-
return !lodash_1.default.isEmpty(value) && !['0', 'false', 'no'].includes(lodash_1.default.toLower(value));
|
|
516
|
-
}
|
|
517
|
-
/**
|
|
518
|
-
*
|
|
519
|
-
* @param {string} [envVarName]
|
|
520
|
-
* @param {number} defaultValue
|
|
521
|
-
* @returns {number}
|
|
522
|
-
*/
|
|
523
|
-
function toNaturalNumber(defaultValue, envVarName) {
|
|
524
|
-
if (!envVarName || lodash_1.default.isUndefined(process.env[envVarName])) {
|
|
525
|
-
return defaultValue;
|
|
526
|
-
}
|
|
527
|
-
const num = parseInt(`${process.env[envVarName]}`, 10);
|
|
528
|
-
return num > 0 ? num : defaultValue;
|
|
529
|
-
}
|
|
530
|
-
/**
|
|
531
|
-
* @param {string} app
|
|
532
|
-
* @param {string[]} supportedAppExtensions
|
|
533
|
-
* @returns {string}
|
|
534
|
-
*/
|
|
535
466
|
function verifyAppExtension(app, supportedAppExtensions) {
|
|
536
|
-
if (supportedAppExtensions.map(lodash_1.default.toLower).includes(lodash_1.default.toLower(
|
|
467
|
+
if (supportedAppExtensions.map(lodash_1.default.toLower).includes(lodash_1.default.toLower(node_path_1.default.extname(app)))) {
|
|
537
468
|
return app;
|
|
538
469
|
}
|
|
539
470
|
throw new Error(`New app path '${app}' did not have ` +
|
|
540
471
|
`${support_1.util.pluralize('extension', supportedAppExtensions.length, false)}: ` +
|
|
541
472
|
supportedAppExtensions);
|
|
542
473
|
}
|
|
543
|
-
/**
|
|
544
|
-
* @param {string} folderPath
|
|
545
|
-
* @returns {Promise<number>}
|
|
546
|
-
*/
|
|
547
474
|
async function calculateFolderIntegrity(folderPath) {
|
|
548
475
|
return (await support_1.fs.glob('**/*', { cwd: folderPath })).length;
|
|
549
476
|
}
|
|
550
|
-
/**
|
|
551
|
-
* @param {string} filePath
|
|
552
|
-
* @returns {Promise<string>}
|
|
553
|
-
*/
|
|
554
477
|
async function calculateFileIntegrity(filePath) {
|
|
555
478
|
return await support_1.fs.hash(filePath);
|
|
556
479
|
}
|
|
557
|
-
/**
|
|
558
|
-
* @param {string} currentPath
|
|
559
|
-
* @param {import('@appium/types').StringRecord} expectedIntegrity
|
|
560
|
-
* @returns {Promise<boolean>}
|
|
561
|
-
*/
|
|
562
480
|
async function isAppIntegrityOk(currentPath, expectedIntegrity = {}) {
|
|
563
481
|
if (!(await support_1.fs.exists(currentPath))) {
|
|
564
482
|
return false;
|
|
@@ -571,10 +489,17 @@ async function isAppIntegrityOk(currentPath, expectedIntegrity = {}) {
|
|
|
571
489
|
// more precise, but we don't need to be very precise here and also don't want to
|
|
572
490
|
// overuse RAM and have a performance drop.
|
|
573
491
|
return (await support_1.fs.stat(currentPath)).isDirectory()
|
|
574
|
-
? (await calculateFolderIntegrity(currentPath)) >= expectedIntegrity?.folder
|
|
492
|
+
? (await calculateFolderIntegrity(currentPath)) >= (expectedIntegrity?.folder ?? 0)
|
|
575
493
|
: (await calculateFileIntegrity(currentPath)) === expectedIntegrity?.file;
|
|
576
494
|
}
|
|
577
|
-
|
|
495
|
+
function toNaturalNumber(defaultValue, envVarName) {
|
|
496
|
+
if (!envVarName || lodash_1.default.isUndefined(process.env[envVarName])) {
|
|
497
|
+
return defaultValue;
|
|
498
|
+
}
|
|
499
|
+
const num = parseInt(`${process.env[envVarName]}`, 10);
|
|
500
|
+
return num > 0 ? num : defaultValue;
|
|
501
|
+
}
|
|
502
|
+
// #endregion
|
|
578
503
|
exports.default = {
|
|
579
504
|
configureApp,
|
|
580
505
|
isPackageOrBundle,
|
|
@@ -582,17 +507,4 @@ exports.default = {
|
|
|
582
507
|
parseCapsArray,
|
|
583
508
|
generateDriverLogPrefix,
|
|
584
509
|
};
|
|
585
|
-
/**
|
|
586
|
-
* @typedef RemoteAppProps
|
|
587
|
-
* @property {Date?} lastModified
|
|
588
|
-
* @property {boolean} immutable
|
|
589
|
-
* @property {number?} maxAge
|
|
590
|
-
* @property {string?} etag
|
|
591
|
-
*/
|
|
592
|
-
/**
|
|
593
|
-
* @typedef RemoteAppData Properties of the remote application (e.g. GET HTTP response) to be downloaded.
|
|
594
|
-
* @property {number} status The HTTP status of the response
|
|
595
|
-
* @property {import('stream').Readable} stream The HTTP response body represented as readable stream
|
|
596
|
-
* @property {import('axios').RawAxiosResponseHeaders | import('axios').AxiosResponseHeaders} headers HTTP response headers
|
|
597
|
-
*/
|
|
598
510
|
//# sourceMappingURL=helpers.js.map
|