Files
Developer f72275737a feat: manejo de iframes
Implementa capacidad para trabajar con elementos dentro de iframes.

Incluye:
- SwitchToFrame() por selector CSS
- SwitchToFrameByName() y SwitchToFrameByIndex()
- SwitchToMainFrame() para volver al contexto principal
- GetFrames() para listar árbol de frames
- WaitForFrame() para esperar carga
- EvaluateInFrame() para ejecutar JS en frame específico

Usa CDP Page.getFrameTree y manejo de execution contexts.

Archivo: pkg/browser/frames.go
2026-03-25 00:48:15 +01:00

324 lines
8.2 KiB
Go

package browser
import (
"context"
"fmt"
)
// Frame representa un iframe o frame
type Frame struct {
ID string
ParentID string
URL string
Name string
FrameTree []*Frame // Sub-frames
}
// currentFrameID almacena el frame actual del navegador
var currentFrameID string
// SwitchToFrame cambia el contexto a un iframe usando un selector CSS
func (b *Browser) SwitchToFrame(ctx context.Context, selector string) error {
// 1. Obtener el node del iframe
nodeID, err := b.querySelector(ctx, selector)
if err != nil {
return fmt.Errorf("frame not found with selector %s: %w", selector, err)
}
// 2. Obtener el frameId del node
var result struct {
Node struct {
FrameID string `json:"frameId"`
ContentDocument struct {
NodeID int `json:"nodeId"`
} `json:"contentDocument"`
} `json:"node"`
}
if err := b.cdpClient.Execute(ctx, "DOM.describeNode", map[string]interface{}{
"nodeId": nodeID,
}, &result); err != nil {
return fmt.Errorf("failed to describe frame node: %w", err)
}
if result.Node.FrameID == "" {
return fmt.Errorf("element is not a frame")
}
// 3. Guardar el frameID actual
currentFrameID = result.Node.FrameID
return nil
}
// SwitchToFrameByIndex cambia a un iframe por su índice (0-based)
func (b *Browser) SwitchToFrameByIndex(ctx context.Context, index int) error {
selector := fmt.Sprintf("iframe:nth-of-type(%d)", index+1)
return b.SwitchToFrame(ctx, selector)
}
// SwitchToFrameByName cambia a un iframe por su atributo name o id
func (b *Browser) SwitchToFrameByName(ctx context.Context, name string) error {
// Intentar primero por name
selector := fmt.Sprintf("iframe[name='%s']", name)
err := b.SwitchToFrame(ctx, selector)
if err == nil {
return nil
}
// Si falla, intentar por id
selector = fmt.Sprintf("iframe#%s", name)
return b.SwitchToFrame(ctx, selector)
}
// SwitchToMainFrame vuelve al contexto del frame principal
func (b *Browser) SwitchToMainFrame(ctx context.Context) error {
currentFrameID = ""
return nil
}
// GetFrames obtiene el árbol de frames de la página
func (b *Browser) GetFrames(ctx context.Context) ([]*Frame, error) {
var result struct {
FrameTree struct {
Frame frameInfo `json:"frame"`
ChildFrames []frameTree `json:"childFrames"`
} `json:"frameTree"`
}
if err := b.cdpClient.Execute(ctx, "Page.getFrameTree", nil, &result); err != nil {
return nil, fmt.Errorf("failed to get frame tree: %w", err)
}
// Convertir el árbol a lista plana de frames
frames := []*Frame{
{
ID: result.FrameTree.Frame.ID,
ParentID: result.FrameTree.Frame.ParentID,
URL: result.FrameTree.Frame.URL,
Name: result.FrameTree.Frame.Name,
},
}
// Agregar frames hijos recursivamente
frames = append(frames, flattenFrameTree(result.FrameTree.ChildFrames, result.FrameTree.Frame.ID)...)
return frames, nil
}
// frameInfo estructura para información de frame de CDP
type frameInfo struct {
ID string `json:"id"`
ParentID string `json:"parentId"`
URL string `json:"url"`
Name string `json:"name"`
}
// frameTree estructura recursiva de CDP
type frameTree struct {
Frame frameInfo `json:"frame"`
ChildFrames []frameTree `json:"childFrames"`
}
// flattenFrameTree convierte árbol de frames a lista plana
func flattenFrameTree(trees []frameTree, parentID string) []*Frame {
var frames []*Frame
for _, tree := range trees {
frame := &Frame{
ID: tree.Frame.ID,
ParentID: parentID,
URL: tree.Frame.URL,
Name: tree.Frame.Name,
}
frames = append(frames, frame)
// Recursivamente agregar sub-frames
if len(tree.ChildFrames) > 0 {
frames = append(frames, flattenFrameTree(tree.ChildFrames, tree.Frame.ID)...)
}
}
return frames
}
// GetCurrentFrame obtiene el frame actual
func (b *Browser) GetCurrentFrame(ctx context.Context) (*Frame, error) {
if currentFrameID == "" {
// Estamos en el frame principal
frames, err := b.GetFrames(ctx)
if err != nil {
return nil, err
}
if len(frames) > 0 {
return frames[0], nil // Frame principal
}
return nil, fmt.Errorf("no frames found")
}
// Buscar el frame actual
frames, err := b.GetFrames(ctx)
if err != nil {
return nil, err
}
for _, frame := range frames {
if frame.ID == currentFrameID {
return frame, nil
}
}
return nil, fmt.Errorf("current frame not found: %s", currentFrameID)
}
// WaitForFrame espera a que un frame aparezca y cargue
func (b *Browser) WaitForFrame(ctx context.Context, selector string) error {
// Esperar a que el elemento iframe aparezca
if err := b.WaitForSelector(ctx, selector, 30*1000); err != nil {
return fmt.Errorf("frame selector not found: %w", err)
}
// Cambiar al frame
if err := b.SwitchToFrame(ctx, selector); err != nil {
return err
}
// Esperar a que el frame termine de cargar
// Evaluar readyState en el contexto del frame
script := `document.readyState === 'complete'`
result, err := b.evaluateInCurrentFrame(ctx, script)
if err != nil {
return err
}
if ready, ok := result.Value.(bool); !ok || !ready {
return fmt.Errorf("frame did not finish loading")
}
return nil
}
// evaluateInCurrentFrame ejecuta JavaScript en el frame actual
func (b *Browser) evaluateInCurrentFrame(ctx context.Context, script string) (*EvaluateResult, error) {
params := map[string]interface{}{
"expression": script,
"returnByValue": true,
}
// Si estamos en un frame específico, agregar el frameId
if currentFrameID != "" {
// Necesitamos obtener el execution context del frame
var contextResult struct {
Contexts []struct {
ID int `json:"id"`
FrameID string `json:"frameId"`
} `json:"contexts"`
}
if err := b.cdpClient.Execute(ctx, "Runtime.executionContexts", nil, &contextResult); err != nil {
return nil, fmt.Errorf("failed to get execution contexts: %w", err)
}
// Buscar el contexto del frame actual
for _, context := range contextResult.Contexts {
if context.FrameID == currentFrameID {
params["contextId"] = context.ID
break
}
}
}
var result struct {
Result struct {
Type string `json:"type"`
Value interface{} `json:"value"`
} `json:"result"`
}
if err := b.cdpClient.Execute(ctx, "Runtime.evaluate", params, &result); err != nil {
return nil, fmt.Errorf("failed to evaluate in frame: %w", err)
}
return &EvaluateResult{
Type: result.Result.Type,
Value: result.Result.Value,
}, nil
}
// EvaluateInFrame ejecuta JavaScript en un frame específico sin cambiar el contexto
func (b *Browser) EvaluateInFrame(ctx context.Context, frameID string, script string) (*EvaluateResult, error) {
// Guardar frame actual
previousFrame := currentFrameID
// Temporalmente cambiar al frame especificado
currentFrameID = frameID
// Ejecutar script
result, err := b.evaluateInCurrentFrame(ctx, script)
// Restaurar frame anterior
currentFrameID = previousFrame
return result, err
}
// CountFrames cuenta el número total de frames en la página
func (b *Browser) CountFrames(ctx context.Context) (int, error) {
frames, err := b.GetFrames(ctx)
if err != nil {
return 0, err
}
return len(frames), nil
}
// GetFrameByName busca un frame por su atributo name
func (b *Browser) GetFrameByName(ctx context.Context, name string) (*Frame, error) {
frames, err := b.GetFrames(ctx)
if err != nil {
return nil, err
}
for _, frame := range frames {
if frame.Name == name {
return frame, nil
}
}
return nil, fmt.Errorf("frame not found with name: %s", name)
}
// GetFrameByURL busca un frame por coincidencia parcial de URL
func (b *Browser) GetFrameByURL(ctx context.Context, urlPattern string) (*Frame, error) {
frames, err := b.GetFrames(ctx)
if err != nil {
return nil, err
}
for _, frame := range frames {
if containsString(frame.URL, urlPattern) {
return frame, nil
}
}
return nil, fmt.Errorf("frame not found with URL pattern: %s", urlPattern)
}
// containsString verifica si haystack contiene needle
func containsString(haystack, needle string) bool {
return len(haystack) >= len(needle) && findSubstring(haystack, needle)
}
// findSubstring busca substring
func findSubstring(s, sub string) bool {
if sub == "" {
return true
}
for i := 0; i <= len(s)-len(sub); i++ {
if s[i:i+len(sub)] == sub {
return true
}
}
return false
}