UNPKG

insite-mcp

Version:

MCP server for browser automation with 52 tools using Playwright

1,267 lines (1,266 loc) 106 kB
/** * Browser Manager - Singleton class for managing Playwright browser lifecycle */ import { chromium, firefox, webkit } from 'playwright'; import { BrowserAutomationError } from './types.js'; import { withTimeout, validateUrl, validateSelector, handlePlaywrightError } from './utils/error-utils.js'; /** * Singleton class managing browser lifecycle and page operations */ export class BrowserManager { static instance = null; browser = null; context = null; page = null; config; consoleLogs = []; networkLogs = []; pendingRequests = new Map(); constructor(config = {}) { this.config = { headless: true, engine: 'chromium', viewport: { width: 1280, height: 720 }, timeout: 30000, ...config, }; } /** * Get singleton instance of BrowserManager */ static getInstance(config) { if (!BrowserManager.instance) { BrowserManager.instance = new BrowserManager(config); } return BrowserManager.instance; } /** * Initialize browser and create default context/page */ async initialize() { try { if (this.browser) { return; // Already initialized } // Get the browser type based on configuration const browserType = this.getBrowserType(this.config.engine); // Launch browser with engine-specific options this.browser = await browserType.launch({ headless: this.config.headless ?? true, ...this.getBrowserLaunchOptions(this.config.engine), }); this.context = await this.browser.newContext({ ...(this.config.viewport ? { viewport: this.config.viewport } : {}), ...(this.config.userAgent ? { userAgent: this.config.userAgent } : {}), ignoreHTTPSErrors: true, }); this.page = await this.context.newPage(); // Set default timeout this.page.setDefaultTimeout(this.config.timeout); this.page.setDefaultNavigationTimeout(this.config.timeout); // Set up console logging this.page.on('console', (msg) => { this.consoleLogs.push({ timestamp: new Date(), level: msg.type(), text: msg.text(), location: msg.location()?.url }); }); // Set up network logging this.page.on('request', (request) => { const requestId = request.url() + '-' + Date.now(); this.pendingRequests.set(requestId, { request, startTime: Date.now() }); }); this.page.on('response', async (response) => { const pendingRequest = Array.from(this.pendingRequests.entries()) .find(([, data]) => data.request.url() === response.url()); const duration = pendingRequest ? Date.now() - pendingRequest[1].startTime : undefined; try { const responseHeaders = {}; Object.entries(response.headers()).forEach(([key, value]) => { responseHeaders[key] = value; }); const requestHeaders = {}; Object.entries(response.request().headers()).forEach(([key, value]) => { requestHeaders[key] = value; }); const logEntry = { timestamp: new Date(), method: response.request().method(), url: response.url(), status: response.status(), statusText: response.statusText(), requestHeaders, responseHeaders }; if (duration !== undefined) { logEntry.duration = duration; } this.networkLogs.push(logEntry); if (pendingRequest) { this.pendingRequests.delete(pendingRequest[0]); } } catch (error) { // Ignore errors in logging to prevent breaking the main flow console.error('Error logging network response:', error); } }); } catch (error) { throw new BrowserAutomationError('BROWSER_NOT_INITIALIZED', `Failed to initialize browser: ${error instanceof Error ? error.message : 'Unknown error'}`, { originalError: error }); } } /** * Get browser type based on engine configuration */ getBrowserType(engine) { switch (engine) { case 'chromium': return chromium; case 'firefox': return firefox; case 'webkit': return webkit; default: throw new BrowserAutomationError('BROWSER_NOT_SUPPORTED', `Unsupported browser engine: ${engine}`); } } /** * Get browser-specific launch options */ getBrowserLaunchOptions(engine) { const baseOptions = { args: [] }; switch (engine) { case 'chromium': baseOptions.args = [ '--no-sandbox', '--disable-dev-shm-usage', '--disable-gpu', '--disable-web-security', '--disable-features=VizDisplayCompositor', ]; break; case 'firefox': // Firefox-specific launch options baseOptions.args = [ '--no-sandbox', ]; break; case 'webkit': // WebKit-specific launch options baseOptions.args = []; break; } return baseOptions; } /** * Get current page instance */ getPage() { if (!this.page) { throw new BrowserAutomationError('PAGE_NOT_LOADED', 'Browser page not available. Call initialize() first.'); } return this.page; } /** * Get current browser instance */ getBrowser() { if (!this.browser) { throw new BrowserAutomationError('BROWSER_NOT_INITIALIZED', 'Browser not initialized. Call initialize() first.'); } return this.browser; } /** * Get current browser context */ getContext() { if (!this.context) { throw new BrowserAutomationError('BROWSER_NOT_INITIALIZED', 'Browser context not available. Call initialize() first.'); } return this.context; } /** * Navigate to URL with proper error handling */ async navigateToUrl(url, waitUntil = 'domcontentloaded') { validateUrl(url); const page = this.getPage(); try { const response = await withTimeout(page.goto(url, { waitUntil }), this.config.timeout, `Navigation to ${url}`); if (!response) { throw new BrowserAutomationError('NAVIGATION_FAILED', `Failed to navigate to URL: ${url}`, { url, waitUntil }); } if (!response.ok()) { throw new BrowserAutomationError('NETWORK_ERROR', `HTTP ${response.status()}: ${response.statusText()}`, { url, status: response.status(), statusText: response.statusText() }); } } catch (error) { throw handlePlaywrightError(error, `Navigation to ${url}`); } } /** * Take screenshot and save to temp directory */ async takeScreenshot(options) { const page = this.getPage(); try { await withTimeout(page.screenshot({ path: options.filePath, fullPage: options.fullPage ?? true, ...(options.quality ? { quality: options.quality } : {}), type: options.format ?? 'png', }), this.config.timeout, `Take screenshot to ${options.filePath}`); return options.filePath; } catch (error) { throw handlePlaywrightError(error, `Take screenshot to ${options.filePath}`); } } /** * Scroll to an element and take a screenshot of it */ async scrollToElementAndScreenshot(selector, options) { validateSelector(selector); const page = this.getPage(); try { const element = page.locator(selector); // Wait for element to be visible const waitOptions = {}; if (options.timeout !== undefined) { waitOptions.timeout = options.timeout; } else if (this.config.timeout !== undefined) { waitOptions.timeout = this.config.timeout; } await element.waitFor({ state: 'visible', ...waitOptions }); // Scroll element into view await element.scrollIntoViewIfNeeded(); // Wait for any animations to complete await page.waitForTimeout(500); // Take screenshot of the element with optional padding await withTimeout(element.screenshot({ path: options.filePath, ...(options.quality ? { quality: options.quality } : {}), type: options.format ?? 'png', }), this.config.timeout, `Take element screenshot to ${options.filePath}`); return options.filePath; } catch (error) { throw handlePlaywrightError(error, `Scroll to and screenshot element ${selector}`); } } /** * Capture the entire scrollable page height in one long image */ async captureFullScrollablePage(options) { const page = this.getPage(); try { // Get the full page dimensions including scrollable content const dimensions = await page.evaluate(() => { return { scrollWidth: Math.max(document.body.scrollWidth, document.body.offsetWidth, document.documentElement.clientWidth, document.documentElement.scrollWidth, document.documentElement.offsetWidth), scrollHeight: Math.max(document.body.scrollHeight, document.body.offsetHeight, document.documentElement.clientHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight), viewportWidth: window.innerWidth, viewportHeight: window.innerHeight }; }); // Set viewport to capture the full width if needed if (dimensions.scrollWidth > dimensions.viewportWidth) { await page.setViewportSize({ width: Math.min(dimensions.scrollWidth, 1920), // Cap at 1920px width for performance height: dimensions.viewportHeight }); } // Scroll to top to ensure we start from the beginning await page.evaluate(() => { window.scrollTo(0, 0); }); // Wait for any loading or animations await page.waitForTimeout(1000); // Take full page screenshot await withTimeout(page.screenshot({ path: options.filePath, fullPage: true, ...(options.quality ? { quality: options.quality } : {}), type: options.format ?? 'png', }), (options.timeout || this.config.timeout) * 2, // Double timeout for large pages `Capture full scrollable page to ${options.filePath}`); return options.filePath; } catch (error) { throw handlePlaywrightError(error, `Capture full scrollable page to ${options.filePath}`); } } /** * Get current URL */ getCurrentUrl() { const page = this.getPage(); return page.url(); } /** * Get page title */ async getPageTitle() { const page = this.getPage(); try { return await withTimeout(page.title(), this.config.timeout, 'Get page title'); } catch (error) { throw handlePlaywrightError(error, 'Get page title'); } } /** * Get viewport information */ getViewportInfo() { const page = this.getPage(); const viewport = page.viewportSize(); if (!viewport) { throw new BrowserAutomationError('BROWSER_NOT_INITIALIZED', 'Viewport information not available'); } return { width: viewport.width, height: viewport.height, devicePixelRatio: 1, // Playwright doesn't expose this directly }; } /** * Get DOM content */ async getDOMContent(selector) { if (selector) { validateSelector(selector); } const page = this.getPage(); try { if (selector) { const element = await page.locator(selector).first(); return await withTimeout(element.innerHTML(), this.config.timeout, `Get DOM content for selector ${selector}`); } else { return await withTimeout(page.content(), this.config.timeout, 'Get page content'); } } catch (error) { throw handlePlaywrightError(error, `Get DOM content${selector ? ` for ${selector}` : ''}`); } } /** * Click element by selector */ async clickElement(selector, options) { validateSelector(selector); const page = this.getPage(); try { const clickOptions = { force: options?.force ?? false, }; if (options?.timeout !== undefined) { clickOptions.timeout = options.timeout; } else if (this.config.timeout !== undefined) { clickOptions.timeout = this.config.timeout; } await page.locator(selector).click(clickOptions); } catch (error) { throw handlePlaywrightError(error, `Click element ${selector}`); } } /** * Type text into an element */ async typeText(selector, text, options) { validateSelector(selector); const page = this.getPage(); try { const element = page.locator(selector); // Clear existing text if requested if (options?.clear !== false) { const clearOptions = {}; if (options?.timeout !== undefined) { clearOptions.timeout = options.timeout; } else if (this.config.timeout !== undefined) { clearOptions.timeout = this.config.timeout; } await element.clear(clearOptions); } // Type the text with optional delay between keystrokes const typeOptions = { delay: options?.delay ?? 0, }; if (options?.timeout !== undefined) { typeOptions.timeout = options.timeout; } else if (this.config.timeout !== undefined) { typeOptions.timeout = this.config.timeout; } await element.type(text, typeOptions); } catch (error) { throw handlePlaywrightError(error, `Type text "${text}" into ${selector}`); } } /** * Hover over an element */ async hoverElement(selector, options) { validateSelector(selector); const page = this.getPage(); try { const hoverOptions = { force: options?.force ?? false, }; if (options?.timeout !== undefined) { hoverOptions.timeout = options.timeout; } else if (this.config.timeout !== undefined) { hoverOptions.timeout = this.config.timeout; } await page.locator(selector).hover(hoverOptions); } catch (error) { throw handlePlaywrightError(error, `Hover over element ${selector}`); } } /** * Scroll the page or an element */ async scrollPage(options) { const page = this.getPage(); try { const direction = options?.direction ?? 'down'; const amount = options?.amount ?? 500; if (options?.selector) { validateSelector(options.selector); const element = page.locator(options.selector); // Scroll within specific element await element.evaluate((el, { direction, amount }) => { const scrollOptions = { behavior: 'smooth' }; switch (direction) { case 'down': scrollOptions.top = el.scrollTop + amount; break; case 'up': scrollOptions.top = el.scrollTop - amount; break; case 'right': scrollOptions.left = el.scrollLeft + amount; break; case 'left': scrollOptions.left = el.scrollLeft - amount; break; } el.scrollTo(scrollOptions); }, { direction, amount }); } else { // Scroll the entire page await page.evaluate(({ direction, amount }) => { const scrollOptions = { behavior: 'smooth' }; switch (direction) { case 'down': scrollOptions.top = window.scrollY + amount; break; case 'up': scrollOptions.top = window.scrollY - amount; break; case 'right': scrollOptions.left = window.scrollX + amount; break; case 'left': scrollOptions.left = window.scrollX - amount; break; } window.scrollTo(scrollOptions); }, { direction, amount }); } // Wait a bit for smooth scrolling to complete await page.waitForTimeout(200); } catch (error) { throw handlePlaywrightError(error, `Scroll page ${options?.direction ?? 'down'}`); } } /** * Press a keyboard key or key combination */ async pressKey(key, options) { const page = this.getPage(); try { if (options?.selector) { validateSelector(options.selector); // Focus element first, then press key const focusOptions = {}; if (options.timeout !== undefined) { focusOptions.timeout = options.timeout; } else if (this.config.timeout !== undefined) { focusOptions.timeout = this.config.timeout; } await page.locator(options.selector).focus(focusOptions); } await page.keyboard.press(key); } catch (error) { throw handlePlaywrightError(error, `Press key "${key}"${options?.selector ? ` on ${options.selector}` : ''}`); } } /** * Wait for an element to be in a specific state */ async waitForElement(selector, options) { validateSelector(selector); const page = this.getPage(); try { const state = options?.state ?? 'visible'; const timeout = options?.timeout ?? this.config.timeout; await page.locator(selector).waitFor({ state, timeout }); } catch (error) { throw handlePlaywrightError(error, `Wait for element ${selector} to be ${options?.state ?? 'visible'}`); } } /** * Wait for navigation to complete */ async waitForNavigation(options) { const page = this.getPage(); try { const waitUntil = options?.waitUntil ?? 'load'; const timeout = options?.timeout ?? this.config.timeout; if (options?.url) { // Wait for navigation to specific URL await page.waitForURL(options.url, { waitUntil, timeout }); } else { // Wait for any navigation - handle commit state separately if (waitUntil === 'commit') { // For commit state, just wait a short time as it's not supported by waitForLoadState await page.waitForTimeout(1000); } else { await page.waitForLoadState(waitUntil, { timeout }); } } } catch (error) { throw handlePlaywrightError(error, 'Wait for navigation'); } } /** * Navigate back in browser history */ async goBack(options) { const page = this.getPage(); try { const timeout = options?.timeout ?? this.config.timeout; const waitUntil = options?.waitUntil ?? 'load'; await withTimeout(page.goBack({ waitUntil: waitUntil === 'commit' ? 'domcontentloaded' : waitUntil, timeout }), timeout, 'Go back in browser history'); } catch (error) { throw handlePlaywrightError(error, 'Go back in browser history'); } } /** * Navigate forward in browser history */ async goForward(options) { const page = this.getPage(); try { const timeout = options?.timeout ?? this.config.timeout; const waitUntil = options?.waitUntil ?? 'load'; await withTimeout(page.goForward({ waitUntil: waitUntil === 'commit' ? 'domcontentloaded' : waitUntil, timeout }), timeout, 'Go forward in browser history'); } catch (error) { throw handlePlaywrightError(error, 'Go forward in browser history'); } } /** * Reload the current page */ async reloadPage(options) { const page = this.getPage(); try { const timeout = options?.timeout ?? this.config.timeout; const waitUntil = options?.waitUntil ?? 'load'; await withTimeout(page.reload({ waitUntil: waitUntil === 'commit' ? 'domcontentloaded' : waitUntil, timeout }), timeout, 'Reload page'); // If ignoreCache is requested, we can force a hard reload using keyboard shortcut if (options?.ignoreCache) { await page.keyboard.press('Control+F5'); } } catch (error) { throw handlePlaywrightError(error, 'Reload page'); } } /** * Execute JavaScript code in the page context */ async evaluateJavaScript(code, options) { const page = this.getPage(); try { const timeout = options?.timeout ?? this.config.timeout; const result = await withTimeout(page.evaluate(code), timeout, `Execute JavaScript code`); return result; } catch (error) { throw handlePlaywrightError(error, 'Execute JavaScript code'); } } /** * Execute JavaScript code on a specific element */ async evaluateJavaScriptOnElement(selector, code, options) { validateSelector(selector); const page = this.getPage(); try { const timeout = options?.timeout ?? this.config.timeout; // First ensure the element exists const element = page.locator(selector).first(); await element.waitFor({ state: 'attached', timeout }); const result = await withTimeout(element.evaluate((el, jsCode) => { // Create a safe evaluation context with the element available const func = new Function('element', jsCode); return func(el); }, code), timeout, `Execute JavaScript on element ${selector}`); return result; } catch (error) { throw handlePlaywrightError(error, `Execute JavaScript on element ${selector}`); } } /** * Get element information using JavaScript evaluation */ async getElementInfo(selector, options) { validateSelector(selector); const page = this.getPage(); try { const timeout = options?.timeout ?? this.config.timeout; const element = page.locator(selector).first(); await element.waitFor({ state: 'attached', timeout }); const info = await withTimeout(element.evaluate((el) => { // Get comprehensive element information const rect = el.getBoundingClientRect(); const computedStyle = window.getComputedStyle(el); // Extract all attributes const attributes = {}; for (let i = 0; i < el.attributes.length; i++) { const attr = el.attributes[i]; attributes[attr.name] = attr.value; } // Get key computed style properties const styleProps = [ 'display', 'visibility', 'opacity', 'position', 'zIndex', 'width', 'height', 'margin', 'padding', 'border', 'backgroundColor', 'color', 'fontSize', 'fontFamily' ]; const computedStyleObj = {}; styleProps.forEach(prop => { computedStyleObj[prop] = computedStyle.getPropertyValue(prop); }); const result = { tagName: el.tagName.toLowerCase(), attributes, computedStyle: computedStyleObj, boundingRect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height, top: rect.top, left: rect.left, right: rect.right, bottom: rect.bottom, }, visible: rect.width > 0 && rect.height > 0 && computedStyle.visibility !== 'hidden' && computedStyle.display !== 'none', enabled: !el.disabled && computedStyle.pointerEvents !== 'none' }; // Add optional properties only if they exist if (el.id) result.id = el.id; if (el.className) result.className = el.className; if (el.textContent) result.textContent = el.textContent; if (el.innerHTML) result.innerHTML = el.innerHTML; return result; }), timeout, `Get element info for ${selector}`); return info; } catch (error) { throw handlePlaywrightError(error, `Get element info for ${selector}`); } } /** * Get console logs with optional filtering */ getConsoleLogs(options) { let logs = [...this.consoleLogs]; if (options?.level) { logs = logs.filter(log => log.level === options.level); } if (options?.limit) { logs = logs.slice(-options.limit); } return logs; } /** * Clear stored console logs */ clearConsoleLogs() { this.consoleLogs = []; } /** * Get network logs with optional filtering */ getNetworkLogs(options) { let logs = [...this.networkLogs]; if (options?.method) { logs = logs.filter(log => log.method === options.method); } if (options?.status) { logs = logs.filter(log => log.status === options.status); } if (options?.urlPattern) { const regex = new RegExp(options.urlPattern); logs = logs.filter(log => regex.test(log.url)); } if (options?.limit) { logs = logs.slice(-options.limit); } return logs; } /** * Clear stored network logs */ clearNetworkLogs() { this.networkLogs = []; this.pendingRequests.clear(); } /** * Set viewport size */ async setViewportSize(width, height) { const page = this.getPage(); try { await page.setViewportSize({ width, height }); // Update internal config this.config.viewport = { width, height }; } catch (error) { throw handlePlaywrightError(error, `Set viewport size to ${width}x${height}`); } } /** * Set user agent string */ async setUserAgent(userAgent) { const browser = this.browser; if (!browser) { throw new BrowserAutomationError('BROWSER_NOT_INITIALIZED', 'Browser not available. Call initialize() first.'); } try { // To change user agent, we need to create a new context // First close the current context and page if (this.page) { await this.page.close(); this.page = null; } if (this.context) { await this.context.close(); this.context = null; } // Create new context with new user agent this.context = await browser.newContext({ ...(this.config.viewport ? { viewport: this.config.viewport } : {}), userAgent: userAgent, ignoreHTTPSErrors: true, }); this.page = await this.context.newPage(); // Set default timeouts this.page.setDefaultTimeout(this.config.timeout); this.page.setDefaultNavigationTimeout(this.config.timeout); // Re-setup console and network logging this.page.on('console', (msg) => { this.consoleLogs.push({ timestamp: new Date(), level: msg.type(), text: msg.text(), location: msg.location()?.url }); }); this.page.on('request', (request) => { const requestId = request.url() + '-' + Date.now(); this.pendingRequests.set(requestId, { request, startTime: Date.now() }); }); this.page.on('response', async (response) => { const pendingRequest = Array.from(this.pendingRequests.entries()) .find(([, data]) => data.request.url() === response.url()); const duration = pendingRequest ? Date.now() - pendingRequest[1].startTime : undefined; try { const responseHeaders = {}; Object.entries(response.headers()).forEach(([key, value]) => { responseHeaders[key] = value; }); const requestHeaders = {}; Object.entries(response.request().headers()).forEach(([key, value]) => { requestHeaders[key] = value; }); const logEntry = { timestamp: new Date(), method: response.request().method(), url: response.url(), status: response.status(), statusText: response.statusText(), requestHeaders, responseHeaders }; if (duration !== undefined) { logEntry.duration = duration; } this.networkLogs.push(logEntry); if (pendingRequest) { this.pendingRequests.delete(pendingRequest[0]); } } catch (error) { // Ignore errors in logging to prevent breaking the main flow console.error('Error logging network response:', error); } }); // Update internal config this.config.userAgent = userAgent; } catch (error) { throw handlePlaywrightError(error, `Set user agent to ${userAgent}`); } } /** * Set geolocation */ async setGeolocation(latitude, longitude, accuracy = 100) { const context = this.context; if (!context) { throw new BrowserAutomationError('BROWSER_NOT_INITIALIZED', 'Browser context not available. Call initialize() first.'); } try { await context.setGeolocation({ latitude, longitude, accuracy }); } catch (error) { throw handlePlaywrightError(error, `Set geolocation to ${latitude}, ${longitude}`); } } /** * Close browser and cleanup resources */ async close() { try { if (this.page) { await this.page.close(); this.page = null; } if (this.context) { await this.context.close(); this.context = null; } if (this.browser) { await this.browser.close(); this.browser = null; } } catch (error) { // Log error but don't throw to ensure cleanup continues console.error('Error during browser cleanup:', error); } finally { BrowserManager.instance = null; } } /** * Switch to a different browser engine */ async switchBrowserEngine(engine) { if (this.config.engine === engine && this.isInitialized()) { return; // Already using the requested engine } // Close current browser if initialized if (this.isInitialized()) { await this.close(); } // Update configuration this.config.engine = engine; // Reinitialize with new engine await this.initialize(); } /** * Get current browser information */ getBrowserInfo() { const version = this.browser?.version(); return { engine: this.config.engine, ...(version ? { version } : {}), isInitialized: this.isInitialized(), }; } /** * Get list of available browser engines */ static getAvailableEngines() { return ['chromium', 'firefox', 'webkit']; } /** * Check if browser is initialized and ready */ isInitialized() { return this.browser !== null && this.context !== null && this.page !== null; } // ====== ADVANCED DEBUGGING METHODS ====== /** * Highlight elements on the page for debugging */ async highlightElement(selector, options) { const page = this.getPage(); validateSelector(selector); try { const elements = page.locator(selector); const count = await elements.count(); if (count === 0) { throw new BrowserAutomationError('ELEMENT_NOT_FOUND', `No elements found matching selector: ${selector}`); } const style = { border: options?.style?.border || '3px solid #ff0000', backgroundColor: options?.style?.backgroundColor || 'rgba(255, 0, 0, 0.1)', outline: options?.style?.outline || 'none' }; const duration = options?.duration ?? 3000; const showInfo = options?.showInfo ?? true; // Apply highlighting and collect element info const elementInfo = await page.evaluate(({ selector, style, showInfo }) => { const elements = document.querySelectorAll(selector); const info = []; elements.forEach((element, index) => { const htmlElement = element; // Store original styles const originalStyles = { border: htmlElement.style.border, backgroundColor: htmlElement.style.backgroundColor, outline: htmlElement.style.outline, position: htmlElement.style.position, zIndex: htmlElement.style.zIndex }; // Apply highlight styles htmlElement.style.border = style.border; htmlElement.style.backgroundColor = style.backgroundColor; htmlElement.style.outline = style.outline; htmlElement.style.position = 'relative'; htmlElement.style.zIndex = '9999'; // Store reference for cleanup htmlElement.__debugHighlightOriginalStyles = originalStyles; if (showInfo) { const rect = htmlElement.getBoundingClientRect(); const elementInfo = { tagName: htmlElement.tagName.toLowerCase(), classes: Array.from(htmlElement.classList), bounds: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } }; if (htmlElement.id) { elementInfo.id = htmlElement.id; } if (htmlElement.textContent) { elementInfo.text = htmlElement.textContent.slice(0, 100); } info.push(elementInfo); // Add info tooltip const tooltip = document.createElement('div'); tooltip.style.cssText = ` position: absolute; top: -30px; left: 0; background: rgba(0, 0, 0, 0.8); color: white; padding: 4px 8px; font-size: 12px; font-family: monospace; border-radius: 4px; z-index: 10000; pointer-events: none; white-space: nowrap; `; tooltip.textContent = `${htmlElement.tagName.toLowerCase()}${htmlElement.id ? '#' + htmlElement.id : ''}${htmlElement.className ? '.' + htmlElement.className.replace(/\s+/g, '.') : ''} (${index + 1}/${elements.length})`; htmlElement.appendChild(tooltip); htmlElement.__debugHighlightTooltip = tooltip; } }); return info; }, { selector, style, showInfo }); // Set cleanup timer if duration > 0 if (duration > 0) { setTimeout(async () => { try { await page.evaluate(({ selector }) => { const elements = document.querySelectorAll(selector); elements.forEach(element => { const htmlElement = element; const originalStyles = htmlElement.__debugHighlightOriginalStyles; const tooltip = htmlElement.__debugHighlightTooltip; if (originalStyles) { // Restore original styles Object.assign(htmlElement.style, originalStyles); delete htmlElement.__debugHighlightOriginalStyles; } if (tooltip) { tooltip.remove(); delete htmlElement.__debugHighlightTooltip; } }); }, { selector }); } catch (error) { // Ignore cleanup errors } }, duration); } return { highlightedCount: count, ...(showInfo ? { elementInfo } : {}) }; } catch (error) { throw handlePlaywrightError(error, `Highlight element ${selector}`); } } /** * Private property to track debug state */ debugState = { enabled: false, level: 'info', features: { consoleLogging: false, networkLogging: false, performanceMonitoring: false, elementInspection: false, errorTracking: false } }; /** * Enable/disable debug mode with enhanced logging */ async enableDebugMode(options) { this.debugState.enabled = options.enabled; if (options.level) { this.debugState.level = options.level; } if (options.features) { Object.assign(this.debugState.features, options.features); } const enabledFeatures = []; if (options.enabled) { // Enable various debug features if (this.debugState.features.consoleLogging) { enabledFeatures.push('Enhanced Console Logging'); } if (this.debugState.features.networkLogging) { enabledFeatures.push('Detailed Network Logging'); } if (this.debugState.features.performanceMonitoring) { enabledFeatures.push('Performance Monitoring'); } if (this.debugState.features.elementInspection) { enabledFeatures.push('Enhanced Element Inspection'); } if (this.debugState.features.errorTracking) { enabledFeatures.push('Advanced Error Tracking'); } } return { status: options.enabled ? 'Debug mode enabled' : 'Debug mode disabled', features: enabledFeatures }; } /** * Enable execution tracing for debugging */ async enableTracing(options) { const context = this.context; try { const enabled = options?.enabled ?? true; if (enabled) { const tracePath = options?.path || `temp/trace-${Date.now()}.zip`; await context.tracing.start({ screenshots: options?.screenshots ?? true, snapshots: options?.snapshots ?? true, sources: options?.sources ?? false, }); this.debugState.tracing = { enabled: true, path: tracePath }; return { status: 'Tracing enabled', tracePath }; } else { if (this.debugState.tracing?.enabled) { const tracePath = this.debugState.tracing.path || `temp/trace-${Date.now()}.zip`; await context.tracing.stop({ path: tracePath }); this.debugState.tracing.enabled = false; return { status: 'Tracing disabled', tracePath }; } else { return { status: 'Tracing was not enabled' }; } } } catch (error) { throw handlePlaywrightError(error, 'Enable/disable tracing'); } } /** * Capture performance timeline data */ async capturePerformanceTimeline(options) { const page = this.getPage(); try { const categories = options?.categories || ['navigation', 'resource', 'paint', 'layout']; const includeMetrics = options?.includeMetrics ?? true; const format = options?.format || 'json'; const duration = options?.duration || 0; // If duration > 0, wait for that duration to capture ongoing performance if (duration > 0) { await page.waitForTimeout(duration); } const performanceData = await page.evaluate(({ categories, includeMetrics }) => { const performance = window.performance; const data = {}; // Capture performance entries by category categories.forEach(category => { const entries = performance.getEntriesByType(category); data[category] = entries.map(entry => ({ name: entry.name, entryType: entry.entryType, startTime: entry.startTime, duration: entry.duration, ...entry })); }); // Capture Web Vitals and other metrics if requested let metrics = {}; if (includeMetrics) { const navigation = performance.getEntriesByType('navigation')[0]; if (navigation) { metrics = { domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart, loadComplete: navigation.loadEventEnd - navigation.loadEventStart, timeToFirstByte: navigation.responseStart - navigation.requestStart, domInteractive: navigation.domInteractive - navigation.fetchStart, ...(navigation.transferSize ? { transferSize: navigation.transferSize } : {}), ...(navigation.encodedBodySize ? { encodedBodySize: navigation.encodedBodySize } : {}), ...(navigation.decodedBodySize ? { decodedBodySize: navigation.decodedBodySize } : {}) }; } // Try to get paint metrics const paintEntries = performance.getEntriesByType('paint'); paintEntries.forEach(entry => { metrics[entry.name.replace('-', '')] = entry.startTime; }); // Memory information if available if (performance.memory) { const memory = performance.memory; metrics['usedJSHeapSize'] = memory['usedJSHeapSize']; metrics['totalJSHeapSize'] = memory['totalJSHeapSize']; metrics['jsHeapSizeLimit'] = memory['jsHeapSizeLimit']; } } return { data, metrics }; }, { categories, includeMetrics }); // Format the data based on requested format let formattedData = performanceData.data; if (format === 'summary') { formattedData = { totalEntries: Object.values(performanceData.data).reduce((sum, entries) => sum + entries.length, 0), categoryCounts: Object.fromEntries(Object.entries(performanceData.data).map(([key, entries]) => [key, entries.length])), categories: Object.keys(performanceData.data) }; } return { format, data: formattedData, ...(includeMetrics ? { metrics: performanceData.metrics } : {}), timestamp: new Date() }; } catch (error) { throw handlePlaywrightError(error, 'Capture performance timeline'); } } // ====== SECURITY & VALIDATION METHODS ====== /** * Handle Content Security Policy configuration and checking */ async handleCSP(options) { const page = this.getPage(); try { const { action, policies, bypassUnsafe, violationCallback } = options; if (action === 'bypass' && bypassUnsafe) { // Inject script to bypass CSP (use with extreme caution) await page.addInitScript(() => { // Remove CSP meta tags document.addEventListener('DOMContentLoaded', () => { const cspMetas = document.querySelectorAll('meta[http-equiv*="Content-Security-Policy" i]'); cspMetas.forEach(meta => meta.remove()); });