750b7abcd5
- .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>
103 lines
3.0 KiB
Go
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
|
|
}
|