insite-mcp
Version:
MCP server for browser automation with 52 tools using Playwright
1,267 lines (1,266 loc) • 106 kB
JavaScript
/**
* 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());
});