fix(runner): an ambiguous name loses to the coordinates it was built from

Attribute values match by substring, so a selector that named one element
where the candidate was built can name several in the tree it resolves
against, and the lookup sent every one of them to the first match. The host
gates blank an ambiguous tag at enumeration time; this closes the gap between
that moment and the action.

A bare-string target carries no coordinates, so the first match stays the
answer there rather than dropping an authored action.

Claude-Session: https://claude.ai/code/session_01A5KmftdEJ49A9z5mF5ESrX
This commit is contained in:
pj committed 2026-08-15 14:02:01 +05:30
1 parent fd7bf17256
commit bf2da973de
2 files changed
+56 -2

No files matched your search

+9 -2
View File
@@ -903,8 +903,15 @@ func resolveCoordinates(action verifier.Action, tree *hierarchy.Tree) (int, int,
return 0, 0, false
}
if tree != nil {
if element := tree.Find(action.On); element != nil {
x, y := element.Bounds.Center()
// An ambiguous selector names several elements while the action's own
// coordinates name one, so the coordinates win. Attribute values match
// by substring, so a selector unique where the candidate was built can
// be ambiguous in the tree it resolves against. A bare-string target
// carries no coordinates, and there the name is all there is.
matches := tree.FindAll(action.On)
hasCoordinates := action.X > 0 && action.Y > 0
if len(matches) > 0 && (len(matches) == 1 || !hasCoordinates) {
x, y := matches[0].Bounds.Center()
if x > 0 && y > 0 {
return x, y, true
}
+47
View File
@@ -1037,6 +1037,53 @@ func TestApplyAction_V8InputTextAtOriginStillTaps(t *testing.T) {
}
}
// Attribute values match by substring, so "data-testid:card" answers to
// "card-1" and "card-10" alike. A candidate built where the selector named one
// element can execute where it names several, and the tree lookup would send
// every one of them to the first match.
func TestApplyAction_AmbiguousSelectorTapsTheActionsOwnCoordinates(t *testing.T) {
tree, err := hierarchy.Parse(`{"attributes":{"resource-id":"root","bounds":"[0,0,1080,2340]"},"children":[
{"attributes":{"data-testid":"card-1","bounds":"[0,100,200,200]"},"children":[]},
{"attributes":{"data-testid":"card-10","bounds":"[0,300,200,400]"},"children":[]}
]}`)
if err != nil {
t.Fatalf("Parse: %v", err)
}
driverMock := mockdriver.New()
action := verifier.Action{Kind: verifier.ActionKindTap, On: "data-testid:card-1", X: 100, Y: 350}
mustDispatch(t, driverMock, action, tree)
for _, dispatched := range driverMock.Actions() {
if dispatched.Kind == mockdriver.ActionTap && dispatched.X == 100 && dispatched.Y == 350 {
return
}
}
t.Errorf("tap reached the driver at %v, want the action's own (100,350)", driverMock.Actions())
}
// A bare-string target carries no coordinates of its own, so an ambiguous name
// is all there is to act on and the first match stays the answer. Refusing it
// would drop an authored action.
func TestApplyAction_AmbiguousSelectorWithoutCoordinatesTapsTheFirstMatch(t *testing.T) {
tree, err := hierarchy.Parse(`{"attributes":{"resource-id":"root","bounds":"[0,0,1080,2340]"},"children":[
{"attributes":{"data-testid":"card-1","bounds":"[0,100,200,200]"},"children":[]},
{"attributes":{"data-testid":"card-10","bounds":"[0,300,200,400]"},"children":[]}
]}`)
if err != nil {
t.Fatalf("Parse: %v", err)
}
driverMock := mockdriver.New()
action := verifier.Action{Kind: verifier.ActionKindTap, On: "data-testid:card-1"}
mustDispatch(t, driverMock, action, tree)
for _, dispatched := range driverMock.Actions() {
if dispatched.Kind == mockdriver.ActionTap && dispatched.X == 100 && dispatched.Y == 150 {
return
}
}
t.Errorf("tap reached the driver at %v, want the first match's centre (100,150)", driverMock.Actions())
}
func TestApplyAction_DoubleTapDispatchesDoubleTapAtCoordinates(t *testing.T) {
driverMock := mockdriver.New()
action := verifier.Action{Kind: verifier.ActionKindDoubleTap, X: 100, Y: 200}