f72275737a
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
324 lines
8.2 KiB
Go
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
|
|
}
|