package ui
import (
"strings"
"testing"
)
const sampleDump = `
`
func parseSample(t *testing.T) *Hierarchy {
t.Helper()
h, err := parseHierarchy([]byte(sampleDump))
if err != nil {
t.Fatalf("parseHierarchy: %v", err)
}
return h
}
func TestBoundsAndCenter(t *testing.T) {
n := &Node{Bounds: "[221,155][798,217]"}
r, err := n.BoundsRect()
if err != nil {
t.Fatalf("BoundsRect: %v", err)
}
if r.X1 != 221 || r.Y1 != 155 || r.X2 != 798 || r.Y2 != 217 {
t.Fatalf("unexpected rect %+v", r)
}
cx, cy, err := n.Center()
if err != nil {
t.Fatalf("Center: %v", err)
}
if cx != (221+798)/2 || cy != (155+217)/2 {
t.Fatalf("unexpected centre (%d,%d)", cx, cy)
}
}
func TestBoundsRejectsGarbage(t *testing.T) {
for _, bad := range []string{"", "nope", "[1,2][3,4]junk", "[10,10][10,20]"} {
if _, err := (&Node{Bounds: bad}).BoundsRect(); err == nil {
t.Fatalf("expected error for bounds %q", bad)
}
}
}
func TestFindTextExactAndSubstring(t *testing.T) {
h := parseSample(t)
if got := len(h.FindText("Connected", true)); got != 1 {
t.Fatalf("exact match: want 1, got %d", got)
}
// Substring is case-insensitive and matches more than one node.
if got := len(h.FindText("network", false)); got != 2 {
t.Fatalf("substring match: want 2 (parent + child), got %d", got)
}
// Content descriptions count as text.
matches := h.FindText("Navigate up", true)
if len(matches) != 1 || !matches[0].Clickable {
t.Fatalf("content-desc match failed: %+v", matches)
}
if got := h.FindText("does not exist", false); got != nil {
t.Fatalf("expected no matches, got %d", len(got))
}
}
func TestPickBestMatchPrefersClickable(t *testing.T) {
h := parseSample(t)
matches := h.FindText("network", false)
if len(matches) != 2 {
t.Fatalf("want 2 matches, got %d", len(matches))
}
best, err := pickBestMatch(matches)
if err != nil {
t.Fatalf("pickBestMatch: %v", err)
}
if !best.Clickable {
t.Fatalf("expected a clickable match, got %s", best.Label())
}
}
func TestPickBestMatchRejectsDisabledOnly(t *testing.T) {
h := parseSample(t)
matches := h.FindText("Disabled thing", true)
if len(matches) != 1 {
t.Fatalf("want 1 match, got %d", len(matches))
}
if _, err := pickBestMatch(matches); err == nil {
t.Fatal("expected an error when the only match is disabled")
}
}
// The Maps search box keeps a stable resource id but a rotating label, so it is
// addressed with the "id:" selector. This covers the full and short id, the
// prefix dispatch, and that a plain selector is still matched by text.
const sampleSelectorDump = `
`
func parseSelectorSample(t *testing.T) *Hierarchy {
t.Helper()
h, err := parseHierarchy([]byte(sampleSelectorDump))
if err != nil {
t.Fatalf("parseHierarchy: %v", err)
}
return h
}
func TestFindIDSelector(t *testing.T) {
h := parseSelectorSample(t)
// Full resource id, and the short name after the "/".
for _, sel := range []string{
"id:search_omnibox_text_box",
"id:com.google.android.apps.maps:id/search_omnibox_text_box",
} {
matches := h.Find(sel, false)
if len(matches) != 1 {
t.Fatalf("Find(%q): want 1 match, got %d", sel, len(matches))
}
if matches[0].Class != "android.widget.EditText" {
t.Fatalf("Find(%q) matched %q", sel, matches[0].Class)
}
}
// Exact matching of the short id.
if got := len(h.Find("id:search_omnibox_text_box", true)); got != 1 {
t.Fatalf("exact id: want 1, got %d", got)
}
// A plain selector is not diverted to the id match.
if got := len(h.Find("search_omnibox_text_box", false)); got != 0 {
t.Fatalf("plain text must not match a resource id, got %d", got)
}
// Text still works through Find.
if got := len(h.Find("Search", true)); got != 1 {
t.Fatalf("text via Find: want 1, got %d", got)
}
// An unknown id is a clean miss, not a fallthrough to text.
if got := h.Find("id:no_such_id", false); len(got) != 0 {
t.Fatalf("unknown id: want 0, got %d", len(got))
}
}
func TestEscapeInputText(t *testing.T) {
cases := map[string]string{
"hello world": "'hello%sworld'",
"Rolling Stones": "'Rolling%sStones'",
"it's": `'it'\''s'`,
}
for in, want := range cases {
if got := escapeInputText(in); got != want {
t.Errorf("escapeInputText(%q) = %q, want %q", in, got, want)
}
}
// A single quote must not be able to break out of the quoting.
got := escapeInputText("a'; rm -rf /; 'b")
if strings.Count(got, "'")%2 != 0 {
t.Fatalf("unbalanced quotes in %q", got)
}
}