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
+82
View File
@@ -0,0 +1,82 @@
---
name: cdp_find_by_role
kind: function
lang: go
domain: browser
version: "1.0.0"
purity: impure
signature: "func CdpFindByRole(c *CDPConn, role string, opts CdpFindByRoleOpts) (ref int, count int, err error)"
description: "Localiza el primer elemento por su ROLE ARIA + accessible name (estilo getByRole de Playwright) reusando el AX tree (Accessibility.getFullAXTree). Devuelve el backendDOMNodeId (#ref) del primer match y el total de matches para detectar ambiguedad."
tags: [browser]
params:
- name: c
desc: "Conexion CDP viva (*CDPConn) del pool. nil => error."
- name: role
desc: "Rol ARIA exacto a matchear (ej 'button', 'link', 'textbox', 'checkbox')."
- name: opts
desc: "CdpFindByRoleOpts: Name (accessible name, vacio = no filtra), Exact (igualdad en vez de substring), Regex (Name como expresion regular RE2), CaseSensitive (default false)."
output: "(ref int, count int, err error): ref = backendDOMNodeId del primer match (#ref para CdpClickRef/CdpHoverRef); count = total de matches (>1 = ambiguo); err si conexion nula, role vacio, regex invalida, fallo CDP o 0 matches."
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "functions/browser/cdp_find_by_role.go"
---
## Ejemplo
```go
c, _ := browser.CdpConnect(9333) // conexion CDP del pool
ref, count, err := browser.CdpFindByRole(c, "button", browser.CdpFindByRoleOpts{
Name: "Aceptar", // substring del accessible name, case-insensitive
})
if err != nil {
log.Fatal(err) // ej: no element with role "button" and name "Aceptar"
}
if count > 1 {
log.Printf("aviso: %d botones matchean 'Aceptar', usando el primero", count)
}
// ref es el mismo #ref que produce page_perceive: alimentarlo a CdpClickRef.
_ = browser.CdpClickRef(c, ref, browser.MouseHumanOpts{})
// Match exacto + case-sensitive:
ref, _, _ = browser.CdpFindByRole(c, "link", browser.CdpFindByRoleOpts{
Name: "Iniciar sesion", Exact: true, CaseSensitive: true,
})
// Match por regex (ej "Eliminar 3 elementos" / "Eliminar 12 elementos"):
ref, _, _ = browser.CdpFindByRole(c, "button", browser.CdpFindByRoleOpts{
Name: `^Eliminar \d+ elementos$`, Regex: true,
})
```
## Cuando usarla
Cuando necesites localizar un control de forma robusta a cambios de DOM/CSS: el rol
ARIA + accessible name sobreviven a refactors de markup y clases CSS que romperian un
selector `nth-of-type`. Es el patron primario que recomienda Playwright (getByRole)
para encontrar elementos accionables (botones, links, inputs). Combina el `ref`
devuelto directamente con `cdp_click_ref` / `cdp_hover_ref` para actuar sin pasar por
un selector fragil. Revisa `count` antes de actuar: si es >1 la busqueda es ambigua
y conviene refinar (Name mas especifico, Exact, o Regex anclada).
## Gotchas
- El `name` que se matchea es el **accessible name computado** por el motor de
accesibilidad de Chrome (deriva de aria-label, label asociado, contenido, alt,
title segun la spec ARIA), **no** el `innerText` del elemento. Si buscas por el
texto visible literal, usa `cdp_find_ref_by_text` en su lugar.
- `count > 1` => ambiguedad: se devuelve el primer match en orden del AX tree, que no
siempre es el visualmente primero ni el que quieres. Refina la busqueda.
- El `role` se compara por **igualdad exacta** del rol ARIA: "button" no matchea
"menuitem" aunque ambos sean clicables. Mira el outline de `page_perceive` /
`cdp_get_ax_outline` para ver el rol real que Chrome asigna a cada nodo.
- Nodos `ignored` del AX tree se descartan. Si el elemento esta oculto (aria-hidden,
display:none) puede no aparecer y dar 0 matches.
- El `ref` es un `backendDOMNodeId`: estable mientras el nodo viva, pero si el DOM
muta entre el find y el click el ref puede quedar obsoleto.