Files
fn_registry/functions/browser/cdp_get_ax_outline_test.go
T
egutierrez 8742cb25be feat(browser): auto-commit con 60 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-07 11:42:31 +02:00

280 lines
8.5 KiB
Go

package browser
import (
"strings"
"testing"
)
// --- renderAXOutline: casos clave portados de render_ax_outline.py ---
func TestRenderAXOutline_ActionableRoleCarriesRef(t *testing.T) {
nodes := []axNode{
{nodeID: "1", role: "WebArea", name: "Page", childIDs: []string{"2"}},
{nodeID: "2", backendDOMNodeID: "555", role: "button", name: "Submit"},
}
got := renderAXOutline(nodes, 0)
want := "WebArea \"Page\"\n button \"Submit\" #ref=555"
if got != want {
t.Errorf("got:\n%q\nwant:\n%q", got, want)
}
}
func TestRenderAXOutline_NonActionableHasNoRef(t *testing.T) {
nodes := []axNode{
{nodeID: "1", backendDOMNodeID: "9", role: "heading", name: "Title"},
}
got := renderAXOutline(nodes, 0)
if strings.Contains(got, "#ref") {
t.Errorf("rol no accionable no debe llevar #ref: %q", got)
}
if got != "heading \"Title\"" {
t.Errorf("got %q", got)
}
}
func TestRenderAXOutline_InputShowsValue(t *testing.T) {
nodes := []axNode{
{nodeID: "1", role: "form", childIDs: []string{"2"}},
{nodeID: "2", backendDOMNodeID: "42", role: "textbox", name: "Email", value: "a@b.com"},
}
got := renderAXOutline(nodes, 0)
want := "form\n textbox \"Email\" = 'a@b.com' #ref=42"
if got != want {
t.Errorf("got:\n%q\nwant:\n%q", got, want)
}
}
func TestRenderAXOutline_ValueWithSingleQuoteUsesDoubleQuote(t *testing.T) {
// Python repr: "it's" -> "it's" (comilla doble como delimitador).
nodes := []axNode{
{nodeID: "1", backendDOMNodeID: "7", role: "textbox", value: "it's"},
}
got := renderAXOutline(nodes, 0)
want := "textbox = \"it's\" #ref=7"
if got != want {
t.Errorf("got %q want %q", got, want)
}
}
func TestRenderAXOutline_SkipRoleElevatesChildren(t *testing.T) {
// El nodo 'none' se omite; su hijo button sube al nivel del padre (depth 1,
// no depth 2), porque el render del skip-node reusa el mismo depth.
nodes := []axNode{
{nodeID: "1", role: "WebArea", name: "Root", childIDs: []string{"2"}},
{nodeID: "2", role: "none", childIDs: []string{"3"}},
{nodeID: "3", backendDOMNodeID: "30", role: "button", name: "Go"},
}
got := renderAXOutline(nodes, 0)
want := "WebArea \"Root\"\n button \"Go\" #ref=30"
if got != want {
t.Errorf("got:\n%q\nwant:\n%q", got, want)
}
}
func TestRenderAXOutline_EmptyRoleElevatesChildren(t *testing.T) {
nodes := []axNode{
{nodeID: "1", role: "", childIDs: []string{"2"}}, // sin role: se omite
{nodeID: "2", backendDOMNodeID: "20", role: "link", name: "Home"},
}
got := renderAXOutline(nodes, 0)
// El nodo raiz sin role eleva su hijo a depth 0.
want := "link \"Home\" #ref=20"
if got != want {
t.Errorf("got %q want %q", got, want)
}
}
func TestRenderAXOutline_IndentationPerLevel(t *testing.T) {
nodes := []axNode{
{nodeID: "1", role: "WebArea", name: "A", childIDs: []string{"2"}},
{nodeID: "2", role: "group", name: "B", childIDs: []string{"3"}},
{nodeID: "3", role: "group", name: "C"},
}
got := renderAXOutline(nodes, 0)
want := "WebArea \"A\"\n group \"B\"\n group \"C\""
if got != want {
t.Errorf("got:\n%q\nwant:\n%q", got, want)
}
}
func TestRenderAXOutline_TruncationAddsSuffix(t *testing.T) {
nodes := []axNode{
{nodeID: "1", role: "WebArea", name: "AAAAAAAAAAAAAAAAAAAA"},
}
got := renderAXOutline(nodes, 10)
if !strings.HasSuffix(got, "\n…[outline truncado]") {
t.Errorf("falta sufijo de truncado: %q", got)
}
// El cuerpo truncado (sin sufijo) no debe exceder los 10 chars.
body := strings.TrimSuffix(got, "\n…[outline truncado]")
if len([]byte(body)) > 10 {
t.Errorf("cuerpo truncado mas largo que maxChars: %q (%d bytes)", body, len(body))
}
}
func TestRenderAXOutline_NoTruncationWhenUnderLimit(t *testing.T) {
nodes := []axNode{{nodeID: "1", role: "button", name: "X", backendDOMNodeID: "1"}}
got := renderAXOutline(nodes, 1000)
if strings.Contains(got, "truncado") {
t.Errorf("no debe truncar bajo el limite: %q", got)
}
}
func TestRenderAXOutline_Empty(t *testing.T) {
if got := renderAXOutline(nil, 0); got != "" {
t.Errorf("nil -> %q, want vacio", got)
}
}
func TestRenderAXOutline_RefFallsBackToNodeID(t *testing.T) {
// Sin backendDOMNodeId, el #ref usa el nodeId.
nodes := []axNode{
{nodeID: "77", role: "button", name: "Fallback"},
}
got := renderAXOutline(nodes, 0)
want := "button \"Fallback\" #ref=77"
if got != want {
t.Errorf("got %q want %q", got, want)
}
}
func TestRenderAXOutline_CycleGuard(t *testing.T) {
// Ciclo 1 -> 2 -> 1: no debe colgar ni duplicar nodos.
nodes := []axNode{
{nodeID: "1", role: "group", name: "A", childIDs: []string{"2"}},
{nodeID: "2", role: "group", name: "B", childIDs: []string{"1"}},
}
got := renderAXOutline(nodes, 0)
if strings.Count(got, "group \"A\"") != 1 {
t.Errorf("nodo A renderizado mas de una vez: %q", got)
}
}
// --- trimAXTree: casos clave portados de trim_ax_tree.py ---
func TestTrimAXTree_DiscardsIgnored(t *testing.T) {
nodes := []axNode{
{nodeID: "1", role: "button", name: "Keep"},
{nodeID: "2", role: "button", name: "Drop", ignored: true},
}
got := trimAXTree(nodes)
if len(got) != 1 || got[0].nodeID != "1" {
t.Errorf("trim debe descartar ignored: %+v", got)
}
}
func TestTrimAXTree_DiscardsEmptyGeneric(t *testing.T) {
nodes := []axNode{
{nodeID: "1", role: "generic"}, // sin name ni childIds -> descartado
{nodeID: "2", role: "none"}, // idem
{nodeID: "3", role: "StaticText", name: ""}, // staticText vacio -> descartado
{nodeID: "4", role: "StaticText", name: "Hola"},
}
got := trimAXTree(nodes)
if len(got) != 1 || got[0].nodeID != "4" {
t.Errorf("trim debe descartar generic/none/staticText vacios: %+v", got)
}
}
func TestTrimAXTree_KeepsGenericWithChildren(t *testing.T) {
nodes := []axNode{
{nodeID: "1", role: "generic", childIDs: []string{"2"}}, // tiene hijos -> se queda
{nodeID: "2", role: "button", name: "X"},
}
got := trimAXTree(nodes)
if len(got) != 2 {
t.Errorf("generic con hijos debe conservarse: %+v", got)
}
}
func TestTrimAXTree_CollapsesSameRoleSingleChild(t *testing.T) {
// list -> list (1 hijo, mismo role): se fusiona, el padre hereda los childIds.
nodes := []axNode{
{nodeID: "1", role: "list", childIDs: []string{"2"}},
{nodeID: "2", role: "list", childIDs: []string{"3"}},
{nodeID: "3", role: "listitem", name: "item"},
}
got := trimAXTree(nodes)
// Nodo 2 desaparece; nodo 1 debe apuntar ahora a 3.
var saw1, saw2 bool
var node1 axNode
for _, n := range got {
if n.nodeID == "1" {
saw1 = true
node1 = n
}
if n.nodeID == "2" {
saw2 = true
}
}
if !saw1 || saw2 {
t.Fatalf("colapso fallido: saw1=%v saw2=%v got=%+v", saw1, saw2, got)
}
if len(node1.childIDs) != 1 || node1.childIDs[0] != "3" {
t.Errorf("padre fusionado debe heredar childIds del hijo: %+v", node1.childIDs)
}
}
func TestTrimAXTree_PreservesOrder(t *testing.T) {
nodes := []axNode{
{nodeID: "3", role: "button", name: "C"},
{nodeID: "1", role: "button", name: "A"},
{nodeID: "2", role: "button", name: "B"},
}
got := trimAXTree(nodes)
if len(got) != 3 || got[0].nodeID != "3" || got[1].nodeID != "1" || got[2].nodeID != "2" {
t.Errorf("orden original no preservado: %+v", got)
}
}
func TestTrimAXTree_Empty(t *testing.T) {
if got := trimAXTree(nil); got != nil {
t.Errorf("nil -> %+v, want nil", got)
}
}
// --- axoPyRepr: paridad con Python repr() ---
func TestAxoPyRepr(t *testing.T) {
cases := []struct{ in, want string }{
{"hola", "'hola'"},
{"it's", "\"it's\""}, // tiene ', no " -> delimitador "
{"say \"hi\"", "'say \"hi\"'"}, // tiene " -> delimitador '
{"both ' and \"", "'both \\' and \"'"}, // ambos -> ' con escape del '
{"a\nb", "'a\\nb'"},
{"back\\slash", "'back\\\\slash'"},
}
for _, c := range cases {
if got := axoPyRepr(c.in); got != c.want {
t.Errorf("axoPyRepr(%q) = %q, want %q", c.in, got, c.want)
}
}
}
// --- axoParseNodes: extraccion del map CDP (numeros como float64) ---
func TestAxoParseNodes(t *testing.T) {
result := map[string]any{
"nodes": []any{
map[string]any{
"nodeId": "1",
"backendDOMNodeId": float64(555), // CDP int llega como float64
"ignored": false,
"role": map[string]any{"value": "button"},
"name": map[string]any{"value": "Go"},
"value": map[string]any{"value": "x"},
"childIds": []any{"2", "3"},
},
},
}
got := axoParseNodes(result)
if len(got) != 1 {
t.Fatalf("got %d nodos, want 1", len(got))
}
n := got[0]
if n.nodeID != "1" || n.backendDOMNodeID != "555" || n.role != "button" ||
n.name != "Go" || n.value != "x" || len(n.childIDs) != 2 {
t.Errorf("parse incorrecto: %+v", n)
}
}