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) } }