package browser import ( "strings" "testing" "time" ) // TestCdpWaitIdleDefaults verifica el comportamiento observable de CdpWaitIdle // sin requerir una instancia Chrome real. func TestCdpWaitIdleDefaults(t *testing.T) { t.Run("conexion nula retorna error inmediato", func(t *testing.T) { err := CdpWaitIdle(nil, CdpWaitIdleOpts{}) if err == nil { t.Fatal("esperaba error para conexion nula, got nil") } }) t.Run("opts con ceros aplica defaults antes de usar", func(t *testing.T) { // Zero-value de CdpWaitIdleOpts debe tener todos los campos en 0 // para que la logica de defaults sea alcanzable. var opts CdpWaitIdleOpts if opts.QuietMs != 0 || opts.Timeout != 0 || opts.MaxInflight != 0 || opts.PollMs != 0 { t.Fatal("zero-value de CdpWaitIdleOpts debe tener todos los campos en 0") } }) t.Run("mensaje de error nil-conn menciona cdp wait idle", func(t *testing.T) { err := CdpWaitIdle(nil, CdpWaitIdleOpts{ QuietMs: 100, Timeout: 500 * time.Millisecond, PollMs: 50, }) if err == nil { t.Fatal("esperaba error, got nil") } if !strings.Contains(err.Error(), "cdp wait idle") { t.Errorf("mensaje de error %q no contiene 'cdp wait idle'", err.Error()) } }) } // TestInflightTracker cubre el nucleo puro del contador de red. No requiere Chrome: // alimenta secuencias de eventos {requestId, resourceType} y verifica el conteo. func TestInflightTracker(t *testing.T) { t.Run("golden: carga normal llega a idle", func(t *testing.T) { tr := NewInflightTracker() tr.OnRequest("r1", "Document") tr.OnRequest("r2", "Script") tr.OnRequest("r3", "Image") if got := tr.Inflight(); got != 3 { t.Fatalf("inflight tras 3 requests = %d, esperaba 3", got) } tr.OnFinish("r1") tr.OnFinish("r2") tr.OnFail("r3") // un recurso que falla tambien deja de estar en vuelo if got := tr.Inflight(); got != 0 { t.Fatalf("inflight tras completar todo = %d, esperaba 0", got) } if !tr.IsIdle(0) { t.Error("esperaba IsIdle(0)=true con inflight=0") } }) t.Run("edge: analytics residual idle ok con MaxInflight=2", func(t *testing.T) { tr := NewInflightTracker() // La pagina cargo, pero 2 beacons de analytics quedan sin finalizar. tr.OnRequest("doc", "Document") tr.OnFinish("doc") tr.OnRequest("beacon1", "Ping") tr.OnRequest("beacon2", "XHR") if got := tr.Inflight(); got != 2 { t.Fatalf("inflight = %d, esperaba 2 (beacons residuales)", got) } if tr.IsIdle(0) { t.Error("con MaxInflight=0 NO deberia ser idle (2 beacons en vuelo)") } if !tr.IsIdle(2) { t.Error("con MaxInflight=2 (default) SI deberia ser idle") } }) t.Run("error/regresion: WebSocket abierto NO impide idle", func(t *testing.T) { tr := NewInflightTracker() tr.OnRequest("doc", "Document") tr.OnFinish("doc") // Un stream WebSocket se abre y nunca emite loadingFinished. tr.OnRequest("ws1", "WebSocket") // Un EventSource (SSE) tampoco termina. tr.OnRequest("sse1", "EventSource") if got := tr.Inflight(); got != 0 { t.Fatalf("inflight = %d, esperaba 0 (WS/SSE excluidos)", got) } if !tr.IsIdle(0) { t.Error("con WS+SSE abiertos pero excluidos, deberia ser idle absoluto") } }) t.Run("edge: finish de request no trackeado es no-op (no va negativo)", func(t *testing.T) { tr := NewInflightTracker() // loadingFinished de un requestId que nunca contamos (p.ej. el handshake // de un WebSocket excluido) no debe romper el conteo. tr.OnFinish("desconocido") tr.OnFail("ws-handshake") if got := tr.Inflight(); got != 0 { t.Fatalf("inflight = %d, esperaba 0 (no negativo)", got) } tr.OnRequest("r1", "Fetch") if got := tr.Inflight(); got != 1 { t.Fatalf("inflight tras un request real = %d, esperaba 1", got) } }) t.Run("edge: requestId duplicado no infla el conteo", func(t *testing.T) { tr := NewInflightTracker() tr.OnRequest("r1", "Fetch") tr.OnRequest("r1", "Fetch") // mismo id (redirect re-emite) if got := tr.Inflight(); got != 1 { t.Fatalf("inflight = %d, esperaba 1 (id deduplicado)", got) } }) }