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>
This commit is contained in:
Egutierrez
2026-06-16 20:49:37 +02:00
parent c4ecf871c8
commit 4187f9b6b1
10 changed files with 1585 additions and 44 deletions
+98
View File
@@ -0,0 +1,98 @@
---
name: cdp_select_dropdown
kind: function
lang: go
domain: browser
version: "1.0.0"
purity: impure
signature: "func CdpSelectDropdown(c *CDPConn, triggerSelector string, optionText string, opts CdpDropdownOpts) error"
description: "Selecciona una opcion en un DESPLEGABLE CUSTOM (combobox/listbox ARIA, react-select, MUI Select, headlessui, select2) — esos donde un <select> nativo NO aplica. Replica el patron de Playwright (que no tiene API para custom dropdowns): click REAL en el trigger (mousedown, no element.click JS), espera la apertura por polling (aria-expanded=true O [role=listbox]/[role=menu] visible O opciones con rect>0), localiza la opcion por texto normalizado (substring o exacto, case-insensitive) y hace click REAL en su centro, con verificacion suave (aria-expanded vuelve a false o Enter como fallback). Reusa CdpEvaluate, CdpClickXYHuman y CdpPressKey."
tags: [browser, chrome, cdp, automation, dropdown, combobox, listbox, aria, select, react-select, mui, headlessui, devtools]
uses_functions: [cdp_evaluate_go_browser, cdp_click_xy_human_go_browser, cdp_press_key_go_browser]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [fmt, strings, time]
params:
- name: c
desc: "conexion CDP activa (*CDPConn)"
- name: triggerSelector
desc: "selector CSS del elemento que abre el desplegable (el boton/combobox sobre el que se hace click real)"
- name: optionText
desc: "texto visible de la opcion a elegir; se normaliza (trim + colapsar espacios) y se compara case-insensitive, por substring si opts.Exact=false o por igualdad si opts.Exact=true"
- name: opts
desc: "CdpDropdownOpts{Exact bool (igualdad vs substring, default substring); TimeoutMs int (espera apertura+opcion, default 3000); OptionRole string (rol ARIA de las opciones, default 'option' — usar 'menuitem' para menus, 'treeitem' para arboles)}"
output: "error si el trigger no existe, si el dropdown no abre dentro del timeout (\"el dropdown no abrio\"), o si la opcion no aparece (\"option %q not found in dropdown\"); nil si el click sobre la opcion se realizo (la verificacion de cierre es suave y no falla duro si queda ambigua)"
tested: false
tests: []
test_file_path: ""
file_path: "functions/browser/cdp_select_dropdown.go"
---
## Ejemplo
```go
conn, _ := CdpConnect(9222)
CdpNavigate(conn, "https://mui.com/material-ui/react-select/")
// Combobox MUI: el trigger es el div con role=combobox; el listbox monta y
// anima al abrir. CdpSelectDropdown clica el trigger, espera a que el listbox
// este visible y entonces clica la opcion "Twenty".
err := CdpSelectDropdown(conn, "[role=combobox]", "Twenty", CdpDropdownOpts{})
if err != nil {
log.Fatal(err)
}
// react-select / headlessui: trigger por clase + match exacto + timeout amplio
// para listas que tardan en montar.
err = CdpSelectDropdown(conn, ".select__control", "España", CdpDropdownOpts{
Exact: true,
TimeoutMs: 6000,
})
// Menu tipo dropdown-menu (no listbox): las opciones son role=menuitem.
err = CdpSelectDropdown(conn, "#user-menu-btn", "Cerrar sesion", CdpDropdownOpts{
OptionRole: "menuitem",
})
```
## Cuando usarla
Usala cuando el desplegable NO es un `<select>` nativo: comboboxes/listboxes ARIA,
react-select, MUI Select, headlessui, select2, Ant Design, o cualquier menu hecho
con `<div>`/`<li>` + JS donde elegir = clicar el trigger y luego clicar la opcion
del menu desplegado. Es el equivalente al patron de Playwright
`click(trigger) -> getByRole('option', {name}) -> click(option)`, con la espera de
apertura ya resuelta. Para un `<select>` nativo de HTML usa `CdpSelectOption` (setea
`select.value` + dispara `input`/`change`), que es mas robusto y directo para ese
caso.
## Gotchas
- **Click real, no element.click()**: muchos dropdowns custom escuchan `mousedown`
(no `click`), por eso esta funcion despacha eventos de raton reales sobre el
centro del bbox. Solo cae a `element.click()` JS si el nodo no tiene geometria.
- **Animaciones de apertura**: el fallo nº1 reportado en Playwright es clicar la
opcion ANTES de que el listbox monte/anime. Por eso hay polling de apertura
(`dropdownWaitOpen`) que no avanza hasta que hay opciones visibles. Si tu
dropdown anima muy lento, sube `TimeoutMs`.
- **Listas virtualizadas** (react-window, virtuoso): solo renderizan las opciones
en viewport. Si la opcion buscada esta fuera del scroll inicial, puede que nunca
se monte y la funcion devuelva "not found" aunque exista. Mitigacion: escribe en
el combobox para filtrar (`CdpTypeText`) antes de llamar a esta funcion, o haz
scroll dentro del listbox primero.
- **Trigger vs contenedor**: `triggerSelector` debe apuntar al elemento que ABRE el
menu (el boton/combobox), no al `[role=listbox]` (que no existe hasta abrir).
- **Match de texto**: normaliza espacios y es case-insensitive; por defecto es
substring (`Exact=false`). Si varias opciones comparten substring, elige la
primera visible en orden de documento — usa `Exact=true` para desambiguar.
- **OptionRole**: por defecto `option` (`[role=option]`). Para menus de acciones usa
`menuitem`; para arboles `treeitem`. La deteccion de apertura tambien considera
`[role=menu]` y `li[role]` para cubrir patrones comunes.
- **Verificacion suave**: tras clicar, si el dropdown sigue abierto la funcion pulsa
`Enter` como fallback y devuelve `nil`. No falla duro si la seleccion no se puede
confirmar inequivocamente pero el click se hizo — comprueba el estado resultante
(texto del trigger, valor del formulario) si necesitas certeza.
- **iframes**: opera en el documento principal (via `CdpEvaluate`). Para un dropdown
dentro de un iframe necesitarias el contexto del frame (no cubierto aqui).