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.
Files changed (139) hide show
  1. package/CHANGELOG.md +145 -0
  2. package/README.md +216 -32
  3. package/bin/browser-commander.js +16 -0
  4. package/eslint.config.js +10 -0
  5. package/examples/react-test-app/src/App.jsx +24 -23
  6. package/extension/background.js +14 -0
  7. package/extension/manifest.json +12 -0
  8. package/extension/relay-handler.js +353 -0
  9. package/package.json +19 -4
  10. package/src/browser/attach/differences.js +131 -0
  11. package/src/browser/attach/extension-relay.js +402 -0
  12. package/src/browser/attach/index.js +81 -0
  13. package/src/browser/attach/snapshot-launch.js +146 -0
  14. package/src/browser/attach/snapshot.js +374 -0
  15. package/src/browser/attach/websocket.js +615 -0
  16. package/src/browser/browser-cookie-cache.js +12 -17
  17. package/src/browser/browser-cookie-credentials.js +6 -9
  18. package/src/browser/browser-cookies.js +5 -2
  19. package/src/browser/cdp-session.js +94 -0
  20. package/src/browser/connector.js +42 -4
  21. package/src/browser/debugging-port.js +155 -0
  22. package/src/browser/launch-options.js +63 -52
  23. package/src/browser/launch-restrictions.json +147 -0
  24. package/src/browser/launcher.js +329 -133
  25. package/src/browser/media.js +5 -0
  26. package/src/browser/migration/bookmarks.js +73 -0
  27. package/src/browser/migration/chromium-crypto.js +89 -0
  28. package/src/browser/migration/cookies.js +155 -0
  29. package/src/browser/migration/extensions.js +187 -0
  30. package/src/browser/migration/firefox-bookmarks.js +144 -0
  31. package/src/browser/migration/firefox-der.js +84 -0
  32. package/src/browser/migration/firefox-nss.js +281 -0
  33. package/src/browser/migration/firefox.js +334 -0
  34. package/src/browser/migration/fs-utils.js +51 -0
  35. package/src/browser/migration/history.js +103 -0
  36. package/src/browser/migration/index.js +307 -0
  37. package/src/browser/migration/os-crypt-keys.js +106 -0
  38. package/src/browser/migration/passwords.js +171 -0
  39. package/src/browser/migration/preferences.js +132 -0
  40. package/src/browser/migration/sqlite-snapshot.js +138 -0
  41. package/src/browser/open-in-user-browser.js +112 -0
  42. package/src/browser/parity.js +439 -0
  43. package/src/browser/profile-directory.js +102 -0
  44. package/src/browser/real-browser.js +513 -306
  45. package/src/browser/restrictions.js +148 -0
  46. package/src/browser/system-browser.js +232 -0
  47. package/src/browser/webdriver.js +572 -0
  48. package/src/cli/args.js +253 -0
  49. package/src/cli/commands.js +332 -0
  50. package/src/cli/dispatcher.js +99 -0
  51. package/src/cli/handle-methods.js +136 -0
  52. package/src/cli/handles.js +273 -0
  53. package/src/cli/high-level-methods.js +310 -0
  54. package/src/cli/main.js +67 -0
  55. package/src/cli/modules.js +100 -0
  56. package/src/cli/rpc-error.js +77 -0
  57. package/src/cli/script.js +77 -0
  58. package/src/cli/serve.js +144 -0
  59. package/src/cli/sessions.js +195 -0
  60. package/src/core/constants.js +7 -1
  61. package/src/core/engine-adapter.js +107 -1
  62. package/src/core/engine-detection.js +18 -3
  63. package/src/core/navigation-safety.js +4 -0
  64. package/src/core/webdriver-conversions.js +347 -0
  65. package/src/core/webdriver-events.js +229 -0
  66. package/src/core/webdriver-page.js +716 -0
  67. package/src/downloads/attach.js +6 -0
  68. package/src/elements/selectors.js +3 -2
  69. package/src/elements/visibility.js +4 -0
  70. package/src/exports.js +61 -2
  71. package/src/factory.js +19 -4
  72. package/src/fingerprint/apply.js +6 -0
  73. package/src/fingerprint/automation-parity.js +7 -8
  74. package/src/fingerprint/limitations.js +1 -1
  75. package/src/fingerprint/limitations.json +45 -2
  76. package/src/parity/harness.js +303 -0
  77. package/src/parity/probe.js +838 -0
  78. package/src/parity/version-page.js +144 -0
  79. package/src/utilities/subprocess.js +199 -0
  80. package/tests/e2e/api-coverage.e2e.test.js +174 -0
  81. package/tests/e2e/cli.e2e.test.js +160 -0
  82. package/tests/e2e/fingerprint-parity.e2e.test.js +104 -46
  83. package/tests/e2e/google-signin-probe.e2e.test.js +112 -0
  84. package/tests/e2e/measure-parity.e2e.test.js +118 -0
  85. package/tests/e2e/playwright.e2e.test.js +25 -17
  86. package/tests/e2e/puppeteer.e2e.test.js +17 -23
  87. package/tests/e2e/webdriver.e2e.test.js +239 -0
  88. package/tests/fixtures/firefox-nss-fixtures.mjs +211 -0
  89. package/tests/helpers/cli-fakes.js +214 -0
  90. package/tests/helpers/declared-api.js +80 -0
  91. package/tests/helpers/e2e-browser.js +40 -14
  92. package/tests/helpers/fake-chrome.js +124 -0
  93. package/tests/helpers/migration-fixtures.js +242 -0
  94. package/tests/helpers/temp-directory.js +24 -0
  95. package/tests/helpers/webdriver-mocks.js +183 -0
  96. package/tests/unit/browser/attach/extension-relay.test.js +360 -0
  97. package/tests/unit/browser/attach/snapshot-launch.test.js +312 -0
  98. package/tests/unit/browser/attach/snapshot.test.js +390 -0
  99. package/tests/unit/browser/attach/websocket.test.js +240 -0
  100. package/tests/unit/browser/browser-cookies.test.js +2 -32
  101. package/tests/unit/browser/cdp-session.test.js +162 -0
  102. package/tests/unit/browser/connector.test.js +23 -0
  103. package/tests/unit/browser/launch-building-blocks.test.js +238 -0
  104. package/tests/unit/browser/launch-options.test.js +41 -22
  105. package/tests/unit/browser/launcher.test.js +281 -102
  106. package/tests/unit/browser/media.test.js +13 -1
  107. package/tests/unit/browser/migration/bookmarks.test.js +69 -0
  108. package/tests/unit/browser/migration/chromium-crypto.test.js +94 -0
  109. package/tests/unit/browser/migration/cookies.test.js +94 -0
  110. package/tests/unit/browser/migration/extensions.test.js +104 -0
  111. package/tests/unit/browser/migration/firefox-nss.test.js +113 -0
  112. package/tests/unit/browser/migration/firefox.test.js +139 -0
  113. package/tests/unit/browser/migration/history.test.js +48 -0
  114. package/tests/unit/browser/migration/index.test.js +117 -0
  115. package/tests/unit/browser/migration/os-crypt-keys.test.js +95 -0
  116. package/tests/unit/browser/migration/passwords.test.js +144 -0
  117. package/tests/unit/browser/migration/preferences.test.js +77 -0
  118. package/tests/unit/browser/migration/sqlite-snapshot.test.js +75 -0
  119. package/tests/unit/browser/open-in-user-browser.test.js +123 -0
  120. package/tests/unit/browser/parity.test.js +281 -0
  121. package/tests/unit/browser/real-browser.test.js +458 -33
  122. package/tests/unit/browser/webdriver.test.js +473 -0
  123. package/tests/unit/cli/bin.test.js +69 -0
  124. package/tests/unit/cli/declared-api.test.js +49 -0
  125. package/tests/unit/cli/dispatcher.test.js +429 -0
  126. package/tests/unit/cli/handles.test.js +170 -0
  127. package/tests/unit/cli/main.test.js +409 -0
  128. package/tests/unit/cli/serve.test.js +167 -0
  129. package/tests/unit/core/webdriver-conversions.test.js +235 -0
  130. package/tests/unit/core/webdriver-page.test.js +534 -0
  131. package/tests/unit/fingerprint/automation-parity.test.js +17 -4
  132. package/tests/unit/fingerprint/limitations.test.js +1 -1
  133. package/tests/unit/parity/harness.test.js +138 -0
  134. package/tests/unit/scripts/ci-timeout-budgets.test.js +4 -0
  135. package/tests/unit/scripts/feature-parity.test.js +221 -0
  136. package/tests/unit/traces/reader.test.js +1 -0
  137. package/tests/unit/traces/recorder.test.js +1 -0
  138. package/tests/unit/utilities/command-stream-only.test.js +28 -0
  139. 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
+ }