diff --git a/dev/NUEVAS_FUNCIONALIDADES.md b/dev/NUEVAS_FUNCIONALIDADES.md new file mode 100644 index 0000000..15869de --- /dev/null +++ b/dev/NUEVAS_FUNCIONALIDADES.md @@ -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 +│ └── / +│ └── 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 diff --git a/dev/issues/006-manejo-tabs-ventanas.md b/dev/issues/006-manejo-tabs-ventanas.md new file mode 100644 index 0000000..4f79ffc --- /dev/null +++ b/dev/issues/006-manejo-tabs-ventanas.md @@ -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/ diff --git a/dev/issues/007-alert-prompt-confirm-handling.md b/dev/issues/007-alert-prompt-confirm-handling.md new file mode 100644 index 0000000..122f550 --- /dev/null +++ b/dev/issues/007-alert-prompt-confirm-handling.md @@ -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 + + + + + + + + + + +``` + +### 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/ diff --git a/dev/issues/008-screenshot-elementos-especificos.md b/dev/issues/008-screenshot-elementos-especificos.md new file mode 100644 index 0000000..6c2ac7a --- /dev/null +++ b/dev/issues/008-screenshot-elementos-especificos.md @@ -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 diff --git a/dev/issues/009-pdf-generation.md b/dev/issues/009-pdf-generation.md new file mode 100644 index 0000000..89fa2dc --- /dev/null +++ b/dev/issues/009-pdf-generation.md @@ -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: ` +
+ +
+ `, + FooterTemplate: ` +
+ Página de +
+ `, +} + +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": "
Header
", + "footerTemplate": "
Footer
", + "preferCSSPageSize": false, + "generateTaggedPDF": false + } +} + +// Response: +{ + "data": "base64_encoded_pdf_data..." +} +``` + +## Variables en templates + +### Header/Footer templates soportan: +- `` - Fecha actual +- `` - Título de la página +- `` - URL de la página +- `` - Número de página actual +- `` - Total de páginas + +### Ejemplo de template completo +```html +
+
+ +
+
+ +
+
+``` + +## 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: `
+ Reporte generado: +
`, + FooterTemplate: `
+ / +
`, +} + +b.SavePDF(ctx, "reporte.pdf", opts) +``` + +### PDF con contenido dinámico +```go +// Generar contenido dinámico +b.Evaluate(ctx, ` + document.body.innerHTML = '

Reporte Dinámico

'; + for (let i = 1; i <= 10; i++) { + document.body.innerHTML += '

Elemento ' + i + '

'; + } +`) + +// 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 diff --git a/dev/issues/010-device-emulation-completo.md b/dev/issues/010-device-emulation-completo.md new file mode 100644 index 0000000..50ef942 --- /dev/null +++ b/dev/issues/010-device-emulation-completo.md @@ -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 diff --git a/dev/issues/011-downloads-handling.md b/dev/issues/011-downloads-handling.md new file mode 100644 index 0000000..5e19f69 --- /dev/null +++ b/dev/issues/011-downloads-handling.md @@ -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 diff --git a/dev/issues/012-browser-contexts-multi-sesion.md b/dev/issues/012-browser-contexts-multi-sesion.md new file mode 100644 index 0000000..71bde5d --- /dev/null +++ b/dev/issues/012-browser-contexts-multi-sesion.md @@ -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 diff --git a/dev/issues/013-video-recording.md b/dev/issues/013-video-recording.md new file mode 100644 index 0000000..108bd43 --- /dev/null +++ b/dev/issues/013-video-recording.md @@ -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 diff --git a/dev/issues/014-network-mocking-avanzado.md b/dev/issues/014-network-mocking-avanzado.md new file mode 100644 index 0000000..61482e1 --- /dev/null +++ b/dev/issues/014-network-mocking-avanzado.md @@ -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 diff --git a/dev/issues/015-geolocation-permissions.md b/dev/issues/015-geolocation-permissions.md new file mode 100644 index 0000000..bbe0259 --- /dev/null +++ b/dev/issues/015-geolocation-permissions.md @@ -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 diff --git a/dev/issues/016-manejo-iframes.md b/dev/issues/016-manejo-iframes.md new file mode 100644 index 0000000..d1ca091 --- /dev/null +++ b/dev/issues/016-manejo-iframes.md @@ -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 diff --git a/dev/issues/017-actions-api.md b/dev/issues/017-actions-api.md new file mode 100644 index 0000000..0a75fe3 --- /dev/null +++ b/dev/issues/017-actions-api.md @@ -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 diff --git a/dev/issues/018-file-uploads.md b/dev/issues/018-file-uploads.md new file mode 100644 index 0000000..fd863e0 --- /dev/null +++ b/dev/issues/018-file-uploads.md @@ -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 `` +- 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 diff --git a/dev/issues/019-expected-conditions-mejoradas.md b/dev/issues/019-expected-conditions-mejoradas.md new file mode 100644 index 0000000..0b9d292 --- /dev/null +++ b/dev/issues/019-expected-conditions-mejoradas.md @@ -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 diff --git a/dev/issues/completed/001-conversor-web-markdown.md b/dev/issues/completed/001-conversor-web-markdown.md new file mode 100644 index 0000000..9c33ff2 --- /dev/null +++ b/dev/issues/completed/001-conversor-web-markdown.md @@ -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 diff --git a/dev/issues/completed/002-accessibility-tree.md b/dev/issues/completed/002-accessibility-tree.md new file mode 100644 index 0000000..473af3c --- /dev/null +++ b/dev/issues/completed/002-accessibility-tree.md @@ -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 diff --git a/dev/issues/completed/003-gestion-cookies-perfil.md b/dev/issues/completed/003-gestion-cookies-perfil.md new file mode 100644 index 0000000..301cdb7 --- /dev/null +++ b/dev/issues/completed/003-gestion-cookies-perfil.md @@ -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// +├── 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 diff --git a/dev/issues/completed/004-gestion-extensiones-chrome.md b/dev/issues/completed/004-gestion-extensiones-chrome.md new file mode 100644 index 0000000..4e3682b --- /dev/null +++ b/dev/issues/completed/004-gestion-extensiones-chrome.md @@ -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/ +│ └── / +│ └── Extensions/ # Extensiones instaladas del perfil +│ └── / +│ └── / +└── 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/ + +# 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 </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/ diff --git a/dev/issues/completed/005-eliminar-timeouts-innecesarios.md b/dev/issues/completed/005-eliminar-timeouts-innecesarios.md new file mode 100644 index 0000000..42516b8 --- /dev/null +++ b/dev/issues/completed/005-eliminar-timeouts-innecesarios.md @@ -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