Compare commits
2 Commits
167a7e5eb7
...
83f1d7c8d3
| Author | SHA1 | Date | |
|---|---|---|---|
| 83f1d7c8d3 | |||
| 216cad4c12 |
@@ -51,12 +51,12 @@ func CdpClickXYHuman(c *CDPConn, x, y float64, opts MouseHumanOpts) error {
|
||||
}
|
||||
|
||||
// clickPauseMs devuelve la pausa (ms) entre press y release según el modo de
|
||||
// velocidad: human 30-90, fast 5-15, instant 0.
|
||||
// velocidad: human 30-90, auto/fast 5-15, instant 0.
|
||||
func clickPauseMs(mode string) int {
|
||||
switch mode {
|
||||
case "instant":
|
||||
return 0
|
||||
case "fast":
|
||||
case "fast", "auto":
|
||||
return 5 + rand.Intn(11) // 5..15
|
||||
default: // "human" o ""
|
||||
return 30 + rand.Intn(61) // 30..90
|
||||
|
||||
@@ -14,8 +14,16 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// cdpCmdTimeout es el tope que sendCDP espera por la respuesta a un comando antes
|
||||
// de rendirse. Sin el, una respuesta que Chrome nunca envia (tab cerrada a media
|
||||
// peticion, proceso colgado) bloquearia la goroutine del tool para siempre — el
|
||||
// agente lo percibe como "lentitud infinita". Con el timeout, el tool falla limpio
|
||||
// y el retry de withConn puede reconectar.
|
||||
const cdpCmdTimeout = 30 * time.Second
|
||||
|
||||
// EventHandler es invocado cuando llega un evento CDP del metodo subscrito.
|
||||
// El handler corre en la goroutine del readLoop — debe ser rapido o despachar
|
||||
// a un canal/goroutine propio. params puede ser nil si Chrome no envia.
|
||||
@@ -36,6 +44,15 @@ type CDPConn struct {
|
||||
handlers map[string][]EventHandler
|
||||
hMu sync.Mutex
|
||||
|
||||
// axEnabled/netEnabled/pageEnabled cachean si ya enviamos el enable de cada
|
||||
// dominio CDP en esta conexion. enable/disable es idempotente pero cuesta un
|
||||
// round-trip; en el hot path del agente (percibir->actuar repetido) re-enviar
|
||||
// Accessibility.enable / Network.enable en cada llamada duplica los RTT.
|
||||
// Habilitar una vez y cachear el flag elimina ese coste por percepcion/espera.
|
||||
axEnabled atomic.Bool
|
||||
netEnabled atomic.Bool
|
||||
pageEnabled atomic.Bool
|
||||
|
||||
// frameCtx cachea el executionContextId del isolated world por frameID, para
|
||||
// que CdpEvalInFrame no cree un mundo aislado nuevo en cada llamada.
|
||||
// frameCtxMu protege solo el lazy-init del puntero (el cache tiene su mutex).
|
||||
@@ -250,12 +267,60 @@ func (c *CDPConn) sendCDP(method string, params map[string]any) (map[string]any,
|
||||
return nil, fmt.Errorf("cdp send %s: %w", method, err)
|
||||
}
|
||||
|
||||
// Esperar respuesta
|
||||
resp := <-ch
|
||||
if resp.Error != nil {
|
||||
return nil, fmt.Errorf("cdp %s: error %d: %s", method, resp.Error.Code, resp.Error.Message)
|
||||
// Esperar respuesta (con timeout para no colgar el tool indefinidamente).
|
||||
select {
|
||||
case resp := <-ch:
|
||||
if resp.Error != nil {
|
||||
return nil, fmt.Errorf("cdp %s: error %d: %s", method, resp.Error.Code, resp.Error.Message)
|
||||
}
|
||||
return resp.Result, nil
|
||||
case <-time.After(cdpCmdTimeout):
|
||||
c.pendMu.Lock()
|
||||
delete(c.pending, id)
|
||||
c.pendMu.Unlock()
|
||||
return nil, fmt.Errorf("cdp %s: sin respuesta tras %s (conexion colgada?)", method, cdpCmdTimeout)
|
||||
}
|
||||
return resp.Result, nil
|
||||
}
|
||||
|
||||
// ensureAX habilita el dominio Accessibility una sola vez por conexion (necesario
|
||||
// antes de Accessibility.getFullAXTree). Idempotente y cacheado: la segunda y
|
||||
// sucesivas llamadas son no-op, evitando un round-trip por percepcion.
|
||||
func (c *CDPConn) ensureAX() error {
|
||||
if c.axEnabled.Load() {
|
||||
return nil
|
||||
}
|
||||
if _, err := c.sendCDP("Accessibility.enable", nil); err != nil {
|
||||
return err
|
||||
}
|
||||
c.axEnabled.Store(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureNetwork habilita el dominio Network una sola vez por conexion. Cacheado:
|
||||
// no lo deshabilitamos al terminar una espera (eso borraria el estado y forzaria
|
||||
// el enable de nuevo); los handlers de eventos se desregistran por su cancel().
|
||||
func (c *CDPConn) ensureNetwork() error {
|
||||
if c.netEnabled.Load() {
|
||||
return nil
|
||||
}
|
||||
if _, err := c.sendCDP("Network.enable", nil); err != nil {
|
||||
return err
|
||||
}
|
||||
c.netEnabled.Store(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensurePage habilita el dominio Page una sola vez por conexion (necesario para
|
||||
// recibir Page.loadEventFired y demas eventos de ciclo de vida de la pagina).
|
||||
func (c *CDPConn) ensurePage() error {
|
||||
if c.pageEnabled.Load() {
|
||||
return nil
|
||||
}
|
||||
if _, err := c.sendCDP("Page.enable", nil); err != nil {
|
||||
return err
|
||||
}
|
||||
c.pageEnabled.Store(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
// readLoop lee mensajes del WebSocket y los enruta a los canales pendientes
|
||||
|
||||
@@ -72,8 +72,10 @@ func CdpGetAXOutline(c *CDPConn, frameID string, maxChars int) (string, error) {
|
||||
return "", fmt.Errorf("cdp get ax outline: conexion nula")
|
||||
}
|
||||
|
||||
// Accessibility.enable es idempotente; necesario antes de getFullAXTree.
|
||||
if _, err := c.sendCDP("Accessibility.enable", nil); err != nil {
|
||||
// Accessibility.enable (idempotente, cacheado por conexion): necesario antes de
|
||||
// getFullAXTree. Cachear el flag evita un round-trip extra en cada percepcion,
|
||||
// que es la operacion mas frecuente del bucle percibir->actuar del agente.
|
||||
if err := c.ensureAX(); err != nil {
|
||||
return "", fmt.Errorf("cdp get ax outline: Accessibility.enable: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,11 +9,12 @@ import (
|
||||
|
||||
// MouseHumanOpts configura el movimiento humano del ratón.
|
||||
type MouseHumanOpts struct {
|
||||
// Mode es la política de velocidad: "human" (default, ""), "fast" o "instant".
|
||||
// Controla los defaults de Steps/DurationMs/JitterPx y la pausa press/release:
|
||||
// Mode es la política de velocidad: "auto"/"fast" (rápido), "human" (sigiloso,
|
||||
// también "") o "instant". Controla los defaults de Steps/DurationMs/JitterPx y
|
||||
// la pausa press/release:
|
||||
// - auto/fast: recta ~5 pts, 40-80ms, jitter mínimo (eventos de ratón reales,
|
||||
// rápido — modo por defecto del MCP para automatización propia).
|
||||
// - human: Bézier ~25 pts, 350-800ms, jitter 2px (sigilo anti-bot alto).
|
||||
// - fast: recta ~5 pts, 40-80ms, jitter mínimo (eventos de ratón reales,
|
||||
// para scraping masivo propio).
|
||||
// - instant: sin movimiento de ratón (CdpMoveMouseHuman es no-op); el click
|
||||
// por #ref usa element.click() JS. Para tests y fallback sin bbox.
|
||||
// Los valores explícitos (Steps/DurationMs/JitterPx != 0) ganan al preset del modo.
|
||||
@@ -37,7 +38,7 @@ type MouseHumanOpts struct {
|
||||
// Un modo desconocido se trata como "human" (el más seguro).
|
||||
func MouseProfileForMode(mode string) MouseHumanOpts {
|
||||
switch mode {
|
||||
case "fast", "instant", "human", "":
|
||||
case "auto", "fast", "instant", "human", "":
|
||||
return MouseHumanOpts{Mode: mode, FromX: -1, FromY: -1}
|
||||
default:
|
||||
return MouseHumanOpts{Mode: "human", FromX: -1, FromY: -1}
|
||||
@@ -56,14 +57,14 @@ func mouseHumanDefaults(opts MouseHumanOpts) MouseHumanOpts {
|
||||
opts.DurationMs = 1
|
||||
}
|
||||
// JitterPx se queda en 0.
|
||||
case "fast":
|
||||
case "fast", "auto":
|
||||
if opts.Steps <= 0 {
|
||||
opts.Steps = 5
|
||||
}
|
||||
if opts.DurationMs <= 0 {
|
||||
opts.DurationMs = 40 + rand.Intn(41) // 40..80
|
||||
}
|
||||
// JitterPx se queda en lo recibido (0 por defecto, sin jitter en fast).
|
||||
// JitterPx se queda en lo recibido (0 por defecto, sin jitter en fast/auto).
|
||||
default: // "human" o ""
|
||||
if opts.Steps <= 0 {
|
||||
opts.Steps = 25
|
||||
|
||||
@@ -14,3 +14,17 @@ func CdpTypeRef(c *CDPConn, backendNodeID int, text string) error {
|
||||
}
|
||||
return CdpTypeText(c, text)
|
||||
}
|
||||
|
||||
// CdpTypeRefFast enfoca el elemento del #ref e inserta el texto en UN solo
|
||||
// round-trip (Input.insertText), sin teclear caracter por caracter. Es el camino
|
||||
// rápido del modo automático: equivale a focus(ref) → CdpInsertText. Para sitios
|
||||
// con detección por pulsación usa CdpTypeRef (modo human, char por char).
|
||||
func CdpTypeRefFast(c *CDPConn, backendNodeID int, text string) error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("cdp type ref fast: conexión nil")
|
||||
}
|
||||
if _, err := c.sendCDP("DOM.focus", map[string]any{"backendNodeId": backendNodeID}); err != nil {
|
||||
return fmt.Errorf("cdp type ref fast: focus ref %d: %w", backendNodeID, err)
|
||||
}
|
||||
return CdpInsertText(c, text)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ name: cdp_type_ref
|
||||
kind: function
|
||||
lang: go
|
||||
domain: browser
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
purity: impure
|
||||
signature: "func CdpTypeRef(c *CDPConn, backendNodeID int, text string) error"
|
||||
description: "Enfoca el elemento identificado por su #ref del AX outline vía DOM.focus y escribe el texto dado usando CdpTypeText. El #ref es el backendDOMNodeId estable del nodo DOM. El elemento debe aceptar input de texto (input, textarea, contenteditable)."
|
||||
description: "Enfoca el elemento identificado por su #ref del AX outline vía DOM.focus y escribe el texto dado usando CdpTypeText (carácter a carácter, camino human). El #ref es el backendDOMNodeId estable del nodo DOM. Para el camino rápido (un solo round-trip Input.insertText) hay CdpTypeRefFast. El elemento debe aceptar input de texto (input, textarea, contenteditable)."
|
||||
tags: [cdp, browser, action, ref, humanized, navegator]
|
||||
uses_functions: [cdp_type_text_go_browser]
|
||||
uses_types: []
|
||||
@@ -49,3 +49,7 @@ Tras `page_perceive` / `render_ax_outline`, cuando el agente quiere escribir en
|
||||
- `DOM.focus` falla si el elemento no es focusable (no es `input`, `textarea`, `contenteditable`, o similar). El error indica el ref y la causa.
|
||||
- Si el elemento necesita un click previo para activarse (algunos inputs con JS custom), combinar con `CdpClickRef` antes de `CdpTypeRef`.
|
||||
- No hace scroll previo — si el elemento no está visible en el viewport el focus CDP puede fallar en algunos navegadores. Combinar con `CdpClickRef` (que sí hace scroll) si hay dudas.
|
||||
|
||||
## Capability growth log
|
||||
|
||||
- v1.1.0 (2026-06-13) — Nueva función hermana `CdpTypeRefFast`: enfoca el #ref e inserta el texto en un solo round-trip (`Input.insertText`) en vez de teclear carácter a carácter. Es el camino rápido del modo automático del MCP (`dom_type_ref` con `mode=auto`); `CdpTypeRef` queda como el camino human (carácter a carácter con pausas aleatorias) para sitios con detección por pulsación.
|
||||
|
||||
@@ -2,27 +2,38 @@ package browser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CdpTypeText escribe texto en el elemento activo de la pagina caracter por caracter.
|
||||
// Usa Input.dispatchKeyEvent para simular pulsaciones de teclado reales.
|
||||
// Recomienda usar CdpClick primero para enfocar el elemento objetivo.
|
||||
// assertEditableFocus verifica que el activeElement de la pagina acepta texto
|
||||
// (input/textarea/select/contentEditable). Sin foco, los caracteres se pierden
|
||||
// silenciosamente (van a document.body); devolvemos un error claro en vez de
|
||||
// "escribir a la nada". Compartido por CdpTypeText (camino human) y CdpInsertText
|
||||
// (camino rapido).
|
||||
func assertEditableFocus(c *CDPConn) error {
|
||||
focus, ferr := CdpEvaluate(c, `(function(){var a=document.activeElement;if(!a)return 'none';var t=a.tagName.toLowerCase();return (t==='input'||t==='textarea'||t==='select'||a.isContentEditable)?'ok':t;})()`)
|
||||
if ferr != nil {
|
||||
return fmt.Errorf("verificar foco: %w", ferr)
|
||||
}
|
||||
if strings.TrimSpace(focus) != "ok" {
|
||||
return fmt.Errorf("no hay campo de texto enfocado (activeElement: %s); enfoca el input primero", strings.TrimSpace(focus))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CdpTypeText escribe texto en el elemento activo de la pagina caracter por
|
||||
// caracter, con una pausa ALEATORIA entre teclas. Es el camino "human": emite
|
||||
// keyDown/keyUp reales por tecla (sitios que validan pulsacion a pulsacion
|
||||
// reaccionan) y el ritmo irregular reduce la deteccion de automatizacion. Para el
|
||||
// camino rapido (modo auto) usa CdpInsertText: un solo round-trip, sin teclear.
|
||||
func CdpTypeText(c *CDPConn, text string) error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("cdp type text: conexion nula")
|
||||
}
|
||||
|
||||
// Verificar que hay un campo editable enfocado. Sin foco, los caracteres se
|
||||
// pierden silenciosamente (van a document.body). Devolvemos error claro en vez
|
||||
// de "escribir a la nada".
|
||||
focus, ferr := CdpEvaluate(c, `(function(){var a=document.activeElement;if(!a)return 'none';var t=a.tagName.toLowerCase();return (t==='input'||t==='textarea'||t==='select'||a.isContentEditable)?'ok':t;})()`)
|
||||
if ferr != nil {
|
||||
return fmt.Errorf("cdp type text: verificar foco: %w", ferr)
|
||||
}
|
||||
if strings.TrimSpace(focus) != "ok" {
|
||||
return fmt.Errorf("cdp type text: no hay campo de texto enfocado (activeElement: %s); usa CdpClick sobre el input primero", strings.TrimSpace(focus))
|
||||
if err := assertEditableFocus(c); err != nil {
|
||||
return fmt.Errorf("cdp type text: %w", err)
|
||||
}
|
||||
|
||||
// keyDown (con `text`) ya inserta el caracter en el elemento focado en
|
||||
@@ -49,9 +60,28 @@ func CdpTypeText(c *CDPConn, text string) error {
|
||||
return fmt.Errorf("cdp type text: keyUp %q: %w", charStr, err)
|
||||
}
|
||||
|
||||
// Pequena pausa entre caracteres para simular escritura humana.
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
// Pausa ALEATORIA entre caracteres (15-65 ms) para imitar el ritmo
|
||||
// irregular de un humano escribiendo, en vez de un intervalo de maquina fijo.
|
||||
time.Sleep(time.Duration(15+rand.Intn(51)) * time.Millisecond)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CdpInsertText inserta todo el texto en el elemento enfocado en UN solo
|
||||
// round-trip via Input.insertText. Es el camino rapido del modo automatico: no
|
||||
// emite keyDown/keyUp por tecla, por lo que sitios que validan pulsacion a
|
||||
// pulsacion (autocompletes muy estrictos) pueden no reaccionar — para esos casos
|
||||
// usa CdpTypeText (modo human). Requiere un campo editable enfocado.
|
||||
func CdpInsertText(c *CDPConn, text string) error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("cdp insert text: conexion nula")
|
||||
}
|
||||
if err := assertEditableFocus(c); err != nil {
|
||||
return fmt.Errorf("cdp insert text: %w", err)
|
||||
}
|
||||
if _, err := c.sendCDP("Input.insertText", map[string]any{"text": text}); err != nil {
|
||||
return fmt.Errorf("cdp insert text: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@ name: cdp_type_text
|
||||
kind: function
|
||||
lang: go
|
||||
domain: browser
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
purity: impure
|
||||
signature: "func CdpTypeText(c *CDPConn, text string) error"
|
||||
description: "Escribe texto en el elemento activo de la pagina caracter por caracter via Input.dispatchKeyEvent. Envia eventos keyDown, char y keyUp por cada caracter con 10ms de pausa entre ellos. Usar CdpClick primero para enfocar el elemento."
|
||||
tags: [chrome, cdp, browser, automation, keyboard, input, devtools]
|
||||
description: "Escribe texto en el elemento activo de la pagina caracter por caracter via Input.dispatchKeyEvent (camino human). Envia keyDown+keyUp por cada caracter con una pausa ALEATORIA (15-65ms) que imita el ritmo irregular humano. Para el camino rapido (un solo round-trip, sin teclear) usa CdpInsertText. Usar CdpClick primero para enfocar el elemento."
|
||||
tags: [chrome, cdp, browser, automation, keyboard, input, devtools, navegator]
|
||||
uses_functions: [cdp_connect_go_browser]
|
||||
uses_types: []
|
||||
returns: []
|
||||
@@ -39,4 +39,10 @@ CdpTypeText(conn, "golang websocket")
|
||||
|
||||
## Notas
|
||||
|
||||
Envia tres eventos por caracter: `keyDown`, `char` (dispara el evento `input` del DOM) y `keyUp`. La pausa de 10ms entre caracteres simula escritura humana y ayuda con inputs que tienen debounce. Para texto largo, considerar inyectar directamente via `CdpEvaluate` con `element.value = "..."` + evento `input`.
|
||||
Envia dos eventos por caracter: `keyDown` (con `text`, que ya inserta el caracter en Chrome) y `keyUp`. No envia un evento `char` extra: lo duplicaba en sitios que reaccionan a eventos `input` (DuckDuckGo, Google). La pausa ALEATORIA de 15-65ms entre caracteres imita el ritmo irregular humano (reduce deteccion) y ayuda con inputs que tienen debounce.
|
||||
|
||||
Para el camino rapido del modo automatico hay `CdpInsertText` (todo el texto en un solo `Input.insertText`, sin keyDown/keyUp por tecla) — mucho mas rapido, pero sitios que validan pulsacion a pulsacion pueden no reaccionar. Para texto largo donde no importa el sigilo, `CdpInsertText` es preferible.
|
||||
|
||||
## Capability growth log
|
||||
|
||||
- v1.1.0 (2026-06-13) — La pausa entre caracteres pasa de 10ms fija a aleatoria 15-65ms (ritmo no-máquina). Nueva función hermana `CdpInsertText`: inserta todo el texto en un solo round-trip (`Input.insertText`) para el modo automático rápido. Se extrajo el chequeo de foco a `assertEditableFocus` (compartido).
|
||||
|
||||
@@ -136,11 +136,14 @@ func CdpWaitIdle(c *CDPConn, opts CdpWaitIdleOpts) error {
|
||||
})
|
||||
defer cancel3()
|
||||
|
||||
// Habilitar dominio Network (igual que cdp_har_record).
|
||||
if _, err := c.sendCDP("Network.enable", nil); err != nil {
|
||||
// Habilitar dominio Network (idempotente, cacheado por conexion). NO lo
|
||||
// deshabilitamos al salir: Network.disable borraria el estado y el siguiente
|
||||
// wait_idle pagaria el enable de nuevo (round-trip extra). Los handlers de
|
||||
// eventos se desregistran por sus cancel() de defer, que es lo unico necesario
|
||||
// para dejar de contar.
|
||||
if err := c.ensureNetwork(); err != nil {
|
||||
return fmt.Errorf("cdp wait idle: Network.enable: %w", err)
|
||||
}
|
||||
defer c.sendCDP("Network.disable", nil) //nolint:errcheck
|
||||
|
||||
deadline := time.Now().Add(opts.Timeout)
|
||||
pollInterval := time.Duration(opts.PollMs) * time.Millisecond
|
||||
|
||||
@@ -6,9 +6,11 @@ import (
|
||||
)
|
||||
|
||||
// CdpWaitLoad espera a que la página actual termine de cargar completamente.
|
||||
// Hace polling de document.readyState via Runtime.evaluate cada 200ms hasta
|
||||
// que sea "complete", o hasta que se agote el timeout.
|
||||
// Retorna error si el timeout se agota o si CdpEvaluate falla (conexion rota).
|
||||
// Bloquea hasta recibir el evento CDP Page.loadEventFired (sin polling): suscribe
|
||||
// el evento via OnEvent y espera en un canal con timeout. Antes de esperar hace un
|
||||
// fast path comprobando document.readyState — si la página ya está "complete",
|
||||
// retorna de inmediato sin armar el handler.
|
||||
// Retorna error si el timeout se agota o si no logra habilitar el dominio Page.
|
||||
func CdpWaitLoad(c *CDPConn, timeout time.Duration) error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("cdp wait load: conexion nula")
|
||||
@@ -17,19 +19,35 @@ func CdpWaitLoad(c *CDPConn, timeout time.Duration) error {
|
||||
timeout = 30 * time.Second
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(timeout)
|
||||
interval := 200 * time.Millisecond
|
||||
|
||||
for time.Now().Before(deadline) {
|
||||
result, err := CdpEvaluate(c, "document.readyState")
|
||||
if err != nil {
|
||||
return fmt.Errorf("cdp wait load: error evaluando readyState: %w", err)
|
||||
}
|
||||
if result == "complete" {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(interval)
|
||||
// Fast path: si el documento ya terminó de cargar, no esperamos eventos.
|
||||
if rs, err := CdpEvaluate(c, "document.readyState"); err == nil && rs == "complete" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("cdp wait load: pagina no cargo despues de %s", timeout)
|
||||
// Habilitar Page (idempotente, cacheado) y suscribir el evento de carga.
|
||||
if err := c.ensurePage(); err != nil {
|
||||
return fmt.Errorf("cdp wait load: Page.enable: %w", err)
|
||||
}
|
||||
loaded := make(chan struct{}, 1)
|
||||
cancel := c.OnEvent("Page.loadEventFired", func(_ string, _ map[string]any) {
|
||||
select {
|
||||
case loaded <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
})
|
||||
defer cancel()
|
||||
|
||||
// Re-chequear readyState tras suscribir: si la carga terminó entre el fast
|
||||
// path y el registro del handler, ya no llegaría el evento (carrera) — lo
|
||||
// captamos aquí en vez de colgarnos hasta el timeout.
|
||||
if rs, err := CdpEvaluate(c, "document.readyState"); err == nil && rs == "complete" {
|
||||
return nil
|
||||
}
|
||||
|
||||
select {
|
||||
case <-loaded:
|
||||
return nil
|
||||
case <-time.After(timeout):
|
||||
return fmt.Errorf("cdp wait load: pagina no cargo despues de %s", timeout)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@ name: cdp_wait_load
|
||||
kind: function
|
||||
lang: go
|
||||
domain: browser
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
purity: impure
|
||||
signature: "func CdpWaitLoad(c *CDPConn, timeout time.Duration) error"
|
||||
description: "Espera a que la pagina actual termine de cargar completamente. Hace polling de document.readyState via Runtime.evaluate cada 200ms hasta que sea \"complete\", o hasta que se agote el timeout. Retorna error inmediato si CdpEvaluate falla (la conexion puede estar rota)."
|
||||
tags: [chrome, cdp, browser, automation, wait, polling, devtools, readystate, load]
|
||||
description: "Espera a que la pagina actual termine de cargar completamente. Bloquea hasta recibir el evento CDP Page.loadEventFired (sin polling), con un fast path inicial de document.readyState: si ya esta complete, retorna de inmediato. Retorna error si se agota el timeout o si no logra habilitar el dominio Page."
|
||||
tags: [chrome, cdp, browser, automation, wait, event, devtools, readystate, load, loadeventfired, navegator]
|
||||
uses_functions: [cdp_evaluate_go_browser]
|
||||
uses_types: []
|
||||
returns: []
|
||||
@@ -42,6 +42,10 @@ html, _ := CdpGetHTML(conn)
|
||||
|
||||
## Notas
|
||||
|
||||
A diferencia de `CdpWaitElement`, que ignora errores de `CdpEvaluate` durante el polling (la pagina puede aun no estar lista), `CdpWaitLoad` retorna el error inmediatamente porque un fallo en `document.readyState` indica una conexion rota, no una condicion transitoria.
|
||||
Bloquea esperando el evento CDP `Page.loadEventFired` (sin polling). Antes de esperar hace un fast path con `document.readyState`: si la página ya está `complete`, retorna de inmediato sin armar el handler. Tras suscribir el evento re-chequea `readyState` una vez más para no perder la carga por una carrera entre el fast path y el registro del handler. Habilita el dominio `Page` vía `ensurePage` (cacheado por conexión, idempotente).
|
||||
|
||||
Si `timeout <= 0` usa 30s por defecto (mas largo que `CdpWaitElement` porque la carga completa de red puede tardar mas que la aparicion de un elemento DOM).
|
||||
|
||||
## Capability growth log
|
||||
|
||||
- v1.1.0 (2026-06-13) — De polling de `document.readyState` cada 200ms a esperar el evento `Page.loadEventFired` (vía `OnEvent` + canal con timeout), con fast path inicial de `readyState`. Elimina los round-trips de polling y la cuantización de ±200ms: si la página ya está cargada retorna en microsegundos.
|
||||
|
||||
Reference in New Issue
Block a user