UNPKG

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
/** * 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