ai-debug-local-mcp
Version:
🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh
286 lines • 9.89 kB
JavaScript
/**
* Manages browser page lifecycle, event handling, and network mocking
* Extracted from LocalDebugEngine for better separation of concerns
*/
export class BrowserManager {
page = null;
mockedResponses = new Map();
consoleHandlers = [];
requestHandlers = [];
responseHandlers = [];
playwrightLoaded = false;
// Store event handler references for cleanup
pageConsoleHandler;
pageRequestHandler;
pageResponseHandler;
pageRequestFailedHandler;
/**
* Attach to a Playwright page and setup event listeners
*/
async attachToPage(page) {
try {
// Validate page and context before attaching
if (!page || page.isClosed()) {
throw new Error('Cannot attach to closed page');
}
// Check if context is accessible (might be closed)
try {
const context = page.context();
// Context exists but may be closing - this is acceptable for now
}
catch (contextError) {
if (contextError.message.includes('Target page, context or browser has been closed')) {
throw new Error('Cannot attach to page - browser context is closed');
}
throw contextError;
}
this.page = page;
// Set reasonable timeouts to prevent AbortSignal accumulation
page.setDefaultTimeout(30000);
page.setDefaultNavigationTimeout(30000);
// Setup event listeners
this.setupEventListeners(page);
// Setup network mocking
await this.setupNetworkMocking(page);
}
catch (error) {
throw new Error(`Failed to attach to page: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Setup event listeners for page events
*/
setupEventListeners(page) {
// Store handler references for later cleanup
this.pageConsoleHandler = (message) => {
this.consoleHandlers.forEach(handler => {
try {
handler(message);
}
catch (error) {
console.error('Error in console handler:', error);
}
});
};
this.pageRequestHandler = (request) => {
this.requestHandlers.forEach(handler => {
try {
handler(request);
}
catch (error) {
console.error('Error in request handler:', error);
}
});
};
this.pageResponseHandler = (response) => {
this.responseHandlers.forEach(handler => {
try {
handler(response);
}
catch (error) {
console.error('Error in response handler:', error);
}
});
};
this.pageRequestFailedHandler = (request) => {
// Log failed requests for debugging
console.debug('Request failed:', request.url());
};
// Add event listeners
page.on('console', this.pageConsoleHandler);
page.on('request', this.pageRequestHandler);
page.on('response', this.pageResponseHandler);
page.on('requestfailed', this.pageRequestFailedHandler);
}
/**
* Setup network request routing for mocking
*/
async setupNetworkMocking(page) {
await page.route('**/*', async (route) => {
try {
const url = route.request().url();
const mockConfig = this.findMockResponse(url);
if (mockConfig) {
// Add delay if specified
if (mockConfig.delay) {
await new Promise(resolve => setTimeout(resolve, mockConfig.delay));
}
// Fulfill with mock response
await route.fulfill({
status: mockConfig.status,
body: typeof mockConfig.body === 'string'
? mockConfig.body
: JSON.stringify(mockConfig.body),
headers: mockConfig.headers || {}
});
}
else {
// Continue with actual request
await route.continue();
}
}
catch (error) {
console.error('Error in network route handler:', error);
// Fallback to continuing the request
try {
await route.continue();
}
catch (continueError) {
console.error('Error continuing route:', continueError);
}
}
});
}
/**
* Find mock response for URL (supports pattern matching)
*/
findMockResponse(url) {
// First try exact match
if (this.mockedResponses.has(url)) {
return this.mockedResponses.get(url);
}
// Then try pattern matching
for (const [pattern, config] of this.mockedResponses.entries()) {
if (this.matchesPattern(url, pattern)) {
return config;
}
}
return undefined;
}
/**
* Check if URL matches pattern (supports wildcards)
*/
matchesPattern(url, pattern) {
if (pattern === url)
return true;
// Convert glob pattern to regex
// Handle wildcards first, then escape other special characters
let regexPattern = pattern
.replace(/\*\*/g, '__DOUBLE_STAR__') // Temporarily replace **
.replace(/\*/g, '__SINGLE_STAR__') // Temporarily replace *
.replace(/\?/g, '__QUESTION__') // Temporarily replace ?
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special regex chars
.replace(/__DOUBLE_STAR__/g, '.*') // ** matches any path (including slashes)
.replace(/__SINGLE_STAR__/g, '[^/]*') // * matches any filename (no slashes)
.replace(/__QUESTION__/g, '.'); // ? matches any single character
const regex = new RegExp(`^${regexPattern}$`);
return regex.test(url);
}
/**
* Add a mock response for network requests
*/
addMockResponse(urlPattern, config) {
this.mockedResponses.set(urlPattern, config);
}
/**
* Remove a mock response
*/
removeMockResponse(urlPattern) {
this.mockedResponses.delete(urlPattern);
}
/**
* Clear all mock responses
*/
clearMockResponses() {
this.mockedResponses.clear();
}
/**
* Check if mock response exists for URL (checks if URL matches any pattern)
*/
hasMockResponse(url) {
// Check exact match first
if (this.mockedResponses.has(url)) {
return true;
}
// Check if this URL matches any existing pattern
for (const pattern of this.mockedResponses.keys()) {
if (this.matchesPattern(url, pattern)) {
return true;
}
}
return false;
}
/**
* Register console message handler
*/
onConsoleMessage(handler) {
this.consoleHandlers.push(handler);
}
/**
* Register request handler
*/
onRequest(handler) {
this.requestHandlers.push(handler);
}
/**
* Register response handler
*/
onResponse(handler) {
this.responseHandlers.push(handler);
}
/**
* Get current page (with validation)
*/
getPage() {
if (this.page && this.page.isClosed()) {
console.warn('⚠️ Page reference exists but page is closed - cleaning up');
this.page = null;
}
return this.page;
}
/**
* Detach from current page and clean up all event listeners
*/
detachPage() {
if (this.page) {
// Remove event listeners to prevent memory leaks
try {
if (this.pageConsoleHandler) {
this.page.off('console', this.pageConsoleHandler);
}
if (this.pageRequestHandler) {
this.page.off('request', this.pageRequestHandler);
}
if (this.pageResponseHandler) {
this.page.off('response', this.pageResponseHandler);
}
if (this.pageRequestFailedHandler) {
this.page.off('requestfailed', this.pageRequestFailedHandler);
}
// Clear network routes to prevent AbortSignal accumulation
this.page.unroute('**/*');
}
catch (error) {
// Ignore errors during cleanup - page might already be closed
console.debug('Error during page detach cleanup:', error);
}
}
// Clear references
this.page = null;
this.pageConsoleHandler = undefined;
this.pageRequestHandler = undefined;
this.pageResponseHandler = undefined;
this.pageRequestFailedHandler = undefined;
}
/**
* Check if page is attached and valid
*/
isPageAttached() {
if (!this.page)
return false;
// Check if page is still valid
if (this.page.isClosed()) {
console.warn('⚠️ Page is closed - cleaning up reference');
this.page = null;
return false;
}
return true;
}
/**
* Get mock response count (for debugging)
*/
getMockResponseCount() {
return this.mockedResponses.size;
}
}
//# sourceMappingURL=browser-manager.js.map