diff --git a/agent-server/nodejs/src/api-server.js b/agent-server/nodejs/src/api-server.js index 9a837bbd26..e8d6e57f37 100644 --- a/agent-server/nodejs/src/api-server.js +++ b/agent-server/nodejs/src/api-server.js @@ -155,6 +155,14 @@ class APIServer { result = await this.executeJavaScript(JSON.parse(body)); break; + case '/page/dom-snapshot': + if (method !== 'POST') { + this.sendError(res, 405, 'Method not allowed'); + return; + } + result = await this.getDOMSnapshot(JSON.parse(body)); + break; + default: this.sendError(res, 404, 'Not found'); return; @@ -324,6 +332,49 @@ class APIServer { return response; } + async getDOMSnapshot(payload) { + const { + clientId, + tabId, + computedStyles = [], + includeDOMRects = true, + includePaintOrder = false + } = payload; + + if (!clientId) { + throw new Error('Client ID is required'); + } + + if (!tabId) { + throw new Error('Tab ID is required'); + } + + const baseClientId = clientId.split(':')[0]; + + logger.info('Capturing DOM snapshot', { + baseClientId, + tabId, + computedStyleCount: computedStyles.length, + includeDOMRects, + includePaintOrder + }); + + // Call the captureDOMSnapshot method from BrowserAgentServer + const result = await this.browserAgentServer.captureDOMSnapshot(tabId, { + computedStyles, + includeDOMRects, + includePaintOrder + }); + + return { + clientId: baseClientId, + tabId: result.tabId, + snapshot: result.snapshot, // Contains { documents, strings } + format: 'dom-snapshot', + timestamp: Date.now() + }; + } + async getScreenshot(payload) { const { clientId, tabId, fullPage = false } = payload; diff --git a/agent-server/nodejs/src/lib/BrowserAgentServer.js b/agent-server/nodejs/src/lib/BrowserAgentServer.js index a9e181c67b..a395879516 100644 --- a/agent-server/nodejs/src/lib/BrowserAgentServer.js +++ b/agent-server/nodejs/src/lib/BrowserAgentServer.js @@ -1306,6 +1306,69 @@ export class BrowserAgentServer extends EventEmitter { } } + /** + * Capture DOM snapshot using CDP DOMSnapshot.captureSnapshot + * @param {string} tabId - Tab ID (target ID) + * @param {Object} options - Snapshot options + * @param {string[]} options.computedStyles - Array of computed style properties to capture (default: []) + * @param {boolean} options.includeDOMRects - Whether to include bounding boxes (default: true) + * @param {boolean} options.includePaintOrder - Whether to include paint order (default: false) + * @returns {Promise} Result with DOM snapshot data + */ + async captureDOMSnapshot(tabId, options = {}) { + const { + computedStyles = [], + includeDOMRects = true, + includePaintOrder = false + } = options; + + try { + logger.info('Capturing DOM snapshot via CDP', { + tabId, + computedStyleCount: computedStyles.length, + includeDOMRects, + includePaintOrder + }); + + // Use DOMSnapshot.captureSnapshot CDP command + const result = await this.sendCDPCommandToTarget(tabId, 'DOMSnapshot.captureSnapshot', { + computedStyles, + includeDOMRects, + includePaintOrder + }); + + // Validate response structure + if (!result.documents || !result.strings) { + throw new Error('Invalid DOMSnapshot response: missing documents or strings array'); + } + + logger.info('DOM snapshot captured successfully', { + tabId, + documentCount: result.documents.length, + stringCount: result.strings.length, + totalNodes: result.documents.reduce((sum, doc) => sum + (doc.nodes?.nodeType?.length || 0), 0) + }); + + return { + tabId, + snapshot: result // Contains documents[] and strings[] + }; + } catch (error) { + logger.error('Failed to capture DOM snapshot via CDP', { + tabId, + error: error.message, + stack: error.stack + }); + + // Check if DOMSnapshot domain is available + if (error.message && error.message.includes('was not found')) { + throw new Error('DOMSnapshot domain not available. Requires Chrome 74+ with CDP enabled.'); + } + + throw error; + } + } + /** * Capture page screenshot using CDP * @param {string} tabId - Tab ID (target ID)