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