fix(runner): surface focus-tap errors in InputText (#12)

* fix(runner): surface focus-tap errors in InputText action

A failed Tap/TapSelector before InputText was swallowed, so text typed
into the wrong field (or no field) still reported success. Return the
error so the step fails explicitly.

* feat(sample-app): add username EditText and snapshot

Gives the spec a real EditText target (content-desc: username_field)
so the InputText action path can be exercised end-to-end. The typed
value is mirrored into MainActivity.username and surfaced as the
"username" snapshot for spec assertions.

* feat(sample-app): exercise InputText action against username field

Adds typeUsername action and usernameNeverShrinks property to the
sample spec, and extends the integration test to assert the bundled
spec emits an InputText(desc:username_field, "alice") action and that
the property correctly violates when a snapshot reports a shorter
string.
This commit is contained in:
pj authored and GitHub committed 2026-04-18 16:51:35 +07:00
1 parent b457e22569
commit 16e55086d8
6 files changed
+101 -8

No files matched your search

+6 -2
View File
@@ -198,9 +198,13 @@ func applyAction(ctx context.Context, drv driver.Driver, action verifier.Action,
return drv.Tap(ctx, x, y)
case verifier.ActionKindInputText:
if x, y, ok := resolveCoordinates(action, tree); ok {
_ = drv.Tap(ctx, x, y)
if err := drv.Tap(ctx, x, y); err != nil {
return err
}
} else if action.On != "" {
_ = drv.TapSelector(ctx, action.On)
if err := drv.TapSelector(ctx, action.On); err != nil {
return err
}
}
return drv.InputText(ctx, action.Text)
default:
+30
View File
@@ -3,6 +3,7 @@ package runner
import (
"context"
"encoding/json"
"errors"
"net"
"os"
"path/filepath"
@@ -234,6 +235,35 @@ func TestRunner_RecordsScreenFieldFromSnapshot(t *testing.T) {
}
}
func TestApplyAction_InputTextSurfacesFocusTapError(t *testing.T) {
t.Run("selector focus tap fails", func(t *testing.T) {
driverMock := mockdriver.New()
driverMock.Failures[mockdriver.ActionTapSelector] = errors.New("adb unreachable")
action := verifier.Action{Kind: verifier.ActionKindInputText, On: "id:username", Text: "alice"}
err := applyAction(context.Background(), driverMock, action, nil)
if err == nil {
t.Fatalf("expected focus tap failure to surface, got nil")
}
if containsAction(driverMock.Actions(), mockdriver.ActionInputText, "") {
t.Errorf("InputText must not run after focus tap failed: %v", driverMock.Actions())
}
})
t.Run("coordinate focus tap fails", func(t *testing.T) {
driverMock := mockdriver.New()
driverMock.Failures[mockdriver.ActionTap] = errors.New("tap driver error")
action := verifier.Action{Kind: verifier.ActionKindInputText, X: 10, Y: 20, Text: "alice"}
err := applyAction(context.Background(), driverMock, action, nil)
if err == nil {
t.Fatalf("expected focus tap failure to surface, got nil")
}
if containsAction(driverMock.Actions(), mockdriver.ActionInputText, "") {
t.Errorf("InputText must not run after focus tap failed: %v", driverMock.Actions())
}
})
}
func mustNewVerifier(t *testing.T) *verifier.Verifier {
t.Helper()
verifierInstance, err := verifier.New()
+20 -5
View File
@@ -16,6 +16,8 @@ const sampleAppHierarchyXML = `<?xml version="1.0" encoding="UTF-8"?>
<node index="0" class="android.widget.LinearLayout" bounds="[64,96][1016,2336]">
<node index="0" class="android.widget.TextView" text="Clicks: 0" bounds="[100,200][900,300]" />
<node index="1" class="android.widget.Button" text="Click me" clickable="true" enabled="true" bounds="[400,800][680,920]" />
<node index="2" class="android.widget.TextView" text="Username: " bounds="[100,1000][900,1080]" />
<node index="3" class="android.widget.EditText" content-desc="username_field" clickable="true" enabled="true" bounds="[100,1200][900,1320]" />
</node>
</node>
</hierarchy>`
@@ -63,18 +65,25 @@ func TestSampleAppSpecTapsClickMe(t *testing.T) {
}
tapHits := 0
for range 200 {
inputHits := 0
for range 400 {
action, err := v.NextAction()
if err != nil {
continue
}
if action.Kind == ActionKindTap && action.On == "text:Click me" {
switch {
case action.Kind == ActionKindTap && action.On == "text:Click me":
tapHits++
case action.Kind == ActionKindInputText && action.On == "desc:username_field" && action.Text == "alice":
inputHits++
}
}
if tapHits == 0 {
t.Fatal("tapClickMe never fired on sample-app hierarchy")
}
if inputHits == 0 {
t.Fatal("typeUsername never fired on sample-app hierarchy")
}
}
// TestSampleAppSpecPropertiesHold checks the three properties declared in the
@@ -88,31 +97,37 @@ func TestSampleAppSpecPropertiesHold(t *testing.T) {
steps := []struct {
appState string
clickCount int
username string
want map[string]ltl.Verdict
}{
{"running", 0, map[string]ltl.Verdict{
{"running", 0, "", map[string]ltl.Verdict{
"appIsRunning": ltl.VerdictHolds,
"clickCountNonNegative": ltl.VerdictHolds,
"clickCountNeverDecreases": ltl.VerdictHolds,
"usernameNeverShrinks": ltl.VerdictHolds,
}},
{"running", 5, map[string]ltl.Verdict{
{"running", 5, "alice", map[string]ltl.Verdict{
"appIsRunning": ltl.VerdictHolds,
"clickCountNonNegative": ltl.VerdictHolds,
"clickCountNeverDecreases": ltl.VerdictHolds,
"usernameNeverShrinks": ltl.VerdictHolds,
}},
{"running", 3, map[string]ltl.Verdict{
{"running", 3, "al", map[string]ltl.Verdict{
"appIsRunning": ltl.VerdictHolds,
"clickCountNonNegative": ltl.VerdictHolds,
"clickCountNeverDecreases": ltl.VerdictViolated,
"usernameNeverShrinks": ltl.VerdictViolated,
}},
}
for index, step := range steps {
stateRaw, _ := json.Marshal(step.appState)
countRaw, _ := json.Marshal(step.clickCount)
usernameRaw, _ := json.Marshal(step.username)
if err := v.PushSnapshot(Snapshots{
"app_state": stateRaw,
"click_count": countRaw,
"username": usernameRaw,
}, nil); err != nil {
t.Fatalf("step %d: %v", index, err)
}