fix(verifier): populate Action.On when tap chooser picks an element

Coordinate-targeted Taps/DoubleTaps left On empty, so action-gated
properties reading lastAction.on couldn't tell which target was hit and
were vacuously skipped. Resolve the picked element to a stable
key:value selector (resource-id, testTag, text, desc) and validate it
resolves back to the same element so we don't accidentally redirect the
tap to a sibling that shares the identifier.
This commit is contained in:
pj committed 2026-05-31 18:53:27 +05:30
1 parent ff1a8928dc
commit ee1fad1d30
2 files changed
+60 -1

No files matched your search

+18
View File
@@ -421,6 +421,24 @@ func TestDoubleTapsBuiltin_TargetsClickable(t *testing.T) {
if action.X != 50 || action.Y != 60 {
t.Errorf("coords = (%d,%d), want (50,60) at SubmitButton center", action.X, action.Y)
}
// Action-gated properties read lastAction.on to tell which target the
// chooser hit. An empty On reduces those properties to vacuously-true and
// they never fire on the real tap event.
if action.On == "" {
t.Fatal("On must be populated so action-gated properties can identify the target")
}
if !strings.Contains(action.On, "SubmitButton") {
t.Errorf("On = %q, want a selector containing SubmitButton", action.On)
}
resolved := tree.Find(action.On)
if resolved == nil {
t.Fatalf("On = %q does not resolve in the same tree", action.On)
}
rx, ry := resolved.Bounds.Center()
if rx != action.X || ry != action.Y {
t.Errorf("On %q resolves to (%d,%d), want the picked element's center (%d,%d)",
action.On, rx, ry, action.X, action.Y)
}
}
func TestDoubleTap_RoundTrip(t *testing.T) {
+42 -1
View File
@@ -612,7 +612,48 @@ func (v *Verifier) generateRandomTapKind(kind ActionKind) (Action, error) {
}
picked := candidates[v.rng.IntN(len(candidates))]
x, y := picked.Bounds.Center()
return Action{Kind: kind, X: x, Y: y}, nil
return Action{Kind: kind, On: selectorForElement(v.lastTree, picked), X: x, Y: y}, nil
}
// selectorForElement builds a canonical "key:value" selector that resolves
// back to the given element via hierarchy.Tree.Find. Prefers resource-id (the
// testTag carrier on Android / accessibilityIdentifier on iOS), falling back
// to text and content-description so action-gated properties can still tell
// what was tapped even on legacy nodes without a testTag. Returns "" when no
// candidate selector uniquely resolves to the picked element so the runner
// keeps using the action's coordinates without re-routing to a sibling that
// shares the same id/text.
func selectorForElement(tree *hierarchy.Tree, element *hierarchy.Element) string {
if element == nil || tree == nil {
return ""
}
candidates := make([]string, 0, 4)
if element.ResourceID != "" {
candidates = append(candidates, "id:"+element.ResourceID)
}
// Some platforms surface the Compose testTag only in the attributes map
// (the sidecar doesn't always promote it to resource-id). Try the raw
// attribute keys before falling back to text-based selectors so an
// element with a unique testTag still gets identified.
for _, key := range []string{"testTag", "identifier", "accessibilityIdentifier"} {
if value := element.Attributes[key]; value != "" {
candidates = append(candidates, key+":"+value)
}
}
if element.Text != "" {
candidates = append(candidates, "text:"+element.Text)
}
if element.Description != "" {
candidates = append(candidates, "desc:"+element.Description)
}
for _, selector := range candidates {
resolved := tree.Find(selector)
if resolved == nil || resolved != element {
continue
}
return selector
}
return ""
}
// inputCorpus is the edge-case string pool the typing builtin draws from to