---
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 <fieldset disabled>). 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.
