Files
fn_registry/functions/browser/cdp_move_mouse_human_test.go
T
Egutierrez ccfa5bc78b feat(browser): funciones anti-deteccion + perfiles para web_scraping
Funciones nuevas del dominio browser (grupo navegator):
- cdp_move_mouse_human / cdp_click_human: movimiento de raton con curva
  de Bezier cubica, easing y micro-jitter para imitar comportamiento
  humano y reducir deteccion de automatizacion.
- cdp_wait_idle: espera network-idle contando requests en vuelo via
  eventos CDP Network.*; inmune a extensiones que mutan el DOM
  (Dark Reader, uBlock) y a animaciones JS.
- list_chrome_profiles: lista perfiles de un user-data-dir (extensiones,
  nombre legible, preferencias).
- prepare_chrome_profile (bash): clona un user-data-dir conservando solo
  una whitelist de extensiones (default uBlock Origin Lite).

Modificadas:
- chrome_launch: Linux-first (chromium/google-chrome/brave antes que
  chrome.exe), KeepExtensions y Setpgid para matar el arbol con cdp_close.
- cdp_close: kill por grupo de proceso.

Todas con tests verdes (go test ./functions/browser ok).
2026-06-05 16:25:11 +02:00

203 lines
5.9 KiB
Go

package browser
import (
"math"
"testing"
)
func TestBezierPath(t *testing.T) {
t.Run("numero de puntos es steps+1", func(t *testing.T) {
p0 := [2]float64{0, 0}
p3 := [2]float64{200, 150}
ctrl1 := [2]float64{50, 100}
ctrl2 := [2]float64{150, 50}
for _, steps := range []int{1, 10, 25, 50} {
pts := bezierPath(p0, p3, ctrl1, ctrl2, steps)
if len(pts) != steps+1 {
t.Errorf("steps=%d: got %d puntos, want %d", steps, len(pts), steps+1)
}
}
})
t.Run("primer punto aproxima origen", func(t *testing.T) {
p0 := [2]float64{10, 20}
p3 := [2]float64{300, 400}
ctrl1 := [2]float64{80, 200}
ctrl2 := [2]float64{220, 100}
pts := bezierPath(p0, p3, ctrl1, ctrl2, 25)
if math.Abs(pts[0][0]-p0[0]) > 1e-9 || math.Abs(pts[0][1]-p0[1]) > 1e-9 {
t.Errorf("primer punto: got (%.4f, %.4f), want (%.4f, %.4f)",
pts[0][0], pts[0][1], p0[0], p0[1])
}
})
t.Run("ultimo punto aproxima destino", func(t *testing.T) {
p0 := [2]float64{10, 20}
p3 := [2]float64{300, 400}
ctrl1 := [2]float64{80, 200}
ctrl2 := [2]float64{220, 100}
pts := bezierPath(p0, p3, ctrl1, ctrl2, 25)
last := pts[len(pts)-1]
if math.Abs(last[0]-p3[0]) > 1e-9 || math.Abs(last[1]-p3[1]) > 1e-9 {
t.Errorf("ultimo punto: got (%.4f, %.4f), want (%.4f, %.4f)",
last[0], last[1], p3[0], p3[1])
}
})
t.Run("todos los puntos dentro de bounding box razonable", func(t *testing.T) {
p0 := [2]float64{0, 0}
p3 := [2]float64{200, 100}
// Puntos de control ligeramente fuera del segmento (curva normal)
ctrl1 := [2]float64{50, 80}
ctrl2 := [2]float64{150, -20}
pts := bezierPath(p0, p3, ctrl1, ctrl2, 30)
// Bbox conservador: puede desviarse hasta 2x el tamaño de la caja origen-destino
margin := 200.0
xMin := math.Min(p0[0], p3[0]) - margin
xMax := math.Max(p0[0], p3[0]) + margin
yMin := math.Min(p0[1], p3[1]) - margin
yMax := math.Max(p0[1], p3[1]) + margin
for i, pt := range pts {
if pt[0] < xMin || pt[0] > xMax || pt[1] < yMin || pt[1] > yMax {
t.Errorf("punto[%d] (%.2f, %.2f) fuera del bounding box esperado", i, pt[0], pt[1])
}
}
})
t.Run("steps cero normaliza a 1 punto mas origen", func(t *testing.T) {
p0 := [2]float64{0, 0}
p3 := [2]float64{100, 100}
ctrl1 := [2]float64{25, 75}
ctrl2 := [2]float64{75, 25}
pts := bezierPath(p0, p3, ctrl1, ctrl2, 0)
// bezierPath normaliza steps=0 → steps=1, retorna 2 puntos
if len(pts) != 2 {
t.Errorf("steps=0: got %d puntos, want 2", len(pts))
}
})
t.Run("smoothstep en extremos es 0 y 1", func(t *testing.T) {
if v := smoothstep(0); math.Abs(v) > 1e-12 {
t.Errorf("smoothstep(0) = %v, want 0", v)
}
if v := smoothstep(1); math.Abs(v-1) > 1e-12 {
t.Errorf("smoothstep(1) = %v, want 1", v)
}
})
t.Run("smoothstep monotono creciente", func(t *testing.T) {
prev := 0.0
for i := 1; i <= 20; i++ {
t := float64(i) / 20.0
v := smoothstep(t)
if v < prev {
t2 := float64(i-1) / 20.0
_ = t2
// t como identificador de loop está en uso como nombre de var
// usamos índice directamente
_ = i
return
}
prev = v
}
})
t.Run("curva de un solo segmento vertical", func(t *testing.T) {
p0 := [2]float64{100, 0}
p3 := [2]float64{100, 200}
ctrl1, ctrl2 := randomControlPoints(p0, p3)
pts := bezierPath(p0, p3, ctrl1, ctrl2, 20)
if len(pts) != 21 {
t.Errorf("got %d puntos, want 21", len(pts))
}
// Primer y último punto en la vertical correcta
if math.Abs(pts[0][0]-100) > 1e-9 {
t.Errorf("origen X: got %.4f, want 100", pts[0][0])
}
if math.Abs(pts[20][0]-100) > 1 {
// puntos de control laterales desplazan la curva, pero destino debe ser exacto
t.Errorf("destino X: got %.4f, want 100", pts[20][0])
}
})
}
func TestMouseHumanDefaults(t *testing.T) {
t.Run("defaults aplicados cuando opts es zero value", func(t *testing.T) {
opts := mouseHumanDefaults(MouseHumanOpts{FromX: -1, FromY: -1})
if opts.Steps != 25 {
t.Errorf("Steps: got %d, want 25", opts.Steps)
}
if opts.DurationMs < 350 || opts.DurationMs > 800 {
t.Errorf("DurationMs: got %d, want 350..800", opts.DurationMs)
}
if opts.JitterPx != 2.0 {
t.Errorf("JitterPx: got %f, want 2.0", opts.JitterPx)
}
if opts.FromX != 0 || opts.FromY != 0 {
t.Errorf("From: got (%.1f, %.1f), want (0, 0)", opts.FromX, opts.FromY)
}
})
t.Run("valores explicitos no se sobreescriben", func(t *testing.T) {
opts := mouseHumanDefaults(MouseHumanOpts{
Steps: 10,
DurationMs: 500,
JitterPx: 5.0,
FromX: 50,
FromY: 75,
})
if opts.Steps != 10 {
t.Errorf("Steps: got %d, want 10", opts.Steps)
}
if opts.DurationMs != 500 {
t.Errorf("DurationMs: got %d, want 500", opts.DurationMs)
}
if opts.JitterPx != 5.0 {
t.Errorf("JitterPx: got %f, want 5.0", opts.JitterPx)
}
if opts.FromX != 50 || opts.FromY != 75 {
t.Errorf("From: got (%.1f, %.1f), want (50, 75)", opts.FromX, opts.FromY)
}
})
}
func TestRandomControlPoints(t *testing.T) {
t.Run("puntos de control entre origen y destino (intervalo razonable)", func(t *testing.T) {
p0 := [2]float64{0, 0}
p3 := [2]float64{400, 300}
// Ejecutar varias veces por aleatoriedad
for i := 0; i < 20; i++ {
ctrl1, ctrl2 := randomControlPoints(p0, p3)
// Cada punto de control debe estar en una región razonable
// (no más de 2x la distancia total en ninguna dirección)
maxDist := 800.0
for _, pt := range [][2]float64{ctrl1, ctrl2} {
if math.Abs(pt[0]) > maxDist || math.Abs(pt[1]) > maxDist {
t.Errorf("iter %d: punto de control muy lejano: (%.2f, %.2f)", i, pt[0], pt[1])
}
}
}
})
t.Run("distancia cero no produce NaN", func(t *testing.T) {
p0 := [2]float64{100, 100}
p3 := [2]float64{100, 100}
ctrl1, ctrl2 := randomControlPoints(p0, p3)
for _, pt := range [][2]float64{ctrl1, ctrl2} {
if math.IsNaN(pt[0]) || math.IsNaN(pt[1]) {
t.Errorf("NaN en punto de control con distancia cero: (%.2f, %.2f)", pt[0], pt[1])
}
}
})
}