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