browser-commander 0.20.0 → 0.21.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 +145 -0
- package/README.md +216 -32
- package/bin/browser-commander.js +16 -0
- package/eslint.config.js +10 -0
- package/examples/react-test-app/src/App.jsx +24 -23
- package/extension/background.js +14 -0
- package/extension/manifest.json +12 -0
- package/extension/relay-handler.js +353 -0
- package/package.json +19 -4
- package/src/browser/attach/differences.js +131 -0
- package/src/browser/attach/extension-relay.js +402 -0
- package/src/browser/attach/index.js +81 -0
- package/src/browser/attach/snapshot-launch.js +146 -0
- package/src/browser/attach/snapshot.js +374 -0
- package/src/browser/attach/websocket.js +615 -0
- package/src/browser/browser-cookie-cache.js +12 -17
- package/src/browser/browser-cookie-credentials.js +6 -9
- package/src/browser/browser-cookies.js +5 -2
- package/src/browser/cdp-session.js +94 -0
- package/src/browser/connector.js +42 -4
- package/src/browser/debugging-port.js +155 -0
- package/src/browser/launch-options.js +63 -52
- package/src/browser/launch-restrictions.json +147 -0
- package/src/browser/launcher.js +329 -133
- package/src/browser/media.js +5 -0
- package/src/browser/migration/bookmarks.js +73 -0
- package/src/browser/migration/chromium-crypto.js +89 -0
- package/src/browser/migration/cookies.js +155 -0
- package/src/browser/migration/extensions.js +187 -0
- package/src/browser/migration/firefox-bookmarks.js +144 -0
- package/src/browser/migration/firefox-der.js +84 -0
- package/src/browser/migration/firefox-nss.js +281 -0
- package/src/browser/migration/firefox.js +334 -0
- package/src/browser/migration/fs-utils.js +51 -0
- package/src/browser/migration/history.js +103 -0
- package/src/browser/migration/index.js +307 -0
- package/src/browser/migration/os-crypt-keys.js +106 -0
- package/src/browser/migration/passwords.js +171 -0
- package/src/browser/migration/preferences.js +132 -0
- package/src/browser/migration/sqlite-snapshot.js +138 -0
- package/src/browser/open-in-user-browser.js +112 -0
- package/src/browser/parity.js +439 -0
- package/src/browser/profile-directory.js +102 -0
- package/src/browser/real-browser.js +513 -306
- package/src/browser/restrictions.js +148 -0
- package/src/browser/system-browser.js +232 -0
- package/src/browser/webdriver.js +572 -0
- package/src/cli/args.js +253 -0
- package/src/cli/commands.js +332 -0
- package/src/cli/dispatcher.js +99 -0
- package/src/cli/handle-methods.js +136 -0
- package/src/cli/handles.js +273 -0
- package/src/cli/high-level-methods.js +310 -0
- package/src/cli/main.js +67 -0
- package/src/cli/modules.js +100 -0
- package/src/cli/rpc-error.js +77 -0
- package/src/cli/script.js +77 -0
- package/src/cli/serve.js +144 -0
- package/src/cli/sessions.js +195 -0
- package/src/core/constants.js +7 -1
- package/src/core/engine-adapter.js +107 -1
- package/src/core/engine-detection.js +18 -3
- package/src/core/navigation-safety.js +4 -0
- package/src/core/webdriver-conversions.js +347 -0
- package/src/core/webdriver-events.js +229 -0
- package/src/core/webdriver-page.js +716 -0
- package/src/downloads/attach.js +6 -0
- package/src/elements/selectors.js +3 -2
- package/src/elements/visibility.js +4 -0
- package/src/exports.js +61 -2
- package/src/factory.js +19 -4
- package/src/fingerprint/apply.js +6 -0
- package/src/fingerprint/automation-parity.js +7 -8
- package/src/fingerprint/limitations.js +1 -1
- package/src/fingerprint/limitations.json +45 -2
- package/src/parity/harness.js +303 -0
- package/src/parity/probe.js +838 -0
- package/src/parity/version-page.js +144 -0
- package/src/utilities/subprocess.js +199 -0
- package/tests/e2e/api-coverage.e2e.test.js +174 -0
- package/tests/e2e/cli.e2e.test.js +160 -0
- package/tests/e2e/fingerprint-parity.e2e.test.js +104 -46
- package/tests/e2e/google-signin-probe.e2e.test.js +112 -0
- package/tests/e2e/measure-parity.e2e.test.js +118 -0
- package/tests/e2e/playwright.e2e.test.js +25 -17
- package/tests/e2e/puppeteer.e2e.test.js +17 -23
- package/tests/e2e/webdriver.e2e.test.js +239 -0
- package/tests/fixtures/firefox-nss-fixtures.mjs +211 -0
- package/tests/helpers/cli-fakes.js +214 -0
- package/tests/helpers/declared-api.js +80 -0
- package/tests/helpers/e2e-browser.js +40 -14
- package/tests/helpers/fake-chrome.js +124 -0
- package/tests/helpers/migration-fixtures.js +242 -0
- package/tests/helpers/temp-directory.js +24 -0
- package/tests/helpers/webdriver-mocks.js +183 -0
- package/tests/unit/browser/attach/extension-relay.test.js +360 -0
- package/tests/unit/browser/attach/snapshot-launch.test.js +312 -0
- package/tests/unit/browser/attach/snapshot.test.js +390 -0
- package/tests/unit/browser/attach/websocket.test.js +240 -0
- package/tests/unit/browser/browser-cookies.test.js +2 -32
- package/tests/unit/browser/cdp-session.test.js +162 -0
- package/tests/unit/browser/connector.test.js +23 -0
- package/tests/unit/browser/launch-building-blocks.test.js +238 -0
- package/tests/unit/browser/launch-options.test.js +41 -22
- package/tests/unit/browser/launcher.test.js +281 -102
- package/tests/unit/browser/media.test.js +13 -1
- package/tests/unit/browser/migration/bookmarks.test.js +69 -0
- package/tests/unit/browser/migration/chromium-crypto.test.js +94 -0
- package/tests/unit/browser/migration/cookies.test.js +94 -0
- package/tests/unit/browser/migration/extensions.test.js +104 -0
- package/tests/unit/browser/migration/firefox-nss.test.js +113 -0
- package/tests/unit/browser/migration/firefox.test.js +139 -0
- package/tests/unit/browser/migration/history.test.js +48 -0
- package/tests/unit/browser/migration/index.test.js +117 -0
- package/tests/unit/browser/migration/os-crypt-keys.test.js +95 -0
- package/tests/unit/browser/migration/passwords.test.js +144 -0
- package/tests/unit/browser/migration/preferences.test.js +77 -0
- package/tests/unit/browser/migration/sqlite-snapshot.test.js +75 -0
- package/tests/unit/browser/open-in-user-browser.test.js +123 -0
- package/tests/unit/browser/parity.test.js +281 -0
- package/tests/unit/browser/real-browser.test.js +458 -33
- package/tests/unit/browser/webdriver.test.js +473 -0
- package/tests/unit/cli/bin.test.js +69 -0
- package/tests/unit/cli/declared-api.test.js +49 -0
- package/tests/unit/cli/dispatcher.test.js +429 -0
- package/tests/unit/cli/handles.test.js +170 -0
- package/tests/unit/cli/main.test.js +409 -0
- package/tests/unit/cli/serve.test.js +167 -0
- package/tests/unit/core/webdriver-conversions.test.js +235 -0
- package/tests/unit/core/webdriver-page.test.js +534 -0
- package/tests/unit/fingerprint/automation-parity.test.js +17 -4
- package/tests/unit/fingerprint/limitations.test.js +1 -1
- package/tests/unit/parity/harness.test.js +138 -0
- package/tests/unit/scripts/ci-timeout-budgets.test.js +4 -0
- package/tests/unit/scripts/feature-parity.test.js +221 -0
- package/tests/unit/traces/reader.test.js +1 -0
- package/tests/unit/traces/recorder.test.js +1 -0
- package/tests/unit/utilities/command-stream-only.test.js +28 -0
- package/tests/unit/utilities/subprocess.test.js +80 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebDriver BiDi events as the page events the rest of Browser Commander
|
|
3
|
+
* listens to (issue #104).
|
|
4
|
+
*
|
|
5
|
+
* Classic WebDriver is request/response only: nothing tells the client that a
|
|
6
|
+
* console message was logged, a dialog opened or a request went out. BiDi
|
|
7
|
+
* does, so when the session has a BiDi connection the page facade subscribes
|
|
8
|
+
* to the matching BiDi event the first time something listens for a page
|
|
9
|
+
* event, and re-emits it in the Puppeteer shape:
|
|
10
|
+
*
|
|
11
|
+
* | page event | BiDi event |
|
|
12
|
+
* | ----------------- | ------------------------------------------------- |
|
|
13
|
+
* | `console` | `log.entryAdded` (console entries) |
|
|
14
|
+
* | `pageerror` | `log.entryAdded` (type `javascript`) |
|
|
15
|
+
* | `framenavigated` | `browsingContext.navigationCommitted` (or |
|
|
16
|
+
* | | `domContentLoaded`), `fragmentNavigated`, |
|
|
17
|
+
* | | `historyUpdated` |
|
|
18
|
+
* | `domcontentloaded`| `browsingContext.domContentLoaded` |
|
|
19
|
+
* | `load` | `browsingContext.load` |
|
|
20
|
+
* | `dialog` | `browsingContext.userPromptOpened` |
|
|
21
|
+
* | `request` | `network.beforeRequestSent` |
|
|
22
|
+
* | `requestfinished` | `network.responseCompleted` |
|
|
23
|
+
* | `response` | `network.responseCompleted` |
|
|
24
|
+
* | `requestfailed` | `network.fetchError` |
|
|
25
|
+
*
|
|
26
|
+
* Without BiDi the events stay silent, which is limitation
|
|
27
|
+
* `webdriver-classic-has-no-events`.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { toConsoleMessage, toNetworkRequest } from './webdriver-conversions.js';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The BiDi events each page event needs. Where a list has alternatives
|
|
34
|
+
* (`oneOf`), the first one the driver accepts is used: older drivers do not
|
|
35
|
+
* know `browsingContext.navigationCommitted`, and `domContentLoaded` is the
|
|
36
|
+
* closest event every driver has.
|
|
37
|
+
*/
|
|
38
|
+
export const PAGE_EVENT_SOURCES = Object.freeze({
|
|
39
|
+
console: [['log.entryAdded']],
|
|
40
|
+
pageerror: [['log.entryAdded']],
|
|
41
|
+
framenavigated: [
|
|
42
|
+
['browsingContext.navigationCommitted', 'browsingContext.domContentLoaded'],
|
|
43
|
+
['browsingContext.fragmentNavigated'],
|
|
44
|
+
['browsingContext.historyUpdated'],
|
|
45
|
+
],
|
|
46
|
+
domcontentloaded: [['browsingContext.domContentLoaded']],
|
|
47
|
+
load: [['browsingContext.load']],
|
|
48
|
+
dialog: [['browsingContext.userPromptOpened']],
|
|
49
|
+
request: [['network.beforeRequestSent']],
|
|
50
|
+
requestfinished: [['network.responseCompleted']],
|
|
51
|
+
response: [['network.responseCompleted']],
|
|
52
|
+
requestfailed: [['network.fetchError']],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const NAVIGATION_EVENTS = new Set([
|
|
56
|
+
'browsingContext.navigationCommitted',
|
|
57
|
+
'browsingContext.fragmentNavigated',
|
|
58
|
+
'browsingContext.historyUpdated',
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A BiDi user prompt as a Puppeteer Dialog.
|
|
63
|
+
*
|
|
64
|
+
* @param {Object} page - The WebDriverPage the prompt belongs to
|
|
65
|
+
* @param {Object} params - `browsingContext.userPromptOpened` parameters
|
|
66
|
+
* @returns {Object}
|
|
67
|
+
*/
|
|
68
|
+
function toDialog(page, params) {
|
|
69
|
+
const handle = (accept, userText) =>
|
|
70
|
+
page.bidiCommand('browsingContext.handleUserPrompt', {
|
|
71
|
+
context: params.context,
|
|
72
|
+
accept,
|
|
73
|
+
...(userText !== undefined ? { userText: String(userText) } : {}),
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
type: () => params.type,
|
|
77
|
+
message: () => params.message ?? '',
|
|
78
|
+
defaultValue: () => params.defaultValue ?? '',
|
|
79
|
+
accept: (promptText) => handle(true, promptText),
|
|
80
|
+
dismiss: () => handle(false),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Bridge BiDi events onto a WebDriverPage.
|
|
86
|
+
*
|
|
87
|
+
* @param {Object} page - The WebDriverPage (an EventEmitter with `bidi()`)
|
|
88
|
+
* @returns {{subscribe: function(string): Promise<void>, ready: function(): Promise<void>, dispose: function(): Promise<void>}}
|
|
89
|
+
*/
|
|
90
|
+
export function createWebDriverEventBridge(page) {
|
|
91
|
+
const subscribed = new Map(); // page event -> Promise
|
|
92
|
+
const handles = [];
|
|
93
|
+
// Which BiDi method was chosen for the navigation alternative; the
|
|
94
|
+
// domContentLoaded fallback also counts as a navigation then.
|
|
95
|
+
let navigationSource = null;
|
|
96
|
+
|
|
97
|
+
const isTop = (context) =>
|
|
98
|
+
context === undefined || context === page.contextIdSync();
|
|
99
|
+
|
|
100
|
+
function dispatch(method, params) {
|
|
101
|
+
switch (method) {
|
|
102
|
+
case 'log.entryAdded': {
|
|
103
|
+
if (!isTop(params.source?.context)) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (params.type === 'javascript') {
|
|
107
|
+
const error = new Error(params.text ?? 'Uncaught error');
|
|
108
|
+
error.stackTrace = params.stackTrace;
|
|
109
|
+
page.emit('pageerror', error);
|
|
110
|
+
} else {
|
|
111
|
+
page.emit('console', toConsoleMessage(params));
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
case 'browsingContext.userPromptOpened':
|
|
116
|
+
if (isTop(params.context)) {
|
|
117
|
+
page.emit('dialog', toDialog(page, params));
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
case 'network.beforeRequestSent':
|
|
121
|
+
page.emit('request', toNetworkRequest(params));
|
|
122
|
+
return;
|
|
123
|
+
case 'network.responseCompleted': {
|
|
124
|
+
const request = toNetworkRequest(params);
|
|
125
|
+
page.emit('response', request.response());
|
|
126
|
+
page.emit('requestfinished', request);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
case 'network.fetchError':
|
|
130
|
+
page.emit('requestfailed', toNetworkRequest(params));
|
|
131
|
+
return;
|
|
132
|
+
default:
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!isTop(params.context)) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (NAVIGATION_EVENTS.has(method) || method === navigationSource) {
|
|
140
|
+
page.recordNavigation(params.url);
|
|
141
|
+
}
|
|
142
|
+
if (method === 'browsingContext.domContentLoaded') {
|
|
143
|
+
page.emit('domcontentloaded');
|
|
144
|
+
} else if (method === 'browsingContext.load') {
|
|
145
|
+
page.emit('load');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const methodHandles = new Map(); // BiDi method -> Promise<handle>
|
|
150
|
+
|
|
151
|
+
function listen(method) {
|
|
152
|
+
if (!methodHandles.has(method)) {
|
|
153
|
+
methodHandles.set(
|
|
154
|
+
method,
|
|
155
|
+
page.subscribeBidi(method, (params) => dispatch(method, params))
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return methodHandles.get(method);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function listenToFirstSupported(alternatives) {
|
|
162
|
+
let lastError = null;
|
|
163
|
+
for (const method of alternatives) {
|
|
164
|
+
try {
|
|
165
|
+
const handle = await listen(method);
|
|
166
|
+
handles.push(handle);
|
|
167
|
+
if (alternatives.length > 1) {
|
|
168
|
+
navigationSource = method;
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
methodHandles.delete(method);
|
|
173
|
+
lastError = error;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
throw lastError;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function subscribeSources(eventName) {
|
|
180
|
+
const bidi = await page.bidi();
|
|
181
|
+
if (!bidi) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
// Events are filtered to the page's own browsing context, so its id has
|
|
185
|
+
// to be known before the first one arrives.
|
|
186
|
+
await page.contextId();
|
|
187
|
+
for (const alternatives of PAGE_EVENT_SOURCES[eventName]) {
|
|
188
|
+
await listenToFirstSupported(alternatives);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
/**
|
|
194
|
+
* Make sure the BiDi events behind a page event are subscribed.
|
|
195
|
+
* Unknown event names and classic sessions are a no-op.
|
|
196
|
+
*
|
|
197
|
+
* @param {string} eventName
|
|
198
|
+
* @returns {Promise<void>}
|
|
199
|
+
*/
|
|
200
|
+
subscribe(eventName) {
|
|
201
|
+
if (!PAGE_EVENT_SOURCES[eventName]) {
|
|
202
|
+
return Promise.resolve();
|
|
203
|
+
}
|
|
204
|
+
if (!subscribed.has(eventName)) {
|
|
205
|
+
const pending = subscribeSources(eventName).catch((error) => {
|
|
206
|
+
page.recordBridgeError(eventName, error);
|
|
207
|
+
});
|
|
208
|
+
subscribed.set(eventName, pending);
|
|
209
|
+
}
|
|
210
|
+
return subscribed.get(eventName);
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
/** Resolves once every subscription requested so far is in place. */
|
|
214
|
+
async ready() {
|
|
215
|
+
await Promise.all(subscribed.values());
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
/** Remove every BiDi subscription the bridge made. */
|
|
219
|
+
async dispose() {
|
|
220
|
+
const all = [...new Set(handles)];
|
|
221
|
+
handles.length = 0;
|
|
222
|
+
subscribed.clear();
|
|
223
|
+
methodHandles.clear();
|
|
224
|
+
await Promise.all(
|
|
225
|
+
all.map((handle) => Promise.resolve(handle?.unsubscribe?.()))
|
|
226
|
+
).catch(() => {});
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|