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 }