UNPKG

super-pancake-automation

Version:

A lightweight DOM-based UI automation framework using Chrome DevTools Protocol

1,306 lines (1,182 loc) 43.3 kB
import { config } from '../config.js'; import path from 'path'; import fs from 'fs'; import { executeSecureFunction, resolveNodeSecurely } from './secure-execution.js'; import { cachedQuerySelector, invalidateCacheForSelector } from './query-cache.js'; import { SuperPancakeError, ElementNotFoundError, TimeoutError, ValidationError, validateSession, validateSelector, validateTimeout, validateText, withRetry } from './errors.js'; // Enable required domains export async function enableDOM(session) { try { await session.send('Page.enable'); await session.send('DOM.enable'); await session.send('Runtime.enable'); } catch (error) { throw new Error(`❌ Failed to enable DOM: ${error.message}`); } } // Navigation export async function navigateTo(session, url) { try { await session.send('Page.navigate', { url }); // Wait for Page.loadEventFired event by polling while (true) { const { result } = await session.send('Runtime.evaluate', { expression: 'document.readyState', returnByValue: true }); if (result.value === 'complete') {break;} await waitForTimeout(config.pollInterval || 100); } } catch (error) { throw new Error(`❌ Failed to navigate to ${url}: ${error.message}`); } } // Query selectors export async function querySelector(session, selector, useCache = true) { validateSession(session); validateSelector(selector); try { const foundId = await cachedQuerySelector(session, selector, useCache); if (!foundId) { throw new ElementNotFoundError(selector); } return foundId; } catch (error) { if (error.name === 'ElementNotFoundError') { throw error; } throw new SuperPancakeError( `Failed to query selector "${selector}": ${error.message}`, 'QUERY_SELECTOR_FAILED', { selector } ); } } export async function querySelectorAll(session, selector) { validateSession(session); validateSelector(selector); try { const { root: { nodeId } } = await session.send('DOM.getDocument'); const { nodeIds } = await session.send('DOM.querySelectorAll', { nodeId, selector }); return nodeIds || []; } catch (error) { throw new SuperPancakeError( `Failed to query all selectors "${selector}": ${error.message}`, 'QUERY_SELECTOR_ALL_FAILED', { selector } ); } } // Basic actions export async function click(session, selectorOrNodeId) { validateSession(session); try { const nodeId = typeof selectorOrNodeId === 'string' ? await querySelector(session, selectorOrNodeId) : selectorOrNodeId; await executeSecureFunction(session, nodeId, 'click'); } catch (error) { throw new SuperPancakeError( `Failed to click on "${selectorOrNodeId}": ${error.message}`, 'CLICK_FAILED', { selector: selectorOrNodeId } ); } } export async function type(session, selector, text) { validateSession(session); validateSelector(selector); validateText(text); try { const nodeId = await querySelector(session, selector); await executeSecureFunction(session, nodeId, 'setValue', [text]); // Invalidate cache after DOM modification invalidateCacheForSelector(session, selector); } catch (error) { throw new SuperPancakeError( `Failed to type into "${selector}": ${error.message}`, 'TYPE_FAILED', { selector, text } ); } } export async function clearInput(session, selector) { await type(session, selector, ''); } export async function getText(session, nodeId) { validateSession(session); try { const { result } = await executeSecureFunction(session, nodeId, 'getText'); return result.value; } catch (error) { throw new SuperPancakeError( `Failed to get text from nodeId "${nodeId}": ${error.message}`, 'GET_TEXT_FAILED', { nodeId } ); } } export async function getAttribute(session, selector, attrName) { validateSession(session); validateSelector(selector); validateText(attrName, 'attrName'); try { const nodeId = await querySelector(session, selector); const { result } = await executeSecureFunction(session, nodeId, 'getAttribute', [attrName]); return result.value; } catch (error) { throw new SuperPancakeError( `Failed to get attribute "${attrName}" from "${selector}": ${error.message}`, 'GET_ATTRIBUTE_FAILED', { selector, attrName } ); } } export async function setAttribute(session, selector, attrName, value) { validateSession(session); validateSelector(selector); validateText(attrName, 'attrName'); validateText(value, 'value'); try { const nodeId = await querySelector(session, selector); await executeSecureFunction(session, nodeId, 'setAttribute', [attrName, value]); } catch (error) { throw new SuperPancakeError( `Failed to set attribute "${attrName}" on "${selector}": ${error.message}`, 'SET_ATTRIBUTE_FAILED', { selector, attrName, value } ); } } export async function isVisible(session, selector) { validateSession(session); validateSelector(selector); try { const nodeId = await querySelector(session, selector); const { result } = await executeSecureFunction(session, nodeId, 'isVisible'); return result.value; } catch (error) { throw new SuperPancakeError( `Failed to check visibility of "${selector}": ${error.message}`, 'VISIBILITY_CHECK_FAILED', { selector } ); } } export async function getValue(session, selector) { validateSession(session); validateSelector(selector); try { const nodeId = await querySelector(session, selector); const { result } = await session.send('Runtime.callFunctionOn', { objectId: (await resolveNodeSecurely(session, nodeId)).object.objectId, functionDeclaration: 'function() { return this.value; }', returnByValue: true }); return result.value; } catch (error) { throw new SuperPancakeError( `Failed to get value from "${selector}": ${error.message}`, 'GET_VALUE_FAILED', { selector } ); } } export async function hover(session, selector) { const nodeId = await querySelector(session, selector); await session.send('Overlay.highlightNode', { nodeId }); } export async function reload(session) { await session.send('Page.reload'); await waitForTimeout(config.timeouts.reload || 1000); } export async function goBack(session) { await session.send('Runtime.evaluate', { expression: 'window.history.back()', returnByValue: true }); } export async function goForward(session) { await session.send('Runtime.evaluate', { expression: 'window.history.forward()', returnByValue: true }); } export async function paste(session, selector, text) { validateSession(session); validateSelector(selector); validateText(text); try { // Get current value and append const currentValue = await getValue(session, selector); const newValue = currentValue + text; const nodeId = await querySelector(session, selector); await executeSecureFunction(session, nodeId, 'setValue', [newValue]); } catch (error) { throw new SuperPancakeError( `Failed to paste text into "${selector}": ${error.message}`, 'PASTE_FAILED', { selector, text } ); } } export async function rightClick(session, selector) { const nodeId = await querySelector(session, selector); const { x, y, width, height } = await getBoundingBox(session, nodeId); const cx = x + width / 2; const cy = y + height / 2; await session.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: cx, y: cy, button: 'right', clickCount: 1 }); await session.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: cx, y: cy, button: 'right', clickCount: 1 }); } export async function scrollIntoView(session, selector) { validateSession(session); validateSelector(selector); try { const nodeId = await querySelector(session, selector); await executeSecureFunction(session, nodeId, 'scrollIntoView'); } catch (error) { throw new SuperPancakeError( `Failed to scroll into view for "${selector}": ${error.message}`, 'SCROLL_INTO_VIEW_FAILED', { selector } ); } } export async function isEnabled(session, selector) { validateSession(session); validateSelector(selector); try { const nodeId = await querySelector(session, selector); const { result } = await executeSecureFunction(session, nodeId, 'isEnabled'); return result.value; } catch (error) { throw new SuperPancakeError( `Failed to check if element is enabled "${selector}": ${error.message}`, 'ENABLED_CHECK_FAILED', { selector } ); } } export async function takeScreenshot(session, fileName = 'screenshot.png') { const { data } = await session.send('Page.captureScreenshot', { format: 'png', fromSurface: true }); fs.writeFileSync(fileName, Buffer.from(data, 'base64')); } export async function takeElementScreenshot(session, selector, fileName = 'element.png') { const nodeId = await querySelector(session, selector); const { model } = await session.send('DOM.getBoxModel', { nodeId }); // If element has 0 width or height, log a warning and capture full page screenshot as fallback if (model.width === 0 || model.height === 0) { console.warn(`⚠️ Element has zero width or height for selector: ${selector}. Capturing full page screenshot as fallback.`); const fallbackFileName = fileName.replace(/(\.png)?$/, `_${selector.replace(/[^\w-]/g, '_')}.png`); const { data } = await session.send('Page.captureScreenshot', { format: 'png', fromSurface: true }); const dir = path.dirname(fallbackFileName); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(fallbackFileName, Buffer.from(data, 'base64')); // Return false to indicate fallback screenshot was taken return { fileName: fallbackFileName, isElementScreenshot: false }; } let data; try { const result = await session.send('Page.captureScreenshot', { format: 'png', clip: { x: model.content[0], y: model.content[1], width: model.width, height: model.height, scale: 1 } }); if (!result || !result.data) { throw new Error('❌ Screenshot capture failed. No data returned.'); } data = result.data; } catch (err) { throw new Error(`❌ Screenshot capture failed: ${err.message}`); } const dir = path.dirname(fileName); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(fileName, Buffer.from(data, 'base64')); // Return true to indicate element screenshot was taken return { fileName, isElementScreenshot: true }; } // Utilities export async function waitForTimeout(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } export async function waitForSelector(session, selector, timeout = config.timeouts.waitForSelector) { validateSession(session); validateSelector(selector); validateTimeout(timeout); const start = Date.now(); while (Date.now() - start < timeout) { try { const nodeId = await querySelector(session, selector); if (nodeId) {return nodeId;} } catch {} await waitForTimeout(config.pollInterval || 100); } throw new TimeoutError('waitForSelector', timeout, { selector }); } async function getBoundingBox(session, nodeId) { const { model } = await session.send('DOM.getBoxModel', { nodeId }); const [x, y] = model.content; return { x, y, width: model.width, height: model.height }; } async function resolveNode(session, nodeId) { const resolved = await session.send('DOM.resolveNode', { nodeId }); if (!resolved || !resolved.object) {throw new Error(`❌ Failed to resolve node: ${nodeId}`);} return resolved; } // Form helpers export async function fillInput(session, selector, value) { validateSession(session); validateSelector(selector); validateText(value, 'value'); try { const nodeId = await querySelector(session, selector); await executeSecureFunction(session, nodeId, 'setValue', [value]); } catch (error) { throw new SuperPancakeError( `Failed to fill input "${selector}": ${error.message}`, 'FILL_INPUT_FAILED', { selector, value } ); } } export async function check(session, selector, checked = true) { validateSession(session); validateSelector(selector); try { const nodeId = await querySelector(session, selector); await executeSecureFunction(session, nodeId, 'setChecked', [checked]); } catch (error) { throw new SuperPancakeError( `Failed to set checked state on "${selector}": ${error.message}`, 'CHECK_FAILED', { selector, checked } ); } } export async function selectOption(session, selector, values) { validateSession(session); validateSelector(selector); if (!Array.isArray(values)) {values = [values];} try { const nodeId = await querySelector(session, selector); await executeSecureFunction(session, nodeId, 'selectOption', [values]); } catch (error) { throw new SuperPancakeError( `Failed to select option on "${selector}": ${error.message}`, 'SELECT_OPTION_FAILED', { selector, values } ); } } export async function triggerClick(session, selector) { const { object } = await resolveNode(session, await querySelector(session, selector)); await session.send('Runtime.callFunctionOn', { objectId: object.objectId, functionDeclaration: 'function() { this.click(); }' }); } export async function pressKey(session, selector, key) { validateSession(session); validateSelector(selector); validateText(key, 'key'); try { const nodeId = await querySelector(session, selector); await executeSecureFunction(session, nodeId, 'pressKey', [key]); } catch (error) { throw new SuperPancakeError( `Failed to press key "${key}" on "${selector}": ${error.message}`, 'PRESS_KEY_FAILED', { selector, key } ); } } export async function focus(session, selector) { validateSession(session); validateSelector(selector); try { const nodeId = await querySelector(session, selector); await executeSecureFunction(session, nodeId, 'focus'); } catch (error) { throw new SuperPancakeError( `Failed to focus on "${selector}": ${error.message}`, 'FOCUS_FAILED', { selector } ); } } export async function dragDrop(session, sourceSelector, targetSelector) { try { const sourceId = await querySelector(session, sourceSelector); const targetId = await querySelector(session, targetSelector); const sourceObj = (await session.send('DOM.resolveNode', { nodeId: sourceId })).object; const targetObj = (await session.send('DOM.resolveNode', { nodeId: targetId })).object; await session.send('Runtime.callFunctionOn', { objectId: sourceObj.objectId, functionDeclaration: `function() { const event = new DragEvent('dragstart', { bubbles: true }); this.dispatchEvent(event); }` }); await session.send('Runtime.callFunctionOn', { objectId: targetObj.objectId, functionDeclaration: `function() { const event = new DragEvent('drop', { bubbles: true }); this.dispatchEvent(event); }` }); } catch (error) { throw new Error(`❌ Failed to dragDrop from "${sourceSelector}" to "${targetSelector}": ${error.message}`); } } export async function uploadFileBuffer(session, selector, filename, content) { const { object } = await resolveNode(session, await querySelector(session, selector)); await session.send('Runtime.callFunctionOn', { objectId: object.objectId, functionDeclaration: `function() { const file = new File([new Blob([${JSON.stringify(content)}])], '${filename}', { type: 'text/plain' }); const dataTransfer = new DataTransfer(); dataTransfer.items.add(file); this.files = dataTransfer.files; this.dispatchEvent(new Event('change', { bubbles: true })); }` }); } // ======================================== // ENHANCED WAIT STRATEGIES // ======================================== export async function waitForElementToBeVisible(session, selector, timeout = config.timeouts?.waitForSelector || 5000) { const start = Date.now(); while (Date.now() - start < timeout) { try { const nodeId = await querySelector(session, selector); if (nodeId && await isVisible(session, nodeId)) { return nodeId; } } catch {} await waitForTimeout(config.pollInterval || 100); } throw new Error(`❌ Element "${selector}" not visible in ${timeout}ms`); } export async function waitForElementToBeHidden(session, selector, timeout = config.timeouts?.waitForSelector || 5000) { const start = Date.now(); while (Date.now() - start < timeout) { try { const nodeId = await querySelector(session, selector); if (!nodeId || !await isVisible(session, nodeId)) { return true; } } catch { return true; // Element not found = hidden } await waitForTimeout(config.pollInterval || 100); } throw new Error(`❌ Element "${selector}" still visible after ${timeout}ms`); } export async function waitForElementToBeClickable(session, selector, timeout = config.timeouts?.waitForSelector || 5000) { const start = Date.now(); while (Date.now() - start < timeout) { try { const nodeId = await querySelector(session, selector); if (nodeId && await isVisible(session, nodeId) && await isEnabled(session, nodeId)) { return nodeId; } } catch {} await waitForTimeout(config.pollInterval || 100); } throw new Error(`❌ Element "${selector}" not clickable in ${timeout}ms`); } export async function waitForElementToContainText(session, selector, text, timeout = config.timeouts?.waitForSelector || 5000) { const start = Date.now(); while (Date.now() - start < timeout) { try { const nodeId = await querySelector(session, selector); if (nodeId) { const elementText = await getText(session, nodeId); if (elementText && elementText.includes(text)) { return nodeId; } } } catch {} await waitForTimeout(config.pollInterval || 100); } throw new Error(`❌ Element "${selector}" does not contain text "${text}" in ${timeout}ms`); } export async function waitForElementToHaveAttribute(session, selector, attribute, value, timeout = config.timeouts?.waitForSelector || 5000) { const start = Date.now(); while (Date.now() - start < timeout) { try { const attrValue = await getAttribute(session, selector, attribute); if (attrValue === value) { return await querySelector(session, selector); } } catch {} await waitForTimeout(config.pollInterval || 100); } throw new Error(`❌ Element "${selector}" does not have attribute "${attribute}" = "${value}" in ${timeout}ms`); } export async function waitForElementToBeEnabled(session, selector, timeout = config.timeouts?.waitForSelector || 5000) { const start = Date.now(); while (Date.now() - start < timeout) { try { const nodeId = await querySelector(session, selector); if (nodeId && await isEnabled(session, nodeId)) { return nodeId; } } catch {} await waitForTimeout(config.pollInterval || 100); } throw new Error(`❌ Element "${selector}" not enabled in ${timeout}ms`); } export async function waitForElementToBeDisabled(session, selector, timeout = config.timeouts?.waitForSelector || 5000) { const start = Date.now(); while (Date.now() - start < timeout) { try { const nodeId = await querySelector(session, selector); if (nodeId && !await isEnabled(session, nodeId)) { return nodeId; } } catch {} await waitForTimeout(config.pollInterval || 100); } throw new Error(`❌ Element "${selector}" not disabled in ${timeout}ms`); } export async function waitForCondition(session, conditionFn, timeout = config.timeouts?.waitForSelector || 5000) { const start = Date.now(); while (Date.now() - start < timeout) { try { const result = await conditionFn(session); if (result) { return result; } } catch {} await waitForTimeout(config.pollInterval || 100); } throw new Error(`❌ Condition not met in ${timeout}ms`); } // ======================================== // ADVANCED FORM METHODS // ======================================== export async function isChecked(session, selector) { try { const { object } = await resolveNode(session, await querySelector(session, selector)); const { result } = await session.send('Runtime.callFunctionOn', { objectId: object.objectId, functionDeclaration: 'function() { return this.checked || false; }', returnByValue: true }); return result.value; } catch (error) { throw new Error(`❌ Failed to check if element "${selector}" is checked: ${error.message}`); } } export async function getSelectedOptions(session, selector) { try { const { object } = await resolveNode(session, await querySelector(session, selector)); const { result } = await session.send('Runtime.callFunctionOn', { objectId: object.objectId, functionDeclaration: `function() { if (this.tagName !== 'SELECT') return []; return Array.from(this.selectedOptions).map(option => ({ value: option.value, text: option.textContent.trim(), index: option.index })); }`, returnByValue: true }); return result.value; } catch (error) { throw new Error(`❌ Failed to get selected options from "${selector}": ${error.message}`); } } export async function getFormData(session, formSelector) { try { const { object } = await resolveNode(session, await querySelector(session, formSelector)); const { result } = await session.send('Runtime.callFunctionOn', { objectId: object.objectId, functionDeclaration: `function() { const formData = {}; const elements = this.elements; for (let i = 0; i < elements.length; i++) { const element = elements[i]; const name = element.name; if (!name) continue; switch (element.type) { case 'checkbox': if (element.checked) { formData[name] = formData[name] ? (Array.isArray(formData[name]) ? [...formData[name], element.value] : [formData[name], element.value]) : element.value; } break; case 'radio': if (element.checked) { formData[name] = element.value; } break; case 'select-multiple': formData[name] = Array.from(element.selectedOptions).map(option => option.value); break; case 'file': formData[name] = element.files ? Array.from(element.files).map(file => file.name) : []; break; default: formData[name] = element.value; } } return formData; }`, returnByValue: true }); return result.value; } catch (error) { throw new Error(`❌ Failed to get form data from "${formSelector}": ${error.message}`); } } export async function selectMultipleOptions(session, selector, values) { try { if (!Array.isArray(values)) {values = [values];} const { object } = await resolveNode(session, await querySelector(session, selector)); await session.send('Runtime.callFunctionOn', { objectId: object.objectId, functionDeclaration: `function() { if (this.tagName !== 'SELECT') throw new Error('Element is not a select'); Array.from(this.options).forEach(option => { option.selected = ${JSON.stringify(values)}.includes(option.value); }); this.dispatchEvent(new Event('change', { bubbles: true })); }` }); } catch (error) { throw new Error(`❌ Failed to select multiple options in "${selector}": ${error.message}`); } } export async function submitForm(session, formSelector) { try { const { object } = await resolveNode(session, await querySelector(session, formSelector)); await session.send('Runtime.callFunctionOn', { objectId: object.objectId, functionDeclaration: 'function() { this.submit(); }' }); } catch (error) { throw new Error(`❌ Failed to submit form "${formSelector}": ${error.message}`); } } export async function resetForm(session, formSelector) { try { const { object } = await resolveNode(session, await querySelector(session, formSelector)); await session.send('Runtime.callFunctionOn', { objectId: object.objectId, functionDeclaration: 'function() { this.reset(); }' }); } catch (error) { throw new Error(`❌ Failed to reset form "${formSelector}": ${error.message}`); } } // ======================================== // DATA EXTRACTION METHODS // ======================================== export async function getTableData(session, tableSelector) { try { const { object } = await resolveNode(session, await querySelector(session, tableSelector)); const { result } = await session.send('Runtime.callFunctionOn', { objectId: object.objectId, functionDeclaration: `function() { const rows = Array.from(this.querySelectorAll('tr')); if (rows.length === 0) return []; const headerRow = rows[0]; const headers = Array.from(headerRow.querySelectorAll('th, td')).map(cell => cell.textContent.trim()); const data = rows.slice(1).map((row, rowIndex) => { const cells = Array.from(row.querySelectorAll('td, th')); const rowData = { _rowIndex: rowIndex }; headers.forEach((header, index) => { const cellText = cells[index] ? cells[index].textContent.trim() : ''; rowData[header || 'column_' + index] = cellText; }); return rowData; }); return data; }`, returnByValue: true }); return result.value; } catch (error) { throw new Error(`❌ Failed to get table data from "${tableSelector}": ${error.message}`); } } export async function getTableHeaders(session, tableSelector) { try { const { object } = await resolveNode(session, await querySelector(session, tableSelector)); const { result } = await session.send('Runtime.callFunctionOn', { objectId: object.objectId, functionDeclaration: `function() { const headerRow = this.querySelector('tr'); if (!headerRow) return []; return Array.from(headerRow.querySelectorAll('th, td')).map(cell => cell.textContent.trim()); }`, returnByValue: true }); return result.value; } catch (error) { throw new Error(`❌ Failed to get table headers from "${tableSelector}": ${error.message}`); } } export async function getTableRow(session, tableSelector, rowIndex) { try { const { object } = await resolveNode(session, await querySelector(session, tableSelector)); const { result } = await session.send('Runtime.callFunctionOn', { objectId: object.objectId, functionDeclaration: `function() { const rows = Array.from(this.querySelectorAll('tr')); const targetRow = rows[${rowIndex}]; if (!targetRow) return null; return Array.from(targetRow.querySelectorAll('td, th')).map(cell => cell.textContent.trim()); }`, returnByValue: true }); return result.value; } catch (error) { throw new Error(`❌ Failed to get table row ${rowIndex} from "${tableSelector}": ${error.message}`); } } export async function getTableCell(session, tableSelector, rowIndex, columnIndex) { try { const { object } = await resolveNode(session, await querySelector(session, tableSelector)); const { result } = await session.send('Runtime.callFunctionOn', { objectId: object.objectId, functionDeclaration: `function() { const rows = Array.from(this.querySelectorAll('tr')); const targetRow = rows[${rowIndex}]; if (!targetRow) return null; const cells = Array.from(targetRow.querySelectorAll('td, th')); const targetCell = cells[${columnIndex}]; return targetCell ? targetCell.textContent.trim() : null; }`, returnByValue: true }); return result.value; } catch (error) { throw new Error(`❌ Failed to get table cell [${rowIndex}, ${columnIndex}] from "${tableSelector}": ${error.message}`); } } export async function getListItems(session, listSelector) { try { const { object } = await resolveNode(session, await querySelector(session, listSelector)); const { result } = await session.send('Runtime.callFunctionOn', { objectId: object.objectId, functionDeclaration: `function() { const items = Array.from(this.querySelectorAll('li')); return items.map((item, index) => ({ index: index, text: item.textContent.trim(), innerHTML: item.innerHTML, value: item.value || item.getAttribute('data-value') || null })); }`, returnByValue: true }); return result.value; } catch (error) { throw new Error(`❌ Failed to get list items from "${listSelector}": ${error.message}`); } } export async function getListItemByIndex(session, listSelector, index) { try { const { object } = await resolveNode(session, await querySelector(session, listSelector)); const { result } = await session.send('Runtime.callFunctionOn', { objectId: object.objectId, functionDeclaration: `function() { const items = Array.from(this.querySelectorAll('li')); const item = items[${index}]; if (!item) return null; return { index: ${index}, text: item.textContent.trim(), innerHTML: item.innerHTML, value: item.value || item.getAttribute('data-value') || null }; }`, returnByValue: true }); return result.value; } catch (error) { throw new Error(`❌ Failed to get list item ${index} from "${listSelector}": ${error.message}`); } } export async function getElementsCount(session, selector) { try { const { root: { nodeId } } = await session.send('DOM.getDocument'); const { nodeIds } = await session.send('DOM.querySelectorAll', { nodeId, selector }); return nodeIds.length; } catch (error) { throw new Error(`❌ Failed to count elements "${selector}": ${error.message}`); } } // ======================================== // VISUAL TESTING METHODS // ======================================== export async function getElementPosition(session, selector) { try { const nodeId = await querySelector(session, selector); const { model } = await session.send('DOM.getBoxModel', { nodeId }); return { x: model.content[0], y: model.content[1] }; } catch (error) { throw new Error(`❌ Failed to get element position "${selector}": ${error.message}`); } } export async function getElementSize(session, selector) { try { const nodeId = await querySelector(session, selector); const { model } = await session.send('DOM.getBoxModel', { nodeId }); return { width: model.width, height: model.height }; } catch (error) { throw new Error(`❌ Failed to get element size "${selector}": ${error.message}`); } } export async function getElementBounds(session, selector) { try { const nodeId = await querySelector(session, selector); const { model } = await session.send('DOM.getBoxModel', { nodeId }); return { x: model.content[0], y: model.content[1], width: model.width, height: model.height, top: model.content[1], left: model.content[0], bottom: model.content[1] + model.height, right: model.content[0] + model.width }; } catch (error) { throw new Error(`❌ Failed to get element bounds "${selector}": ${error.message}`); } } export async function isElementInViewport(session, selector) { try { const { object } = await resolveNode(session, await querySelector(session, selector)); const { result } = await session.send('Runtime.callFunctionOn', { objectId: object.objectId, functionDeclaration: `function() { const rect = this.getBoundingClientRect(); return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) ); }`, returnByValue: true }); return result.value; } catch (error) { throw new Error(`❌ Failed to check if element "${selector}" is in viewport: ${error.message}`); } } export async function scrollToElement(session, selector) { try { const { object } = await resolveNode(session, await querySelector(session, selector)); await session.send('Runtime.callFunctionOn', { objectId: object.objectId, functionDeclaration: 'function() { this.scrollIntoView({ behavior: "smooth", block: "center" }); }' }); // Wait for smooth scroll to complete await waitForTimeout(500); } catch (error) { throw new Error(`❌ Failed to scroll to element "${selector}": ${error.message}`); } } export async function scrollToTop(session) { try { await session.send('Runtime.evaluate', { expression: 'window.scrollTo({ top: 0, behavior: "smooth" });' }); await waitForTimeout(500); } catch (error) { throw new Error(`❌ Failed to scroll to top: ${error.message}`); } } export async function scrollToBottom(session) { try { await session.send('Runtime.evaluate', { expression: 'window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" });' }); await waitForTimeout(500); } catch (error) { throw new Error(`❌ Failed to scroll to bottom: ${error.message}`); } } export async function getScrollPosition(session) { try { const { result } = await session.send('Runtime.evaluate', { expression: 'JSON.stringify({ x: window.scrollX, y: window.scrollY });', returnByValue: true }); return JSON.parse(result.value); } catch (error) { throw new Error(`❌ Failed to get scroll position: ${error.message}`); } } // ======================================== // ADVANCED INTERACTIONS // ======================================== export async function doubleClick(session, selector) { try { const nodeId = await querySelector(session, selector); const { x, y, width, height } = await getBoundingBox(session, nodeId); const cx = x + width / 2; const cy = y + height / 2; await session.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: cx, y: cy, button: 'left', clickCount: 2 }); await session.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: cx, y: cy, button: 'left', clickCount: 2 }); } catch (error) { throw new Error(`❌ Failed to double click "${selector}": ${error.message}`); } } export async function sendKeys(session, selector, keys) { try { const { object } = await resolveNode(session, await querySelector(session, selector)); await session.send('Runtime.callFunctionOn', { objectId: object.objectId, functionDeclaration: 'function() { this.focus(); }' }); // Send each key for (const key of keys) { await session.send('Input.dispatchKeyEvent', { type: 'keyDown', key: key, code: key, text: key.length === 1 ? key : undefined }); await session.send('Input.dispatchKeyEvent', { type: 'keyUp', key: key, code: key, text: key.length === 1 ? key : undefined }); } } catch (error) { throw new Error(`❌ Failed to send keys "${keys}" to "${selector}": ${error.message}`); } } export async function clearAndType(session, selector, text) { try { // First clear the field await clearInput(session, selector); // Then type the new text await type(session, selector, text); } catch (error) { throw new Error(`❌ Failed to clear and type "${text}" in "${selector}": ${error.message}`); } } // ======================================== // ELEMENT ANALYSIS METHODS // ======================================== export async function getElementTagName(session, selector) { try { const { object } = await resolveNode(session, await querySelector(session, selector)); const { result } = await session.send('Runtime.callFunctionOn', { objectId: object.objectId, functionDeclaration: 'function() { return this.tagName.toLowerCase(); }', returnByValue: true }); return result.value; } catch (error) { throw new Error(`❌ Failed to get tag name of "${selector}": ${error.message}`); } } export async function getElementClasses(session, selector) { try { const { object } = await resolveNode(session, await querySelector(session, selector)); const { result } = await session.send('Runtime.callFunctionOn', { objectId: object.objectId, functionDeclaration: 'function() { return Array.from(this.classList); }', returnByValue: true }); return result.value; } catch (error) { throw new Error(`❌ Failed to get classes of "${selector}": ${error.message}`); } } export async function hasClass(session, selector, className) { try { const { object } = await resolveNode(session, await querySelector(session, selector)); const { result } = await session.send('Runtime.callFunctionOn', { objectId: object.objectId, functionDeclaration: `function() { return this.classList.contains('${className}'); }`, returnByValue: true }); return result.value; } catch (error) { throw new Error(`❌ Failed to check if "${selector}" has class "${className}": ${error.message}`); } } export async function getElementChildren(session, selector) { try { const { object } = await resolveNode(session, await querySelector(session, selector)); const { result } = await session.send('Runtime.callFunctionOn', { objectId: object.objectId, functionDeclaration: `function() { return Array.from(this.children).map((child, index) => ({ index: index, tagName: child.tagName.toLowerCase(), className: child.className, id: child.id, textContent: child.textContent.trim() })); }`, returnByValue: true }); return result.value; } catch (error) { throw new Error(`❌ Failed to get children of "${selector}": ${error.message}`); } } export async function getElementParent(session, selector) { try { const { object } = await resolveNode(session, await querySelector(session, selector)); const { result } = await session.send('Runtime.callFunctionOn', { objectId: object.objectId, functionDeclaration: `function() { const parent = this.parentElement; if (!parent) return null; return { tagName: parent.tagName.toLowerCase(), className: parent.className, id: parent.id, textContent: parent.textContent.trim() }; }`, returnByValue: true }); return result.value; } catch (error) { throw new Error(`❌ Failed to get parent of "${selector}": ${error.message}`); } } // ======================================== // ADDITIONAL ADVANCED INTERACTIONS // ======================================== export async function mouseDown(session, selector, button = 'left') { try { const nodeId = await querySelector(session, selector); const { x, y, width, height } = await getBoundingBox(session, nodeId); const cx = x + width / 2; const cy = y + height / 2; await session.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: cx, y: cy, button: button, clickCount: 1 }); } catch (error) { throw new Error(`❌ Failed to mouse down on "${selector}": ${error.message}`); } } export async function mouseUp(session, selector, button = 'left') { try { const nodeId = await querySelector(session, selector); const { x, y, width, height } = await getBoundingBox(session, nodeId); const cx = x + width / 2; const cy = y + height / 2; await session.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: cx, y: cy, button: button, clickCount: 1 }); } catch (error) { throw new Error(`❌ Failed to mouse up on "${selector}": ${error.message}`); } } export async function selectText(session, selector, startOffset, endOffset) { try { const { object } = await resolveNode(session, await querySelector(session, selector)); await session.send('Runtime.callFunctionOn', { objectId: object.objectId, functionDeclaration: `function() { this.focus(); if (this.setSelectionRange) { this.setSelectionRange(${startOffset}, ${endOffset}); } else if (this.createTextRange) { const range = this.createTextRange(); range.collapse(true); range.moveStart('character', ${startOffset}); range.moveEnd('character', ${endOffset - startOffset}); range.select(); } }` }); } catch (error) { throw new Error(`❌ Failed to select text in "${selector}" from ${startOffset} to ${endOffset}: ${error.message}`); } } export async function uploadMultipleFiles(session, selector, filePaths) { try { if (!Array.isArray(filePaths)) { throw new Error('filePaths must be an array'); } const { object } = await resolveNode(session, await querySelector(session, selector)); const fileContents = []; // Read all files for (const filePath of filePaths) { if (!fs.existsSync(filePath)) { throw new Error(`File not found: ${filePath}`); } const content = fs.readFileSync(filePath); const fileName = path.basename(filePath); fileContents.push({ name: fileName, content: Array.from(content) }); } await session.send('Runtime.callFunctionOn', { objectId: object.objectId, functionDeclaration: `function() { const files = ${JSON.stringify(fileContents)}.map(fileData => { const uint8Array = new Uint8Array(fileData.content); return new File([uint8Array], fileData.name); }); const dataTransfer = new DataTransfer(); files.forEach(file => dataTransfer.items.add(file)); this.files = dataTransfer.files; this.dispatchEvent(new Event('change', { bubbles: true })); }` }); } catch (error) { throw new Error(`❌ Failed to upload multiple files to "${selector}": ${error.message}`); } }