package main import ( "fmt" "time" "fn-registry/functions/infra" ) // StepResult es el resultado de ejecutar un paso. type StepResult struct { Index int Action string Output string // resultado de evaluate/get_html, path de screenshot, etc. Err error Elapsed time.Duration } // RunnerOpts configura la ejecucion del runner. type RunnerOpts struct { AbortOnError bool } // Runner ejecuta los pasos de un Script sobre una conexion CDP activa. type Runner struct { conn *infra.CDPConn opts RunnerOpts } // NewRunner crea un Runner con la conexion CDP dada. func NewRunner(conn *infra.CDPConn, opts RunnerOpts) *Runner { return &Runner{conn: conn, opts: opts} } // Run ejecuta todos los pasos del script y retorna los resultados de cada paso. // Siempre retorna todos los resultados procesados hasta el momento, incluso si aborta. func (r *Runner) Run(script *Script) ([]StepResult, error) { results := make([]StepResult, 0, len(script.Steps)) for i, step := range script.Steps { start := time.Now() output, err := r.runStep(step) elapsed := time.Since(start) res := StepResult{ Index: i, Action: step.Action, Output: output, Err: err, Elapsed: elapsed, } results = append(results, res) if err != nil { if step.ContinueOnError { // Continuar con el siguiente paso aunque este fallo continue } if r.opts.AbortOnError { return results, fmt.Errorf("step[%d] %s: %w", i, step.Action, err) } // Por defecto: abortar si el paso fallo y no tiene continue_on_error return results, fmt.Errorf("step[%d] %s: %w", i, step.Action, err) } } return results, nil } // runStep ejecuta un paso individual y retorna su output y error. func (r *Runner) runStep(step Step) (string, error) { switch step.Action { case "navigate": if err := infra.CdpNavigate(r.conn, step.URL); err != nil { return "", err } // Esperar a que la página cargue completamente timeout := time.Duration(step.TimeoutMs) * time.Millisecond if timeout <= 0 { timeout = 15 * time.Second } return "", infra.CdpWaitLoad(r.conn, timeout) case "wait_load": timeout := time.Duration(step.TimeoutMs) * time.Millisecond if timeout <= 0 { timeout = 15 * time.Second } return "", infra.CdpWaitLoad(r.conn, timeout) case "wait": timeout := time.Duration(step.TimeoutMs) * time.Millisecond if timeout <= 0 { timeout = 10 * time.Second } return "", infra.CdpWaitElement(r.conn, step.Selector, timeout) case "click": return "", infra.CdpClick(r.conn, step.Selector) case "type": // Hacer click primero para enfocar el elemento if err := infra.CdpClick(r.conn, step.Selector); err != nil { return "", fmt.Errorf("enfocar elemento para type: %w", err) } return "", infra.CdpTypeText(r.conn, step.Text) case "screenshot": opts := infra.CdpScreenshotOpts{ FullPage: step.FullPage, Format: "png", } if err := infra.CdpScreenshot(r.conn, step.Path, opts); err != nil { return "", err } return step.Path, nil case "evaluate": result, err := infra.CdpEvaluate(r.conn, step.Expr) if err != nil { return "", err } return result, nil case "get_html": html, err := infra.CdpGetHTML(r.conn) if err != nil { return "", err } // Truncar para el log (el HTML puede ser muy largo) if len(html) > 200 { return html[:200] + "...", nil } return html, nil case "sleep": time.Sleep(time.Duration(step.Ms) * time.Millisecond) return fmt.Sprintf("slept %dms", step.Ms), nil default: return "", fmt.Errorf("accion desconocida: %q", step.Action) } }