@monitoro/herd
Version:
Automate your browser, build AI web tools and MCP servers with Monitoro Herd
422 lines (421 loc) • 12.7 kB
JavaScript
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);
}
}