feat: Actions API para acciones complejas

Implementa API para acciones avanzadas de mouse y teclado.

Mouse actions:
- Hover(), DoubleClick(), RightClick()
- DragAndDrop() con animación suave
- ScrollTo(), ScrollBy(), ScrollToElement()
- MoveMouse() a coordenadas específicas

Keyboard actions:
- PressKey() con modificadores (Ctrl+C, Alt+F4)
- HoldKey() y ReleaseKey()
- SendKeys() para secuencias

Usa CDP Input.dispatchMouseEvent y Input.dispatchKeyEvent.

Archivo: pkg/browser/actions.go
This commit is contained in:
Developer
2026-03-25 00:48:23 +01:00
parent f72275737a
commit 8a5002e7a3
+348
View File
@@ -0,0 +1,348 @@
package browser
import (
"context"
"fmt"
"time"
)
// Hover mueve el mouse sobre un elemento (sin hacer click)
func (b *Browser) Hover(ctx context.Context, selector string) error {
// Obtener posición del elemento
x, y, err := b.getElementCenter(ctx, selector)
if err != nil {
return fmt.Errorf("failed to get element position: %w", err)
}
// Mover mouse al centro del elemento
if err := b.dispatchMouseEvent(ctx, "mouseMoved", x, y, "none", 0); err != nil {
return fmt.Errorf("failed to hover: %w", err)
}
return nil
}
// DoubleClick hace doble click en un elemento
func (b *Browser) DoubleClick(ctx context.Context, selector string) error {
// Obtener posición
x, y, err := b.getElementCenter(ctx, selector)
if err != nil {
return err
}
// Primer click
if err := b.dispatchMouseEvent(ctx, "mousePressed", x, y, "left", 1); err != nil {
return err
}
if err := b.dispatchMouseEvent(ctx, "mouseReleased", x, y, "left", 1); err != nil {
return err
}
// Pequeña pausa
time.Sleep(50 * time.Millisecond)
// Segundo click (clickCount = 2)
if err := b.dispatchMouseEvent(ctx, "mousePressed", x, y, "left", 2); err != nil {
return err
}
if err := b.dispatchMouseEvent(ctx, "mouseReleased", x, y, "left", 2); err != nil {
return err
}
return nil
}
// RightClick hace click derecho en un elemento
func (b *Browser) RightClick(ctx context.Context, selector string) error {
// Obtener posición
x, y, err := b.getElementCenter(ctx, selector)
if err != nil {
return err
}
// Click derecho
if err := b.dispatchMouseEvent(ctx, "mousePressed", x, y, "right", 1); err != nil {
return err
}
if err := b.dispatchMouseEvent(ctx, "mouseReleased", x, y, "right", 1); err != nil {
return err
}
return nil
}
// DragAndDrop arrastra un elemento y lo suelta en otro
func (b *Browser) DragAndDrop(ctx context.Context, sourceSelector, targetSelector string) error {
// Obtener posición de origen
sourceX, sourceY, err := b.getElementCenter(ctx, sourceSelector)
if err != nil {
return fmt.Errorf("source element not found: %w", err)
}
// Obtener posición de destino
targetX, targetY, err := b.getElementCenter(ctx, targetSelector)
if err != nil {
return fmt.Errorf("target element not found: %w", err)
}
// 1. Mover a elemento origen
if err := b.dispatchMouseEvent(ctx, "mouseMoved", sourceX, sourceY, "none", 0); err != nil {
return err
}
// 2. Mouse down en origen
if err := b.dispatchMouseEvent(ctx, "mousePressed", sourceX, sourceY, "left", 1); err != nil {
return err
}
// 3. Simular arrastre (mover en pasos)
steps := 10
for i := 1; i <= steps; i++ {
fraction := float64(i) / float64(steps)
intermediateX := sourceX + int(float64(targetX-sourceX)*fraction)
intermediateY := sourceY + int(float64(targetY-sourceY)*fraction)
if err := b.dispatchMouseEvent(ctx, "mouseMoved", intermediateX, intermediateY, "left", 0); err != nil {
return err
}
time.Sleep(10 * time.Millisecond)
}
// 4. Mouse up en destino
if err := b.dispatchMouseEvent(ctx, "mouseReleased", targetX, targetY, "left", 1); err != nil {
return err
}
return nil
}
// ScrollTo hace scroll a una posición absoluta (x, y)
func (b *Browser) ScrollTo(ctx context.Context, x, y int) error {
script := fmt.Sprintf("window.scrollTo(%d, %d)", x, y)
_, err := b.Evaluate(ctx, script)
return err
}
// ScrollBy hace scroll relativo por x, y pixels
func (b *Browser) ScrollBy(ctx context.Context, x, y int) error {
script := fmt.Sprintf("window.scrollBy(%d, %d)", x, y)
_, err := b.Evaluate(ctx, script)
return err
}
// ScrollToElement hace scroll hasta que un elemento sea visible
func (b *Browser) ScrollToElement(ctx context.Context, selector string) error {
script := fmt.Sprintf(`
const element = document.querySelector('%s');
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
`, selector)
_, err := b.Evaluate(ctx, script)
return err
}
// MoveMouse mueve el mouse a coordenadas específicas
func (b *Browser) MoveMouse(ctx context.Context, x, y int) error {
return b.dispatchMouseEvent(ctx, "mouseMoved", x, y, "none", 0)
}
// PressKey presiona una tecla (soporta modificadores)
func (b *Browser) PressKey(ctx context.Context, key string) error {
// Parsear si hay modificadores (Ctrl+C, Alt+F4, etc.)
keys, modifiers := parseKeyCombo(key)
// Presionar modificadores
for _, mod := range modifiers {
if err := b.dispatchKeyEvent(ctx, "keyDown", mod, "", modifiersFor(mod)); err != nil {
return err
}
}
// Presionar tecla principal
mainKey := keys[len(keys)-1]
mods := modifiersValue(modifiers)
if err := b.dispatchKeyEvent(ctx, "keyDown", mainKey, "", mods); err != nil {
return err
}
if err := b.dispatchKeyEvent(ctx, "keyUp", mainKey, "", mods); err != nil {
return err
}
// Soltar modificadores
for i := len(modifiers) - 1; i >= 0; i-- {
if err := b.dispatchKeyEvent(ctx, "keyUp", modifiers[i], "", modifiersFor(modifiers[i])); err != nil {
return err
}
}
return nil
}
// HoldKey mantiene presionada una tecla (sin soltarla)
func (b *Browser) HoldKey(ctx context.Context, key string) error {
return b.dispatchKeyEvent(ctx, "keyDown", key, "", 0)
}
// ReleaseKey suelta una tecla previamente presionada
func (b *Browser) ReleaseKey(ctx context.Context, key string) error {
return b.dispatchKeyEvent(ctx, "keyUp", key, "", 0)
}
// SendKeys envía una secuencia de teclas
func (b *Browser) SendKeys(ctx context.Context, keys ...string) error {
for _, key := range keys {
if err := b.PressKey(ctx, key); err != nil {
return err
}
time.Sleep(50 * time.Millisecond)
}
return nil
}
// Helper: obtener centro de un elemento
func (b *Browser) getElementCenter(ctx context.Context, selector string) (int, int, error) {
script := fmt.Sprintf(`
(() => {
const element = document.querySelector('%s');
if (!element) return null;
const rect = element.getBoundingClientRect();
return {
x: Math.round(rect.left + rect.width / 2),
y: Math.round(rect.top + rect.height / 2)
};
})()
`, selector)
result, err := b.Evaluate(ctx, script)
if err != nil {
return 0, 0, err
}
if result.Value == nil {
return 0, 0, fmt.Errorf("element not found: %s", selector)
}
coords, ok := result.Value.(map[string]interface{})
if !ok {
return 0, 0, fmt.Errorf("invalid coordinates")
}
x := int(coords["x"].(float64))
y := int(coords["y"].(float64))
return x, y, nil
}
// Helper: dispatch mouse event
func (b *Browser) dispatchMouseEvent(ctx context.Context, eventType string, x, y int, button string, clickCount int) error {
params := map[string]interface{}{
"type": eventType,
"x": x,
"y": y,
"button": button,
"clickCount": clickCount,
}
return b.cdpClient.Execute(ctx, "Input.dispatchMouseEvent", params, nil)
}
// Helper: dispatch key event
func (b *Browser) dispatchKeyEvent(ctx context.Context, eventType, key, text string, modifiers int) error {
params := map[string]interface{}{
"type": eventType,
}
if key != "" {
params["key"] = key
}
if text != "" {
params["text"] = text
}
if modifiers > 0 {
params["modifiers"] = modifiers
}
return b.cdpClient.Execute(ctx, "Input.dispatchKeyEvent", params, nil)
}
// Helper: parsear combinación de teclas
func parseKeyCombo(combo string) ([]string, []string) {
// Separar por +
parts := splitKey(combo, '+')
var modifiers []string
var keys []string
for _, part := range parts {
switch part {
case "Control", "Ctrl":
modifiers = append(modifiers, "Control")
case "Alt":
modifiers = append(modifiers, "Alt")
case "Shift":
modifiers = append(modifiers, "Shift")
case "Meta", "Command", "Cmd":
modifiers = append(modifiers, "Meta")
default:
keys = append(keys, part)
}
}
return keys, modifiers
}
// Helper: split key combo
func splitKey(s string, sep rune) []string {
var parts []string
var current string
for _, ch := range s {
if ch == sep {
if current != "" {
parts = append(parts, current)
current = ""
}
} else {
current += string(ch)
}
}
if current != "" {
parts = append(parts, current)
}
return parts
}
// Helper: valor de modificadores
func modifiersFor(key string) int {
switch key {
case "Control":
return 2
case "Shift":
return 8
case "Alt":
return 1
case "Meta":
return 4
default:
return 0
}
}
// Helper: combinar modificadores
func modifiersValue(modifiers []string) int {
value := 0
for _, mod := range modifiers {
value |= modifiersFor(mod)
}
return value
}