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:
@@ -3,10 +3,10 @@ name: cdp_select_option
|
||||
kind: function
|
||||
lang: go
|
||||
domain: browser
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
purity: impure
|
||||
signature: "func CdpSelectOption(c *CDPConn, selector string, value string) error"
|
||||
description: "Selecciona la <option> de un <select> (localizado por selector CSS) cuyo value coincide con el valor dado; si ningun value coincide, busca por texto visible de la option. Tras setear select.value despacha los eventos 'input' y 'change' con bubbles:true para que frameworks (React/Vue) reaccionen al cambio. Via Runtime.evaluate, reusa CdpEvaluate."
|
||||
description: "Selecciona una <option> de un <select> nativo (localizado por selector CSS) replicando la semantica de Playwright (injectedScript.selectOptions). Match por value exacto, luego label/texto exacto, luego label normalizado (whitespace-collapse + strip zero-width/soft-hyphen), luego substring normalizado, y por ultimo indice si value es entero. Setea option.selected (soporta <select multiple>), hace focus, y despacha 'input' {bubbles,composed} + 'change' {bubbles}. Valida que el elemento sea <select> (error claro si no) y sigue <label for>. Via Runtime.evaluate, reusa CdpEvaluate."
|
||||
tags: [chrome, cdp, browser, automation, select, dropdown, form, dom, devtools]
|
||||
uses_functions: [cdp_evaluate_go_browser]
|
||||
uses_types: []
|
||||
@@ -20,8 +20,8 @@ params:
|
||||
- name: selector
|
||||
desc: "selector CSS del elemento <select> a modificar"
|
||||
- name: value
|
||||
desc: "value de la <option> a seleccionar; si no hay match por value, se busca por texto visible (textContent trimeado)"
|
||||
output: "error si el select no existe (\"select not found\") o ninguna option coincide por value ni por texto (\"option not found\"); nil si la selección y los eventos se despacharon correctamente"
|
||||
desc: "criterio de seleccion. Se prueba en orden: value exacto → label/texto exacto → label normalizado (whitespace-collapse + strip U+200B/U+00AD) → label por substring normalizado → indice (si value es un entero)"
|
||||
output: "error si el selector no encuentra elemento (\"element not found\"), si el elemento no es un <select> (\"element is not a <select> ...\"), o si ninguna option coincide (\"option not found in <select>\"); nil si la selección y los eventos se despacharon correctamente"
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
@@ -43,6 +43,11 @@ if err := CdpSelectOption(conn, "#country", "ES"); err != nil {
|
||||
if err := CdpSelectOption(conn, "select[name=lang]", "Español"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Seleccionar por indice (3a opcion) cuando ni value ni texto son estables
|
||||
if err := CdpSelectOption(conn, "#size", "2"); err != nil { // index 2 = 3a option
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
@@ -50,21 +55,53 @@ if err := CdpSelectOption(conn, "select[name=lang]", "Español"); err != nil {
|
||||
Usala cuando necesites elegir una opcion de un `<select>` nativo en un formulario
|
||||
web y quieras que un framework (React, Vue, Angular) reaccione al cambio. Es la
|
||||
forma robusta de rellenar dropdowns durante automatizacion/scraping: a diferencia
|
||||
de un click sobre la option, setea `select.value` y dispara `input`+`change`, que
|
||||
es lo que los frameworks escuchan. Combinala con `CdpClick` para enviar el
|
||||
formulario despues.
|
||||
de un click sobre la option, setea `option.selected` y dispara `input`+`change`,
|
||||
que es lo que los frameworks escuchan. Combinala con `CdpClick` para enviar el
|
||||
formulario despues. Si no conoces el `value` interno, pasa el texto visible (se
|
||||
normaliza el whitespace) o el indice numerico de la option.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Solo funciona con `<select>` nativos (HTML). Dropdowns custom hechos con `<div>`
|
||||
+ JS (ej. react-select, headlessui) NO son `<select>` reales: para esos hay que
|
||||
clickar y elegir la opcion del menu desplegado, no usar esta funcion.
|
||||
- El match por value es exacto (`===`); el fallback por texto compara `textContent`
|
||||
trimeado de forma exacta tras `.trim()` (no substring, no case-insensitive).
|
||||
- No hace scroll ni verifica visibilidad: opera sobre el DOM directamente. Si el
|
||||
`<select>` esta deshabilitado (`disabled`), el value se setea igual pero la UI
|
||||
puede ignorarlo segun el framework.
|
||||
- Para `<select multiple>` solo selecciona una opcion (la que coincide) y resetea
|
||||
el resto, porque setea `select.value` (no añade a `selectedOptions`).
|
||||
- Si el elemento aun no existe (carga dinamica), retorna "select not found" sin
|
||||
- **Solo `<select>` nativos.** Si el elemento no es un `<select>` retorna error
|
||||
claro `element is not a <select> ...`. Dropdowns custom hechos con `<div>` + JS
|
||||
(react-select, headlessui, Radix, etc.) NO son `<select>` reales: para esos usa
|
||||
`cdp_select_dropdown` (cuando exista) o clica el trigger con `CdpClickRef` y
|
||||
luego la opcion del menu desplegado (`CdpFindRefByText` + `CdpClickRef`). NO uses
|
||||
esta funcion para ellos.
|
||||
- **Orden de matching del `value` recibido** (se prueba en este orden y para en el
|
||||
primer match):
|
||||
1. `option.value` exacto (`===`).
|
||||
2. `option.label` / `textContent` exacto (sin normalizar).
|
||||
3. label/texto NORMALIZADO exacto: se quita zero-width space (U+200B) y soft
|
||||
hyphen (U+00AD), se hace `trim`, y se colapsa cualquier whitespace (`\s+`) a un
|
||||
solo espacio — igual que `normalizeWhiteSpace` de Playwright.
|
||||
4. label/texto por SUBSTRING normalizado (primera option cuyo label normalizado
|
||||
contenga el value normalizado). Util para etiquetas largas; cuidado con
|
||||
ambiguedad (gana la primera en orden de documento).
|
||||
5. fallback por INDICE: solo si `value` es un entero `>= 0` valido (`"2"` → 3a
|
||||
option). Por eso un `value` que casualmente sea numerico puede caer aqui si no
|
||||
hubo ningun match textual antes — preferi el `value` real cuando exista.
|
||||
El matching es case-sensitive en todos los pasos (no se hace lowercase).
|
||||
- **`<select multiple>` soportado:** setea `option.selected = true` sobre la option
|
||||
encontrada sin tocar el resto de selecciones. En un `<select>` simple deselecciona
|
||||
las demas antes de marcar la elegida. (La version 1.0.0 solo seteaba `select.value`
|
||||
y reseteaba el multiple — corregido.)
|
||||
- **Eventos:** dispara `input` con `{bubbles:true, composed:true}` (el `composed`
|
||||
permite cruzar shadow DOM, p.ej. web components que envuelven el `<select>`) y
|
||||
luego `change` con `{bubbles:true}`, en ese orden. Hace `focus()` del select antes.
|
||||
- No hace scroll ni verifica visibilidad/enabled: opera sobre el DOM directamente.
|
||||
Si el `<select>` o la `<option>` estan `disabled`, la seleccion se aplica igual
|
||||
pero la UI puede ignorarla segun el framework (Playwright aqui devolveria
|
||||
`optionnotenabled`; esta funcion no chequea enabled — mantiene KISS).
|
||||
- Si el elemento aun no existe (carga dinamica), retorna `element not found` sin
|
||||
esperar — combinar con `CdpWaitElement` para elementos diferidos.
|
||||
|
||||
## Capability growth log
|
||||
|
||||
- v1.1.0 (2026-06-16) — alineada con Playwright `injectedScript.selectOptions`:
|
||||
valida que el elemento sea `<select>` (error claro si no, apuntando a dropdowns
|
||||
custom), sigue `<label for>`, matching multi-criterio (value → label exacto →
|
||||
label normalizado whitespace-collapse → substring → indice), usa
|
||||
`option.selected` en vez de solo `select.value` (soporta `<select multiple>`),
|
||||
añade `composed:true` al evento `input` (cruza shadow DOM) y `focus()` previo.
|
||||
Firma intacta (no rompe el caller del MCP `dom_select_option`).
|
||||
|
||||
Reference in New Issue
Block a user