Files
fn_registry/functions/browser/cdp_find_by_text.go
T
egutierrez 750b7abcd5 chore: auto-commit (97 archivos)
- .claude/CLAUDE.md
- .claude/agents/fn-recopilador/SKILL.md
- .claude/rules/INDEX.md
- .claude/rules/cpp_apps.md
- bash/functions/infra/build_cpp_windows.sh
- cpp/CMakeLists.txt
- cpp/PATTERNS.md
- cpp/framework/app_base.cpp
- cpp/framework/app_base.h
- dev/issues/README.md
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 18:11:24 +02:00

103 lines
3.0 KiB
Go

package browser
import (
"encoding/json"
"fmt"
"strings"
)
// FindByTextOpts configura la busqueda por texto visible.
type FindByTextOpts struct {
// Tag filtra por nombre de tag (ej "button", "a"). Vacio = cualquiera.
Tag string
// Exact: true = innerText.trim() === text. false (default) = contiene.
Exact bool
// CaseSensitive: false (default) = comparacion lowercased.
CaseSensitive bool
}
// CdpFindByText busca el primer elemento cuyo `innerText` matchea `text` y
// devuelve un selector CSS unico utilizable con CdpClick / CdpEvaluate.
// Prefiere elementos hoja (no contenedores que envuelven hijos con el mismo
// texto) — asi el click va al elemento mas interno, donde el handler vive.
//
// El selector retornado es:
// - "#<id>" si el elemento tiene id.
// - path "tag:nth-of-type(n) > tag:nth-of-type(n) > ..." si no.
//
// Retorna ("", nil) si no encuentra nada (no es error). Error solo si la
// evaluacion JS rompe (conexion CDP caida).
func CdpFindByText(c *CDPConn, text string, opts FindByTextOpts) (string, error) {
if c == nil {
return "", fmt.Errorf("cdp find by text: conexion nula")
}
if text == "" {
return "", fmt.Errorf("cdp find by text: texto vacio")
}
// Serializamos opts como JSON literal en el script para evitar quoting hell.
payload, _ := json.Marshal(map[string]any{
"text": text,
"tag": opts.Tag,
"exact": opts.Exact,
"cs": opts.CaseSensitive,
})
js := fmt.Sprintf(`
(function() {
var P = %s;
var target = P.cs ? P.text : P.text.toLowerCase();
var nodes = document.querySelectorAll(P.tag || '*');
function norm(v) {
v = (v || '').replace(/\s+/g, ' ').trim();
return P.cs ? v : v.toLowerCase();
}
function matches(el) {
var v = norm(el.innerText || el.textContent || '');
return P.exact ? v === target : v.indexOf(target) >= 0;
}
function leafmost(el) {
for (var i = 0; i < el.children.length; i++) {
if (matches(el.children[i])) return false;
}
return true;
}
function selectorOf(el) {
if (el.id) return '#' + CSS.escape(el.id);
var path = [];
while (el && el.nodeType === 1 && el.tagName !== 'HTML') {
var sel = el.tagName.toLowerCase();
var parent = el.parentNode;
if (parent && parent.children) {
var sib = Array.prototype.filter.call(parent.children, function(c) {
return c.tagName === el.tagName;
});
if (sib.length > 1) sel += ':nth-of-type(' + (sib.indexOf(el) + 1) + ')';
}
path.unshift(sel);
if (el === document.body) break;
el = el.parentElement;
}
return path.join(' > ');
}
for (var i = 0; i < nodes.length; i++) {
var el = nodes[i];
if (matches(el) && leafmost(el)) {
return selectorOf(el);
}
}
return '';
})()`, string(payload))
res, err := CdpEvaluate(c, js)
if err != nil {
return "", fmt.Errorf("cdp find by text: %w", err)
}
// CdpEvaluate retorna el valor stringificado. Para "" devuelve cadena vacia.
res = strings.TrimSpace(res)
if res == "" || res == "<nil>" {
return "", nil
}
return res, nil
}