browser-commander 0.23.0 → 0.24.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/.jscpd-baseline.json +47 -29
- package/CHANGELOG.md +8 -0
- package/README.md +12 -3
- package/eslint.config.js +1 -1
- package/examples/react-test-app/package-lock.json +890 -0
- package/examples/react-test-app/package.json +4 -4
- package/package.json +17 -17
- package/src/browser/connector.js +46 -4
- package/src/browser/launcher.js +26 -1
- package/src/browser/real-browser.js +8 -2
- package/src/browser/storage-state.js +27 -0
- package/src/browser/webdriver.js +18 -2
- package/src/cli/args.js +5 -0
- package/src/cli/commands.js +10 -6
- package/src/cli/handle-methods.js +19 -2
- package/src/cli/high-level-methods.js +20 -3
- package/src/cli/modules.js +5 -1
- package/src/cli/sessions.js +14 -2
- package/src/tests/index.js +1 -0
- package/tests/e2e/engine-matrix.e2e.test.js +107 -0
- package/tests/e2e/playwright.e2e.test.js +11 -17
- package/tests/e2e/puppeteer.e2e.test.js +13 -17
- package/tests/unit/cli/dispatcher.test.js +2 -2
- package/tests/unit/cli/selenium-parity.test.js +186 -0
- package/tests/unit/tests/index.test.js +2 -1
- package/tests/unit/traces/links.test.js +2 -2
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
"preview": "vite preview"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"react": "^
|
|
13
|
-
"react-dom": "^
|
|
12
|
+
"react": "^19.3.0",
|
|
13
|
+
"react-dom": "^19.3.0"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@vitejs/plugin-react": "^
|
|
17
|
-
"vite": "^
|
|
16
|
+
"@vitejs/plugin-react": "^6.1.2",
|
|
17
|
+
"vite": "^8.3.2"
|
|
18
18
|
}
|
|
19
19
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "browser-commander",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Universal browser automation library that supports
|
|
3
|
+
"version": "0.24.0",
|
|
4
|
+
"description": "Universal browser automation library that supports Playwright, Puppeteer, and Selenium with a unified API",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"browser-commander": "bin/browser-commander.js"
|
|
@@ -61,12 +61,12 @@
|
|
|
61
61
|
},
|
|
62
62
|
"homepage": "https://github.com/link-foundation/browser-commander#readme",
|
|
63
63
|
"engines": {
|
|
64
|
-
"node": ">=22.
|
|
64
|
+
"node": ">=22.12.0"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
|
-
"playwright": ">=1.
|
|
68
|
-
"puppeteer": ">=
|
|
69
|
-
"selenium-webdriver": ">=4.
|
|
67
|
+
"playwright": ">=1.63.0",
|
|
68
|
+
"puppeteer": ">=25.12.0",
|
|
69
|
+
"selenium-webdriver": ">=4.50.0"
|
|
70
70
|
},
|
|
71
71
|
"peerDependenciesMeta": {
|
|
72
72
|
"playwright": {
|
|
@@ -81,24 +81,24 @@
|
|
|
81
81
|
},
|
|
82
82
|
"dependencies": {
|
|
83
83
|
"better-sqlite3": "^13.0.3",
|
|
84
|
-
"command-stream": "^1.
|
|
85
|
-
"links-notation": "^0.
|
|
84
|
+
"command-stream": "^1.4.0",
|
|
85
|
+
"links-notation": "^0.22.0",
|
|
86
86
|
"log-lazy": "^1.0.4",
|
|
87
87
|
"test-anywhere": "^0.9.1"
|
|
88
88
|
},
|
|
89
89
|
"devDependencies": {
|
|
90
|
-
"@changesets/cli": "^3.0.
|
|
90
|
+
"@changesets/cli": "^3.0.3",
|
|
91
91
|
"@eslint/js": "^10.0.1",
|
|
92
|
-
"eslint": "^10.
|
|
92
|
+
"eslint": "^10.12.0",
|
|
93
93
|
"eslint-config-prettier": "^10.1.8",
|
|
94
94
|
"eslint-plugin-prettier": "^5.5.6",
|
|
95
|
-
"jscpd": "^5.
|
|
96
|
-
"jsdoc": "^4.0.
|
|
97
|
-
"playwright": "^1.
|
|
98
|
-
"prettier": "^3.9.
|
|
99
|
-
"puppeteer": "^25.
|
|
100
|
-
"selenium-webdriver": "^4.
|
|
101
|
-
"typescript": "^
|
|
95
|
+
"jscpd": "^5.4.0",
|
|
96
|
+
"jsdoc": "^4.0.5",
|
|
97
|
+
"playwright": "^1.63.0",
|
|
98
|
+
"prettier": "^3.9.9",
|
|
99
|
+
"puppeteer": "^25.12.0",
|
|
100
|
+
"selenium-webdriver": "^4.50.0",
|
|
101
|
+
"typescript": "^7.0.2",
|
|
102
102
|
"yaml": "^2.9.1"
|
|
103
103
|
},
|
|
104
104
|
"allowScripts": {
|
package/src/browser/connector.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { attachDownloads } from '../downloads/attach.js';
|
|
2
|
+
import { connectWebDriver, launchWebDriver } from './webdriver.js';
|
|
2
3
|
import {
|
|
3
4
|
loadStorageState,
|
|
4
5
|
restorePlaywrightStorageState,
|
|
5
6
|
restorePuppeteerStorageState,
|
|
7
|
+
restoreWebDriverStorageState,
|
|
6
8
|
} from './storage-state.js';
|
|
7
9
|
|
|
8
10
|
/**
|
|
@@ -11,9 +13,9 @@ import {
|
|
|
11
13
|
* @param {string} engine
|
|
12
14
|
*/
|
|
13
15
|
export function assertSupportedEngine(engine) {
|
|
14
|
-
if (!['playwright', 'puppeteer'].includes(engine)) {
|
|
16
|
+
if (!['playwright', 'puppeteer', 'selenium'].includes(engine)) {
|
|
15
17
|
throw new Error(
|
|
16
|
-
`Invalid engine: ${engine}. Expected 'playwright' or '
|
|
18
|
+
`Invalid engine: ${engine}. Expected 'playwright', 'puppeteer' or 'selenium'`
|
|
17
19
|
);
|
|
18
20
|
}
|
|
19
21
|
}
|
|
@@ -28,6 +30,28 @@ function validateConnectionOptions({ engine, cdpEndpoint, wsEndpoint }) {
|
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
async function connectSelenium(options, dependencies) {
|
|
34
|
+
if (Boolean(options.serverUrl) === Boolean(options.cdpEndpoint)) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
'selenium requires exactly one of serverUrl or cdpEndpoint'
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
if (options.wsEndpoint) {
|
|
40
|
+
throw new Error('selenium requires an HTTP cdpEndpoint or serverUrl');
|
|
41
|
+
}
|
|
42
|
+
const connected = options.serverUrl
|
|
43
|
+
? await (dependencies.connectWebDriver ?? connectWebDriver)(options)
|
|
44
|
+
: await (dependencies.launchWebDriver ?? launchWebDriver)({
|
|
45
|
+
...options,
|
|
46
|
+
debuggerAddress: new URL(options.cdpEndpoint).host,
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
...connected,
|
|
50
|
+
browser: connected.driver,
|
|
51
|
+
disconnect: connected.close,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
31
55
|
function addDefinedOptions(options, values) {
|
|
32
56
|
for (const [key, value] of Object.entries(values)) {
|
|
33
57
|
if (value !== undefined) {
|
|
@@ -135,10 +159,12 @@ async function connectPuppeteer({ options, loadPuppeteer, storageState }) {
|
|
|
135
159
|
}
|
|
136
160
|
|
|
137
161
|
/**
|
|
138
|
-
* Attach
|
|
162
|
+
* Attach over CDP or create a native session on a WebDriver server.
|
|
139
163
|
*
|
|
140
164
|
* @param {Object} options - Connection options
|
|
141
|
-
* @param {'playwright'|'puppeteer'} [options.engine='playwright'] - Automation engine
|
|
165
|
+
* @param {'playwright'|'puppeteer'|'selenium'} [options.engine='playwright'] - Automation engine
|
|
166
|
+
* @param {string} [options.serverUrl] - Selenium Grid or WebDriver server URL
|
|
167
|
+
* @param {Object} [options.capabilities] - Native WebDriver capabilities
|
|
142
168
|
* @param {string} [options.cdpEndpoint] - HTTP DevTools endpoint, such as http://127.0.0.1:9222
|
|
143
169
|
* @param {string} [options.wsEndpoint] - DevTools browser WebSocket endpoint
|
|
144
170
|
* @param {number} [options.slowMo] - Delay engine operations by this many milliseconds
|
|
@@ -171,6 +197,22 @@ export async function connectBrowserWithDependencies(
|
|
|
171
197
|
verbose: false,
|
|
172
198
|
...options,
|
|
173
199
|
};
|
|
200
|
+
if (normalizedOptions.engine === 'selenium') {
|
|
201
|
+
assertSupportedEngine(normalizedOptions.engine);
|
|
202
|
+
const storageState = await prepareStorageState(normalizedOptions);
|
|
203
|
+
const result = await connectSelenium(normalizedOptions, dependencies);
|
|
204
|
+
try {
|
|
205
|
+
await restoreWebDriverStorageState({ page: result.page, storageState });
|
|
206
|
+
const downloads = await attachDownloads({
|
|
207
|
+
...normalizedOptions,
|
|
208
|
+
...result,
|
|
209
|
+
});
|
|
210
|
+
return { ...result, downloads };
|
|
211
|
+
} catch (error) {
|
|
212
|
+
await result.close();
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
174
216
|
validateConnectionOptions(normalizedOptions);
|
|
175
217
|
|
|
176
218
|
const storageState = await prepareStorageState(normalizedOptions);
|
package/src/browser/launcher.js
CHANGED
|
@@ -16,10 +16,12 @@ import { resolveRestrictions } from './restrictions.js';
|
|
|
16
16
|
import { resolveSystemBrowserExecutable } from './system-browser.js';
|
|
17
17
|
import { applyFingerprint } from '../fingerprint/apply.js';
|
|
18
18
|
import { attachDownloads } from '../downloads/attach.js';
|
|
19
|
+
import { launchWebDriver } from './webdriver.js';
|
|
19
20
|
import {
|
|
20
21
|
loadStorageState,
|
|
21
22
|
restorePlaywrightStorageState,
|
|
22
23
|
restorePuppeteerStorageState,
|
|
24
|
+
restoreWebDriverStorageState,
|
|
23
25
|
} from './storage-state.js';
|
|
24
26
|
|
|
25
27
|
/** How `launchBrowser` starts the browser (issue #103). */
|
|
@@ -131,6 +133,13 @@ async function launchReal(options, dependencies) {
|
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
async function launchWithEngine(options, dependencies) {
|
|
136
|
+
if (options.engine === 'selenium') {
|
|
137
|
+
const launched = await (dependencies.launchWebDriver ?? launchWebDriver)({
|
|
138
|
+
...options,
|
|
139
|
+
args: [...options.args, ...options.extraArgs],
|
|
140
|
+
});
|
|
141
|
+
return { ...launched, browser: launched.driver };
|
|
142
|
+
}
|
|
134
143
|
const {
|
|
135
144
|
engine,
|
|
136
145
|
userDataDir: requestedUserDataDir,
|
|
@@ -261,7 +270,7 @@ async function unfocusAddressBar(page, verbose, settleMs = 500) {
|
|
|
261
270
|
* limitations.json (`engine-launch-switches`).
|
|
262
271
|
*
|
|
263
272
|
* @param {Object} options - Configuration options
|
|
264
|
-
* @param {string} [options.engine='playwright'] - Browser automation engine: 'playwright' or '
|
|
273
|
+
* @param {string} [options.engine='playwright'] - Browser automation engine: 'playwright', 'puppeteer' or 'selenium'
|
|
265
274
|
* @param {'real'|'engine'} [options.launch='real'] - Who starts the browser: Browser Commander (a hand-started command line) or the automation engine
|
|
266
275
|
* @param {string} [options.userDataDir] - Persistent profile directory. When omitted a fresh temporary profile is created and deleted on close.
|
|
267
276
|
* @param {boolean} [options.headless=false] - Run in headless mode
|
|
@@ -309,6 +318,16 @@ export async function launchBrowserWithDependencies(
|
|
|
309
318
|
...rest
|
|
310
319
|
} = options;
|
|
311
320
|
validateLaunchMode({ engine, launch, attach: rest.attach });
|
|
321
|
+
if (engine === 'selenium') {
|
|
322
|
+
if (colorScheme !== undefined || fingerprint !== undefined || downloads) {
|
|
323
|
+
throw new Error(
|
|
324
|
+
'selenium does not support CDP emulation, fingerprint overrides or managed downloads (webdriver-no-cdp-emulation)'
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
if (launch === 'real' && rest.browser === 'firefox') {
|
|
328
|
+
throw new Error("selenium Firefox requires launch: 'engine'");
|
|
329
|
+
}
|
|
330
|
+
}
|
|
312
331
|
// Validate arguments before anything is started or written to disk.
|
|
313
332
|
resolveChromeArgs({ args, extraArgs, ignoreDefaultArgs, restrictions });
|
|
314
333
|
const resolvedStorageState = await loadStorageState(storageState);
|
|
@@ -341,6 +360,12 @@ export async function launchBrowserWithDependencies(
|
|
|
341
360
|
}
|
|
342
361
|
|
|
343
362
|
try {
|
|
363
|
+
if (engine === 'selenium' && launch === 'engine') {
|
|
364
|
+
await restoreWebDriverStorageState({
|
|
365
|
+
page,
|
|
366
|
+
storageState: resolvedStorageState,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
344
369
|
// A Playwright-launched context applies colorScheme itself; an attached
|
|
345
370
|
// browser and Puppeteer need page-level emulation.
|
|
346
371
|
const emulateColorScheme =
|
|
@@ -259,6 +259,10 @@ async function waitForExit(browserProcess, timeout) {
|
|
|
259
259
|
}
|
|
260
260
|
|
|
261
261
|
async function requestBrowserClose({ engine, browser, originalClose }) {
|
|
262
|
+
if (engine === 'selenium') {
|
|
263
|
+
await originalClose();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
262
266
|
if (engine === 'puppeteer') {
|
|
263
267
|
// A connected Puppeteer Browser sends Browser.close and waits for it.
|
|
264
268
|
await originalClose.call(browser);
|
|
@@ -278,8 +282,9 @@ function createCloser({
|
|
|
278
282
|
userDataDir,
|
|
279
283
|
temporaryProfile,
|
|
280
284
|
closeTimeout,
|
|
285
|
+
closeConnection,
|
|
281
286
|
}) {
|
|
282
|
-
const originalClose = browser?.close;
|
|
287
|
+
const originalClose = closeConnection ?? browser?.close;
|
|
283
288
|
let closing;
|
|
284
289
|
const close = () => {
|
|
285
290
|
closing ??= (async () => {
|
|
@@ -298,7 +303,7 @@ function createCloser({
|
|
|
298
303
|
})();
|
|
299
304
|
return closing;
|
|
300
305
|
};
|
|
301
|
-
if (browser && originalClose) {
|
|
306
|
+
if (browser && originalClose && engine !== 'selenium') {
|
|
302
307
|
// `browser.close()` means "close the browser" for every caller, so it
|
|
303
308
|
// shuts the spawned process down and deletes a temporary profile too.
|
|
304
309
|
browser.close = close;
|
|
@@ -670,6 +675,7 @@ export async function launchAndConnectRealBrowserWithDependencies(
|
|
|
670
675
|
userDataDir,
|
|
671
676
|
temporaryProfile,
|
|
672
677
|
closeTimeout,
|
|
678
|
+
closeConnection: engine === 'selenium' ? connection.close : undefined,
|
|
673
679
|
});
|
|
674
680
|
return {
|
|
675
681
|
...connection,
|
|
@@ -30,6 +30,33 @@ function restoreOriginLocalStorage(origins) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/** Restore classic WebDriver cookies; origin preload scripts require BiDi. */
|
|
34
|
+
export async function restoreWebDriverStorageState({ page, storageState }) {
|
|
35
|
+
if (!storageState) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const origins = storageState.origins ?? [];
|
|
39
|
+
if (origins.length) {
|
|
40
|
+
await page.evaluateOnNewDocument(restoreOriginLocalStorage, origins);
|
|
41
|
+
await page.evaluate(restoreOriginLocalStorage, origins);
|
|
42
|
+
}
|
|
43
|
+
const originalUrl = page.url();
|
|
44
|
+
const cookies = storageState.cookies ?? [];
|
|
45
|
+
try {
|
|
46
|
+
for (const cookie of cookies) {
|
|
47
|
+
const origin = cookie.url
|
|
48
|
+
? new URL(cookie.url).origin
|
|
49
|
+
: `${cookie.secure ? 'https' : 'http'}://${cookie.domain.replace(/^\./u, '')}`;
|
|
50
|
+
await page.goto(origin);
|
|
51
|
+
await page.setCookie(cookie);
|
|
52
|
+
}
|
|
53
|
+
} finally {
|
|
54
|
+
if (cookies.length) {
|
|
55
|
+
await page.goto(originalUrl);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
33
60
|
/**
|
|
34
61
|
* Apply Playwright-compatible storage state to a Puppeteer page.
|
|
35
62
|
* @param {Object} options - Restore options
|
package/src/browser/webdriver.js
CHANGED
|
@@ -302,11 +302,20 @@ export function buildBrowserOptions({
|
|
|
302
302
|
args,
|
|
303
303
|
automationParity,
|
|
304
304
|
bidi,
|
|
305
|
+
debuggerAddress,
|
|
305
306
|
}) {
|
|
306
307
|
let options;
|
|
307
308
|
let browserArgs;
|
|
308
309
|
if (browser === 'chrome') {
|
|
309
310
|
options = new selenium.chrome.Options();
|
|
311
|
+
if (debuggerAddress) {
|
|
312
|
+
options.debuggerAddress(debuggerAddress);
|
|
313
|
+
if (bidi) {
|
|
314
|
+
options.set('webSocketUrl', true);
|
|
315
|
+
options.set('unhandledPromptBehavior', 'ignore');
|
|
316
|
+
}
|
|
317
|
+
return { options, args: [] };
|
|
318
|
+
}
|
|
310
319
|
browserArgs = buildRealBrowserArgs({
|
|
311
320
|
userDataDir,
|
|
312
321
|
remoteDebuggingPort,
|
|
@@ -450,8 +459,12 @@ export async function launchWebDriver(options = {}, dependencies = {}) {
|
|
|
450
459
|
env,
|
|
451
460
|
startupTimeout = 30_000,
|
|
452
461
|
verbose = false,
|
|
462
|
+
debuggerAddress,
|
|
453
463
|
} = options;
|
|
454
464
|
assertBrowser(browser);
|
|
465
|
+
if (debuggerAddress && browser !== 'chrome') {
|
|
466
|
+
throw new Error('debuggerAddress is only supported by Chrome WebDriver');
|
|
467
|
+
}
|
|
455
468
|
if (browser === 'firefox' && restrictions.length > 0) {
|
|
456
469
|
throw new Error(
|
|
457
470
|
'Launch restrictions are Chrome switches; not available for firefox'
|
|
@@ -476,8 +489,10 @@ export async function launchWebDriver(options = {}, dependencies = {}) {
|
|
|
476
489
|
...(dependencies.platform ? { platform: dependencies.platform } : {}),
|
|
477
490
|
});
|
|
478
491
|
|
|
479
|
-
const temporaryProfile = !requestedUserDataDir;
|
|
480
|
-
const userDataDir =
|
|
492
|
+
const temporaryProfile = !debuggerAddress && !requestedUserDataDir;
|
|
493
|
+
const userDataDir = debuggerAddress
|
|
494
|
+
? undefined
|
|
495
|
+
: await resolveProfile(browser, requestedUserDataDir);
|
|
481
496
|
const childEnv = driverEnvironment(restrictions, env);
|
|
482
497
|
|
|
483
498
|
let driverProcess;
|
|
@@ -513,6 +528,7 @@ export async function launchWebDriver(options = {}, dependencies = {}) {
|
|
|
513
528
|
args,
|
|
514
529
|
automationParity,
|
|
515
530
|
bidi,
|
|
531
|
+
debuggerAddress,
|
|
516
532
|
});
|
|
517
533
|
driver = await buildSession({
|
|
518
534
|
selenium,
|
package/src/cli/args.js
CHANGED
|
@@ -31,12 +31,15 @@ const BROWSER_OPTIONS = Object.freeze({
|
|
|
31
31
|
'local-state': STRINGS,
|
|
32
32
|
'default-browser-check': FLAG,
|
|
33
33
|
'first-run': FLAG,
|
|
34
|
+
'driver-path': STRING,
|
|
35
|
+
bidi: FLAG,
|
|
34
36
|
});
|
|
35
37
|
|
|
36
38
|
/** Options accepted by every page command. */
|
|
37
39
|
const PAGE_OPTIONS = Object.freeze({
|
|
38
40
|
...BROWSER_OPTIONS,
|
|
39
41
|
'cdp-endpoint': STRING,
|
|
42
|
+
'server-url': STRING,
|
|
40
43
|
url: STRING,
|
|
41
44
|
});
|
|
42
45
|
|
|
@@ -248,6 +251,8 @@ export function launchParams(options) {
|
|
|
248
251
|
localState: settings(options.localState, '--local-state'),
|
|
249
252
|
defaultBrowserCheck: options.defaultBrowserCheck,
|
|
250
253
|
firstRun: options.firstRun,
|
|
254
|
+
driverPath: options.driverPath,
|
|
255
|
+
bidi: options.bidi,
|
|
251
256
|
};
|
|
252
257
|
return Object.fromEntries(
|
|
253
258
|
Object.entries(params).filter(([, value]) => value !== undefined)
|
package/src/cli/commands.js
CHANGED
|
@@ -75,12 +75,16 @@ async function withPageSession(parsed, io, work, { skipUrl = false } = {}) {
|
|
|
75
75
|
const { options } = parsed;
|
|
76
76
|
const dispatcher = createDispatcher({ dependencies: io.dependencies });
|
|
77
77
|
try {
|
|
78
|
-
const opened =
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
78
|
+
const opened =
|
|
79
|
+
options.cdpEndpoint || options.serverUrl
|
|
80
|
+
? await dispatcher.dispatch('session.connect', {
|
|
81
|
+
cdpEndpoint: options.cdpEndpoint,
|
|
82
|
+
engine: options.engine,
|
|
83
|
+
...(options.serverUrl ? { serverUrl: options.serverUrl } : {}),
|
|
84
|
+
...(options.driverPath ? { driverPath: options.driverPath } : {}),
|
|
85
|
+
...(options.bidi !== undefined ? { bidi: options.bidi } : {}),
|
|
86
|
+
})
|
|
87
|
+
: await dispatcher.dispatch('session.launch', launchParams(options));
|
|
84
88
|
if (options.url && !skipUrl) {
|
|
85
89
|
await dispatcher.dispatch('page.goto', {
|
|
86
90
|
session: opened.session,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { decodeValue, describeObject, encodeValue } from './handles.js';
|
|
10
10
|
import { invalidParams, requireString } from './rpc-error.js';
|
|
11
11
|
|
|
12
|
-
const ENGINE_ROOTS = Object.freeze(['playwright', 'puppeteer']);
|
|
12
|
+
const ENGINE_ROOTS = Object.freeze(['playwright', 'puppeteer', 'selenium']);
|
|
13
13
|
|
|
14
14
|
function targetOf(state, params) {
|
|
15
15
|
const handle = params.handle;
|
|
@@ -36,11 +36,14 @@ async function root(state, params) {
|
|
|
36
36
|
browser: handleOrNull(state, session.browser),
|
|
37
37
|
context: handleOrNull(state, session.context),
|
|
38
38
|
page: handleOrNull(state, session.page),
|
|
39
|
+
...(session.engine === 'selenium'
|
|
40
|
+
? { driver: handleOrNull(state, session.driver) }
|
|
41
|
+
: {}),
|
|
39
42
|
};
|
|
40
43
|
}
|
|
41
44
|
if (!ENGINE_ROOTS.includes(name)) {
|
|
42
45
|
throw invalidParams(
|
|
43
|
-
`Unknown root "${name}"; expected playwright, puppeteer or session:<id>`
|
|
46
|
+
`Unknown root "${name}"; expected playwright, puppeteer, selenium or session:<id>`
|
|
44
47
|
);
|
|
45
48
|
}
|
|
46
49
|
const module = await state.dependencies.loadEngine(name);
|
|
@@ -71,6 +74,19 @@ async function get(state, params) {
|
|
|
71
74
|
);
|
|
72
75
|
}
|
|
73
76
|
|
|
77
|
+
/** Instantiate an engine constructor obtained through `handle.get`. */
|
|
78
|
+
function construct(state, params) {
|
|
79
|
+
const target = targetOf(state, params);
|
|
80
|
+
const args = params.args ?? [];
|
|
81
|
+
if (typeof target !== 'function' || !Array.isArray(args)) {
|
|
82
|
+
throw invalidParams(
|
|
83
|
+
'handle.construct requires a constructor handle and an args array'
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
const result = Reflect.construct(target, decodeValue(args, state.handles));
|
|
87
|
+
return encodeValue(result, state.handles);
|
|
88
|
+
}
|
|
89
|
+
|
|
74
90
|
function dispose(state, params) {
|
|
75
91
|
const id =
|
|
76
92
|
typeof params.handle === 'object' && params.handle
|
|
@@ -129,6 +145,7 @@ export const HANDLE_METHODS = Object.freeze({
|
|
|
129
145
|
'handle.root': root,
|
|
130
146
|
'handle.call': call,
|
|
131
147
|
'handle.get': get,
|
|
148
|
+
'handle.construct': construct,
|
|
132
149
|
'handle.dispose': dispose,
|
|
133
150
|
'handle.describe': describe,
|
|
134
151
|
'events.subscribe': subscribe,
|
|
@@ -37,14 +37,27 @@ async function launchSession(state, params) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
async function connectSession(state, params) {
|
|
40
|
-
const cdpEndpoint = requireString(params, 'cdpEndpoint');
|
|
41
40
|
const engine = resolveEngine(params.engine);
|
|
41
|
+
const endpoint =
|
|
42
|
+
engine === 'selenium' && params.serverUrl !== undefined
|
|
43
|
+
? {
|
|
44
|
+
serverUrl: requireString(params, 'serverUrl'),
|
|
45
|
+
capabilities: params.capabilities,
|
|
46
|
+
}
|
|
47
|
+
: { cdpEndpoint: requireString(params, 'cdpEndpoint') };
|
|
42
48
|
const connected = await state.dependencies.connectBrowser({
|
|
43
49
|
engine,
|
|
44
|
-
|
|
50
|
+
...endpoint,
|
|
51
|
+
...(params.driverPath ? { driverPath: params.driverPath } : {}),
|
|
52
|
+
...(params.bidi !== undefined ? { bidi: params.bidi } : {}),
|
|
45
53
|
});
|
|
46
54
|
const session = state.sessions.add(sessionFromConnect(engine, connected));
|
|
47
|
-
return {
|
|
55
|
+
return {
|
|
56
|
+
session,
|
|
57
|
+
...(endpoint.serverUrl
|
|
58
|
+
? { serverUrl: endpoint.serverUrl }
|
|
59
|
+
: { cdpEndpoint: endpoint.cdpEndpoint }),
|
|
60
|
+
};
|
|
48
61
|
}
|
|
49
62
|
|
|
50
63
|
async function closeSession(state, params) {
|
|
@@ -70,6 +83,10 @@ async function fill(state, params) {
|
|
|
70
83
|
const { engine, page } = sessionOf(state, params);
|
|
71
84
|
if (engine === 'playwright') {
|
|
72
85
|
await page.fill(selector, value);
|
|
86
|
+
} else if (engine === 'selenium') {
|
|
87
|
+
const element = await page.waitForSelector(selector, { visible: true });
|
|
88
|
+
await element.clear();
|
|
89
|
+
await element.sendKeys(value);
|
|
73
90
|
} else {
|
|
74
91
|
await page.locator(selector).fill(value);
|
|
75
92
|
}
|
package/src/cli/modules.js
CHANGED
|
@@ -70,7 +70,11 @@ export const DEFAULT_DEPENDENCIES = Object.freeze({
|
|
|
70
70
|
(await import('../browser/browser-profiles.js')).listBrowserProfiles(
|
|
71
71
|
options
|
|
72
72
|
),
|
|
73
|
-
loadEngine: (name) =>
|
|
73
|
+
loadEngine: (name) =>
|
|
74
|
+
importOptional(
|
|
75
|
+
name === 'selenium' ? 'selenium-webdriver' : name,
|
|
76
|
+
`The ${name} engine`
|
|
77
|
+
),
|
|
74
78
|
openInUserBrowser: async (url) =>
|
|
75
79
|
(
|
|
76
80
|
await importOptional(
|
package/src/cli/sessions.js
CHANGED
|
@@ -14,7 +14,7 @@ export const BROWSER_CHANNELS = Object.freeze({
|
|
|
14
14
|
chromium: 'chromium',
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
const ENGINES = Object.freeze(['playwright', 'puppeteer']);
|
|
17
|
+
const ENGINES = Object.freeze(['playwright', 'puppeteer', 'selenium']);
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Launch options that are forwarded to `launchBrowser()` as they are, so the
|
|
@@ -38,6 +38,8 @@ const LAUNCH_PASSTHROUGH = Object.freeze([
|
|
|
38
38
|
'localState',
|
|
39
39
|
'defaultBrowserCheck',
|
|
40
40
|
'firstRun',
|
|
41
|
+
'driverPath',
|
|
42
|
+
'bidi',
|
|
41
43
|
]);
|
|
42
44
|
|
|
43
45
|
/** Validate an engine name, defaulting to Playwright. */
|
|
@@ -85,13 +87,17 @@ export function buildLaunchOptions(params = {}) {
|
|
|
85
87
|
engine: resolveEngine(params.engine),
|
|
86
88
|
headless: params.headless === true,
|
|
87
89
|
};
|
|
88
|
-
const channel =
|
|
90
|
+
const channel =
|
|
91
|
+
options.engine === 'selenium' ? undefined : resolveChannel(params.browser);
|
|
89
92
|
const defined = {
|
|
90
93
|
launch: params.launch,
|
|
91
94
|
executablePath: params.executablePath,
|
|
92
95
|
userDataDir: params.userDataDir,
|
|
93
96
|
restrictions: params.restrictions,
|
|
94
97
|
channel,
|
|
98
|
+
...(options.engine === 'selenium' && params.browser !== undefined
|
|
99
|
+
? { browser: params.browser }
|
|
100
|
+
: {}),
|
|
95
101
|
};
|
|
96
102
|
for (const name of LAUNCH_PASSTHROUGH) {
|
|
97
103
|
defined[name] ??= params[name];
|
|
@@ -121,6 +127,7 @@ export function sessionFromLaunch(engine, launched) {
|
|
|
121
127
|
: handle,
|
|
122
128
|
context: playwright ? handle : (handle?.defaultBrowserContext?.() ?? null),
|
|
123
129
|
page: launched.page,
|
|
130
|
+
driver: launched.driver ?? (engine === 'selenium' ? handle : null),
|
|
124
131
|
args: launched.args ?? [],
|
|
125
132
|
close: () => launched.close(),
|
|
126
133
|
connected: false,
|
|
@@ -141,10 +148,15 @@ export function sessionFromConnect(engine, connected) {
|
|
|
141
148
|
? (page?.context?.() ?? null)
|
|
142
149
|
: (browser?.defaultBrowserContext?.() ?? null),
|
|
143
150
|
page,
|
|
151
|
+
driver: connected.driver ?? null,
|
|
144
152
|
// Playwright's close() on a connectOverCDP browser disconnects;
|
|
145
153
|
// Puppeteer's close() would quit the browser, disconnect() does not.
|
|
146
154
|
close: async () => {
|
|
147
155
|
await connected.downloads?.dispose?.();
|
|
156
|
+
if (engine === 'selenium') {
|
|
157
|
+
await connected.close();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
148
160
|
await (playwright ? browser.close() : browser.disconnect());
|
|
149
161
|
},
|
|
150
162
|
connected: true,
|