8742cb25be
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
280 lines
8.5 KiB
Go
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)
|
|
}
|
|
}
|