Files
fn_registry/functions/browser/cdp_wait_actionable.md
T
Egutierrez 4187f9b6b1 feat(browser): actionability + dropdowns + fill + role locator (estilo Playwright)
Tras estudiar el código de Playwright (sources/playwright), 4 primitivas nuevas y
1 endurecida para que la interacción web sea fiable:

- cdp_wait_actionable: visible + stable (2 rAF) + enabled + hit-test (elementFromPoint
  cruzando shadow DOM) + retry backoff + scroll cycling. Devuelve el punto validado.
  Réplica de _retryAction/_checkElementIsStable/expectHitTarget de Playwright.
- cdp_select_dropdown: desplegables custom (combobox/MUI/select2/headlessui): click real
  en trigger -> espera apertura (aria-expanded/[role=option] visible) -> click real en
  la opción. Resuelve el fallo nº1: clicar antes de que monte el listbox.
- cdp_select_option (endurecida v1.1.0): valida <select> real, match value/label
  normalizado/índice, option.selected para multiple, eventos input{composed}+change.
- cdp_fill: escribir fiable en inputs React/Vue: focus -> select-all -> Input.insertText
  (sin native value setter, como Playwright); native setter solo para inputs especiales.
- cdp_find_by_role: localizar por rol ARIA + accessible name (estilo getByRole),
  reutilizando el AX tree de cdp_get_ax_outline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 20:49:37 +02:00

86 lines
5.2 KiB
Markdown

---
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.