docs: issues técnicas para nuevas funcionalidades
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/
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
# 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/
|
||||
Reference in New Issue
Block a user