From f72275737a5dc83e99fd5d5b67bb5c3542041b42 Mon Sep 17 00:00:00 2001 From: Developer Date: Wed, 25 Mar 2026 00:48:15 +0100 Subject: [PATCH] feat: manejo de iframes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- pkg/browser/frames.go | 323 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 pkg/browser/frames.go diff --git a/pkg/browser/frames.go b/pkg/browser/frames.go new file mode 100644 index 0000000..9c0c2e8 --- /dev/null +++ b/pkg/browser/frames.go @@ -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 +}