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>
84 lines
2.7 KiB
Go
84 lines
2.7 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitea.lab.audasmedia.com.au/sam/artemis_control/internal/adb"
|
|
)
|
|
|
|
// ScreenSignature returns a fingerprint of what is on screen: the foreground
|
|
// package plus every visible label and the bounds it sits in.
|
|
//
|
|
// Two dumps of an unchanged screen produce the same signature, and any change a
|
|
// human would see — new text, different text, an element moved or a different
|
|
// app in front — produces a different one. It is the raw material for wedge
|
|
// detection: if an input is delivered and the signature never moves, the app
|
|
// never acted on that input.
|
|
//
|
|
// The foreground package is best effort. If it cannot be read the labels alone
|
|
// still make a usable fingerprint, because a screen that stops changing is
|
|
// exactly the failure this guards against.
|
|
func ScreenSignature(r *adb.Runner) (string, error) {
|
|
h, err := Dump(r)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
fg, _ := ForegroundPackage(r)
|
|
return h.Signature(fg), nil
|
|
}
|
|
|
|
// Signature is the pure half of ScreenSignature: a stable fingerprint of one
|
|
// parsed dump. Labels are sorted so that the order uiautomator happened to
|
|
// report them in never looks like a change, and bounds are included so that an
|
|
// element moving is a change even when the words stay the same.
|
|
func (h *Hierarchy) Signature(foreground string) string {
|
|
parts := make([]string, 0, len(h.TextNodes()))
|
|
h.Walk(func(n *Node) {
|
|
label := strings.TrimSpace(n.Text)
|
|
if label == "" {
|
|
label = strings.TrimSpace(n.ContentDesc)
|
|
}
|
|
if label == "" {
|
|
return
|
|
}
|
|
parts = append(parts, strings.TrimSpace(n.Bounds)+"|"+label)
|
|
})
|
|
sort.Strings(parts)
|
|
return foreground + "\n" + strings.Join(parts, "\n")
|
|
}
|
|
|
|
// WaitForChange waits until the screen no longer matches before, and returns
|
|
// the new signature.
|
|
//
|
|
// The caller captures before with ScreenSignature *before* the input it wants to
|
|
// judge. That ordering is the whole check: an app that has stopped consuming
|
|
// input still draws the screen it had, so the signature after the input is
|
|
// identical to the one before it. A dump is not instant, so a baseline taken
|
|
// after the input would already include the app's response and could never see
|
|
// the wedge.
|
|
func WaitForChange(r *adb.Runner, before string, timeout time.Duration) (string, error) {
|
|
if strings.TrimSpace(before) == "" {
|
|
return "", fmt.Errorf("no screen signature was captured before the input")
|
|
}
|
|
if timeout <= 0 {
|
|
timeout = 10 * time.Second
|
|
}
|
|
deadline := time.Now().Add(timeout)
|
|
for {
|
|
sig, err := ScreenSignature(r)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if sig != before {
|
|
return sig, nil
|
|
}
|
|
if time.Now().After(deadline) {
|
|
return before, fmt.Errorf("the screen did not change within %s", timeout)
|
|
}
|
|
time.Sleep(PollInterval)
|
|
}
|
|
}
|