feat: Phase 5 — maps_navigate, and read the screen before the input
The Maps recipe: open the search box, type the place, and confirm the result list names it. Every label comes from a live uiautomator dump (search_omnibox_text_box, typed_suggest_container, the account sheet), never from a guess. Text only, one step_timeout/total_timeout, guards for the account sheet. The first live run exposed an ordering bug in expect_change. A dump is not instant, so a baseline taken after the input already showed the app's response and the check could never see a wedge; it failed against a healthy Maps. The engine now reads the screen before the step above runs and hands that signature to the check. Each retry re-reads it. Live: maps_navigate ok in 27.6s, result list names "Melbourne Museum". The wedge reason and its bounded retry are demonstrated with a probe recipe whose input changes nothing; a real wedge could not be forced, and docs/phase-5-maps-recipe.md says so plainly. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -178,6 +178,56 @@ func TestSpotifyRecipeIsValid(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestMapsRecipeIsValid keeps the shipped Phase 5 recipe honest: a typo in
|
||||
// recipes/maps_navigate.recipe fails the test suite, not the phone.
|
||||
func TestMapsRecipeIsValid(t *testing.T) {
|
||||
rec, err := Load(filepath.Join("..", "..", "recipes", "maps_navigate.recipe"))
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if rec.Task != "maps_navigate" || rec.App != "com.google.android.apps.maps" {
|
||||
t.Fatalf("header = %q / %q", rec.Task, rec.App)
|
||||
}
|
||||
if len(rec.MissingParams(map[string]string{"query": "x"})) != 0 {
|
||||
t.Fatal("maps_navigate must accept a query parameter")
|
||||
}
|
||||
if len(rec.Guards) == 0 {
|
||||
t.Fatal("maps_navigate needs guards for screens it must not tap")
|
||||
}
|
||||
|
||||
var sawForeground, sawChange int
|
||||
for i, s := range rec.Steps {
|
||||
switch s.Verb {
|
||||
case "foreground":
|
||||
sawForeground++
|
||||
case "expect_change":
|
||||
sawChange++
|
||||
// The engine reads the screen before the step above, so the check
|
||||
// needs a step above it.
|
||||
if i == 0 {
|
||||
t.Fatal("expect_change must have a step above it")
|
||||
}
|
||||
}
|
||||
// No coordinates, nowhere: every argument is text, a duration or a
|
||||
// placeholder, never a bare screen coordinate.
|
||||
for _, a := range s.Args {
|
||||
if _, err := strconv.Atoi(a); err == nil {
|
||||
t.Fatalf("step %s has a bare number %q as an argument: this recipe must name text, not coordinates", s.Verb, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
if sawForeground == 0 {
|
||||
t.Fatal("maps_navigate must pin the foreground app before matching text")
|
||||
}
|
||||
if sawChange == 0 {
|
||||
t.Fatal("maps_navigate must check that its inputs changed the screen (the wedged-app check)")
|
||||
}
|
||||
last := rec.Steps[len(rec.Steps)-1]
|
||||
if last.Verb != "wait" || last.Args[0] != "{query}" {
|
||||
t.Fatalf("the last step must confirm the result list names the place, got %+v", last)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAndList(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
write := func(name, body string) {
|
||||
@@ -290,7 +340,7 @@ func TestForegroundStep(t *testing.T) {
|
||||
func TestStepDurationsAreRecorded(t *testing.T) {
|
||||
rec := &Recipe{StepTimeout: time.Second}
|
||||
step := Step{Verb: "sleep", Timeout: 50 * time.Millisecond}
|
||||
sr := executeStep(adb.New(""), rec, step, 2, nil, 0, 0, time.Now().Add(time.Minute), func(string, ...any) {})
|
||||
sr := executeStep(adb.New(""), rec, step, 2, nil, 0, 0, time.Now().Add(time.Minute), "", func(string, ...any) {})
|
||||
if !sr.OK {
|
||||
t.Fatalf("sleep step failed: %s", sr.Error)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user