UNPKG

@monitoro/herd

Version:

Automate your browser, build AI web tools and MCP servers with Monitoro Herd

422 lines (421 loc) 12.7 kB
import { EventEmitter } from 'events'; import { JSDOM } from 'jsdom'; import { Node } from './Node.js'; export class Page extends EventEmitter { constructor(client, device, tabId) { super(); this.tab = null; this.client = client; this.device = device; this.tabId = typeof tabId === 'string' ? parseInt(tabId, 10) : tabId; if (isNaN(this.tabId)) { throw new Error('Invalid tabId: must be convertible to integer'); } } get id() { return this.tabId; } get url() { return this.tab?.url || ''; } get title() { return this.tab?.title || ''; } get active() { return this.tab?.active || false; } /** * Navigate to a URL */ async goto(url, options = {}) { const waitForNav = options.waitForNavigation === undefined ? 'networkidle2' : options.waitForNavigation; if (waitForNav) { const navigationPromise = this.waitForNavigation(waitForNav); await this.client.sendCommand(this.device.deviceId, 'Tabs.updateTab', { id: this.tabId, data: { url } }).then(response => { const tab = response?.result; if (tab) { this.updateInfo(tab); } }).catch(() => { }); await navigationPromise; } else { await this.client.sendCommand(this.device.deviceId, 'Tabs.updateTab', { id: this.tabId, data: { url } }).then(response => { const tab = response?.result; if (tab) { this.updateInfo(tab); } }).catch(() => { }); } // Small delay to ensure page is ready await new Promise(resolve => setTimeout(resolve, 500)); } /** * Find an element on the page */ async find(selector, options = {}) { const response = await this.client.sendCommand(this.device.deviceId, 'Page.find', { tabId: this.tabId, selector, options }); return response?.result; } /** * Click an element */ async click(selector, options = {}) { await this.executeWithNavigation(options.waitForNavigation, 'Page.click', { tabId: this.tabId, selector, options: options }); } /** * Type text into a form field */ async type(selector, text, options = {}) { await this.executeWithNavigation(options.waitForNavigation, 'Page.type', { tabId: this.tabId, selector, text, options: options }); } /** * Focus an element */ async focus(selector, options = {}) { await this.executeWithNavigation(options.waitForNavigation, 'Page.focus', { tabId: this.tabId, selector }); } /** * Blur an element */ async blur(selector, options = {}) { await this.executeWithNavigation(options.waitForNavigation, 'Page.blur', { tabId: this.tabId, selector }); } /** * Press a key */ async press(key, options = {}) { await this.executeWithNavigation(options.waitForNavigation, 'Page.press', { tabId: this.tabId, key }); } /** * Hover over an element */ async hover(selector, options = {}) { await this.executeWithNavigation(options.waitForNavigation, 'Page.hover', { tabId: this.tabId, selector }); } /** * Move mouse to coordinates or element */ async moveMouse(target, options = {}) { await this.executeWithNavigation(options.waitForNavigation, 'Page.moveMouse', { tabId: this.tabId, target }); } /** * Drag and drop elements */ async drag(sourceSelector, targetSelector, options = {}) { await this.executeWithNavigation(options.waitForNavigation, 'Page.drag', { tabId: this.tabId, sourceSelector, targetSelector }); } /** * Scroll by x,y amount */ async scroll(x, y, options = {}) { await this.executeWithNavigation(options.waitForNavigation, 'Page.scroll', { tabId: this.tabId, x, y }); } /** * Scroll to absolute position */ async scrollTo(x, y, options = {}) { await this.executeWithNavigation(options.waitForNavigation, 'Page.scrollTo', { tabId: this.tabId, x, y }); } /** * Scroll element into view */ async scrollIntoView(selector, options = {}) { await this.executeWithNavigation(options.waitForNavigation, 'Page.scrollIntoView', { tabId: this.tabId, selector }); } /** * Dispatch an event */ async dispatchEvent(eventName, selector, detail, options = {}) { await this.executeWithNavigation(options.waitForNavigation, 'Page.dispatchEvent', { tabId: this.tabId, eventName, selector, detail }); } /** * Set value of form element */ async setValue(selector, value, options = {}) { await this.executeWithNavigation(options.waitForNavigation, 'Page.setValue', { tabId: this.tabId, selector, value }); } async executeWithNavigation(waitForNavigation, command, payload) { if (waitForNavigation) { this.client.sendCommand(this.device.deviceId, command, payload).catch(() => { }); return this.waitForNavigation(waitForNavigation); } else { this.client.sendCommand(this.device.deviceId, command, payload).catch(() => { }); return new Promise(resolve => setTimeout(resolve, 100)); } } // alias for $ async querySelector(selector) { return this.$(selector); } // alias for $$ async querySelectorAll(selector) { return this.$$(selector); } /** * Query the page for a single element (deserialized) */ async $(selector, rootSelector) { const node = await this._$(selector, rootSelector); if (!node) { return null; } return new Node(node, this); } /** * Query the page for multiple elements (deserialized) */ async $$(selector, rootSelector) { const nodes = await this._$$(selector, rootSelector); // Return Node instances return nodes.map((node) => new Node(node, this)); } async elementFromPoint(x, y) { const node = await this._elementFromPoint(x, y); if (!node) { return null; } return new Node(node, this); } /** * Query the page for a single element (serialized) */ async _$(selector, rootSelector) { const response = await this.client.sendCommand(this.device.deviceId, 'Page.$', { tabId: this.tabId, selector, rootSelector }); // Return Node instance return response?.result; } /** * Query the page for multiple elements (serialized) */ async _$$(selector, rootSelector) { const response = await this.client.sendCommand(this.device.deviceId, 'Page.$$', { tabId: this.tabId, selector, rootSelector }); // Return Node instances return response?.result; } async _elementFromPoint(x, y) { const response = await this.client.sendCommand(this.device.deviceId, 'Page.elementFromPoint', { tabId: this.tabId, x, y }); return response?.result; } /** * Extract data from the page using selectors */ async extract(config) { const response = await this.client.sendCommand(this.device.deviceId, 'Page.extract', { tabId: this.tabId, config }); return response?.result; } /** * Wait for an element to appear/disappear */ async waitForElement(selector, options = {}) { const response = await this.client.sendCommand(this.device.deviceId, 'Page.waitForElement', { tabId: this.tabId, selector, options }); return response?.result; } /** * Wait for a selector to appear/disappear (alias for waitForElement) */ async waitForSelector(selector, options = {}) { return this.waitForElement(selector, options); } /** * Wait for navigation to complete */ async waitForNavigation(condition) { await this.client.sendCommand(this.device.deviceId, 'Page.waitForNavigation', { tabId: this.tabId, condition }); } /** * Evaluate JavaScript in the page context */ async evaluate(script) { const response = await this.client.sendCommand(this.device.deviceId, 'Page.evaluate', { tabId: this.tabId, script: typeof script === 'function' ? script.toString() : script }); return response?.result; } /** * Go back in history */ async back(options = {}) { const waitForNav = options.waitForNavigation === undefined ? 'networkidle2' : options.waitForNavigation; if (waitForNav) { const navigationPromise = this.waitForNavigation(waitForNav); this.client.sendCommand(this.device.deviceId, 'Page.back', { tabId: this.tabId }).catch(() => { }); await navigationPromise; } else { await this.client.sendCommand(this.device.deviceId, 'Page.back', { tabId: this.tabId }).catch(() => { }); } } /** * Go forward in history */ async forward(options = {}) { const waitForNav = options.waitForNavigation === undefined ? 'networkidle2' : options.waitForNavigation; if (waitForNav) { const navigationPromise = this.waitForNavigation(waitForNav); this.client.sendCommand(this.device.deviceId, 'Page.forward', { tabId: this.tabId }).catch(() => { }); await navigationPromise; } else { await this.client.sendCommand(this.device.deviceId, 'Page.forward', { tabId: this.tabId }).catch(() => { }); } } /** * Reload the page */ async reload(options = {}) { const waitForNav = options.waitForNavigation === undefined ? 'networkidle2' : options.waitForNavigation; if (waitForNav) { const navigationPromise = this.waitForNavigation(waitForNav); this.client.sendCommand(this.device.deviceId, 'Page.reload', { tabId: this.tabId }).catch(() => { }); await navigationPromise; } else { await this.client.sendCommand(this.device.deviceId, 'Page.reload', { tabId: this.tabId }).catch(() => { }); } } /** * Make the tab active */ async activate() { const response = await this.client.sendCommand(this.device.deviceId, 'Tabs.updateTab', { id: this.tabId, data: { active: true } }); const tab = response?.result; if (tab) { this.updateInfo(tab); } } /** * Close the page */ async close() { await this.client.sendCommand(this.device.deviceId, 'Tabs.closeTab', { id: this.tabId }); this.removeAllListeners(); } /** * Subscribe to page events * * WARNING: This method doesn't match any of the provided method names. * Consider renaming or removing if not needed. */ onEvent(callback) { return this.client.subscribeToDeviceEvent(this.device.deviceId, `page.${this.tabId}`, callback); } /** * Update tab information */ updateInfo(tab) { this.tab = tab; this.emit('updated', tab); } /** * Returns a read-only DOM representation of the page. WARNING: events are not replicated to the remote page. */ async dom() { const data = await this.extract({ html: { _$: 'html', attribute: 'innerHTML' } }); return new JSDOM(data.html); } }