3253828fef
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>
292 lines
6.8 KiB
Go
292 lines
6.8 KiB
Go
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
|
|
}
|