stealthwright
Version:
Chrome DevTools Protocol automation library with Playwright-like API, designed for improved detection avoidance
1,705 lines (1,479 loc) • 53 kB
JavaScript
/**
* Stealthwright Page Module
*
* This module provides page interaction capabilities similar to Playwright.
* It includes element locators, navigation, and page actions.
*/
const fs = require('fs-extra');
const path = require('path');
const { errors } = require('./errors');
/**
* Locator class for interacting with DOM elements
*/
class Locator {
/**
* Create a new locator
* @param {Page} page - The page instance
* @param {string} selector - CSS selector for the element
*/
constructor(page, selector) {
this.page = page;
this.selector = selector;
this.timeout = 30000; // Default timeout in ms
this.interval = 350; // Default polling interval in ms
}
/**
* Check if the element exists in the DOM
* @returns {Promise<boolean>} - True if element exists
*/
async elementExists() {
try {
const params = {
expression: `document.querySelector("${this.selector}") !== null`,
returnByValue: true
};
const response = await this.page.browser.sendCommand("Runtime.evaluate", params);
if (response && response.result && response.result.result) {
return response.result.result.value === true;
}
return false;
} catch (error) {
console.error(`Error checking element existence: ${error}`);
return false;
}
}
/**
* Wait for element to appear in the DOM
* @param {Object} [options] - Wait options
* @param {number} [options.timeout] - Custom timeout in ms
* @param {string} [options.state='visible'] - Wait state (attached, detached, visible, hidden)
* @returns {Promise<boolean>} - True if element appeared before timeout
*/
async waitFor(options = {}) {
const startTime = Date.now();
const timeoutToUse = options.timeout || this.timeout;
const state = options.state || 'visible';
let checkFunction;
switch (state) {
case 'attached':
checkFunction = async () => await this.elementExists();
break;
case 'detached':
checkFunction = async () => !(await this.elementExists());
break;
case 'visible':
checkFunction = async () => await this.isVisible();
break;
case 'hidden':
checkFunction = async () => !(await this.isVisible());
break;
default:
checkFunction = async () => await this.elementExists();
}
while (Date.now() - startTime < timeoutToUse) {
const result = await checkFunction();
if (result) {
return true;
}
// Sleep for the polling interval
await new Promise(resolve => setTimeout(resolve, this.interval));
}
throw new errors.TimeoutError(`Timed out waiting ${timeoutToUse}ms for selector "${this.selector}" to be in state "${state}"`);
}
/**
* Fill a form field with text
* @param {string} value - Text to fill into the field
* @param {Object} [options] - Fill options
* @returns {Promise<void>}
*/
async fill(value, options = {}) {
try {
await this.waitFor({ state: 'visible' });
// Focus element
await this.page.browser.sendCommand("Runtime.evaluate", {
expression: `document.querySelector("${this.selector}").focus()`
});
// Clear existing content (Cmd+A then Backspace)
await this.page.browser.sendCommand("Input.dispatchKeyEvent", {
type: "keyDown",
modifiers: 2, // Command key modifier
key: "a"
});
await this.page.browser.sendCommand("Input.dispatchKeyEvent", {
type: "keyDown",
key: "Backspace"
});
// Insert new text
await this.page.browser.sendCommand("Input.insertText", {
text: value
});
console.log(`Filled selector ${this.selector} with value: ${value}`);
} catch (error) {
console.error(`Failed to fill element: ${error}`);
throw error;
}
}
/**
* Click on an element
* @param {Object} [options] - Click options
* @returns {Promise<void>}
*/
async click(options = {}) {
try {
await this.waitFor({ state: 'visible' });
await this.page.browser.sendCommand("Runtime.evaluate", {
expression: `
(function() {
const element = document.querySelector("${this.selector}");
if (!element) throw new Error('Element not found');
// Get element position for native click
const rect = element.getBoundingClientRect();
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2;
// Create click info for debugging
window._stealthwrightClickInfo = { selector: "${this.selector}", x, y };
// Standard DOM click (works for most cases)
element.click();
return { x, y };
})()
`,
returnByValue: true,
awaitPromise: true
});
console.log(`Clicked on selector: ${this.selector}`);
} catch (error) {
console.error(`Failed to click element: ${error}`);
throw error;
}
}
/**
* Double click on an element
* @param {Object} [options] - Click options
* @returns {Promise<void>}
*/
async dblclick(options = {}) {
try {
await this.waitFor({ state: 'visible' });
await this.page.browser.sendCommand("Runtime.evaluate", {
expression: `
(function() {
const element = document.querySelector("${this.selector}");
if (!element) throw new Error('Element not found');
// Create and dispatch a double-click event
const event = new MouseEvent('dblclick', {
bubbles: true,
cancelable: true,
view: window
});
element.dispatchEvent(event);
})()
`,
awaitPromise: true
});
console.log(`Double-clicked on selector: ${this.selector}`);
} catch (error) {
console.error(`Failed to double-click element: ${error}`);
throw error;
}
}
/**
* Type text sequentially with delays between each character
* @param {string} text - Text to type
* @param {Object} [options] - Type options
* @param {number} [options.delay=100] - Delay between keypresses in ms
* @returns {Promise<void>}
*/
async type(text, options = {}) {
try {
await this.waitFor({ state: 'visible' });
const delay = options.delay || 100;
// Focus element
await this.page.browser.sendCommand("Runtime.evaluate", {
expression: `document.querySelector("${this.selector}").focus()`
});
// Type each character with delay
for (const char of text) {
await this.page.browser.sendCommand("Input.insertText", {
text: char
});
await new Promise(resolve => setTimeout(resolve, delay));
}
console.log(`Typed text into selector ${this.selector}: ${text}`);
} catch (error) {
console.error(`Failed to type text: ${error}`);
throw error;
}
}
/**
* Type text with random typing mistakes that are corrected
* @param {string} text - Text to type
* @param {Object} [options] - Type options
* @param {number} [options.delay=100] - Delay between keypresses in ms
* @param {number} [options.mistakeProbability=0.3] - Probability of making a mistake (0-1)
* @returns {Promise<void>}
*/
async typeWithMistakes(text, options = {}) {
try {
await this.waitFor({ state: 'visible' });
const delay = options.delay || 100;
const mistakeProbability = options.mistakeProbability || 0.3;
// Focus element
await this.page.browser.sendCommand("Runtime.evaluate", {
expression: `document.querySelector("${this.selector}").focus()`
});
// Type each character with potential mistakes
for (const char of text) {
// Chance to make a mistake
if (Math.random() < mistakeProbability) {
// Type a wrong character
const wrongChar = String.fromCharCode(97 + Math.floor(Math.random() * 26)); // random a-z
await this.page.browser.sendCommand("Input.insertText", {
text: wrongChar
});
await new Promise(resolve => setTimeout(resolve, delay));
// Delete wrong character
await this.page.browser.sendCommand("Input.dispatchKeyEvent", {
type: "rawKeyDown",
key: "Backspace",
windowsVirtualKeyCode: 8,
nativeVirtualKeyCode: 8
});
await new Promise(resolve => setTimeout(resolve, delay));
}
// Type correct character
await this.page.browser.sendCommand("Input.insertText", {
text: char
});
await new Promise(resolve => setTimeout(resolve, delay));
}
console.log(`Typed text with mistakes into selector ${this.selector}: ${text}`);
} catch (error) {
console.error(`Failed to type text with mistakes: ${error}`);
throw error;
}
}
/**
* Press a key on the keyboard
* @param {string} key - Key to press
* @param {Object} [options] - Press options
* @returns {Promise<void>}
*/
async press(key, options = {}) {
try {
await this.waitFor({ state: 'visible' });
// Focus element
await this.page.browser.sendCommand("Runtime.evaluate", {
expression: `document.querySelector("${this.selector}").focus()`
});
// For Enter key, use a more robust approach with proper key codes
if (key === 'Enter' || key === 'Return') {
// Key down event
await this.page.browser.sendCommand("Input.dispatchKeyEvent", {
type: "keyDown",
windowsVirtualKeyCode: 13, // Enter key code
code: "Enter",
key: "Enter",
text: "\r"
});
// Add a small delay (key held down)
await new Promise(resolve => setTimeout(resolve, 10));
// Key up event
await this.page.browser.sendCommand("Input.dispatchKeyEvent", {
type: "keyUp",
windowsVirtualKeyCode: 13, // Enter key code
code: "Enter",
key: "Enter"
});
console.log(`Pressed Enter key on selector ${this.selector}`);
return;
}
// For other keys, use the standard approach
await this.page.browser.sendCommand("Input.dispatchKeyEvent", {
type: "keyDown",
key: key
});
await this.page.browser.sendCommand("Input.dispatchKeyEvent", {
type: "keyUp",
key: key
});
console.log(`Pressed key ${key} on selector ${this.selector}`);
} catch (error) {
console.error(`Failed to press key: ${error}`);
throw error;
}
}
/**
* Get the inner text of an element
* @returns {Promise<string>} - Inner text content
*/
async innerText() {
try {
await this.waitFor({ state: 'attached' });
const response = await this.page.browser.sendCommand("Runtime.evaluate", {
expression: `document.querySelector("${this.selector}").innerText`,
returnByValue: true
});
if (response && response.result && response.result.result) {
return response.result.result.value;
}
throw new Error(`Unexpected response format for inner text: ${JSON.stringify(response)}`);
} catch (error) {
console.error(`Failed to get inner text: ${error}`);
throw error;
}
}
/**
* Get the text content of an element
* @returns {Promise<string>} - Text content
*/
async textContent() {
try {
await this.waitFor({ state: 'attached' });
const response = await this.page.browser.sendCommand("Runtime.evaluate", {
expression: `document.querySelector("${this.selector}").textContent`,
returnByValue: true
});
if (response && response.result && response.result.result) {
return response.result.result.value;
}
throw new Error(`Unexpected response format for text content: ${JSON.stringify(response)}`);
} catch (error) {
console.error(`Failed to get text content: ${error}`);
throw error;
}
}
/**
* Get an attribute value from an element
* @param {string} attributeName - Name of the attribute
* @returns {Promise<string|null>} - Attribute value or null if not found
*/
async getAttribute(attributeName) {
try {
await this.waitFor({ state: 'attached' });
const response = await this.page.browser.sendCommand("Runtime.evaluate", {
expression: `document.querySelector("${this.selector}").getAttribute("${attributeName}")`,
returnByValue: true
});
if (response && response.result && response.result.result) {
return response.result.result.value;
}
return null;
} catch (error) {
console.error(`Failed to get attribute: ${error}`);
throw error;
}
}
/**
* Check if an element is visible
* @returns {Promise<boolean>} - True if element is visible
*/
async isVisible() {
try {
const exists = await this.elementExists();
if (!exists) return false;
const response = await this.page.browser.sendCommand("Runtime.evaluate", {
expression: `
(function() {
const element = document.querySelector("${this.selector}");
if (!element) return false;
const style = window.getComputedStyle(element);
if (style.display === 'none' ||
style.visibility === 'hidden' ||
style.opacity === '0') {
return false;
}
// Check if element is in viewport
const rect = element.getBoundingClientRect();
return rect.width > 0 &&
rect.height > 0 &&
rect.top < window.innerHeight &&
rect.left < window.innerWidth &&
rect.bottom > 0 &&
rect.right > 0;
})()
`,
returnByValue: true
});
if (response && response.result && response.result.result) {
return response.result.result.value === true;
}
return false;
} catch (error) {
console.error(`Failed to check visibility: ${error}`);
return false;
}
}
/**
* Check element states
* @returns {Promise<Object>} - Element states
*/
async highlight() {
try {
await this.waitFor({ state: 'attached' });
await this.page.browser.sendCommand("Runtime.evaluate", {
expression: `
(function() {
const element = document.querySelector("${this.selector}");
if (!element) return;
const originalOutline = element.style.outline;
element.style.outline = '3px solid red';
setTimeout(() => {
element.style.outline = originalOutline;
}, 2000);
})()
`
});
} catch (error) {
console.error(`Failed to highlight element: ${error}`);
throw error;
}
}
/**
* Get bounding box of element
* @returns {Promise<Object>} - Bounding box (x, y, width, height)
*/
async boundingBox() {
try {
await this.waitFor({ state: 'attached' });
const response = await this.page.browser.sendCommand("Runtime.evaluate", {
expression: `
(function() {
const element = document.querySelector("${this.selector}");
if (!element) return null;
const rect = element.getBoundingClientRect();
return {
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height
};
})()
`,
returnByValue: true
});
if (response && response.result && response.result.result && response.result.result.value) {
return response.result.result.value;
}
return null;
} catch (error) {
console.error(`Failed to get bounding box: ${error}`);
throw error;
}
}
/**
* Check if element is checked (for checkboxes, radio buttons)
* @returns {Promise<boolean>} - True if element is checked
*/
async isChecked() {
try {
await this.waitFor({ state: 'attached' });
const response = await this.page.browser.sendCommand("Runtime.evaluate", {
expression: `
(function() {
const element = document.querySelector("${this.selector}");
if (!element) return false;
return element.checked === true;
})()
`,
returnByValue: true
});
if (response && response.result && response.result.result) {
return response.result.result.value === true;
}
return false;
} catch (error) {
console.error(`Failed to check if element is checked: ${error}`);
return false;
}
}
/**
* Get all selected options from a select element
* @returns {Promise<Array<string>>} - Array of selected option values
*/
async selectedOptions() {
try {
await this.waitFor({ state: 'attached' });
const response = await this.page.browser.sendCommand("Runtime.evaluate", {
expression: `
(function() {
const element = document.querySelector("${this.selector}");
if (!element || element.tagName.toLowerCase() !== 'select')
return [];
return Array.from(element.selectedOptions).map(option => ({
value: option.value,
text: option.text
}));
})()
`,
returnByValue: true
});
if (response && response.result && response.result.result && response.result.result.value) {
return response.result.result.value;
}
return [];
} catch (error) {
console.error(`Failed to get selected options: ${error}`);
return [];
}
}
/**
* Select option(s) from a select element
* @param {string|Array<string>} values - Value or array of values to select
* @returns {Promise<void>}
*/
async selectOption(values) {
try {
await this.waitFor({ state: 'visible' });
const valuesArray = Array.isArray(values) ? values : [values];
const valuesJson = JSON.stringify(valuesArray);
await this.page.browser.sendCommand("Runtime.evaluate", {
expression: `
(function() {
const element = document.querySelector("${this.selector}");
if (!element || element.tagName.toLowerCase() !== 'select')
throw new Error('Element is not a select element');
const values = ${valuesJson};
// Deselect all options first (for multi-select)
if (element.multiple) {
for (const option of element.options) {
option.selected = false;
}
}
// Select the options that match the values
let matched = false;
for (const option of element.options) {
if (values.includes(option.value)) {
option.selected = true;
matched = true;
}
}
if (!matched) {
throw new Error('No options matched the specified values');
}
// Dispatch change event
element.dispatchEvent(new Event('change', { bubbles: true }));
})()
`,
awaitPromise: true
});
console.log(`Selected options ${valuesJson} on selector ${this.selector}`);
} catch (error) {
console.error(`Failed to select options: ${error}`);
throw error;
}
}
/**
* Check or uncheck a checkbox or radio button
* @param {boolean} [checked=true] - Whether to check or uncheck
* @returns {Promise<void>}
*/
async setChecked(checked = true) {
try {
await this.waitFor({ state: 'visible' });
await this.page.browser.sendCommand("Runtime.evaluate", {
expression: `
(function() {
const element = document.querySelector("${this.selector}");
if (!element) throw new Error('Element not found');
if (element.type !== 'checkbox' && element.type !== 'radio')
throw new Error('Element is not a checkbox or radio button');
const currentChecked = element.checked;
if (currentChecked !== ${checked}) {
element.click();
}
})()
`,
awaitPromise: true
});
console.log(`Set checked state to ${checked} on selector ${this.selector}`);
} catch (error) {
console.error(`Failed to set checked state: ${error}`);
throw error;
}
}
/**
* Hover over an element
* @returns {Promise<void>}
*/
async hover() {
try {
await this.waitFor({ state: 'visible' });
await this.page.browser.sendCommand("Runtime.evaluate", {
expression: `
(function() {
const element = document.querySelector("${this.selector}");
if (!element) throw new Error('Element not found');
// Create and dispatch mouse events
element.dispatchEvent(new MouseEvent('mouseover', {
bubbles: true,
cancelable: true,
view: window
}));
element.dispatchEvent(new MouseEvent('mouseenter', {
bubbles: false,
cancelable: true,
view: window
}));
})()
`,
awaitPromise: true
});
console.log(`Hovered over selector: ${this.selector}`);
} catch (error) {
console.error(`Failed to hover over element: ${error}`);
throw error;
}
}
}
/**
* Page class for interacting with browser pages
*/
class Page {
/**
* Create a new Page
* @param {Browser} browser - Browser instance
*/
constructor(browser) {
this.browser = browser;
this.closed = false;
this.url = 'about:blank';
this.defaultTimeout = 30000;
this._listeners = {};
}
/**
* Create a locator for selecting DOM elements
* @param {string} selector - CSS selector
* @returns {Locator} - Locator instance
*/
locator(selector) {
return new Locator(this, selector);
}
/**
* Wait for a specified amount of time
* @param {number} milliseconds - Time to wait in ms
* @returns {Promise<void>}
*/
async waitForTimeout(milliseconds) {
await new Promise(resolve => setTimeout(resolve, milliseconds));
}
/**
* Navigate to a URL
* @param {string} url - URL to navigate to
* @param {Object} [options] - Navigation options
* @param {boolean} [options.waitUntil='load'] - When to consider navigation finished
* @param {number} [options.timeout] - Navigation timeout in ms
* @returns {Promise<Response|null>} - Response or null
*/
async goto(url, options = {}) {
try {
const waitUntil = options.waitUntil || 'load';
const timeout = options.timeout || this.defaultTimeout;
console.log(`Navigating to: ${url}`);
await this.browser.sendCommand("Page.enable", {});
await this.browser.sendCommand("Network.enable", {});
const navigationPromise = this._waitForNavigation({ waitUntil, timeout });
await this.browser.sendCommand("Page.navigate", { url });
this.url = url;
// Wait for navigation to complete
await navigationPromise;
console.log(`Successfully navigated to: ${url}`);
return { ok: true };
} catch (error) {
console.error(`Failed to navigate to ${url}: ${error}`);
throw error;
}
}
/**
* Evaluate a function in the page context
* @param {Function|string} pageFunction - Function or string to evaluate
* @param {...any} args - Arguments to pass to the function
* @returns {Promise<any>} - Result of the function evaluation
*/
async evaluate(pageFunction, ...args) {
try {
// Handle both function and string
let expression;
if (typeof pageFunction === 'function') {
// Convert function and arguments to string representation
const stringifiedArgs = JSON.stringify(args);
expression = `(${pageFunction.toString()})(${stringifiedArgs.slice(1, -1)})`;
} else {
expression = pageFunction;
}
const response = await this.browser.sendCommand("Runtime.evaluate", {
expression,
returnByValue: true,
awaitPromise: true
});
if (response && response.result && response.result.result) {
return response.result.result.value;
}
return null;
} catch (error) {
console.error(`Failed to evaluate script: ${error}`);
throw error;
}
}
/**
* Evaluate a function in the page context and return a JSHandle
* @param {Function|string} pageFunction - Function or string to evaluate
* @param {...any} args - Arguments to pass to the function
* @returns {Promise<any>} - Result as a serialized handle
*/
async evaluateHandle(pageFunction, ...args) {
try {
// Handle both function and string
let expression;
if (typeof pageFunction === 'function') {
// Convert function and arguments to string representation
const stringifiedArgs = JSON.stringify(args);
expression = `(${pageFunction.toString()})(${stringifiedArgs.slice(1, -1)})`;
} else {
expression = pageFunction;
}
const response = await this.browser.sendCommand("Runtime.evaluate", {
expression,
returnByValue: false,
awaitPromise: true
});
if (response && response.result) {
return response.result;
}
return null;
} catch (error) {
console.error(`Failed to evaluate handle: ${error}`);
throw error;
}
}
/**
* Wait for navigation to complete
* @param {Object} [options={}] - Navigation options
* @param {string} [options.waitUntil='load'] - Navigation event to wait for
* @param {number} [options.timeout=30000] - Navigation timeout in ms
* @returns {Promise<void>}
*/
async _waitForNavigation(options = {}) {
const waitUntil = options.waitUntil || 'load';
const timeout = options.timeout || 30000;
const startTime = Date.now();
return new Promise((resolve, reject) => {
const checkNavigation = async () => {
try {
if (Date.now() - startTime > timeout) {
reject(new errors.TimeoutError(`Navigation timeout of ${timeout}ms exceeded`));
return;
}
let readyState = 'loading';
try {
const response = await this.browser.sendCommand("Runtime.evaluate", {
expression: "document.readyState",
returnByValue: true
});
if (response && response.result && response.result.result) {
readyState = response.result.result.value;
}
} catch (err) {
// Ignore evaluation errors during navigation
setTimeout(checkNavigation, 100);
return;
}
let isDone = false;
switch (waitUntil) {
case 'load':
isDone = readyState === 'complete';
break;
case 'domcontentloaded':
isDone = readyState === 'interactive' || readyState === 'complete';
break;
case 'networkidle0':
// Simplified approximation - check complete and wait a bit
if (readyState === 'complete') {
await new Promise(r => setTimeout(r, 500));
isDone = true;
}
break;
case 'networkidle2':
// Simplified approximation - check complete and wait a bit
if (readyState === 'complete') {
await new Promise(r => setTimeout(r, 300));
isDone = true;
}
break;
default:
isDone = readyState === 'complete';
}
if (isDone) {
resolve();
} else {
setTimeout(checkNavigation, 100);
}
} catch (error) {
setTimeout(checkNavigation, 100);
}
};
checkNavigation();
});
}
/**
* Wait for navigation to complete
* @param {Object} [options={}] - Navigation options
* @returns {Promise<void>}
*/
async waitForNavigation(options = {}) {
return this._waitForNavigation(options);
}
/**
* Wait for a selector to appear
* @param {string} selector - CSS selector to wait for
* @param {Object} [options] - Wait options
* @returns {Promise<Locator>} - Locator for the found element
*/
async waitForSelector(selector, options = {}) {
const locator = this.locator(selector);
await locator.waitFor(options);
return locator;
}
/**
* Wait for a function to return a truthy value
* @param {Function} predicate - Function to evaluate
* @param {Object} [options] - Wait options
* @returns {Promise<any>} - Return value of the predicate
*/
async waitForFunction(predicate, options = {}) {
const timeout = options.timeout || this.defaultTimeout;
const polling = options.polling || 'raf'; // 'raf' or number in ms
const startTime = Date.now();
let pollingInterval;
if (polling === 'raf') {
pollingInterval = 16; // Approximate to 60fps
} else if (typeof polling === 'number') {
pollingInterval = polling;
} else {
pollingInterval = 100; // Default polling
}
return new Promise(async (resolve, reject) => {
const checkPredicate = async () => {
try {
if (Date.now() - startTime > timeout) {
reject(new errors.TimeoutError(`Timed out while waiting for predicate to return truthy value`));
return;
}
const result = await this.evaluate(predicate);
if (result) {
resolve(result);
} else {
setTimeout(checkPredicate, pollingInterval);
}
} catch (error) {
setTimeout(checkPredicate, pollingInterval);
}
};
checkPredicate();
});
}
/**
* Save cookies to a file
* @param {string} filePath - File path to save cookies
* @returns {Promise<void>}
*/
async saveCookies(filePath) {
try {
// Get all cookies from the browser
const response = await this.browser.sendCommand("Network.getAllCookies", {});
if (!response || !response.result || !response.result.cookies) {
console.error("No cookies found to save");
return;
}
const cookies = response.result.cookies;
console.log(`Found ${cookies.length} cookies to save`);
// Create directory if it doesn't exist
const dirPath = path.dirname(filePath);
await fs.ensureDir(dirPath);
// Write the cookie file
await fs.writeJson(filePath, cookies, { spaces: 2 });
console.log(`Cookies saved to: ${filePath}`);
} catch (error) {
console.error(`Failed to save cookies: ${error}`);
throw error;
}
}
/**
* Load cookies from a file
* @param {string} filePath - File path to load cookies from
* @returns {Promise<void>}
*/
async loadCookies(filePath) {
try {
// Check if file exists
if (!await fs.pathExists(filePath)) {
throw new Error(`Cookie file not found: ${filePath}`);
}
// Read and parse cookies
const cookies = await fs.readJson(filePath);
console.log(`Loading ${cookies.length} cookies from ${filePath}`);
// Inject each cookie
for (const cookie of cookies) {
await this.browser.sendCommand("Network.setCookie", cookie);
}
console.log(`Successfully loaded cookies`);
} catch (error) {
console.error(`Failed to load cookies: ${error}`);
throw error;
}
}
/**
* Take a screenshot of the current page
* @param {Object} [options={}] - Screenshot options
* @param {string} [options.path] - File path to save screenshot
* @param {boolean} [options.fullPage=false] - Whether to take a full page screenshot
* @returns {Promise<Buffer>} - Screenshot as a Buffer
*/
async screenshot(options = {}) {
try {
const fullPage = options.fullPage || false;
if (fullPage) {
// Get page dimensions
const dimensions = await this.evaluate(() => {
return {
width: Math.max(
document.body.scrollWidth,
document.documentElement.scrollWidth,
document.body.offsetWidth,
document.documentElement.offsetWidth,
document.body.clientWidth,
document.documentElement.clientWidth
),
height: Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight,
document.body.offsetHeight,
document.documentElement.offsetHeight,
document.body.clientHeight,
document.documentElement.clientHeight
)
};
});
// Set viewport to match full page size
await this.browser.sendCommand('Emulation.setDeviceMetricsOverride', {
width: dimensions.width,
height: dimensions.height,
deviceScaleFactor: 1,
mobile: false
});
}
// Capture screenshot
const result = await this.browser.sendCommand('Page.captureScreenshot');
if (fullPage) {
// Reset viewport
await this.browser.sendCommand('Emulation.clearDeviceMetricsOverride');
}
if (!result || !result.result || !result.result.data) {
throw new Error('Failed to capture screenshot');
}
const buffer = Buffer.from(result.result.data, 'base64');
// Save to file if path provided
if (options.path) {
await fs.ensureDir(path.dirname(options.path));
await fs.writeFile(options.path, buffer);
console.log(`Screenshot saved to: ${options.path}`);
}
return buffer;
} catch (error) {
console.error(`Failed to take screenshot: ${error}`);
throw error;
}
}
/**
* Find multiple elements using a selector
* @param {string} selector - CSS selector
* @returns {Promise<Array>} - Array of element handles
*/
async $$(selector) {
try {
const response = await this.browser.sendCommand("Runtime.evaluate", {
expression: `Array.from(document.querySelectorAll("${selector}")).map((el, i) => {
return { index: i, text: el.innerText || el.textContent || '' };
})`,
returnByValue: true
});
if (response && response.result && response.result.result && response.result.result.value) {
return response.result.result.value;
}
return [];
} catch (error) {
console.error(`Failed to find elements: ${error}`);
return [];
}
}
/**
* Find a single element using a selector
* @param {string} selector - CSS selector
* @returns {Promise<Object|null>} - Element handle or null
*/
async $(selector) {
try {
const response = await this.browser.sendCommand("Runtime.evaluate", {
expression: `
(function() {
const element = document.querySelector("${selector}");
if (!element) return null;
return {
exists: true,
textContent: element.textContent || '',
innerText: element.innerText || '',
tagName: element.tagName,
id: element.id,
className: element.className
};
})()
`,
returnByValue: true
});
if (response && response.result && response.result.result && response.result.result.value) {
return response.result.result.value;
}
return null;
} catch (error) {
console.error(`Failed to find element: ${error}`);
return null;
}
}
/**
* Navigate back in the browser history
* @param {Object} [options] - Navigation options
* @returns {Promise<void>}
*/
async goBack(options = {}) {
try {
const waitOptions = { ...options };
const navigationPromise = this._waitForNavigation(waitOptions);
await this.browser.sendCommand("Page.navigate", {
url: "javascript:history.back()"
});
await navigationPromise;
} catch (error) {
console.error(`Failed to navigate back: ${error}`);
throw error;
}
}
/**
* Navigate forward in the browser history
* @param {Object} [options] - Navigation options
* @returns {Promise<void>}
*/
async goForward(options = {}) {
try {
const waitOptions = { ...options };
const navigationPromise = this._waitForNavigation(waitOptions);
await this.browser.sendCommand("Page.navigate", {
url: "javascript:history.forward()"
});
await navigationPromise;
} catch (error) {
console.error(`Failed to navigate forward: ${error}`);
throw error;
}
}
/**
* Reload the current page
* @param {Object} [options] - Navigation options
* @returns {Promise<void>}
*/
async reload(options = {}) {
try {
const waitOptions = { ...options };
const navigationPromise = this._waitForNavigation(waitOptions);
await this.browser.sendCommand("Page.reload");
await navigationPromise;
} catch (error) {
console.error(`Failed to reload page: ${error}`);
throw error;
}
}
/**
* Get the page title
* @returns {Promise<string>} - Page title
*/
async title() {
try {
const response = await this.browser.sendCommand("Runtime.evaluate", {
expression: "document.title",
returnByValue: true
});
if (response && response.result && response.result.result) {
return response.result.result.value;
}
return "";
} catch (error) {
console.error(`Failed to get page title: ${error}`);
throw error;
}
}
/**
* Get the current URL
* @returns {Promise<string>} - Current URL
*/
async url() {
try {
const response = await this.browser.sendCommand("Runtime.evaluate", {
expression: "window.location.href",
returnByValue: true
});
if (response && response.result && response.result.result) {
this.url = response.result.result.value;
return this.url;
}
return this.url;
} catch (error) {
console.error(`Failed to get page URL: ${error}`);
throw error;
}
}
/**
* Set HTTP headers
* @param {Object} headers - HTTP headers to set
* @returns {Promise<void>}
*/
async setExtraHTTPHeaders(headers) {
try {
await this.browser.sendCommand('Network.setExtraHTTPHeaders', {
headers
});
} catch (error) {
console.error(`Failed to set extra HTTP headers: ${error}`);
throw error;
}
}
/**
* Add a script tag to the page
* @param {Object} options - Script options
* @param {string} [options.url] - URL to load script from
* @param {string} [options.path] - Path to load script from
* @param {string} [options.content] - Script content
* @returns {Promise<void>}
*/
async addScriptTag(options) {
try {
let scriptContent;
if (options.url) {
await this.evaluate((url) => {
const script = document.createElement('script');
script.src = url;
script.type = 'text/javascript';
document.head.appendChild(script);
return new Promise((resolve, reject) => {
script.onload = resolve;
script.onerror = reject;
});
}, options.url);
return;
} else if (options.path) {
scriptContent = await fs.readFile(options.path, 'utf8');
} else if (options.content) {
scriptContent = options.content;
} else {
throw new Error('Either url, path or content must be specified');
}
await this.evaluate((content) => {
const script = document.createElement('script');
script.type = 'text/javascript';
script.text = content;
document.head.appendChild(script);
}, scriptContent);
} catch (error) {
console.error(`Failed to add script tag: ${error}`);
throw error;
}
}
/**
* Add a style tag to the page
* @param {Object} options - Style options
* @param {string} [options.url] - URL to load style from
* @param {string} [options.path] - Path to load style from
* @param {string} [options.content] - Style content
* @returns {Promise<void>}
*/
async addStyleTag(options) {
try {
let styleContent;
if (options.url) {
await this.evaluate((url) => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = url;
document.head.appendChild(link);
return new Promise((resolve, reject) => {
link.onload = resolve;
link.onerror = reject;
});
}, options.url);
return;
} else if (options.path) {
styleContent = await fs.readFile(options.path, 'utf8');
} else if (options.content) {
styleContent = options.content;
} else {
throw new Error('Either url, path or content must be specified');
}
await this.evaluate((content) => {
const style = document.createElement('style');
style.type = 'text/css';
style.appendChild(document.createTextNode(content));
document.head.appendChild(style);
}, styleContent);
} catch (error) {
console.error(`Failed to add style tag: ${error}`);
throw error;
}
}
/**
* Get current page content
* @returns {Promise<string>} - Page HTML content
*/
async content() {
try {
const response = await this.browser.sendCommand("Runtime.evaluate", {
expression: "document.documentElement.outerHTML",
returnByValue: true
});
if (response && response.result && response.result.result) {
return response.result.result.value;
}
return "";
} catch (error) {
console.error(`Failed to get page content: ${error}`);
throw error;
}
}
/**
* Set page content
* @param {string} html - HTML content to set
* @param {Object} [options] - Set content options
* @returns {Promise<void>}
*/
async setContent(html, options = {}) {
try {
const waitUntil = options.waitUntil || 'load';
const timeout = options.timeout || this.defaultTimeout;
const navigationPromise = this._waitForNavigation({
waitUntil,
timeout
});
await this.browser.sendCommand("Runtime.evaluate", {
expression: `document.open(); document.write(${JSON.stringify(html)}); document.close();`
});
await navigationPromise;
} catch (error) {
console.error(`Failed to set page content: ${error}`);
throw error;
}
}
/**
* Focus an element
* @param {string} selector - CSS selector
* @returns {Promise<void>}
*/
async focus(selector) {
const locator = this.locator(selector);
await locator.waitFor({ state: 'visible' });
await this.browser.sendCommand("Runtime.evaluate", {
expression: `document.querySelector("${selector}").focus()`
});
}
/**
* Get all cookies
* @returns {Promise<Array>} - Array of cookies
*/
async cookies() {
try {
const response = await this.browser.sendCommand("Network.getAllCookies", {});
if (response && response.result && response.result.cookies) {
return response.result.cookies;
}
return [];
} catch (error) {
console.error(`Failed to get cookies: ${error}`);
throw error;
}
}
/**
* Set cookies
* @param {Array} cookies - Cookies to set
* @returns {Promise<void>}
*/
async setCookies(cookies) {
try {
for (const cookie of cookies) {
await this.browser.sendCommand("Network.setCookie", cookie);
}
} catch (error) {
console.error(`Failed to set cookies: ${error}`);
throw error;
}
}
/**
* Delete cookies
* @param {Object} [options] - Cookie deletion options
* @returns {Promise<void>}
*/
async deleteCookies(options = {}) {
try {
if (options.name) {
// Delete specific cookie
await this.browser.sendCommand("Network.deleteCookies", {
name: options.name,
url: options.url,
domain: options.domain,
path: options.path
});
} else {
// Delete all cookies
await this.browser.sendCommand("Network.clearBrowserCookies", {});
}
} catch (error) {
console.error(`Failed to delete cookies: ${error}`);
throw error;
}
}
/**
* Emulate media type
* @param {Object} options - Media options
* @param {string} [options.media] - Media type ('screen', 'print', etc.)
* @returns {Promise<void>}
*/
async emulateMedia(options = {}) {
try {
if (options.media) {
await this.browser.sendCommand("Emulation.setEmulatedMedia", {
media: options.media
});
}
} catch (error) {
console.error(`Failed to emulate media: ${error}`);
throw error;
}
}
/**
* Close the page
* @returns {Promise<void>}
*/
async close() {
try {
if (this.closed) return;
// Use Target.closeTarget to close the page
await this.browser.sendCommand("Target.closeTarget", {
targetId: this._targetId
});
this.closed = true;
} catch (error) {
console.error(`Failed to close page: ${error}`);
throw error;
}
}
/**
* Set default navigation timeout
* @param {number} timeout - Timeout in milliseconds
*/
setDefaultNavigationTimeout(timeout) {
this.defaultTimeout = timeout;
}
/**
* Set default timeout
* @param {number} timeout - Timeout in milliseconds
*/
setDefaultTimeout(timeout) {
this.defaultTimeout = timeout;
}
/**
* Execute CDP command
* @param {string} method - CDP method
* @param {Object} [params] - CDP method parameters
* @returns {Promise<Object>} - Command result
*/
async cdp(method, params = {}) {
return this.browser.sendCommand(method, params);
}
/**
* Add event listener
* @param {string} event - Event name
* @param {Function} handler - Event handler
*/
on(event, handler) {
if (!this._listeners[event]) {
this._listeners[event] = [];
}
this._listeners[event].push(handler);
}
/**
* Remove event listener
* @param {string} event - Event name
* @param {Function} handler - Event handler
*/
off(event, handler) {
if (!this._listeners[event]) return;
this._listeners[event] = this._listeners[event].filter(h => h !== handler);
}
/**
* Expose function to page
* @param {string} name - Function name
* @param {Function} fn - Function to expose
*/
async exposeFunction(name, fn) {
try {
// Create a globally unique ID for this function
const id = `stealthwright_${Date.now()}_${Math.floor(Math.random() * 10000)}`;
// Store the function reference
global[id] = fn;
// Create a binding in the page
// hi antibot company (i will change this so dont even bother lol)
await this.browser.sendCommand("Runtime.evaluate", {
expression: `
window["${name}"] = async function() {
return fetch("https://stealthwright-bridge/${id}", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(Array.from(arguments))
}).then(r => r.json());
}
`
});
// TODO: Implement actual bridge to call the function
// This is a simplified version that doesn't actually work
console.log(`Function ${name} exposed (placeholder implementation)`);
} catch (error) {
console.error(`Failed to expose function: ${error}`);
throw error;
}
}
// -------------- NEW CONVENIENCE METHODS (PLAYWRIGHT-STYLE) --------------
/**
* Fill a form field with text (direct method)
* @param {string} selector - CSS selector for the element
* @param {string} value - Text to fill into the field
* @param {Object} [options] - Fill options
* @returns {Promise<void>}
*/
async fill(selector, value, options = {}) {
return this.locator(selector).fill(value, options);
}
/**
* Click on an element (direct method)
* @param {string} selector - CSS selector for the element
* @param {Object} [options] - Click options
* @returns {Promise<void>}
*/
async click(selector, options = {}) {
return this.locator(selector).click(options);
}
/**
* Double-click on an element (direct method)
* @param {string} selector - CSS selector for the element
* @param {Object} [options] - Click options
* @returns {Promise<void>}
*/
async dblclick(selector, options = {}) {
return this.locator(selector).dblclick(options);
}
/**
* Type text sequentially (direct method)
* @param {string} selector - CSS selector for the