firefox-devtools-mcp 0.2.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.
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import readline from 'readline';
6
+ import os from 'os';
7
+ import { fileURLToPath } from 'url';
8
+ import { execSync } from 'child_process';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+
13
+ const rl = readline.createInterface({
14
+ input: process.stdin,
15
+ output: process.stdout,
16
+ });
17
+
18
+ // Colors for terminal output
19
+ const colors = {
20
+ reset: '\x1b[0m',
21
+ bright: '\x1b[1m',
22
+ green: '\x1b[32m',
23
+ yellow: '\x1b[33m',
24
+ blue: '\x1b[34m',
25
+ red: '\x1b[31m',
26
+ };
27
+
28
+ function question(query) {
29
+ return new Promise((resolve) => rl.question(query, resolve));
30
+ }
31
+
32
+ function detectNodePath() {
33
+ try {
34
+ // First try to get current Node.js path
35
+ const nodeVersion = process.version;
36
+ console.log(`${colors.green}✓ Detected Node.js version: ${nodeVersion}${colors.reset}`);
37
+
38
+ // Check if using nvm
39
+ const nvmDir = process.env.NVM_DIR;
40
+ if (nvmDir) {
41
+ // Try to get the current version from nvm
42
+ try {
43
+ const currentVersion = execSync('nvm current', { encoding: 'utf8' }).trim();
44
+ const nodePath = path.join(nvmDir, 'versions', 'node', currentVersion, 'bin', 'node');
45
+ if (fs.existsSync(nodePath)) {
46
+ console.log(`${colors.green}✓ Using nvm Node.js at: ${nodePath}${colors.reset}`);
47
+ return nodePath;
48
+ }
49
+ } catch (e) {
50
+ // nvm not available, continue
51
+ }
52
+ }
53
+
54
+ // Use the current Node.js executable path
55
+ console.log(`${colors.green}✓ Using Node.js at: ${process.execPath}${colors.reset}`);
56
+ return process.execPath;
57
+ } catch (error) {
58
+ console.log(`${colors.yellow}⚠️ Could not detect Node.js path, using default${colors.reset}`);
59
+ return 'node';
60
+ }
61
+ }
62
+
63
+ async function main() {
64
+ console.log(
65
+ `${colors.bright}${colors.blue}🚀 Firefox DevTools MCP Configuration Setup${colors.reset}\n`
66
+ );
67
+
68
+ // Ask which client to configure
69
+ console.log(`${colors.bright}Select MCP client to configure:${colors.reset}`);
70
+ console.log('1. Claude Desktop (desktop app)');
71
+ console.log('2. Claude Code (CLI tool)');
72
+ console.log('3. Both');
73
+ console.log('4. Display config only (manual setup)');
74
+
75
+ const clientChoice = (await question('\nSelect option (1-4) [2]: ')) || '2';
76
+
77
+ // Get project path
78
+ const projectPath = path.resolve(__dirname, '..');
79
+ const distIndexPath = path.join(projectPath, 'dist', 'index.js');
80
+
81
+ // Check if we need to build first
82
+ const srcIndexPath = path.join(projectPath, 'src', 'index.ts');
83
+ if (!fs.existsSync(distIndexPath) && fs.existsSync(srcIndexPath)) {
84
+ console.log(`${colors.yellow}⚠️ Dist file not found. Building project...${colors.reset}`);
85
+ try {
86
+ execSync('npm run build', { cwd: projectPath, stdio: 'inherit' });
87
+ console.log(`${colors.green}✓ Build completed${colors.reset}\n`);
88
+ } catch (error) {
89
+ console.log(
90
+ `${colors.red}❌ Build failed. Please run 'npm run build' manually${colors.reset}`
91
+ );
92
+ process.exit(1);
93
+ }
94
+ }
95
+
96
+ if (!fs.existsSync(distIndexPath)) {
97
+ console.log(`${colors.red}❌ Dist file not found at: ${distIndexPath}${colors.reset}`);
98
+ console.log(`${colors.yellow}Please run 'npm run build' first${colors.reset}`);
99
+ process.exit(1);
100
+ }
101
+
102
+ console.log(`${colors.green}✓ Found server dist at: ${distIndexPath}${colors.reset}\n`);
103
+
104
+ // Get Firefox configuration
105
+ console.log(`${colors.bright}Firefox Configuration:${colors.reset}`);
106
+ console.log(`${colors.yellow}(Press Enter to use default values)${colors.reset}\n`);
107
+
108
+ const headless = (await question('Run in headless mode? (y/n) [n]: ')) || 'n';
109
+ const viewport = (await question('Viewport size (e.g., 1280x720) [1280x720]: ')) || '1280x720';
110
+
111
+ // Detect Node.js path
112
+ const nodePath = detectNodePath();
113
+
114
+ // Ask user about Node.js configuration
115
+ console.log(`\n${colors.bright}Node.js Configuration:${colors.reset}`);
116
+ console.log(`1. Use detected Node.js: ${nodePath}`);
117
+ console.log(`2. Specify custom Node.js path`);
118
+ console.log(`3. Use system default (node)`);
119
+
120
+ const nodeChoice = await question('\nSelect option (1-3) [1]: ') || '1';
121
+ let finalNodePath = nodePath;
122
+
123
+ switch (nodeChoice) {
124
+ case '2':
125
+ finalNodePath = await question('Enter full path to Node.js executable: ');
126
+ if (!fs.existsSync(finalNodePath)) {
127
+ console.log(`${colors.yellow}⚠️ Path not found, using detected path${colors.reset}`);
128
+ finalNodePath = nodePath;
129
+ }
130
+ break;
131
+ case '3':
132
+ finalNodePath = 'node';
133
+ break;
134
+ }
135
+
136
+ // Create MCP config
137
+ const mcpConfig = {
138
+ mcpServers: {
139
+ 'firefox-devtools': {
140
+ command: finalNodePath,
141
+ args: [distIndexPath, '--headless=' + (headless.toLowerCase() === 'y' ? 'true' : 'false'), '--viewport=' + viewport],
142
+ },
143
+ },
144
+ };
145
+
146
+ // Determine config paths based on client choice
147
+ const platform = os.platform();
148
+ let desktopConfigPath, desktopConfigDir;
149
+ let claudeCodeConfigPath, claudeCodeConfigDir;
150
+
151
+ // Claude Desktop paths
152
+ if (platform === 'darwin') {
153
+ desktopConfigDir = path.join(os.homedir(), 'Library', 'Application Support', 'Claude');
154
+ desktopConfigPath = path.join(desktopConfigDir, 'claude_desktop_config.json');
155
+ } else if (platform === 'win32') {
156
+ desktopConfigDir = path.join(os.homedir(), 'AppData', 'Roaming', 'Claude');
157
+ desktopConfigPath = path.join(desktopConfigDir, 'claude_desktop_config.json');
158
+ } else {
159
+ desktopConfigDir = path.join(os.homedir(), '.config', 'claude');
160
+ desktopConfigPath = path.join(desktopConfigDir, 'claude_desktop_config.json');
161
+ }
162
+
163
+ // Claude Code paths
164
+ if (platform === 'darwin') {
165
+ claudeCodeConfigDir = path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'Code');
166
+ claudeCodeConfigPath = path.join(claudeCodeConfigDir, 'mcp_settings.json');
167
+ } else if (platform === 'win32') {
168
+ claudeCodeConfigDir = path.join(os.homedir(), 'AppData', 'Roaming', 'Claude', 'Code');
169
+ claudeCodeConfigPath = path.join(claudeCodeConfigDir, 'mcp_settings.json');
170
+ } else {
171
+ claudeCodeConfigDir = path.join(os.homedir(), '.config', 'claude', 'code');
172
+ claudeCodeConfigPath = path.join(claudeCodeConfigDir, 'mcp_settings.json');
173
+ }
174
+
175
+ // Handle display-only option
176
+ if (clientChoice === '4') {
177
+ console.log(
178
+ `\n${colors.bright}Copy this configuration to your MCP config:${colors.reset}\n`
179
+ );
180
+ console.log(JSON.stringify(mcpConfig, null, 2));
181
+
182
+ console.log(`\n${colors.bright}${colors.blue}Config file locations:${colors.reset}`);
183
+ console.log(`Claude Desktop: ${desktopConfigPath}`);
184
+ console.log(`Claude Code: ${claudeCodeConfigPath}`);
185
+ rl.close();
186
+ return;
187
+ }
188
+
189
+ // Helper function to save config
190
+ async function saveConfig(configPath, configDir, clientName) {
191
+ let existingConfig = {};
192
+ if (fs.existsSync(configPath)) {
193
+ try {
194
+ existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
195
+ if (existingConfig.mcpServers && existingConfig.mcpServers['firefox-devtools']) {
196
+ const overwrite = await question(
197
+ `\n${colors.yellow}⚠️ 'firefox-devtools' server already exists in ${clientName}. Overwrite? (y/n): ${colors.reset}`
198
+ );
199
+ if (overwrite.toLowerCase() !== 'y') {
200
+ console.log(`Skipped ${clientName}`);
201
+ return false;
202
+ }
203
+ }
204
+ } catch (error) {
205
+ console.log(
206
+ `${colors.yellow}⚠️ Could not read existing ${clientName} config, will create new one${colors.reset}`
207
+ );
208
+ }
209
+ }
210
+
211
+ // Merge configs
212
+ const finalConfig = {
213
+ ...existingConfig,
214
+ mcpServers: {
215
+ ...existingConfig.mcpServers,
216
+ ...mcpConfig.mcpServers,
217
+ },
218
+ };
219
+
220
+ if (!fs.existsSync(configDir)) {
221
+ fs.mkdirSync(configDir, { recursive: true });
222
+ }
223
+
224
+ fs.writeFileSync(configPath, JSON.stringify(finalConfig, null, 2));
225
+ console.log(`\n${colors.green}✅ Configuration saved to ${clientName}: ${configPath}${colors.reset}`);
226
+ return true;
227
+ }
228
+
229
+ // Save to appropriate client(s)
230
+ if (clientChoice === '1') {
231
+ await saveConfig(desktopConfigPath, desktopConfigDir, 'Claude Desktop');
232
+ console.log(`${colors.yellow}⚠️ Restart Claude Desktop to apply changes${colors.reset}`);
233
+ } else if (clientChoice === '2') {
234
+ await saveConfig(claudeCodeConfigPath, claudeCodeConfigDir, 'Claude Code');
235
+ console.log(`${colors.yellow}⚠️ Restart Claude Code to apply changes${colors.reset}`);
236
+ } else if (clientChoice === '3') {
237
+ await saveConfig(desktopConfigPath, desktopConfigDir, 'Claude Desktop');
238
+ await saveConfig(claudeCodeConfigPath, claudeCodeConfigDir, 'Claude Code');
239
+ console.log(`${colors.yellow}⚠️ Restart Claude Desktop and Claude Code to apply changes${colors.reset}`);
240
+ }
241
+
242
+ // Show next steps
243
+ console.log(`\n${colors.bright}${colors.blue}Next Steps:${colors.reset}`);
244
+ if (clientChoice === '1' || clientChoice === '3') {
245
+ console.log(`1. Restart Claude Desktop`);
246
+ }
247
+ if (clientChoice === '2' || clientChoice === '3') {
248
+ console.log(`${clientChoice === '3' ? '2' : '1'}. Restart Claude Code (or reload window)`);
249
+ }
250
+ console.log(
251
+ `${clientChoice === '3' ? '3' : '2'}. Test with: "${colors.green}List all open pages in Firefox${colors.reset}" or "${colors.green}Take a screenshot${colors.reset}"`
252
+ );
253
+
254
+ rl.close();
255
+ }
256
+
257
+ main().catch((error) => {
258
+ console.error(`${colors.red}Error: ${error.message}${colors.reset}`);
259
+ process.exit(1);
260
+ });
@@ -0,0 +1,380 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Test script for new BiDi-based FirefoxDevTools
5
+ * Tests all main functionality with clean BiDi implementation
6
+ *
7
+ * Runs offline by default. Set TEST_ONLINE=1 to enable online tests.
8
+ */
9
+
10
+ import { FirefoxDevTools } from '../dist/index.js';
11
+ import {
12
+ loadHTML,
13
+ waitShort,
14
+ shouldRunOnlineTests,
15
+ skipOnlineTest,
16
+ } from './_helpers/page-loader.js';
17
+
18
+ async function main() {
19
+ console.log('🧪 Testing BiDi-based FirefoxDevTools...\n');
20
+
21
+ const options = {
22
+ firefoxPath: undefined, // Auto-detect
23
+ headless: false,
24
+ profilePath: undefined,
25
+ viewport: { width: 1280, height: 720 },
26
+ args: [],
27
+ startUrl: 'about:blank', // Offline-first
28
+ };
29
+
30
+ const firefox = new FirefoxDevTools(options);
31
+
32
+ try {
33
+ // 1. Connect
34
+ console.log('📡 Connecting to Firefox via BiDi...');
35
+ await firefox.connect();
36
+ console.log('✅ Connected!\n');
37
+
38
+ // 2. List tabs
39
+ console.log('📄 Listing tabs...');
40
+ await firefox.refreshTabs();
41
+ const tabs = firefox.getTabs();
42
+ console.log(`✅ Found ${tabs.length} tab(s):`);
43
+ tabs.forEach((tab, idx) => {
44
+ console.log(` [${idx}] ${tab.title} - ${tab.url}`);
45
+ });
46
+ console.log();
47
+
48
+ // 3. Test evaluate with offline page
49
+ console.log('📖 Testing evaluate (offline)...');
50
+ const test1 = await firefox.evaluate('return 1 + 1');
51
+ console.log(` 1 + 1 = ${test1}`);
52
+ const test2 = await firefox.evaluate('return document.title');
53
+ console.log(` document.title = ${test2}`);
54
+ console.log('✅ Evaluate works\n');
55
+
56
+ // 4. Test console messages (offline)
57
+ console.log('⚡ Testing console messages (offline)...');
58
+ await firefox.evaluate(`
59
+ console.log('🎯 BiDi test log message!');
60
+ console.warn('⚠️ BiDi test warning!');
61
+ console.error('❌ BiDi test error!');
62
+ console.info('ℹ️ BiDi test info!');
63
+ `);
64
+ await waitShort(1000);
65
+
66
+ const messages = await firefox.getConsoleMessages();
67
+ console.log(`✅ Captured ${messages.length} console message(s):`);
68
+ messages.forEach((msg) => {
69
+ const levelEmoji = { debug: '🐛', info: 'ℹ️', warn: '⚠️', error: '❌' }[msg.level] || '📘';
70
+ console.log(` ${levelEmoji} [${msg.level}] ${msg.text}`);
71
+ });
72
+ console.log();
73
+
74
+ // 5. Drag & drop test (offline)
75
+ console.log('🧲 Testing drag & drop (offline)...');
76
+ try {
77
+ await loadHTML(
78
+ firefox,
79
+ `<head><title>DnD Test</title><style>
80
+ #drag{width:80px;height:80px;background:#08f;color:#fff;display:flex;align-items:center;justify-content:center}
81
+ #drop{width:160px;height:100px;border:3px dashed #888;margin-left:16px;display:inline-flex;align-items:center;justify-content:center}
82
+ #ok{color:green;font-weight:bold}
83
+ </style></head><body>
84
+ <div id=drag draggable=true>Drag</div><div id=drop>Drop here</div>
85
+ <script>
86
+ const drop = document.getElementById('drop');
87
+ drop.addEventListener('drop', (e)=>{e.preventDefault();drop.innerHTML='<span id=ok>OK</span>';});
88
+ drop.addEventListener('dragover', (e)=>e.preventDefault());
89
+ </script>
90
+ </body>`
91
+ );
92
+ await firefox.dragAndDropBySelectors('#drag', '#drop');
93
+ const ok = await firefox.evaluate("return !!document.querySelector('#ok')");
94
+ console.log(ok ? '✅ Drag & drop worked\n' : '❌ Drag & drop failed\n');
95
+ } catch (e) {
96
+ console.log('⚠️ Skipping drag & drop test:', e.message);
97
+ }
98
+
99
+ // 6. File upload test (offline)
100
+ console.log('📁 Testing file upload (offline)...');
101
+ try {
102
+ const fs = await import('node:fs/promises');
103
+ const os = await import('node:os');
104
+ const path = await import('node:path');
105
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'bidi-test-'));
106
+ const filePath = path.join(tmp, 'hello.txt');
107
+ await fs.writeFile(filePath, 'hello bidi');
108
+
109
+ await loadHTML(
110
+ firefox,
111
+ `<head><title>Upload Test</title><style>#file{display:none}</style></head><body>
112
+ <label for=file>Pick file</label>
113
+ <input id=file type=file>
114
+ <script>document.getElementById('file').addEventListener('change',()=>{document.body.setAttribute('data-ok','1')});</script>
115
+ </body>`
116
+ );
117
+ await firefox.uploadFileBySelector('#file', filePath);
118
+ const ok = await firefox.evaluate("return document.body.getAttribute('data-ok') === '1'");
119
+ console.log(ok ? '✅ File upload worked\n' : '❌ File upload failed\n');
120
+ } catch (e) {
121
+ console.log('⚠️ Skipping file upload test:', e.message);
122
+ }
123
+
124
+ // 7. Viewport resize test
125
+ console.log('📐 Resizing viewport to 1024x600...');
126
+ try {
127
+ await firefox.setViewportSize(1024, 600);
128
+ console.log('✅ Viewport resized\n');
129
+ } catch (e) {
130
+ console.log('⚠️ Skipping viewport resize test:', e.message);
131
+ }
132
+
133
+ // 8. Snapshot tests (offline with data: URL)
134
+ console.log('📸 Testing snapshot functionality (offline)...');
135
+ try {
136
+ await loadHTML(
137
+ firefox,
138
+ `<head><title>Test Page</title></head><body>
139
+ <h1>Example Domain</h1>
140
+ <p>This domain is for use in documentation examples.</p>
141
+ <p><a href="https://iana.org/domains/example">Learn more</a></p>
142
+ </body>`
143
+ );
144
+
145
+ console.log(' Taking first snapshot...');
146
+ const snapshot1 = await firefox.takeSnapshot();
147
+ console.log(`✅ Snapshot taken! (ID: ${snapshot1.json.snapshotId})`);
148
+ console.log(` First few lines of text output:`);
149
+ const lines = snapshot1.text.split('\n').slice(0, 6);
150
+ lines.forEach((line) => console.log(` ${line}`));
151
+
152
+ // Test UID resolution
153
+ console.log('\n Testing UID resolution...');
154
+ const firstUid = snapshot1.json.root.uid;
155
+ const selector = firefox.resolveUidToSelector(firstUid);
156
+ console.log(` ✅ UID ${firstUid} resolves to selector: ${selector}`);
157
+
158
+ const element = await firefox.resolveUidToElement(firstUid);
159
+ console.log(` ✅ UID ${firstUid} resolves to WebElement: ${!!element}`);
160
+
161
+ // Test staleness detection (navigation)
162
+ console.log('\n Testing staleness detection...');
163
+ await firefox.navigate('about:blank');
164
+ await waitShort();
165
+
166
+ try {
167
+ firefox.resolveUidToSelector(firstUid);
168
+ console.log(' ❌ Staleness detection failed - should have thrown error');
169
+ } catch (e) {
170
+ console.log(` ✅ Staleness detected correctly: ${e.message}`);
171
+ }
172
+
173
+ // Test iframe support
174
+ console.log('\n Testing iframe support...');
175
+ await loadHTML(
176
+ firefox,
177
+ `<head><title>Iframe Test</title></head><body>
178
+ <h1>Main Page</h1>
179
+ <p>This is the main page</p>
180
+ <iframe srcdoc="<h2>Iframe Content</h2><p>This is inside the iframe</p>"></iframe>
181
+ </body>`
182
+ );
183
+ const snapshot2 = await firefox.takeSnapshot();
184
+ const hasIframe = JSON.stringify(snapshot2.json).includes('isIframe');
185
+ console.log(` ${hasIframe ? '✅' : '❌'} Iframe detected in snapshot: ${hasIframe}`);
186
+
187
+ console.log('\n✅ Snapshot tests completed!\n');
188
+ } catch (e) {
189
+ console.log('⚠️ Snapshot test failed:', e.message);
190
+ if (e.stack) console.log(e.stack);
191
+ }
192
+
193
+ // 9. Screenshot tests (offline)
194
+ console.log('📷 Testing screenshot functionality (offline)...');
195
+ try {
196
+ await loadHTML(
197
+ firefox,
198
+ `<head><title>Screenshot Test</title></head><body>
199
+ <h1>Test Heading</h1>
200
+ <p>This is a test paragraph.</p>
201
+ </body>`
202
+ );
203
+
204
+ const pageScreenshot = await firefox.takeScreenshotPage();
205
+ console.log(` ✅ Page screenshot captured (${pageScreenshot.length} chars base64)`);
206
+
207
+ const isValidBase64 = /^[A-Za-z0-9+/=]+$/.test(pageScreenshot);
208
+ const isPNG = pageScreenshot.startsWith('iVBOR');
209
+ console.log(` ${isValidBase64 ? '✅' : '❌'} Valid base64: ${isValidBase64}`);
210
+ console.log(` ${isPNG ? '✅' : '❌'} PNG format: ${isPNG}`);
211
+
212
+ // Element screenshot by UID
213
+ const snapshot = await firefox.takeSnapshot();
214
+ const targetNode = snapshot.json.root.children?.find((n) => n.tag === 'h1');
215
+
216
+ if (targetNode && targetNode.uid) {
217
+ const elementScreenshot = await firefox.takeScreenshotByUid(targetNode.uid);
218
+ console.log(` ✅ Element screenshot captured (${elementScreenshot.length} chars base64)`);
219
+ } else {
220
+ console.log(' ⚠️ No suitable element found for screenshot test');
221
+ }
222
+
223
+ console.log('\n✅ Screenshot tests completed!\n');
224
+ } catch (e) {
225
+ console.log('⚠️ Screenshot test failed:', e.message);
226
+ }
227
+
228
+ // 10. Dialog handling tests (offline)
229
+ console.log('💬 Testing dialog handling (offline)...');
230
+ try {
231
+ await firefox.navigate('about:blank');
232
+
233
+ // Alert dialog
234
+ console.log(' Testing alert dialog...');
235
+ await firefox.evaluate('setTimeout(() => alert("Test alert!"), 100)');
236
+ await waitShort(200);
237
+ await firefox.acceptDialog();
238
+ console.log(' ✅ Alert dialog accepted');
239
+
240
+ // Confirm dialog - accept
241
+ console.log('\n Testing confirm dialog (accept)...');
242
+ await firefox.evaluate('setTimeout(() => { window.confirmResult = confirm("Test confirm?"); }, 100)');
243
+ await waitShort(200);
244
+ await firefox.acceptDialog();
245
+ const confirmAccepted = await firefox.evaluate('return window.confirmResult');
246
+ console.log(` ${confirmAccepted ? '✅' : '❌'} Confirm accepted: ${confirmAccepted}`);
247
+
248
+ // Confirm dialog - dismiss
249
+ console.log('\n Testing confirm dialog (dismiss)...');
250
+ await firefox.evaluate(
251
+ 'setTimeout(() => { window.confirmResult2 = confirm("Test confirm 2?"); }, 100)'
252
+ );
253
+ await waitShort(200);
254
+ await firefox.dismissDialog();
255
+ const confirmDismissed = await firefox.evaluate('return window.confirmResult2');
256
+ console.log(` ${!confirmDismissed ? '✅' : '❌'} Confirm dismissed: ${!confirmDismissed}`);
257
+
258
+ // Prompt dialog
259
+ console.log('\n Testing prompt dialog...');
260
+ await firefox.evaluate('setTimeout(() => { window.promptResult = prompt("Enter your name:"); }, 100)');
261
+ await waitShort(200);
262
+ await firefox.acceptDialog('John Doe');
263
+ const promptResult = await firefox.evaluate('return window.promptResult');
264
+ console.log(` ${promptResult === 'John Doe' ? '✅' : '❌'} Prompt result: ${promptResult}`);
265
+
266
+ // Error handling
267
+ console.log('\n Testing error handling (no dialog)...');
268
+ try {
269
+ await firefox.acceptDialog();
270
+ console.log(' ❌ Should have thrown error for missing dialog');
271
+ } catch (e) {
272
+ console.log(` ✅ Error caught correctly: ${e.message}`);
273
+ }
274
+
275
+ console.log('\n✅ Dialog handling tests completed!\n');
276
+ } catch (e) {
277
+ console.log('⚠️ Dialog test failed:', e.message);
278
+ }
279
+
280
+ // ============================================================================
281
+ // ONLINE TESTS (optional - set TEST_ONLINE=1)
282
+ // ============================================================================
283
+
284
+ if (shouldRunOnlineTests()) {
285
+ console.log('\n🌐 Running online tests...\n');
286
+
287
+ // Navigate to example.com
288
+ console.log('🧭 Navigating to example.com...');
289
+ await firefox.navigate('https://example.com');
290
+ console.log('✅ Navigation completed');
291
+ await waitShort(2000);
292
+
293
+ const newTitle = await firefox.evaluate('return document.title');
294
+ console.log(`✅ Page title: ${newTitle}\n`);
295
+
296
+ // Tab management
297
+ console.log('📑 Creating new tab...');
298
+ const newTabIdx = await firefox.createNewPage('https://www.mozilla.org');
299
+ console.log(`✅ Created new tab [${newTabIdx}]\n`);
300
+ await waitShort(3000);
301
+
302
+ await firefox.refreshTabs();
303
+ const allTabs = firefox.getTabs();
304
+ console.log('📄 All tabs:');
305
+ allTabs.forEach((tab, idx) => {
306
+ const marker = idx === firefox.getSelectedTabIdx() ? '👉' : ' ';
307
+ console.log(`${marker} [${idx}] ${tab.title.substring(0, 50)} - ${tab.url}`);
308
+ });
309
+ console.log();
310
+
311
+ // Network monitoring
312
+ console.log('🌐 Testing network monitoring...');
313
+ try {
314
+ await firefox.startNetworkMonitoring();
315
+ console.log(' Network monitoring started');
316
+
317
+ await waitShort(100);
318
+ await firefox.navigate('https://example.com');
319
+ await waitShort(3000);
320
+
321
+ const requests = await firefox.getNetworkRequests();
322
+ console.log(`✅ Captured ${requests.length} network request(s):`);
323
+
324
+ requests.slice(0, 5).forEach((req, idx) => {
325
+ const statusEmoji = req.status >= 400 ? '❌' : req.status >= 300 ? '⚠️' : '✅';
326
+ console.log(
327
+ ` ${idx + 1}. ${statusEmoji} [${req.method}] ${req.status || '?'} ${req.url.substring(0, 80)}`
328
+ );
329
+ });
330
+
331
+ if (requests.length > 5) {
332
+ console.log(` ... and ${requests.length - 5} more requests`);
333
+ }
334
+
335
+ await firefox.stopNetworkMonitoring();
336
+ firefox.clearNetworkRequests();
337
+ console.log('✅ Network monitoring stopped\n');
338
+ } catch (e) {
339
+ console.log('⚠️ Network monitoring test failed:', e.message, '\n');
340
+ }
341
+
342
+ // History navigation
343
+ console.log('↩️ Testing back/forward navigation...');
344
+ try {
345
+ await firefox.navigate('https://example.com');
346
+ await waitShort(1000);
347
+ await firefox.navigate('https://www.mozilla.org');
348
+ await waitShort(1000);
349
+ await firefox.navigateBack();
350
+ const titleBack = await firefox.evaluate('return document.title');
351
+ console.log(' Back title:', titleBack);
352
+ await firefox.navigateForward();
353
+ const titleFwd = await firefox.evaluate('return document.title');
354
+ console.log(' Forward title:', titleFwd);
355
+ console.log('✅ History navigation tested\n');
356
+ } catch (e) {
357
+ console.log('⚠️ Skipping history test:', e.message);
358
+ }
359
+ } else {
360
+ skipOnlineTest('Online tests (navigation, network, history, tabs)');
361
+ }
362
+
363
+ console.log('✅ All BiDi DevTools tests completed! 🎉\n');
364
+ } catch (error) {
365
+ console.error('❌ Test failed:', error.message);
366
+ if (error.stack) {
367
+ console.error(error.stack);
368
+ }
369
+ process.exit(1);
370
+ } finally {
371
+ console.log('🧹 Closing connection...');
372
+ await firefox.close();
373
+ console.log('✅ Done');
374
+ }
375
+ }
376
+
377
+ main().catch((error) => {
378
+ console.error('💥 Fatal error:', error);
379
+ process.exit(1);
380
+ });