chrometools-mcp 2.4.2 → 3.1.2
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/CHANGELOG.md +540 -0
- package/COMPONENT_MAPPING_SPEC.md +1217 -0
- package/README.md +494 -38
- package/bridge/bridge-client.js +472 -0
- package/bridge/bridge-service.js +399 -0
- package/bridge/install.js +241 -0
- package/browser/browser-manager.js +107 -2
- package/browser/page-manager.js +226 -69
- package/docs/CHROME_EXTENSION.md +219 -0
- package/docs/PAGE_OBJECT_MODEL_CONCEPT.md +1756 -0
- package/element-finder-utils.js +138 -28
- package/extension/background.js +643 -0
- package/extension/content.js +715 -0
- package/extension/icons/create-icons.js +164 -0
- package/extension/icons/icon128.png +0 -0
- package/extension/icons/icon16.png +0 -0
- package/extension/icons/icon48.png +0 -0
- package/extension/manifest.json +58 -0
- package/extension/popup/popup.css +437 -0
- package/extension/popup/popup.html +102 -0
- package/extension/popup/popup.js +415 -0
- package/extension/recorder-overlay.css +93 -0
- package/figma-tools.js +120 -0
- package/index.js +3347 -2518
- package/models/BaseInputModel.js +93 -0
- package/models/CheckboxGroupModel.js +199 -0
- package/models/CheckboxModel.js +103 -0
- package/models/ColorInputModel.js +53 -0
- package/models/DateInputModel.js +67 -0
- package/models/RadioGroupModel.js +126 -0
- package/models/RangeInputModel.js +60 -0
- package/models/SelectModel.js +97 -0
- package/models/TextInputModel.js +34 -0
- package/models/TextareaModel.js +59 -0
- package/models/TimeInputModel.js +49 -0
- package/models/index.js +122 -0
- package/package.json +3 -2
- package/pom/apom-converter.js +267 -0
- package/pom/apom-tree-converter.js +515 -0
- package/pom/element-id-generator.js +175 -0
- package/recorder/page-object-generator.js +16 -0
- package/recorder/scenario-executor.js +80 -2
- package/server/tool-definitions.js +839 -656
- package/server/tool-groups.js +3 -2
- package/server/tool-schemas.js +367 -296
- package/server/websocket-bridge.js +447 -0
- package/utils/selector-resolver.js +186 -0
- package/utils/ui-framework-detector.js +392 -0
|
@@ -7,12 +7,25 @@
|
|
|
7
7
|
import puppeteer from 'puppeteer';
|
|
8
8
|
import { spawn } from 'child_process';
|
|
9
9
|
import http from 'http';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
10
12
|
import { getChromePath, getTempDir, isWSL, CHROME_DEBUG_PORT } from '../utils/platform-utils.js';
|
|
13
|
+
import { handleNewTab, openPages, lastPage } from './page-manager.js';
|
|
14
|
+
|
|
15
|
+
// Get extension path (use forward slashes for Chrome on Windows)
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const EXTENSION_PATH = path.join(__dirname, '..', 'extension').replace(/\\/g, '/');
|
|
11
18
|
|
|
12
19
|
// Global browser instance (persists between requests)
|
|
13
20
|
let browserPromise = null;
|
|
14
21
|
let chromeProcess = null;
|
|
15
22
|
|
|
23
|
+
// Track if we connected to existing Chrome (extension won't be auto-loaded)
|
|
24
|
+
let connectedToExistingChrome = false;
|
|
25
|
+
|
|
26
|
+
// Track pages we've already seen to avoid double-handling
|
|
27
|
+
const knownTargets = new WeakSet();
|
|
28
|
+
|
|
16
29
|
/**
|
|
17
30
|
* Debug log helper (only logs to stderr when DEBUG=1)
|
|
18
31
|
* @param {...any} args - Arguments to log
|
|
@@ -91,15 +104,24 @@ export async function getBrowser() {
|
|
|
91
104
|
debugLog("Connected to existing Chrome instance");
|
|
92
105
|
debugLog("WebSocket endpoint:", endpoint);
|
|
93
106
|
|
|
107
|
+
// Mark that we connected to existing Chrome (extension won't be auto-loaded)
|
|
108
|
+
connectedToExistingChrome = true;
|
|
109
|
+
console.error("[chrometools-mcp] Connected to existing Chrome. Extension may need manual installation.");
|
|
110
|
+
|
|
94
111
|
// Set up disconnect handler to reset browserPromise
|
|
95
112
|
browser.on('disconnected', () => {
|
|
96
113
|
debugLog("Browser disconnected");
|
|
97
114
|
browserPromise = null;
|
|
115
|
+
connectedToExistingChrome = false;
|
|
98
116
|
});
|
|
99
117
|
|
|
118
|
+
// Set up new tab tracking
|
|
119
|
+
setupNewTabTracking(browser);
|
|
120
|
+
|
|
100
121
|
return browser;
|
|
101
122
|
} catch (connectError) {
|
|
102
123
|
debugLog("No existing Chrome found, launching new instance...");
|
|
124
|
+
connectedToExistingChrome = false;
|
|
103
125
|
}
|
|
104
126
|
|
|
105
127
|
// Launch new Chrome with remote debugging enabled
|
|
@@ -109,12 +131,19 @@ export async function getBrowser() {
|
|
|
109
131
|
debugLog("Chrome path:", chromePath);
|
|
110
132
|
debugLog("User data dir:", userDataDir);
|
|
111
133
|
|
|
112
|
-
|
|
134
|
+
// Build Chrome launch arguments
|
|
135
|
+
const chromeArgs = [
|
|
113
136
|
`--remote-debugging-port=${CHROME_DEBUG_PORT}`,
|
|
114
137
|
'--no-first-run',
|
|
115
138
|
'--no-default-browser-check',
|
|
116
139
|
`--user-data-dir=${userDataDir}`,
|
|
117
|
-
|
|
140
|
+
// Auto-load ChromeTools extension
|
|
141
|
+
`--load-extension=${EXTENSION_PATH}`,
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
debugLog("Extension path:", EXTENSION_PATH);
|
|
145
|
+
|
|
146
|
+
chromeProcess = spawn(chromePath, chromeArgs, {
|
|
118
147
|
detached: true,
|
|
119
148
|
stdio: 'ignore',
|
|
120
149
|
});
|
|
@@ -141,6 +170,9 @@ export async function getBrowser() {
|
|
|
141
170
|
browserPromise = null;
|
|
142
171
|
});
|
|
143
172
|
|
|
173
|
+
// Set up new tab tracking
|
|
174
|
+
setupNewTabTracking(browser);
|
|
175
|
+
|
|
144
176
|
return browser;
|
|
145
177
|
} catch (error) {
|
|
146
178
|
// Check if it's a display-related error in WSL
|
|
@@ -189,6 +221,61 @@ This requires an X server to display the browser GUI.
|
|
|
189
221
|
return await browserPromise;
|
|
190
222
|
}
|
|
191
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Setup tracking for new tabs opened via window.open, target="_blank", etc.
|
|
226
|
+
* @param {Browser} browser - Puppeteer browser instance
|
|
227
|
+
*/
|
|
228
|
+
function setupNewTabTracking(browser) {
|
|
229
|
+
browser.on('targetcreated', async (target) => {
|
|
230
|
+
// Only handle page targets (not service workers, etc.)
|
|
231
|
+
if (target.type() !== 'page') {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Skip if we've already processed this target
|
|
236
|
+
if (knownTargets.has(target)) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
knownTargets.add(target);
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const page = await target.page();
|
|
243
|
+
if (!page) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Check if this page is already tracked (created via getOrCreatePage)
|
|
248
|
+
const currentUrl = page.url();
|
|
249
|
+
for (const [url, trackedPage] of openPages.entries()) {
|
|
250
|
+
if (trackedPage === page) {
|
|
251
|
+
debugLog('Page already tracked, skipping:', url);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Get opener URL if available
|
|
257
|
+
const opener = target.opener();
|
|
258
|
+
let openerUrl = '';
|
|
259
|
+
if (opener) {
|
|
260
|
+
try {
|
|
261
|
+
const openerPage = await opener.page();
|
|
262
|
+
openerUrl = openerPage ? openerPage.url() : '';
|
|
263
|
+
} catch (e) {
|
|
264
|
+
// Opener might not be available
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Handle the new tab
|
|
269
|
+
await handleNewTab(page, openerUrl);
|
|
270
|
+
|
|
271
|
+
} catch (error) {
|
|
272
|
+
debugLog('Error handling new tab:', error.message);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
debugLog('New tab tracking enabled');
|
|
277
|
+
}
|
|
278
|
+
|
|
192
279
|
/**
|
|
193
280
|
* Close browser and cleanup
|
|
194
281
|
* @returns {Promise<void>}
|
|
@@ -203,4 +290,22 @@ export async function closeBrowser() {
|
|
|
203
290
|
}
|
|
204
291
|
browserPromise = null;
|
|
205
292
|
}
|
|
293
|
+
connectedToExistingChrome = false;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Check if we connected to an existing Chrome instance
|
|
298
|
+
* (extension won't be auto-loaded in this case)
|
|
299
|
+
* @returns {boolean}
|
|
300
|
+
*/
|
|
301
|
+
export function isConnectedToExistingChrome() {
|
|
302
|
+
return connectedToExistingChrome;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Get extension path for manual installation
|
|
307
|
+
* @returns {string}
|
|
308
|
+
*/
|
|
309
|
+
export function getExtensionPath() {
|
|
310
|
+
return EXTENSION_PATH;
|
|
206
311
|
}
|
package/browser/page-manager.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { getBrowser } from './browser-manager.js';
|
|
8
|
-
|
|
8
|
+
// Note: injectRecorder removed - now using Chrome Extension for recording
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Debug log helper (only logs to stderr when DEBUG=1)
|
|
@@ -21,6 +21,9 @@ function debugLog(...args) {
|
|
|
21
21
|
export const openPages = new Map();
|
|
22
22
|
export let lastPage = null;
|
|
23
23
|
|
|
24
|
+
// New tab events queue - for notifying AI about new tabs
|
|
25
|
+
export const newTabEvents = [];
|
|
26
|
+
|
|
24
27
|
// Console logs storage
|
|
25
28
|
export const consoleLogs = [];
|
|
26
29
|
|
|
@@ -30,9 +33,6 @@ export const networkRequests = [];
|
|
|
30
33
|
// Page analysis cache
|
|
31
34
|
export const pageAnalysisCache = new Map();
|
|
32
35
|
|
|
33
|
-
// Track pages with recorder injected
|
|
34
|
-
export const pagesWithRecorder = new WeakSet();
|
|
35
|
-
|
|
36
36
|
// Track pages with network monitoring to prevent duplicate setup
|
|
37
37
|
const pagesWithNetworkMonitoring = new WeakSet();
|
|
38
38
|
|
|
@@ -126,60 +126,7 @@ export async function setupNetworkMonitoring(page) {
|
|
|
126
126
|
});
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
* Setup recorder auto-reinjection on navigation
|
|
131
|
-
* @param {Page} page - Puppeteer page instance
|
|
132
|
-
*/
|
|
133
|
-
export async function setupRecorderAutoReinjection(page) {
|
|
134
|
-
let reinjectionTimeout = null;
|
|
135
|
-
let lastUrl = null;
|
|
136
|
-
|
|
137
|
-
// Handle navigation events (form submits, link clicks, history API)
|
|
138
|
-
page.on('framenavigated', async (frame) => {
|
|
139
|
-
// Only handle main frame navigation
|
|
140
|
-
if (frame !== page.mainFrame()) return;
|
|
141
|
-
|
|
142
|
-
// Get current URL
|
|
143
|
-
const currentUrl = frame.url();
|
|
144
|
-
|
|
145
|
-
// Skip if URL hasn't changed (prevents duplicate injections on same page)
|
|
146
|
-
if (currentUrl === lastUrl) {
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
lastUrl = currentUrl;
|
|
150
|
-
|
|
151
|
-
// Clear any pending reinjection
|
|
152
|
-
if (reinjectionTimeout) {
|
|
153
|
-
clearTimeout(reinjectionTimeout);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Debounce reinjection (wait 100ms for navigation to settle)
|
|
157
|
-
reinjectionTimeout = setTimeout(async () => {
|
|
158
|
-
// Check if this page had recorder before
|
|
159
|
-
if (pagesWithRecorder.has(page)) {
|
|
160
|
-
try {
|
|
161
|
-
// Project ID will be determined from URL in browser context
|
|
162
|
-
await injectRecorder(page);
|
|
163
|
-
} catch (error) {
|
|
164
|
-
debugLog('Failed to re-inject recorder:', error.message);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}, 100);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
// Handle page reloads (F5, Ctrl+R) - use 'load' event
|
|
171
|
-
page.on('load', async () => {
|
|
172
|
-
// Check if this page had recorder before
|
|
173
|
-
if (pagesWithRecorder.has(page)) {
|
|
174
|
-
try {
|
|
175
|
-
// Project ID will be determined from URL in browser context
|
|
176
|
-
await injectRecorder(page);
|
|
177
|
-
} catch (error) {
|
|
178
|
-
debugLog('Failed to re-inject recorder after reload:', error.message);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
}
|
|
129
|
+
// Note: setupRecorderAutoReinjection removed - Chrome Extension handles recording now
|
|
183
130
|
|
|
184
131
|
/**
|
|
185
132
|
* Get or create page for URL
|
|
@@ -238,10 +185,11 @@ export async function getOrCreatePage(url) {
|
|
|
238
185
|
// Setup network monitoring with auto-reinitialization on navigation
|
|
239
186
|
await setupNetworkMonitoring(page);
|
|
240
187
|
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
188
|
+
// Use longer timeout (60s) and less strict wait condition for slow sites like Yahoo
|
|
189
|
+
await page.goto(url, {
|
|
190
|
+
waitUntil: 'domcontentloaded', // Less strict than networkidle2, works for sites with continuous loading
|
|
191
|
+
timeout: 60000 // Increased from default 30s to 60s
|
|
192
|
+
});
|
|
245
193
|
openPages.set(url, page);
|
|
246
194
|
lastPage = page;
|
|
247
195
|
|
|
@@ -279,13 +227,6 @@ export async function getLastOpenPage() {
|
|
|
279
227
|
throw new Error('No page is currently open. Use openBrowser first to open a page.');
|
|
280
228
|
}
|
|
281
229
|
|
|
282
|
-
// Setup recorder auto-reinjection if not already set up
|
|
283
|
-
// Check if page already has navigation listener
|
|
284
|
-
const listenerCount = lastPage.listenerCount('framenavigated');
|
|
285
|
-
if (listenerCount === 0) {
|
|
286
|
-
setupRecorderAutoReinjection(lastPage);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
230
|
return lastPage;
|
|
290
231
|
}
|
|
291
232
|
|
|
@@ -296,3 +237,219 @@ export async function getLastOpenPage() {
|
|
|
296
237
|
export function setLastPage(page) {
|
|
297
238
|
lastPage = page;
|
|
298
239
|
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Setup a new page with all monitoring (console, network, recorder)
|
|
243
|
+
* Used for both explicitly created pages and auto-detected new tabs
|
|
244
|
+
* @param {Page} page - Puppeteer page instance
|
|
245
|
+
* @param {string} [source='manual'] - How the page was created ('manual', 'popup', 'newTab')
|
|
246
|
+
* @returns {Promise<void>}
|
|
247
|
+
*/
|
|
248
|
+
export async function setupNewPage(page, source = 'manual') {
|
|
249
|
+
// Set up console log capture
|
|
250
|
+
try {
|
|
251
|
+
const client = await page.target().createCDPSession();
|
|
252
|
+
await client.send('Runtime.enable');
|
|
253
|
+
await client.send('Log.enable');
|
|
254
|
+
|
|
255
|
+
client.on('Runtime.consoleAPICalled', (event) => {
|
|
256
|
+
const timestamp = new Date().toISOString();
|
|
257
|
+
const args = event.args.map(arg => {
|
|
258
|
+
if (arg.value !== undefined) return arg.value;
|
|
259
|
+
if (arg.description) return arg.description;
|
|
260
|
+
return String(arg);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
consoleLogs.push({
|
|
264
|
+
type: event.type,
|
|
265
|
+
timestamp,
|
|
266
|
+
message: args.join(' '),
|
|
267
|
+
stackTrace: event.stackTrace
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
client.on('Log.entryAdded', (event) => {
|
|
272
|
+
const entry = event.entry;
|
|
273
|
+
consoleLogs.push({
|
|
274
|
+
type: entry.level,
|
|
275
|
+
timestamp: new Date(entry.timestamp).toISOString(),
|
|
276
|
+
message: entry.text,
|
|
277
|
+
source: entry.source,
|
|
278
|
+
url: entry.url,
|
|
279
|
+
lineNumber: entry.lineNumber
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
} catch (error) {
|
|
283
|
+
debugLog('Failed to setup console capture for new page:', error.message);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Setup network monitoring
|
|
287
|
+
await setupNetworkMonitoring(page);
|
|
288
|
+
|
|
289
|
+
debugLog(`New page setup complete (source: ${source})`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Handle a newly detected tab (from targetcreated event)
|
|
294
|
+
* @param {Page} page - New Puppeteer page
|
|
295
|
+
* @param {string} openerUrl - URL of the page that opened this tab
|
|
296
|
+
*/
|
|
297
|
+
export async function handleNewTab(page, openerUrl) {
|
|
298
|
+
const timestamp = new Date().toISOString();
|
|
299
|
+
|
|
300
|
+
// Wait for the page to have a URL (initial about:blank -> actual URL)
|
|
301
|
+
let url = page.url();
|
|
302
|
+
let attempts = 0;
|
|
303
|
+
while ((url === 'about:blank' || url === '') && attempts < 20) {
|
|
304
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
305
|
+
url = page.url();
|
|
306
|
+
attempts++;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
debugLog(`New tab detected: ${url} (opened from: ${openerUrl})`);
|
|
310
|
+
|
|
311
|
+
// Setup monitoring on the new page
|
|
312
|
+
await setupNewPage(page, 'newTab');
|
|
313
|
+
|
|
314
|
+
// Register the page
|
|
315
|
+
if (url && url !== 'about:blank') {
|
|
316
|
+
openPages.set(url, page);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Make it the active page
|
|
320
|
+
lastPage = page;
|
|
321
|
+
|
|
322
|
+
// Add event to queue for AI notification
|
|
323
|
+
newTabEvents.push({
|
|
324
|
+
timestamp,
|
|
325
|
+
url,
|
|
326
|
+
openerUrl,
|
|
327
|
+
title: await page.title().catch(() => ''),
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Bring the new tab to front
|
|
331
|
+
try {
|
|
332
|
+
await page.bringToFront();
|
|
333
|
+
} catch (error) {
|
|
334
|
+
debugLog('Failed to bring new tab to front:', error.message);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get and clear new tab events (for AI notification)
|
|
340
|
+
* @returns {Array} - Array of new tab events
|
|
341
|
+
*/
|
|
342
|
+
export function getAndClearNewTabEvents() {
|
|
343
|
+
const events = [...newTabEvents];
|
|
344
|
+
newTabEvents.length = 0;
|
|
345
|
+
return events;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Get all open pages as array with metadata
|
|
350
|
+
* @returns {Promise<Array>} - Array of page info objects
|
|
351
|
+
*/
|
|
352
|
+
export async function getAllPages() {
|
|
353
|
+
const pages = [];
|
|
354
|
+
|
|
355
|
+
for (const [url, page] of openPages.entries()) {
|
|
356
|
+
if (!page.isClosed()) {
|
|
357
|
+
try {
|
|
358
|
+
const title = await page.title().catch(() => '');
|
|
359
|
+
const currentUrl = page.url();
|
|
360
|
+
pages.push({
|
|
361
|
+
url: currentUrl,
|
|
362
|
+
originalUrl: url,
|
|
363
|
+
title,
|
|
364
|
+
isActive: page === lastPage,
|
|
365
|
+
});
|
|
366
|
+
} catch (error) {
|
|
367
|
+
// Page might be in invalid state, skip it
|
|
368
|
+
debugLog('Error getting page info:', error.message);
|
|
369
|
+
}
|
|
370
|
+
} else {
|
|
371
|
+
// Clean up closed pages
|
|
372
|
+
openPages.delete(url);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return pages;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Switch to a page by URL or index
|
|
381
|
+
* @param {string|number} identifier - URL (partial match) or index
|
|
382
|
+
* @returns {Promise<Page>} - The switched-to page
|
|
383
|
+
*/
|
|
384
|
+
export async function switchToPage(identifier) {
|
|
385
|
+
const pages = await getAllPages();
|
|
386
|
+
|
|
387
|
+
if (pages.length === 0) {
|
|
388
|
+
throw new Error('No pages are currently open.');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
let targetPage = null;
|
|
392
|
+
|
|
393
|
+
if (typeof identifier === 'number') {
|
|
394
|
+
// Switch by index
|
|
395
|
+
if (identifier < 0 || identifier >= pages.length) {
|
|
396
|
+
throw new Error(`Invalid tab index: ${identifier}. Valid range: 0-${pages.length - 1}`);
|
|
397
|
+
}
|
|
398
|
+
const targetUrl = pages[identifier].originalUrl;
|
|
399
|
+
targetPage = openPages.get(targetUrl);
|
|
400
|
+
} else {
|
|
401
|
+
// Switch by URL (partial match)
|
|
402
|
+
const matchingPage = pages.find(p =>
|
|
403
|
+
p.url.includes(identifier) || p.originalUrl.includes(identifier)
|
|
404
|
+
);
|
|
405
|
+
if (!matchingPage) {
|
|
406
|
+
throw new Error(`No page found matching: ${identifier}`);
|
|
407
|
+
}
|
|
408
|
+
targetPage = openPages.get(matchingPage.originalUrl);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (!targetPage || targetPage.isClosed()) {
|
|
412
|
+
throw new Error('Target page is not available.');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Make it the active page
|
|
416
|
+
lastPage = targetPage;
|
|
417
|
+
|
|
418
|
+
// Bring to front
|
|
419
|
+
await targetPage.bringToFront();
|
|
420
|
+
|
|
421
|
+
return targetPage;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Connect Puppeteer to a tab by URL (finds existing browser page)
|
|
426
|
+
* Used when switching tabs via extension - need to sync Puppeteer
|
|
427
|
+
* @param {string} url - URL of the tab to connect to
|
|
428
|
+
* @returns {Promise<Page|null>} - The page or null if not found
|
|
429
|
+
*/
|
|
430
|
+
export async function connectToTabByUrl(url) {
|
|
431
|
+
const browser = await getBrowser();
|
|
432
|
+
const allPages = await browser.pages();
|
|
433
|
+
|
|
434
|
+
// Find page matching URL
|
|
435
|
+
for (const page of allPages) {
|
|
436
|
+
try {
|
|
437
|
+
const pageUrl = page.url();
|
|
438
|
+
if (pageUrl === url || pageUrl.includes(url) || url.includes(pageUrl)) {
|
|
439
|
+
// Setup monitoring if not already done
|
|
440
|
+
if (!openPages.has(url)) {
|
|
441
|
+
await setupNewPage(page, 'extension-switch');
|
|
442
|
+
openPages.set(url, page);
|
|
443
|
+
}
|
|
444
|
+
lastPage = page;
|
|
445
|
+
debugLog(`Connected Puppeteer to tab: ${url}`);
|
|
446
|
+
return page;
|
|
447
|
+
}
|
|
448
|
+
} catch (error) {
|
|
449
|
+
debugLog('Error checking page URL:', error.message);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
debugLog(`No browser page found for URL: ${url}`);
|
|
454
|
+
return null;
|
|
455
|
+
}
|