mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
* 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.
142 lines
4.4 KiB
Go
142 lines
4.4 KiB
Go
package verifier
|
|
|
|
import (
|
|
"encoding/json"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/priyanshujain/uatu/internal/bundler"
|
|
"github.com/priyanshujain/uatu/internal/hierarchy"
|
|
"github.com/priyanshujain/uatu/internal/ltl"
|
|
)
|
|
|
|
const sampleAppHierarchyXML = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<hierarchy rotation="0">
|
|
<node index="0" class="android.widget.FrameLayout" package="dev.uatu.sample" bounds="[0,0][1080,2400]">
|
|
<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>`
|
|
|
|
// bundleSampleAppSpec bundles examples/sample-app/spec.ts via the real
|
|
// @uatu/spec API so the integration test exercises the same path the CLI uses.
|
|
func bundleSampleAppSpec(t *testing.T) string {
|
|
t.Helper()
|
|
specPath, err := filepath.Abs("../../examples/sample-app/spec.ts")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
apiPath, err := filepath.Abs("../../pkg/spec-api/src/index.ts")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bundle, err := bundler.Bundle(bundler.Options{
|
|
EntryFile: specPath,
|
|
Aliases: map[string]string{"@uatu/spec": apiPath},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return string(bundle.JavaScript)
|
|
}
|
|
|
|
// TestSampleAppSpecTapsClickMe verifies the bundled sample-app spec emits a
|
|
// Tap on the "Click me" button when that button is present in the hierarchy.
|
|
func TestSampleAppSpecTapsClickMe(t *testing.T) {
|
|
v := newVerifier(t)
|
|
if err := v.Load(bundleSampleAppSpec(t)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tree, err := hierarchy.Parse(sampleAppHierarchyXML)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
snapshots := Snapshots{
|
|
"app_state": json.RawMessage(`"running"`),
|
|
"click_count": json.RawMessage(`0`),
|
|
}
|
|
if err := v.PushSnapshot(snapshots, tree); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tapHits := 0
|
|
inputHits := 0
|
|
for range 400 {
|
|
action, err := v.NextAction()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
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
|
|
// sample-app spec evaluate correctly across a realistic snapshot sequence.
|
|
func TestSampleAppSpecPropertiesHold(t *testing.T) {
|
|
v := newVerifier(t)
|
|
if err := v.Load(bundleSampleAppSpec(t)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
steps := []struct {
|
|
appState string
|
|
clickCount int
|
|
username string
|
|
want map[string]ltl.Verdict
|
|
}{
|
|
{"running", 0, "", map[string]ltl.Verdict{
|
|
"appIsRunning": ltl.VerdictHolds,
|
|
"clickCountNonNegative": ltl.VerdictHolds,
|
|
"clickCountNeverDecreases": ltl.VerdictHolds,
|
|
"usernameNeverShrinks": ltl.VerdictHolds,
|
|
}},
|
|
{"running", 5, "alice", map[string]ltl.Verdict{
|
|
"appIsRunning": ltl.VerdictHolds,
|
|
"clickCountNonNegative": ltl.VerdictHolds,
|
|
"clickCountNeverDecreases": ltl.VerdictHolds,
|
|
"usernameNeverShrinks": ltl.VerdictHolds,
|
|
}},
|
|
{"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)
|
|
}
|
|
got := v.EvaluateProperties()
|
|
for property, want := range step.want {
|
|
if got[property] != want {
|
|
t.Errorf("step %d %q: got %v, want %v", index, property, got[property], want)
|
|
}
|
|
}
|
|
}
|
|
}
|