Initial commit: navegator - Chrome CDP automation for LLMs
Add complete navegator system for stealthy browser automation: - CDP client with WebSocket communication - Browser API with navigation, storage, network, runtime - Stealth flags and anti-detection scripts - Persistent profile support - Examples and comprehensive documentation Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLaunchBrowser(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Crear directorio temporal para perfiles
|
||||
tempDir := t.TempDir()
|
||||
|
||||
config := DefaultConfig()
|
||||
config.ProfilesBaseDir = tempDir
|
||||
config.ProfileName = "test-launch"
|
||||
config.StealthFlags.Headless = true
|
||||
config.StartTimeout = 15 * time.Second
|
||||
|
||||
// Lanzar navegador
|
||||
b, err := Launch(ctx, config)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to launch browser: %v", err)
|
||||
}
|
||||
defer b.Close()
|
||||
|
||||
// Verificar que el perfil se creó
|
||||
profilePath := filepath.Join(tempDir, "test-launch")
|
||||
if _, err := os.Stat(profilePath); os.IsNotExist(err) {
|
||||
t.Errorf("Profile directory not created: %s", profilePath)
|
||||
}
|
||||
|
||||
// Verificar que tenemos debug URL
|
||||
if b.DebugURL() == "" {
|
||||
t.Error("Debug URL is empty")
|
||||
}
|
||||
|
||||
// Verificar que tenemos target ID
|
||||
if b.TargetID() == "" {
|
||||
t.Error("Target ID is empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNavigate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
tempDir := t.TempDir()
|
||||
config := DefaultConfig()
|
||||
config.ProfilesBaseDir = tempDir
|
||||
config.ProfileName = "test-navigate"
|
||||
config.StealthFlags.Headless = true
|
||||
|
||||
b, err := Launch(ctx, config)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to launch browser: %v", err)
|
||||
}
|
||||
defer b.Close()
|
||||
|
||||
// Navegar a example.com
|
||||
opts := DefaultNavigateOptions()
|
||||
opts.Timeout = 15 * time.Second
|
||||
|
||||
err = b.Navigate(ctx, "https://example.com", opts)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to navigate: %v", err)
|
||||
}
|
||||
|
||||
// Verificar que estamos en la página correcta
|
||||
result, err := b.Evaluate(ctx, "window.location.href")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to evaluate location: %v", err)
|
||||
}
|
||||
|
||||
url, ok := result.Value.(string)
|
||||
if !ok || url != "https://example.com/" {
|
||||
t.Errorf("Expected URL https://example.com/, got %v", result.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScreenshot(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
tempDir := t.TempDir()
|
||||
config := DefaultConfig()
|
||||
config.ProfilesBaseDir = tempDir
|
||||
config.ProfileName = "test-screenshot"
|
||||
config.StealthFlags.Headless = true
|
||||
|
||||
b, err := Launch(ctx, config)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to launch browser: %v", err)
|
||||
}
|
||||
defer b.Close()
|
||||
|
||||
// Navegar
|
||||
opts := DefaultNavigateOptions()
|
||||
opts.Timeout = 15 * time.Second
|
||||
if err := b.Navigate(ctx, "https://example.com", opts); err != nil {
|
||||
t.Logf("Navigation warning: %v", err)
|
||||
}
|
||||
|
||||
// Tomar screenshot
|
||||
screenshot, err := b.Screenshot(ctx, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to take screenshot: %v", err)
|
||||
}
|
||||
|
||||
// Verificar que tiene contenido
|
||||
if len(screenshot) == 0 {
|
||||
t.Error("Screenshot is empty")
|
||||
}
|
||||
|
||||
// Verificar que es PNG válido (empieza con magic bytes)
|
||||
if len(screenshot) < 8 || screenshot[0] != 0x89 || screenshot[1] != 0x50 {
|
||||
t.Error("Screenshot is not a valid PNG")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
tempDir := t.TempDir()
|
||||
config := DefaultConfig()
|
||||
config.ProfilesBaseDir = tempDir
|
||||
config.ProfileName = "test-evaluate"
|
||||
config.StealthFlags.Headless = true
|
||||
|
||||
b, err := Launch(ctx, config)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to launch browser: %v", err)
|
||||
}
|
||||
defer b.Close()
|
||||
|
||||
// Navegar
|
||||
opts := DefaultNavigateOptions()
|
||||
opts.Timeout = 15 * time.Second
|
||||
if err := b.Navigate(ctx, "https://example.com", opts); err != nil {
|
||||
t.Logf("Navigation warning: %v", err)
|
||||
}
|
||||
|
||||
// Evaluar JavaScript
|
||||
result, err := b.Evaluate(ctx, "2 + 2")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to evaluate: %v", err)
|
||||
}
|
||||
|
||||
// Verificar resultado
|
||||
val, ok := result.Value.(float64)
|
||||
if !ok || val != 4 {
|
||||
t.Errorf("Expected 4, got %v", result.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStealthFlags(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
tempDir := t.TempDir()
|
||||
config := DefaultConfig()
|
||||
config.ProfilesBaseDir = tempDir
|
||||
config.ProfileName = "test-stealth"
|
||||
config.StealthFlags.Headless = true
|
||||
|
||||
b, err := Launch(ctx, config)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to launch browser: %v", err)
|
||||
}
|
||||
defer b.Close()
|
||||
|
||||
// Navegar
|
||||
opts := DefaultNavigateOptions()
|
||||
opts.Timeout = 15 * time.Second
|
||||
if err := b.Navigate(ctx, "https://example.com", opts); err != nil {
|
||||
t.Logf("Navigation warning: %v", err)
|
||||
}
|
||||
|
||||
// Verificar navigator.webdriver = false
|
||||
result, err := b.Evaluate(ctx, "navigator.webdriver")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to evaluate webdriver: %v", err)
|
||||
}
|
||||
|
||||
// Debe ser false o undefined
|
||||
if result.Value != false && result.Value != nil {
|
||||
t.Errorf("navigator.webdriver should be false, got %v", result.Value)
|
||||
}
|
||||
|
||||
// Verificar window.chrome existe
|
||||
chromeExists, err := b.Evaluate(ctx, "typeof window.chrome !== 'undefined'")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to check chrome object: %v", err)
|
||||
}
|
||||
|
||||
if chromeExists.Value != true {
|
||||
t.Error("window.chrome should exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecorder(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
recordFile := filepath.Join(tempDir, "test-recording.log")
|
||||
|
||||
recorder, err := NewRecorder(recordFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create recorder: %v", err)
|
||||
}
|
||||
defer recorder.Close()
|
||||
|
||||
// Registrar acción
|
||||
recorder.Record("TestAction", map[string]interface{}{
|
||||
"param1": "value1",
|
||||
"param2": 123,
|
||||
}, "test result", nil)
|
||||
|
||||
recorder.Close()
|
||||
|
||||
// Verificar que el archivo existe y tiene contenido
|
||||
content, err := os.ReadFile(recordFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read recording file: %v", err)
|
||||
}
|
||||
|
||||
if len(content) == 0 {
|
||||
t.Error("Recording file is empty")
|
||||
}
|
||||
|
||||
// Verificar que contiene JSON
|
||||
contentStr := string(content)
|
||||
if !contains(contentStr, "TestAction") {
|
||||
t.Error("Recording doesn't contain action name")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfilePersistence(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
tempDir := t.TempDir()
|
||||
profileName := "test-persistence"
|
||||
|
||||
// Primera sesión: crear perfil
|
||||
config1 := DefaultConfig()
|
||||
config1.ProfilesBaseDir = tempDir
|
||||
config1.ProfileName = profileName
|
||||
config1.StealthFlags.Headless = true
|
||||
|
||||
b1, err := Launch(ctx, config1)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to launch browser (session 1): %v", err)
|
||||
}
|
||||
|
||||
// Verificar perfil creado
|
||||
profilePath := filepath.Join(tempDir, profileName)
|
||||
if _, err := os.Stat(profilePath); os.IsNotExist(err) {
|
||||
t.Fatalf("Profile not created: %s", profilePath)
|
||||
}
|
||||
|
||||
b1.Close()
|
||||
|
||||
// Segunda sesión: reutilizar perfil
|
||||
config2 := DefaultConfig()
|
||||
config2.ProfilesBaseDir = tempDir
|
||||
config2.ProfileName = profileName
|
||||
config2.StealthFlags.Headless = true
|
||||
|
||||
b2, err := Launch(ctx, config2)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to launch browser (session 2): %v", err)
|
||||
}
|
||||
defer b2.Close()
|
||||
|
||||
// Perfil debe seguir existiendo
|
||||
if _, err := os.Stat(profilePath); os.IsNotExist(err) {
|
||||
t.Error("Profile was deleted between sessions")
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && containsHelper(s, substr))
|
||||
}
|
||||
|
||||
func containsHelper(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user