Skip to content

Commit 1b0f461

Browse files
committed
feat: enable BiDi support for connect-existing mode
GeckodriverHttpDriver now requests webSocketUrl: true in the session capabilities and exposes a getBidi() method that opens a WebSocket to Firefox's Remote Agent. This allows BiDi-dependent features (console events, network events) to work when using --connect-existing. The WebSocket is opened lazily on the first getBidi() call and closed on quit/kill. The IBiDi interface already defined upstream is satisfied by wrapping the ws WebSocket instance. No behavior change for launch mode (continues to use selenium-webdriver with enableBidi). No behavior change for connect-existing when Firefox was not started with --remote-debugging-port (getBidi throws a clear error, same as before but with a better message).
1 parent 66a8240 commit 1b0f461

1 file changed

Lines changed: 45 additions & 6 deletions

File tree

src/firefox/core.ts

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { spawn, type ChildProcess } from 'node:child_process';
88
import { mkdirSync, openSync, closeSync } from 'node:fs';
99
import { homedir } from 'node:os';
1010
import { join } from 'node:path';
11+
import WebSocket from 'ws';
1112
import type { FirefoxLaunchOptions } from './types.js';
1213
import { log, logDebug } from '../utils/logger.js';
1314

@@ -129,11 +130,14 @@ class GeckodriverHttpDriver implements IDriver {
129130
private baseUrl: string;
130131
private sessionId: string;
131132
private gdProcess: ChildProcess;
133+
private webSocketUrl: string | null;
134+
private bidiConnection: IBiDi | null = null;
132135

133-
constructor(baseUrl: string, sessionId: string, gdProcess: ChildProcess) {
136+
constructor(baseUrl: string, sessionId: string, gdProcess: ChildProcess, webSocketUrl: string | null) {
134137
this.baseUrl = baseUrl;
135138
this.sessionId = sessionId;
136139
this.gdProcess = gdProcess;
140+
this.webSocketUrl = webSocketUrl;
137141
}
138142

139143
static async connect(marionettePort: number): Promise<GeckodriverHttpDriver> {
@@ -206,11 +210,11 @@ class GeckodriverHttpDriver implements IDriver {
206210

207211
const baseUrl = `http://127.0.0.1:${port}`;
208212

209-
// Create a WebDriver session
213+
// Create a WebDriver session with BiDi opt-in
210214
const resp = await fetch(`${baseUrl}/session`, {
211215
method: 'POST',
212216
headers: { 'Content-Type': 'application/json' },
213-
body: JSON.stringify({ capabilities: { alwaysMatch: {} } }),
217+
body: JSON.stringify({ capabilities: { alwaysMatch: { webSocketUrl: true } } }),
214218
});
215219
const json = (await resp.json()) as {
216220
value: { sessionId: string; capabilities: Record<string, unknown> };
@@ -219,7 +223,14 @@ class GeckodriverHttpDriver implements IDriver {
219223
throw new Error(`Failed to create session: ${JSON.stringify(json)}`);
220224
}
221225

222-
return new GeckodriverHttpDriver(baseUrl, json.value.sessionId, gd);
226+
const wsUrl = json.value.capabilities.webSocketUrl as string | undefined;
227+
if (wsUrl) {
228+
logDebug(`BiDi WebSocket URL: ${wsUrl}`);
229+
} else {
230+
logDebug('BiDi WebSocket URL not available (Firefox may not support it or Remote Agent is not running)');
231+
}
232+
233+
return new GeckodriverHttpDriver(baseUrl, json.value.sessionId, gd, wsUrl ?? null);
223234
}
224235

225236
private async cmd(method: string, path: string, body?: unknown): Promise<unknown> {
@@ -422,6 +433,10 @@ class GeckodriverHttpDriver implements IDriver {
422433
}
423434

424435
async quit(): Promise<void> {
436+
if (this.bidiConnection) {
437+
(this.bidiConnection.socket as unknown as WebSocket).close();
438+
this.bidiConnection = null;
439+
}
425440
try {
426441
await this.cmd('DELETE', '');
427442
} catch {
@@ -432,11 +447,35 @@ class GeckodriverHttpDriver implements IDriver {
432447

433448
/** Kill the geckodriver process without closing Firefox */
434449
kill(): void {
450+
if (this.bidiConnection) {
451+
(this.bidiConnection.socket as unknown as WebSocket).close();
452+
this.bidiConnection = null;
453+
}
435454
this.gdProcess.kill();
436455
}
437456

438-
getBidi(): Promise<IBiDi> {
439-
throw new Error('BiDi not available in connect-existing mode');
457+
/**
458+
* Return a BiDi handle. Opens a WebSocket to Firefox's Remote Agent on
459+
* first call, using the webSocketUrl returned in the session capabilities.
460+
*/
461+
async getBidi(): Promise<IBiDi> {
462+
if (this.bidiConnection) return this.bidiConnection;
463+
if (!this.webSocketUrl) {
464+
throw new Error(
465+
'BiDi is not available: no webSocketUrl in session capabilities. ' +
466+
'Ensure Firefox was started with --remote-debugging-port.'
467+
);
468+
}
469+
470+
const ws = new WebSocket(this.webSocketUrl);
471+
await new Promise<void>((resolve, reject) => {
472+
ws.on('open', resolve);
473+
ws.on('error', reject);
474+
});
475+
logDebug('BiDi WebSocket connected');
476+
477+
this.bidiConnection = { socket: ws as unknown as IBiDiSocket };
478+
return this.bidiConnection;
440479
}
441480
}
442481

0 commit comments

Comments
 (0)