- Added 9 new tools: visual overlay, highlight, toast, drag-drop, etc. - Fixed evaluateJS to support function arguments - Fixed hover for non-visible elements - Fixed storage operations with try/catch - Added 10 new wait tools: clickable, element visible, text - Fixed tool name mapping in server.js for MCP protocol - Updated README with 60+ tools documentation - Version bump to 3.1.1
786 lines
22 KiB
JavaScript
786 lines
22 KiB
JavaScript
const { chromium } = require('playwright');
|
|
|
|
let browser = null;
|
|
let context = null;
|
|
let pages = [];
|
|
let activePageIndex = 0;
|
|
let downloadsPath = null;
|
|
let networkLogs = [];
|
|
let blockedResources = [];
|
|
|
|
const STEALTH_SCRIPTS = [
|
|
`Object.defineProperty(navigator, 'webdriver', { get: () => undefined });`,
|
|
`Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });`,
|
|
`Object.defineProperty(navigator, 'languages', { get: () => ['pt-BR', 'pt', 'en-US', 'en'] });`,
|
|
`window.chrome = { runtime: {}, loadTimes: function(){}, csi: function(){} };`,
|
|
`const originalQuery = window.navigator.permissions.query;
|
|
window.navigator.permissions.query = (parameters) => (
|
|
parameters.name === 'notifications' ?
|
|
Promise.resolve({ state: Notification.permission }) :
|
|
originalQuery(parameters)
|
|
);`,
|
|
`delete navigator.__proto__.webdriver;`,
|
|
`const iframeDesc = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, 'contentWindow');
|
|
Object.defineProperty(HTMLIFrameElement.prototype, 'contentWindow', {
|
|
get: function() {
|
|
const iframe = iframeDesc.get.call(this);
|
|
if (iframe && !iframe.chrome) {
|
|
iframe.chrome = window.chrome;
|
|
}
|
|
return iframe;
|
|
}
|
|
});`,
|
|
`const getParameter = WebGLRenderingContext.prototype.getParameter;
|
|
WebGLRenderingContext.prototype.getParameter = function(parameter) {
|
|
if (parameter === 37445) return 'Intel Inc.';
|
|
if (parameter === 37446) return 'Intel Iris OpenGL Engine';
|
|
return getParameter.call(this, parameter);
|
|
};`
|
|
];
|
|
|
|
async function start(options = {}) {
|
|
if (browser) return;
|
|
|
|
const {
|
|
headless = false,
|
|
proxy = null,
|
|
downloadsDir = null,
|
|
viewport = { width: 1280, height: 720 },
|
|
userAgent = null,
|
|
locale = 'pt-BR',
|
|
timezoneId = 'America/Sao_Paulo',
|
|
geolocation = null,
|
|
permissions = ['geolocation'],
|
|
ignoreHTTPSErrors = true,
|
|
colorScheme = 'light',
|
|
args = []
|
|
} = options;
|
|
|
|
downloadsPath = downloadsDir;
|
|
|
|
const launchArgs = [
|
|
'--disable-blink-features=AutomationControlled',
|
|
'--disable-infobars',
|
|
'--no-sandbox',
|
|
'--disable-setuid-sandbox',
|
|
'--disable-dev-shm-usage',
|
|
'--disable-accelerated-2d-canvas',
|
|
'--disable-gpu',
|
|
'--window-size=1280,720',
|
|
'--disable-extensions',
|
|
...args
|
|
];
|
|
|
|
const launchOptions = {
|
|
headless,
|
|
args: launchArgs
|
|
};
|
|
|
|
if (proxy) {
|
|
launchOptions.proxy = typeof proxy === 'string' ? { server: proxy } : proxy;
|
|
}
|
|
|
|
browser = await chromium.launch(launchOptions);
|
|
|
|
const contextOptions = {
|
|
viewport,
|
|
userAgent: userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
|
locale,
|
|
timezoneId,
|
|
ignoreHTTPSErrors,
|
|
colorScheme,
|
|
permissions: geolocation ? permissions : [],
|
|
javaScriptEnabled: true,
|
|
bypassCSP: true,
|
|
offline: false,
|
|
hasTouch: false,
|
|
isMobile: false,
|
|
deviceScaleFactor: 1
|
|
};
|
|
|
|
if (geolocation) {
|
|
contextOptions.geolocation = geolocation;
|
|
}
|
|
|
|
if (downloadsPath) {
|
|
contextOptions.acceptDownloads = true;
|
|
}
|
|
|
|
context = await browser.newContext(contextOptions);
|
|
|
|
await context.addInitScript(STEALTH_SCRIPTS.join('\n'));
|
|
|
|
const page = await context.newPage();
|
|
pages = [page];
|
|
activePageIndex = 0;
|
|
|
|
if (downloadsPath) {
|
|
page.on('download', async (download) => {
|
|
const filePath = `${downloadsPath}/${download.suggestedFilename()}`;
|
|
await download.saveAs(filePath);
|
|
});
|
|
}
|
|
|
|
page.on('request', (req) => {
|
|
if (blockedResources.some(r => req.resourceType() === r)) {
|
|
req.abort();
|
|
return;
|
|
}
|
|
networkLogs.push({
|
|
type: 'request',
|
|
url: req.url(),
|
|
method: req.method(),
|
|
resourceType: req.resourceType(),
|
|
timestamp: Date.now()
|
|
});
|
|
if (networkLogs.length > 1000) networkLogs = networkLogs.slice(-500);
|
|
});
|
|
|
|
page.on('response', (res) => {
|
|
networkLogs.push({
|
|
type: 'response',
|
|
url: res.url(),
|
|
status: res.status(),
|
|
timestamp: Date.now()
|
|
});
|
|
});
|
|
|
|
browser.on('disconnected', () => {
|
|
browser = null;
|
|
context = null;
|
|
pages = [];
|
|
activePageIndex = 0;
|
|
networkLogs = [];
|
|
});
|
|
}
|
|
|
|
async function ensurePage() {
|
|
if (!browser) {
|
|
await start();
|
|
return pages[activePageIndex];
|
|
}
|
|
|
|
const currentPage = pages[activePageIndex];
|
|
if (!currentPage || currentPage.isClosed()) {
|
|
const newPage = await context.newPage();
|
|
pages[activePageIndex] = newPage;
|
|
setupPageListeners(newPage);
|
|
return newPage;
|
|
}
|
|
|
|
return currentPage;
|
|
}
|
|
|
|
function setupPageListeners(page) {
|
|
if (downloadsPath) {
|
|
page.on('download', async (download) => {
|
|
const filePath = `${downloadsPath}/${download.suggestedFilename()}`;
|
|
await download.saveAs(filePath);
|
|
});
|
|
}
|
|
|
|
page.on('request', (req) => {
|
|
if (blockedResources.some(r => req.resourceType() === r)) {
|
|
req.abort();
|
|
return;
|
|
}
|
|
networkLogs.push({
|
|
type: 'request',
|
|
url: req.url(),
|
|
method: req.method(),
|
|
resourceType: req.resourceType(),
|
|
timestamp: Date.now()
|
|
});
|
|
});
|
|
|
|
page.on('response', (res) => {
|
|
networkLogs.push({
|
|
type: 'response',
|
|
url: res.url(),
|
|
status: res.status(),
|
|
timestamp: Date.now()
|
|
});
|
|
});
|
|
}
|
|
|
|
async function goto(url, options = {}) {
|
|
const p = await ensurePage();
|
|
const {
|
|
waitUntil = 'domcontentloaded',
|
|
timeout = 30000,
|
|
referer = undefined
|
|
} = options;
|
|
|
|
const response = await p.goto(url, { waitUntil, timeout, referer });
|
|
await p.waitForTimeout(1000);
|
|
|
|
return {
|
|
status: response?.status() || null,
|
|
url: p.url(),
|
|
title: await p.title()
|
|
};
|
|
}
|
|
|
|
async function click(selector, options = {}) {
|
|
const p = await ensurePage();
|
|
await p.waitForSelector(selector, { state: 'visible', timeout: options.timeout || 10000 });
|
|
await p.click(selector, {
|
|
button: options.button || 'left',
|
|
clickCount: options.clickCount || 1,
|
|
delay: options.delay || 0,
|
|
force: options.force || false
|
|
});
|
|
}
|
|
|
|
async function type(selector, text, options = {}) {
|
|
const p = await ensurePage();
|
|
await p.waitForSelector(selector, { state: 'visible', timeout: 10000 });
|
|
|
|
if (options.clear) {
|
|
await p.fill(selector, '');
|
|
}
|
|
|
|
if (options.slowly) {
|
|
await p.click(selector);
|
|
await p.keyboard.type(text, { delay: options.delay || 50 });
|
|
} else {
|
|
await p.fill(selector, text);
|
|
}
|
|
}
|
|
|
|
async function content() {
|
|
const p = await ensurePage();
|
|
return await p.content();
|
|
}
|
|
|
|
async function title() {
|
|
const p = await ensurePage();
|
|
return await p.title();
|
|
}
|
|
|
|
async function currentUrl() {
|
|
const p = await ensurePage();
|
|
return p.url();
|
|
}
|
|
|
|
async function screenshot(options = {}) {
|
|
const p = await ensurePage();
|
|
const {
|
|
fullPage = true,
|
|
path = `screenshot-${Date.now()}.png`,
|
|
type = 'png',
|
|
quality,
|
|
clip,
|
|
element = null
|
|
} = options;
|
|
|
|
if (element) {
|
|
const el = await p.$(element);
|
|
if (el) {
|
|
await el.screenshot({ path, type, quality });
|
|
return path;
|
|
}
|
|
}
|
|
|
|
await p.screenshot({ path, fullPage, type, quality, clip });
|
|
return path;
|
|
}
|
|
|
|
async function newTab(url = null) {
|
|
if (!browser || !context) await start();
|
|
if (!context) throw new Error('Browser context not available');
|
|
const page = await context.newPage();
|
|
setupPageListeners(page);
|
|
pages.push(page);
|
|
activePageIndex = pages.length - 1;
|
|
|
|
if (url) {
|
|
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
await page.waitForTimeout(1000);
|
|
}
|
|
|
|
return page;
|
|
}
|
|
|
|
async function switchTab(index) {
|
|
if (index < 0 || index >= pages.length) {
|
|
throw new Error(`Tab index ${index} out of range. Available tabs: ${pages.length}`);
|
|
}
|
|
activePageIndex = index;
|
|
return pages[activePageIndex];
|
|
}
|
|
|
|
function listTabs() {
|
|
return pages.map((p, i) => ({
|
|
index: i,
|
|
url: p.url(),
|
|
title: null,
|
|
isActive: i === activePageIndex,
|
|
isClosed: p.isClosed()
|
|
}));
|
|
}
|
|
|
|
async function closeTab(index = null) {
|
|
const idx = index !== null ? index : activePageIndex;
|
|
if (idx < 0 || idx >= pages.length) {
|
|
throw new Error(`Tab index ${idx} out of range`);
|
|
}
|
|
|
|
const page = pages[idx];
|
|
await page.close();
|
|
pages.splice(idx, 1);
|
|
|
|
if (pages.length === 0) {
|
|
activePageIndex = 0;
|
|
const newPage = await context.newPage();
|
|
setupPageListeners(newPage);
|
|
pages.push(newPage);
|
|
} else if (activePageIndex >= pages.length) {
|
|
activePageIndex = pages.length - 1;
|
|
}
|
|
|
|
return { closed: idx, activeTab: activePageIndex, totalTabs: pages.length };
|
|
}
|
|
|
|
async function goBack() {
|
|
const p = await ensurePage();
|
|
await p.goBack({ waitUntil: 'domcontentloaded' });
|
|
await p.waitForTimeout(500);
|
|
return p.url();
|
|
}
|
|
|
|
async function goForward() {
|
|
const p = await ensurePage();
|
|
await p.goForward({ waitUntil: 'domcontentloaded' });
|
|
await p.waitForTimeout(500);
|
|
return p.url();
|
|
}
|
|
|
|
async function reload() {
|
|
const p = await ensurePage();
|
|
await p.reload({ waitUntil: 'domcontentloaded' });
|
|
await p.waitForTimeout(500);
|
|
return p.url();
|
|
}
|
|
|
|
async function evaluateJS(expression, arg = null) {
|
|
const p = await ensurePage();
|
|
|
|
if (typeof expression === 'function') {
|
|
if (arg !== null && arg !== undefined) {
|
|
return await p.evaluate(expression, arg);
|
|
}
|
|
return await p.evaluate(expression);
|
|
}
|
|
|
|
if (arg !== null && arg !== undefined && arg !== {}) {
|
|
return await p.evaluate((expr, argument) => {
|
|
try {
|
|
if (!argument) return eval(expr);
|
|
const keys = Object.keys(argument);
|
|
const values = Object.values(argument);
|
|
if (keys.length === 0) return eval(expr);
|
|
const fn = new Function(...keys, 'return ' + expr);
|
|
return fn(...values);
|
|
} catch (e) {
|
|
return { error: e.message, expression: expr };
|
|
}
|
|
}, { expr: expression, arg });
|
|
}
|
|
return await p.evaluate(expression);
|
|
}
|
|
|
|
async function evaluateOnSelector(selector, expression) {
|
|
const p = await ensurePage();
|
|
return await p.evaluate(({ sel, expr }) => {
|
|
const el = document.querySelector(sel);
|
|
if (!el) return null;
|
|
const fn = new Function('el', 'return ' + expr);
|
|
return fn(el);
|
|
}, { sel: selector, expr: expression });
|
|
}
|
|
|
|
async function hover(selector) {
|
|
const p = await ensurePage();
|
|
const result = await p.evaluate((sel) => {
|
|
const el = document.querySelector(sel);
|
|
if (!el) return { success: false, error: 'Element not found' };
|
|
el.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
const rect = el.getBoundingClientRect();
|
|
return {
|
|
success: true,
|
|
x: rect.left + rect.width / 2,
|
|
y: rect.top + rect.height / 2,
|
|
visible: rect.width > 0 && rect.height > 0
|
|
};
|
|
}, selector);
|
|
|
|
if (!result.success) throw new Error(result.error);
|
|
|
|
if (result.visible) {
|
|
await p.mouse.move(result.x, result.y);
|
|
}
|
|
|
|
return { success: true };
|
|
}
|
|
|
|
async function select(selector, values) {
|
|
const p = await ensurePage();
|
|
await p.waitForSelector(selector, { state: 'attached', timeout: 10000 });
|
|
return await p.selectOption(selector, values);
|
|
}
|
|
|
|
async function dragAndDrop(sourceSelector, targetSelector) {
|
|
const p = await ensurePage();
|
|
await p.waitForSelector(sourceSelector, { state: 'visible', timeout: 10000 });
|
|
await p.waitForSelector(targetSelector, { state: 'visible', timeout: 10000 });
|
|
await p.dragAndDrop(sourceSelector, targetSelector);
|
|
}
|
|
|
|
async function pressKey(key, options = {}) {
|
|
const p = await ensurePage();
|
|
const { selector = null, delay = 0 } = options;
|
|
|
|
if (selector) {
|
|
await p.waitForSelector(selector, { state: 'visible', timeout: 10000 });
|
|
await p.focus(selector);
|
|
}
|
|
|
|
await p.keyboard.press(key, { delay });
|
|
}
|
|
|
|
async function scrollTo(position) {
|
|
const p = await ensurePage();
|
|
|
|
if (typeof position === 'number') {
|
|
await p.evaluate((y) => window.scrollTo(0, y), position);
|
|
} else if (typeof position === 'object' && position.selector) {
|
|
await p.evaluate((sel) => {
|
|
const el = document.querySelector(sel);
|
|
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}, position.selector);
|
|
} else if (position === 'top') {
|
|
await p.evaluate(() => window.scrollTo(0, 0));
|
|
} else if (position === 'bottom') {
|
|
await p.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
|
}
|
|
}
|
|
|
|
async function setViewport(width, height) {
|
|
const p = await ensurePage();
|
|
await p.setViewportSize({ width, height });
|
|
}
|
|
|
|
async function emulateDevice(deviceName) {
|
|
const devices = {
|
|
'iphone-se': { viewport: { width: 375, height: 667 }, userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1', isMobile: true, hasTouch: true, deviceScaleFactor: 2 },
|
|
'iphone-14': { viewport: { width: 390, height: 844 }, userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1', isMobile: true, hasTouch: true, deviceScaleFactor: 3 },
|
|
'iphone-14-pro-max': { viewport: { width: 430, height: 932 }, userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1', isMobile: true, hasTouch: true, deviceScaleFactor: 3 },
|
|
'ipad': { viewport: { width: 768, height: 1024 }, userAgent: 'Mozilla/5.0 (iPad; CPU OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1', isMobile: true, hasTouch: true, deviceScaleFactor: 2 },
|
|
'ipad-pro': { viewport: { width: 1024, height: 1366 }, userAgent: 'Mozilla/5.0 (iPad; CPU OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1', isMobile: true, hasTouch: true, deviceScaleFactor: 2 },
|
|
'pixel-5': { viewport: { width: 393, height: 851 }, userAgent: 'Mozilla/5.0 (Linux; Android 13; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36', isMobile: true, hasTouch: true, deviceScaleFactor: 2.75 },
|
|
'galaxy-s20': { viewport: { width: 360, height: 800 }, userAgent: 'Mozilla/5.0 (Linux; Android 13; SM-G981B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36', isMobile: true, hasTouch: true, deviceScaleFactor: 3 },
|
|
'galaxy-s23-ultra': { viewport: { width: 384, height: 854 }, userAgent: 'Mozilla/5.0 (Linux; Android 13; SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36', isMobile: true, hasTouch: true, deviceScaleFactor: 3.5 },
|
|
'desktop-1080p': { viewport: { width: 1920, height: 1080 }, userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', isMobile: false, hasTouch: false, deviceScaleFactor: 1 },
|
|
'desktop-4k': { viewport: { width: 3840, height: 2160 }, userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', isMobile: false, hasTouch: false, deviceScaleFactor: 2 }
|
|
};
|
|
|
|
const device = devices[deviceName.toLowerCase()];
|
|
if (!device) {
|
|
throw new Error(`Device "${deviceName}" not found. Available: ${Object.keys(devices).join(', ')}`);
|
|
}
|
|
|
|
if (!context) await start();
|
|
|
|
await context.close();
|
|
|
|
const contextOptions = {
|
|
viewport: device.viewport,
|
|
userAgent: device.userAgent,
|
|
isMobile: device.isMobile,
|
|
hasTouch: device.hasTouch,
|
|
deviceScaleFactor: device.deviceScaleFactor,
|
|
locale: 'pt-BR',
|
|
timezoneId: 'America/Sao_Paulo',
|
|
ignoreHTTPSErrors: true
|
|
};
|
|
|
|
context = await browser.newContext(contextOptions);
|
|
await context.addInitScript(STEALTH_SCRIPTS.join('\n'));
|
|
|
|
pages = [];
|
|
const page = await context.newPage();
|
|
setupPageListeners(page);
|
|
pages.push(page);
|
|
activePageIndex = 0;
|
|
|
|
return { device: deviceName, viewport: device.viewport };
|
|
}
|
|
|
|
async function pdf(options = {}) {
|
|
const p = await ensurePage();
|
|
const {
|
|
path = `page-${Date.now()}.pdf`,
|
|
format = 'A4',
|
|
landscape = false,
|
|
printBackground = true,
|
|
margin = { top: '20px', bottom: '20px', left: '20px', right: '20px' }
|
|
} = options;
|
|
|
|
await p.pdf({ path, format, landscape, printBackground, margin });
|
|
return path;
|
|
}
|
|
|
|
async function getCookies(urls = []) {
|
|
if (!context) await start();
|
|
if (urls.length > 0) {
|
|
return await context.cookies(urls);
|
|
}
|
|
return await context.cookies();
|
|
}
|
|
|
|
async function setCookies(cookies) {
|
|
if (!context) await start();
|
|
await context.addCookies(cookies);
|
|
return { set: cookies.length };
|
|
}
|
|
|
|
async function clearCookies() {
|
|
if (!context) return { cleared: 0 };
|
|
const allCookies = await context.cookies();
|
|
await context.clearCookies();
|
|
return { cleared: allCookies.length };
|
|
}
|
|
|
|
async function getStorage() {
|
|
const p = await ensurePage();
|
|
|
|
const storage = await p.evaluate(() => {
|
|
try {
|
|
const local = {};
|
|
const session = {};
|
|
|
|
for (let i = 0; i < localStorage.length; i++) {
|
|
const key = localStorage.key(i);
|
|
local[key] = localStorage.getItem(key);
|
|
}
|
|
|
|
for (let i = 0; i < sessionStorage.length; i++) {
|
|
const key = sessionStorage.key(i);
|
|
session[key] = sessionStorage.getItem(key);
|
|
}
|
|
|
|
return { localStorage: local, sessionStorage: session };
|
|
} catch (e) {
|
|
return { localStorage: {}, sessionStorage: {}, error: e.message };
|
|
}
|
|
});
|
|
|
|
return { origin: p.url(), ...storage };
|
|
}
|
|
|
|
async function setStorage(data) {
|
|
const p = await ensurePage();
|
|
|
|
await p.evaluate((storageData) => {
|
|
try {
|
|
if (storageData.localStorage) {
|
|
Object.entries(storageData.localStorage).forEach(([k, v]) => localStorage.setItem(k, v));
|
|
}
|
|
if (storageData.sessionStorage) {
|
|
Object.entries(storageData.sessionStorage).forEach(([k, v]) => sessionStorage.setItem(k, v));
|
|
}
|
|
} catch (e) {}
|
|
}, data);
|
|
|
|
return { set: true };
|
|
}
|
|
|
|
async function clearStorage() {
|
|
const p = await ensurePage();
|
|
await p.evaluate(() => {
|
|
try {
|
|
localStorage.clear();
|
|
sessionStorage.clear();
|
|
} catch (e) {}
|
|
});
|
|
return { cleared: true };
|
|
}
|
|
|
|
async function getNetworkLogs(filter = null) {
|
|
if (!filter) return networkLogs;
|
|
|
|
return networkLogs.filter(log => {
|
|
if (filter.type && log.type !== filter.type) return false;
|
|
if (filter.url && !log.url.includes(filter.url)) return false;
|
|
if (filter.method && log.method !== filter.method) return false;
|
|
if (filter.resourceType && log.resourceType !== filter.resourceType) return false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
function clearNetworkLogs() {
|
|
const count = networkLogs.length;
|
|
networkLogs = [];
|
|
return { cleared: count };
|
|
}
|
|
|
|
function blockResources(types = []) {
|
|
blockedResources = [...new Set([...blockedResources, ...types])];
|
|
return { blocked: blockedResources };
|
|
}
|
|
|
|
function unblockResources(types = []) {
|
|
if (types.length === 0) {
|
|
blockedResources = [];
|
|
} else {
|
|
blockedResources = blockedResources.filter(t => !types.includes(t));
|
|
}
|
|
return { blocked: blockedResources };
|
|
}
|
|
|
|
async function waitForURL(urlPattern, options = {}) {
|
|
const p = await ensurePage();
|
|
const { timeout = 30000 } = options;
|
|
|
|
if (typeof urlPattern === 'string') {
|
|
await p.waitForURL(`**${urlPattern}**`, { timeout });
|
|
} else if (urlPattern instanceof RegExp) {
|
|
await p.waitForURL(urlPattern, { timeout });
|
|
}
|
|
|
|
return p.url();
|
|
}
|
|
|
|
async function uploadFile(selector, filePaths) {
|
|
const p = await ensurePage();
|
|
await p.waitForSelector(selector, { state: 'attached', timeout: 10000 });
|
|
const files = Array.isArray(filePaths) ? filePaths : [filePaths];
|
|
await p.setInputFiles(selector, files);
|
|
return { uploaded: files.length, files };
|
|
}
|
|
|
|
async function download(url) {
|
|
const p = await ensurePage();
|
|
|
|
const [dl] = await Promise.all([
|
|
p.waitForEvent('download'),
|
|
p.evaluate((u) => {
|
|
const a = document.createElement('a');
|
|
a.href = u;
|
|
a.download = '';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
}, url)
|
|
]);
|
|
|
|
const filename = dl.suggestedFilename();
|
|
const savePath = downloadsPath ? `${downloadsPath}/${filename}` : filename;
|
|
await dl.saveAs(savePath);
|
|
|
|
return { filename, path: savePath };
|
|
}
|
|
|
|
async function close() {
|
|
if (browser) {
|
|
try {
|
|
await browser.close();
|
|
} catch (e) { }
|
|
browser = null;
|
|
context = null;
|
|
pages = [];
|
|
activePageIndex = 0;
|
|
networkLogs = [];
|
|
blockedResources = [];
|
|
}
|
|
}
|
|
|
|
async function waitForTimeout(ms) {
|
|
const p = await ensurePage();
|
|
await p.waitForTimeout(ms);
|
|
}
|
|
|
|
async function getPerformanceMetrics() {
|
|
const p = await ensurePage();
|
|
return await p.evaluate(() => {
|
|
const perf = performance.getEntriesByType('navigation')[0];
|
|
if (!perf) return null;
|
|
return {
|
|
dns: perf.domainLookupEnd - perf.domainLookupStart,
|
|
tcp: perf.connectEnd - perf.connectStart,
|
|
ttfb: perf.responseStart - perf.requestStart,
|
|
download: perf.responseEnd - perf.responseStart,
|
|
domInteractive: perf.domInteractive,
|
|
domComplete: perf.domComplete,
|
|
loadEvent: perf.loadEventEnd - perf.loadEventStart,
|
|
total: perf.loadEventEnd - perf.navigationStart
|
|
};
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
start,
|
|
goto,
|
|
click,
|
|
type,
|
|
content,
|
|
title,
|
|
currentUrl,
|
|
screenshot,
|
|
close,
|
|
newTab,
|
|
switchTab,
|
|
listTabs,
|
|
closeTab,
|
|
ensurePage,
|
|
goBack,
|
|
goForward,
|
|
reload,
|
|
evaluateJS,
|
|
evaluateOnSelector,
|
|
hover,
|
|
select,
|
|
dragAndDrop,
|
|
pressKey,
|
|
scrollTo,
|
|
setViewport,
|
|
emulateDevice,
|
|
pdf,
|
|
getCookies,
|
|
setCookies,
|
|
clearCookies,
|
|
getStorage,
|
|
setStorage,
|
|
clearStorage,
|
|
getNetworkLogs,
|
|
clearNetworkLogs,
|
|
blockResources,
|
|
unblockResources,
|
|
waitForURL,
|
|
uploadFile,
|
|
download,
|
|
waitForTimeout,
|
|
getPerformanceMetrics,
|
|
|
|
get page() {
|
|
return pages[activePageIndex];
|
|
},
|
|
|
|
get browser() {
|
|
return browser;
|
|
},
|
|
|
|
get context() {
|
|
return context;
|
|
},
|
|
|
|
get tabs() {
|
|
return pages;
|
|
},
|
|
|
|
get activeTab() {
|
|
return activePageIndex;
|
|
}
|
|
};
|