# 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/