firefox-devtools-mcp 0.7.4 → 0.8.1
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/README.md +31 -1
- package/dist/index.js +508 -142
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,6 +43,7 @@ claude mcp add firefox-devtools npx firefox-devtools-mcp@latest \
|
|
|
43
43
|
Option B — Edit Claude Code settings JSON
|
|
44
44
|
|
|
45
45
|
Add to your Claude Code config file:
|
|
46
|
+
|
|
46
47
|
- macOS: `~/Library/Application Support/Claude/Code/mcp_settings.json`
|
|
47
48
|
- Linux: `~/.config/claude/code/mcp_settings.json`
|
|
48
49
|
- Windows: `%APPDATA%\Claude\Code\mcp_settings.json`
|
|
@@ -75,6 +76,7 @@ npx @modelcontextprotocol/inspector npx firefox-devtools-mcp@latest --start-url
|
|
|
75
76
|
```
|
|
76
77
|
|
|
77
78
|
Then call tools like:
|
|
79
|
+
|
|
78
80
|
- `list_pages`, `select_page`, `navigate_page`
|
|
79
81
|
- `take_snapshot` then `click_by_uid` / `fill_by_uid`
|
|
80
82
|
- `list_network_requests` (always‑on capture), `get_network_request`
|
|
@@ -91,6 +93,30 @@ You can pass flags or environment variables (names on the right):
|
|
|
91
93
|
- `--firefox-arg` — extra Firefox arguments (repeatable)
|
|
92
94
|
- `--start-url` — open this URL on start (`START_URL`)
|
|
93
95
|
- `--accept-insecure-certs` — ignore TLS errors (`ACCEPT_INSECURE_CERTS=true`)
|
|
96
|
+
- `--connect-existing` — attach to an already-running Firefox instead of launching a new one (`CONNECT_EXISTING=true`)
|
|
97
|
+
- `--marionette-port` — Marionette port for connect-existing mode, default 2828 (`MARIONETTE_PORT`)
|
|
98
|
+
|
|
99
|
+
### Connect to existing Firefox
|
|
100
|
+
|
|
101
|
+
Use `--connect-existing` to automate your real browsing session — with cookies, logins, and open tabs intact:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# Start Firefox with Marionette enabled
|
|
105
|
+
firefox --marionette
|
|
106
|
+
|
|
107
|
+
# Run the MCP server
|
|
108
|
+
npx firefox-devtools-mcp --connect-existing --marionette-port 2828
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Or set `marionette.enabled` to `true` in `about:config` (or `user.js`) to enable Marionette on every launch.
|
|
112
|
+
|
|
113
|
+
BiDi-dependent features (console events, network events) are not available in connect-existing mode; all other features work normally.
|
|
114
|
+
|
|
115
|
+
> **Warning:** Do not leave Marionette enabled during normal browsing. It sets
|
|
116
|
+
> `navigator.webdriver = true` and changes other browser fingerprint signals,
|
|
117
|
+
> which can trigger bot detection on sites protected by Cloudflare, Akamai, etc.
|
|
118
|
+
> Only enable Marionette when you need MCP automation, then restart Firefox
|
|
119
|
+
> normally afterward.
|
|
94
120
|
|
|
95
121
|
## Tool overview
|
|
96
122
|
|
|
@@ -134,6 +160,7 @@ npm run inspector:dev
|
|
|
134
160
|
- Stale UIDs after navigation: take a fresh snapshot (`take_snapshot`) before using UID tools.
|
|
135
161
|
- Windows 10: Error during discovery for MCP server 'firefox-devtools': MCP error -32000: Connection closed
|
|
136
162
|
- **Solution 1** Call using `cmd` (For more info https://github.com/modelcontextprotocol/servers/issues/1082#issuecomment-2791786310)
|
|
163
|
+
|
|
137
164
|
```json
|
|
138
165
|
"mcpServers": {
|
|
139
166
|
"firefox-devtools": {
|
|
@@ -141,10 +168,12 @@ npm run inspector:dev
|
|
|
141
168
|
"args": ["/c", "npx", "-y", "firefox-devtools-mcp@latest"]
|
|
142
169
|
}
|
|
143
170
|
}
|
|
144
|
-
```
|
|
171
|
+
```
|
|
172
|
+
|
|
145
173
|
> **The Key Change:** On Windows, running a Node.js package via `npx` often requires the `cmd /c` prefix to be executed correctly from within another process like VSCode's extension host. Therefore, `"command": "npx"` was replaced with `"command": "cmd"`, and the actual `npx` command was moved into the `"args"` array, preceded by `"/c"`. This fix allows Windows to interpret the command correctly and launch the server.
|
|
146
174
|
|
|
147
175
|
- **Solution 2** Instead of another layer of shell you can write the absolute path to `npx`:
|
|
176
|
+
|
|
148
177
|
```json
|
|
149
178
|
"mcpServers": {
|
|
150
179
|
"firefox-devtools": {
|
|
@@ -153,6 +182,7 @@ npm run inspector:dev
|
|
|
153
182
|
}
|
|
154
183
|
}
|
|
155
184
|
```
|
|
185
|
+
|
|
156
186
|
Note: The path above is an example. You must adjust it to match the actual location of `npx` on your machine. Depending on your setup, the file extension might be `.cmd`, `.bat`, or `.exe` rather than `.ps1`. Also, ensure you use double backslashes (`\\`) as path delimiters, as required by the JSON format.
|
|
157
187
|
|
|
158
188
|
## Versioning
|
package/dist/index.js
CHANGED
|
@@ -22056,6 +22056,16 @@ var init_cli = __esm({
|
|
|
22056
22056
|
type: "string",
|
|
22057
22057
|
description: "URL to open when Firefox starts (default: about:home)",
|
|
22058
22058
|
default: process.env.START_URL ?? "about:home"
|
|
22059
|
+
},
|
|
22060
|
+
connectExisting: {
|
|
22061
|
+
type: "boolean",
|
|
22062
|
+
description: "Connect to an already-running Firefox instance via Marionette instead of launching a new one. Requires Firefox to be running with marionette.enabled=true (set in user.js or launched with --marionette).",
|
|
22063
|
+
default: (process.env.CONNECT_EXISTING ?? "false") === "true"
|
|
22064
|
+
},
|
|
22065
|
+
marionettePort: {
|
|
22066
|
+
type: "number",
|
|
22067
|
+
description: "Marionette port to connect to when using --connect-existing (default: 2828)",
|
|
22068
|
+
default: Number(process.env.MARIONETTE_PORT ?? "2828")
|
|
22059
22069
|
}
|
|
22060
22070
|
};
|
|
22061
22071
|
}
|
|
@@ -22064,11 +22074,320 @@ var init_cli = __esm({
|
|
|
22064
22074
|
// src/firefox/core.ts
|
|
22065
22075
|
import { Builder, Browser } from "selenium-webdriver";
|
|
22066
22076
|
import firefox from "selenium-webdriver/firefox.js";
|
|
22067
|
-
|
|
22077
|
+
import { spawn } from "child_process";
|
|
22078
|
+
function findGeckodriverInCache(fs, path, cacheBase) {
|
|
22079
|
+
const ext = process.platform === "win32" ? ".exe" : "";
|
|
22080
|
+
const binaryName = `geckodriver${ext}`;
|
|
22081
|
+
try {
|
|
22082
|
+
if (!fs.existsSync(cacheBase)) {
|
|
22083
|
+
return "";
|
|
22084
|
+
}
|
|
22085
|
+
for (const platformDir of fs.readdirSync(cacheBase)) {
|
|
22086
|
+
const platformPath = path.join(cacheBase, platformDir);
|
|
22087
|
+
if (!fs.statSync(platformPath).isDirectory()) {
|
|
22088
|
+
continue;
|
|
22089
|
+
}
|
|
22090
|
+
const versionDirs = fs.readdirSync(platformPath).sort().reverse();
|
|
22091
|
+
for (const versionDir of versionDirs) {
|
|
22092
|
+
const candidate = path.join(platformPath, versionDir, binaryName);
|
|
22093
|
+
if (fs.existsSync(candidate)) {
|
|
22094
|
+
return candidate;
|
|
22095
|
+
}
|
|
22096
|
+
}
|
|
22097
|
+
}
|
|
22098
|
+
} catch {
|
|
22099
|
+
}
|
|
22100
|
+
return "";
|
|
22101
|
+
}
|
|
22102
|
+
var GeckodriverElement, GeckodriverHttpDriver, FirefoxCore;
|
|
22068
22103
|
var init_core3 = __esm({
|
|
22069
22104
|
"src/firefox/core.ts"() {
|
|
22070
22105
|
"use strict";
|
|
22071
22106
|
init_logger();
|
|
22107
|
+
GeckodriverElement = class {
|
|
22108
|
+
constructor(cmd, elementId) {
|
|
22109
|
+
this.cmd = cmd;
|
|
22110
|
+
this.elementId = elementId;
|
|
22111
|
+
}
|
|
22112
|
+
async click() {
|
|
22113
|
+
await this.cmd("POST", `/element/${this.elementId}/click`, {});
|
|
22114
|
+
}
|
|
22115
|
+
async clear() {
|
|
22116
|
+
await this.cmd("POST", `/element/${this.elementId}/clear`, {});
|
|
22117
|
+
}
|
|
22118
|
+
async sendKeys(...args2) {
|
|
22119
|
+
const text = args2.join("");
|
|
22120
|
+
await this.cmd("POST", `/element/${this.elementId}/value`, { text });
|
|
22121
|
+
}
|
|
22122
|
+
async isDisplayed() {
|
|
22123
|
+
return await this.cmd("GET", `/element/${this.elementId}/displayed`);
|
|
22124
|
+
}
|
|
22125
|
+
async takeScreenshot() {
|
|
22126
|
+
return await this.cmd("GET", `/element/${this.elementId}/screenshot`);
|
|
22127
|
+
}
|
|
22128
|
+
};
|
|
22129
|
+
GeckodriverHttpDriver = class _GeckodriverHttpDriver {
|
|
22130
|
+
baseUrl;
|
|
22131
|
+
sessionId;
|
|
22132
|
+
gdProcess;
|
|
22133
|
+
constructor(baseUrl, sessionId, gdProcess) {
|
|
22134
|
+
this.baseUrl = baseUrl;
|
|
22135
|
+
this.sessionId = sessionId;
|
|
22136
|
+
this.gdProcess = gdProcess;
|
|
22137
|
+
}
|
|
22138
|
+
static async connect(marionettePort) {
|
|
22139
|
+
const path = await import("path");
|
|
22140
|
+
const { execFileSync } = await import("child_process");
|
|
22141
|
+
let geckodriverPath;
|
|
22142
|
+
try {
|
|
22143
|
+
const { createRequire } = await import("module");
|
|
22144
|
+
const require2 = createRequire(import.meta.url);
|
|
22145
|
+
const swPkg = require2.resolve("selenium-webdriver/package.json");
|
|
22146
|
+
const swDir = path.dirname(swPkg);
|
|
22147
|
+
const platform = process.platform === "win32" ? "windows" : process.platform === "darwin" ? "macos" : "linux";
|
|
22148
|
+
const ext = process.platform === "win32" ? ".exe" : "";
|
|
22149
|
+
const smBin = path.join(swDir, "bin", platform, `selenium-manager${ext}`);
|
|
22150
|
+
const result = JSON.parse(
|
|
22151
|
+
execFileSync(smBin, ["--browser", "firefox", "--output", "json"], { encoding: "utf-8" })
|
|
22152
|
+
);
|
|
22153
|
+
geckodriverPath = result.result.driver_path;
|
|
22154
|
+
} catch {
|
|
22155
|
+
const os = await import("os");
|
|
22156
|
+
const fs = await import("fs");
|
|
22157
|
+
const cacheBase = path.join(os.homedir(), ".cache/selenium/geckodriver");
|
|
22158
|
+
geckodriverPath = findGeckodriverInCache(fs, path, cacheBase);
|
|
22159
|
+
if (!geckodriverPath) {
|
|
22160
|
+
throw new Error("Cannot find geckodriver binary. Ensure selenium-webdriver is installed.");
|
|
22161
|
+
}
|
|
22162
|
+
}
|
|
22163
|
+
logDebug(`Using geckodriver: ${geckodriverPath}`);
|
|
22164
|
+
const gd = spawn(
|
|
22165
|
+
geckodriverPath,
|
|
22166
|
+
["--connect-existing", "--marionette-port", String(marionettePort), "--port", "0"],
|
|
22167
|
+
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
22168
|
+
);
|
|
22169
|
+
const port = await new Promise((resolve4, reject) => {
|
|
22170
|
+
const timeout = setTimeout(() => reject(new Error("Geckodriver startup timeout")), 1e4);
|
|
22171
|
+
const onData = (data) => {
|
|
22172
|
+
const msg = data.toString();
|
|
22173
|
+
logDebug(`[geckodriver] ${msg.trim()}`);
|
|
22174
|
+
const match = msg.match(/Listening on\s+\S+:(\d+)/);
|
|
22175
|
+
if (match?.[1]) {
|
|
22176
|
+
clearTimeout(timeout);
|
|
22177
|
+
resolve4(parseInt(match[1], 10));
|
|
22178
|
+
}
|
|
22179
|
+
};
|
|
22180
|
+
gd.stdout?.on("data", onData);
|
|
22181
|
+
gd.stderr?.on("data", onData);
|
|
22182
|
+
gd.on("error", (err) => {
|
|
22183
|
+
clearTimeout(timeout);
|
|
22184
|
+
reject(err);
|
|
22185
|
+
});
|
|
22186
|
+
gd.on("exit", (code) => {
|
|
22187
|
+
clearTimeout(timeout);
|
|
22188
|
+
reject(new Error(`Geckodriver exited with code ${code}`));
|
|
22189
|
+
});
|
|
22190
|
+
});
|
|
22191
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
22192
|
+
const resp = await fetch(`${baseUrl}/session`, {
|
|
22193
|
+
method: "POST",
|
|
22194
|
+
headers: { "Content-Type": "application/json" },
|
|
22195
|
+
body: JSON.stringify({ capabilities: { alwaysMatch: {} } })
|
|
22196
|
+
});
|
|
22197
|
+
const json2 = await resp.json();
|
|
22198
|
+
if (!json2.value?.sessionId) {
|
|
22199
|
+
throw new Error(`Failed to create session: ${JSON.stringify(json2)}`);
|
|
22200
|
+
}
|
|
22201
|
+
return new _GeckodriverHttpDriver(baseUrl, json2.value.sessionId, gd);
|
|
22202
|
+
}
|
|
22203
|
+
async cmd(method, path, body) {
|
|
22204
|
+
const url2 = `${this.baseUrl}/session/${this.sessionId}${path}`;
|
|
22205
|
+
const opts = {
|
|
22206
|
+
method,
|
|
22207
|
+
headers: { "Content-Type": "application/json" }
|
|
22208
|
+
};
|
|
22209
|
+
if (body !== void 0) {
|
|
22210
|
+
opts.body = JSON.stringify(body);
|
|
22211
|
+
}
|
|
22212
|
+
const resp = await fetch(url2, opts);
|
|
22213
|
+
const json2 = await resp.json();
|
|
22214
|
+
if (json2.value && typeof json2.value === "object" && "error" in json2.value) {
|
|
22215
|
+
const err = json2.value;
|
|
22216
|
+
throw new Error(`${err.error}: ${err.message}`);
|
|
22217
|
+
}
|
|
22218
|
+
return json2.value;
|
|
22219
|
+
}
|
|
22220
|
+
// WebDriver-compatible methods used by the rest of the codebase
|
|
22221
|
+
async getTitle() {
|
|
22222
|
+
return await this.cmd("GET", "/title");
|
|
22223
|
+
}
|
|
22224
|
+
async getCurrentUrl() {
|
|
22225
|
+
return await this.cmd("GET", "/url");
|
|
22226
|
+
}
|
|
22227
|
+
async getWindowHandle() {
|
|
22228
|
+
return await this.cmd("GET", "/window");
|
|
22229
|
+
}
|
|
22230
|
+
async getAllWindowHandles() {
|
|
22231
|
+
return await this.cmd("GET", "/window/handles");
|
|
22232
|
+
}
|
|
22233
|
+
async get(url2) {
|
|
22234
|
+
await this.cmd("POST", "/url", { url: url2 });
|
|
22235
|
+
}
|
|
22236
|
+
async getPageSource() {
|
|
22237
|
+
return await this.cmd("GET", "/source");
|
|
22238
|
+
}
|
|
22239
|
+
async executeScript(script, ...args2) {
|
|
22240
|
+
return await this.cmd("POST", "/execute/sync", { script, args: args2 });
|
|
22241
|
+
}
|
|
22242
|
+
async executeAsyncScript(script, ...args2) {
|
|
22243
|
+
return await this.cmd("POST", "/execute/async", { script, args: args2 });
|
|
22244
|
+
}
|
|
22245
|
+
async takeScreenshot() {
|
|
22246
|
+
return await this.cmd("GET", "/screenshot");
|
|
22247
|
+
}
|
|
22248
|
+
async close() {
|
|
22249
|
+
await this.cmd("DELETE", "/window");
|
|
22250
|
+
}
|
|
22251
|
+
async getSession() {
|
|
22252
|
+
return { getId: () => this.sessionId };
|
|
22253
|
+
}
|
|
22254
|
+
// Element finding
|
|
22255
|
+
async findElement(locator) {
|
|
22256
|
+
const loc = locator;
|
|
22257
|
+
const using = loc.using ?? "css selector";
|
|
22258
|
+
const value = loc.value ?? "";
|
|
22259
|
+
const result = await this.cmd("POST", "/element", { using, value });
|
|
22260
|
+
const elementId = Object.values(result)[0];
|
|
22261
|
+
return new GeckodriverElement(this.cmd.bind(this), elementId);
|
|
22262
|
+
}
|
|
22263
|
+
async findElements(locator) {
|
|
22264
|
+
const loc = locator;
|
|
22265
|
+
const using = loc.using ?? "css selector";
|
|
22266
|
+
const value = loc.value ?? "";
|
|
22267
|
+
const results = await this.cmd("POST", "/elements", { using, value });
|
|
22268
|
+
return results.map((r) => new GeckodriverElement(this.cmd.bind(this), Object.values(r)[0]));
|
|
22269
|
+
}
|
|
22270
|
+
// Polling wait — compatible with selenium's Condition objects and plain functions.
|
|
22271
|
+
// Used by dom.ts helpers for element location and visibility polling.
|
|
22272
|
+
async wait(condition, timeout = 5e3) {
|
|
22273
|
+
const fn = typeof condition === "function" ? condition : condition.fn;
|
|
22274
|
+
const deadline = Date.now() + timeout;
|
|
22275
|
+
let lastError;
|
|
22276
|
+
while (Date.now() < deadline) {
|
|
22277
|
+
try {
|
|
22278
|
+
const result = await fn(this);
|
|
22279
|
+
if (result) {
|
|
22280
|
+
return result;
|
|
22281
|
+
}
|
|
22282
|
+
} catch (e) {
|
|
22283
|
+
lastError = e instanceof Error ? e : new Error(String(e));
|
|
22284
|
+
}
|
|
22285
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
22286
|
+
}
|
|
22287
|
+
throw lastError ?? new Error(`wait() timed out after ${timeout}ms`);
|
|
22288
|
+
}
|
|
22289
|
+
switchTo() {
|
|
22290
|
+
return {
|
|
22291
|
+
window: async (handle) => {
|
|
22292
|
+
await this.cmd("POST", "/window", { handle });
|
|
22293
|
+
},
|
|
22294
|
+
newWindow: async (type) => {
|
|
22295
|
+
return await this.cmd("POST", "/window/new", { type });
|
|
22296
|
+
},
|
|
22297
|
+
alert: async () => {
|
|
22298
|
+
return {
|
|
22299
|
+
accept: async () => {
|
|
22300
|
+
await this.cmd("POST", "/alert/accept");
|
|
22301
|
+
},
|
|
22302
|
+
dismiss: async () => {
|
|
22303
|
+
await this.cmd("POST", "/alert/dismiss");
|
|
22304
|
+
},
|
|
22305
|
+
getText: async () => {
|
|
22306
|
+
return await this.cmd("GET", "/alert/text");
|
|
22307
|
+
},
|
|
22308
|
+
sendKeys: async (text) => {
|
|
22309
|
+
await this.cmd("POST", "/alert/text", { text });
|
|
22310
|
+
}
|
|
22311
|
+
};
|
|
22312
|
+
}
|
|
22313
|
+
};
|
|
22314
|
+
}
|
|
22315
|
+
navigate() {
|
|
22316
|
+
return {
|
|
22317
|
+
back: async () => {
|
|
22318
|
+
await this.cmd("POST", "/back");
|
|
22319
|
+
},
|
|
22320
|
+
forward: async () => {
|
|
22321
|
+
await this.cmd("POST", "/forward");
|
|
22322
|
+
},
|
|
22323
|
+
refresh: async () => {
|
|
22324
|
+
await this.cmd("POST", "/refresh");
|
|
22325
|
+
}
|
|
22326
|
+
};
|
|
22327
|
+
}
|
|
22328
|
+
manage() {
|
|
22329
|
+
return {
|
|
22330
|
+
window: () => {
|
|
22331
|
+
return {
|
|
22332
|
+
setRect: async (rect) => {
|
|
22333
|
+
await this.cmd("POST", "/window/rect", rect);
|
|
22334
|
+
}
|
|
22335
|
+
};
|
|
22336
|
+
}
|
|
22337
|
+
};
|
|
22338
|
+
}
|
|
22339
|
+
actions(_opts) {
|
|
22340
|
+
const actionSequences = [];
|
|
22341
|
+
const builder = {
|
|
22342
|
+
move: (opts) => {
|
|
22343
|
+
actionSequences.push({
|
|
22344
|
+
type: "pointer",
|
|
22345
|
+
id: "mouse",
|
|
22346
|
+
parameters: { pointerType: "mouse" },
|
|
22347
|
+
actions: [{ type: "pointerMove", ...opts }]
|
|
22348
|
+
});
|
|
22349
|
+
return builder;
|
|
22350
|
+
},
|
|
22351
|
+
click: () => {
|
|
22352
|
+
const last = actionSequences[actionSequences.length - 1];
|
|
22353
|
+
if (last) {
|
|
22354
|
+
last.actions.push({ type: "pointerDown", button: 0 }, { type: "pointerUp", button: 0 });
|
|
22355
|
+
}
|
|
22356
|
+
return builder;
|
|
22357
|
+
},
|
|
22358
|
+
doubleClick: (_el) => {
|
|
22359
|
+
const last = actionSequences[actionSequences.length - 1];
|
|
22360
|
+
if (last) {
|
|
22361
|
+
last.actions.push(
|
|
22362
|
+
{ type: "pointerDown", button: 0 },
|
|
22363
|
+
{ type: "pointerUp", button: 0 },
|
|
22364
|
+
{ type: "pointerDown", button: 0 },
|
|
22365
|
+
{ type: "pointerUp", button: 0 }
|
|
22366
|
+
);
|
|
22367
|
+
}
|
|
22368
|
+
return builder;
|
|
22369
|
+
},
|
|
22370
|
+
perform: async () => {
|
|
22371
|
+
await this.cmd("POST", "/actions", { actions: actionSequences });
|
|
22372
|
+
},
|
|
22373
|
+
clear: async () => {
|
|
22374
|
+
await this.cmd("DELETE", "/actions");
|
|
22375
|
+
}
|
|
22376
|
+
};
|
|
22377
|
+
return builder;
|
|
22378
|
+
}
|
|
22379
|
+
async quit() {
|
|
22380
|
+
try {
|
|
22381
|
+
await this.cmd("DELETE", "");
|
|
22382
|
+
} catch {
|
|
22383
|
+
}
|
|
22384
|
+
this.gdProcess.kill();
|
|
22385
|
+
}
|
|
22386
|
+
/** Kill the geckodriver process without closing Firefox */
|
|
22387
|
+
kill() {
|
|
22388
|
+
this.gdProcess.kill();
|
|
22389
|
+
}
|
|
22390
|
+
};
|
|
22072
22391
|
FirefoxCore = class {
|
|
22073
22392
|
constructor(options) {
|
|
22074
22393
|
this.options = options;
|
|
@@ -22076,45 +22395,56 @@ var init_core3 = __esm({
|
|
|
22076
22395
|
driver = null;
|
|
22077
22396
|
currentContextId = null;
|
|
22078
22397
|
/**
|
|
22079
|
-
* Launch Firefox and establish BiDi connection
|
|
22398
|
+
* Launch Firefox (or connect to an existing instance) and establish BiDi connection
|
|
22080
22399
|
*/
|
|
22081
22400
|
async connect() {
|
|
22082
|
-
|
|
22083
|
-
|
|
22084
|
-
|
|
22085
|
-
|
|
22086
|
-
firefoxOptions.addArguments("-headless");
|
|
22087
|
-
}
|
|
22088
|
-
if (this.options.viewport) {
|
|
22089
|
-
firefoxOptions.windowSize({
|
|
22090
|
-
width: this.options.viewport.width,
|
|
22091
|
-
height: this.options.viewport.height
|
|
22092
|
-
});
|
|
22093
|
-
}
|
|
22094
|
-
if (this.options.firefoxPath) {
|
|
22095
|
-
firefoxOptions.setBinary(this.options.firefoxPath);
|
|
22096
|
-
}
|
|
22097
|
-
if (this.options.args && this.options.args.length > 0) {
|
|
22098
|
-
firefoxOptions.addArguments(...this.options.args);
|
|
22099
|
-
}
|
|
22100
|
-
if (this.options.profilePath) {
|
|
22101
|
-
firefoxOptions.setProfile(this.options.profilePath);
|
|
22401
|
+
if (this.options.connectExisting) {
|
|
22402
|
+
log("\u{1F517} Connecting to existing Firefox via Marionette...");
|
|
22403
|
+
} else {
|
|
22404
|
+
log("\u{1F680} Launching Firefox via Selenium WebDriver BiDi...");
|
|
22102
22405
|
}
|
|
22103
|
-
if (this.options.
|
|
22104
|
-
|
|
22406
|
+
if (this.options.connectExisting) {
|
|
22407
|
+
const port = this.options.marionettePort ?? 2828;
|
|
22408
|
+
this.driver = await GeckodriverHttpDriver.connect(port);
|
|
22409
|
+
} else {
|
|
22410
|
+
const firefoxOptions = new firefox.Options();
|
|
22411
|
+
firefoxOptions.enableBidi();
|
|
22412
|
+
if (this.options.headless) {
|
|
22413
|
+
firefoxOptions.addArguments("-headless");
|
|
22414
|
+
}
|
|
22415
|
+
if (this.options.viewport) {
|
|
22416
|
+
firefoxOptions.windowSize({
|
|
22417
|
+
width: this.options.viewport.width,
|
|
22418
|
+
height: this.options.viewport.height
|
|
22419
|
+
});
|
|
22420
|
+
}
|
|
22421
|
+
if (this.options.firefoxPath) {
|
|
22422
|
+
firefoxOptions.setBinary(this.options.firefoxPath);
|
|
22423
|
+
}
|
|
22424
|
+
if (this.options.args && this.options.args.length > 0) {
|
|
22425
|
+
firefoxOptions.addArguments(...this.options.args);
|
|
22426
|
+
}
|
|
22427
|
+
if (this.options.profilePath) {
|
|
22428
|
+
firefoxOptions.setProfile(this.options.profilePath);
|
|
22429
|
+
}
|
|
22430
|
+
if (this.options.acceptInsecureCerts) {
|
|
22431
|
+
firefoxOptions.setAcceptInsecureCerts(true);
|
|
22432
|
+
}
|
|
22433
|
+
this.driver = await new Builder().forBrowser(Browser.FIREFOX).setFirefoxOptions(firefoxOptions).build();
|
|
22105
22434
|
}
|
|
22106
|
-
|
|
22107
|
-
|
|
22435
|
+
log(
|
|
22436
|
+
this.options.connectExisting ? "\u2705 Connected to existing Firefox" : "\u2705 Firefox launched with BiDi"
|
|
22437
|
+
);
|
|
22108
22438
|
this.currentContextId = await this.driver.getWindowHandle();
|
|
22109
22439
|
logDebug(`Browsing context ID: ${this.currentContextId}`);
|
|
22110
|
-
if (this.options.startUrl) {
|
|
22440
|
+
if (this.options.startUrl && !this.options.connectExisting) {
|
|
22111
22441
|
await this.driver.get(this.options.startUrl);
|
|
22112
22442
|
logDebug(`Navigated to: ${this.options.startUrl}`);
|
|
22113
22443
|
}
|
|
22114
22444
|
log("\u2705 Firefox DevTools ready");
|
|
22115
22445
|
}
|
|
22116
22446
|
/**
|
|
22117
|
-
* Get
|
|
22447
|
+
* Get driver instance (throw if not connected)
|
|
22118
22448
|
*/
|
|
22119
22449
|
getDriver() {
|
|
22120
22450
|
if (!this.driver) {
|
|
@@ -22142,6 +22472,9 @@ var init_core3 = __esm({
|
|
|
22142
22472
|
* Reset driver state (used when Firefox is detected as closed)
|
|
22143
22473
|
*/
|
|
22144
22474
|
reset() {
|
|
22475
|
+
if (this.driver && this.options.connectExisting && "kill" in this.driver) {
|
|
22476
|
+
this.driver.kill();
|
|
22477
|
+
}
|
|
22145
22478
|
this.driver = null;
|
|
22146
22479
|
this.currentContextId = null;
|
|
22147
22480
|
logDebug("Driver state reset");
|
|
@@ -22159,11 +22492,17 @@ var init_core3 = __esm({
|
|
|
22159
22492
|
this.currentContextId = contextId;
|
|
22160
22493
|
}
|
|
22161
22494
|
/**
|
|
22162
|
-
* Close driver and cleanup
|
|
22495
|
+
* Close driver and cleanup.
|
|
22496
|
+
* When connected to an existing Firefox instance, only kills geckodriver
|
|
22497
|
+
* without closing the browser.
|
|
22163
22498
|
*/
|
|
22164
22499
|
async close() {
|
|
22165
22500
|
if (this.driver) {
|
|
22166
|
-
|
|
22501
|
+
if (this.options.connectExisting && "kill" in this.driver) {
|
|
22502
|
+
this.driver.kill();
|
|
22503
|
+
} else if ("quit" in this.driver) {
|
|
22504
|
+
await this.driver.quit();
|
|
22505
|
+
}
|
|
22167
22506
|
this.driver = null;
|
|
22168
22507
|
}
|
|
22169
22508
|
log("\u2705 Firefox DevTools closed");
|
|
@@ -22545,7 +22884,7 @@ var init_events = __esm({
|
|
|
22545
22884
|
});
|
|
22546
22885
|
|
|
22547
22886
|
// src/firefox/dom.ts
|
|
22548
|
-
import {
|
|
22887
|
+
import { Key } from "selenium-webdriver";
|
|
22549
22888
|
var DomInteractions;
|
|
22550
22889
|
var init_dom = __esm({
|
|
22551
22890
|
"src/firefox/dom.ts"() {
|
|
@@ -22568,27 +22907,63 @@ var init_dom = __esm({
|
|
|
22568
22907
|
const html = await this.evaluate("return document.documentElement.outerHTML");
|
|
22569
22908
|
return String(html);
|
|
22570
22909
|
}
|
|
22910
|
+
// ============================================================================
|
|
22911
|
+
// Element polling helpers (work with both WebDriver and GeckodriverHttpDriver)
|
|
22912
|
+
// ============================================================================
|
|
22913
|
+
/**
|
|
22914
|
+
* Poll for an element matching a CSS selector until found or timeout.
|
|
22915
|
+
*/
|
|
22916
|
+
async waitForElement(selector, timeout = 5e3) {
|
|
22917
|
+
const deadline = Date.now() + timeout;
|
|
22918
|
+
let lastError;
|
|
22919
|
+
while (Date.now() < deadline) {
|
|
22920
|
+
try {
|
|
22921
|
+
return await this.driver.findElement({ using: "css selector", value: selector });
|
|
22922
|
+
} catch (e) {
|
|
22923
|
+
lastError = e instanceof Error ? e : new Error(String(e));
|
|
22924
|
+
}
|
|
22925
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
22926
|
+
}
|
|
22927
|
+
throw lastError ?? new Error(`Element not found: ${selector}`);
|
|
22928
|
+
}
|
|
22929
|
+
/**
|
|
22930
|
+
* Wait until an element reports isDisplayed(), ignoring failures.
|
|
22931
|
+
*/
|
|
22932
|
+
async waitForVisible(el, timeout = 5e3) {
|
|
22933
|
+
const deadline = Date.now() + timeout;
|
|
22934
|
+
while (Date.now() < deadline) {
|
|
22935
|
+
try {
|
|
22936
|
+
if (await el.isDisplayed()) {
|
|
22937
|
+
return;
|
|
22938
|
+
}
|
|
22939
|
+
} catch {
|
|
22940
|
+
}
|
|
22941
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
22942
|
+
}
|
|
22943
|
+
}
|
|
22944
|
+
// ============================================================================
|
|
22945
|
+
// Selector-based input methods
|
|
22946
|
+
// ============================================================================
|
|
22571
22947
|
/**
|
|
22572
22948
|
* Click element by CSS selector
|
|
22573
22949
|
*/
|
|
22574
22950
|
async clickBySelector(selector) {
|
|
22575
|
-
const el = await this.
|
|
22576
|
-
await this.
|
|
22577
|
-
});
|
|
22951
|
+
const el = await this.waitForElement(selector, 5e3);
|
|
22952
|
+
await this.waitForVisible(el, 5e3);
|
|
22578
22953
|
await el.click();
|
|
22579
22954
|
}
|
|
22580
22955
|
/**
|
|
22581
22956
|
* Hover over element by CSS selector
|
|
22582
22957
|
*/
|
|
22583
22958
|
async hoverBySelector(selector) {
|
|
22584
|
-
const el = await this.
|
|
22959
|
+
const el = await this.waitForElement(selector, 5e3);
|
|
22585
22960
|
await this.driver.actions({ async: true }).move({ origin: el }).perform();
|
|
22586
22961
|
}
|
|
22587
22962
|
/**
|
|
22588
22963
|
* Fill input field by CSS selector
|
|
22589
22964
|
*/
|
|
22590
22965
|
async fillBySelector(selector, text) {
|
|
22591
|
-
const el = await this.
|
|
22966
|
+
const el = await this.waitForElement(selector, 5e3);
|
|
22592
22967
|
try {
|
|
22593
22968
|
await el.clear();
|
|
22594
22969
|
} catch {
|
|
@@ -22602,27 +22977,22 @@ var init_dom = __esm({
|
|
|
22602
22977
|
*/
|
|
22603
22978
|
async dragAndDropBySelectors(sourceSelector, targetSelector) {
|
|
22604
22979
|
await this.driver.executeScript(
|
|
22605
|
-
|
|
22606
|
-
|
|
22607
|
-
|
|
22608
|
-
|
|
22609
|
-
|
|
22610
|
-
|
|
22611
|
-
|
|
22612
|
-
|
|
22613
|
-
|
|
22614
|
-
|
|
22615
|
-
|
|
22616
|
-
|
|
22617
|
-
|
|
22618
|
-
|
|
22619
|
-
|
|
22620
|
-
|
|
22621
|
-
dispatch("dragenter", tgt, dt);
|
|
22622
|
-
dispatch("dragover", tgt, dt);
|
|
22623
|
-
dispatch("drop", tgt, dt);
|
|
22624
|
-
dispatch("dragend", src, dt);
|
|
22625
|
-
},
|
|
22980
|
+
`
|
|
22981
|
+
var srcSel = arguments[0], tgtSel = arguments[1];
|
|
22982
|
+
var src = document.querySelector(srcSel);
|
|
22983
|
+
var tgt = document.querySelector(tgtSel);
|
|
22984
|
+
if (!src || !tgt) throw new Error('dragAndDrop: element not found');
|
|
22985
|
+
function dispatch(type, target, dt) {
|
|
22986
|
+
var evt = new DragEvent(type, { bubbles: true, cancelable: true, dataTransfer: dt });
|
|
22987
|
+
return target.dispatchEvent(evt);
|
|
22988
|
+
}
|
|
22989
|
+
var dt = typeof DataTransfer !== 'undefined' ? new DataTransfer() : undefined;
|
|
22990
|
+
dispatch('dragstart', src, dt);
|
|
22991
|
+
dispatch('dragenter', tgt, dt);
|
|
22992
|
+
dispatch('dragover', tgt, dt);
|
|
22993
|
+
dispatch('drop', tgt, dt);
|
|
22994
|
+
dispatch('dragend', src, dt);
|
|
22995
|
+
`,
|
|
22626
22996
|
sourceSelector,
|
|
22627
22997
|
targetSelector
|
|
22628
22998
|
);
|
|
@@ -22631,27 +23001,24 @@ var init_dom = __esm({
|
|
|
22631
23001
|
* File upload: unhide if needed, then send local path to <input type=file>.
|
|
22632
23002
|
*/
|
|
22633
23003
|
async uploadFileBySelector(selector, filePath) {
|
|
22634
|
-
const el = await this.
|
|
22635
|
-
await this.driver.executeScript(
|
|
22636
|
-
|
|
22637
|
-
|
|
22638
|
-
|
|
22639
|
-
|
|
22640
|
-
|
|
22641
|
-
|
|
22642
|
-
|
|
22643
|
-
|
|
22644
|
-
|
|
22645
|
-
|
|
22646
|
-
|
|
22647
|
-
|
|
22648
|
-
|
|
22649
|
-
|
|
22650
|
-
|
|
22651
|
-
|
|
22652
|
-
s.zIndex = "2147483647";
|
|
22653
|
-
}
|
|
22654
|
-
}, selector);
|
|
23004
|
+
const el = await this.waitForElement(selector, 5e3);
|
|
23005
|
+
await this.driver.executeScript(
|
|
23006
|
+
`
|
|
23007
|
+
var sel = arguments[0];
|
|
23008
|
+
var e = document.querySelector(sel);
|
|
23009
|
+
if (!e) throw new Error('uploadFile: element not found');
|
|
23010
|
+
if (e.tagName !== 'INPUT' || e.type !== 'file')
|
|
23011
|
+
throw new Error('uploadFile: selector must target <input type=file>');
|
|
23012
|
+
var style = window.getComputedStyle(e);
|
|
23013
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
|
|
23014
|
+
var s = e.style;
|
|
23015
|
+
s.display = 'block'; s.visibility = 'visible'; s.opacity = '1';
|
|
23016
|
+
s.position = 'fixed'; s.left = '0px'; s.top = '0px';
|
|
23017
|
+
s.zIndex = '2147483647';
|
|
23018
|
+
}
|
|
23019
|
+
`,
|
|
23020
|
+
selector
|
|
23021
|
+
);
|
|
22655
23022
|
await el.sendKeys(filePath);
|
|
22656
23023
|
}
|
|
22657
23024
|
// ============================================================================
|
|
@@ -22666,8 +23033,7 @@ var init_dom = __esm({
|
|
|
22666
23033
|
throw new Error("clickByUid: resolveUid callback not set. Ensure snapshot is initialized.");
|
|
22667
23034
|
}
|
|
22668
23035
|
const el = await this.resolveUid(uid);
|
|
22669
|
-
await this.
|
|
22670
|
-
});
|
|
23036
|
+
await this.waitForVisible(el, 5e3);
|
|
22671
23037
|
if (dblClick) {
|
|
22672
23038
|
await this.driver.actions({ async: true }).doubleClick(el).perform();
|
|
22673
23039
|
} else {
|
|
@@ -22715,25 +23081,20 @@ var init_dom = __esm({
|
|
|
22715
23081
|
const fromEl = await this.resolveUid(fromUid);
|
|
22716
23082
|
const toEl = await this.resolveUid(toUid);
|
|
22717
23083
|
await this.driver.executeScript(
|
|
22718
|
-
|
|
22719
|
-
|
|
22720
|
-
|
|
22721
|
-
|
|
22722
|
-
|
|
22723
|
-
|
|
22724
|
-
|
|
22725
|
-
|
|
22726
|
-
|
|
22727
|
-
|
|
22728
|
-
|
|
22729
|
-
|
|
22730
|
-
|
|
22731
|
-
|
|
22732
|
-
dispatch("dragenter", tgtEl, dt);
|
|
22733
|
-
dispatch("dragover", tgtEl, dt);
|
|
22734
|
-
dispatch("drop", tgtEl, dt);
|
|
22735
|
-
dispatch("dragend", srcEl, dt);
|
|
22736
|
-
},
|
|
23084
|
+
`
|
|
23085
|
+
var srcEl = arguments[0], tgtEl = arguments[1];
|
|
23086
|
+
if (!srcEl || !tgtEl) throw new Error('dragAndDrop: element not found');
|
|
23087
|
+
function dispatch(type, target, dt) {
|
|
23088
|
+
var evt = new DragEvent(type, { bubbles: true, cancelable: true, dataTransfer: dt });
|
|
23089
|
+
return target.dispatchEvent(evt);
|
|
23090
|
+
}
|
|
23091
|
+
var dt = typeof DataTransfer !== 'undefined' ? new DataTransfer() : undefined;
|
|
23092
|
+
dispatch('dragstart', srcEl, dt);
|
|
23093
|
+
dispatch('dragenter', tgtEl, dt);
|
|
23094
|
+
dispatch('dragover', tgtEl, dt);
|
|
23095
|
+
dispatch('drop', tgtEl, dt);
|
|
23096
|
+
dispatch('dragend', srcEl, dt);
|
|
23097
|
+
`,
|
|
22737
23098
|
fromEl,
|
|
22738
23099
|
toEl
|
|
22739
23100
|
);
|
|
@@ -22763,25 +23124,22 @@ var init_dom = __esm({
|
|
|
22763
23124
|
);
|
|
22764
23125
|
}
|
|
22765
23126
|
const el = await this.resolveUid(uid);
|
|
22766
|
-
await this.driver.executeScript(
|
|
22767
|
-
|
|
22768
|
-
|
|
22769
|
-
|
|
22770
|
-
|
|
22771
|
-
|
|
22772
|
-
|
|
22773
|
-
|
|
22774
|
-
|
|
22775
|
-
|
|
22776
|
-
|
|
22777
|
-
|
|
22778
|
-
|
|
22779
|
-
|
|
22780
|
-
|
|
22781
|
-
|
|
22782
|
-
s.zIndex = "2147483647";
|
|
22783
|
-
}
|
|
22784
|
-
}, el);
|
|
23127
|
+
await this.driver.executeScript(
|
|
23128
|
+
`
|
|
23129
|
+
var element = arguments[0];
|
|
23130
|
+
if (!element) throw new Error('uploadFile: element not found');
|
|
23131
|
+
if (element.tagName !== 'INPUT' || element.type !== 'file')
|
|
23132
|
+
throw new Error('uploadFile: element must be <input type=file>');
|
|
23133
|
+
var style = window.getComputedStyle(element);
|
|
23134
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
|
|
23135
|
+
var s = element.style;
|
|
23136
|
+
s.display = 'block'; s.visibility = 'visible'; s.opacity = '1';
|
|
23137
|
+
s.position = 'fixed'; s.left = '0px'; s.top = '0px';
|
|
23138
|
+
s.zIndex = '2147483647';
|
|
23139
|
+
}
|
|
23140
|
+
`,
|
|
23141
|
+
el
|
|
23142
|
+
);
|
|
22785
23143
|
await el.sendKeys(filePath);
|
|
22786
23144
|
await this.waitForEventsAfterAction();
|
|
22787
23145
|
}
|
|
@@ -23105,7 +23463,6 @@ var init_formatter = __esm({
|
|
|
23105
23463
|
});
|
|
23106
23464
|
|
|
23107
23465
|
// src/firefox/snapshot/resolver.ts
|
|
23108
|
-
import { By as By2 } from "selenium-webdriver";
|
|
23109
23466
|
var UidResolver;
|
|
23110
23467
|
var init_resolver = __esm({
|
|
23111
23468
|
"src/firefox/snapshot/resolver.ts"() {
|
|
@@ -23177,7 +23534,7 @@ var init_resolver = __esm({
|
|
|
23177
23534
|
return entry.css;
|
|
23178
23535
|
}
|
|
23179
23536
|
/**
|
|
23180
|
-
* Resolve UID to
|
|
23537
|
+
* Resolve UID to element (with staleness check and caching)
|
|
23181
23538
|
* Tries CSS first, falls back to XPath
|
|
23182
23539
|
*/
|
|
23183
23540
|
async resolveUidToElement(uid) {
|
|
@@ -23192,12 +23549,12 @@ var init_resolver = __esm({
|
|
|
23192
23549
|
await cached2.cachedElement.isDisplayed();
|
|
23193
23550
|
logDebug(`Using cached element for UID: ${uid}`);
|
|
23194
23551
|
return cached2.cachedElement;
|
|
23195
|
-
} catch
|
|
23552
|
+
} catch {
|
|
23196
23553
|
logDebug(`Cached element stale for UID: ${uid}, re-finding...`);
|
|
23197
23554
|
}
|
|
23198
23555
|
}
|
|
23199
23556
|
try {
|
|
23200
|
-
const element = await this.driver.findElement(
|
|
23557
|
+
const element = await this.driver.findElement({ using: "css selector", value: entry.css });
|
|
23201
23558
|
this.elementCache.set(uid, {
|
|
23202
23559
|
selector: entry.css,
|
|
23203
23560
|
...entry.xpath && { xpath: entry.xpath },
|
|
@@ -23207,12 +23564,12 @@ var init_resolver = __esm({
|
|
|
23207
23564
|
});
|
|
23208
23565
|
logDebug(`Found element by CSS for UID: ${uid}`);
|
|
23209
23566
|
return element;
|
|
23210
|
-
} catch
|
|
23567
|
+
} catch {
|
|
23211
23568
|
logDebug(`CSS selector failed for UID: ${uid}, trying XPath fallback...`);
|
|
23212
23569
|
const xpathSelector = entry.xpath;
|
|
23213
23570
|
if (xpathSelector) {
|
|
23214
23571
|
try {
|
|
23215
|
-
const element = await this.driver.findElement(
|
|
23572
|
+
const element = await this.driver.findElement({ using: "xpath", value: xpathSelector });
|
|
23216
23573
|
this.elementCache.set(uid, {
|
|
23217
23574
|
selector: entry.css,
|
|
23218
23575
|
...xpathSelector && { xpath: xpathSelector },
|
|
@@ -23222,7 +23579,7 @@ var init_resolver = __esm({
|
|
|
23222
23579
|
});
|
|
23223
23580
|
logDebug(`Found element by XPath for UID: ${uid}`);
|
|
23224
23581
|
return element;
|
|
23225
|
-
} catch
|
|
23582
|
+
} catch {
|
|
23226
23583
|
throw new Error(
|
|
23227
23584
|
`Element not found for UID: ${uid}. The element may have changed. Take a fresh snapshot.`
|
|
23228
23585
|
);
|
|
@@ -23431,14 +23788,17 @@ var init_firefox = __esm({
|
|
|
23431
23788
|
this.snapshot.clear();
|
|
23432
23789
|
}
|
|
23433
23790
|
};
|
|
23434
|
-
|
|
23435
|
-
|
|
23436
|
-
|
|
23437
|
-
|
|
23438
|
-
|
|
23439
|
-
|
|
23440
|
-
|
|
23441
|
-
|
|
23791
|
+
const hasBidi = "getBidi" in driver && typeof driver.getBidi === "function";
|
|
23792
|
+
if (hasBidi) {
|
|
23793
|
+
this.consoleEvents = new ConsoleEvents(driver, {
|
|
23794
|
+
onNavigate,
|
|
23795
|
+
autoClearOnNavigate: false
|
|
23796
|
+
});
|
|
23797
|
+
this.networkEvents = new NetworkEvents(driver, {
|
|
23798
|
+
onNavigate,
|
|
23799
|
+
autoClearOnNavigate: false
|
|
23800
|
+
});
|
|
23801
|
+
}
|
|
23442
23802
|
this.dom = new DomInteractions(
|
|
23443
23803
|
driver,
|
|
23444
23804
|
(uid) => this.snapshot.resolveUidToElement(uid)
|
|
@@ -23448,8 +23808,12 @@ var init_firefox = __esm({
|
|
|
23448
23808
|
() => this.core.getCurrentContextId(),
|
|
23449
23809
|
(id) => this.core.setCurrentContextId(id)
|
|
23450
23810
|
);
|
|
23451
|
-
|
|
23452
|
-
|
|
23811
|
+
if (this.consoleEvents) {
|
|
23812
|
+
await this.consoleEvents.subscribe(void 0);
|
|
23813
|
+
}
|
|
23814
|
+
if (this.networkEvents) {
|
|
23815
|
+
await this.networkEvents.subscribe(void 0);
|
|
23816
|
+
}
|
|
23453
23817
|
}
|
|
23454
23818
|
// ============================================================================
|
|
23455
23819
|
// DOM / Evaluate
|
|
@@ -23538,13 +23902,13 @@ var init_firefox = __esm({
|
|
|
23538
23902
|
// ============================================================================
|
|
23539
23903
|
async getConsoleMessages() {
|
|
23540
23904
|
if (!this.consoleEvents) {
|
|
23541
|
-
throw new Error("
|
|
23905
|
+
throw new Error("Console events not available in connect-existing mode (requires BiDi)");
|
|
23542
23906
|
}
|
|
23543
23907
|
return this.consoleEvents.getMessages();
|
|
23544
23908
|
}
|
|
23545
23909
|
clearConsoleMessages() {
|
|
23546
23910
|
if (!this.consoleEvents) {
|
|
23547
|
-
throw new Error("
|
|
23911
|
+
throw new Error("Console events not available in connect-existing mode (requires BiDi)");
|
|
23548
23912
|
}
|
|
23549
23913
|
this.consoleEvents.clearMessages();
|
|
23550
23914
|
}
|
|
@@ -23629,25 +23993,25 @@ var init_firefox = __esm({
|
|
|
23629
23993
|
// ============================================================================
|
|
23630
23994
|
async startNetworkMonitoring() {
|
|
23631
23995
|
if (!this.networkEvents) {
|
|
23632
|
-
throw new Error("
|
|
23996
|
+
throw new Error("Network events not available in connect-existing mode (requires BiDi)");
|
|
23633
23997
|
}
|
|
23634
23998
|
this.networkEvents.startMonitoring();
|
|
23635
23999
|
}
|
|
23636
24000
|
async stopNetworkMonitoring() {
|
|
23637
24001
|
if (!this.networkEvents) {
|
|
23638
|
-
throw new Error("
|
|
24002
|
+
throw new Error("Network events not available in connect-existing mode (requires BiDi)");
|
|
23639
24003
|
}
|
|
23640
24004
|
this.networkEvents.stopMonitoring();
|
|
23641
24005
|
}
|
|
23642
24006
|
async getNetworkRequests() {
|
|
23643
24007
|
if (!this.networkEvents) {
|
|
23644
|
-
throw new Error("
|
|
24008
|
+
throw new Error("Network events not available in connect-existing mode (requires BiDi)");
|
|
23645
24009
|
}
|
|
23646
24010
|
return this.networkEvents.getRequests();
|
|
23647
24011
|
}
|
|
23648
24012
|
clearNetworkRequests() {
|
|
23649
24013
|
if (!this.networkEvents) {
|
|
23650
|
-
throw new Error("
|
|
24014
|
+
throw new Error("Network events not available in connect-existing mode (requires BiDi)");
|
|
23651
24015
|
}
|
|
23652
24016
|
this.networkEvents.clearRequests();
|
|
23653
24017
|
}
|
|
@@ -25647,7 +26011,9 @@ async function getFirefox() {
|
|
|
25647
26011
|
viewport: args.viewport ?? void 0,
|
|
25648
26012
|
args: args.firefoxArg ?? void 0,
|
|
25649
26013
|
startUrl: args.startUrl ?? void 0,
|
|
25650
|
-
acceptInsecureCerts: args.acceptInsecureCerts
|
|
26014
|
+
acceptInsecureCerts: args.acceptInsecureCerts,
|
|
26015
|
+
connectExisting: args.connectExisting,
|
|
26016
|
+
marionettePort: args.marionettePort
|
|
25651
26017
|
};
|
|
25652
26018
|
firefox2 = new FirefoxClient(options);
|
|
25653
26019
|
await firefox2.connect();
|