Files
navegator/pkg/browser/tabs.go
T
Developer bab0836507 feat: manejo de múltiples tabs/ventanas
Implementa gestión completa de tabs del navegador.

Incluye:
- GetTabs() para listar todos los tabs
- NewTab() para crear nuevos tabs
- CloseTab() y CloseOtherTabs()
- SwitchToTab() para cambiar foco
- WaitForNewTab() con callback de acción
- GetTabByURL() y GetTabByTitle() para búsqueda
- OnTabCreated() para eventos

Usa CDP Target domain para comunicación.

Archivo: pkg/browser/tabs.go
2026-03-25 00:48:07 +01:00

312 lines
6.8 KiB
Go

package browser
import (
"context"
"encoding/json"
"fmt"
"sync"
)
// Tab representa un tab del navegador
type Tab struct {
ID string
URL string
Title string
Type string // "page" | "background_page" | ...
Attached bool
}
// tabHandler almacena handlers para eventos de tabs
type tabHandler struct {
onCreate func(*Tab)
}
var (
tabHandlers = &tabHandler{}
tabMutex sync.RWMutex
)
// GetTabs obtiene todos los tabs abiertos
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
}
// NewTab crea un nuevo tab y retorna su ID
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
}
// CloseTab cierra un tab específico
func (b *Browser) CloseTab(ctx context.Context, tabID string) error {
params := map[string]interface{}{
"targetId": tabID,
}
var result struct {
Success bool `json:"success"`
}
if err := b.cdpClient.Execute(ctx, "Target.closeTarget", params, &result); err != nil {
return fmt.Errorf("failed to close tab: %w", err)
}
if !result.Success {
return fmt.Errorf("failed to close tab: CDP returned success=false")
}
return nil
}
// SwitchToTab cambia el foco a un tab específico
func (b *Browser) SwitchToTab(ctx context.Context, tabID string) error {
// Activar tab
activateParams := map[string]interface{}{
"targetId": tabID,
}
if err := b.cdpClient.Execute(ctx, "Target.activateTarget", activateParams, nil); err != nil {
return fmt.Errorf("failed to activate tab: %w", err)
}
// Attach al tab si no está attached
attachParams := map[string]interface{}{
"targetId": tabID,
"flatten": true,
}
var attachResult struct {
SessionID string `json:"sessionId"`
}
if err := b.cdpClient.Execute(ctx, "Target.attachToTarget", attachParams, &attachResult); err != nil {
// Puede que ya esté attached, continuar
}
// Actualizar targetID actual del browser
b.targetID = tabID
return nil
}
// GetCurrentTab obtiene el tab actual
func (b *Browser) GetCurrentTab(ctx context.Context) (*Tab, error) {
tabs, err := b.GetTabs(ctx)
if err != nil {
return nil, err
}
// Buscar el tab con el targetID actual
for _, tab := range tabs {
if tab.ID == b.targetID {
return tab, nil
}
}
// Si no encontramos, retornar el primero
if len(tabs) > 0 {
return tabs[0], nil
}
return nil, fmt.Errorf("no tabs found")
}
// WaitForNewTab espera a que se abra un nuevo tab y lo retorna
func (b *Browser) WaitForNewTab(ctx context.Context, action func()) (*Tab, error) {
// Obtener tabs actuales
currentTabs, err := b.GetTabs(ctx)
if err != nil {
return nil, err
}
currentIDs := make(map[string]bool)
for _, tab := range currentTabs {
currentIDs[tab.ID] = true
}
// Canal para recibir nuevo tab
newTabChan := make(chan *Tab, 1)
// Registrar listener temporal para nuevos tabs
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"`
}
if err := json.Unmarshal(params, &event); err != nil {
return
}
// Solo procesar tabs de tipo "page"
if event.TargetInfo.Type == "page" {
// Verificar que es un tab nuevo
if !currentIDs[event.TargetInfo.TargetID] {
newTab := &Tab{
ID: event.TargetInfo.TargetID,
URL: event.TargetInfo.URL,
Title: event.TargetInfo.Title,
Type: event.TargetInfo.Type,
}
select {
case newTabChan <- newTab:
default:
}
}
}
})
// Ejecutar acción que abrirá el tab
if action != nil {
action()
}
// Esperar nuevo tab
select {
case newTab := <-newTabChan:
return newTab, nil
case <-ctx.Done():
return nil, fmt.Errorf("timeout waiting for new tab: %w", ctx.Err())
}
}
// OnTabCreated registra callback para cuando se crea un nuevo tab
func (b *Browser) OnTabCreated(handler func(*Tab)) error {
tabMutex.Lock()
defer tabMutex.Unlock()
tabHandlers.onCreate = handler
// Registrar listener de eventos
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"`
}
if err := json.Unmarshal(params, &event); err != nil {
return
}
if event.TargetInfo.Type == "page" {
tab := &Tab{
ID: event.TargetInfo.TargetID,
URL: event.TargetInfo.URL,
Title: event.TargetInfo.Title,
Type: event.TargetInfo.Type,
}
tabMutex.RLock()
if tabHandlers.onCreate != nil {
tabHandlers.onCreate(tab)
}
tabMutex.RUnlock()
}
})
return nil
}
// CloseOtherTabs cierra todos los tabs excepto el actual
func (b *Browser) CloseOtherTabs(ctx context.Context) error {
currentTab, err := b.GetCurrentTab(ctx)
if err != nil {
return err
}
tabs, err := b.GetTabs(ctx)
if err != nil {
return err
}
for _, tab := range tabs {
if tab.ID != currentTab.ID {
if err := b.CloseTab(ctx, tab.ID); err != nil {
// Continuar cerrando otros tabs incluso si uno falla
continue
}
}
}
return nil
}
// GetTabByURL busca un tab por URL (coincidencia parcial)
func (b *Browser) GetTabByURL(ctx context.Context, urlPattern string) (*Tab, error) {
tabs, err := b.GetTabs(ctx)
if err != nil {
return nil, err
}
for _, tab := range tabs {
if containsString(tab.URL, urlPattern) {
return tab, nil
}
}
return nil, fmt.Errorf("no tab found with URL pattern: %s", urlPattern)
}
// GetTabByTitle busca un tab por título (coincidencia parcial)
func (b *Browser) GetTabByTitle(ctx context.Context, titlePattern string) (*Tab, error) {
tabs, err := b.GetTabs(ctx)
if err != nil {
return nil, err
}
for _, tab := range tabs {
if containsString(tab.Title, titlePattern) {
return tab, nil
}
}
return nil, fmt.Errorf("no tab found with title pattern: %s", titlePattern)
}