@appium/base-driver 9.3.2 → 9.3.4
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/LICENSE +1 -1
- package/build/lib/basedriver/capabilities.d.ts +59 -36
- package/build/lib/basedriver/capabilities.d.ts.map +1 -1
- package/build/lib/basedriver/capabilities.js +57 -45
- package/build/lib/basedriver/capabilities.js.map +1 -1
- package/build/lib/basedriver/commands/event.d.ts +5 -9
- package/build/lib/basedriver/commands/event.d.ts.map +1 -1
- package/build/lib/basedriver/commands/event.js +28 -49
- package/build/lib/basedriver/commands/event.js.map +1 -1
- package/build/lib/basedriver/commands/execute.d.ts +5 -11
- package/build/lib/basedriver/commands/execute.d.ts.map +1 -1
- package/build/lib/basedriver/commands/execute.js +15 -39
- package/build/lib/basedriver/commands/execute.js.map +1 -1
- package/build/lib/basedriver/commands/find.d.ts +5 -12
- package/build/lib/basedriver/commands/find.d.ts.map +1 -1
- package/build/lib/basedriver/commands/find.js +38 -98
- package/build/lib/basedriver/commands/find.js.map +1 -1
- package/build/lib/basedriver/commands/index.d.ts +7 -3
- package/build/lib/basedriver/commands/index.d.ts.map +1 -1
- package/build/lib/basedriver/commands/index.js +7 -28
- package/build/lib/basedriver/commands/index.js.map +1 -1
- package/build/lib/basedriver/commands/log.d.ts +5 -10
- package/build/lib/basedriver/commands/log.d.ts.map +1 -1
- package/build/lib/basedriver/commands/log.js +17 -50
- package/build/lib/basedriver/commands/log.js.map +1 -1
- package/build/lib/basedriver/commands/mixin.d.ts +12 -0
- package/build/lib/basedriver/commands/mixin.d.ts.map +1 -0
- package/build/lib/basedriver/commands/mixin.js +18 -0
- package/build/lib/basedriver/commands/mixin.js.map +1 -0
- package/build/lib/basedriver/commands/session.d.ts +5 -11
- package/build/lib/basedriver/commands/session.d.ts.map +1 -1
- package/build/lib/basedriver/commands/session.js +18 -53
- package/build/lib/basedriver/commands/session.js.map +1 -1
- package/build/lib/basedriver/commands/settings.d.ts +5 -9
- package/build/lib/basedriver/commands/settings.d.ts.map +1 -1
- package/build/lib/basedriver/commands/settings.js +14 -34
- package/build/lib/basedriver/commands/settings.js.map +1 -1
- package/build/lib/basedriver/commands/timeout.d.ts +5 -9
- package/build/lib/basedriver/commands/timeout.d.ts.map +1 -1
- package/build/lib/basedriver/commands/timeout.js +107 -129
- package/build/lib/basedriver/commands/timeout.js.map +1 -1
- package/build/lib/basedriver/core.d.ts +14 -20
- package/build/lib/basedriver/core.d.ts.map +1 -1
- package/build/lib/basedriver/core.js +32 -22
- package/build/lib/basedriver/core.js.map +1 -1
- package/build/lib/basedriver/device-settings.d.ts +11 -11
- package/build/lib/basedriver/device-settings.d.ts.map +1 -1
- package/build/lib/basedriver/device-settings.js +7 -8
- package/build/lib/basedriver/device-settings.js.map +1 -1
- package/build/lib/basedriver/driver.d.ts +23 -108
- package/build/lib/basedriver/driver.d.ts.map +1 -1
- package/build/lib/basedriver/driver.js +38 -135
- package/build/lib/basedriver/driver.js.map +1 -1
- package/build/lib/basedriver/helpers.d.ts +21 -98
- package/build/lib/basedriver/helpers.d.ts.map +1 -1
- package/build/lib/basedriver/helpers.js +178 -182
- package/build/lib/basedriver/helpers.js.map +1 -1
- package/build/lib/express/server.d.ts +3 -15
- package/build/lib/express/server.d.ts.map +1 -1
- package/build/lib/express/server.js +4 -2
- package/build/lib/express/server.js.map +1 -1
- package/build/lib/express/websocket.d.ts +5 -44
- package/build/lib/express/websocket.d.ts.map +1 -1
- package/build/lib/express/websocket.js +10 -39
- package/build/lib/express/websocket.js.map +1 -1
- package/build/lib/helpers/capabilities.d.ts +2 -2
- package/build/lib/helpers/capabilities.d.ts.map +1 -1
- package/build/lib/helpers/capabilities.js +2 -3
- package/build/lib/helpers/capabilities.js.map +1 -1
- package/build/lib/protocol/protocol.d.ts +1 -1
- package/build/lib/protocol/protocol.d.ts.map +1 -1
- package/build/lib/protocol/protocol.js +10 -2
- package/build/lib/protocol/protocol.js.map +1 -1
- package/build/lib/protocol/routes.d.ts +1 -0
- package/build/lib/protocol/routes.d.ts.map +1 -1
- package/build/lib/protocol/routes.js +12 -10
- package/build/lib/protocol/routes.js.map +1 -1
- package/lib/basedriver/capabilities.js +70 -56
- package/lib/basedriver/commands/event.ts +49 -0
- package/lib/basedriver/commands/execute.ts +40 -0
- package/lib/basedriver/commands/find.ts +80 -0
- package/lib/basedriver/commands/index.ts +7 -0
- package/lib/basedriver/commands/log.ts +34 -0
- package/lib/basedriver/commands/mixin.ts +15 -0
- package/lib/basedriver/commands/session.ts +36 -0
- package/lib/basedriver/commands/settings.ts +26 -0
- package/lib/basedriver/commands/timeout.ts +155 -0
- package/lib/basedriver/core.js +11 -28
- package/lib/basedriver/device-settings.js +9 -11
- package/lib/basedriver/{driver.js → driver.ts} +71 -180
- package/lib/basedriver/helpers.js +214 -212
- package/lib/express/server.js +4 -2
- package/lib/express/websocket.js +10 -39
- package/lib/helpers/capabilities.js +2 -3
- package/lib/protocol/protocol.js +11 -2
- package/lib/protocol/routes.js +12 -13
- package/package.json +11 -7
- package/lib/basedriver/commands/event.js +0 -63
- package/lib/basedriver/commands/execute.js +0 -45
- package/lib/basedriver/commands/find.js +0 -108
- package/lib/basedriver/commands/index.js +0 -35
- package/lib/basedriver/commands/log.js +0 -64
- package/lib/basedriver/commands/session.js +0 -57
- package/lib/basedriver/commands/settings.js +0 -38
- package/lib/basedriver/commands/timeout.js +0 -168
|
@@ -1,112 +1,34 @@
|
|
|
1
|
-
declare const _default: import(
|
|
1
|
+
declare const _default: import('@appium/types').DriverHelpers;
|
|
2
2
|
export default _default;
|
|
3
|
-
export type
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* - immutable: Optional boolean value. Contains true if the file has an `immutable` mark
|
|
9
|
-
* in `Cache-control` header
|
|
10
|
-
* - maxAge: Optional integer representation of `maxAge` parameter in `Cache-control` header
|
|
11
|
-
* - timestamp: The timestamp this item has been added to the cache (measured in Unix epoch
|
|
12
|
-
* milliseconds)
|
|
13
|
-
* - integrity: An object containing either `file` property with SHA1 hash of the file
|
|
14
|
-
* or `folder` property with total amount of cached files and subfolders
|
|
15
|
-
* - fullPath: the full path to the cached app
|
|
16
|
-
*/
|
|
17
|
-
cachedAppInfo: any | null;
|
|
18
|
-
/**
|
|
19
|
-
* Whether the app has been downloaded from a remote URL
|
|
20
|
-
*/
|
|
21
|
-
isUrl: boolean;
|
|
22
|
-
/**
|
|
23
|
-
* Optional headers object. Only present if `isUrl` is true and if the server
|
|
24
|
-
* responds to HEAD requests. All header names are normalized to lowercase.
|
|
25
|
-
*/
|
|
26
|
-
headers: any | null;
|
|
27
|
-
/**
|
|
28
|
-
* A string containing full path to the preprocessed application package (either
|
|
29
|
-
* downloaded or a local one)
|
|
30
|
-
*/
|
|
31
|
-
appPath: string;
|
|
3
|
+
export type RemoteAppProps = {
|
|
4
|
+
lastModified: Date | null;
|
|
5
|
+
immutable: boolean;
|
|
6
|
+
maxAge: number | null;
|
|
7
|
+
etag: string | null;
|
|
32
8
|
};
|
|
33
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Properties of the remote application (e.g. GET HTTP response) to be downloaded.
|
|
11
|
+
*/
|
|
12
|
+
export type RemoteAppData = {
|
|
34
13
|
/**
|
|
35
|
-
* The
|
|
36
|
-
* local file system (might be a file or a folder path)
|
|
14
|
+
* The HTTP status of the response
|
|
37
15
|
*/
|
|
38
|
-
|
|
39
|
-
};
|
|
40
|
-
export type ConfigureAppOptions = {
|
|
16
|
+
status: number;
|
|
41
17
|
/**
|
|
42
|
-
*
|
|
43
|
-
* to the application after it is downloaded/preprocessed. This function may be async
|
|
44
|
-
* and is expected to accept single object parameter.
|
|
45
|
-
* The function is expected to either return a falsy value, which means the app must not be
|
|
46
|
-
* cached and a fresh copy of it is downloaded each time. If this function returns an object
|
|
47
|
-
* containing `appPath` property then the integrity of it will be verified and stored into
|
|
48
|
-
* the cache.
|
|
18
|
+
* The HTTP response body represented as readable stream
|
|
49
19
|
*/
|
|
50
|
-
|
|
20
|
+
stream: import('stream').Readable;
|
|
51
21
|
/**
|
|
52
|
-
*
|
|
53
|
-
* including starting dots). This property is mandatory and must not be empty.
|
|
22
|
+
* HTTP response headers
|
|
54
23
|
*/
|
|
55
|
-
|
|
56
|
-
};
|
|
57
|
-
export type RemoteAppProps = {
|
|
58
|
-
lastModified: Date | null;
|
|
59
|
-
immutable: boolean;
|
|
60
|
-
maxAge: number | null;
|
|
24
|
+
headers: import('axios').RawAxiosResponseHeaders | import('axios').AxiosResponseHeaders;
|
|
61
25
|
};
|
|
62
26
|
/**
|
|
63
|
-
* @typedef PostProcessOptions
|
|
64
|
-
* @property {?Object} cachedAppInfo The information about the previously cached app instance (if exists):
|
|
65
|
-
* - packageHash: SHA1 hash of the package if it is a file and not a folder
|
|
66
|
-
* - lastModified: Optional Date instance, the value of file's `Last-Modified` header
|
|
67
|
-
* - immutable: Optional boolean value. Contains true if the file has an `immutable` mark
|
|
68
|
-
* in `Cache-control` header
|
|
69
|
-
* - maxAge: Optional integer representation of `maxAge` parameter in `Cache-control` header
|
|
70
|
-
* - timestamp: The timestamp this item has been added to the cache (measured in Unix epoch
|
|
71
|
-
* milliseconds)
|
|
72
|
-
* - integrity: An object containing either `file` property with SHA1 hash of the file
|
|
73
|
-
* or `folder` property with total amount of cached files and subfolders
|
|
74
|
-
* - fullPath: the full path to the cached app
|
|
75
|
-
* @property {boolean} isUrl Whether the app has been downloaded from a remote URL
|
|
76
|
-
* @property {?Object} headers Optional headers object. Only present if `isUrl` is true and if the server
|
|
77
|
-
* responds to HEAD requests. All header names are normalized to lowercase.
|
|
78
|
-
* @property {string} appPath A string containing full path to the preprocessed application package (either
|
|
79
|
-
* downloaded or a local one)
|
|
80
|
-
*/
|
|
81
|
-
/**
|
|
82
|
-
* @typedef PostProcessResult
|
|
83
|
-
* @property {string} appPath The full past to the post-processed application package on the
|
|
84
|
-
* local file system (might be a file or a folder path)
|
|
85
|
-
*/
|
|
86
|
-
/**
|
|
87
|
-
* @typedef ConfigureAppOptions
|
|
88
|
-
* @property {(obj: PostProcessOptions) => (Promise<PostProcessResult|undefined>|PostProcessResult|undefined)} [onPostProcess]
|
|
89
|
-
* Optional function, which should be applied
|
|
90
|
-
* to the application after it is downloaded/preprocessed. This function may be async
|
|
91
|
-
* and is expected to accept single object parameter.
|
|
92
|
-
* The function is expected to either return a falsy value, which means the app must not be
|
|
93
|
-
* cached and a fresh copy of it is downloaded each time. If this function returns an object
|
|
94
|
-
* containing `appPath` property then the integrity of it will be verified and stored into
|
|
95
|
-
* the cache.
|
|
96
|
-
* @property {string[]} supportedExtensions List of supported application extensions (
|
|
97
|
-
* including starting dots). This property is mandatory and must not be empty.
|
|
98
|
-
*/
|
|
99
|
-
/**
|
|
100
|
-
* Prepares an app to be used in an automated test. The app gets cached automatically
|
|
101
|
-
* if it is an archive or if it is downloaded from an URL.
|
|
102
|
-
* If the downloaded app has `.zip` extension, this method will unzip it.
|
|
103
|
-
* The unzip does not work when `onPostProcess` is provided.
|
|
104
27
|
*
|
|
105
|
-
* @param {string} app
|
|
106
|
-
* @param {string|string[]|ConfigureAppOptions} options
|
|
107
|
-
* @returns The full path to the resulting application bundle
|
|
28
|
+
* @param {string} app
|
|
29
|
+
* @param {string|string[]|import('@appium/types').ConfigureAppOptions} options
|
|
108
30
|
*/
|
|
109
|
-
export function configureApp(app: string, options?: string | string[] | ConfigureAppOptions): Promise<any>;
|
|
31
|
+
export function configureApp(app: string, options?: string | string[] | import('@appium/types').ConfigureAppOptions): Promise<any>;
|
|
110
32
|
export function isPackageOrBundle(app: any): boolean;
|
|
111
33
|
/**
|
|
112
34
|
* Finds all instances 'firstKey' and create a duplicate with the key 'secondKey',
|
|
@@ -133,5 +55,6 @@ export function parseCapsArray(cap: string | Array<string>): any[];
|
|
|
133
55
|
* @param {string?} sessionId session identifier (if exists)
|
|
134
56
|
* @returns {string}
|
|
135
57
|
*/
|
|
136
|
-
export function generateDriverLogPrefix(obj: import(
|
|
58
|
+
export function generateDriverLogPrefix(obj: import("@appium/types").Core<any, import("@appium/types").StringRecord<any>>, sessionId?: string | null): string;
|
|
59
|
+
export const BASEDRIVER_VER: string;
|
|
137
60
|
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../lib/basedriver/helpers.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../lib/basedriver/helpers.js"],"names":[],"mappings":"wBA2kBW,OAAO,eAAe,EAAE,aAAa;;;kBAmBlC,IAAI;eACJ,OAAO;YACP,MAAM;UACN,MAAM;;;;;;;;;YAKN,MAAM;;;;YACN,OAAO,QAAQ,EAAE,QAAQ;;;;aACzB,OAAO,OAAO,EAAE,uBAAuB,GAAG,OAAO,OAAO,EAAE,oBAAoB;;AAtgB5F;;;;GAIG;AACH,kCAHW,MAAM,YACN,MAAM,GAAC,MAAM,EAAE,GAAC,OAAO,eAAe,EAAE,mBAAmB,gBA6PrE;AA4JD,qDAEC;AAED;;;;;;;;;GASG;AACH,oFAuBC;AAED;;;;;GAKG;AACH,oCAFW,MAAM,GAAC,aAAa,SAoB9B;AAED;;;;;;GAMG;AACH,uIAHW,MAAM,UACJ,MAAM,CAKlB"}
|
|
@@ -3,7 +3,7 @@ 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.generateDriverLogPrefix = exports.parseCapsArray = exports.duplicateKeys = exports.isPackageOrBundle = exports.configureApp = void 0;
|
|
6
|
+
exports.BASEDRIVER_VER = exports.generateDriverLogPrefix = exports.parseCapsArray = exports.duplicateKeys = exports.isPackageOrBundle = exports.configureApp = void 0;
|
|
7
7
|
const lodash_1 = __importDefault(require("lodash"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const url_1 = __importDefault(require("url"));
|
|
@@ -12,11 +12,20 @@ const support_1 = require("@appium/support");
|
|
|
12
12
|
const lru_cache_1 = __importDefault(require("lru-cache"));
|
|
13
13
|
const async_lock_1 = __importDefault(require("async-lock"));
|
|
14
14
|
const axios_1 = __importDefault(require("axios"));
|
|
15
|
+
const bluebird_1 = __importDefault(require("bluebird"));
|
|
16
|
+
// for compat with running tests transpiled and in-place
|
|
17
|
+
const { version: BASEDRIVER_VER } = support_1.fs.readPackageJsonFrom(__dirname);
|
|
18
|
+
exports.BASEDRIVER_VER = BASEDRIVER_VER;
|
|
15
19
|
const IPA_EXT = '.ipa';
|
|
16
|
-
const ZIP_EXTS = ['.zip', IPA_EXT];
|
|
20
|
+
const ZIP_EXTS = new Set(['.zip', IPA_EXT]);
|
|
17
21
|
const ZIP_MIME_TYPES = ['application/zip', 'application/x-zip-compressed', 'multipart/x-zip'];
|
|
18
22
|
const CACHED_APPS_MAX_AGE = 1000 * 60 * 60 * 24; // ms
|
|
19
23
|
const MAX_CACHED_APPS = 1024;
|
|
24
|
+
const HTTP_STATUS_NOT_MODIFIED = 304;
|
|
25
|
+
const DEFAULT_REQ_HEADERS = Object.freeze({
|
|
26
|
+
'user-agent': `Appium (BaseDriver v${BASEDRIVER_VER})`,
|
|
27
|
+
});
|
|
28
|
+
const AVG_DOWNLOAD_SPEED_MEASUREMENT_THRESHOLD_SEC = 2;
|
|
20
29
|
const APPLICATIONS_CACHE = new lru_cache_1.default({
|
|
21
30
|
max: MAX_CACHED_APPS,
|
|
22
31
|
ttl: CACHED_APPS_MAX_AGE,
|
|
@@ -51,65 +60,6 @@ process.on('exit', () => {
|
|
|
51
60
|
}
|
|
52
61
|
}
|
|
53
62
|
});
|
|
54
|
-
/**
|
|
55
|
-
*
|
|
56
|
-
* @param {string} url
|
|
57
|
-
* @returns {Promise<import('axios').AxiosResponse['headers']>}
|
|
58
|
-
*/
|
|
59
|
-
async function retrieveHeaders(url) {
|
|
60
|
-
try {
|
|
61
|
-
return (await (0, axios_1.default)({
|
|
62
|
-
url,
|
|
63
|
-
method: 'HEAD',
|
|
64
|
-
timeout: 5000,
|
|
65
|
-
})).headers;
|
|
66
|
-
}
|
|
67
|
-
catch (e) {
|
|
68
|
-
logger_1.default.info(`Cannot send HEAD request to '${url}'. Original error: ${e.message}`);
|
|
69
|
-
}
|
|
70
|
-
return {};
|
|
71
|
-
}
|
|
72
|
-
function getCachedApplicationPath(link, currentAppProps = {}, cachedAppInfo = {}) {
|
|
73
|
-
const refresh = () => {
|
|
74
|
-
logger_1.default.debug(`A fresh copy of the application is going to be downloaded from ${link}`);
|
|
75
|
-
return null;
|
|
76
|
-
};
|
|
77
|
-
if (!lodash_1.default.isPlainObject(cachedAppInfo) || !lodash_1.default.isPlainObject(currentAppProps)) {
|
|
78
|
-
// if an invalid arg is passed then assume cache miss
|
|
79
|
-
return refresh();
|
|
80
|
-
}
|
|
81
|
-
const { lastModified: currentModified, immutable: currentImmutable,
|
|
82
|
-
// maxAge is in seconds
|
|
83
|
-
maxAge: currentMaxAge, } = currentAppProps;
|
|
84
|
-
const {
|
|
85
|
-
// Date instance
|
|
86
|
-
lastModified,
|
|
87
|
-
// boolean
|
|
88
|
-
immutable,
|
|
89
|
-
// Unix time in milliseconds
|
|
90
|
-
timestamp, fullPath, } = cachedAppInfo;
|
|
91
|
-
if (lastModified && currentModified) {
|
|
92
|
-
if (currentModified.getTime() <= lastModified.getTime()) {
|
|
93
|
-
logger_1.default.debug(`The application at ${link} has not been modified since ${lastModified}`);
|
|
94
|
-
return fullPath;
|
|
95
|
-
}
|
|
96
|
-
logger_1.default.debug(`The application at ${link} has been modified since ${lastModified}`);
|
|
97
|
-
return refresh();
|
|
98
|
-
}
|
|
99
|
-
if (immutable && currentImmutable) {
|
|
100
|
-
logger_1.default.debug(`The application at ${link} is immutable`);
|
|
101
|
-
return fullPath;
|
|
102
|
-
}
|
|
103
|
-
if (currentMaxAge && timestamp) {
|
|
104
|
-
const msLeft = timestamp + currentMaxAge * 1000 - Date.now();
|
|
105
|
-
if (msLeft > 0) {
|
|
106
|
-
logger_1.default.debug(`The cached application '${path_1.default.basename(fullPath)}' will expire in ${msLeft / 1000}s`);
|
|
107
|
-
return fullPath;
|
|
108
|
-
}
|
|
109
|
-
logger_1.default.debug(`The cached application '${path_1.default.basename(fullPath)}' has expired`);
|
|
110
|
-
}
|
|
111
|
-
return refresh();
|
|
112
|
-
}
|
|
113
63
|
function verifyAppExtension(app, supportedAppExtensions) {
|
|
114
64
|
if (supportedAppExtensions.map(lodash_1.default.toLower).includes(lodash_1.default.toLower(path_1.default.extname(app)))) {
|
|
115
65
|
return app;
|
|
@@ -140,53 +90,11 @@ async function isAppIntegrityOk(currentPath, expectedIntegrity = {}) {
|
|
|
140
90
|
: (await calculateFileIntegrity(currentPath)) === expectedIntegrity?.file;
|
|
141
91
|
}
|
|
142
92
|
/**
|
|
143
|
-
* @typedef PostProcessOptions
|
|
144
|
-
* @property {?Object} cachedAppInfo The information about the previously cached app instance (if exists):
|
|
145
|
-
* - packageHash: SHA1 hash of the package if it is a file and not a folder
|
|
146
|
-
* - lastModified: Optional Date instance, the value of file's `Last-Modified` header
|
|
147
|
-
* - immutable: Optional boolean value. Contains true if the file has an `immutable` mark
|
|
148
|
-
* in `Cache-control` header
|
|
149
|
-
* - maxAge: Optional integer representation of `maxAge` parameter in `Cache-control` header
|
|
150
|
-
* - timestamp: The timestamp this item has been added to the cache (measured in Unix epoch
|
|
151
|
-
* milliseconds)
|
|
152
|
-
* - integrity: An object containing either `file` property with SHA1 hash of the file
|
|
153
|
-
* or `folder` property with total amount of cached files and subfolders
|
|
154
|
-
* - fullPath: the full path to the cached app
|
|
155
|
-
* @property {boolean} isUrl Whether the app has been downloaded from a remote URL
|
|
156
|
-
* @property {?Object} headers Optional headers object. Only present if `isUrl` is true and if the server
|
|
157
|
-
* responds to HEAD requests. All header names are normalized to lowercase.
|
|
158
|
-
* @property {string} appPath A string containing full path to the preprocessed application package (either
|
|
159
|
-
* downloaded or a local one)
|
|
160
|
-
*/
|
|
161
|
-
/**
|
|
162
|
-
* @typedef PostProcessResult
|
|
163
|
-
* @property {string} appPath The full past to the post-processed application package on the
|
|
164
|
-
* local file system (might be a file or a folder path)
|
|
165
|
-
*/
|
|
166
|
-
/**
|
|
167
|
-
* @typedef ConfigureAppOptions
|
|
168
|
-
* @property {(obj: PostProcessOptions) => (Promise<PostProcessResult|undefined>|PostProcessResult|undefined)} [onPostProcess]
|
|
169
|
-
* Optional function, which should be applied
|
|
170
|
-
* to the application after it is downloaded/preprocessed. This function may be async
|
|
171
|
-
* and is expected to accept single object parameter.
|
|
172
|
-
* The function is expected to either return a falsy value, which means the app must not be
|
|
173
|
-
* cached and a fresh copy of it is downloaded each time. If this function returns an object
|
|
174
|
-
* containing `appPath` property then the integrity of it will be verified and stored into
|
|
175
|
-
* the cache.
|
|
176
|
-
* @property {string[]} supportedExtensions List of supported application extensions (
|
|
177
|
-
* including starting dots). This property is mandatory and must not be empty.
|
|
178
|
-
*/
|
|
179
|
-
/**
|
|
180
|
-
* Prepares an app to be used in an automated test. The app gets cached automatically
|
|
181
|
-
* if it is an archive or if it is downloaded from an URL.
|
|
182
|
-
* If the downloaded app has `.zip` extension, this method will unzip it.
|
|
183
|
-
* The unzip does not work when `onPostProcess` is provided.
|
|
184
93
|
*
|
|
185
|
-
* @param {string} app
|
|
186
|
-
* @param {string|string[]|ConfigureAppOptions} options
|
|
187
|
-
* @returns The full path to the resulting application bundle
|
|
94
|
+
* @param {string} app
|
|
95
|
+
* @param {string|string[]|import('@appium/types').ConfigureAppOptions} options
|
|
188
96
|
*/
|
|
189
|
-
async function configureApp(app, options = /** @type {ConfigureAppOptions} */ ({})) {
|
|
97
|
+
async function configureApp(app, options = /** @type {import('@appium/types').ConfigureAppOptions} */ ({})) {
|
|
190
98
|
if (!lodash_1.default.isString(app)) {
|
|
191
99
|
// immediately shortcircuit if not given an app
|
|
192
100
|
return;
|
|
@@ -208,13 +116,14 @@ async function configureApp(app, options = /** @type {ConfigureAppOptions} */ ({
|
|
|
208
116
|
let newApp = app;
|
|
209
117
|
let shouldUnzipApp = false;
|
|
210
118
|
let packageHash = null;
|
|
211
|
-
/** @type {import('axios').AxiosResponse['headers']
|
|
212
|
-
let headers =
|
|
119
|
+
/** @type {import('axios').AxiosResponse['headers']|undefined} */
|
|
120
|
+
let headers = undefined;
|
|
213
121
|
/** @type {RemoteAppProps} */
|
|
214
122
|
const remoteAppProps = {
|
|
215
123
|
lastModified: null,
|
|
216
124
|
immutable: false,
|
|
217
125
|
maxAge: null,
|
|
126
|
+
etag: null,
|
|
218
127
|
};
|
|
219
128
|
const { protocol, pathname } = url_1.default.parse(newApp);
|
|
220
129
|
const isUrl = protocol === null ? false : ['http:', 'https:'].includes(protocol);
|
|
@@ -223,86 +132,109 @@ async function configureApp(app, options = /** @type {ConfigureAppOptions} */ ({
|
|
|
223
132
|
if (isUrl) {
|
|
224
133
|
// Use the app from remote URL
|
|
225
134
|
logger_1.default.info(`Using downloadable app '${newApp}'`);
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
135
|
+
const reqHeaders = {
|
|
136
|
+
...DEFAULT_REQ_HEADERS,
|
|
137
|
+
};
|
|
138
|
+
if (cachedAppInfo?.etag) {
|
|
139
|
+
reqHeaders['if-none-match'] = remoteAppProps.etag;
|
|
140
|
+
}
|
|
141
|
+
else if (cachedAppInfo?.lastModified) {
|
|
142
|
+
reqHeaders['if-modified-since'] = remoteAppProps.lastModified?.toString();
|
|
143
|
+
}
|
|
144
|
+
let { headers, stream, status } = await queryAppLink(newApp, reqHeaders);
|
|
145
|
+
try {
|
|
146
|
+
if (!lodash_1.default.isEmpty(headers)) {
|
|
147
|
+
logger_1.default.debug(`Etag: ${remoteAppProps?.etag} -> ${headers.etag}`);
|
|
148
|
+
if (headers.etag) {
|
|
149
|
+
remoteAppProps.etag = headers.etag;
|
|
150
|
+
}
|
|
151
|
+
logger_1.default.debug(`Last-Modified: ${remoteAppProps?.['last-modified']} -> ${headers['last-modified']}`);
|
|
152
|
+
if (headers['last-modified']) {
|
|
153
|
+
remoteAppProps.lastModified = new Date(headers['last-modified']);
|
|
154
|
+
}
|
|
155
|
+
logger_1.default.debug(`Cache-Control: ${remoteAppProps?.['cache-control']} -> ${headers['cache-control']}`);
|
|
156
|
+
if (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']);
|
|
159
|
+
if (maxAgeMatch) {
|
|
160
|
+
remoteAppProps.maxAge = parseInt(maxAgeMatch[1], 10);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
230
163
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
164
|
+
if (cachedAppInfo && status === HTTP_STATUS_NOT_MODIFIED) {
|
|
165
|
+
if (await isAppIntegrityOk(cachedAppInfo.fullPath, cachedAppInfo.integrity)) {
|
|
166
|
+
logger_1.default.info(`Reusing previously downloaded application at '${cachedAppInfo.fullPath}'`);
|
|
167
|
+
return verifyAppExtension(cachedAppInfo.fullPath, supportedAppExtensions);
|
|
168
|
+
}
|
|
169
|
+
logger_1.default.info(`The application at '${cachedAppInfo.fullPath}' does not exist anymore ` +
|
|
170
|
+
`or its integrity has been damaged. Deleting it from the internal cache`);
|
|
171
|
+
APPLICATIONS_CACHE.delete(app);
|
|
172
|
+
if (!stream.closed) {
|
|
173
|
+
stream.destroy();
|
|
237
174
|
}
|
|
175
|
+
({ stream, headers, status } = await queryAppLink(newApp, { ...DEFAULT_REQ_HEADERS }));
|
|
238
176
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
177
|
+
let fileName = null;
|
|
178
|
+
const basename = support_1.fs.sanitizeName(path_1.default.basename(decodeURIComponent(pathname ?? '')), {
|
|
179
|
+
replacement: SANITIZE_REPLACEMENT,
|
|
180
|
+
});
|
|
181
|
+
const extname = path_1.default.extname(basename);
|
|
182
|
+
// to determine if we need to unzip the app, we have a number of places
|
|
183
|
+
// to look: content type, content disposition, or the file extension
|
|
184
|
+
if (ZIP_EXTS.has(extname)) {
|
|
185
|
+
fileName = basename;
|
|
186
|
+
shouldUnzipApp = true;
|
|
246
187
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
// to determine if we need to unzip the app, we have a number of places
|
|
257
|
-
// to look: content type, content disposition, or the file extension
|
|
258
|
-
if (ZIP_EXTS.includes(extname)) {
|
|
259
|
-
fileName = basename;
|
|
260
|
-
shouldUnzipApp = true;
|
|
261
|
-
}
|
|
262
|
-
if (headers['content-type']) {
|
|
263
|
-
const ct = headers['content-type'];
|
|
264
|
-
logger_1.default.debug(`Content-Type: ${ct}`);
|
|
265
|
-
// the filetype may not be obvious for certain urls, so check the mime type too
|
|
266
|
-
if (ZIP_MIME_TYPES.some((mimeType) => new RegExp(`\\b${lodash_1.default.escapeRegExp(mimeType)}\\b`).test(ct))) {
|
|
267
|
-
if (!fileName) {
|
|
268
|
-
fileName = `${DEFAULT_BASENAME}.zip`;
|
|
188
|
+
if (headers['content-type']) {
|
|
189
|
+
const ct = headers['content-type'];
|
|
190
|
+
logger_1.default.debug(`Content-Type: ${ct}`);
|
|
191
|
+
// the filetype may not be obvious for certain urls, so check the mime type too
|
|
192
|
+
if (ZIP_MIME_TYPES.some((mimeType) => new RegExp(`\\b${lodash_1.default.escapeRegExp(mimeType)}\\b`).test(ct))) {
|
|
193
|
+
if (!fileName) {
|
|
194
|
+
fileName = `${DEFAULT_BASENAME}.zip`;
|
|
195
|
+
}
|
|
196
|
+
shouldUnzipApp = true;
|
|
269
197
|
}
|
|
270
|
-
shouldUnzipApp = true;
|
|
271
198
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
199
|
+
if (headers['content-disposition'] && /^attachment/i.test(headers['content-disposition'])) {
|
|
200
|
+
logger_1.default.debug(`Content-Disposition: ${headers['content-disposition']}`);
|
|
201
|
+
const match = /filename="([^"]+)/i.exec(headers['content-disposition']);
|
|
202
|
+
if (match) {
|
|
203
|
+
fileName = support_1.fs.sanitizeName(match[1], {
|
|
204
|
+
replacement: SANITIZE_REPLACEMENT,
|
|
205
|
+
});
|
|
206
|
+
shouldUnzipApp = shouldUnzipApp || ZIP_EXTS.has(path_1.default.extname(fileName));
|
|
207
|
+
}
|
|
281
208
|
}
|
|
209
|
+
if (!fileName) {
|
|
210
|
+
// assign the default file name and the extension if none has been detected
|
|
211
|
+
const resultingName = basename
|
|
212
|
+
? basename.substring(0, basename.length - extname.length)
|
|
213
|
+
: DEFAULT_BASENAME;
|
|
214
|
+
let resultingExt = extname;
|
|
215
|
+
if (!supportedAppExtensions.includes(resultingExt)) {
|
|
216
|
+
logger_1.default.info(`The current file extension '${resultingExt}' is not supported. ` +
|
|
217
|
+
`Defaulting to '${lodash_1.default.first(supportedAppExtensions)}'`);
|
|
218
|
+
resultingExt = /** @type {string} */ (lodash_1.default.first(supportedAppExtensions));
|
|
219
|
+
}
|
|
220
|
+
fileName = `${resultingName}${resultingExt}`;
|
|
221
|
+
}
|
|
222
|
+
const targetPath = await support_1.tempDir.path({
|
|
223
|
+
prefix: fileName,
|
|
224
|
+
suffix: '',
|
|
225
|
+
});
|
|
226
|
+
newApp = await fetchApp(stream, targetPath);
|
|
282
227
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
? basename.substring(0, basename.length - extname.length)
|
|
287
|
-
: DEFAULT_BASENAME;
|
|
288
|
-
let resultingExt = extname;
|
|
289
|
-
if (!supportedAppExtensions.includes(resultingExt)) {
|
|
290
|
-
logger_1.default.info(`The current file extension '${resultingExt}' is not supported. ` +
|
|
291
|
-
`Defaulting to '${lodash_1.default.first(supportedAppExtensions)}'`);
|
|
292
|
-
resultingExt = /** @type {string} */ (lodash_1.default.first(supportedAppExtensions));
|
|
228
|
+
finally {
|
|
229
|
+
if (!stream.closed) {
|
|
230
|
+
stream.destroy();
|
|
293
231
|
}
|
|
294
|
-
fileName = `${resultingName}${resultingExt}`;
|
|
295
232
|
}
|
|
296
|
-
const targetPath = await support_1.tempDir.path({
|
|
297
|
-
prefix: fileName,
|
|
298
|
-
suffix: '',
|
|
299
|
-
});
|
|
300
|
-
newApp = await downloadApp(newApp, targetPath);
|
|
301
233
|
}
|
|
302
234
|
else if (await support_1.fs.exists(newApp)) {
|
|
303
235
|
// Use the local app
|
|
304
236
|
logger_1.default.info(`Using local app '${newApp}'`);
|
|
305
|
-
shouldUnzipApp = ZIP_EXTS.
|
|
237
|
+
shouldUnzipApp = ZIP_EXTS.has(path_1.default.extname(newApp));
|
|
306
238
|
}
|
|
307
239
|
else {
|
|
308
240
|
let errorMessage = `The application at '${newApp}' does not exist or is not accessible`;
|
|
@@ -372,12 +304,13 @@ async function configureApp(app, options = /** @type {ConfigureAppOptions} */ ({
|
|
|
372
304
|
return appPathToCache;
|
|
373
305
|
};
|
|
374
306
|
if (lodash_1.default.isFunction(onPostProcess)) {
|
|
375
|
-
const result = await onPostProcess(
|
|
307
|
+
const result = await onPostProcess(
|
|
308
|
+
/** @type {import('@appium/types').PostProcessOptions<import('axios').AxiosResponseHeaders>} */ ({
|
|
376
309
|
cachedAppInfo: lodash_1.default.clone(cachedAppInfo),
|
|
377
310
|
isUrl,
|
|
378
311
|
headers: lodash_1.default.clone(headers),
|
|
379
312
|
appPath: newApp,
|
|
380
|
-
});
|
|
313
|
+
}));
|
|
381
314
|
return !result?.appPath || app === result?.appPath || !(await support_1.fs.exists(result?.appPath))
|
|
382
315
|
? newApp
|
|
383
316
|
: await storeAppInCache(result.appPath);
|
|
@@ -389,17 +322,73 @@ async function configureApp(app, options = /** @type {ConfigureAppOptions} */ ({
|
|
|
389
322
|
});
|
|
390
323
|
}
|
|
391
324
|
exports.configureApp = configureApp;
|
|
392
|
-
|
|
393
|
-
|
|
325
|
+
/**
|
|
326
|
+
* Sends a HTTP GET query to fetch the app with caching enabled.
|
|
327
|
+
* Follows https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching
|
|
328
|
+
*
|
|
329
|
+
* @param {string} appLink The URL to download an app from
|
|
330
|
+
* @param {import('axios').RawAxiosRequestHeaders} reqHeaders Additional HTTP request headers
|
|
331
|
+
* @returns {Promise<RemoteAppData>}
|
|
332
|
+
*/
|
|
333
|
+
async function queryAppLink(appLink, reqHeaders) {
|
|
334
|
+
const { href } = url_1.default.parse(appLink);
|
|
335
|
+
/**
|
|
336
|
+
* @type {import('axios').RawAxiosRequestConfig}
|
|
337
|
+
*/
|
|
338
|
+
const requestOpts = {
|
|
339
|
+
url: href,
|
|
340
|
+
responseType: 'stream',
|
|
341
|
+
timeout: APP_DOWNLOAD_TIMEOUT_MS,
|
|
342
|
+
validateStatus: (status) => (status >= 200 && status < 300) || status === HTTP_STATUS_NOT_MODIFIED,
|
|
343
|
+
headers: reqHeaders,
|
|
344
|
+
};
|
|
394
345
|
try {
|
|
395
|
-
await
|
|
396
|
-
|
|
346
|
+
const { data: stream, headers, status } = await (0, axios_1.default)(requestOpts);
|
|
347
|
+
return {
|
|
348
|
+
stream,
|
|
349
|
+
headers,
|
|
350
|
+
status,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
catch (err) {
|
|
354
|
+
throw new Error(`Cannot download the app from ${href}: ${err.message}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Retrieves app payload from the given stream. Also meters the download performance.
|
|
359
|
+
*
|
|
360
|
+
* @param {import('stream').Readable} srcStream The incoming stream
|
|
361
|
+
* @param {string} dstPath The target file path to be written
|
|
362
|
+
* @returns {Promise<string>} The same dstPath
|
|
363
|
+
* @throws {Error} If there was a failure while downloading the file
|
|
364
|
+
*/
|
|
365
|
+
async function fetchApp(srcStream, dstPath) {
|
|
366
|
+
const timer = new support_1.timing.Timer().start();
|
|
367
|
+
try {
|
|
368
|
+
const writer = support_1.fs.createWriteStream(dstPath);
|
|
369
|
+
srcStream.pipe(writer);
|
|
370
|
+
await new bluebird_1.default((resolve, reject) => {
|
|
371
|
+
srcStream.once('error', reject);
|
|
372
|
+
writer.once('finish', resolve);
|
|
373
|
+
writer.once('error', (e) => {
|
|
374
|
+
srcStream.unpipe(writer);
|
|
375
|
+
reject(e);
|
|
376
|
+
});
|
|
397
377
|
});
|
|
398
378
|
}
|
|
399
379
|
catch (err) {
|
|
400
|
-
throw new Error(`
|
|
380
|
+
throw new Error(`Cannot fetch the application: ${err.message}`);
|
|
401
381
|
}
|
|
402
|
-
|
|
382
|
+
const secondsElapsed = timer.getDuration().asSeconds;
|
|
383
|
+
const { size } = await support_1.fs.stat(dstPath);
|
|
384
|
+
logger_1.default.debug(`The application (${support_1.util.toReadableSizeString(size)}) ` +
|
|
385
|
+
`has been downloaded to '${dstPath}' in ${secondsElapsed.toFixed(3)}s`);
|
|
386
|
+
// it does not make much sense to approximate the speed for short downloads
|
|
387
|
+
if (secondsElapsed >= AVG_DOWNLOAD_SPEED_MEASUREMENT_THRESHOLD_SEC) {
|
|
388
|
+
const bytesPerSec = Math.floor(size / secondsElapsed);
|
|
389
|
+
logger_1.default.debug(`Approximate download speed: ${support_1.util.toReadableSizeString(bytesPerSec)}/s`);
|
|
390
|
+
}
|
|
391
|
+
return dstPath;
|
|
403
392
|
}
|
|
404
393
|
/**
|
|
405
394
|
* Extracts the bundle from an archive into the given folder
|
|
@@ -553,5 +542,12 @@ exports.default = {
|
|
|
553
542
|
* @property {Date?} lastModified
|
|
554
543
|
* @property {boolean} immutable
|
|
555
544
|
* @property {number?} maxAge
|
|
545
|
+
* @property {string?} etag
|
|
546
|
+
*/
|
|
547
|
+
/**
|
|
548
|
+
* @typedef RemoteAppData Properties of the remote application (e.g. GET HTTP response) to be downloaded.
|
|
549
|
+
* @property {number} status The HTTP status of the response
|
|
550
|
+
* @property {import('stream').Readable} stream The HTTP response body represented as readable stream
|
|
551
|
+
* @property {import('axios').RawAxiosResponseHeaders | import('axios').AxiosResponseHeaders} headers HTTP response headers
|
|
556
552
|
*/
|
|
557
553
|
//# sourceMappingURL=helpers.js.map
|