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.
@@ -9,11 +9,11 @@
9
9
  "preview": "vite preview"
10
10
  },
11
11
  "dependencies": {
12
- "react": "^18.2.0",
13
- "react-dom": "^18.2.0"
12
+ "react": "^19.3.0",
13
+ "react-dom": "^19.3.0"
14
14
  },
15
15
  "devDependencies": {
16
- "@vitejs/plugin-react": "^4.2.0",
17
- "vite": "^5.0.0"
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.23.0",
4
- "description": "Universal browser automation library that supports both Playwright and Puppeteer with a unified API",
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.0.0"
64
+ "node": ">=22.12.0"
65
65
  },
66
66
  "peerDependencies": {
67
- "playwright": ">=1.40.0",
68
- "puppeteer": ">=21.0.0",
69
- "selenium-webdriver": ">=4.20.0"
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.2.0",
85
- "links-notation": "^0.20.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.2",
90
+ "@changesets/cli": "^3.0.3",
91
91
  "@eslint/js": "^10.0.1",
92
- "eslint": "^10.10.0",
92
+ "eslint": "^10.12.0",
93
93
  "eslint-config-prettier": "^10.1.8",
94
94
  "eslint-plugin-prettier": "^5.5.6",
95
- "jscpd": "^5.1.2",
96
- "jsdoc": "^4.0.4",
97
- "playwright": "^1.62.1",
98
- "prettier": "^3.9.6",
99
- "puppeteer": "^25.10.0",
100
- "selenium-webdriver": "^4.49.0",
101
- "typescript": "^5.9.3",
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": {
@@ -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 'puppeteer'`
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 to an already-running Chromium-family browser over CDP.
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);
@@ -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 'puppeteer'
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
@@ -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 = await resolveProfile(browser, requestedUserDataDir);
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)
@@ -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 = options.cdpEndpoint
79
- ? await dispatcher.dispatch('session.connect', {
80
- cdpEndpoint: options.cdpEndpoint,
81
- engine: options.engine,
82
- })
83
- : await dispatcher.dispatch('session.launch', launchParams(options));
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
- cdpEndpoint,
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 { session, cdpEndpoint };
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
  }
@@ -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) => importOptional(name, `The ${name} engine`),
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(
@@ -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 = resolveChannel(params.browser);
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,
@@ -57,6 +57,7 @@ export {
57
57
  export const SUPPORTED_BROWSER_TEST_ENGINES = Object.freeze([
58
58
  'playwright',
59
59
  'puppeteer',
60
+ 'selenium',
60
61
  ]);
61
62
 
62
63
  export const DEFAULT_TIMINGS_FILENAME = '.browser-commander-test-timings.json';