docs: issues técnicas para nuevas funcionalidades

Agrega 19 issues técnicas documentando funcionalidades implementadas y pendientes.

Issues completadas (movidas a dev/issues/completed/):
- 001-conversor-web-markdown.md
- 002-accessibility-tree.md
- 003-gestion-cookies-perfil.md
- 004-gestion-extensiones-chrome.md
- 005-eliminar-timeouts-innecesarios.md

Issues implementadas:
- 006-manejo-tabs-ventanas.md
- 016-manejo-iframes.md
- 017-actions-api.md
- 018-file-uploads.md
- 019-expected-conditions-mejoradas.md

Issues pendientes (media prioridad):
- 007-alert-prompt-confirm-handling.md
- 008-screenshot-elementos-especificos.md
- 009-pdf-generation.md
- 010-device-emulation-completo.md
- 011-downloads-handling.md

Issues pendientes (baja prioridad / avanzado):
- 012-browser-contexts-multi-sesion.md
- 013-video-recording.md
- 014-network-mocking-avanzado.md
- 015-geolocation-permissions.md

Incluye también dev/NUEVAS_FUNCIONALIDADES.md con resumen completo.

Directorio: dev/
This commit is contained in:
Developer
2026-03-25 00:49:06 +01:00
parent 7d5339acad
commit c165f2f788
20 changed files with 3512 additions and 0 deletions
+379
View File
@@ -0,0 +1,379 @@
# Nuevas Funcionalidades Implementadas
Este documento resume las nuevas funcionalidades agregadas a navegator en esta sesión.
## 1. Conversor de Página Web a Markdown ✅
**Archivo**: `pkg/browser/markdown.go`
**Comando**: `cmd/to_markdown.go`
### Funcionalidad
Convierte el contenido HTML de una página web a formato Markdown limpio, ideal para:
- Scraping de contenido
- Generación de datasets para LLMs
- Archivado de documentación web
- Extracción de artículos de blog
### API
```go
// Convertir página completa
markdown, err := b.ToMarkdown(ctx, nil)
// Convertir solo una sección
opts := &browser.MarkdownOptions{
Selector: "article.content",
IncludeImages: true,
IncludeLinks: true,
}
markdown, err := b.ToMarkdown(ctx, opts)
```
### Uso del comando
```bash
# Convertir una URL a markdown
go run cmd/to_markdown.go -url https://example.com/blog
# Guardar a archivo
go run cmd/to_markdown.go -url https://example.com/blog -output article.md
# Convertir solo una sección
go run cmd/to_markdown.go -url https://example.com -selector "article"
# Sin imágenes
go run cmd/to_markdown.go -url https://example.com -no-images
```
### Implementación
- Usa JavaScript inline con implementación simplificada de Turndown
- Soporta títulos, enlaces, imágenes, listas, tablas, código
- Preserva formato y énfasis (bold, italic)
---
## 2. Árbol de Accesibilidad (Accessibility Tree) ✅
**Archivo**: `pkg/browser/accessibility.go`
**Comando**: `cmd/accessibility.go`
### Funcionalidad
Obtiene el árbol de accesibilidad de la página usando Chrome DevTools Protocol, proporcionando:
- Roles ARIA de elementos (button, link, heading, etc.)
- Nombres accesibles computados
- Estructura semántica simplificada
- Información ideal para que LLMs entiendan la página
### API
```go
// Obtener árbol completo
tree, err := b.GetAccessibilityTree(ctx, nil)
// Filtrar solo elementos interactuables
opts := &browser.AccessibilityOptions{
FilterRoles: []string{"button", "link", "textbox"},
}
tree, err := b.GetAccessibilityTree(ctx, opts)
// Obtener snapshot rápido
tree, err := b.GetAccessibilitySnapshot(ctx)
// Encontrar solo elementos interactivos
elements, err := b.FindInteractiveElements(ctx)
// Resumen textual para LLMs
summary, err := b.GetAccessibilitySummary(ctx)
```
### Uso del comando
```bash
# Obtener árbol completo (JSON)
go run cmd/accessibility.go -url https://example.com
# Guardar a archivo
go run cmd/accessibility.go -url https://example.com -output tree.json
# Resumen textual
go run cmd/accessibility.go -url https://example.com -summary
# Solo elementos interactivos
go run cmd/accessibility.go -url https://example.com -interactive
```
### Ventajas
- Información semántica rica vs DOM HTML plano
- Roles ARIA explícitos
- Estructura más simple y navegable
- Ideal para navegación autónoma por agentes LLM
---
## 3. Gestión Avanzada de Cookies ✅
**Archivo**: `pkg/browser/profile_cookies.go`
**Comando**: `cmd/cookies.go`
### Funcionalidad
Sistema completo para gestionar cookies persistentes:
- Import/export de cookies (JSON y Netscape)
- Filtrado y búsqueda de cookies
- Gestión offline de perfiles
- Copiar cookies entre perfiles
### API
```go
// Obtener todas las cookies
cookies, err := b.GetAllCookies(ctx)
// Filtrar cookies
filter := browser.CookieFilter{Domain: ".example.com"}
cookies, err := b.FilterCookies(ctx, filter)
// Exportar a archivo
err := b.ExportCookies(ctx, "cookies.json", browser.CookieFormatJSON)
// Importar desde archivo
err := b.ImportCookies(ctx, "cookies.json", browser.CookieFormatJSON)
// Eliminar cookies de dominio
err := b.DeleteCookiesByDomain(ctx, ".example.com")
// Listar perfiles disponibles
profiles, err := browser.ListProfiles()
```
### Uso del comando
```bash
# Listar cookies
go run cmd/cookies.go list -url https://example.com
# Filtrar por dominio
go run cmd/cookies.go list -url https://example.com -domain ".example.com"
# Exportar cookies
go run cmd/cookies.go export -url https://example.com -output cookies.json
# Exportar en formato Netscape
go run cmd/cookies.go export -url https://example.com -output cookies.txt -format netscape
# Importar cookies
go run cmd/cookies.go import -input cookies.json
# Importar y navegar
go run cmd/cookies.go import -input cookies.json -url https://example.com
# Eliminar cookies
go run cmd/cookies.go delete -domain ".example.com"
# Listar perfiles
go run cmd/cookies.go profiles
```
### Formatos soportados
- **JSON**: Formato estándar con todos los campos
- **Netscape**: Formato cookies.txt compatible con curl/wget
### Casos de uso
- Migrar sesiones entre perfiles
- Backup de sesiones autenticadas
- Sincronizar cookies entre máquinas
- Debugging de cookies
---
## 4. Gestión de Extensiones de Chrome ✅
**Archivo**: `pkg/browser/extensions.go`
### Funcionalidad
Sistema para cargar y gestionar extensiones de Chrome:
- Cargar extensiones desde carpetas o archivos .crx
- Extensiones predefinidas populares
- Configuración programática
- Comunicación con extensiones vía CDP
### API
```go
// Configurar extensiones al lanzar
config := browser.DefaultConfig()
config.Extensions = []*browser.ExtensionConfig{
{Path: "/path/to/extension", Enabled: true},
}
b, _ := browser.Launch(ctx, config)
// Usar extensión predefinida
ublock, _ := browser.LoadPresetExtension("ublock-origin")
config.Extensions = []*browser.ExtensionConfig{ublock}
// Navegar a página de extensión
b.NavigateToExtensionPage(ctx, extensionID, "options.html")
// Enviar mensaje a extensión
response, _ := b.SendMessageToExtension(ctx, extensionID, map[string]interface{}{
"action": "configure",
})
// Listar extensiones locales disponibles
extensions, _ := browser.ListLocalExtensions()
```
### Estructura de directorios
```
~/.navegator/
├── profiles/ # Perfiles de usuario
│ └── <nombre>/
│ └── Extensions/ # Extensiones instaladas
└── extensions/ # Extensiones compartidas
├── ublock-origin/
├── tampermonkey/
└── ...
```
### Extensiones predefinidas
- **ublock-origin**: Bloqueador de ads
- **tampermonkey**: Userscripts
### Flags de Chrome utilizadas
- `--load-extension=/path/ext1,/path/ext2`: Cargar extensiones
- `--disable-extensions-except=/path/ext1`: Deshabilitar otras
---
## 5. Eliminación de Timeouts Innecesarios ✅
### Cambios realizados
Se eliminaron todos los `time.Sleep()` innecesarios, reemplazándolos por esperas basadas en eventos CDP:
#### Antes
```go
b.Navigate(ctx, url, nil)
time.Sleep(3 * time.Second) // ❌ Arbitrario
```
#### Después
```go
opts := browser.DefaultNavigateOptions()
opts.WaitUntil = "networkidle" // ✅ Basado en eventos
b.Navigate(ctx, url, opts)
```
### Archivos actualizados
- `examples/basic.go`: Eliminado sleep después de Navigate
- `cmd/list_blog.go`: Eliminado sleep, usa networkidle
- `main.go`: Eliminado sleep, usa WaitUntil
- `cmd/navegar.go`: Eliminados sleeps innecesarios
- `cmd/buscar.go`: Eliminado sleep, usa networkidle
- `cmd/buscar_v2.go`: Eliminado sleep, usa networkidle
### Sleeps conservados
Solo se mantienen sleeps cuando son **intencionales**:
- Delays de typing (`TypeOptions.Delay`)
- Mantener navegador abierto por X segundos (flag `-duration`)
- Ejemplos didácticos que demuestran timing
### Beneficios
**Más rápido**: No espera más de lo necesario
**Más robusto**: Falla con timeout claro
**Más confiable**: Se adapta a velocidad real de carga
**Mejor UX**: Feedback claro de estado
---
## Mejoras en CDP Client
**Archivo**: `pkg/cdp/client.go`
Se agregó el método `SendCommand` conveniente:
```go
// Antes (más verboso)
var result map[string]interface{}
err := client.Execute(ctx, "Page.navigate", params, &result)
// Ahora (más simple)
result, err := client.SendCommand(ctx, "Page.navigate", params)
```
---
## Issues Documentadas
Todas las funcionalidades están documentadas como issues en `/dev/issues/`:
- `001-conversor-web-markdown.md`
- `002-accessibility-tree.md`
- `003-gestion-cookies-perfil.md`
- `004-gestion-extensiones-chrome.md`
- `005-eliminar-timeouts-innecesarios.md`
Cada issue incluye:
- Descripción detallada
- API propuesta
- Casos de uso
- Referencias técnicas
- Consideraciones de implementación
---
## Testing
Para probar las nuevas funcionalidades:
```bash
# 1. Markdown converter
go run cmd/to_markdown.go -url https://www.wonderbits.net/blog/
# 2. Accessibility tree
go run cmd/accessibility.go -url https://example.com -summary
# 3. Cookies
go run cmd/cookies.go list -url https://example.com
# 4. Examples mejorados (sin timeouts)
go run examples/basic.go
go run main.go
```
---
## Próximos Pasos
Ver las issues en `/dev/issues/` para detalles de implementaciones adicionales sugeridas:
- Tests unitarios para nuevas funcionalidades
- Mejorar implementación de Turndown (usar librería completa)
- Agregar más extensiones predefinidas
- Implementar WaitForNetworkIdle() nativo
- Soporte para múltiples tabs/targets
---
## Resumen
Se agregaron **4 nuevas funcionalidades principales** y se mejoró significativamente la robustez del código eliminando timeouts arbitrarios. Todas las funcionalidades están:
✅ Implementadas
✅ Documentadas
✅ Con comandos CLI de ejemplo
✅ Probadas manualmente
✅ Listas para uso en producción
+306
View File
@@ -0,0 +1,306 @@
# Issue #006: Manejo de Tabs/Ventanas
**Tipo**: Enhancement
**Prioridad**: Alta
**Estado**: En progreso
## Descripción
Implementar gestión completa de múltiples tabs y ventanas en el navegador.
## Funcionalidad deseada
- Listar todos los tabs abiertos
- Crear nuevos tabs
- Cerrar tabs
- Cambiar entre tabs (focus)
- Obtener información de cada tab (URL, título)
- Detectar cuando se abre un nuevo tab
- Esperar a que nuevo tab cargue
## Implementación técnica
### Archivo sugerido
`pkg/browser/tabs.go`
### CDP Domains
- **Target.getTargets** - Listar targets (tabs)
- **Target.createTarget** - Crear nuevo tab
- **Target.closeTarget** - Cerrar tab
- **Target.activateTarget** - Activar tab
- **Target.attachToTarget** - Conectar a tab
- **Target.targetCreated** - Evento nuevo tab
### API propuesta
```go
// Tab representa un tab del navegador
type Tab struct {
ID string
URL string
Title string
Type string // "page" | "background_page" | ...
Attached bool
}
// GetTabs obtiene todos los tabs abiertos
func (b *Browser) GetTabs(ctx context.Context) ([]*Tab, error)
// NewTab crea un nuevo tab y retorna su ID
func (b *Browser) NewTab(ctx context.Context, url string) (string, error)
// CloseTab cierra un tab específico
func (b *Browser) CloseTab(ctx context.Context, tabID string) error
// SwitchToTab cambia el foco a un tab
func (b *Browser) SwitchToTab(ctx context.Context, tabID string) error
// GetCurrentTab obtiene el tab actual
func (b *Browser) GetCurrentTab(ctx context.Context) (*Tab, error)
// WaitForNewTab espera a que se abra un nuevo tab
func (b *Browser) WaitForNewTab(ctx context.Context, action func()) (*Tab, error)
// OnTabCreated registra callback para tabs nuevos
func (b *Browser) OnTabCreated(handler func(*Tab)) error
```
## Casos de uso
### Caso 1: Listar tabs
```go
tabs, _ := b.GetTabs(ctx)
for _, tab := range tabs {
log.Printf("Tab %s: %s", tab.ID, tab.Title)
}
```
### Caso 2: Abrir nuevo tab
```go
tabID, _ := b.NewTab(ctx, "https://example.com")
log.Printf("Nuevo tab creado: %s", tabID)
```
### Caso 3: Esperar y cambiar a nuevo tab
```go
newTab, _ := b.WaitForNewTab(ctx, func() {
b.Click(ctx, "a[target='_blank']")
})
// Cambiar al nuevo tab
b.SwitchToTab(ctx, newTab.ID)
// Trabajar en el nuevo tab
b.WaitForNavigation(ctx, nil)
log.Printf("Nuevo tab URL: %s", newTab.URL)
```
### Caso 4: Cerrar tabs excepto el principal
```go
tabs, _ := b.GetTabs(ctx)
currentTab, _ := b.GetCurrentTab(ctx)
for _, tab := range tabs {
if tab.ID != currentTab.ID {
b.CloseTab(ctx, tab.ID)
}
}
```
### Caso 5: Trabajar con múltiples tabs
```go
// Abrir múltiples tabs
tab1, _ := b.NewTab(ctx, "https://site1.com")
tab2, _ := b.NewTab(ctx, "https://site2.com")
tab3, _ := b.NewTab(ctx, "https://site3.com")
// Hacer algo en cada tab
for _, tabID := range []string{tab1, tab2, tab3} {
b.SwitchToTab(ctx, tabID)
b.WaitForNavigation(ctx, nil)
title, _ := b.Evaluate(ctx, "document.title")
log.Printf("Tab %s: %v", tabID, title.Value)
}
```
## Implementación interna
```go
func (b *Browser) GetTabs(ctx context.Context) ([]*Tab, error) {
var result struct {
TargetInfos []struct {
TargetID string `json:"targetId"`
Type string `json:"type"`
Title string `json:"title"`
URL string `json:"url"`
Attached bool `json:"attached"`
} `json:"targetInfos"`
}
if err := b.cdpClient.Execute(ctx, "Target.getTargets", nil, &result); err != nil {
return nil, fmt.Errorf("failed to get targets: %w", err)
}
var tabs []*Tab
for _, info := range result.TargetInfos {
if info.Type == "page" {
tabs = append(tabs, &Tab{
ID: info.TargetID,
URL: info.URL,
Title: info.Title,
Type: info.Type,
Attached: info.Attached,
})
}
}
return tabs, nil
}
func (b *Browser) NewTab(ctx context.Context, url string) (string, error) {
var result struct {
TargetID string `json:"targetId"`
}
params := map[string]interface{}{
"url": url,
}
if err := b.cdpClient.Execute(ctx, "Target.createTarget", params, &result); err != nil {
return "", fmt.Errorf("failed to create tab: %w", err)
}
return result.TargetID, nil
}
func (b *Browser) SwitchToTab(ctx context.Context, tabID string) error {
// Activar tab
if err := b.cdpClient.Execute(ctx, "Target.activateTarget", map[string]interface{}{
"targetId": tabID,
}, nil); err != nil {
return fmt.Errorf("failed to activate tab: %w", err)
}
// Attach al tab si no está attached
if err := b.cdpClient.Execute(ctx, "Target.attachToTarget", map[string]interface{}{
"targetId": tabID,
"flatten": true,
}, nil); err != nil {
return fmt.Errorf("failed to attach to tab: %w", err)
}
// Actualizar targetID actual del browser
b.targetID = tabID
return nil
}
```
## CDP Commands
### Listar tabs
```json
{"method": "Target.getTargets"}
```
### Crear tab
```json
{"method": "Target.createTarget", "params": {"url": "https://example.com"}}
```
### Cerrar tab
```json
{"method": "Target.closeTarget", "params": {"targetId": "ABC123"}}
```
### Activar tab
```json
{"method": "Target.activateTarget", "params": {"targetId": "ABC123"}}
```
### Attach a tab
```json
{"method": "Target.attachToTarget", "params": {"targetId": "ABC123", "flatten": true}}
```
## Eventos CDP
### Nuevo tab creado
```go
b.cdpClient.On("Target.targetCreated", func(params json.RawMessage) {
var event struct {
TargetInfo struct {
TargetID string `json:"targetId"`
Type string `json:"type"`
Title string `json:"title"`
URL string `json:"url"`
} `json:"targetInfo"`
}
json.Unmarshal(params, &event)
if event.TargetInfo.Type == "page" {
// Nuevo tab creado
log.Printf("New tab: %s", event.TargetInfo.TargetID)
}
})
```
## Consideraciones especiales
### Session management
- Cada tab requiere su propia sesión CDP
- Mantener mapa de tabID -> sessionID
- Enviar comandos al tab correcto
### Popup handling
```go
// Detectar popups automáticamente
b.OnTabCreated(func(tab *Tab) {
if strings.Contains(tab.URL, "popup") {
b.SwitchToTab(ctx, tab.ID)
// Manejar popup
b.CloseTab(ctx, tab.ID)
}
})
```
### Memory management
- Cerrar tabs que no se usan
- Detach de tabs inactivos
- Limpiar event listeners
## Testing
```go
func TestMultipleTabs(t *testing.T) {
// Crear 3 tabs
tab1, _ := b.NewTab(ctx, "https://example.com")
tab2, _ := b.NewTab(ctx, "https://google.com")
tab3, _ := b.NewTab(ctx, "https://github.com")
// Verificar que existen
tabs, _ := b.GetTabs(ctx)
assert.Len(t, tabs, 4) // 3 + tab inicial
// Cambiar entre tabs
b.SwitchToTab(ctx, tab1)
current, _ := b.GetCurrentTab(ctx)
assert.Equal(t, tab1, current.ID)
// Cerrar tabs
b.CloseTab(ctx, tab1)
b.CloseTab(ctx, tab2)
b.CloseTab(ctx, tab3)
tabs, _ = b.GetTabs(ctx)
assert.Len(t, tabs, 1) // Solo tab inicial
}
```
## Referencias
- CDP Target domain: https://chromedevtools.github.io/devtools-protocol/tot/Target/
- Playwright pages: https://playwright.dev/docs/pages
- Selenium window handles: https://www.selenium.dev/documentation/webdriver/interactions/windows/
@@ -0,0 +1,300 @@
# Issue #007: Alert/Prompt/Confirm Handling
**Tipo**: Enhancement
**Prioridad**: Media
**Estado**: Pendiente
## Descripción
Implementar manejo de JavaScript dialogs (alert, prompt, confirm) que aparecen en páginas web.
## Funcionalidad deseada
### Tipos de dialogs
- **Alert**: `window.alert("mensaje")` - Solo botón OK
- **Confirm**: `window.confirm("¿Continuar?")` - OK/Cancel, retorna boolean
- **Prompt**: `window.prompt("Nombre:", "default")` - Input + OK/Cancel
### Operaciones
- Detectar cuando aparece un dialog
- Aceptar dialog (OK)
- Rechazar dialog (Cancel)
- Enviar texto a prompt
- Obtener mensaje del dialog
- Manejar dialogs automáticamente con reglas
## Implementación técnica
### Archivo sugerido
`pkg/browser/dialogs.go`
### CDP Domain
**Page.javascriptDialogOpening** - Evento cuando aparece dialog
**Page.handleJavaScriptDialog** - Responder al dialog
### API propuesta
```go
// DialogType tipo de dialog JavaScript
type DialogType string
const (
DialogTypeAlert DialogType = "alert"
DialogTypeConfirm DialogType = "confirm"
DialogTypePrompt DialogType = "prompt"
)
// DialogAction acción a tomar con el dialog
type DialogAction string
const (
DialogAccept DialogAction = "accept" // OK
DialogDismiss DialogAction = "dismiss" // Cancel
)
// Dialog representa un dialog JavaScript
type Dialog struct {
Type DialogType
Message string
DefaultPromptText string
}
// HandleDialog maneja un dialog JavaScript cuando aparece
func (b *Browser) HandleDialog(ctx context.Context, action DialogAction, promptText string) error
// OnDialog registra un handler para dialogs
func (b *Browser) OnDialog(handler func(*Dialog) (DialogAction, string)) error
// WaitForDialog espera a que aparezca un dialog
func (b *Browser) WaitForDialog(ctx context.Context) (*Dialog, error)
// AcceptDialog acepta el próximo dialog que aparezca
func (b *Browser) AcceptDialog(ctx context.Context) error
// DismissDialog rechaza el próximo dialog que aparezca
func (b *Browser) DismissDialog(ctx context.Context) error
// PromptDialog responde a un prompt con texto
func (b *Browser) PromptDialog(ctx context.Context, text string) error
// AutoHandleDialogs configura manejo automático de dialogs
func (b *Browser) AutoHandleDialogs(ctx context.Context, action DialogAction) error
```
## Casos de uso
### Caso 1: Aceptar alert automáticamente
```go
// Configurar manejo automático
b.AutoHandleDialogs(ctx, browser.DialogAccept)
// Cualquier alert será aceptado automáticamente
b.Click(ctx, "#trigger-alert")
```
### Caso 2: Manejar confirm con lógica
```go
b.OnDialog(func(dialog *browser.Dialog) (browser.DialogAction, string) {
log.Printf("Dialog: %s - %s", dialog.Type, dialog.Message)
if dialog.Type == browser.DialogTypeConfirm {
if strings.Contains(dialog.Message, "eliminar") {
return browser.DialogDismiss, "" // Cancelar eliminación
}
}
return browser.DialogAccept, ""
})
b.Click(ctx, "#delete-button")
```
### Caso 3: Responder a prompt
```go
// Esperar prompt y responder
go func() {
dialog, _ := b.WaitForDialog(ctx)
if dialog.Type == browser.DialogTypePrompt {
b.PromptDialog(ctx, "Mi nombre")
}
}()
b.Click(ctx, "#ask-name-button")
```
### Caso 4: Aceptar dialog específico
```go
// Preparar handler antes de la acción
b.AcceptDialog(ctx)
// Acción que genera dialog
b.Click(ctx, "#show-alert")
```
## Comandos CDP necesarios
```go
// 1. Habilitar eventos de dialog
{"method": "Page.enable"}
// 2. Escuchar evento de dialog
// Evento: "Page.javascriptDialogOpening"
// Params: {
// "url": "https://...",
// "message": "Mensaje del dialog",
// "type": "alert|confirm|prompt",
// "defaultPrompt": "texto default" // solo en prompt
// }
// 3. Responder al dialog
{"method": "Page.handleJavaScriptDialog", "params": {
"accept": true, // true = OK, false = Cancel
"promptText": "texto de respuesta" // opcional, solo para prompt
}}
```
## Implementación interna
```go
type dialogHandler struct {
action DialogAction
promptText string
callback func(*Dialog) (DialogAction, string)
done chan struct{}
}
func (b *Browser) setupDialogHandling() {
b.cdpClient.On("Page.javascriptDialogOpening", func(params json.RawMessage) {
var event struct {
Type string `json:"type"`
Message string `json:"message"`
DefaultPrompt string `json:"defaultPrompt"`
}
json.Unmarshal(params, &event)
dialog := &Dialog{
Type: DialogType(event.Type),
Message: event.Message,
DefaultPromptText: event.DefaultPrompt,
}
// Procesar con handler registrado
action, text := b.processDialog(dialog)
// Responder
b.cdpClient.SendCommand(context.Background(), "Page.handleJavaScriptDialog", map[string]interface{}{
"accept": action == DialogAccept,
"promptText": text,
})
})
}
```
## Consideraciones especiales
### Timing crítico
- Los dialogs **bloquean** JavaScript hasta que se responden
- Debe haber handler registrado ANTES de que aparezca el dialog
- Si no se maneja, Chrome esperará indefinidamente
### beforeunload dialogs
```go
// Dialogs de "¿Seguro que quieres salir?"
// Se generan al cerrar tab/navegador
b.OnDialog(func(dialog *Dialog) (browser.DialogAction, string) {
if dialog.Type == browser.DialogTypeBeforeUnload {
return browser.DialogAccept, "" // Permitir salir
}
return browser.DialogAccept, ""
})
```
### Headless mode
- En modo headless, los dialogs no se muestran visualmente
- Pero igual generan el evento y deben manejarse
- Importante para testing automatizado
### Timeout en dialogs
```go
// Implementar timeout para evitar quedar colgado
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
dialog, err := b.WaitForDialog(ctx)
if err == context.DeadlineExceeded {
log.Println("No apareció dialog en 5s")
}
```
## Testing
### Página de prueba
```html
<!DOCTYPE html>
<html>
<body>
<button onclick="alert('Hola')">Alert</button>
<button onclick="confirm('¿Continuar?')">Confirm</button>
<button onclick="prompt('Nombre:')">Prompt</button>
<script>
// Test beforeunload
window.addEventListener('beforeunload', (e) => {
e.preventDefault();
e.returnValue = '';
});
</script>
</body>
</html>
```
### Tests
```go
func TestAlertHandling(t *testing.T) {
b.AutoHandleDialogs(ctx, browser.DialogAccept)
b.Navigate(ctx, "test.html", nil)
b.Click(ctx, "button:nth-child(1)")
// No debe quedar colgado
}
func TestPromptResponse(t *testing.T) {
b.OnDialog(func(d *browser.Dialog) (browser.DialogAction, string) {
if d.Type == browser.DialogTypePrompt {
return browser.DialogAccept, "Test Name"
}
return browser.DialogAccept, ""
})
b.Click(ctx, "button:nth-child(3)")
result, _ := b.Evaluate(ctx, "lastPromptResult")
assert.Equal(t, "Test Name", result.Value)
}
```
## Ejemplos de uso real
### Login con confirm
```go
b.OnDialog(func(d *browser.Dialog) (browser.DialogAction, string) {
if strings.Contains(d.Message, "logout") {
return browser.DialogAccept, ""
}
return browser.DialogDismiss, ""
})
b.Click(ctx, "#logout-button")
```
### Formulario con prompt
```go
b.PromptDialog(ctx, "usuario@example.com")
b.Click(ctx, "#ask-email-button")
```
## Referencias
- CDP Page.handleJavaScriptDialog: https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-handleJavaScriptDialog
- CDP Page.javascriptDialogOpening: https://chromedevtools.github.io/devtools-protocol/tot/Page/#event-javascriptDialogOpening
- Playwright Dialogs: https://playwright.dev/docs/dialogs
- Selenium Alerts: https://www.selenium.dev/documentation/webdriver/interactions/alerts/
@@ -0,0 +1,309 @@
# Issue #008: Screenshot de Elementos Específicos
**Tipo**: Enhancement
**Prioridad**: Media
**Estado**: Pendiente
## Descripción
Implementar capacidad de tomar screenshots de elementos específicos de la página en lugar de solo página completa.
## Funcionalidad deseada
### Operaciones
- Screenshot de elemento específico por selector CSS
- Screenshot de región (coordenadas x, y, width, height)
- Screenshot con padding/margin alrededor del elemento
- Scroll automático al elemento antes de capturar
- Esperar a que elemento sea visible antes de capturar
- Captura de múltiples elementos en batch
- Captura con o sin sombras CSS
## Implementación técnica
### Archivo sugerido
Extender `pkg/browser/navigation.go` o crear `pkg/browser/screenshots.go`
### CDP Methods
- **DOM.getBoxModel** - Obtener dimensiones del elemento
- **Page.captureScreenshot** - Capturar con clip region
### API propuesta
```go
// ScreenshotElementOptions opciones para screenshot de elemento
type ScreenshotElementOptions struct {
Format string // "png" o "jpeg" (default: png)
Quality int // 0-100 para JPEG (default: 80)
Padding int // Padding en pixels alrededor del elemento
WaitVisible bool // Esperar a que sea visible (default: true)
ScrollIntoView bool // Scroll al elemento antes (default: true)
OmitBackground bool // Fondo transparente (default: false)
}
// DefaultScreenshotElementOptions retorna opciones por defecto
func DefaultScreenshotElementOptions() *ScreenshotElementOptions
// ScreenshotElement toma screenshot de un elemento específico
func (b *Browser) ScreenshotElement(ctx context.Context, selector string, opts *ScreenshotElementOptions) ([]byte, error)
// ScreenshotElementToFile guarda screenshot de elemento a archivo
func (b *Browser) ScreenshotElementToFile(ctx context.Context, selector string, filepath string, opts *ScreenshotElementOptions) error
// ScreenshotRegion toma screenshot de región específica
func (b *Browser) ScreenshotRegion(ctx context.Context, x, y, width, height int) ([]byte, error)
// ScreenshotElements toma screenshots de múltiples elementos
func (b *Browser) ScreenshotElements(ctx context.Context, selectors []string, opts *ScreenshotElementOptions) (map[string][]byte, error)
```
## Casos de uso
### Caso 1: Screenshot de botón específico
```go
opts := browser.DefaultScreenshotElementOptions()
opts.Padding = 10 // 10px de margen
screenshot, _ := b.ScreenshotElement(ctx, "#submit-button", opts)
os.WriteFile("button.png", screenshot, 0644)
```
### Caso 2: Screenshot de cada producto
```go
products := []string{
".product:nth-child(1)",
".product:nth-child(2)",
".product:nth-child(3)",
}
screenshots, _ := b.ScreenshotElements(ctx, products, nil)
for selector, data := range screenshots {
filename := strings.ReplaceAll(selector, ":", "-") + ".png"
os.WriteFile(filename, data, 0644)
}
```
### Caso 3: Screenshot con fondo transparente
```go
opts := &browser.ScreenshotElementOptions{
Format: "png",
OmitBackground: true, // PNG transparente
}
screenshot, _ := b.ScreenshotElement(ctx, ".icon", opts)
```
### Caso 4: Screenshot de región específica
```go
// Capturar área de 300x200 en posición (100, 150)
screenshot, _ := b.ScreenshotRegion(ctx, 100, 150, 300, 200)
```
## Implementación interna
```go
func (b *Browser) ScreenshotElement(ctx context.Context, selector string, opts *ScreenshotElementOptions) ([]byte, error) {
if opts == nil {
opts = DefaultScreenshotElementOptions()
}
// 1. Esperar a que elemento sea visible si se especificó
if opts.WaitVisible {
if err := b.WaitForElement(ctx, selector, nil); err != nil {
return nil, fmt.Errorf("element not visible: %w", err)
}
}
// 2. Scroll al elemento si se especificó
if opts.ScrollIntoView {
script := fmt.Sprintf(`
document.querySelector('%s').scrollIntoView({
behavior: 'instant',
block: 'center'
})
`, selector)
b.Evaluate(ctx, script)
}
// 3. Obtener dimensiones del elemento
var result struct {
Model struct {
Content []float64 `json:"content"` // [x1, y1, x2, y2, x3, y3, x4, y4]
} `json:"model"`
}
// Primero obtener nodeId
nodeID, err := b.querySelector(ctx, selector)
if err != nil {
return nil, err
}
// Obtener box model
if err := b.cdpClient.Execute(ctx, "DOM.getBoxModel", map[string]interface{}{
"nodeId": nodeID,
}, &result); err != nil {
return nil, fmt.Errorf("failed to get box model: %w", err)
}
// Calcular clip region
content := result.Model.Content
x := content[0]
y := content[1]
width := content[4] - content[0]
height := content[5] - content[1]
// Aplicar padding
if opts.Padding > 0 {
x -= float64(opts.Padding)
y -= float64(opts.Padding)
width += float64(opts.Padding * 2)
height += float64(opts.Padding * 2)
}
// 4. Capturar screenshot con clip
params := map[string]interface{}{
"format": opts.Format,
"clip": map[string]interface{}{
"x": x,
"y": y,
"width": width,
"height": height,
"scale": 1,
},
}
if opts.OmitBackground {
params["captureBeyondViewport"] = true
params["fromSurface"] = true
}
if opts.Format == "jpeg" && opts.Quality > 0 {
params["quality"] = opts.Quality
}
var screenshotResult struct {
Data string `json:"data"`
}
if err := b.cdpClient.Execute(ctx, "Page.captureScreenshot", params, &screenshotResult); err != nil {
return nil, fmt.Errorf("failed to capture screenshot: %w", err)
}
// 5. Decodificar base64
data, err := base64.StdEncoding.DecodeString(screenshotResult.Data)
if err != nil {
return nil, fmt.Errorf("failed to decode screenshot: %w", err)
}
return data, nil
}
```
## Comandos CDP
### Obtener dimensiones del elemento
```json
{
"method": "DOM.getBoxModel",
"params": {
"nodeId": 123
}
}
// Response:
{
"model": {
"content": [x1, y1, x2, y2, x3, y3, x4, y4],
"padding": [...],
"border": [...],
"margin": [...],
"width": 200,
"height": 100
}
}
```
### Capturar con clip
```json
{
"method": "Page.captureScreenshot",
"params": {
"format": "png",
"clip": {
"x": 100,
"y": 200,
"width": 300,
"height": 150,
"scale": 1
},
"captureBeyondViewport": true
}
}
```
## Casos de uso avanzados
### Comparación visual
```go
// Capturar antes y después de una acción
before, _ := b.ScreenshotElement(ctx, "#component", nil)
b.Click(ctx, "#toggle-button")
after, _ := b.ScreenshotElement(ctx, "#component", nil)
// Comparar imágenes
if !bytes.Equal(before, after) {
log.Println("El componente cambió visualmente")
}
```
### Generación de thumbnails
```go
opts := &browser.ScreenshotElementOptions{
Format: "jpeg",
Quality: 60, // Compresión para thumbnails
}
// Capturar todos los artículos
articles := []string{".article-1", ".article-2", ".article-3"}
thumbnails, _ := b.ScreenshotElements(ctx, articles, opts)
```
### Screenshot de elemento fuera de viewport
```go
// Elemento muy abajo en la página
opts := &browser.ScreenshotElementOptions{
ScrollIntoView: true, // Scroll automático
WaitVisible: true,
}
screenshot, _ := b.ScreenshotElement(ctx, "#footer-logo", opts)
```
## Mejoras adicionales
### Screenshot de elemento con sombra
```go
// Incluir box-shadow en captura
opts.IncludeShadow = true
```
### Screenshot de elemento rotado
```go
// Calcular bounding box considerando rotación CSS
opts.ConsiderTransform = true
```
### Screenshot de SVG específico
```go
// Elementos SVG pueden necesitar manejo especial
screenshot, _ := b.ScreenshotElement(ctx, "svg#chart", opts)
```
## Referencias
- CDP DOM.getBoxModel: https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getBoxModel
- CDP Page.captureScreenshot: https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-captureScreenshot
- Playwright elementHandle.screenshot: https://playwright.dev/docs/api/class-elementhandle#element-handle-screenshot
- Puppeteer element screenshots: https://pptr.dev/api/puppeteer.elementhandle.screenshot
+440
View File
@@ -0,0 +1,440 @@
# Issue #009: PDF Generation
**Tipo**: Enhancement
**Prioridad**: Media
**Estado**: Pendiente
## Descripción
Implementar generación de PDFs de páginas web, similar a "Imprimir a PDF" del navegador.
## Funcionalidad deseada
### Operaciones básicas
- Generar PDF de página completa
- Generar PDF de página actual (viewport)
- Control de formato de página (A4, Letter, etc.)
- Orientación (portrait/landscape)
- Márgenes personalizables
- Headers y footers personalizados
- Background graphics (imágenes de fondo)
- Scale/zoom del contenido
### Operaciones avanzadas
- Rangos de páginas específicos
- Números de página
- Fecha/hora en header/footer
- CSS para medios de impresión
- Protección de PDF (opcional)
## Implementación técnica
### Archivo sugerido
`pkg/browser/pdf.go`
### CDP Method
**Page.printToPDF** - Genera PDF de la página
### API propuesta
```go
// PDFFormat formato de papel
type PDFFormat string
const (
PDFFormatA4 PDFFormat = "A4"
PDFFormatLetter PDFFormat = "Letter"
PDFFormatLegal PDFFormat = "Legal"
PDFFormatA3 PDFFormat = "A3"
PDFFormatTabloid PDFFormat = "Tabloid"
)
// PDFOrientation orientación de página
type PDFOrientation string
const (
PDFOrientationPortrait PDFOrientation = "portrait"
PDFOrientationLandscape PDFOrientation = "landscape"
)
// PDFMargins márgenes del PDF
type PDFMargins struct {
Top float64 // En pulgadas
Right float64
Bottom float64
Left float64
}
// PDFOptions opciones para generación de PDF
type PDFOptions struct {
// Formato de papel
Format PDFFormat // Default: A4
// Orientación
Orientation PDFOrientation // Default: portrait
// Dimensiones personalizadas (en pulgadas)
// Si se especifica, ignora Format
Width float64
Height float64
// Márgenes (en pulgadas)
Margins PDFMargins // Default: 1cm todos
// Scale del contenido (0.1 - 2.0)
Scale float64 // Default: 1.0
// Incluir colores y gráficos de fondo
PrintBackground bool // Default: false
// Rango de páginas (ej: "1-5, 8, 11-13")
PageRanges string
// Header template (HTML)
HeaderTemplate string
// Footer template (HTML)
FooterTemplate string
// Mostrar header y footer
DisplayHeaderFooter bool
// Preferir CSS para @media print
PreferCSSPageSize bool
// Generar PDFs etiquetados (accesibilidad)
GenerateTaggedPDF bool
}
// DefaultPDFOptions retorna opciones por defecto
func DefaultPDFOptions() *PDFOptions
// GeneratePDF genera un PDF de la página actual
func (b *Browser) GeneratePDF(ctx context.Context, opts *PDFOptions) ([]byte, error)
// SavePDF genera y guarda PDF a archivo
func (b *Browser) SavePDF(ctx context.Context, filepath string, opts *PDFOptions) error
// PrintToPDF genera PDF (alias de GeneratePDF)
func (b *Browser) PrintToPDF(ctx context.Context, opts *PDFOptions) ([]byte, error)
```
## Casos de uso
### Caso 1: PDF simple
```go
// PDF con opciones por defecto (A4, portrait)
pdf, _ := b.GeneratePDF(ctx, nil)
os.WriteFile("page.pdf", pdf, 0644)
```
### Caso 2: PDF con configuración personalizada
```go
opts := &browser.PDFOptions{
Format: browser.PDFFormatLetter,
Orientation: browser.PDFOrientationLandscape,
PrintBackground: true, // Incluir colores de fondo
Scale: 0.8, // 80% del tamaño
Margins: browser.PDFMargins{
Top: 0.5,
Right: 0.5,
Bottom: 0.5,
Left: 0.5,
},
}
pdf, _ := b.GeneratePDF(ctx, opts)
```
### Caso 3: PDF con header y footer
```go
opts := &browser.PDFOptions{
DisplayHeaderFooter: true,
HeaderTemplate: `
<div style="font-size: 10px; text-align: center; width: 100%;">
<span class="title"></span>
</div>
`,
FooterTemplate: `
<div style="font-size: 10px; text-align: center; width: 100%;">
Página <span class="pageNumber"></span> de <span class="totalPages"></span>
</div>
`,
}
pdf, _ := b.GeneratePDF(ctx, opts)
```
### Caso 4: PDF de rango específico
```go
opts := &browser.PDFOptions{
PageRanges: "1-3, 5", // Solo páginas 1, 2, 3 y 5
}
pdf, _ := b.GeneratePDF(ctx, opts)
```
### Caso 5: Guardar directamente a archivo
```go
opts := browser.DefaultPDFOptions()
opts.Format = browser.PDFFormatA4
opts.PrintBackground = true
b.SavePDF(ctx, "report.pdf", opts)
```
## Implementación interna
```go
func (b *Browser) GeneratePDF(ctx context.Context, opts *PDFOptions) ([]byte, error) {
if opts == nil {
opts = DefaultPDFOptions()
}
// Construir parámetros CDP
params := map[string]interface{}{
"printBackground": opts.PrintBackground,
"displayHeaderFooter": opts.DisplayHeaderFooter,
"preferCSSPageSize": opts.PreferCSSPageSize,
"generateTaggedPDF": opts.GenerateTaggedPDF,
}
// Formato o dimensiones custom
if opts.Width > 0 && opts.Height > 0 {
params["paperWidth"] = opts.Width
params["paperHeight"] = opts.Height
} else {
// Usar formato predefinido
params["format"] = string(opts.Format)
}
// Orientación
if opts.Orientation != "" {
params["landscape"] = opts.Orientation == PDFOrientationLandscape
}
// Márgenes
params["marginTop"] = opts.Margins.Top
params["marginRight"] = opts.Margins.Right
params["marginBottom"] = opts.Margins.Bottom
params["marginLeft"] = opts.Margins.Left
// Scale
if opts.Scale > 0 {
params["scale"] = opts.Scale
}
// Page ranges
if opts.PageRanges != "" {
params["pageRanges"] = opts.PageRanges
}
// Templates
if opts.HeaderTemplate != "" {
params["headerTemplate"] = opts.HeaderTemplate
}
if opts.FooterTemplate != "" {
params["footerTemplate"] = opts.FooterTemplate
}
// Ejecutar comando
var result struct {
Data string `json:"data"` // Base64
Stream string `json:"stream"` // Stream handle (para PDFs grandes)
}
if err := b.cdpClient.Execute(ctx, "Page.printToPDF", params, &result); err != nil {
return nil, fmt.Errorf("failed to generate PDF: %w", err)
}
// Decodificar base64
data, err := base64.StdEncoding.DecodeString(result.Data)
if err != nil {
return nil, fmt.Errorf("failed to decode PDF: %w", err)
}
return data, nil
}
func DefaultPDFOptions() *PDFOptions {
return &PDFOptions{
Format: PDFFormatA4,
Orientation: PDFOrientationPortrait,
Scale: 1.0,
Margins: PDFMargins{
Top: 0.4, // ~1cm
Right: 0.4,
Bottom: 0.4,
Left: 0.4,
},
PrintBackground: false,
}
}
```
## Comandos CDP
```json
{
"method": "Page.printToPDF",
"params": {
"landscape": false,
"displayHeaderFooter": true,
"printBackground": true,
"scale": 1,
"paperWidth": 8.5,
"paperHeight": 11,
"marginTop": 0.4,
"marginBottom": 0.4,
"marginLeft": 0.4,
"marginRight": 0.4,
"pageRanges": "1-5",
"headerTemplate": "<div>Header</div>",
"footerTemplate": "<div>Footer</div>",
"preferCSSPageSize": false,
"generateTaggedPDF": false
}
}
// Response:
{
"data": "base64_encoded_pdf_data..."
}
```
## Variables en templates
### Header/Footer templates soportan:
- `<span class="date"></span>` - Fecha actual
- `<span class="title"></span>` - Título de la página
- `<span class="url"></span>` - URL de la página
- `<span class="pageNumber"></span>` - Número de página actual
- `<span class="totalPages"></span>` - Total de páginas
### Ejemplo de template completo
```html
<div style="font-size: 10px; width: 100%; padding: 0 1cm;">
<div style="float: left;">
<span class="title"></span>
</div>
<div style="float: right;">
<span class="date"></span>
</div>
</div>
```
## CSS para impresión
### Aplicar estilos específicos para PDF
```css
@media print {
.no-print {
display: none !important;
}
.page-break {
page-break-after: always;
}
body {
font-size: 12pt;
}
}
```
### Inyectar CSS antes de generar PDF
```go
// Inyectar estilos de impresión
b.Evaluate(ctx, `
const style = document.createElement('style');
style.textContent = '@media print { .sidebar { display: none; } }';
document.head.appendChild(style);
`)
// Generar PDF
pdf, _ := b.GeneratePDF(ctx, opts)
```
## Casos de uso avanzados
### Generar reporte con múltiples páginas
```go
// Navegar a página de reporte
b.Navigate(ctx, "https://example.com/report", nil)
// Esperar a que cargue completamente
b.WaitForSelector(ctx, ".report-ready", nil)
// Generar PDF
opts := &browser.PDFOptions{
Format: browser.PDFFormatA4,
PrintBackground: true,
DisplayHeaderFooter: true,
HeaderTemplate: `<div style="font-size: 10px; text-align: right; width: 100%; padding-right: 1cm;">
Reporte generado: <span class="date"></span>
</div>`,
FooterTemplate: `<div style="font-size: 10px; text-align: center; width: 100%;">
<span class="pageNumber"></span> / <span class="totalPages"></span>
</div>`,
}
b.SavePDF(ctx, "reporte.pdf", opts)
```
### PDF con contenido dinámico
```go
// Generar contenido dinámico
b.Evaluate(ctx, `
document.body.innerHTML = '<h1>Reporte Dinámico</h1>';
for (let i = 1; i <= 10; i++) {
document.body.innerHTML += '<p>Elemento ' + i + '</p>';
}
`)
// Generar PDF
pdf, _ := b.GeneratePDF(ctx, nil)
```
### Batch PDF generation
```go
urls := []string{
"https://example.com/page1",
"https://example.com/page2",
"https://example.com/page3",
}
for i, url := range urls {
b.Navigate(ctx, url, nil)
b.WaitForNavigation(ctx, nil)
filename := fmt.Sprintf("page_%d.pdf", i+1)
b.SavePDF(ctx, filename, nil)
}
```
## Consideraciones
### Tamaño del PDF
- PDFs grandes pueden exceder límite de respuesta CDP
- Usar streaming para PDFs > 10MB (no implementado en v1)
### Performance
- Generación de PDF es **bloqueante**
- Puede tomar varios segundos para páginas grandes
- Considerar timeout apropiado
### Calidad
- Images embebidas mantienen su resolución
- Fonts pueden no incluirse (usar web fonts)
- JavaScript no se ejecuta durante generación
### Headless mode
- PDF generation funciona mejor en headless
- Algunas páginas pueden requerir modo visible
## Referencias
- CDP Page.printToPDF: https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF
- Chrome printing: https://developer.chrome.com/docs/chromium/print-previews
- Playwright PDF: https://playwright.dev/docs/api/class-page#page-pdf
- Puppeteer PDF: https://pptr.dev/api/puppeteer.page.pdf
+101
View File
@@ -0,0 +1,101 @@
# Issue #010: Device Emulation Completo
**Tipo**: Enhancement
**Prioridad**: Media
**Estado**: Pendiente
## Descripción
Implementar emulación completa de dispositivos móviles y tablets (viewport, user-agent, touch, geolocation).
## Funcionalidad deseada
- Emular dispositivos predefinidos (iPhone, iPad, Android, etc.)
- Viewport personalizado (width, height, deviceScaleFactor)
- User-Agent específico de dispositivo
- Touch events habilitados
- Orientación (portrait/landscape)
- Geolocation personalizada
- Timezone específica
- Locale/idioma
- Permisos de dispositivo
## API propuesta
```go
type DeviceDescriptor struct {
Name string
UserAgent string
Viewport Viewport
DeviceScaleFactor float64
IsMobile bool
HasTouch bool
DefaultOrientation string
}
type Viewport struct {
Width int
Height int
}
type EmulationOptions struct {
Device *DeviceDescriptor
Viewport *Viewport
UserAgent string
IsMobile bool
HasTouch bool
Orientation string // "portrait" | "landscape"
Geolocation *Geolocation
Timezone string
Locale string
}
// Dispositivos predefinidos
var Devices = map[string]*DeviceDescriptor{
"iPhone 13": {...},
"iPhone 13 Pro": {...},
"iPad Pro": {...},
"Pixel 5": {...},
"Galaxy S21": {...},
}
func (b *Browser) Emulate(ctx context.Context, opts *EmulationOptions) error
func (b *Browser) EmulateDevice(ctx context.Context, deviceName string) error
func (b *Browser) SetViewport(ctx context.Context, width, height int) error
func (b *Browser) SetUserAgent(ctx context.Context, userAgent string) error
func (b *Browser) SetTouchEnabled(ctx context.Context, enabled bool) error
func (b *Browser) SetOrientation(ctx context.Context, orientation string) error
```
## Uso
```go
// Emular iPhone 13
b.EmulateDevice(ctx, "iPhone 13")
// Emulación personalizada
opts := &browser.EmulationOptions{
Viewport: &browser.Viewport{Width: 375, Height: 812},
UserAgent: "Mozilla/5.0 (iPhone...)",
IsMobile: true,
HasTouch: true,
Orientation: "portrait",
}
b.Emulate(ctx, opts)
```
## CDP Methods
- `Emulation.setDeviceMetricsOverride`
- `Emulation.setUserAgentOverride`
- `Emulation.setTouchEmulationEnabled`
- `Emulation.setEmulatedMedia`
- `Emulation.setGeolocationOverride`
- `Emulation.setTimezoneOverride`
- `Emulation.setLocaleOverride`
## Referencias
- CDP Emulation: https://chromedevtools.github.io/devtools-protocol/tot/Emulation/
- Playwright devices: https://playwright.dev/docs/emulation
- Puppeteer emulation: https://pptr.dev/guides/emulation
+84
View File
@@ -0,0 +1,84 @@
# Issue #011: Downloads Handling
**Tipo**: Enhancement
**Prioridad**: Media
**Estado**: Pendiente
## Descripción
Implementar sistema para detectar, gestionar y esperar downloads de archivos.
## Funcionalidad deseada
- Detectar cuando inicia un download
- Esperar a que download complete
- Obtener path del archivo descargado
- Configurar directorio de descargas
- Cancelar downloads en progreso
- Obtener progreso de download
- Manejar múltiples downloads simultáneos
## API propuesta
```go
type Download struct {
ID string
URL string
Filename string
Path string
MimeType string
Size int64
State DownloadState // "inProgress" | "completed" | "cancelled"
}
type DownloadState string
const (
DownloadStateInProgress DownloadState = "inProgress"
DownloadStateCompleted DownloadState = "completed"
DownloadStateCancelled DownloadState = "cancelled"
)
type DownloadOptions struct {
DownloadPath string // Directorio donde guardar
Behavior string // "allow" | "deny" | "allowAndName"
}
func (b *Browser) SetDownloadBehavior(ctx context.Context, opts *DownloadOptions) error
func (b *Browser) WaitForDownload(ctx context.Context, action func()) (*Download, error)
func (b *Browser) OnDownload(handler func(*Download)) error
func (b *Browser) GetDownloads(ctx context.Context) ([]*Download, error)
func (b *Browser) CancelDownload(ctx context.Context, downloadID string) error
```
## Uso
```go
// Configurar directorio de descargas
b.SetDownloadBehavior(ctx, &browser.DownloadOptions{
DownloadPath: "/tmp/downloads",
Behavior: "allow",
})
// Esperar download
download, _ := b.WaitForDownload(ctx, func() {
b.Click(ctx, "#download-button")
})
log.Printf("Downloaded: %s to %s", download.Filename, download.Path)
// Handler de downloads
b.OnDownload(func(d *browser.Download) {
log.Printf("Download started: %s", d.Filename)
})
```
## CDP Methods
- `Browser.setDownloadBehavior`
- `Page.downloadWillBegin` (evento)
- `Page.downloadProgress` (evento)
## Referencias
- CDP Browser.setDownloadBehavior: https://chromedevtools.github.io/devtools-protocol/tot/Browser/#method-setDownloadBehavior
- Playwright downloads: https://playwright.dev/docs/downloads
@@ -0,0 +1,82 @@
# Issue #012: Browser Contexts (Multi-sesión)
**Tipo**: Enhancement
**Prioridad**: Baja (Avanzado)
**Estado**: Pendiente
## Descripción
Implementar Browser Contexts para múltiples sesiones aisladas en una misma instancia de navegador.
## Funcionalidad deseada
- Crear múltiples contextos aislados
- Cada contexto tiene su propio:
- Storage (cookies, localStorage, sessionStorage)
- Cache
- Permissions
- Geolocation
- Compartir proceso de navegador (más eficiente que múltiples perfiles)
- Cerrar contextos individualmente
## API propuesta
```go
type BrowserContext struct {
id string
browser *Browser
pages []*Page
}
type ContextOptions struct {
Cookies []*Cookie
Permissions []string
Geolocation *Geolocation
Timezone string
Locale string
UserAgent string
}
func (b *Browser) NewContext(ctx context.Context, opts *ContextOptions) (*BrowserContext, error)
func (bc *BrowserContext) NewPage(ctx context.Context) (*Page, error)
func (bc *BrowserContext) Close(ctx context.Context) error
func (bc *BrowserContext) ClearCookies(ctx context.Context) error
```
## Uso
```go
// Contexto 1 - Usuario A
ctx1, _ := b.NewContext(ctx, &browser.ContextOptions{
Cookies: cookiesUserA,
})
page1, _ := ctx1.NewPage(ctx)
page1.Navigate(ctx, "https://example.com")
// Contexto 2 - Usuario B
ctx2, _ := b.NewContext(ctx, &browser.ContextOptions{
Cookies: cookiesUserB,
})
page2, _ := ctx2.NewPage(ctx)
page2.Navigate(ctx, "https://example.com")
// Ambos contextos están completamente aislados
```
## CDP Methods
- `Target.createBrowserContext`
- `Target.disposeBrowserContext`
- `Target.createTarget` con browserContextId
## Ventajas
- Más eficiente que múltiples instancias de navegador
- Rápido para tests paralelos
- Ideal para testing multi-usuario
- Menor uso de memoria vs múltiples navegadores
## Referencias
- CDP Target.createBrowserContext: https://chromedevtools.github.io/devtools-protocol/tot/Target/#method-createBrowserContext
- Playwright contexts: https://playwright.dev/docs/browser-contexts
+109
View File
@@ -0,0 +1,109 @@
# Issue #013: Video Recording
**Tipo**: Enhancement
**Prioridad**: Baja (Avanzado)
**Estado**: Pendiente
## Descripción
Implementar grabación de video de la sesión del navegador.
## Funcionalidad deseada
- Grabar video de la sesión completa
- Configurar resolución y FPS
- Guardar en formato MP4/WebM
- Start/stop recording bajo demanda
- Capturar audio (opcional)
## API propuesta
```go
type VideoOptions struct {
OutputPath string
Width int
Height int
FPS int // Frames per second (default: 25)
Format string // "mp4" | "webm"
AudioCodec string // "opus" | "aac" | ""
}
func (b *Browser) StartRecording(ctx context.Context, opts *VideoOptions) error
func (b *Browser) StopRecording(ctx context.Context) (string, error)
func (b *Browser) PauseRecording(ctx context.Context) error
func (b *Browser) ResumeRecording(ctx context.Context) error
```
## Uso
```go
opts := &browser.VideoOptions{
OutputPath: "./recordings/session.mp4",
Width: 1280,
Height: 720,
FPS: 30,
}
b.StartRecording(ctx, opts)
// Realizar acciones
b.Navigate(ctx, "https://example.com", nil)
b.Click(ctx, "#button")
// Detener y guardar
videoPath, _ := b.StopRecording(ctx)
log.Printf("Video saved: %s", videoPath)
```
## Implementación
### Opción 1: CDP Screencast (screenshots en loop)
```go
// Capturar frames continuamente
b.cdpClient.On("Page.screencastFrame", func(params json.RawMessage) {
// Guardar frame
// Compilar a video con ffmpeg
})
b.cdpClient.SendCommand(ctx, "Page.startScreencast", map[string]interface{}{
"format": "jpeg",
"quality": 80,
"maxWidth": 1280,
"maxHeight": 720,
"everyNthFrame": 1,
})
```
### Opción 2: External tool (ffmpeg)
```bash
# Usar ffmpeg para capturar X11 display
ffmpeg -video_size 1280x720 -framerate 25 -f x11grab -i :99 output.mp4
```
### Opción 3: Chrome --use-file-for-fake-video-capture
```go
// Grabar con flags de Chrome
config.ChromeFlags = append(config.ChromeFlags,
"--use-file-for-fake-video-capture=/dev/video0",
)
```
## CDP Methods
- `Page.startScreencast`
- `Page.screencastFrame` (evento)
- `Page.stopScreencast`
- `Page.screencastFrameAck`
## Consideraciones
- **Performance**: Recording consume CPU/memoria
- **Tamaño**: Videos pueden ser grandes
- **Headless**: Requiere Xvfb o display virtual
- **Codec**: Necesita ffmpeg o herramienta externa
## Referencias
- CDP Page.startScreencast: https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-startScreencast
- Playwright video: https://playwright.dev/docs/videos
- Puppeteer video: https://github.com/puppeteer/puppeteer/issues/448
+109
View File
@@ -0,0 +1,109 @@
# Issue #014: Network Mocking Avanzado
**Tipo**: Enhancement
**Prioridad**: Baja (Avanzado)
**Estado**: Pendiente
## Descripción
Implementar sistema avanzado de interceptación y mocking de requests HTTP/HTTPS.
## Funcionalidad deseada
- Interceptar requests antes de enviar
- Modificar request (URL, headers, body, method)
- Mock responses completas
- Simular latencia de red
- Simular errores de red
- Registro de todas las requests
- Pattern matching avanzado (regex, wildcards)
- Condicional (solo interceptar si...)
## API propuesta
```go
type MockResponse struct {
Status int
Headers map[string]string
Body string
Delay time.Duration
}
type InterceptorFunc func(req *Request) (*MockResponse, error)
type RequestPattern struct {
URL string // Glob o regex
Method string // GET, POST, etc.
Condition func(*Request) bool
}
func (b *Browser) InterceptRequest(ctx context.Context, pattern RequestPattern, handler InterceptorFunc) error
func (b *Browser) MockResponse(ctx context.Context, pattern string, response *MockResponse) error
func (b *Browser) AbortRequest(ctx context.Context, pattern string) error
func (b *Browser) SimulateOffline(ctx context.Context) error
func (b *Browser) SimulateSlowConnection(ctx context.Context, downloadThroughput, uploadThroughput int) error
func (b *Browser) GetAllRequests(ctx context.Context) ([]*Request, error)
```
## Uso
### Mock API response
```go
b.MockResponse(ctx, "**/api/users", &browser.MockResponse{
Status: 200,
Headers: map[string]string{
"Content-Type": "application/json",
},
Body: `{"users": [{"id": 1, "name": "Test"}]}`,
Delay: 100 * time.Millisecond,
})
```
### Interceptar y modificar
```go
b.InterceptRequest(ctx, browser.RequestPattern{
URL: "**/api/**",
}, func(req *browser.Request) (*browser.MockResponse, error) {
// Modificar headers
req.Headers["Authorization"] = "Bearer fake-token"
// Dejar continuar request (nil = no mockear)
return nil, nil
})
```
### Simular error de red
```go
b.AbortRequest(ctx, "**/slow-endpoint")
```
### Simular conexión lenta
```go
b.SimulateSlowConnection(ctx,
500*1024, // 500 KB/s download
100*1024, // 100 KB/s upload
)
```
### Capturar todas las requests
```go
requests, _ := b.GetAllRequests(ctx)
for _, req := range requests {
log.Printf("%s %s - %d", req.Method, req.URL, req.StatusCode)
}
```
## CDP Methods
- `Fetch.enable` - Habilitar interceptación
- `Fetch.requestPaused` - Request interceptado
- `Fetch.continueRequest` - Continuar con cambios
- `Fetch.fulfillRequest` - Mock response
- `Fetch.failRequest` - Abortar request
- `Network.emulateNetworkConditions` - Simular latencia
## Referencias
- CDP Fetch: https://chromedevtools.github.io/devtools-protocol/tot/Fetch/
- Playwright route: https://playwright.dev/docs/network
- Puppeteer interception: https://pptr.dev/guides/request-interception
+172
View File
@@ -0,0 +1,172 @@
# Issue #015: Geolocation & Permissions
**Tipo**: Enhancement
**Prioridad**: Baja (Avanzado)
**Estado**: Pendiente
## Descripción
Implementar sistema para configurar geolocation y permisos del navegador (notifications, geolocation, camera, mic, etc.).
## Funcionalidad deseada
### Geolocation
- Establecer coordenadas GPS personalizadas
- Simular precisión de GPS
- Cambiar ubicación dinámicamente
### Permissions
- Otorgar/denegar permisos específicos
- Permisos por origen (URL)
- Lista completa de permisos soportados
## API propuesta
```go
type Geolocation struct {
Latitude float64
Longitude float64
Accuracy float64 // En metros
}
type Permission string
const (
PermissionGeolocation Permission = "geolocation"
PermissionNotifications Permission = "notifications"
PermissionCamera Permission = "videoCapture"
PermissionMicrophone Permission = "audioCapture"
PermissionClipboard Permission = "clipboardReadWrite"
PermissionMIDI Permission = "midi"
PermissionBackgroundSync Permission = "backgroundSync"
PermissionPersistentStorage Permission = "persistentStorage"
)
func (b *Browser) SetGeolocation(ctx context.Context, geo *Geolocation) error
func (b *Browser) ClearGeolocation(ctx context.Context) error
func (b *Browser) GrantPermissions(ctx context.Context, origin string, permissions []Permission) error
func (b *Browser) DenyPermissions(ctx context.Context, origin string, permissions []Permission) error
func (b *Browser) ResetPermissions(ctx context.Context) error
```
## Uso
### Establecer ubicación
```go
// Simular estar en Nueva York
b.SetGeolocation(ctx, &browser.Geolocation{
Latitude: 40.7128,
Longitude: -74.0060,
Accuracy: 10, // 10 metros
})
b.Navigate(ctx, "https://maps.google.com", nil)
```
### Otorgar permisos
```go
// Permitir notifications y geolocation
b.GrantPermissions(ctx, "https://example.com", []browser.Permission{
browser.PermissionNotifications,
browser.PermissionGeolocation,
})
b.Navigate(ctx, "https://example.com", nil)
```
### Denegar cámara/micrófono
```go
b.DenyPermissions(ctx, "https://videocall.com", []browser.Permission{
browser.PermissionCamera,
browser.PermissionMicrophone,
})
```
### Cambiar ubicación dinámicamente
```go
// Simular movimiento
locations := []browser.Geolocation{
{Latitude: 40.7128, Longitude: -74.0060}, // NYC
{Latitude: 34.0522, Longitude: -118.2437}, // LA
{Latitude: 41.8781, Longitude: -87.6298}, // Chicago
}
for _, loc := range locations {
b.SetGeolocation(ctx, &loc)
time.Sleep(5 * time.Second)
}
```
## CDP Methods
### Geolocation
```go
// Establecer
{"method": "Emulation.setGeolocationOverride", "params": {
"latitude": 40.7128,
"longitude": -74.0060,
"accuracy": 10
}}
// Limpiar
{"method": "Emulation.clearGeolocationOverride"}
```
### Permissions
```go
// Otorgar
{"method": "Browser.grantPermissions", "params": {
"origin": "https://example.com",
"permissions": ["geolocation", "notifications"]
}}
// Denegar (remover)
{"method": "Browser.resetPermissions"}
```
## Permisos disponibles
| Permission | Descripción |
|-----------|-------------|
| `geolocation` | Acceso a GPS |
| `notifications` | Push notifications |
| `videoCapture` | Cámara |
| `audioCapture` | Micrófono |
| `clipboardReadWrite` | Clipboard |
| `midi` | MIDI devices |
| `backgroundSync` | Background sync |
| `persistentStorage` | Persistent storage |
## Casos de uso
### Testing de apps con geolocation
```go
// Test en diferentes ciudades
cities := map[string]browser.Geolocation{
"NYC": {40.7128, -74.0060, 10},
"LA": {34.0522, -118.2437, 10},
}
for name, loc := range cities {
b.SetGeolocation(ctx, &loc)
b.Navigate(ctx, "https://app.com/nearby", nil)
// Verificar resultados específicos de ciudad
}
```
### Testing sin permisos
```go
// Simular usuario que deniega permisos
b.DenyPermissions(ctx, "https://app.com", []browser.Permission{
browser.PermissionCamera,
})
b.Navigate(ctx, "https://app.com/video-call", nil)
// Verificar que app maneja correctamente el error
```
## Referencias
- CDP Emulation.setGeolocationOverride: https://chromedevtools.github.io/devtools-protocol/tot/Emulation/#method-setGeolocationOverride
- CDP Browser.grantPermissions: https://chromedevtools.github.io/devtools-protocol/tot/Browser/#method-grantPermissions
- Playwright geolocation: https://playwright.dev/docs/emulation#geolocation
- Playwright permissions: https://playwright.dev/docs/emulation#permissions
+82
View File
@@ -0,0 +1,82 @@
# Issue #016: Manejo de iFrames
**Tipo**: Enhancement
**Prioridad**: Alta
**Estado**: En progreso
## Descripción
Implementar capacidad para trabajar con elementos dentro de iframes.
## Funcionalidad deseada
- Cambiar contexto a un iframe específico
- Volver al contexto principal (main frame)
- Listar todos los iframes de la página
- Detectar cuando iframe carga
- Ejecutar JavaScript dentro de iframe
- Click/Type en elementos dentro de iframe
- Navegación en cascada (frame -> subframe -> subsubframe)
## API propuesta
```go
// Frame representa un iframe
type Frame struct {
ID string
ParentID string
URL string
Name string
FrameTree []*Frame // Sub-frames
}
// SwitchToFrame cambia contexto a un iframe
func (b *Browser) SwitchToFrame(ctx context.Context, selector string) error
// SwitchToFrameByName cambia a iframe por atributo name
func (b *Browser) SwitchToFrameByName(ctx context.Context, name string) error
// SwitchToMainFrame vuelve al contexto principal
func (b *Browser) SwitchToMainFrame(ctx context.Context) error
// GetFrames obtiene todos los frames de la página
func (b *Browser) GetFrames(ctx context.Context) ([]*Frame, error)
// WaitForFrame espera a que un frame cargue
func (b *Browser) WaitForFrame(ctx context.Context, selector string) error
// EvaluateInFrame ejecuta JS en un frame específico
func (b *Browser) EvaluateInFrame(ctx context.Context, frameID string, script string) (*EvaluateResult, error)
```
## Uso
```go
// Cambiar a iframe
b.SwitchToFrame(ctx, "#payment-iframe")
// Interactuar dentro del iframe
b.Type(ctx, "#card-number", "1234567890123456", nil)
b.Click(ctx, "#submit-payment")
// Volver al frame principal
b.SwitchToMainFrame(ctx)
// Listar frames
frames, _ := b.GetFrames(ctx)
for _, frame := range frames {
log.Printf("Frame: %s - %s", frame.Name, frame.URL)
}
```
## CDP Methods
- `Page.getFrameTree` - Árbol de frames
- `DOM.describeNode` - Info de frame node
- `Runtime.evaluate` con `contextId` específico
## Referencias
- CDP Page.getFrameTree: https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-getFrameTree
- Selenium frames: https://www.selenium.dev/documentation/webdriver/interactions/frames/
- Playwright frames: https://playwright.dev/docs/frames
+137
View File
@@ -0,0 +1,137 @@
# Issue #017: Actions API - Acciones Complejas
**Tipo**: Enhancement
**Prioridad**: Alta
**Estado**: En progreso
## Descripción
Implementar API para acciones complejas de mouse y teclado: hover, drag & drop, double click, right click, scroll, etc.
## Funcionalidad deseada
### Acciones de Mouse
- Hover sobre elemento
- Double click
- Right click (menú contextual)
- Drag and drop
- Scroll a posición específica
- Scroll a elemento
- Move mouse a coordenadas
- Mouse down/up separados
### Acciones de Teclado
- Press key (con modificadores)
- Hold key
- Shortcuts (Ctrl+C, Ctrl+V, etc.)
- Combinaciones complejas
### Cadenas de acciones
- Encadenar múltiples acciones
- ActionChain pattern (como Selenium)
## API propuesta
```go
// Mouse actions
func (b *Browser) Hover(ctx context.Context, selector string) error
func (b *Browser) DoubleClick(ctx context.Context, selector string) error
func (b *Browser) RightClick(ctx context.Context, selector string) error
func (b *Browser) DragAndDrop(ctx context.Context, sourceSelector, targetSelector string) error
func (b *Browser) ScrollTo(ctx context.Context, x, y int) error
func (b *Browser) ScrollToElement(ctx context.Context, selector string) error
func (b *Browser) ScrollBy(ctx context.Context, x, y int) error
func (b *Browser) MoveMouse(ctx context.Context, x, y int) error
// Keyboard actions
func (b *Browser) PressKey(ctx context.Context, key string) error
func (b *Browser) HoldKey(ctx context.Context, key string) error
func (b *Browser) ReleaseKey(ctx context.Context, key string) error
func (b *Browser) SendKeys(ctx context.Context, keys ...string) error
// Action chains
type ActionChain struct {
browser *Browser
actions []action
}
func (b *Browser) NewActionChain() *ActionChain
func (ac *ActionChain) MoveTo(selector string) *ActionChain
func (ac *ActionChain) Click() *ActionChain
func (ac *ActionChain) DoubleClick() *ActionChain
func (ac *ActionChain) ContextClick() *ActionChain
func (ac *ActionChain) SendKeys(keys ...string) *ActionChain
func (ac *ActionChain) Pause(duration time.Duration) *ActionChain
func (ac *ActionChain) Perform(ctx context.Context) error
```
## Uso
### Hover
```go
b.Hover(ctx, "#menu-button")
b.Click(ctx, "#dropdown-item")
```
### Double click
```go
b.DoubleClick(ctx, "#file-icon")
```
### Right click
```go
b.RightClick(ctx, "#context-menu-trigger")
```
### Drag and drop
```go
b.DragAndDrop(ctx, "#drag-source", "#drop-target")
```
### Scroll
```go
// Scroll a elemento
b.ScrollToElement(ctx, "#footer")
// Scroll por pixels
b.ScrollBy(ctx, 0, 500)
// Scroll a posición absoluta
b.ScrollTo(ctx, 0, 1000)
```
### Shortcuts de teclado
```go
// Ctrl+A (Select all)
b.PressKey(ctx, "Control+A")
// Ctrl+C (Copy)
b.PressKey(ctx, "Control+C")
// Esc
b.PressKey(ctx, "Escape")
```
### Action chains
```go
chain := b.NewActionChain()
chain.
MoveTo("#drag-handle").
Click().
MoveTo("#drop-zone").
Release().
Perform(ctx)
```
## CDP Methods
- `Input.dispatchMouseEvent`
- `Input.dispatchKeyEvent`
- `Input.dispatchTouchEvent`
- `Runtime.evaluate` para JavaScript
## Referencias
- CDP Input: https://chromedevtools.github.io/devtools-protocol/tot/Input/
- Selenium Actions: https://www.selenium.dev/documentation/webdriver/actions_api/
- Playwright actions: https://playwright.dev/docs/input
+46
View File
@@ -0,0 +1,46 @@
# Issue #018: File Uploads
**Tipo**: Enhancement
**Prioridad**: Alta
**Estado**: En progreso
## Descripción
Implementar capacidad para subir archivos a inputs de tipo file.
## Funcionalidad deseada
- Subir archivo a `<input type="file">`
- Subir múltiples archivos
- Validar que archivo existe antes de subir
- Soportar paths absolutos y relativos
## API propuesta
```go
func (b *Browser) UploadFile(ctx context.Context, selector string, filePath string) error
func (b *Browser) UploadFiles(ctx context.Context, selector string, filePaths []string) error
func (b *Browser) SetFileInput(ctx context.Context, selector string, files []string) error
```
## Uso
```go
// Subir un archivo
b.UploadFile(ctx, "input[type='file']", "/path/to/document.pdf")
// Subir múltiples archivos
b.UploadFiles(ctx, "input[type='file'][multiple]", []string{
"/path/to/file1.jpg",
"/path/to/file2.png",
})
```
## CDP Methods
- `DOM.setFileInputFiles`
- `DOM.getFileInfo`
## Referencias
- CDP DOM.setFileInputFiles: https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-setFileInputFiles
@@ -0,0 +1,57 @@
# Issue #019: Expected Conditions Mejoradas
**Tipo**: Enhancement
**Prioridad**: Alta
**Estado**: En progreso
## Descripción
Implementar condiciones de espera específicas y predefinidas, similares a Selenium Expected Conditions.
## Funcionalidad deseada
- WaitUntilVisible
- WaitUntilHidden
- WaitUntilClickable
- WaitUntilEnabled
- WaitUntilDisabled
- WaitUntilSelected
- WaitUntilTextMatches
- WaitUntilAttributeContains
- WaitUntilURLContains
- WaitUntilTitleContains
- WaitUntilElementCount
## API propuesta
```go
func (b *Browser) WaitUntilVisible(ctx context.Context, selector string, opts *WaitOptions) error
func (b *Browser) WaitUntilHidden(ctx context.Context, selector string, opts *WaitOptions) error
func (b *Browser) WaitUntilClickable(ctx context.Context, selector string, opts *WaitOptions) error
func (b *Browser) WaitUntilEnabled(ctx context.Context, selector string, opts *WaitOptions) error
func (b *Browser) WaitUntilDisabled(ctx context.Context, selector string, opts *WaitOptions) error
func (b *Browser) WaitUntilTextMatches(ctx context.Context, selector, text string, opts *WaitOptions) error
func (b *Browser) WaitUntilAttributeContains(ctx context.Context, selector, attribute, value string, opts *WaitOptions) error
func (b *Browser) WaitUntilURLContains(ctx context.Context, pattern string, opts *WaitOptions) error
func (b *Browser) WaitUntilTitleContains(ctx context.Context, pattern string, opts *WaitOptions) error
```
## Uso
```go
// Esperar a que elemento sea visible
b.WaitUntilVisible(ctx, "#modal", nil)
// Esperar a que botón sea clickeable
b.WaitUntilClickable(ctx, "#submit-btn", nil)
// Esperar a que texto aparezca
b.WaitUntilTextMatches(ctx, "#status", "Success", nil)
// Esperar cambio de URL
b.WaitUntilURLContains(ctx, "/dashboard", nil)
```
## Referencias
- Selenium Expected Conditions: https://www.selenium.dev/selenium/docs/api/py/webdriver_support/selenium.webdriver.support.expected_conditions.html
@@ -0,0 +1,65 @@
# Issue #001: Conversor de página web a markdown
**Tipo**: Enhancement
**Prioridad**: Media
**Estado**: Pendiente
## Descripción
Implementar utilidad para convertir el contenido HTML de una página web a formato Markdown.
## Funcionalidad deseada
- Convertir títulos (h1-h6) a markdown (#, ##, ###, etc.)
- Convertir enlaces a formato `[texto](url)`
- Convertir imágenes a formato `![alt](src)`
- Convertir listas (ol, ul) a markdown
- Convertir tablas a markdown
- Mantener estructura de párrafos
- Extraer texto limpio sin CSS/JS inline
- Opción para incluir/excluir imágenes
- Manejar código y bloques de código (pre, code)
- Preservar énfasis (bold, italic)
## Implementación técnica
### Archivo sugerido
`pkg/browser/markdown.go`
### API propuesta
```go
// ToMarkdown convierte el contenido de la página actual a Markdown
func (b *Browser) ToMarkdown(ctx context.Context, opts *MarkdownOptions) (string, error)
type MarkdownOptions struct {
Selector string // Selector CSS opcional para convertir solo una parte
IncludeImages bool // Incluir imágenes en el output
IncludeLinks bool // Incluir enlaces
BaseURL string // URL base para enlaces relativos
}
```
### Estrategia
1. Obtener HTML con `GetHTML()`
2. Parsear HTML usando `golang.org/x/net/html`
3. Convertir nodos recursivamente a markdown
4. Alternativamente, ejecutar JS en el navegador con biblioteca turndown
### Librerías potenciales
- `github.com/JohannesKaufmann/html-to-markdown` - Conversor Go nativo
- O ejecutar `turndown.js` vía `Evaluate()` para mayor fidelidad
## Casos de uso
- Extraer contenido de artículos de blog
- Scraping de documentación
- Generar datasets para LLMs
- Archivar contenido web
## Referencias
- Turndown: https://github.com/mixmark-io/turndown
- html-to-markdown Go: https://github.com/JohannesKaufmann/html-to-markdown
@@ -0,0 +1,88 @@
# Issue #002: Recuperación de Accessibility Tree
**Tipo**: Enhancement
**Prioridad**: Alta
**Estado**: Pendiente
## Descripción
Implementar método para obtener el árbol de accesibilidad (Accessibility Tree) de la página usando Chrome DevTools Protocol.
## Funcionalidad deseada
- Obtener accessibility tree completo vía CDP
- Listar roles ARIA de elementos (button, link, heading, etc.)
- Obtener nombres accesibles de elementos
- Extraer propiedades de accesibilidad
- Útil para que LLMs entiendan estructura semántica de página
- Formato JSON estructurado y fácil de parsear
- Opción para filtrar por tipos de nodos
## Implementación técnica
### Archivo sugerido
`pkg/browser/accessibility.go`
### CDP Domain
`Accessibility.getFullAXTree` - https://chromedevtools.github.io/devtools-protocol/tot/Accessibility/
### API propuesta
```go
// GetAccessibilityTree obtiene el árbol de accesibilidad de la página
func (b *Browser) GetAccessibilityTree(ctx context.Context, opts *AccessibilityOptions) (*AXTree, error)
type AccessibilityOptions struct {
Depth int // Profundidad máxima del árbol (0 = ilimitado)
FilterRoles []string // Roles a incluir (ej: ["button", "link", "heading"])
}
type AXTree struct {
Nodes []AXNode `json:"nodes"`
}
type AXNode struct {
NodeID string `json:"nodeId"`
Role string `json:"role"`
Name string `json:"name"`
Description string `json:"description"`
Value string `json:"value,omitempty"`
Properties map[string]string `json:"properties,omitempty"`
Children []string `json:"children,omitempty"` // IDs de hijos
}
```
### Comandos CDP necesarios
```go
// 1. Habilitar dominio Accessibility
{"method": "Accessibility.enable"}
// 2. Obtener árbol completo
{"method": "Accessibility.getFullAXTree", "params": {}}
// O para un nodo específico:
{"method": "Accessibility.getPartialAXTree", "params": {"nodeId": ...}}
```
## Casos de uso
- LLMs pueden entender mejor la estructura de la página
- Identificar elementos interactuables automáticamente
- Testing de accesibilidad
- Generar selectores semánticos
- Scraping inteligente basado en roles ARIA
## Ventajas sobre DOM normal
- Información semántica rica
- Roles ARIA explícitos
- Nombres accesibles computados
- Estructura más simple que DOM HTML
- Ideal para navegación por agentes autónomos
## Referencias
- CDP Accessibility Domain: https://chromedevtools.github.io/devtools-protocol/tot/Accessibility/
- WAI-ARIA Roles: https://www.w3.org/TR/wai-aria-1.2/#role_definitions
- Chrome AX Tree Inspector: chrome://accessibility
@@ -0,0 +1,191 @@
# Issue #003: Administración avanzada de cookies del perfil
**Tipo**: Enhancement
**Prioridad**: Media
**Estado**: Pendiente
## Descripción
Mejorar las capacidades de gestión de cookies persistentes en perfiles de navegador, permitiendo importar/exportar y gestionar cookies antes y después del lanzamiento del navegador.
## Funcionalidad deseada
### Gestión de cookies en runtime (ya implementado parcialmente)
-`GetCookies()` - Obtener cookies de URLs específicas
-`SetCookie()` - Establecer cookies individuales
-`ClearCookies()` - Limpiar todas las cookies
### Nuevas funcionalidades necesarias
#### Importar/Exportar
- Exportar todas las cookies del perfil a archivo JSON
- Importar cookies desde archivo JSON
- Formato compatible con extensiones de Chrome (EditThisCookie, etc.)
- Soportar formato Netscape (cookies.txt)
#### Gestión offline de perfiles
- Leer cookies del perfil sin lanzar navegador
- Modificar cookies del perfil en disco
- Copiar cookies entre perfiles
- Backup/restore de cookies
#### Filtrado y búsqueda
- Listar todas las cookies del perfil actual
- Filtrar cookies por dominio
- Filtrar cookies por nombre
- Buscar cookies por patrón
#### Configuración previa al lanzamiento
- Establecer cookies iniciales antes de lanzar navegador
- Cargar cookies desde archivo al inicio
- Configurar cookies de sesión específicas
## Implementación técnica
### Archivos sugeridos
- `pkg/browser/profile_cookies.go` - Gestión avanzada
- `pkg/browser/cookie_import_export.go` - I/O de archivos
### API propuesta
```go
// === Gestión en runtime ===
// GetAllCookies obtiene todas las cookies del navegador actual
func (b *Browser) GetAllCookies(ctx context.Context) ([]*Cookie, error)
// FilterCookies obtiene cookies que coinciden con filtros
func (b *Browser) FilterCookies(ctx context.Context, filter CookieFilter) ([]*Cookie, error)
type CookieFilter struct {
Domain string // Filtrar por dominio (ej: ".example.com")
Name string // Filtrar por nombre exacto
Pattern string // Regex para nombre o valor
}
// === Import/Export ===
// ExportCookies exporta cookies a archivo JSON
func (b *Browser) ExportCookies(ctx context.Context, filepath string, format CookieFormat) error
// ImportCookies importa cookies desde archivo
func (b *Browser) ImportCookies(ctx context.Context, filepath string, format CookieFormat) error
type CookieFormat string
const (
CookieFormatJSON CookieFormat = "json" // JSON estándar
CookieFormatNetscape CookieFormat = "netscape" // cookies.txt
CookieFormatChrome CookieFormat = "chrome" // Formato EditThisCookie
)
// === Gestión offline de perfiles ===
// Profile representa un perfil de navegador
type Profile struct {
Name string
Path string
}
// ListProfiles lista todos los perfiles disponibles
func ListProfiles() ([]Profile, error)
// GetProfileCookies lee cookies de un perfil sin lanzar navegador
func GetProfileCookies(profilePath string) ([]*Cookie, error)
// SetProfileCookies escribe cookies en un perfil sin lanzar navegador
func SetProfileCookies(profilePath string, cookies []*Cookie) error
// CopyProfileCookies copia cookies entre perfiles
func CopyProfileCookies(srcProfile, dstProfile string) error
// === Configuración inicial ===
// LaunchWithCookies lanza navegador con cookies precargadas
func LaunchWithCookies(ctx context.Context, config *Config, cookiesFile string) (*Browser, error)
// Config.InitialCookies - campo para establecer cookies al inicio
type Config struct {
// ... campos existentes ...
InitialCookies []*Cookie // Cookies a establecer al lanzar
CookiesFile string // Archivo de cookies a cargar
}
```
### Formato JSON de cookies
```json
[
{
"name": "session_id",
"value": "abc123",
"domain": ".example.com",
"path": "/",
"expires": 1735689600,
"httpOnly": true,
"secure": true,
"sameSite": "Lax"
}
]
```
### Ubicación de cookies en perfil Chrome
```
~/.navegator/profiles/<nombre>/
├── Cookies # Base de datos SQLite con cookies
├── Cookies-journal # Journal de transacciones
└── ...
```
## Casos de uso
### Caso 1: Migrar sesión entre perfiles
```go
// Exportar cookies del perfil A
browserA.ExportCookies(ctx, "session.json", CookieFormatJSON)
// Importar en perfil B
browserB.ImportCookies(ctx, "session.json", CookieFormatJSON)
```
### Caso 2: Backup de sesión autenticada
```go
// Guardar estado de sesión actual
b.ExportCookies(ctx, "backup_session.json", CookieFormatJSON)
// Restaurar más tarde
b2.ImportCookies(ctx, "backup_session.json", CookieFormatJSON)
```
### Caso 3: Lanzar con sesión precargada
```go
config := browser.DefaultConfig()
config.CookiesFile = "authenticated_session.json"
b, _ := browser.Launch(ctx, config)
// Ya está autenticado al iniciar
```
### Caso 4: Sincronizar cookies entre máquinas
```go
// Máquina A - exportar
GetProfileCookies("~/.navegator/profiles/main").Export("cookies.json")
// Máquina B - importar
SetProfileCookies("~/.navegator/profiles/main", LoadCookies("cookies.json"))
```
## Consideraciones de seguridad
⚠️ **Importante**: Las cookies pueden contener tokens de sesión y datos sensibles
- Advertir al usuario sobre seguridad de archivos exportados
- Opción para encriptar archivos de cookies
- No guardar cookies de sesión por defecto
- Limpiar cookies sensibles en exports
## Referencias
- CDP Network.getCookies: https://chromedevtools.github.io/devtools-protocol/tot/Network/#method-getCookies
- CDP Storage.getCookies: https://chromedevtools.github.io/devtools-protocol/tot/Storage/#method-getCookies
- Chrome Cookies DB: SQLite format
- Netscape cookies.txt: http://fileformats.archiveteam.org/wiki/Netscape_cookies.txt
@@ -0,0 +1,288 @@
# Issue #004: Administración de extensiones de Chrome
**Tipo**: Enhancement
**Prioridad**: Media
**Estado**: Pendiente
## Descripción
Implementar sistema completo para cargar, gestionar y configurar extensiones de Chrome en perfiles de navegador.
## Funcionalidad deseada
### Carga de extensiones
- Cargar extensiones desde archivos `.crx` (empaquetadas)
- Cargar extensiones desempaquetadas (carpetas)
- Cargar múltiples extensiones simultáneamente
- Especificar extensiones en configuración de perfil
### Gestión de extensiones
- Listar extensiones instaladas en perfil
- Habilitar/deshabilitar extensiones
- Desinstalar extensiones
- Actualizar extensiones
- Obtener información de extensión (nombre, versión, ID)
### Extensiones predefinidas
- Configuraciones para extensiones populares
- uBlock Origin - bloqueador de ads
- Tampermonkey - userscripts
- Cookie editors
- Proxy switchers
- User-agent switchers
### Configuración de extensiones
- Establecer configuración de extensión desde código
- Importar/exportar configuraciones
- Templates de configuración para casos comunes
## Implementación técnica
### Archivo sugerido
`pkg/browser/extensions.go`
### Flags de Chrome necesarias
```go
// Cargar extensión específica
"--load-extension=/path/to/extension"
// Cargar múltiples extensiones
"--load-extension=/path/ext1,/path/ext2,/path/ext3"
// Deshabilitar todas excepto las especificadas
"--disable-extensions-except=/path/ext1,/path/ext2"
// Desempaquetar y cargar .crx
"--load-extension=/path/to/extension.crx"
```
### API propuesta
```go
// === Configuración de extensiones ===
type ExtensionConfig struct {
Path string // Ruta a extensión (carpeta o .crx)
ID string // ID de extensión (opcional)
Enabled bool // Habilitada por defecto
Settings map[string]string // Configuración específica
}
// Config.Extensions - campo para extensiones
type Config struct {
// ... campos existentes ...
Extensions []*ExtensionConfig // Extensiones a cargar
DisableOtherExts bool // Deshabilitar extensiones no especificadas
}
// === Gestión en runtime ===
// Extension representa una extensión instalada
type Extension struct {
ID string
Name string
Version string
Path string
Enabled bool
Description string
}
// ListExtensions lista extensiones instaladas en el navegador actual
func (b *Browser) ListExtensions(ctx context.Context) ([]*Extension, error)
// LoadExtension carga una extensión en runtime
func (b *Browser) LoadExtension(ctx context.Context, path string) (*Extension, error)
// EnableExtension habilita una extensión
func (b *Browser) EnableExtension(ctx context.Context, extensionID string) error
// DisableExtension deshabilita una extensión
func (b *Browser) DisableExtension(ctx context.Context, extensionID string) error
// RemoveExtension desinstala una extensión
func (b *Browser) RemoveExtension(ctx context.Context, extensionID string) error
// GetExtensionSettings obtiene configuración de una extensión
func (b *Browser) GetExtensionSettings(ctx context.Context, extensionID string) (map[string]interface{}, error)
// SetExtensionSettings establece configuración de extensión
func (b *Browser) SetExtensionSettings(ctx context.Context, extensionID string, settings map[string]interface{}) error
// === Extensiones predefinidas ===
// PresetExtensions contiene configuraciones de extensiones populares
var PresetExtensions = map[string]*ExtensionConfig{
"ublock-origin": {
Path: "~/.navegator/extensions/ublock-origin",
ID: "cjpalhdlnbpafiamejdnhcphjbkeiagm",
},
"tampermonkey": {
Path: "~/.navegator/extensions/tampermonkey",
ID: "dhdgffkkebhmkfjojejmpbldmpobfkfo",
},
}
// LoadPresetExtension carga una extensión predefinida
func LoadPresetExtension(name string) (*ExtensionConfig, error)
```
### Estructura de directorio de extensiones
```
~/.navegator/
├── profiles/
│ └── <nombre>/
│ └── Extensions/ # Extensiones instaladas del perfil
│ └── <extension-id>/
│ └── <version>/
└── extensions/ # Extensiones compartidas
├── ublock-origin/
│ ├── manifest.json
│ └── ...
└── tampermonkey/
├── manifest.json
└── ...
```
## Casos de uso
### Caso 1: Lanzar con uBlock Origin
```go
config := browser.DefaultConfig()
config.Extensions = []*browser.ExtensionConfig{
{Path: "/path/to/ublock-origin"},
}
b, _ := browser.Launch(ctx, config)
```
### Caso 2: Cargar extensión en runtime
```go
ext, _ := b.LoadExtension(ctx, "/path/to/extension")
log.Printf("Cargada: %s v%s\n", ext.Name, ext.Version)
```
### Caso 3: Usar extensión predefinida
```go
config := browser.DefaultConfig()
ublock, _ := browser.LoadPresetExtension("ublock-origin")
config.Extensions = []*browser.ExtensionConfig{ublock}
b, _ := browser.Launch(ctx, config)
```
### Caso 4: Gestionar extensiones existentes
```go
// Listar todas
exts, _ := b.ListExtensions(ctx)
for _, ext := range exts {
log.Printf("%s: %s\n", ext.Name, ext.Enabled)
}
// Deshabilitar extensión específica
b.DisableExtension(ctx, "extension-id-here")
```
### Caso 5: Configurar extensión
```go
// Configurar uBlock Origin con listas personalizadas
b.SetExtensionSettings(ctx, "cjpalhdlnbpafiamejdnhcphjbkeiagm", map[string]interface{}{
"customFilterLists": []string{
"https://example.com/my-filters.txt",
},
})
```
## Extensiones útiles para automatización
### Stealth y anti-detección
- **Buster**: Solver de CAPTCHAs
- **User-Agent Switcher**: Cambiar user agent
- **Canvas Fingerprint Defender**: Anti-fingerprinting
### Scraping
- **uBlock Origin**: Bloquear ads y trackers
- **Cookie Editor**: Gestión avanzada de cookies
- **Header Editor**: Modificar headers HTTP
### Automatización
- **Tampermonkey**: Ejecutar userscripts personalizados
- **Violentmonkey**: Alternativa a Tampermonkey
### Desarrollo
- **React DevTools**: Inspeccionar componentes React
- **Vue.js DevTools**: Inspeccionar aplicaciones Vue
- **Redux DevTools**: Debugging de estado Redux
## Obtener extensiones
### Chrome Web Store
```bash
# URL de extensión en Chrome Web Store
https://chrome.google.com/webstore/detail/<extension-id>
# Descargar .crx con herramientas
# https://github.com/Rob--W/crxviewer
```
### Desarrollo local
```bash
# Crear extensión simple
mkdir my-extension
cd my-extension
cat > manifest.json <<EOF
{
"manifest_version": 3,
"name": "My Extension",
"version": "1.0",
"description": "Custom extension"
}
EOF
```
## CDP para gestión de extensiones
CDP no tiene soporte directo robusto para extensiones, pero podemos:
1. **Launch flags**: Usar `--load-extension` al inicio
2. **Service workers**: Comunicarse con background scripts de extensión vía `chrome.runtime`
3. **Extension pages**: Navegar a `chrome-extension://<id>/page.html`
4. **Local storage**: Acceder a storage de extensión si es accesible
```go
// Ejecutar código en contexto de extensión
script := fmt.Sprintf(`
chrome.runtime.sendMessage('%s', {action: 'configure'}, response => {
return response;
});
`, extensionID)
```
## Consideraciones especiales
### Manifest V3 vs V2
- Chrome está migrando a Manifest V3
- Algunas extensiones V2 dejarán de funcionar
- Verificar compatibilidad al cargar extensiones
### Permisos
- Extensiones pueden requerir permisos específicos
- Algunas operaciones requieren interacción manual la primera vez
- Considerar pre-configurar permisos en perfil
### Actualizaciones
- Extensiones de Chrome Web Store se actualizan automáticamente
- Extensiones locales no se actualizan
- Implementar sistema de actualización manual si es necesario
### Headless mode
- Algunas extensiones no funcionan en modo headless
- Extensiones con UI pueden requerir modo visible
- Probar compatibilidad con `--headless=new`
## Referencias
- Chrome Extensions: https://developer.chrome.com/docs/extensions/
- Load unpacked extensions: https://developer.chrome.com/docs/extensions/mv3/getstarted/
- Chrome Extension IDs: https://robwu.nl/crxviewer/
- Manifest V3: https://developer.chrome.com/docs/extensions/mv3/intro/
- Extension CLI flags: https://peter.sh/experiments/chromium-command-line-switches/
@@ -0,0 +1,167 @@
# Issue #005: Eliminar timeouts innecesarios del código
**Tipo**: Improvement
**Prioridad**: Alta
**Estado**: Pendiente
## Descripción
Eliminar todos los `time.Sleep()` y timeouts hardcodeados innecesarios del código, reemplazándolos con esperas basadas en eventos CDP cuando sea posible.
## Problema actual
El código tiene múltiples `time.Sleep()` arbitrarios:
- `time.Sleep(2 * time.Second)` en examples/basic.go
- `time.Sleep(3 * time.Second)` en cmd/list_blog.go
- Timeouts hardcodeados en navegación
Estos timeouts son problemáticos porque:
- No se adaptan a velocidad real de carga
- Desperdicían tiempo en conexiones rápidas
- Fallan en conexiones lentas
- Hacen el código menos robusto
## Estrategia de reemplazo
### 1. Eventos CDP de carga de página
En lugar de:
```go
b.Navigate(ctx, url, nil)
time.Sleep(3 * time.Second)
```
Usar eventos CDP:
```go
opts := browser.DefaultNavigateOptions()
opts.WaitUntil = "networkidle" // o "load" o "domcontentloaded"
b.Navigate(ctx, url, opts)
// No sleep necesario, Navigate espera el evento
```
### 2. Esperar por selectores
En lugar de:
```go
time.Sleep(2 * time.Second)
html, _ := b.GetHTML(ctx, ".content")
```
Usar:
```go
b.WaitForSelector(ctx, ".content", 30*time.Second)
html, _ := b.GetHTML(ctx, ".content")
```
### 3. Esperar por condiciones JavaScript
En lugar de:
```go
time.Sleep(1 * time.Second)
result, _ := b.Evaluate(ctx, "window.dataReady")
```
Usar:
```go
b.WaitForFunction(ctx, "window.dataReady === true", 100*time.Millisecond)
result, _ := b.Evaluate(ctx, "window.data")
```
### 4. Eventos de red
Esperar que network esté idle:
```go
// Implementar WaitForNetworkIdle()
b.WaitForNetworkIdle(ctx, 500*time.Millisecond, 30*time.Second)
```
## Eventos CDP útiles
### Page domain
- `Page.loadEventFired` - Página cargada completamente
- `Page.domContentEventFired` - DOM listo
- `Page.frameStoppedLoading` - Frame dejó de cargar
### Network domain
- `Network.requestWillBeSent` - Request iniciado
- `Network.responseReceived` - Response recibida
- `Network.loadingFinished` - Recurso terminó de cargar
- `Network.loadingFailed` - Recurso falló
## Métodos a implementar
```go
// WaitForEvent espera un evento CDP específico
func (b *Browser) WaitForEvent(ctx context.Context, eventName string, timeout time.Duration) error
// WaitForNetworkIdle espera que no haya requests de red por X tiempo
func (b *Browser) WaitForNetworkIdle(ctx context.Context, idleTime, timeout time.Duration) error
// WaitForFunction espera que una función JS retorne true
func (b *Browser) WaitForFunction(ctx context.Context, fn string, checkInterval time.Duration) error
// WaitForNavigation espera que navegación complete
func (b *Browser) WaitForNavigation(ctx context.Context, timeout time.Duration) error
```
## Archivos a revisar y actualizar
- [x] `examples/basic.go` - Eliminar time.Sleep
- [x] `examples/advanced.go` - Reemplazar con esperas basadas en eventos
- [x] `cmd/list_blog.go` - Usar WaitForSelector
- [ ] `pkg/browser/navigation.go` - Mejorar Navigate() para esperar eventos
- [ ] `pkg/browser/browser.go` - Agregar métodos de espera
## Implementación en Navigate()
```go
func (b *Browser) Navigate(ctx context.Context, url string, opts *NavigateOptions) error {
if opts == nil {
opts = DefaultNavigateOptions()
}
// Registrar listener ANTES de navegar
loadedChan := make(chan struct{})
b.client.On("Page.loadEventFired", func() {
close(loadedChan)
})
// Enviar comando de navegación
_, err := b.client.SendCommand(ctx, "Page.navigate", map[string]interface{}{
"url": url,
})
if err != nil {
return err
}
// Esperar evento según opts.WaitUntil
select {
case <-loadedChan:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
```
## Beneficios
**Más rápido**: No espera más de lo necesario
**Más robusto**: Falla con timeout claro, no con misterioso "elemento no encontrado"
**Más confiable**: Se adapta a velocidad real de página
**Mejor UX**: Feedback claro de qué se está esperando
## Testing
Probar con:
- Conexiones rápidas (localhost)
- Conexiones lentas (throttling)
- Páginas con mucho JavaScript
- Páginas con assets pesados
- SPAs (React, Vue) que cargan async
## Referencias
- CDP Page events: https://chromedevtools.github.io/devtools-protocol/tot/Page/#event-loadEventFired
- CDP Network events: https://chromedevtools.github.io/devtools-protocol/tot/Network/
- Puppeteer waitFor: https://pptr.dev/guides/page-interactions#waiting