--- name: cdp_wait_actionable kind: function lang: go domain: browser version: "1.0.0" purity: impure signature: "func CdpWaitActionable(c *CDPConn, backendNodeID int, needEnabled bool, timeout time.Duration) (x float64, y float64, err error)" description: "Bloquea hasta que el elemento del #ref sea accionable (listo para un click/hover fiable) o expire timeout. Reproduce el modelo de actionability de Playwright: en bucle con backoff [0,20,100,100,500]ms comprueba visible (client rects + computed style), stable (mismo getBoundingClientRect en dos requestAnimationFrame seguidos), enabled opcional (disabled / aria-disabled / fieldset disabled subiendo la jerarquía), scroll into view rotando alineación block (center/start/end), y hit-test (elementFromPoint subiendo por shadow DOM apunta al target o descendiente). Devuelve el punto central (x,y) en coords de viewport listo para Input.dispatchMouseEvent. Al expirar, el error indica qué estado falló (not visible / not stable / disabled / outside viewport / intercepted by other element)." tags: [cdp, browser, action, ref, actionability, browser-actionability, navegator] uses_functions: [] uses_types: [] returns: [] returns_optional: false error_type: "error_go_core" imports: [] tested: false tests: [] test_file_path: "" params: - name: c desc: "Conexión CDP activa al tab objetivo." - name: backendNodeID desc: "El #ref del AX outline = backendDOMNodeId estable del nodo DOM. Se obtiene de page_perceive / render_ax_outline." - name: needEnabled desc: "Si true, exige también el estado enabled (no disabled, no aria-disabled=true, no dentro de
). Pasar false para elementos no interactivos (texto, contenedores) donde enabled no aplica." - name: timeout desc: "Tiempo máximo de espera antes de rendirse. <=0 usa 5s por defecto. El bucle de reintento nunca duerme más allá de este deadline." output: "(x, y) punto central del elemento en coordenadas de viewport (CSS px), listo para despachar el pointer, cuando todos los chequeos pasan; error si la conexión es nil, el nodo no resuelve a objectId, se desconecta del DOM, o expira el timeout (con el estado que falló al final)." file_path: "functions/browser/cdp_wait_actionable.go" --- ## Ejemplo ```go // Tras un page_perceive que devuelve outline con #ref=1234, esperar a que el // elemento sea accionable y luego clicar el punto exacto que devuelve: conn, _ := CdpConnect(9222) x, y, err := CdpWaitActionable(conn, 1234, true, 5*time.Second) if err != nil { log.Fatalf("no accionable: %v", err) // ej: "intercepted by other element: div#cookie-banner" } // x,y ya están en viewport, estables y sin overlay encima: click fiable. _ = CdpClickXYHuman(conn, x, y, MouseHumanOpts{}) ``` ## Cuando usarla Antes de CUALQUIER click/hover/type que deba ser fiable sobre un #ref del outline. Llamarla justo después de `page_perceive` y antes de `cdp_click_ref` / `cdp_click_xy_human` / `dom_*_ref` para evitar los fallos clásicos del navegador: clicar un botón que aún se está animando hacia su posición, un elemento tapado por un banner de cookies / modal / spinner, o un control todavía `disabled`. Es la puerta de actionability que separa "el nodo existe en el DOM" de "el nodo está listo para recibir el evento ahí donde lo voy a despachar". Usar `needEnabled=true` para botones/inputs/enlaces; `needEnabled=false` para hover sobre texto o medir un contenedor. ## Gotchas - **Coste de polling.** Es síncrona y bloqueante: hace un `Runtime.callFunctionOn` por iteración + 2 `requestAnimationFrame` por chequeo de estabilidad. En el peor caso poll-ea hasta `timeout` con backoff creciente (0,20,100,100,500ms → 500ms). No la metas en un bucle apretado sobre N elementos sin necesidad; una sola llamada por acción es lo correcto. Timeouts altos sobre elementos que nunca llegan (genuinamente ocultos) cuestan el timeout entero. - **Shadow DOM.** El hit-test sube por shadow roots (`assignedSlot` / `parentNode.host`) y por eso funciona con web components con shadow root *abierto*. Con shadow roots **cerrados** `elementFromPoint` no expone el interior y el hit-test puede reportar `intercepted` erróneamente; en ese caso usar el click vía `element.click()` (modo instant de `cdp_click_ref`), que no depende del hit-test geométrico. - **iframes.** Opera sobre el contexto de la página/frame al que apunta el `*CDPConn`. Un `backendNodeID` de otro frame no resuelve aquí: hay que tener la conexión/contexto del frame correcto (ver `cdp_eval_in_frame`). Las coordenadas devueltas son relativas al viewport de ESE documento, no compuestas con el offset del iframe en la página padre. - **Estabilidad vs animaciones infinitas.** Un elemento con una animación CSS perpetua que mueve su rect (spinner que se desplaza, marquee) nunca pasará el chequeo `stable` y agotará el timeout con "not stable". Es comportamiento correcto (no es accionable de forma fiable), pero conviene saberlo. - **El punto devuelto es (x,y) de viewport**, no de página. Es lo que `Input.dispatchMouseEvent` espera. Si necesitas coords de página (con scroll), el JS interno ya las calcula (`pageX/pageY`) pero la firma pública expone solo las de viewport para encajar con el dispatch de pointer.