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.
- package/LICENSE +21 -0
- package/README.md +126 -0
- package/dist/index.js +15826 -0
- package/dist/snapshot.injected.global.js +1 -0
- package/package.json +102 -0
- package/scripts/_helpers/page-loader.js +121 -0
- package/scripts/demo-server.js +325 -0
- package/scripts/setup-mcp-config.js +260 -0
- package/scripts/test-bidi-devtools.js +380 -0
- package/scripts/test-console.js +148 -0
- package/scripts/test-dialog.js +102 -0
- package/scripts/test-input-tools.js +233 -0
- package/scripts/test-lifecycle-hooks.js +124 -0
- package/scripts/test-screenshot.js +190 -0
|
@@ -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
|
+
});
|