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/
307 lines
7.4 KiB
Markdown
307 lines
7.4 KiB
Markdown
# 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/
|