fix: address the Maps search box by id, and re-prove Phase 5

The Maps omnibox hint rotates: it read "Search here" in the Phase 5 run and
"Try petrol stations, ATMs" in the AUD-128 re-verification, so the recipe's
text match on it failed. The resource id does not rotate.

- internal/ui: add Find/FindID and the "id:" selector; Find dispatches an
  id: selector to the resource id and everything else to text as before.
- internal/recipe: wait/tap/wait_gone/tap_if and the tap helper use Find, so a
  recipe may name a resource id. Guards stay text-only.
- recipes/maps_navigate.recipe: wait/tap id:search_omnibox_text_box.
- tests for the id selector; docs and README updated.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-10-17 09:12:01 +11:00
parent a7c07c645e
commit 3ef11da808
9 changed files with 206 additions and 26 deletions

View File

@@ -103,16 +103,56 @@ func shortID(id string) string {
return id
}
// IDSelector marks a selector that matches a resource id rather than text. A
// step that names "id:search_omnibox_text_box" finds the element with that
// resource id, whatever its visible label happens to be.
//
// This exists because some labels are not stable: the Google Maps search box
// carries a rotating hint as its content description ("Search here", "Try
// petrol stations, ATMs", ...), so a recipe that matched that text broke when
// the phone picked a different hint. The resource id does not rotate.
const IDSelector = "id:"
// FindText returns nodes whose text or content description matches query.
// When exact is false the match is case-insensitive and substring-based.
func (h *Hierarchy) FindText(query string, exact bool) []*Node {
return h.find(query, exact, false)
}
// FindID returns nodes whose resource id matches query. When exact is false the
// match is case-insensitive and substring-based, so the short name
// "search_omnibox_text_box" matches the full
// "com.google.android.apps.maps:id/search_omnibox_text_box".
func (h *Hierarchy) FindID(query string, exact bool) []*Node {
return h.find(query, exact, true)
}
// Find returns nodes matching a recipe selector. A selector that starts with
// "id:" is matched against the resource id; any other selector is matched
// against text and content description (FR-4).
func (h *Hierarchy) Find(query string, exact bool) []*Node {
if id, ok := strings.CutPrefix(query, IDSelector); ok {
return h.FindID(id, exact)
}
return h.FindText(query, exact)
}
func (h *Hierarchy) find(query string, exact, byID bool) []*Node {
if query == "" {
return nil
}
needle := strings.ToLower(query)
var out []*Node
h.Walk(func(n *Node) {
for _, candidate := range []string{n.Text, n.ContentDesc} {
var candidates []string
if byID {
if n.ResourceID != "" {
candidates = []string{shortID(n.ResourceID), n.ResourceID}
}
} else {
candidates = []string{n.Text, n.ContentDesc}
}
for _, candidate := range candidates {
if candidate == "" {
continue
}