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: // - "#" 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 == "" { return "", nil } return res, nil }