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
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user