4187f9b6b1
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>
86 lines
5.2 KiB
Markdown
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.
|