feat: MCP Browser v3.0 - 42 tools with stealth mode, multi-tab, PDF, cookies, storage, network logs, device emulation
This commit is contained in:
@@ -1,254 +1,198 @@
|
|||||||
# MCP-Browser
|
# MCP-Browser v3.0.0
|
||||||
|
|
||||||
MCP server que permite a qualquer agente de IA controlar um navegador real usando [Playwright](https://playwright.dev/).
|
MCP server para automação de navegador de alta qualidade usando [Playwright](https://playwright.dev/) com **42 tools** para automação avançada.
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
## Funcionalidades
|
## Novidades v3.0
|
||||||
|
|
||||||
- Controle total de navegador via IA
|
- **Stealth Mode** - Anti-detecção avançada
|
||||||
- 17 tools prontas para uso
|
- **25 novas tools** - Muito mais poder
|
||||||
- Reuso automático de abas (mesmo domínio)
|
- **Multi-Tab real** - Gerenciamento completo
|
||||||
- Preenchimento automático de formulários
|
- **PDF generation** - Geração de PDFs
|
||||||
- Fluxos agentísticos com retry
|
- **Network Monitoring** - Logs de rede
|
||||||
- Compatível com qualquer plataforma MCP
|
- **Device Emulation** - iPhone, Pixel, etc
|
||||||
|
- **Performance Metrics** - Métricas detalhadas
|
||||||
## Pré-requisitos
|
|
||||||
|
|
||||||
| Dependência | Versão mínima |
|
|
||||||
|-------------|---------------|
|
|
||||||
| Node.js | 18+ |
|
|
||||||
| npm | 9+ |
|
|
||||||
|
|
||||||
## Instalação
|
## Instalação
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Clonar o repositório
|
|
||||||
git clone https://git.jf.eng.br/jfeng/MCP-Browser.git
|
|
||||||
cd MCP-Browser
|
|
||||||
|
|
||||||
# 2. Instalar dependências
|
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# 3. Instalar Chromium do Playwright
|
|
||||||
npx playwright install chromium
|
npx playwright install chromium
|
||||||
|
|
||||||
# Linux: instalar dependências do sistema
|
|
||||||
sudo npx playwright install-deps chromium
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuração por plataforma
|
## Configuração
|
||||||
|
|
||||||
Todas as plataformas usam o mesmo padrão:
|
|
||||||
|
|
||||||
```
|
|
||||||
Comando: node
|
|
||||||
Argumento: /caminho/absoluto/MCP-Browser/server.js
|
|
||||||
Transport: stdio
|
|
||||||
```
|
|
||||||
|
|
||||||
### OpenCode
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
opencode mcp add
|
node server.js
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Nome:** `meu-navegador`
|
## Tools Disponíveis (42)
|
||||||
- **Tipo:** `Local`
|
|
||||||
- **Comando:** `node /caminho/MCP-Browser/server.js`
|
|
||||||
|
|
||||||
### Cursor
|
### Navegação
|
||||||
|
| Tool | Descrição |
|
||||||
|
|------|-----------|
|
||||||
|
| `open_url` | Abre URL |
|
||||||
|
| `new_tab` | Nova aba |
|
||||||
|
| `switch_tab` | Troca aba |
|
||||||
|
| `list_tabs` | Lista abas |
|
||||||
|
| `close_tab` | Fecha aba |
|
||||||
|
| `go_back` | Voltar |
|
||||||
|
| `go_forward` | Avançar |
|
||||||
|
| `reload` | Recarregar |
|
||||||
|
| `wait_for_url` | Espera URL |
|
||||||
|
|
||||||
`~/.cursor/mcp.json` (global) ou `.cursor/mcp.json` (projeto):
|
### Interação
|
||||||
|
| Tool | Descrição |
|
||||||
|
|------|-----------|
|
||||||
|
| `click` | Clica (suporta duplo, right-click) |
|
||||||
|
| `smart_click` | Clica por texto |
|
||||||
|
| `type` | Digita (suporta lentamente) |
|
||||||
|
| `smart_type` | Digita por label |
|
||||||
|
| `hover` | Faz hover |
|
||||||
|
| `select` | Seleciona opção |
|
||||||
|
| `press_key` | Pressiona tecla |
|
||||||
|
| `scroll_to` | Rola página |
|
||||||
|
| `upload_file` | Upload de arquivo |
|
||||||
|
|
||||||
```json
|
### Extração
|
||||||
{
|
| Tool | Descrição |
|
||||||
"mcpServers": {
|
|------|-----------|
|
||||||
"browser": {
|
| `get_text` | Extrai texto |
|
||||||
"command": "node",
|
| `get_html` | Extrai HTML |
|
||||||
"args": ["/caminho/MCP-Browser/server.js"]
|
| `get_links` | Lista links |
|
||||||
}
|
| `get_buttons` | Lista botões |
|
||||||
}
|
| `get_forms` | Lista formulários |
|
||||||
}
|
| `extract_elements` | Extrai elementos |
|
||||||
```
|
| `get_url` | URL atual |
|
||||||
|
| `get_title` | Título da página |
|
||||||
|
| `evaluate_js` | Executa JavaScript |
|
||||||
|
|
||||||
### Claude Desktop
|
### Screens & PDF
|
||||||
|
| Tool | Descrição |
|
||||||
|
|------|-----------|
|
||||||
|
| `screenshot` | Screenshot (full/elemento) |
|
||||||
|
| `pdf` | Gera PDF |
|
||||||
|
|
||||||
`%APPDATA%\Claude\claude_desktop_config.json` (Windows)
|
### Viewport & Device
|
||||||
`~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
|
| Tool | Descrição |
|
||||||
|
|------|-----------|
|
||||||
|
| `set_viewport` | Define viewport |
|
||||||
|
| `emulate_device` | Emula dispositivo |
|
||||||
|
|
||||||
```json
|
### Cookies & Storage
|
||||||
{
|
| Tool | Descrição |
|
||||||
"mcpServers": {
|
|------|-----------|
|
||||||
"browser": {
|
| `get_cookies` | Obtém cookies |
|
||||||
"command": "node",
|
| `set_cookies` | Define cookies |
|
||||||
"args": ["/caminho/MCP-Browser/server.js"]
|
| `clear_cookies` | Limpa cookies |
|
||||||
}
|
| `get_storage` | Obtém storage |
|
||||||
}
|
| `set_storage` | Define storage |
|
||||||
}
|
| `clear_storage` | Limpa storage |
|
||||||
```
|
|
||||||
|
|
||||||
### Windsurf
|
### Network
|
||||||
|
| Tool | Descrição |
|
||||||
|
|------|-----------|
|
||||||
|
| `get_network_logs` | Logs de rede |
|
||||||
|
| `block_resources` | Bloqueia recursos |
|
||||||
|
|
||||||
`~/.windsurf/mcp.json`:
|
### Performance
|
||||||
|
| Tool | Descrição |
|
||||||
|
|------|-----------|
|
||||||
|
| `get_performance_metrics` | Métricas |
|
||||||
|
|
||||||
```json
|
### Wait
|
||||||
{
|
| Tool | Descrição |
|
||||||
"mcpServers": {
|
|------|-----------|
|
||||||
"browser": {
|
| `wait` | Aguarda ms |
|
||||||
"command": "node",
|
| `wait_for_selector` | Espera elemento |
|
||||||
"args": ["/caminho/MCP-Browser/server.js"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Trae
|
### Automação
|
||||||
|
| Tool | Descrição |
|
||||||
|
|------|-----------|
|
||||||
|
| `fill_form_auto` | Preenche formulário |
|
||||||
|
| `agent_flow` | Fluxo automático |
|
||||||
|
| `agent_flow_v2` | Fluxo com retry |
|
||||||
|
|
||||||
Settings → Extensions → MCP → Adicionar server:
|
### Controle
|
||||||
|
| Tool | Descrição |
|
||||||
|
|------|-----------|
|
||||||
|
| `close_browser` | Fecha navegador |
|
||||||
|
|
||||||
- **Name:** `browser`
|
## Exemplo de Uso
|
||||||
- **Command:** `node`
|
|
||||||
- **Args:** `["/caminho/MCP-Browser/server.js"]`
|
|
||||||
|
|
||||||
### Antigravity
|
```javascript
|
||||||
|
// Abrir site
|
||||||
Configuração de MCP → Novo server:
|
|
||||||
|
|
||||||
- **Name:** `browser`
|
|
||||||
- **Type:** `stdio`
|
|
||||||
- **Command:** `node /caminho/MCP-Browser/server.js`
|
|
||||||
|
|
||||||
### VS Code
|
|
||||||
|
|
||||||
`.vscode/mcp.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"browser": {
|
|
||||||
"command": "node",
|
|
||||||
"args": ["/caminho/MCP-Browser/server.js"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Qualquer plataforma MCP
|
|
||||||
|
|
||||||
Se suporta **MCP stdio transport**, use:
|
|
||||||
|
|
||||||
```
|
|
||||||
Comando: node
|
|
||||||
Argumento: /caminho/absoluto/MCP-Browser/server.js
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tools disponíveis (17)
|
|
||||||
|
|
||||||
| # | Tool | Descrição | Parâmetros |
|
|
||||||
|---|------|-----------|------------|
|
|
||||||
| 1 | `open_url` | Abre uma URL | `url` |
|
|
||||||
| 2 | `click` | Clica por seletor CSS | `selector` |
|
|
||||||
| 3 | `type` | Digita texto em campo | `selector`, `text` |
|
|
||||||
| 4 | `screenshot` | Captura screenshot | `fullPage` (opcional) |
|
|
||||||
| 5 | `get_text` | Extrai texto da página | `selector` (opcional) |
|
|
||||||
| 6 | `get_links` | Lista links | `selector` (opcional) |
|
|
||||||
| 7 | `wait_for_selector` | Espera elemento aparecer | `selector`, `timeout` |
|
|
||||||
| 8 | `extract_elements` | Extrai elementos com atributos | `selector`, `attributes` |
|
|
||||||
| 9 | `smart_click` | Clica por texto visível | `text` |
|
|
||||||
| 10 | `smart_type` | Digita por label/placeholder | `label`, `text` |
|
|
||||||
| 11 | `get_buttons` | Lista botões disponíveis | — |
|
|
||||||
| 12 | `smart_wait_navigation` | Espera mudança de URL | `timeout` (opcional) |
|
|
||||||
| 13 | `get_forms` | Lista formulários e campos | — |
|
|
||||||
| 14 | `fill_form_auto` | Preenche formulário auto | `data` |
|
|
||||||
| 15 | `agent_flow` | Fluxo automático simples | `goal`, `data` |
|
|
||||||
| 16 | `agent_flow_v2` | Fluxo com retry/fallback | `goal`, `data`, `retries` |
|
|
||||||
| 17 | `close_browser` | Fecha o navegador | — |
|
|
||||||
|
|
||||||
## Exemplos de uso
|
|
||||||
|
|
||||||
### Abrir site e extrair conteúdo
|
|
||||||
|
|
||||||
```
|
|
||||||
open_url("https://example.com")
|
open_url("https://example.com")
|
||||||
get_text()
|
|
||||||
get_links()
|
|
||||||
screenshot()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Login automático
|
// Login automático
|
||||||
|
fill_form_auto({"email": "user@test.com", "password": "123"})
|
||||||
```
|
|
||||||
open_url("https://site.com/login")
|
|
||||||
fill_form_auto({"email": "user@test.com", "password": "1234"})
|
|
||||||
smart_click("Entrar")
|
smart_click("Entrar")
|
||||||
smart_wait_navigation()
|
wait_for_url("**dashboard**")
|
||||||
|
|
||||||
|
// Extrair dados
|
||||||
|
get_links()
|
||||||
|
get_text(".product-title")
|
||||||
|
|
||||||
|
// Screenshot
|
||||||
|
screenshot({fullPage: true})
|
||||||
|
|
||||||
|
// Emular mobile
|
||||||
|
emulate_device("iphone-14")
|
||||||
```
|
```
|
||||||
|
|
||||||
### Scraping com atributos
|
## Dispositivos Disponíveis
|
||||||
|
|
||||||
```
|
- iphone-se, iphone-14, iphone-14-pro-max
|
||||||
open_url("https://site.com/produtos")
|
- ipad, ipad-pro
|
||||||
extract_elements(".produto", ["href", "data-id"])
|
- pixel-5, galaxy-s20, galaxy-s23-ultra
|
||||||
```
|
- desktop-1080p, desktop-4k
|
||||||
|
|
||||||
## Estrutura do projeto
|
## Estrutura
|
||||||
|
|
||||||
```
|
```
|
||||||
MCP-Browser/
|
MCP-Browser/
|
||||||
├── server.js # Servidor MCP (entry point)
|
├── server.js # Servidor MCP
|
||||||
├── browser.js # Engine Playwright (singleton)
|
├── browser.js # Engine Playwright
|
||||||
├── package.json
|
├── package.json
|
||||||
├── docs.html # Documentação detalhada
|
└── tools/ # 42 tools
|
||||||
└── tools/
|
|
||||||
├── openUrl.js
|
├── openUrl.js
|
||||||
├── click.js
|
├── click.js
|
||||||
├── type.js
|
├── getURL.js
|
||||||
├── screenshot.js
|
├── getTitle.js
|
||||||
├── getText.js
|
├── getHTML.js
|
||||||
├── getLinks.js
|
├── goBack.js
|
||||||
├── getButtons.js
|
├── goForward.js
|
||||||
├── getForms.js
|
├── reload.js
|
||||||
├── extractElements.js
|
├── evaluateJS.js
|
||||||
├── waitForSelector.js
|
├── waitForURL.js
|
||||||
├── smartClick.js
|
├── newTab.js
|
||||||
├── smartType.js
|
├── switchTab.js
|
||||||
├── fillFormAuto.js
|
├── listTabs.js
|
||||||
├── smartWaitNavigation.js
|
├── closeTab.js
|
||||||
├── agentFlow.js
|
├── setViewport.js
|
||||||
├── agentFlowV2.js
|
├── emulateDevice.js
|
||||||
└── closeBrowser.js
|
├── hover.js
|
||||||
|
├── select.js
|
||||||
|
├── pressKey.js
|
||||||
|
├── scrollTo.js
|
||||||
|
├── pdf.js
|
||||||
|
├── getCookies.js
|
||||||
|
├── setCookies.js
|
||||||
|
├── clearCookies.js
|
||||||
|
├── getStorage.js
|
||||||
|
├── setStorage.js
|
||||||
|
├── clearStorage.js
|
||||||
|
├── getNetworkLogs.js
|
||||||
|
├── blockResources.js
|
||||||
|
├── uploadFile.js
|
||||||
|
├── getPerformanceMetrics.js
|
||||||
|
└── wait.js
|
||||||
```
|
```
|
||||||
|
|
||||||
## Comportamento
|
|
||||||
|
|
||||||
- **Reuso de abas:** mesmo domínio reutiliza aba, domínios diferentes abrem nova aba
|
|
||||||
- **Headed mode:** navegador abre visível. Para headless, edite `browser.js` e mude `headless: true`
|
|
||||||
- **Singleton:** uma única instância do navegador compartilhada entre todas as tools
|
|
||||||
|
|
||||||
## Solução de problemas
|
|
||||||
|
|
||||||
| Problema | Solução |
|
|
||||||
|----------|---------|
|
|
||||||
| Tools não aparecem | Reiniciar a IDE após configurar MCP |
|
|
||||||
| `node: not found` | Instalar Node.js 18+ e adicionar ao PATH |
|
|
||||||
| Chromium não encontrado | `npx playwright install chromium` |
|
|
||||||
| Erro de permissão (Linux) | `sudo npx playwright install-deps chromium` |
|
|
||||||
| Caminho com espaços | Usar aspas no caminho |
|
|
||||||
|
|
||||||
## Caminhos por SO
|
|
||||||
|
|
||||||
| Sistema | Exemplo |
|
|
||||||
|---------|---------|
|
|
||||||
| Windows | `C:/Users/seuuser/mcp-browser/server.js` |
|
|
||||||
| macOS | `/Users/seuuser/mcp-browser/server.js` |
|
|
||||||
| Linux | `/home/seuuser/mcp-browser/server.js` |
|
|
||||||
|
|
||||||
> **Importante:** Nunca usar `\` simples. Sempre `/` ou `\\`.
|
|
||||||
|
|
||||||
## Licença
|
## Licença
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|||||||
+653
-38
@@ -2,64 +2,250 @@ const { chromium } = require('playwright');
|
|||||||
|
|
||||||
let browser = null;
|
let browser = null;
|
||||||
let context = null;
|
let context = null;
|
||||||
let page = null;
|
let pages = [];
|
||||||
|
let activePageIndex = 0;
|
||||||
|
let downloadsPath = null;
|
||||||
|
let networkLogs = [];
|
||||||
|
let blockedResources = [];
|
||||||
|
|
||||||
async function start() {
|
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;
|
if (browser) return;
|
||||||
if (page) return;
|
|
||||||
|
|
||||||
browser = await chromium.launch({
|
const {
|
||||||
headless: false,
|
headless = false,
|
||||||
args: ['--disable-blink-features=AutomationControlled']
|
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
context = await browser.newContext({
|
page.on('response', (res) => {
|
||||||
viewport: { width: 1280, height: 720 },
|
networkLogs.push({
|
||||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
type: 'response',
|
||||||
|
url: res.url(),
|
||||||
|
status: res.status(),
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
page = await context.newPage();
|
|
||||||
|
|
||||||
browser.on('disconnected', () => {
|
browser.on('disconnected', () => {
|
||||||
browser = null;
|
browser = null;
|
||||||
context = null;
|
context = null;
|
||||||
page = null;
|
pages = [];
|
||||||
|
activePageIndex = 0;
|
||||||
|
networkLogs = [];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensurePage() {
|
async function ensurePage() {
|
||||||
if (!browser) {
|
if (!browser) {
|
||||||
await start();
|
await start();
|
||||||
return page;
|
return pages[activePageIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!page || page.isClosed()) {
|
const currentPage = pages[activePageIndex];
|
||||||
page = await context.newPage();
|
if (!currentPage || currentPage.isClosed()) {
|
||||||
|
const newPage = await context.newPage();
|
||||||
|
pages[activePageIndex] = newPage;
|
||||||
|
setupPageListeners(newPage);
|
||||||
|
return newPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
return page;
|
return currentPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function goto(url) {
|
function setupPageListeners(page) {
|
||||||
const p = await ensurePage();
|
if (downloadsPath) {
|
||||||
await p.goto(url, {
|
page.on('download', async (download) => {
|
||||||
waitUntil: 'domcontentloaded',
|
const filePath = `${downloadsPath}/${download.suggestedFilename()}`;
|
||||||
timeout: 30000
|
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()
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await p.waitForTimeout(1500);
|
page.on('response', (res) => {
|
||||||
|
networkLogs.push({
|
||||||
|
type: 'response',
|
||||||
|
url: res.url(),
|
||||||
|
status: res.status(),
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function click(selector) {
|
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();
|
const p = await ensurePage();
|
||||||
await p.waitForSelector(selector, { state: 'visible', timeout: 10000 });
|
await p.waitForSelector(selector, { state: 'visible', timeout: 10000 });
|
||||||
|
|
||||||
|
if (options.clear) {
|
||||||
|
await p.fill(selector, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.slowly) {
|
||||||
await p.click(selector);
|
await p.click(selector);
|
||||||
}
|
await p.keyboard.type(text, { delay: options.delay || 50 });
|
||||||
|
} else {
|
||||||
async function type(selector, text) {
|
|
||||||
const p = await ensurePage();
|
|
||||||
await p.waitForSelector(selector, { state: 'visible', timeout: 10000 });
|
|
||||||
await p.fill(selector, text);
|
await p.fill(selector, text);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function content() {
|
async function content() {
|
||||||
@@ -72,29 +258,418 @@ async function title() {
|
|||||||
return await p.title();
|
return await p.title();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function screenshot(path, fullPage = true) {
|
async function currentUrl() {
|
||||||
const p = await ensurePage();
|
const p = await ensurePage();
|
||||||
await p.screenshot({ path, fullPage });
|
return p.url();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function newTab() {
|
async function screenshot(options = {}) {
|
||||||
const p = await context.newPage();
|
const p = await ensurePage();
|
||||||
page = p;
|
const {
|
||||||
return p;
|
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) {
|
||||||
|
const p = await ensurePage();
|
||||||
|
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;
|
||||||
|
return eval(expr);
|
||||||
|
}, { sel: selector, expr: expression });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hover(selector) {
|
||||||
|
const p = await ensurePage();
|
||||||
|
await p.waitForSelector(selector, { state: 'visible', timeout: 10000 });
|
||||||
|
await p.hover(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(() => {
|
||||||
|
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 };
|
||||||
|
});
|
||||||
|
|
||||||
|
return { origin: p.url(), ...storage };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setStorage(data) {
|
||||||
|
const p = await ensurePage();
|
||||||
|
|
||||||
|
await p.evaluate((storageData) => {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}, data);
|
||||||
|
|
||||||
|
return { set: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearStorage() {
|
||||||
|
const p = await ensurePage();
|
||||||
|
await p.evaluate(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
sessionStorage.clear();
|
||||||
|
});
|
||||||
|
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() {
|
async function close() {
|
||||||
if (browser) {
|
if (browser) {
|
||||||
try {
|
try {
|
||||||
await browser.close();
|
await browser.close();
|
||||||
} catch (e) {
|
} catch (e) { }
|
||||||
}
|
|
||||||
browser = null;
|
browser = null;
|
||||||
context = null;
|
context = null;
|
||||||
page = 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 = {
|
module.exports = {
|
||||||
start,
|
start,
|
||||||
goto,
|
goto,
|
||||||
@@ -102,13 +677,45 @@ module.exports = {
|
|||||||
type,
|
type,
|
||||||
content,
|
content,
|
||||||
title,
|
title,
|
||||||
|
currentUrl,
|
||||||
screenshot,
|
screenshot,
|
||||||
close,
|
close,
|
||||||
newTab,
|
newTab,
|
||||||
|
switchTab,
|
||||||
|
listTabs,
|
||||||
|
closeTab,
|
||||||
ensurePage,
|
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() {
|
get page() {
|
||||||
return page;
|
return pages[activePageIndex];
|
||||||
},
|
},
|
||||||
|
|
||||||
get browser() {
|
get browser() {
|
||||||
@@ -117,5 +724,13 @@ module.exports = {
|
|||||||
|
|
||||||
get context() {
|
get context() {
|
||||||
return context;
|
return context;
|
||||||
|
},
|
||||||
|
|
||||||
|
get tabs() {
|
||||||
|
return pages;
|
||||||
|
},
|
||||||
|
|
||||||
|
get activeTab() {
|
||||||
|
return activePageIndex;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+6
-5
@@ -1,12 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "opencode-mcp-browser",
|
"name": "opencode-mcp-browser",
|
||||||
"version": "1.0.0",
|
"version": "3.0.0",
|
||||||
"description": "",
|
"description": "MCP Browser - Automação de navegador de alta qualidade com 42 tools",
|
||||||
"main": "index.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"start": "node server.js",
|
||||||
|
"test": "echo \"Execute: node server.js\" && exit 0"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": ["mcp", "browser", "playwright", "automation", "scraping"],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
|
|||||||
@@ -1,108 +1,122 @@
|
|||||||
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
||||||
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
||||||
|
const { ListToolsRequestSchema, CallToolRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
|
||||||
|
|
||||||
const {
|
const tools = {
|
||||||
ListToolsRequestSchema,
|
openUrl: require('./tools/openUrl'),
|
||||||
CallToolRequestSchema
|
click: require('./tools/click'),
|
||||||
} = require('@modelcontextprotocol/sdk/types.js');
|
type: require('./tools/type'),
|
||||||
|
screenshot: require('./tools/screenshot'),
|
||||||
|
getText: require('./tools/getText'),
|
||||||
|
getLinks: require('./tools/getLinks'),
|
||||||
|
waitForSelector: require('./tools/waitForSelector'),
|
||||||
|
extractElements: require('./tools/extractElements'),
|
||||||
|
smartClick: require('./tools/smartClick'),
|
||||||
|
smartType: require('./tools/smartType'),
|
||||||
|
getButtons: require('./tools/getButtons'),
|
||||||
|
smartWaitNavigation: require('./tools/smartWaitNavigation'),
|
||||||
|
getForms: require('./tools/getForms'),
|
||||||
|
fillFormAuto: require('./tools/fillFormAuto'),
|
||||||
|
agentFlow: require('./tools/agentFlow'),
|
||||||
|
agentFlowV2: require('./tools/agentFlowV2'),
|
||||||
|
closeBrowser: require('./tools/closeBrowser'),
|
||||||
|
getURL: require('./tools/getURL'),
|
||||||
|
getTitle: require('./tools/getTitle'),
|
||||||
|
getHTML: require('./tools/getHTML'),
|
||||||
|
goBack: require('./tools/goBack'),
|
||||||
|
goForward: require('./tools/goForward'),
|
||||||
|
reload: require('./tools/reload'),
|
||||||
|
evaluateJS: require('./tools/evaluateJS'),
|
||||||
|
waitForURL: require('./tools/waitForURL'),
|
||||||
|
newTab: require('./tools/newTab'),
|
||||||
|
switchTab: require('./tools/switchTab'),
|
||||||
|
listTabs: require('./tools/listTabs'),
|
||||||
|
closeTab: require('./tools/closeTab'),
|
||||||
|
setViewport: require('./tools/setViewport'),
|
||||||
|
emulateDevice: require('./tools/emulateDevice'),
|
||||||
|
hover: require('./tools/hover'),
|
||||||
|
select: require('./tools/select'),
|
||||||
|
pressKey: require('./tools/pressKey'),
|
||||||
|
scrollTo: require('./tools/scrollTo'),
|
||||||
|
pdf: require('./tools/pdf'),
|
||||||
|
getCookies: require('./tools/getCookies'),
|
||||||
|
setCookies: require('./tools/setCookies'),
|
||||||
|
clearCookies: require('./tools/clearCookies'),
|
||||||
|
getStorage: require('./tools/getStorage'),
|
||||||
|
setStorage: require('./tools/setStorage'),
|
||||||
|
clearStorage: require('./tools/clearStorage'),
|
||||||
|
getNetworkLogs: require('./tools/getNetworkLogs'),
|
||||||
|
blockResources: require('./tools/blockResources'),
|
||||||
|
uploadFile: require('./tools/uploadFile'),
|
||||||
|
getPerformanceMetrics: require('./tools/getPerformanceMetrics'),
|
||||||
|
wait: require('./tools/wait')
|
||||||
|
};
|
||||||
|
|
||||||
// TOOLS
|
const server = new Server({ name: 'browser', version: '3.0.0' }, { capabilities: { tools: {} } });
|
||||||
const openUrl = require('./tools/openUrl');
|
|
||||||
const click = require('./tools/click');
|
|
||||||
const type = require('./tools/type');
|
|
||||||
const screenshot = require('./tools/screenshot');
|
|
||||||
const getText = require('./tools/getText');
|
|
||||||
const getLinks = require('./tools/getLinks');
|
|
||||||
const waitForSelector = require('./tools/waitForSelector');
|
|
||||||
const extractElements = require('./tools/extractElements');
|
|
||||||
const smartClick = require('./tools/smartClick');
|
|
||||||
const smartType = require('./tools/smartType');
|
|
||||||
const getButtons = require('./tools/getButtons');
|
|
||||||
const smartWaitNavigation = require('./tools/smartWaitNavigation');
|
|
||||||
const getForms = require('./tools/getForms');
|
|
||||||
const fillFormAuto = require('./tools/fillFormAuto');
|
|
||||||
const agentFlow = require('./tools/agentFlow');
|
|
||||||
const agentFlowV2 = require('./tools/agentFlowV2');
|
|
||||||
const closeBrowser = require('./tools/closeBrowser');
|
|
||||||
|
|
||||||
// SERVER MCP
|
const toolSchemas = [
|
||||||
const server = new Server(
|
{ name: 'open_url', description: 'Abre uma URL no navegador', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'URL para abrir' }, waitUntil: { type: 'string', enum: ['domcontentloaded', 'load', 'networkidle'] } }, required: ['url'] } },
|
||||||
{
|
{ name: 'click', description: 'Clica em elemento pelo seletor CSS', inputSchema: { type: 'object', properties: { selector: { type: 'string' }, button: { type: 'string', enum: ['left', 'right', 'middle'] }, double: { type: 'boolean' } }, required: ['selector'] } },
|
||||||
name: "browser",
|
{ name: 'type', description: 'Digita texto em campo', inputSchema: { type: 'object', properties: { selector: { type: 'string' }, text: { type: 'string' }, clear: { type: 'boolean' }, slowly: { type: 'boolean' } }, required: ['selector', 'text'] } },
|
||||||
version: "2.0.0"
|
{ name: 'screenshot', description: 'Captura screenshot', inputSchema: { type: 'object', properties: { fullPage: { type: 'boolean' }, element: { type: 'string' }, path: { type: 'string' } } } },
|
||||||
},
|
{ name: 'get_text', description: 'Extrai texto', inputSchema: { type: 'object', properties: { selector: { type: 'string' } } } },
|
||||||
{
|
{ name: 'get_links', description: 'Lista links', inputSchema: { type: 'object', properties: { selector: { type: 'string' } } } },
|
||||||
capabilities: {
|
{ name: 'wait_for_selector', description: 'Espera elemento', inputSchema: { type: 'object', properties: { selector: { type: 'string' }, timeout: { type: 'number' }, state: { type: 'string', enum: ['attached', 'detached', 'visible', 'hidden'] } }, required: ['selector'] } },
|
||||||
tools: {}
|
{ name: 'extract_elements', description: 'Extrai elementos com atributos', inputSchema: { type: 'object', properties: { selector: { type: 'string' }, attributes: { type: 'array', items: { type: 'string' } } }, required: ['selector'] } },
|
||||||
}
|
{ name: 'smart_click', description: 'Clica por texto visível', inputSchema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] } },
|
||||||
}
|
{ name: 'smart_type', description: 'Digita por label/placeholder', inputSchema: { type: 'object', properties: { label: { type: 'string' }, text: { type: 'string' } }, required: ['label', 'text'] } },
|
||||||
);
|
{ name: 'get_buttons', description: 'Lista botões', inputSchema: { type: 'object', properties: {} } },
|
||||||
|
{ name: 'smart_wait_navigation', description: 'Espera mudança de URL', inputSchema: { type: 'object', properties: { timeout: { type: 'number' } } } },
|
||||||
|
{ name: 'get_forms', description: 'Lista formulários', inputSchema: { type: 'object', properties: {} } },
|
||||||
|
{ name: 'fill_form_auto', description: 'Preenche formulário', inputSchema: { type: 'object', properties: { data: { type: 'object' } }, required: ['data'] } },
|
||||||
|
{ name: 'agent_flow', description: 'Fluxo automático simples', inputSchema: { type: 'object', properties: { goal: { type: 'string' }, data: { type: 'object' } }, required: ['goal'] } },
|
||||||
|
{ name: 'agent_flow_v2', description: 'Fluxo com retry', inputSchema: { type: 'object', properties: { goal: { type: 'string' }, data: { type: 'object' }, retries: { type: 'number' } }, required: ['goal'] } },
|
||||||
|
{ name: 'close_browser', description: 'Fecha navegador', inputSchema: { type: 'object', properties: {} } },
|
||||||
|
{ name: 'get_url', description: 'Obtém URL atual', inputSchema: { type: 'object', properties: {} } },
|
||||||
|
{ name: 'get_title', description: 'Obtém título', inputSchema: { type: 'object', properties: {} } },
|
||||||
|
{ name: 'get_html', description: 'Obtém HTML', inputSchema: { type: 'object', properties: { selector: { type: 'string' } } } },
|
||||||
|
{ name: 'go_back', description: 'Voltar no histórico', inputSchema: { type: 'object', properties: {} } },
|
||||||
|
{ name: 'go_forward', description: 'Avançar no histórico', inputSchema: { type: 'object', properties: {} } },
|
||||||
|
{ name: 'reload', description: 'Recarrega página', inputSchema: { type: 'object', properties: {} } },
|
||||||
|
{ name: 'evaluate_js', description: 'Executa JavaScript', inputSchema: { type: 'object', properties: { expression: { type: 'string' }, selector: { type: 'string' } }, required: ['expression'] } },
|
||||||
|
{ name: 'wait_for_url', description: 'Espera URL específica', inputSchema: { type: 'object', properties: { pattern: { type: 'string' }, timeout: { type: 'number' } }, required: ['pattern'] } },
|
||||||
|
{ name: 'new_tab', description: 'Nova aba', inputSchema: { type: 'object', properties: { url: { type: 'string' } } } },
|
||||||
|
{ name: 'switch_tab', description: 'Muda aba', inputSchema: { type: 'object', properties: { index: { type: 'number' } }, required: ['index'] } },
|
||||||
|
{ name: 'list_tabs', description: 'Lista abas', inputSchema: { type: 'object', properties: {} } },
|
||||||
|
{ name: 'close_tab', description: 'Fecha aba', inputSchema: { type: 'object', properties: { index: { type: 'number' } } } },
|
||||||
|
{ name: 'set_viewport', description: 'Define viewport', inputSchema: { type: 'object', properties: { width: { type: 'number' }, height: { type: 'number' } }, required: ['width', 'height'] } },
|
||||||
|
{ name: 'emulate_device', description: 'Emula dispositivo', inputSchema: { type: 'object', properties: { device: { type: 'string' } }, required: ['device'] } },
|
||||||
|
{ name: 'hover', description: 'Faz hover', inputSchema: { type: 'object', properties: { selector: { type: 'string' } }, required: ['selector'] } },
|
||||||
|
{ name: 'select', description: 'Seleciona opções', inputSchema: { type: 'object', properties: { selector: { type: 'string' }, values: { type: 'array', items: { type: 'string' } } }, required: ['selector', 'values'] } },
|
||||||
|
{ name: 'press_key', description: 'Pressiona tecla', inputSchema: { type: 'object', properties: { key: { type: 'string' }, selector: { type: 'string' } }, required: ['key'] } },
|
||||||
|
{ name: 'scroll_to', description: 'Rola página', inputSchema: { type: 'object', properties: { position: { oneOf: [{ type: 'number' }, { type: 'string', enum: ['top', 'bottom'] }, { type: 'object', properties: { selector: { type: 'string' } } }] } } } },
|
||||||
|
{ name: 'pdf', description: 'Gera PDF', inputSchema: { type: 'object', properties: { path: { type: 'string' }, format: { type: 'string' }, landscape: { type: 'boolean' } } } },
|
||||||
|
{ name: 'get_cookies', description: 'Obtém cookies', inputSchema: { type: 'object', properties: { urls: { type: 'array', items: { type: 'string' } } } } },
|
||||||
|
{ name: 'set_cookies', description: 'Define cookies', inputSchema: { type: 'object', properties: { cookies: { type: 'array' } }, required: ['cookies'] } },
|
||||||
|
{ name: 'clear_cookies', description: 'Limpa cookies', inputSchema: { type: 'object', properties: {} } },
|
||||||
|
{ name: 'get_storage', description: 'Obtém storage', inputSchema: { type: 'object', properties: {} } },
|
||||||
|
{ name: 'set_storage', description: 'Define storage', inputSchema: { type: 'object', properties: { localStorage: { type: 'object' }, sessionStorage: { type: 'object' } } } },
|
||||||
|
{ name: 'clear_storage', description: 'Limpa storage', inputSchema: { type: 'object', properties: {} } },
|
||||||
|
{ name: 'get_network_logs', description: 'Logs de rede', inputSchema: { type: 'object', properties: { type: { type: 'string' }, url: { type: 'string' } } } },
|
||||||
|
{ name: 'block_resources', description: 'Bloqueia recursos', inputSchema: { type: 'object', properties: { types: { type: 'array', items: { type: 'string' } } }, required: ['types'] } },
|
||||||
|
{ name: 'upload_file', description: 'Upload de arquivo', inputSchema: { type: 'object', properties: { selector: { type: 'string' }, filePaths: { oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }] } }, required: ['selector', 'filePaths'] } },
|
||||||
|
{ name: 'get_performance_metrics', description: 'Métricas de performance', inputSchema: { type: 'object', properties: {} } },
|
||||||
|
{ name: 'wait', description: 'Aguarda ms', inputSchema: { type: 'object', properties: { ms: { type: 'number' } }, required: ['ms'] } }
|
||||||
|
];
|
||||||
|
|
||||||
// LISTAR TOOLS
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: toolSchemas }));
|
||||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
||||||
return {
|
|
||||||
tools: [
|
|
||||||
{ name: "open_url", description: "Abre uma URL", inputSchema: { type: "object", properties: { url: { type: "string" } }, required: ["url"] } },
|
|
||||||
{ name: "click", description: "Clica por seletor", inputSchema: { type: "object", properties: { selector: { type: "string" } }, required: ["selector"] } },
|
|
||||||
{ name: "type", description: "Digita texto", inputSchema: { type: "object", properties: { selector: { type: "string" }, text: { type: "string" } }, required: ["selector", "text"] } },
|
|
||||||
{ name: "screenshot", description: "Screenshot", inputSchema: { type: "object", properties: { fullPage: { type: "boolean" } } } },
|
|
||||||
{ name: "get_text", description: "Extrai texto", inputSchema: { type: "object", properties: { selector: { type: "string" } } } },
|
|
||||||
{ name: "get_links", description: "Lista links", inputSchema: { type: "object", properties: { selector: { type: "string" } } } },
|
|
||||||
{ name: "wait_for_selector", description: "Espera elemento", inputSchema: { type: "object", properties: { selector: { type: "string" }, timeout: { type: "number" } }, required: ["selector"] } },
|
|
||||||
{ name: "extract_elements", description: "Extrai elementos", inputSchema: { type: "object", properties: { selector: { type: "string" }, attributes: { type: "array", items: { type: "string" } } }, required: ["selector"] } },
|
|
||||||
{ name: "smart_click", description: "Clica por texto visível", inputSchema: { type: "object", properties: { text: { type: "string" } }, required: ["text"] } },
|
|
||||||
{ name: "smart_type", description: "Digita por label/placeholder", inputSchema: { type: "object", properties: { label: { type: "string" }, text: { type: "string" } }, required: ["label", "text"] } },
|
|
||||||
{ name: "get_buttons", description: "Lista botões disponíveis", inputSchema: { type: "object", properties: {} } },
|
|
||||||
{ name: "smart_wait_navigation", description: "Espera mudança de página", inputSchema: { type: "object", properties: { timeout: { type: "number" } } } },
|
|
||||||
{ name: "get_forms", description: "Lista formulários da página", inputSchema: { type: "object", properties: {} } },
|
|
||||||
{ name: "fill_form_auto", description: "Preenche formulário automaticamente", inputSchema: { type: "object", properties: { data: { type: "object" } }, required: ["data"] } },
|
|
||||||
{ name: "agent_flow", description: "Fluxo automático simples", inputSchema: { type: "object", properties: { goal: { type: "string" }, data: { type: "object" } }, required: ["goal"] } },
|
|
||||||
{ name: "agent_flow_v2", description: "Fluxo automático avançado com retry e fallback", inputSchema: { type: "object", properties: { goal: { type: "string" }, data: { type: "object" }, retries: { type: "number" } }, required: ["goal"] } },
|
|
||||||
{ name: "close_browser", description: "Fecha o navegador", inputSchema: { type: "object", properties: {} } }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// EXECUTAR TOOL
|
|
||||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
const { name, arguments: args } = request.params;
|
const { name, arguments: args } = request.params;
|
||||||
|
const tool = tools[name.replace(/-/g, '').replace(/_([a-z])/g, (_, c) => c.toUpperCase())] || tools[name];
|
||||||
const tools = {
|
if (!tool) throw new Error(`Tool "${name}" não encontrada`);
|
||||||
open_url: openUrl,
|
try {
|
||||||
click,
|
const result = await tool(args || {});
|
||||||
type,
|
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
||||||
screenshot,
|
} catch (error) {
|
||||||
get_text: getText,
|
return { content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }], isError: true };
|
||||||
get_links: getLinks,
|
|
||||||
wait_for_selector: waitForSelector,
|
|
||||||
extract_elements: extractElements,
|
|
||||||
smart_click: smartClick,
|
|
||||||
smart_type: smartType,
|
|
||||||
get_buttons: getButtons,
|
|
||||||
smart_wait_navigation: smartWaitNavigation,
|
|
||||||
get_forms: getForms,
|
|
||||||
fill_form_auto: fillFormAuto,
|
|
||||||
agent_flow: agentFlow,
|
|
||||||
agent_flow_v2: agentFlowV2,
|
|
||||||
close_browser: closeBrowser
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!tools[name]) {
|
|
||||||
throw new Error("Tool não encontrada");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await tools[name](args || {});
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: JSON.stringify(result)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// START SERVER
|
|
||||||
const transport = new StdioServerTransport();
|
const transport = new StdioServerTransport();
|
||||||
server.connect(transport);
|
server.connect(transport).catch(console.error);
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async ({ types }) => {
|
||||||
|
if (!types || !Array.isArray(types)) throw new Error('Types array é obrigatório');
|
||||||
|
await browser.start();
|
||||||
|
const result = browser.blockResources(types);
|
||||||
|
return { success: true, action: 'block_resources', ...result };
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async () => {
|
||||||
|
await browser.start();
|
||||||
|
const result = await browser.clearCookies();
|
||||||
|
return { success: true, action: 'clear_cookies', ...result };
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async () => {
|
||||||
|
await browser.start();
|
||||||
|
await browser.clearStorage();
|
||||||
|
return { success: true, action: 'clear_storage', cleared: true };
|
||||||
|
};
|
||||||
+4
-15
@@ -1,19 +1,8 @@
|
|||||||
const browser = require('../browser');
|
const browser = require('../browser');
|
||||||
|
|
||||||
module.exports = async ({ selector }) => {
|
module.exports = async ({ selector, button = 'left', double = false }) => {
|
||||||
if (!selector) {
|
if (!selector) throw new Error('Selector é obrigatório');
|
||||||
throw new Error('Selector é obrigatório');
|
|
||||||
}
|
|
||||||
|
|
||||||
await browser.start();
|
await browser.start();
|
||||||
const p = await browser.ensurePage();
|
await browser.click(selector, { button, clickCount: double ? 2 : 1 });
|
||||||
|
return { success: true, action: 'click', selector, button, double };
|
||||||
await p.waitForSelector(selector, { state: 'visible', timeout: 10000 });
|
|
||||||
await p.click(selector);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: "click",
|
|
||||||
selector
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async ({ index = null }) => {
|
||||||
|
await browser.start();
|
||||||
|
const result = await browser.closeTab(index);
|
||||||
|
return { success: true, action: 'close_tab', ...result };
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async ({ device }) => {
|
||||||
|
if (!device) throw new Error('Device é obrigatório');
|
||||||
|
await browser.start();
|
||||||
|
const result = await browser.emulateDevice(device);
|
||||||
|
return { success: true, action: 'emulate_device', ...result };
|
||||||
|
};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async ({ expression, selector = null }) => {
|
||||||
|
if (!expression) throw new Error('Expression é obrigatória');
|
||||||
|
await browser.start();
|
||||||
|
if (selector) {
|
||||||
|
const result = await browser.evaluateOnSelector(selector, expression);
|
||||||
|
return { success: true, action: 'evaluate_js', selector, result };
|
||||||
|
}
|
||||||
|
const result = await browser.evaluateJS(expression);
|
||||||
|
return { success: true, action: 'evaluate_js', result };
|
||||||
|
};
|
||||||
@@ -1,36 +1,15 @@
|
|||||||
const browser = require('../browser');
|
const browser = require('../browser');
|
||||||
|
|
||||||
module.exports = async ({ selector, attributes = [] }) => {
|
module.exports = async ({ selector, attributes = [] }) => {
|
||||||
if (!selector) {
|
if (!selector) throw new Error('Selector é obrigatório');
|
||||||
throw new Error('Selector é obrigatório');
|
|
||||||
}
|
|
||||||
|
|
||||||
await browser.start();
|
await browser.start();
|
||||||
const p = await browser.ensurePage();
|
const attrs = Array.isArray(attributes) ? attributes : [];
|
||||||
|
const elements = await browser.evaluateJS(({ sel, attrs: a }) => {
|
||||||
const attrsStr = JSON.stringify(Array.isArray(attributes) ? attributes : []);
|
return Array.from(document.querySelectorAll(sel)).map(el => {
|
||||||
|
const data = { text: (el.innerText || el.textContent || '').trim() };
|
||||||
const elements = await p.evaluate((sel) => {
|
a.forEach(attr => data[attr] = el.getAttribute(attr));
|
||||||
const attrs = JSON.parse(sel.__attrs);
|
|
||||||
const els = Array.from(document.querySelectorAll(sel.__sel));
|
|
||||||
|
|
||||||
return els.map(el => {
|
|
||||||
const data = {
|
|
||||||
text: (el.innerText || el.textContent || '').trim()
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let i = 0; i < attrs.length; i++) {
|
|
||||||
data[attrs[i]] = el.getAttribute(attrs[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
});
|
});
|
||||||
}, { __sel: selector, __attrs: attrsStr });
|
}, { sel: selector, attrs });
|
||||||
|
return { success: true, action: 'extract_elements', count: elements.length, elements };
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: "extract_elements",
|
|
||||||
count: elements.length,
|
|
||||||
elements
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
+12
-43
@@ -1,62 +1,31 @@
|
|||||||
const browser = require('../browser');
|
const browser = require('../browser');
|
||||||
|
|
||||||
module.exports = async ({ data }) => {
|
module.exports = async ({ data }) => {
|
||||||
if (!data) {
|
if (!data) throw new Error('Data é obrigatória');
|
||||||
throw new Error('Data é obrigatória');
|
|
||||||
}
|
|
||||||
|
|
||||||
await browser.start();
|
await browser.start();
|
||||||
const p = await browser.ensurePage();
|
const result = await browser.evaluateJS((formData) => {
|
||||||
|
|
||||||
const result = await p.evaluate((formData) => {
|
|
||||||
const inputs = Array.from(document.querySelectorAll('input, textarea, select'));
|
const inputs = Array.from(document.querySelectorAll('input, textarea, select'));
|
||||||
|
const filled = [];
|
||||||
let filled = [];
|
|
||||||
|
|
||||||
inputs.forEach(input => {
|
inputs.forEach(input => {
|
||||||
const key = Object.keys(formData).find(k =>
|
const key = Object.keys(formData).find(k =>
|
||||||
(input.name && input.name.toLowerCase().includes(k.toLowerCase())) ||
|
(input.name && input.name.toLowerCase().includes(k.toLowerCase())) ||
|
||||||
(input.placeholder && input.placeholder.toLowerCase().includes(k.toLowerCase())) ||
|
(input.placeholder || '').toLowerCase().includes(k.toLowerCase()) ||
|
||||||
(input.id && input.id.toLowerCase().includes(k.toLowerCase())) ||
|
(input.id || '').toLowerCase().includes(k.toLowerCase()) ||
|
||||||
(input.getAttribute('aria-label') || '').toLowerCase().includes(k.toLowerCase())
|
(input.getAttribute('aria-label') || '').toLowerCase().includes(k.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
if (key) {
|
if (key) {
|
||||||
const value = formData[key];
|
const value = formData[key];
|
||||||
|
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
|
||||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
const nativeTextAreaSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
|
||||||
window.HTMLInputElement.prototype, 'value'
|
if (input.tagName === 'INPUT' && nativeSetter) nativeSetter.call(input, value);
|
||||||
)?.set;
|
else if (input.tagName === 'TEXTAREA' && nativeTextAreaSetter) nativeTextAreaSetter.call(input, value);
|
||||||
|
else input.value = value;
|
||||||
const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(
|
|
||||||
window.HTMLTextAreaElement.prototype, 'value'
|
|
||||||
)?.set;
|
|
||||||
|
|
||||||
if (input.tagName === 'INPUT' && nativeInputValueSetter) {
|
|
||||||
nativeInputValueSetter.call(input, value);
|
|
||||||
} else if (input.tagName === 'TEXTAREA' && nativeTextAreaValueSetter) {
|
|
||||||
nativeTextAreaValueSetter.call(input, value);
|
|
||||||
} else {
|
|
||||||
input.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
filled.push({ field: input.name || input.id || input.placeholder || 'unknown', value });
|
||||||
filled.push({
|
|
||||||
field: input.name || input.id || input.placeholder || 'unknown',
|
|
||||||
value: value
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return filled;
|
return filled;
|
||||||
}, data);
|
}, data);
|
||||||
|
return { success: true, action: 'fill_form_auto', filled: result, count: result.length };
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: "fill_form_auto",
|
|
||||||
filled: result,
|
|
||||||
count: result.length
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
+5
-17
@@ -2,22 +2,10 @@ const browser = require('../browser');
|
|||||||
|
|
||||||
module.exports = async () => {
|
module.exports = async () => {
|
||||||
await browser.start();
|
await browser.start();
|
||||||
const p = await browser.ensurePage();
|
const buttons = await browser.evaluateJS(() => {
|
||||||
|
return Array.from(document.querySelectorAll('button, input[type="submit"], input[type="button"], [role="button"], a.btn, a.button'))
|
||||||
const buttons = await p.evaluate(() => {
|
.map(el => ({ text: (el.innerText || el.value || el.getAttribute('aria-label') || '').trim(), type: el.tagName.toLowerCase(), id: el.id || null }))
|
||||||
const elements = Array.from(document.querySelectorAll('button, input[type="submit"], input[type="button"], [role="button"], a.btn, a.button'));
|
.filter(b => b.text !== '');
|
||||||
|
|
||||||
return elements.map(el => ({
|
|
||||||
text: (el.innerText || el.value || el.getAttribute('aria-label') || '').trim(),
|
|
||||||
type: el.tagName.toLowerCase(),
|
|
||||||
id: el.id || null
|
|
||||||
})).filter(b => b.text !== '');
|
|
||||||
});
|
});
|
||||||
|
return { success: true, action: 'get_buttons', count: buttons.length, buttons };
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: "get_buttons",
|
|
||||||
count: buttons.length,
|
|
||||||
buttons
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async ({ urls = [] }) => {
|
||||||
|
await browser.start();
|
||||||
|
const cookies = await browser.getCookies(urls);
|
||||||
|
return { success: true, action: 'get_cookies', count: cookies.length, cookies };
|
||||||
|
};
|
||||||
+6
-23
@@ -2,31 +2,14 @@ const browser = require('../browser');
|
|||||||
|
|
||||||
module.exports = async () => {
|
module.exports = async () => {
|
||||||
await browser.start();
|
await browser.start();
|
||||||
const p = await browser.ensurePage();
|
const forms = await browser.evaluateJS(() => {
|
||||||
|
return Array.from(document.querySelectorAll('form')).map(form => ({
|
||||||
const forms = await p.evaluate(() => {
|
|
||||||
const formElements = Array.from(document.querySelectorAll('form'));
|
|
||||||
|
|
||||||
return formElements.map(form => {
|
|
||||||
const inputs = Array.from(form.querySelectorAll('input, textarea, select'));
|
|
||||||
|
|
||||||
return {
|
|
||||||
action: form.action || null,
|
action: form.action || null,
|
||||||
method: form.method || 'GET',
|
method: form.method || 'GET',
|
||||||
fields: inputs.map(input => ({
|
fields: Array.from(form.querySelectorAll('input, textarea, select')).map(input => ({
|
||||||
name: input.name || null,
|
name: input.name || null, type: input.type || input.tagName.toLowerCase(), placeholder: input.placeholder || '', id: input.id || null
|
||||||
type: input.type || input.tagName.toLowerCase(),
|
|
||||||
placeholder: input.placeholder || '',
|
|
||||||
id: input.id || null
|
|
||||||
}))
|
}))
|
||||||
};
|
}));
|
||||||
});
|
});
|
||||||
});
|
return { success: true, action: 'get_forms', count: forms.length, forms };
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: "get_forms",
|
|
||||||
count: forms.length,
|
|
||||||
forms
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async ({ selector = null }) => {
|
||||||
|
await browser.start();
|
||||||
|
if (selector) {
|
||||||
|
const html = await browser.evaluateJS((sel) => document.querySelector(sel)?.outerHTML || null, selector);
|
||||||
|
return { success: true, action: 'get_html', selector, html };
|
||||||
|
}
|
||||||
|
const html = await browser.content();
|
||||||
|
return { success: true, action: 'get_html', html: html.substring(0, 50000) };
|
||||||
|
};
|
||||||
+4
-14
@@ -2,21 +2,11 @@ const browser = require('../browser');
|
|||||||
|
|
||||||
module.exports = async ({ selector = 'a' }) => {
|
module.exports = async ({ selector = 'a' }) => {
|
||||||
await browser.start();
|
await browser.start();
|
||||||
const p = await browser.ensurePage();
|
const links = await browser.evaluateJS((sel) => {
|
||||||
|
return Array.from(document.querySelectorAll(sel)).map(el => ({
|
||||||
const links = await p.evaluate((sel) => {
|
|
||||||
const elements = Array.from(document.querySelectorAll(sel));
|
|
||||||
|
|
||||||
return elements.map(el => ({
|
|
||||||
text: (el.innerText || el.textContent || '').trim(),
|
text: (el.innerText || el.textContent || '').trim(),
|
||||||
href: el.href || el.getAttribute('href') || ''
|
href: el.href || el.getAttribute('href') || ''
|
||||||
})).filter(link => link.href && link.href !== '');
|
})).filter(link => link.href);
|
||||||
}, selector);
|
}, selector);
|
||||||
|
return { success: true, action: 'get_links', count: links.length, links };
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: "get_links",
|
|
||||||
count: links.length,
|
|
||||||
links
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async ({ type = null, url = null, method = null }) => {
|
||||||
|
await browser.start();
|
||||||
|
const filter = {};
|
||||||
|
if (type) filter.type = type;
|
||||||
|
if (url) filter.url = url;
|
||||||
|
if (method) filter.method = method;
|
||||||
|
const logs = Object.keys(filter).length > 0 ? await browser.getNetworkLogs(filter) : await browser.getNetworkLogs();
|
||||||
|
return { success: true, action: 'get_network_logs', count: logs.length, logs };
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async () => {
|
||||||
|
await browser.start();
|
||||||
|
const metrics = await browser.getPerformanceMetrics();
|
||||||
|
return { success: true, action: 'get_performance_metrics', metrics };
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async () => {
|
||||||
|
await browser.start();
|
||||||
|
const storage = await browser.getStorage();
|
||||||
|
return { success: true, action: 'get_storage', ...storage };
|
||||||
|
};
|
||||||
+3
-13
@@ -2,20 +2,10 @@ const browser = require('../browser');
|
|||||||
|
|
||||||
module.exports = async ({ selector = 'body' }) => {
|
module.exports = async ({ selector = 'body' }) => {
|
||||||
await browser.start();
|
await browser.start();
|
||||||
const p = await browser.ensurePage();
|
const text = await browser.evaluateJS((sel) => {
|
||||||
|
|
||||||
const text = await p.evaluate((sel) => {
|
|
||||||
const el = document.querySelector(sel);
|
const el = document.querySelector(sel);
|
||||||
if (!el) return null;
|
if (!el) return null;
|
||||||
|
return (el.innerText || el.textContent || '').trim();
|
||||||
const t = el.innerText || el.textContent || '';
|
|
||||||
return t.trim() || null;
|
|
||||||
}, selector);
|
}, selector);
|
||||||
|
return { success: true, action: 'get_text', selector, text };
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: "get_text",
|
|
||||||
selector,
|
|
||||||
text
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async () => {
|
||||||
|
await browser.start();
|
||||||
|
const title = await browser.title();
|
||||||
|
return { success: true, action: 'get_title', title };
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async () => {
|
||||||
|
await browser.start();
|
||||||
|
const url = await browser.currentUrl();
|
||||||
|
return { success: true, action: 'get_url', url };
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async () => {
|
||||||
|
await browser.start();
|
||||||
|
const url = await browser.goBack();
|
||||||
|
return { success: true, action: 'go_back', url };
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async () => {
|
||||||
|
await browser.start();
|
||||||
|
const url = await browser.goForward();
|
||||||
|
return { success: true, action: 'go_forward', url };
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async ({ selector }) => {
|
||||||
|
if (!selector) throw new Error('Selector é obrigatório');
|
||||||
|
await browser.start();
|
||||||
|
await browser.hover(selector);
|
||||||
|
return { success: true, action: 'hover', selector };
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async () => {
|
||||||
|
await browser.start();
|
||||||
|
const tabs = browser.listTabs();
|
||||||
|
const tabsWithTitle = await Promise.all(tabs.map(async (t, i) => {
|
||||||
|
if (!t.isClosed) {
|
||||||
|
try {
|
||||||
|
const p = browser.tabs[i];
|
||||||
|
t.title = await p.title();
|
||||||
|
} catch (e) { t.title = null; }
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
}));
|
||||||
|
return { success: true, action: 'list_tabs', tabs: tabsWithTitle };
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async ({ url = null }) => {
|
||||||
|
await browser.start();
|
||||||
|
await browser.newTab(url);
|
||||||
|
const p = browser.page;
|
||||||
|
const finalUrl = url || p.url();
|
||||||
|
const title = await p.title();
|
||||||
|
return { success: true, action: 'new_tab', url: finalUrl, title };
|
||||||
|
};
|
||||||
+14
-46
@@ -1,56 +1,24 @@
|
|||||||
const browser = require('../browser');
|
const browser = require('../browser');
|
||||||
|
|
||||||
module.exports = async ({ url }) => {
|
module.exports = async ({ url, waitUntil = 'domcontentloaded' }) => {
|
||||||
if (!url) {
|
if (!url) throw new Error('URL é obrigatória');
|
||||||
throw new Error('URL é obrigatória');
|
|
||||||
}
|
|
||||||
|
|
||||||
await browser.start();
|
await browser.start();
|
||||||
const p = await browser.ensurePage();
|
const p = await browser.ensurePage();
|
||||||
|
|
||||||
const currentUrl = p.url();
|
const currentUrl = p.url();
|
||||||
|
|
||||||
if (currentUrl && currentUrl !== 'about:blank') {
|
if (currentUrl && currentUrl !== 'about:blank') {
|
||||||
if (currentUrl.includes(new URL(url).hostname)) {
|
try {
|
||||||
await p.goto(url, {
|
const targetUrl = new URL(url);
|
||||||
waitUntil: 'domcontentloaded',
|
if (currentUrl.includes(targetUrl.hostname)) {
|
||||||
timeout: 30000
|
const result = await browser.goto(url, { waitUntil });
|
||||||
});
|
return { success: true, ...result, tab: 'reused' };
|
||||||
await p.waitForTimeout(1500);
|
}
|
||||||
|
} catch (e) {}
|
||||||
return {
|
await browser.newTab(url);
|
||||||
success: true,
|
const newPage = browser.page;
|
||||||
url,
|
return { success: true, url, title: await newPage.title(), tab: 'new' };
|
||||||
title: await p.title(),
|
|
||||||
tab: 'reused'
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPage = await browser.newTab();
|
const result = await browser.goto(url, { waitUntil });
|
||||||
await newPage.goto(url, {
|
return { success: true, ...result, tab: 'existing' };
|
||||||
waitUntil: 'domcontentloaded',
|
|
||||||
timeout: 30000
|
|
||||||
});
|
|
||||||
await newPage.waitForTimeout(1500);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
url,
|
|
||||||
title: await newPage.title(),
|
|
||||||
tab: 'new'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
await p.goto(url, {
|
|
||||||
waitUntil: 'domcontentloaded',
|
|
||||||
timeout: 30000
|
|
||||||
});
|
|
||||||
await p.waitForTimeout(1500);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
url,
|
|
||||||
title: await p.title(),
|
|
||||||
tab: 'existing'
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async ({ path = null, format = 'A4', landscape = false }) => {
|
||||||
|
await browser.start();
|
||||||
|
const file = await browser.pdf({ path, format, landscape });
|
||||||
|
return { success: true, action: 'pdf', file };
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async ({ key, selector = null }) => {
|
||||||
|
if (!key) throw new Error('Key é obrigatória');
|
||||||
|
await browser.start();
|
||||||
|
await browser.pressKey(key, { selector });
|
||||||
|
return { success: true, action: 'press_key', key, selector };
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async () => {
|
||||||
|
await browser.start();
|
||||||
|
const url = await browser.reload();
|
||||||
|
return { success: true, action: 'reload', url };
|
||||||
|
};
|
||||||
+3
-12
@@ -1,16 +1,7 @@
|
|||||||
const browser = require('../browser');
|
const browser = require('../browser');
|
||||||
|
|
||||||
module.exports = async ({ fullPage = true }) => {
|
module.exports = async ({ fullPage = true, element = null, path: customPath = null }) => {
|
||||||
await browser.start();
|
await browser.start();
|
||||||
const p = await browser.ensurePage();
|
const file = await browser.screenshot({ fullPage, path: customPath, element });
|
||||||
|
return { success: true, action: 'screenshot', file };
|
||||||
const path = `screenshot-${Date.now()}.png`;
|
|
||||||
|
|
||||||
await p.screenshot({ path, fullPage });
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: "screenshot",
|
|
||||||
file: path
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async ({ position }) => {
|
||||||
|
if (!position) throw new Error('Position é obrigatória');
|
||||||
|
await browser.start();
|
||||||
|
await browser.scrollTo(position);
|
||||||
|
return { success: true, action: 'scroll_to', position };
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async ({ selector, values }) => {
|
||||||
|
if (!selector || !values) throw new Error('Selector e values são obrigatórios');
|
||||||
|
await browser.start();
|
||||||
|
const result = await browser.select(selector, values);
|
||||||
|
return { success: true, action: 'select', selector, selected: result };
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async ({ cookies }) => {
|
||||||
|
if (!cookies || !Array.isArray(cookies)) throw new Error('Cookies array é obrigatório');
|
||||||
|
await browser.start();
|
||||||
|
const result = await browser.setCookies(cookies);
|
||||||
|
return { success: true, action: 'set_cookies', ...result };
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async ({ localStorage = {}, sessionStorage = {} }) => {
|
||||||
|
await browser.start();
|
||||||
|
const data = {};
|
||||||
|
if (Object.keys(localStorage).length > 0) data.localStorage = localStorage;
|
||||||
|
if (Object.keys(sessionStorage).length > 0) data.sessionStorage = sessionStorage;
|
||||||
|
await browser.setStorage(data);
|
||||||
|
return { success: true, action: 'set_storage', set: true };
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async ({ width, height }) => {
|
||||||
|
if (width === undefined || height === undefined) throw new Error('Width e height são obrigatórios');
|
||||||
|
await browser.start();
|
||||||
|
await browser.setViewport(width, height);
|
||||||
|
return { success: true, action: 'set_viewport', width, height };
|
||||||
|
};
|
||||||
+7
-34
@@ -1,54 +1,27 @@
|
|||||||
const browser = require('../browser');
|
const browser = require('../browser');
|
||||||
|
|
||||||
module.exports = async ({ text }) => {
|
module.exports = async ({ text }) => {
|
||||||
if (!text) {
|
if (!text) throw new Error('Texto é obrigatório');
|
||||||
throw new Error('Texto é obrigatório');
|
|
||||||
}
|
|
||||||
|
|
||||||
await browser.start();
|
await browser.start();
|
||||||
const p = await browser.ensurePage();
|
const p = await browser.ensurePage();
|
||||||
|
|
||||||
const clicked = await p.evaluate((targetText) => {
|
const clicked = await p.evaluate((targetText) => {
|
||||||
const elements = Array.from(document.querySelectorAll(
|
const elements = Array.from(document.querySelectorAll('a, button, input[type="submit"], input[type="button"], [role="button"], [onclick]'));
|
||||||
'a, button, input[type="submit"], input[type="button"], [role="button"], [onclick]'
|
|
||||||
));
|
|
||||||
|
|
||||||
const el = elements.find(e => {
|
const el = elements.find(e => {
|
||||||
const txt = (e.innerText || e.textContent || e.value || e.getAttribute('aria-label') || '').trim().toLowerCase();
|
const txt = (e.innerText || e.textContent || e.value || e.getAttribute('aria-label') || '').trim().toLowerCase();
|
||||||
return txt.includes(targetText.toLowerCase());
|
return txt.includes(targetText.toLowerCase());
|
||||||
});
|
});
|
||||||
|
|
||||||
if (el) {
|
if (el) {
|
||||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
el.click();
|
el.click();
|
||||||
|
return { clicked: true, href: el.tagName === 'A' ? el.href : null };
|
||||||
if (el.tagName === 'A' && el.href) {
|
|
||||||
return { clicked: true, href: el.href };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { clicked: true, href: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { clicked: false, href: null };
|
return { clicked: false, href: null };
|
||||||
}, text);
|
}, text);
|
||||||
|
|
||||||
if (!clicked.clicked) {
|
if (!clicked.clicked) throw new Error(`Elemento não encontrado: "${text}"`);
|
||||||
throw new Error(`Elemento não encontrado pelo texto: "${text}"`);
|
if (clicked.href) { try { await p.waitForNavigation({ timeout: 5000, waitUntil: 'domcontentloaded' }); } catch (e) {} }
|
||||||
}
|
else await p.waitForTimeout(1000);
|
||||||
|
|
||||||
if (clicked.href) {
|
return { success: true, action: 'smart_click', text, navigatedTo: clicked.href };
|
||||||
try {
|
|
||||||
await p.waitForNavigation({ timeout: 5000, waitUntil: 'domcontentloaded' });
|
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await p.waitForTimeout(1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: "smart_click",
|
|
||||||
text,
|
|
||||||
navigatedTo: clicked.href || null
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
+16
-67
@@ -1,84 +1,33 @@
|
|||||||
const browser = require('../browser');
|
const browser = require('../browser');
|
||||||
|
|
||||||
module.exports = async ({ label, text }) => {
|
module.exports = async ({ label, text }) => {
|
||||||
if (!label || !text) {
|
if (!label || !text) throw new Error('Label e text são obrigatórios');
|
||||||
throw new Error('Label e text são obrigatórios');
|
|
||||||
}
|
|
||||||
|
|
||||||
await browser.start();
|
await browser.start();
|
||||||
const p = await browser.ensurePage();
|
const success = await browser.evaluateJS(({ labelText, inputText }) => {
|
||||||
|
const setInput = (input, value) => {
|
||||||
const success = await p.evaluate(({ labelText, inputText }) => {
|
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
|
||||||
function setInputValue(input, value) {
|
const nativeTextAreaSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
|
||||||
const nativeSetter = Object.getOwnPropertyDescriptor(
|
if (input.tagName === 'INPUT' && nativeSetter) nativeSetter.call(input, value);
|
||||||
window.HTMLInputElement.prototype, 'value'
|
else if (input.tagName === 'TEXTAREA' && nativeTextAreaSetter) nativeTextAreaSetter.call(input, value);
|
||||||
)?.set;
|
else input.value = value;
|
||||||
|
|
||||||
const nativeTextAreaSetter = Object.getOwnPropertyDescriptor(
|
|
||||||
window.HTMLTextAreaElement.prototype, 'value'
|
|
||||||
)?.set;
|
|
||||||
|
|
||||||
if (input.tagName === 'INPUT' && nativeSetter) {
|
|
||||||
nativeSetter.call(input, value);
|
|
||||||
} else if (input.tagName === 'TEXTAREA' && nativeTextAreaSetter) {
|
|
||||||
nativeTextAreaSetter.call(input, value);
|
|
||||||
} else {
|
|
||||||
input.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
}
|
};
|
||||||
|
const foundLabel = Array.from(document.querySelectorAll('label')).find(l => (l.innerText || l.textContent || '').toLowerCase().includes(labelText.toLowerCase()));
|
||||||
const labels = Array.from(document.querySelectorAll('label'));
|
|
||||||
|
|
||||||
const foundLabel = labels.find(l =>
|
|
||||||
(l.innerText || l.textContent || '').toLowerCase().includes(labelText.toLowerCase())
|
|
||||||
);
|
|
||||||
|
|
||||||
if (foundLabel) {
|
if (foundLabel) {
|
||||||
const forAttr = foundLabel.getAttribute('for');
|
const forAttr = foundLabel.getAttribute('for');
|
||||||
|
if (forAttr) { const input = document.getElementById(forAttr); if (input) { setInput(input, inputText); return true; } }
|
||||||
if (forAttr) {
|
const input = foundLabel.querySelector('input, textarea'); if (input) { setInput(input, inputText); return true; }
|
||||||
const input = document.getElementById(forAttr);
|
|
||||||
if (input) {
|
|
||||||
setInputValue(input, inputText);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
const foundInput = Array.from(document.querySelectorAll('input, textarea')).find(i =>
|
||||||
|
|
||||||
const input = foundLabel.querySelector('input, textarea');
|
|
||||||
if (input) {
|
|
||||||
setInputValue(input, inputText);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputs = Array.from(document.querySelectorAll('input, textarea'));
|
|
||||||
|
|
||||||
const foundInput = inputs.find(i =>
|
|
||||||
(i.placeholder || '').toLowerCase().includes(labelText.toLowerCase()) ||
|
(i.placeholder || '').toLowerCase().includes(labelText.toLowerCase()) ||
|
||||||
(i.name || '').toLowerCase().includes(labelText.toLowerCase()) ||
|
(i.name || '').toLowerCase().includes(labelText.toLowerCase()) ||
|
||||||
(i.id || '').toLowerCase().includes(labelText.toLowerCase()) ||
|
(i.id || '').toLowerCase().includes(labelText.toLowerCase()) ||
|
||||||
(i.getAttribute('aria-label') || '').toLowerCase().includes(labelText.toLowerCase())
|
(i.getAttribute('aria-label') || '').toLowerCase().includes(labelText.toLowerCase())
|
||||||
);
|
);
|
||||||
|
if (foundInput) { setInput(foundInput, inputText); return true; }
|
||||||
if (foundInput) {
|
|
||||||
setInputValue(foundInput, inputText);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}, { labelText: label, inputText: text });
|
}, { labelText: label, inputText: text });
|
||||||
|
if (!success) throw new Error('Campo não encontrado');
|
||||||
if (!success) {
|
return { success: true, action: 'smart_type', label, text };
|
||||||
throw new Error('Campo não encontrado');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: "smart_type",
|
|
||||||
label,
|
|
||||||
text
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,25 +2,9 @@ const browser = require('../browser');
|
|||||||
|
|
||||||
module.exports = async ({ timeout = 15000 }) => {
|
module.exports = async ({ timeout = 15000 }) => {
|
||||||
await browser.start();
|
await browser.start();
|
||||||
|
const oldUrl = await browser.currentUrl();
|
||||||
const p = await browser.ensurePage();
|
const p = await browser.ensurePage();
|
||||||
|
try { await Promise.race([p.waitForNavigation({ timeout, waitUntil: 'domcontentloaded' }), p.waitForLoadState('domcontentloaded', { timeout })]); } catch (e) {}
|
||||||
const oldUrl = p.url();
|
const newUrl = await browser.currentUrl();
|
||||||
|
return { success: true, action: 'smart_wait_navigation', oldUrl, newUrl, changed: oldUrl !== newUrl };
|
||||||
try {
|
|
||||||
await Promise.race([
|
|
||||||
p.waitForNavigation({ timeout, waitUntil: 'domcontentloaded' }),
|
|
||||||
p.waitForLoadState('domcontentloaded', { timeout })
|
|
||||||
]);
|
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
|
|
||||||
const newUrl = p.url();
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: "smart_wait_navigation",
|
|
||||||
oldUrl,
|
|
||||||
newUrl,
|
|
||||||
changed: oldUrl !== newUrl
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async ({ index }) => {
|
||||||
|
if (index === undefined) throw new Error('Index é obrigatório');
|
||||||
|
await browser.start();
|
||||||
|
await browser.switchTab(index);
|
||||||
|
const url = await browser.currentUrl();
|
||||||
|
const title = await browser.title();
|
||||||
|
return { success: true, action: 'switch_tab', index, url, title };
|
||||||
|
};
|
||||||
+4
-16
@@ -1,20 +1,8 @@
|
|||||||
const browser = require('../browser');
|
const browser = require('../browser');
|
||||||
|
|
||||||
module.exports = async ({ selector, text }) => {
|
module.exports = async ({ selector, text, clear = false, slowly = false }) => {
|
||||||
if (!selector || !text) {
|
if (!selector || !text) throw new Error('Selector e text são obrigatórios');
|
||||||
throw new Error('Selector e text são obrigatórios');
|
|
||||||
}
|
|
||||||
|
|
||||||
await browser.start();
|
await browser.start();
|
||||||
const p = await browser.ensurePage();
|
await browser.type(selector, text, { clear, slowly });
|
||||||
|
return { success: true, action: 'type', selector, text, clear, slowly };
|
||||||
await p.waitForSelector(selector, { state: 'visible', timeout: 10000 });
|
|
||||||
await p.fill(selector, text);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: "type",
|
|
||||||
selector,
|
|
||||||
text
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async ({ selector, filePaths }) => {
|
||||||
|
if (!selector) throw new Error('Selector é obrigatório');
|
||||||
|
if (!filePaths) throw new Error('FilePaths é obrigatório');
|
||||||
|
await browser.start();
|
||||||
|
const result = await browser.uploadFile(selector, filePaths);
|
||||||
|
return { success: true, action: 'upload_file', ...result };
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async ({ ms }) => {
|
||||||
|
if (!ms || typeof ms !== 'number') throw new Error('MS é obrigatório e deve ser número');
|
||||||
|
await browser.start();
|
||||||
|
await browser.waitForTimeout(ms);
|
||||||
|
return { success: true, action: 'wait', ms };
|
||||||
|
};
|
||||||
@@ -1,22 +1,9 @@
|
|||||||
const browser = require('../browser');
|
const browser = require('../browser');
|
||||||
|
|
||||||
module.exports = async ({ selector, timeout = 15000 }) => {
|
module.exports = async ({ selector, timeout = 15000, state = 'attached' }) => {
|
||||||
if (!selector) {
|
if (!selector) throw new Error('Selector é obrigatório');
|
||||||
throw new Error('Selector é obrigatório');
|
|
||||||
}
|
|
||||||
|
|
||||||
await browser.start();
|
await browser.start();
|
||||||
const p = await browser.ensurePage();
|
const p = await browser.ensurePage();
|
||||||
|
const el = await p.waitForSelector(selector, { timeout, state });
|
||||||
const el = await p.waitForSelector(selector, {
|
return { success: true, action: 'wait_for_selector', selector, found: !!el };
|
||||||
timeout,
|
|
||||||
state: 'attached'
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
action: "wait_for_selector",
|
|
||||||
selector,
|
|
||||||
found: !!el
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
const browser = require('../browser');
|
||||||
|
|
||||||
|
module.exports = async ({ pattern, timeout = 30000 }) => {
|
||||||
|
if (!pattern) throw new Error('Pattern é obrigatório');
|
||||||
|
await browser.start();
|
||||||
|
const url = await browser.waitForURL(pattern, { timeout });
|
||||||
|
return { success: true, action: 'wait_for_url', pattern, url };
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user