appium-android-driver 7.8.3 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/build/lib/commands/app-management.d.ts +129 -5
- package/build/lib/commands/app-management.d.ts.map +1 -1
- package/build/lib/commands/app-management.js +433 -128
- package/build/lib/commands/app-management.js.map +1 -1
- package/build/lib/commands/appearance.d.ts +17 -4
- package/build/lib/commands/appearance.d.ts.map +1 -1
- package/build/lib/commands/appearance.js +32 -33
- package/build/lib/commands/appearance.js.map +1 -1
- package/build/lib/commands/context/cache.d.ts +19 -0
- package/build/lib/commands/context/cache.d.ts.map +1 -0
- package/build/lib/commands/context/cache.js +32 -0
- package/build/lib/commands/context/cache.js.map +1 -0
- package/build/lib/commands/context/exports.d.ts +141 -0
- package/build/lib/commands/context/exports.d.ts.map +1 -0
- package/build/lib/commands/context/exports.js +351 -0
- package/build/lib/commands/context/exports.js.map +1 -0
- package/build/lib/commands/context/helpers.d.ts +98 -0
- package/build/lib/commands/context/helpers.d.ts.map +1 -0
- package/build/lib/commands/context/helpers.js +715 -0
- package/build/lib/commands/context/helpers.js.map +1 -0
- package/build/lib/commands/device/common.d.ts +23 -0
- package/build/lib/commands/device/common.d.ts.map +1 -0
- package/build/lib/commands/device/common.js +230 -0
- package/build/lib/commands/device/common.js.map +1 -0
- package/build/lib/commands/device/emulator-actions.d.ts +114 -0
- package/build/lib/commands/device/emulator-actions.d.ts.map +1 -0
- package/build/lib/commands/device/emulator-actions.js +197 -0
- package/build/lib/commands/device/emulator-actions.js.map +1 -0
- package/build/lib/commands/device/emulator-console.d.ts +7 -0
- package/build/lib/commands/device/emulator-console.d.ts.map +1 -0
- package/build/lib/commands/device/emulator-console.js +24 -0
- package/build/lib/commands/device/emulator-console.js.map +1 -0
- package/build/lib/commands/device/utils.d.ts +50 -0
- package/build/lib/commands/device/utils.d.ts.map +1 -0
- package/build/lib/commands/device/utils.js +238 -0
- package/build/lib/commands/device/utils.js.map +1 -0
- package/build/lib/commands/deviceidle.d.ts +8 -5
- package/build/lib/commands/deviceidle.d.ts.map +1 -1
- package/build/lib/commands/deviceidle.js +31 -37
- package/build/lib/commands/deviceidle.js.map +1 -1
- package/build/lib/commands/element.d.ts +99 -5
- package/build/lib/commands/element.d.ts.map +1 -1
- package/build/lib/commands/element.js +152 -116
- package/build/lib/commands/element.js.map +1 -1
- package/build/lib/commands/execute.d.ts +12 -4
- package/build/lib/commands/execute.d.ts.map +1 -1
- package/build/lib/commands/execute.js +83 -78
- package/build/lib/commands/execute.js.map +1 -1
- package/build/lib/commands/file-actions.d.ts +42 -5
- package/build/lib/commands/file-actions.d.ts.map +1 -1
- package/build/lib/commands/file-actions.js +230 -194
- package/build/lib/commands/file-actions.js.map +1 -1
- package/build/lib/commands/find.d.ts +5 -4
- package/build/lib/commands/find.d.ts.map +1 -1
- package/build/lib/commands/find.js +7 -10
- package/build/lib/commands/find.js.map +1 -1
- package/build/lib/commands/geolocation.d.ts +45 -0
- package/build/lib/commands/geolocation.d.ts.map +1 -0
- package/build/lib/commands/geolocation.js +182 -0
- package/build/lib/commands/geolocation.js.map +1 -0
- package/build/lib/commands/ime.d.ts +25 -5
- package/build/lib/commands/ime.d.ts.map +1 -1
- package/build/lib/commands/ime.js +59 -42
- package/build/lib/commands/ime.js.map +1 -1
- package/build/lib/commands/intent.d.ts +56 -5
- package/build/lib/commands/intent.d.ts.map +1 -1
- package/build/lib/commands/intent.js +135 -83
- package/build/lib/commands/intent.js.map +1 -1
- package/build/lib/commands/keyboard.d.ts +58 -4
- package/build/lib/commands/keyboard.d.ts.map +1 -1
- package/build/lib/commands/keyboard.js +119 -17
- package/build/lib/commands/keyboard.js.map +1 -1
- package/build/lib/commands/lock/exports.d.ts +301 -0
- package/build/lib/commands/lock/exports.d.ts.map +1 -0
- package/build/lib/commands/lock/exports.js +121 -0
- package/build/lib/commands/lock/exports.js.map +1 -0
- package/build/lib/commands/lock/helpers.d.ts +349 -0
- package/build/lib/commands/lock/helpers.d.ts.map +1 -0
- package/build/lib/commands/lock/helpers.js +375 -0
- package/build/lib/commands/lock/helpers.js.map +1 -0
- package/build/lib/commands/log.d.ts +59 -5
- package/build/lib/commands/log.d.ts.map +1 -1
- package/build/lib/commands/log.js +150 -140
- package/build/lib/commands/log.js.map +1 -1
- package/build/lib/commands/media-projection.d.ts +16 -5
- package/build/lib/commands/media-projection.d.ts.map +1 -1
- package/build/lib/commands/media-projection.js +69 -58
- package/build/lib/commands/media-projection.js.map +1 -1
- package/build/lib/commands/memory.d.ts +9 -5
- package/build/lib/commands/memory.d.ts.map +1 -1
- package/build/lib/commands/memory.js +19 -24
- package/build/lib/commands/memory.js.map +1 -1
- package/build/lib/commands/misc.d.ts +42 -0
- package/build/lib/commands/misc.d.ts.map +1 -0
- package/build/lib/commands/misc.js +100 -0
- package/build/lib/commands/misc.js.map +1 -0
- package/build/lib/commands/network.d.ts +61 -5
- package/build/lib/commands/network.d.ts.map +1 -1
- package/build/lib/commands/network.js +196 -189
- package/build/lib/commands/network.js.map +1 -1
- package/build/lib/commands/performance.d.ts +67 -27
- package/build/lib/commands/performance.d.ts.map +1 -1
- package/build/lib/commands/performance.js +105 -80
- package/build/lib/commands/performance.js.map +1 -1
- package/build/lib/commands/permissions.d.ts +12 -6
- package/build/lib/commands/permissions.d.ts.map +1 -1
- package/build/lib/commands/permissions.js +65 -62
- package/build/lib/commands/permissions.js.map +1 -1
- package/build/lib/commands/recordscreen.d.ts +44 -5
- package/build/lib/commands/recordscreen.d.ts.map +1 -1
- package/build/lib/commands/recordscreen.js +131 -126
- package/build/lib/commands/recordscreen.js.map +1 -1
- package/build/lib/commands/resources.d.ts +16 -0
- package/build/lib/commands/resources.d.ts.map +1 -0
- package/build/lib/commands/resources.js +91 -0
- package/build/lib/commands/resources.js.map +1 -0
- package/build/lib/commands/shell.d.ts +8 -5
- package/build/lib/commands/shell.d.ts.map +1 -1
- package/build/lib/commands/shell.js +29 -33
- package/build/lib/commands/shell.js.map +1 -1
- package/build/lib/commands/streamscreen.d.ts +34 -6
- package/build/lib/commands/streamscreen.d.ts.map +1 -1
- package/build/lib/commands/streamscreen.js +166 -162
- package/build/lib/commands/streamscreen.js.map +1 -1
- package/build/lib/commands/system-bars.d.ts +18 -13
- package/build/lib/commands/system-bars.d.ts.map +1 -1
- package/build/lib/commands/system-bars.js +68 -64
- package/build/lib/commands/system-bars.js.map +1 -1
- package/build/lib/commands/time.d.ts +14 -0
- package/build/lib/commands/time.d.ts.map +1 -0
- package/build/lib/commands/time.js +39 -0
- package/build/lib/commands/time.js.map +1 -0
- package/build/lib/commands/touch.d.ts +99 -6
- package/build/lib/commands/touch.d.ts.map +1 -1
- package/build/lib/commands/touch.js +399 -280
- package/build/lib/commands/touch.js.map +1 -1
- package/build/lib/commands/types.d.ts +110 -2
- package/build/lib/commands/types.d.ts.map +1 -1
- package/build/lib/doctor/checks.d.ts.map +1 -1
- package/build/lib/doctor/checks.js +4 -4
- package/build/lib/doctor/checks.js.map +1 -1
- package/build/lib/driver.d.ts +224 -27
- package/build/lib/driver.d.ts.map +1 -1
- package/build/lib/driver.js +232 -7
- package/build/lib/driver.js.map +1 -1
- package/build/lib/index.d.ts +1 -4
- package/build/lib/index.d.ts.map +1 -1
- package/build/lib/index.js +1 -13
- package/build/lib/index.js.map +1 -1
- package/build/lib/logger.js.map +1 -1
- package/build/lib/method-map.d.ts +0 -23
- package/build/lib/method-map.d.ts.map +1 -1
- package/build/lib/method-map.js +0 -11
- package/build/lib/method-map.js.map +1 -1
- package/build/lib/utils.d.ts +12 -0
- package/build/lib/utils.d.ts.map +1 -1
- package/build/lib/utils.js +38 -2
- package/build/lib/utils.js.map +1 -1
- package/lib/commands/app-management.js +470 -145
- package/lib/commands/appearance.js +29 -36
- package/lib/commands/context/cache.js +29 -0
- package/lib/commands/context/exports.js +379 -0
- package/lib/commands/context/helpers.js +802 -0
- package/lib/commands/device/common.js +264 -0
- package/lib/commands/device/emulator-actions.js +194 -0
- package/lib/commands/device/emulator-console.js +24 -0
- package/lib/commands/device/utils.js +285 -0
- package/lib/commands/deviceidle.js +31 -44
- package/lib/commands/element.js +149 -142
- package/lib/commands/execute.js +86 -87
- package/lib/commands/file-actions.js +249 -222
- package/lib/commands/find.ts +13 -19
- package/lib/commands/geolocation.js +179 -0
- package/lib/commands/ime.js +53 -45
- package/lib/commands/intent.js +149 -91
- package/lib/commands/keyboard.js +114 -17
- package/lib/commands/lock/exports.js +139 -0
- package/lib/commands/lock/helpers.js +379 -0
- package/lib/commands/log.js +170 -166
- package/lib/commands/media-projection.js +75 -70
- package/lib/commands/memory.js +17 -29
- package/lib/commands/misc.js +94 -0
- package/lib/commands/network.js +209 -223
- package/lib/commands/performance.js +88 -73
- package/lib/commands/permissions.js +83 -84
- package/lib/commands/recordscreen.js +171 -170
- package/lib/commands/resources.js +96 -0
- package/lib/commands/shell.js +28 -42
- package/lib/commands/streamscreen.js +207 -206
- package/lib/commands/system-bars.js +76 -77
- package/lib/commands/time.js +36 -0
- package/lib/commands/touch.js +442 -346
- package/lib/commands/types.ts +123 -2
- package/lib/doctor/checks.js +24 -16
- package/lib/driver.ts +454 -12
- package/lib/index.ts +1 -13
- package/lib/logger.js +1 -1
- package/lib/method-map.js +0 -11
- package/lib/utils.js +40 -3
- package/package.json +1 -1
- package/build/lib/commands/actions.d.ts +0 -8
- package/build/lib/commands/actions.d.ts.map +0 -1
- package/build/lib/commands/actions.js +0 -207
- package/build/lib/commands/actions.js.map +0 -1
- package/build/lib/commands/alert.d.ts +0 -8
- package/build/lib/commands/alert.d.ts.map +0 -1
- package/build/lib/commands/alert.js +0 -29
- package/build/lib/commands/alert.js.map +0 -1
- package/build/lib/commands/context.d.ts +0 -10
- package/build/lib/commands/context.d.ts.map +0 -1
- package/build/lib/commands/context.js +0 -431
- package/build/lib/commands/context.js.map +0 -1
- package/build/lib/commands/emu-console.d.ts +0 -7
- package/build/lib/commands/emu-console.d.ts.map +0 -1
- package/build/lib/commands/emu-console.js +0 -27
- package/build/lib/commands/emu-console.js.map +0 -1
- package/build/lib/commands/general.d.ts +0 -9
- package/build/lib/commands/general.d.ts.map +0 -1
- package/build/lib/commands/general.js +0 -293
- package/build/lib/commands/general.js.map +0 -1
- package/build/lib/commands/index.d.ts +0 -28
- package/build/lib/commands/index.d.ts.map +0 -1
- package/build/lib/commands/index.js +0 -57
- package/build/lib/commands/index.js.map +0 -1
- package/build/lib/commands/mixins.d.ts +0 -747
- package/build/lib/commands/mixins.d.ts.map +0 -1
- package/build/lib/commands/mixins.js +0 -19
- package/build/lib/commands/mixins.js.map +0 -1
- package/build/lib/helpers/android.d.ts +0 -163
- package/build/lib/helpers/android.d.ts.map +0 -1
- package/build/lib/helpers/android.js +0 -818
- package/build/lib/helpers/android.js.map +0 -1
- package/build/lib/helpers/index.d.ts +0 -7
- package/build/lib/helpers/index.d.ts.map +0 -1
- package/build/lib/helpers/index.js +0 -29
- package/build/lib/helpers/index.js.map +0 -1
- package/build/lib/helpers/types.d.ts +0 -122
- package/build/lib/helpers/types.d.ts.map +0 -1
- package/build/lib/helpers/types.js +0 -3
- package/build/lib/helpers/types.js.map +0 -1
- package/build/lib/helpers/unlock.d.ts +0 -32
- package/build/lib/helpers/unlock.d.ts.map +0 -1
- package/build/lib/helpers/unlock.js +0 -273
- package/build/lib/helpers/unlock.js.map +0 -1
- package/build/lib/helpers/webview.d.ts +0 -74
- package/build/lib/helpers/webview.d.ts.map +0 -1
- package/build/lib/helpers/webview.js +0 -448
- package/build/lib/helpers/webview.js.map +0 -1
- package/lib/commands/actions.js +0 -244
- package/lib/commands/alert.js +0 -34
- package/lib/commands/context.js +0 -507
- package/lib/commands/emu-console.js +0 -31
- package/lib/commands/general.js +0 -343
- package/lib/commands/index.ts +0 -54
- package/lib/commands/mixins.ts +0 -976
- package/lib/helpers/android.ts +0 -1153
- package/lib/helpers/index.ts +0 -6
- package/lib/helpers/types.ts +0 -136
- package/lib/helpers/unlock.ts +0 -329
- package/lib/helpers/webview.ts +0 -610
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
import {util, timing} from '@appium/support';
|
|
2
|
+
import _ from 'lodash';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import net from 'node:net';
|
|
5
|
+
import {findAPortNotInUse} from 'portscanner';
|
|
6
|
+
import {sleep} from 'asyncbox';
|
|
7
|
+
import B from 'bluebird';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import http from 'node:http';
|
|
11
|
+
import Chromedriver from 'appium-chromedriver';
|
|
12
|
+
import {toDetailsCacheKey, getWebviewDetails, WEBVIEWS_DETAILS_CACHE} from './cache';
|
|
13
|
+
|
|
14
|
+
// https://cs.chromium.org/chromium/src/chrome/browser/devtools/device/android_device_info_query.cc
|
|
15
|
+
export const CHROME_BROWSER_PACKAGE_ACTIVITY = /** @type {const} */ ({
|
|
16
|
+
chrome: {
|
|
17
|
+
pkg: 'com.android.chrome',
|
|
18
|
+
activity: 'com.google.android.apps.chrome.Main',
|
|
19
|
+
},
|
|
20
|
+
chromium: {
|
|
21
|
+
pkg: 'org.chromium.chrome.shell',
|
|
22
|
+
activity: '.ChromeShellActivity',
|
|
23
|
+
},
|
|
24
|
+
chromebeta: {
|
|
25
|
+
pkg: 'com.chrome.beta',
|
|
26
|
+
activity: 'com.google.android.apps.chrome.Main',
|
|
27
|
+
},
|
|
28
|
+
browser: {
|
|
29
|
+
pkg: 'com.android.browser',
|
|
30
|
+
activity: 'com.android.browser.BrowserActivity',
|
|
31
|
+
},
|
|
32
|
+
'chromium-browser': {
|
|
33
|
+
pkg: 'org.chromium.chrome',
|
|
34
|
+
activity: 'com.google.android.apps.chrome.Main',
|
|
35
|
+
},
|
|
36
|
+
'chromium-webview': {
|
|
37
|
+
pkg: 'org.chromium.webview_shell',
|
|
38
|
+
activity: 'org.chromium.webview_shell.WebViewBrowserActivity',
|
|
39
|
+
},
|
|
40
|
+
default: {
|
|
41
|
+
pkg: 'com.android.chrome',
|
|
42
|
+
activity: 'com.google.android.apps.chrome.Main',
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
export const CHROME_PACKAGE_NAME = 'com.android.chrome';
|
|
46
|
+
export const KNOWN_CHROME_PACKAGE_NAMES = [
|
|
47
|
+
CHROME_PACKAGE_NAME,
|
|
48
|
+
'com.chrome.beta',
|
|
49
|
+
'com.chrome.dev',
|
|
50
|
+
'com.chrome.canary',
|
|
51
|
+
];
|
|
52
|
+
const CHROMEDRIVER_AUTODOWNLOAD_FEATURE = 'chromedriver_autodownload';
|
|
53
|
+
const CROSSWALK_SOCKET_PATTERN = /@([\w.]+)_devtools_remote\b/;
|
|
54
|
+
const CHROMIUM_DEVTOOLS_SOCKET = 'chrome_devtools_remote';
|
|
55
|
+
export const NATIVE_WIN = 'NATIVE_APP';
|
|
56
|
+
export const WEBVIEW_WIN = 'WEBVIEW';
|
|
57
|
+
export const CHROMIUM_WIN = 'CHROMIUM';
|
|
58
|
+
export const WEBVIEW_BASE = `${WEBVIEW_WIN}_`;
|
|
59
|
+
export const DEVTOOLS_SOCKET_PATTERN = /@[\w.]+_devtools_remote_?([\w.]+_)?(\d+)?\b/;
|
|
60
|
+
const WEBVIEW_PID_PATTERN = new RegExp(`^${WEBVIEW_BASE}(\\d+)`);
|
|
61
|
+
const WEBVIEW_PKG_PATTERN = new RegExp(`^${WEBVIEW_BASE}([^\\d\\s][\\w.]*)`);
|
|
62
|
+
const WEBVIEW_WAIT_INTERVAL_MS = 200;
|
|
63
|
+
const CDP_REQ_TIMEOUT = 2000; // ms
|
|
64
|
+
const DEVTOOLS_PORTS_RANGE = [10900, 11000];
|
|
65
|
+
const DEVTOOLS_PORT_ALLOCATION_GUARD = util.getLockFileGuard(
|
|
66
|
+
path.resolve(os.tmpdir(), 'android_devtools_port_guard'),
|
|
67
|
+
{timeout: 7, tryRecovery: true},
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @returns {Promise<number>}
|
|
72
|
+
*/
|
|
73
|
+
async function getFreePort() {
|
|
74
|
+
return await new Promise((resolve, reject) => {
|
|
75
|
+
const srv = net.createServer();
|
|
76
|
+
srv.listen(0, () => {
|
|
77
|
+
const address = srv.address();
|
|
78
|
+
let port;
|
|
79
|
+
if (_.has(address, 'port')) {
|
|
80
|
+
// @ts-ignore The above condition covers possible errors
|
|
81
|
+
port = address.port;
|
|
82
|
+
} else {
|
|
83
|
+
reject(new Error('Cannot determine any free port number'));
|
|
84
|
+
}
|
|
85
|
+
srv.close(() => resolve(port));
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* https://chromedevtools.github.io/devtools-protocol/
|
|
92
|
+
*
|
|
93
|
+
* @param {string} host
|
|
94
|
+
* @param {number} port
|
|
95
|
+
* @param {string} endpoint
|
|
96
|
+
* @returns {Promise<object[]>}
|
|
97
|
+
*/
|
|
98
|
+
async function cdpGetRequest(host, port, endpoint) {
|
|
99
|
+
return (await axios({
|
|
100
|
+
url: `http://${host}:${port}${endpoint}`,
|
|
101
|
+
timeout: CDP_REQ_TIMEOUT,
|
|
102
|
+
// We need to set this from Node.js v19 onwards.
|
|
103
|
+
// Otherwise, in situation with multiple webviews,
|
|
104
|
+
// the preceding webview pages will be incorrectly retrieved as the current ones.
|
|
105
|
+
// https://nodejs.org/en/blog/announcements/v19-release-announce#https11-keepalive-by-default
|
|
106
|
+
httpAgent: new http.Agent({keepAlive: false}),
|
|
107
|
+
})).data;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {string} host
|
|
112
|
+
* @param {number} port
|
|
113
|
+
* @returns {Promise<object[]>}
|
|
114
|
+
*/
|
|
115
|
+
async function cdpList(host, port) {
|
|
116
|
+
return cdpGetRequest(host, port, '/json/list');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @param {string} host
|
|
121
|
+
* @param {number} port
|
|
122
|
+
* @returns {Promise<object[]>}
|
|
123
|
+
*/
|
|
124
|
+
async function cdpInfo(host, port) {
|
|
125
|
+
return cdpGetRequest(host, port, '/json/version');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
*
|
|
130
|
+
* @param {string} browser
|
|
131
|
+
* @returns {import('type-fest').ValueOf<typeof CHROME_BROWSER_PACKAGE_ACTIVITY>}
|
|
132
|
+
*/
|
|
133
|
+
export function getChromePkg(browser) {
|
|
134
|
+
return (
|
|
135
|
+
CHROME_BROWSER_PACKAGE_ACTIVITY[browser.toLowerCase()] ||
|
|
136
|
+
CHROME_BROWSER_PACKAGE_ACTIVITY.default
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Create Chromedriver capabilities based on the provided
|
|
142
|
+
* Appium capabilities
|
|
143
|
+
*
|
|
144
|
+
* @this {import('../../driver').AndroidDriver}
|
|
145
|
+
* @param {any} opts
|
|
146
|
+
* @param {string} deviceId
|
|
147
|
+
* @param {import('../types').WebViewDetails | null} [webViewDetails]
|
|
148
|
+
* @returns {import('@appium/types').StringRecord}
|
|
149
|
+
*/
|
|
150
|
+
function createChromedriverCaps(opts, deviceId, webViewDetails) {
|
|
151
|
+
const caps = {chromeOptions: {}};
|
|
152
|
+
|
|
153
|
+
const androidPackage =
|
|
154
|
+
opts.chromeOptions?.androidPackage ||
|
|
155
|
+
opts.appPackage ||
|
|
156
|
+
webViewDetails?.info?.['Android-Package'];
|
|
157
|
+
if (androidPackage) {
|
|
158
|
+
// chromedriver raises an invalid argument error when androidPackage is 'null'
|
|
159
|
+
|
|
160
|
+
caps.chromeOptions.androidPackage = androidPackage;
|
|
161
|
+
}
|
|
162
|
+
if (_.isBoolean(opts.chromeUseRunningApp)) {
|
|
163
|
+
caps.chromeOptions.androidUseRunningApp = opts.chromeUseRunningApp;
|
|
164
|
+
}
|
|
165
|
+
if (opts.chromeAndroidPackage) {
|
|
166
|
+
caps.chromeOptions.androidPackage = opts.chromeAndroidPackage;
|
|
167
|
+
}
|
|
168
|
+
if (opts.chromeAndroidActivity) {
|
|
169
|
+
caps.chromeOptions.androidActivity = opts.chromeAndroidActivity;
|
|
170
|
+
}
|
|
171
|
+
if (opts.chromeAndroidProcess) {
|
|
172
|
+
caps.chromeOptions.androidProcess = opts.chromeAndroidProcess;
|
|
173
|
+
} else if (webViewDetails?.process?.name && webViewDetails?.process?.id) {
|
|
174
|
+
caps.chromeOptions.androidProcess = webViewDetails.process.name;
|
|
175
|
+
}
|
|
176
|
+
if (_.toLower(opts.browserName) === 'chromium-webview') {
|
|
177
|
+
caps.chromeOptions.androidActivity = opts.appActivity;
|
|
178
|
+
}
|
|
179
|
+
if (opts.pageLoadStrategy) {
|
|
180
|
+
caps.pageLoadStrategy = opts.pageLoadStrategy;
|
|
181
|
+
}
|
|
182
|
+
const isChrome = _.toLower(caps.chromeOptions.androidPackage) === 'chrome';
|
|
183
|
+
if (_.includes(KNOWN_CHROME_PACKAGE_NAMES, caps.chromeOptions.androidPackage) || isChrome) {
|
|
184
|
+
// if we have extracted package from context name, it could come in as bare
|
|
185
|
+
// "chrome", and so we should make sure the details are correct, including
|
|
186
|
+
// not using an activity or process id
|
|
187
|
+
if (isChrome) {
|
|
188
|
+
caps.chromeOptions.androidPackage = CHROME_PACKAGE_NAME;
|
|
189
|
+
}
|
|
190
|
+
delete caps.chromeOptions.androidActivity;
|
|
191
|
+
delete caps.chromeOptions.androidProcess;
|
|
192
|
+
}
|
|
193
|
+
// add device id from adb
|
|
194
|
+
caps.chromeOptions.androidDeviceSerial = deviceId;
|
|
195
|
+
|
|
196
|
+
if (_.isPlainObject(opts.loggingPrefs) || _.isPlainObject(opts.chromeLoggingPrefs)) {
|
|
197
|
+
if (opts.loggingPrefs) {
|
|
198
|
+
this.log.warn(
|
|
199
|
+
`The 'loggingPrefs' cap is deprecated; use the 'chromeLoggingPrefs' cap instead`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
caps.loggingPrefs = opts.chromeLoggingPrefs || opts.loggingPrefs;
|
|
203
|
+
}
|
|
204
|
+
if (opts.enablePerformanceLogging) {
|
|
205
|
+
this.log.warn(
|
|
206
|
+
`The 'enablePerformanceLogging' cap is deprecated; simply use ` +
|
|
207
|
+
`the 'chromeLoggingPrefs' cap instead, with a 'performance' key set to 'ALL'`,
|
|
208
|
+
);
|
|
209
|
+
const newPref = {performance: 'ALL'};
|
|
210
|
+
// don't overwrite other logging prefs that have been sent in if they exist
|
|
211
|
+
caps.loggingPrefs = caps.loggingPrefs ? Object.assign({}, caps.loggingPrefs, newPref) : newPref;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (opts.chromeOptions?.Arguments) {
|
|
215
|
+
// merge `Arguments` and `args`
|
|
216
|
+
opts.chromeOptions.args = [...(opts.chromeOptions.args || []), ...opts.chromeOptions.Arguments];
|
|
217
|
+
delete opts.chromeOptions.Arguments;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
this.log.debug(
|
|
221
|
+
'Precalculated Chromedriver capabilities: ' + JSON.stringify(caps.chromeOptions, null, 2),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
/** @type {string[]} */
|
|
225
|
+
const protectedCapNames = [];
|
|
226
|
+
for (const [opt, val] of _.toPairs(opts.chromeOptions)) {
|
|
227
|
+
if (_.isUndefined(caps.chromeOptions[opt])) {
|
|
228
|
+
caps.chromeOptions[opt] = val;
|
|
229
|
+
} else {
|
|
230
|
+
protectedCapNames.push(opt);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (!_.isEmpty(protectedCapNames)) {
|
|
234
|
+
this.log.info(
|
|
235
|
+
'The following Chromedriver capabilities cannot be overridden ' +
|
|
236
|
+
'by the provided chromeOptions:',
|
|
237
|
+
);
|
|
238
|
+
for (const optName of protectedCapNames) {
|
|
239
|
+
this.log.info(` ${optName} (${JSON.stringify(opts.chromeOptions[optName])})`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return caps;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Parse webview names for getContexts
|
|
248
|
+
*
|
|
249
|
+
* @this {import('../../driver').AndroidDriver}
|
|
250
|
+
* @param {import('../types').WebviewsMapping[]} webviewsMapping
|
|
251
|
+
* @param {import('../types').GetWebviewsOpts} options
|
|
252
|
+
* @returns {string[]}
|
|
253
|
+
*/
|
|
254
|
+
export function parseWebviewNames(
|
|
255
|
+
webviewsMapping,
|
|
256
|
+
{ensureWebviewsHavePages = true, isChromeSession = false} = {},
|
|
257
|
+
) {
|
|
258
|
+
if (isChromeSession) {
|
|
259
|
+
return [CHROMIUM_WIN];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** @type {string[]} */
|
|
263
|
+
const result = [];
|
|
264
|
+
for (const {webview, pages, proc, webviewName} of webviewsMapping) {
|
|
265
|
+
if (ensureWebviewsHavePages && !pages?.length) {
|
|
266
|
+
this.log.info(
|
|
267
|
+
`Skipping the webview '${webview}' at '${proc}' ` +
|
|
268
|
+
`since it has reported having zero pages`,
|
|
269
|
+
);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (webviewName) {
|
|
273
|
+
result.push(webviewName);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
this.log.debug(
|
|
277
|
+
`Found ${util.pluralize('webview', result.length, true)}: ${JSON.stringify(result)}`,
|
|
278
|
+
);
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Allocates a local port for devtools communication
|
|
284
|
+
*
|
|
285
|
+
* @this {import('../../driver').AndroidDriver}
|
|
286
|
+
* @param {string} socketName - The remote Unix socket name
|
|
287
|
+
* @param {number?} [webviewDevtoolsPort=null] - The local port number or null to apply
|
|
288
|
+
* autodetection
|
|
289
|
+
* @returns {Promise<[string, number]>} The host name and the port number to connect to if the
|
|
290
|
+
* remote socket has been forwarded successfully
|
|
291
|
+
* @throws {Error} If there was an error while allocating the local port
|
|
292
|
+
*/
|
|
293
|
+
async function allocateDevtoolsChannel(socketName, webviewDevtoolsPort = null) {
|
|
294
|
+
// socket names come with '@', but this should not be a part of the abstract
|
|
295
|
+
// remote port, so remove it
|
|
296
|
+
const remotePort = socketName.replace(/^@/, '');
|
|
297
|
+
let [startPort, endPort] = DEVTOOLS_PORTS_RANGE;
|
|
298
|
+
if (webviewDevtoolsPort) {
|
|
299
|
+
endPort = webviewDevtoolsPort + (endPort - startPort);
|
|
300
|
+
startPort = webviewDevtoolsPort;
|
|
301
|
+
}
|
|
302
|
+
this.log.debug(
|
|
303
|
+
`Forwarding remote port ${remotePort} to a local ` + `port in range ${startPort}..${endPort}`,
|
|
304
|
+
);
|
|
305
|
+
if (!webviewDevtoolsPort) {
|
|
306
|
+
this.log.debug(
|
|
307
|
+
`You could use the 'webviewDevtoolsPort' capability to customize ` +
|
|
308
|
+
`the starting port number`,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
const port = await DEVTOOLS_PORT_ALLOCATION_GUARD(async () => {
|
|
312
|
+
let localPort;
|
|
313
|
+
try {
|
|
314
|
+
localPort = await findAPortNotInUse(startPort, endPort);
|
|
315
|
+
} catch (e) {
|
|
316
|
+
throw new Error(
|
|
317
|
+
`Cannot find any free port to forward the Devtools socket ` +
|
|
318
|
+
`in range ${startPort}..${endPort}. You could set the starting port number ` +
|
|
319
|
+
`manually by providing the 'webviewDevtoolsPort' capability`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
await this.adb.adbExec(['forward', `tcp:${localPort}`, `localabstract:${remotePort}`]);
|
|
323
|
+
return localPort;
|
|
324
|
+
});
|
|
325
|
+
return [this.adb.adbHost ?? '127.0.0.1', port];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* This is a wrapper for Chrome Debugger Protocol data collection.
|
|
330
|
+
* No error is thrown if CDP request fails - in such case no data will be
|
|
331
|
+
* recorded into the corresponding `webviewsMapping` item.
|
|
332
|
+
*
|
|
333
|
+
* @this {import('../../driver').AndroidDriver}
|
|
334
|
+
* @param {import('../types').WebviewProps[]} webviewsMapping The current webviews mapping
|
|
335
|
+
* !!! Each item of this array gets mutated (`info`/`pages` properties get added
|
|
336
|
+
* based on the provided `opts`) if the requested details have been
|
|
337
|
+
* successfully retrieved for it !!!
|
|
338
|
+
* @param {import('../types').DetailCollectionOptions} [opts={}] If both `ensureWebviewsHavePages` and
|
|
339
|
+
* `enableWebviewDetailsCollection` properties are falsy then no details collection
|
|
340
|
+
* is performed
|
|
341
|
+
* @returns {Promise<void>}
|
|
342
|
+
*/
|
|
343
|
+
async function collectWebviewsDetails(webviewsMapping, opts = {}) {
|
|
344
|
+
if (_.isEmpty(webviewsMapping)) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const {
|
|
349
|
+
webviewDevtoolsPort = null,
|
|
350
|
+
ensureWebviewsHavePages = null,
|
|
351
|
+
enableWebviewDetailsCollection = null,
|
|
352
|
+
} = opts;
|
|
353
|
+
|
|
354
|
+
if (!ensureWebviewsHavePages) {
|
|
355
|
+
this.log.info(
|
|
356
|
+
`Not checking whether webviews have active pages; use the ` +
|
|
357
|
+
`'ensureWebviewsHavePages' cap to turn this check on`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!enableWebviewDetailsCollection) {
|
|
362
|
+
this.log.info(
|
|
363
|
+
`Not collecting web view details. Details collection might help ` +
|
|
364
|
+
`to make Chromedriver initialization more precise. Use the 'enableWebviewDetailsCollection' ` +
|
|
365
|
+
`cap to turn it on`,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (!ensureWebviewsHavePages && !enableWebviewDetailsCollection) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Connect to each devtools socket and retrieve web view details
|
|
374
|
+
this.log.debug(
|
|
375
|
+
`Collecting CDP data of ${util.pluralize('webview', webviewsMapping.length, true)}`,
|
|
376
|
+
);
|
|
377
|
+
const detailCollectors = [];
|
|
378
|
+
for (const item of webviewsMapping) {
|
|
379
|
+
detailCollectors.push(
|
|
380
|
+
(async () => {
|
|
381
|
+
let port;
|
|
382
|
+
let host;
|
|
383
|
+
try {
|
|
384
|
+
[host, port] = await allocateDevtoolsChannel.bind(this)(item.proc, webviewDevtoolsPort);
|
|
385
|
+
if (enableWebviewDetailsCollection) {
|
|
386
|
+
item.info = await cdpInfo(host, port);
|
|
387
|
+
}
|
|
388
|
+
if (ensureWebviewsHavePages) {
|
|
389
|
+
item.pages = await cdpList(host, port);
|
|
390
|
+
}
|
|
391
|
+
} catch (e) {
|
|
392
|
+
this.log.debug(e);
|
|
393
|
+
} finally {
|
|
394
|
+
if (port) {
|
|
395
|
+
try {
|
|
396
|
+
await this.adb.removePortForward(port);
|
|
397
|
+
} catch (e) {
|
|
398
|
+
this.log.debug(e);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
})(),
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
await B.all(detailCollectors);
|
|
406
|
+
this.log.debug(`CDP data collection completed`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Get a list of available webviews mapping by introspecting processes with adb,
|
|
411
|
+
* where webviews are listed. It's possible to pass in a 'deviceSocket' arg, which
|
|
412
|
+
* limits the webview possibilities to the one running on the Chromium devtools
|
|
413
|
+
* socket we're interested in (see note on webviewsFromProcs). We can also
|
|
414
|
+
* direct this method to verify whether a particular webview process actually
|
|
415
|
+
* has any pages (if a process exists but no pages are found, Chromedriver will
|
|
416
|
+
* not actually be able to connect to it, so this serves as a guard for that
|
|
417
|
+
* strange failure mode). The strategy for checking whether any pages are
|
|
418
|
+
* active involves sending a request to the remote debug server on the device,
|
|
419
|
+
* hence it is also possible to specify the port on the host machine which
|
|
420
|
+
* should be used for this communication.
|
|
421
|
+
*
|
|
422
|
+
* @this {import('../../driver').AndroidDriver}
|
|
423
|
+
* @param {import('../types').GetWebviewsOpts} [opts={}]
|
|
424
|
+
* @returns {Promise<import('../types').WebviewsMapping[]>}
|
|
425
|
+
*/
|
|
426
|
+
export async function getWebViewsMapping({
|
|
427
|
+
androidDeviceSocket = null,
|
|
428
|
+
ensureWebviewsHavePages = true,
|
|
429
|
+
webviewDevtoolsPort = null,
|
|
430
|
+
enableWebviewDetailsCollection = true,
|
|
431
|
+
waitForWebviewMs = 0,
|
|
432
|
+
} = {}) {
|
|
433
|
+
this.log.debug(`Getting a list of available webviews`);
|
|
434
|
+
|
|
435
|
+
if (!_.isNumber(waitForWebviewMs)) {
|
|
436
|
+
waitForWebviewMs = parseInt(`${waitForWebviewMs}`, 10) || 0;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** @type {import('../types').WebviewsMapping[]} */
|
|
440
|
+
let webviewsMapping;
|
|
441
|
+
const timer = new timing.Timer().start();
|
|
442
|
+
do {
|
|
443
|
+
webviewsMapping = await webviewsFromProcs.bind(this)(androidDeviceSocket);
|
|
444
|
+
|
|
445
|
+
if (webviewsMapping.length > 0) {
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
this.log.debug(`No webviews found in ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`);
|
|
450
|
+
await sleep(WEBVIEW_WAIT_INTERVAL_MS);
|
|
451
|
+
} while (timer.getDuration().asMilliSeconds < waitForWebviewMs);
|
|
452
|
+
|
|
453
|
+
await collectWebviewsDetails.bind(this)(webviewsMapping, {
|
|
454
|
+
ensureWebviewsHavePages,
|
|
455
|
+
enableWebviewDetailsCollection,
|
|
456
|
+
webviewDevtoolsPort,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
for (const webviewMapping of webviewsMapping) {
|
|
460
|
+
const {webview, info} = webviewMapping;
|
|
461
|
+
webviewMapping.webviewName = null;
|
|
462
|
+
|
|
463
|
+
let wvName = webview;
|
|
464
|
+
/** @type {{name: string; id: string | null} | undefined} */
|
|
465
|
+
let process;
|
|
466
|
+
if (!androidDeviceSocket) {
|
|
467
|
+
const pkgMatch = WEBVIEW_PKG_PATTERN.exec(webview);
|
|
468
|
+
try {
|
|
469
|
+
// web view name could either be suffixed with PID or the package name
|
|
470
|
+
// package names could not start with a digit
|
|
471
|
+
const pkg = pkgMatch ? pkgMatch[1] : await procFromWebview.bind(this)(webview);
|
|
472
|
+
wvName = `${WEBVIEW_BASE}${pkg}`;
|
|
473
|
+
const pidMatch = WEBVIEW_PID_PATTERN.exec(webview);
|
|
474
|
+
process = {
|
|
475
|
+
name: pkg,
|
|
476
|
+
id: pidMatch ? pidMatch[1] : null,
|
|
477
|
+
};
|
|
478
|
+
} catch (e) {
|
|
479
|
+
this.log.debug(e.stack);
|
|
480
|
+
this.log.warn(e.message);
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
webviewMapping.webviewName = wvName;
|
|
486
|
+
const key = toDetailsCacheKey(this.adb, wvName);
|
|
487
|
+
if (info || process) {
|
|
488
|
+
WEBVIEWS_DETAILS_CACHE.set(key, {info, process});
|
|
489
|
+
} else if (WEBVIEWS_DETAILS_CACHE.has(key)) {
|
|
490
|
+
WEBVIEWS_DETAILS_CACHE.delete(key);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return webviewsMapping;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Take a webview name like WEBVIEW_4296 and use 'adb shell ps' to figure out
|
|
498
|
+
* which app package is associated with that webview. One of the reasons we
|
|
499
|
+
* want to do this is to make sure we're listing webviews for the actual AUT,
|
|
500
|
+
* not some other running app
|
|
501
|
+
*
|
|
502
|
+
* @this {import('../../driver').AndroidDriver}
|
|
503
|
+
* @param {string} webview
|
|
504
|
+
* @returns {Promise<string>}
|
|
505
|
+
*/
|
|
506
|
+
async function procFromWebview(webview) {
|
|
507
|
+
const pidMatch = WEBVIEW_PID_PATTERN.exec(webview);
|
|
508
|
+
if (!pidMatch) {
|
|
509
|
+
throw new Error(`Could not find PID for webview '${webview}'`);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const pid = pidMatch[1];
|
|
513
|
+
this.log.debug(`${webview} mapped to pid ${pid}`);
|
|
514
|
+
this.log.debug(`Getting process name for webview '${webview}'`);
|
|
515
|
+
const pkg = await this.adb.getNameByPid(pid);
|
|
516
|
+
this.log.debug(`Got process name: '${pkg}'`);
|
|
517
|
+
return pkg;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* This function gets a list of android system processes and returns ones
|
|
522
|
+
* that look like webviews
|
|
523
|
+
* See https://cs.chromium.org/chromium/src/chrome/browser/devtools/device/android_device_info_query.cc
|
|
524
|
+
* for more details
|
|
525
|
+
*
|
|
526
|
+
* @this {import('../../driver').AndroidDriver}
|
|
527
|
+
* @returns {Promise<string[]>} a list of matching webview socket names (including the leading '@')
|
|
528
|
+
*/
|
|
529
|
+
async function getPotentialWebviewProcs() {
|
|
530
|
+
const out = await this.adb.shell(['cat', '/proc/net/unix']);
|
|
531
|
+
/** @type {string[]} */
|
|
532
|
+
const names = [];
|
|
533
|
+
/** @type {string[]} */
|
|
534
|
+
const allMatches = [];
|
|
535
|
+
for (const line of out.split('\n')) {
|
|
536
|
+
// Num RefCount Protocol Flags Type St Inode Path
|
|
537
|
+
const [, , , flags, , st, , sockPath] = line.trim().split(/\s+/);
|
|
538
|
+
if (!sockPath) {
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if (sockPath.startsWith('@')) {
|
|
542
|
+
allMatches.push(line.trim());
|
|
543
|
+
}
|
|
544
|
+
if (flags !== '00010000' || st !== '01') {
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (!DEVTOOLS_SOCKET_PATTERN.test(sockPath)) {
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
names.push(sockPath);
|
|
552
|
+
}
|
|
553
|
+
if (_.isEmpty(names)) {
|
|
554
|
+
this.log.debug('Found no active devtools sockets');
|
|
555
|
+
if (!_.isEmpty(allMatches)) {
|
|
556
|
+
this.log.debug(`Other sockets are: ${JSON.stringify(allMatches, null, 2)}`);
|
|
557
|
+
}
|
|
558
|
+
} else {
|
|
559
|
+
this.log.debug(
|
|
560
|
+
`Parsed ${names.length} active devtools ${util.pluralize('socket', names.length, false)}: ` +
|
|
561
|
+
JSON.stringify(names),
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
// sometimes the webview process shows up multiple times per app
|
|
565
|
+
return _.uniq(names);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* This function retrieves a list of system processes that look like webviews,
|
|
570
|
+
* and returns them along with the webview context name appropriate for it.
|
|
571
|
+
* If we pass in a deviceSocket, we only attempt to find webviews which match
|
|
572
|
+
* that socket name (this is for apps which embed Chromium, which isn't the
|
|
573
|
+
* same as chrome-backed webviews).
|
|
574
|
+
*
|
|
575
|
+
* @this {import('../../driver').AndroidDriver}
|
|
576
|
+
* @param {string?} [deviceSocket=null] - the explictly-named device socket to use
|
|
577
|
+
* @returns {Promise<import('../types').WebviewProc[]>}
|
|
578
|
+
*/
|
|
579
|
+
async function webviewsFromProcs(deviceSocket = null) {
|
|
580
|
+
const socketNames = await getPotentialWebviewProcs.bind(this)();
|
|
581
|
+
/** @type {{proc: string; webview: string}[]} */
|
|
582
|
+
const webviews = [];
|
|
583
|
+
for (const socketName of socketNames) {
|
|
584
|
+
if (deviceSocket === CHROMIUM_DEVTOOLS_SOCKET && socketName === `@${deviceSocket}`) {
|
|
585
|
+
webviews.push({
|
|
586
|
+
proc: socketName,
|
|
587
|
+
webview: CHROMIUM_WIN,
|
|
588
|
+
});
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const socketNameMatch = DEVTOOLS_SOCKET_PATTERN.exec(socketName);
|
|
593
|
+
if (!socketNameMatch) {
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
const matchedSocketName = socketNameMatch[2];
|
|
597
|
+
const crosswalkMatch = CROSSWALK_SOCKET_PATTERN.exec(socketName);
|
|
598
|
+
if (!matchedSocketName && !crosswalkMatch) {
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if ((deviceSocket && socketName === `@${deviceSocket}`) || !deviceSocket) {
|
|
603
|
+
webviews.push({
|
|
604
|
+
proc: socketName,
|
|
605
|
+
webview: matchedSocketName
|
|
606
|
+
? `${WEBVIEW_BASE}${matchedSocketName}`
|
|
607
|
+
: // @ts-expect-error: XXX crosswalkMatch can absolutely be null
|
|
608
|
+
`${WEBVIEW_BASE}${crosswalkMatch[1]}`,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return webviews;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* @this {import('../../driver').AndroidDriver}
|
|
617
|
+
* @param {import('../types').PortSpec} [portSpec]
|
|
618
|
+
* @returns {Promise<number>}
|
|
619
|
+
*/
|
|
620
|
+
async function getChromedriverPort(portSpec) {
|
|
621
|
+
// if the user didn't give us any specific information about chromedriver
|
|
622
|
+
// port ranges, just find any free port
|
|
623
|
+
if (!portSpec) {
|
|
624
|
+
const port = await getFreePort();
|
|
625
|
+
this.log.debug(`A port was not given, using random free port: ${port}`);
|
|
626
|
+
return port;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// otherwise find the free port based on a list or range provided by the user
|
|
630
|
+
this.log.debug(`Finding a free port for chromedriver using spec ${JSON.stringify(portSpec)}`);
|
|
631
|
+
let foundPort = null;
|
|
632
|
+
for (const potentialPort of portSpec) {
|
|
633
|
+
/** @type {number} */
|
|
634
|
+
let port;
|
|
635
|
+
/** @type {number} */
|
|
636
|
+
let stopPort;
|
|
637
|
+
if (Array.isArray(potentialPort)) {
|
|
638
|
+
[port, stopPort] = potentialPort.map((p) => parseInt(String(p), 10));
|
|
639
|
+
} else {
|
|
640
|
+
port = parseInt(String(potentialPort), 10); // ensure we have a number and not a string
|
|
641
|
+
stopPort = port;
|
|
642
|
+
}
|
|
643
|
+
this.log.debug(`Checking port range ${port}:${stopPort}`);
|
|
644
|
+
try {
|
|
645
|
+
foundPort = await findAPortNotInUse(port, stopPort);
|
|
646
|
+
break;
|
|
647
|
+
} catch (e) {
|
|
648
|
+
this.log.debug(`Nothing in port range ${port}:${stopPort} was available`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (foundPort === null) {
|
|
653
|
+
throw new Error(
|
|
654
|
+
`Could not find a free port for chromedriver using ` +
|
|
655
|
+
`chromedriverPorts spec ${JSON.stringify(portSpec)}`,
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
this.log.debug(`Using free port ${foundPort} for chromedriver`);
|
|
660
|
+
return foundPort;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* @this {import('../../driver').AndroidDriver}
|
|
665
|
+
* @returns {boolean}
|
|
666
|
+
*/
|
|
667
|
+
function isChromedriverAutodownloadEnabled() {
|
|
668
|
+
if (this.isFeatureEnabled(CHROMEDRIVER_AUTODOWNLOAD_FEATURE)) {
|
|
669
|
+
return true;
|
|
670
|
+
}
|
|
671
|
+
this.log.debug(
|
|
672
|
+
`Automated Chromedriver download is disabled. ` +
|
|
673
|
+
`Use '${CHROMEDRIVER_AUTODOWNLOAD_FEATURE}' server feature to enable it`,
|
|
674
|
+
);
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* @this {import('../../driver').AndroidDriver}
|
|
680
|
+
* @param {import('../../driver').AndroidDriverOpts} opts
|
|
681
|
+
* @param {string} curDeviceId
|
|
682
|
+
* @param {string} [context]
|
|
683
|
+
* @returns {Promise<Chromedriver>}
|
|
684
|
+
*/
|
|
685
|
+
export async function setupNewChromedriver(opts, curDeviceId, context) {
|
|
686
|
+
// @ts-ignore TODO: Remove the legacy
|
|
687
|
+
if (opts.chromeDriverPort) {
|
|
688
|
+
this.log.warn(
|
|
689
|
+
`The 'chromeDriverPort' capability is deprecated. Please use 'chromedriverPort' instead`,
|
|
690
|
+
);
|
|
691
|
+
// @ts-ignore TODO: Remove the legacy
|
|
692
|
+
opts.chromedriverPort = opts.chromeDriverPort;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (opts.chromedriverPort) {
|
|
696
|
+
this.log.debug(`Using user-specified port ${opts.chromedriverPort} for chromedriver`);
|
|
697
|
+
} else {
|
|
698
|
+
// if a single port wasn't given, we'll look for a free one
|
|
699
|
+
opts.chromedriverPort = await getChromedriverPort.bind(this)(opts.chromedriverPorts);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const details = context ? getWebviewDetails(this.adb, context) : undefined;
|
|
703
|
+
if (!_.isEmpty(details)) {
|
|
704
|
+
this.log.debug(
|
|
705
|
+
'Passing web view details to the Chromedriver constructor: ' +
|
|
706
|
+
JSON.stringify(details, null, 2),
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const chromedriver = new Chromedriver({
|
|
711
|
+
port: String(opts.chromedriverPort),
|
|
712
|
+
executable: opts.chromedriverExecutable,
|
|
713
|
+
// eslint-disable-next-line object-shorthand
|
|
714
|
+
adb: /** @type {any} */ (this.adb),
|
|
715
|
+
cmdArgs: /** @type {string[]} */ (opts.chromedriverArgs),
|
|
716
|
+
verbose: !!opts.showChromedriverLog,
|
|
717
|
+
executableDir: opts.chromedriverExecutableDir,
|
|
718
|
+
mappingPath: opts.chromedriverChromeMappingFile,
|
|
719
|
+
// @ts-ignore this property exists
|
|
720
|
+
bundleId: opts.chromeBundleId,
|
|
721
|
+
useSystemExecutable: opts.chromedriverUseSystemExecutable,
|
|
722
|
+
disableBuildCheck: opts.chromedriverDisableBuildCheck,
|
|
723
|
+
// @ts-ignore this is ok
|
|
724
|
+
details,
|
|
725
|
+
isAutodownloadEnabled: isChromedriverAutodownloadEnabled.bind(this)(),
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
// make sure there are chromeOptions
|
|
729
|
+
opts.chromeOptions = opts.chromeOptions || {};
|
|
730
|
+
// try out any prefixed chromeOptions,
|
|
731
|
+
// and strip the prefix
|
|
732
|
+
for (const opt of _.keys(opts)) {
|
|
733
|
+
if (opt.endsWith(':chromeOptions')) {
|
|
734
|
+
this?.log?.warn(`Merging '${opt}' into 'chromeOptions'. This may cause unexpected behavior`);
|
|
735
|
+
_.merge(opts.chromeOptions, opts[opt]);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const caps = /** @type {any} */ (createChromedriverCaps.bind(this)(opts, curDeviceId, details));
|
|
740
|
+
this.log.debug(
|
|
741
|
+
`Before starting chromedriver, androidPackage is '${caps.chromeOptions.androidPackage}'`,
|
|
742
|
+
);
|
|
743
|
+
await chromedriver.start(caps);
|
|
744
|
+
return chromedriver;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* @this {import('../../driver').AndroidDriver}
|
|
749
|
+
* @template {Chromedriver} T
|
|
750
|
+
* @param {T} chromedriver
|
|
751
|
+
* @returns {Promise<T>}
|
|
752
|
+
*/
|
|
753
|
+
export async function setupExistingChromedriver(chromedriver) {
|
|
754
|
+
// check the status by sending a simple window-based command to ChromeDriver
|
|
755
|
+
// if there is an error, we want to recreate the ChromeDriver session
|
|
756
|
+
if (!(await chromedriver.hasWorkingWebview())) {
|
|
757
|
+
this.log.debug('ChromeDriver is not associated with a window. Re-initializing the session.');
|
|
758
|
+
await chromedriver.restart();
|
|
759
|
+
}
|
|
760
|
+
return chromedriver;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* @this {import('../../driver').AndroidDriver}
|
|
765
|
+
* @returns {boolean}
|
|
766
|
+
*/
|
|
767
|
+
export function shouldDismissChromeWelcome() {
|
|
768
|
+
return (
|
|
769
|
+
!!this.opts.chromeOptions &&
|
|
770
|
+
_.isArray(this.opts.chromeOptions.args) &&
|
|
771
|
+
this.opts.chromeOptions.args.includes('--no-first-run')
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* @this {import('../../driver').AndroidDriver}
|
|
777
|
+
* @returns {Promise<void>}
|
|
778
|
+
*/
|
|
779
|
+
export async function dismissChromeWelcome() {
|
|
780
|
+
this.log.info('Trying to dismiss Chrome welcome');
|
|
781
|
+
let activity = await this.getCurrentActivity();
|
|
782
|
+
if (activity !== 'org.chromium.chrome.browser.firstrun.FirstRunActivity') {
|
|
783
|
+
this.log.info('Chrome welcome dialog never showed up! Continuing');
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
let el = await this.findElOrEls('id', 'com.android.chrome:id/terms_accept', false);
|
|
787
|
+
await this.click(/** @type {string} */ (el.ELEMENT));
|
|
788
|
+
try {
|
|
789
|
+
let el = await this.findElOrEls('id', 'com.android.chrome:id/negative_button', false);
|
|
790
|
+
await this.click(/** @type {string} */ (el.ELEMENT));
|
|
791
|
+
} catch (e) {
|
|
792
|
+
// DO NOTHING, THIS DEVICE DIDNT LAUNCH THE SIGNIN DIALOG
|
|
793
|
+
// IT MUST BE A NON GMS DEVICE
|
|
794
|
+
this.log.warn(
|
|
795
|
+
`This device did not show Chrome SignIn dialog, ${/** @type {Error} */ (e).message}`,
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* @typedef {import('appium-adb').ADB} ADB
|
|
802
|
+
*/
|