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; } };