c165f2f788
Agrega 19 issues técnicas documentando funcionalidades implementadas y pendientes. Issues completadas (movidas a dev/issues/completed/): - 001-conversor-web-markdown.md - 002-accessibility-tree.md - 003-gestion-cookies-perfil.md - 004-gestion-extensiones-chrome.md - 005-eliminar-timeouts-innecesarios.md Issues implementadas: - 006-manejo-tabs-ventanas.md - 016-manejo-iframes.md - 017-actions-api.md - 018-file-uploads.md - 019-expected-conditions-mejoradas.md Issues pendientes (media prioridad): - 007-alert-prompt-confirm-handling.md - 008-screenshot-elementos-especificos.md - 009-pdf-generation.md - 010-device-emulation-completo.md - 011-downloads-handling.md Issues pendientes (baja prioridad / avanzado): - 012-browser-contexts-multi-sesion.md - 013-video-recording.md - 014-network-mocking-avanzado.md - 015-geolocation-permissions.md Incluye también dev/NUEVAS_FUNCIONALIDADES.md con resumen completo. Directorio: dev/
7.4 KiB
7.4 KiB
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
// 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
tabs, _ := b.GetTabs(ctx)
for _, tab := range tabs {
log.Printf("Tab %s: %s", tab.ID, tab.Title)
}
Caso 2: Abrir nuevo tab
tabID, _ := b.NewTab(ctx, "https://example.com")
log.Printf("Nuevo tab creado: %s", tabID)
Caso 3: Esperar y cambiar a nuevo tab
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
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
// 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
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
{"method": "Target.getTargets"}
Crear tab
{"method": "Target.createTarget", "params": {"url": "https://example.com"}}
Cerrar tab
{"method": "Target.closeTarget", "params": {"targetId": "ABC123"}}
Activar tab
{"method": "Target.activateTarget", "params": {"targetId": "ABC123"}}
Attach a tab
{"method": "Target.attachToTarget", "params": {"targetId": "ABC123", "flatten": true}}
Eventos CDP
Nuevo tab creado
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
// 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
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/