bab0836507
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
312 lines
6.8 KiB
Go
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)
|
|
}
|