Files
fn_registry/functions/browser/cdp_select_option.go
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

154 lines
5.6 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package browser
import (
"fmt"
"strings"
)
// CdpSelectOption selecciona una <option> de un <select> nativo (localizado por
// selector CSS) replicando la semantica de Playwright (injectedScript.selectOptions).
//
// Orden de matching de value contra cada <option>, en este orden:
// 1. value exacto: option.value === value.
// 2. label/texto exacto: option.label === value (sin normalizar).
// 3. label/texto NORMALIZADO: normalizeWhiteSpace(option.label) === normalizeWhiteSpace(value),
// donde normalizar = quitar zero-width space (U+200B) y soft hyphen (U+00AD),
// trim, y colapsar cualquier secuencia de whitespace a un solo espacio.
// 4. label/texto por substring NORMALIZADO: la primera option cuyo label normalizado
// contenga el value normalizado (fallback para etiquetas largas).
// 5. fallback por indice: solo si value es un entero (>= 0) y existe esa posicion.
//
// Sobre la option encontrada hace focus del select, setea option.selected = true
// (no solo select.value, para que funcione tambien con <select multiple>) y despacha
// 'input' {bubbles:true, composed:true} seguido de 'change' {bubbles:true}, en ese
// orden, para que frameworks (React/Vue/Angular) y shadow DOM reaccionen al cambio.
//
// Si el selector apunta a un <label for=...>, sigue la referencia hasta su control
// (retarget follow-label) antes de validar que sea un <select>.
//
// Devuelve error claro si:
// - el selector no encuentra elemento ("element not found"),
// - el elemento no es un <select> ("element is not a <select> ..."),
// - ninguna option coincide ("option not found in <select>").
func CdpSelectOption(c *CDPConn, selector string, value string) error {
if c == nil {
return fmt.Errorf("cdp select option: conexion nula")
}
// Script JS alineado con Playwright. Devuelve centinelas en string:
// __OK__:<value> cuando selecciona; el resto son codigos de error claros.
// Usamos jsString para inyectar selector/value de forma segura (anti-inyeccion).
js := fmt.Sprintf(`(function() {
function normWS(t) {
return (t == null ? '' : String(t))
.replace(/[­]/g, '')
.trim()
.replace(/\s+/g, ' ');
}
var el = document.querySelector(%s);
if (!el) return '__NO_EL__';
// retarget follow-label: si es un <label for>, salta a su control.
if (el.nodeName.toLowerCase() === 'label') {
var labelled = null;
var forId = el.getAttribute('for');
if (forId) labelled = document.getElementById(forId);
if (!labelled) labelled = el.querySelector('select, input, textarea');
if (labelled) el = labelled;
}
if (el.nodeName.toLowerCase() !== 'select') return '__NOT_SELECT__';
var sel = el;
var want = %s;
var wantNorm = normWS(want);
var opts = Array.prototype.slice.call(sel.options);
var match = null;
// 1. value exacto.
for (var i = 0; i < opts.length && !match; i++) {
if (opts[i].value === want) match = opts[i];
}
// 2. label/texto exacto.
if (!match) {
for (var j = 0; j < opts.length && !match; j++) {
if (opts[j].label === want || (opts[j].textContent || '') === want) match = opts[j];
}
}
// 3. label/texto normalizado exacto.
if (!match && wantNorm !== '') {
for (var k = 0; k < opts.length && !match; k++) {
var ln = normWS(opts[k].label || opts[k].textContent);
if (ln === wantNorm) match = opts[k];
}
}
// 4. label/texto por substring normalizado.
if (!match && wantNorm !== '') {
for (var m = 0; m < opts.length && !match; m++) {
var ln2 = normWS(opts[m].label || opts[m].textContent);
if (ln2.indexOf(wantNorm) !== -1) match = opts[m];
}
}
// 5. fallback por indice: solo si want es un entero >= 0 valido.
if (!match && /^[0-9]+$/.test(want)) {
var idx = parseInt(want, 10);
if (idx >= 0 && idx < opts.length) match = opts[idx];
}
if (!match) return '__NO_OPTION__';
try { sel.focus(); } catch (e) {}
// option.selected en vez de solo select.value: necesario para <select multiple>
// y mas fiel a como un usuario elige una entrada concreta.
if (!sel.multiple) {
for (var n = 0; n < opts.length; n++) opts[n].selected = false;
}
match.selected = true;
sel.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
sel.dispatchEvent(new Event('change', { bubbles: true }));
return '__OK__:' + match.value;
})()`, jsString(selector), jsString(value))
res, err := CdpEvaluate(c, js)
if err != nil {
return fmt.Errorf("cdp select option: evaluar selector %q: %w", selector, err)
}
res = strings.Trim(res, `"`)
switch {
case strings.HasPrefix(res, "__OK__"):
return nil
case res == "__NO_EL__":
return fmt.Errorf("cdp select option: element not found para selector %q", selector)
case res == "__NOT_SELECT__":
return fmt.Errorf("cdp select option: element %q is not a <select> (use cdp_select_dropdown / click el trigger+option para dropdowns custom)", selector)
case res == "__NO_OPTION__":
return fmt.Errorf("cdp select option: option %q not found in <select> %q", value, selector)
default:
return fmt.Errorf("cdp select option: resultado inesperado %q para selector %q", res, selector)
}
}
// jsString convierte un string Go en un literal JS seguro (entre comillas dobles,
// con escapes para comillas, backslashes y saltos de linea). Evita la inyeccion
// de codigo al interpolar selectores/valores arbitrarios en el script JS.
func jsString(s string) string {
var b strings.Builder
b.WriteByte('"')
for _, r := range s {
switch r {
case '"':
b.WriteString(`\"`)
case '\\':
b.WriteString(`\\`)
case '\n':
b.WriteString(`\n`)
case '\r':
b.WriteString(`\r`)
case '\t':
b.WriteString(`\t`)
default:
b.WriteRune(r)
}
}
b.WriteByte('"')
return b.String()
}