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>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user