mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
test(verifier): rewrite integration tests for sample-app spec
The old tests loaded merchant-android uiautomator dumps from /tmp and skipped when absent. Replace with two hermetic tests that bundle examples/sample-app/spec.ts against a synthetic hierarchy: one checks tapClickMe fires, the other drives the three properties through a holds/holds/violated snapshot sequence.
This commit is contained in:
1 parent
0c775c199d
commit
40c92d4569
1 file changed
+70
-88
@@ -2,29 +2,29 @@ package verifier
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math/rand/v2"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/priyanshujain/uatu/internal/bundler"
|
||||
"github.com/priyanshujain/uatu/internal/hierarchy"
|
||||
"github.com/priyanshujain/uatu/internal/ltl"
|
||||
)
|
||||
|
||||
// TestSpecDrivesLanguageThenMobile reproduces the real run path: on the
|
||||
// language screen the spec must produce Tap(english); on the mobile screen
|
||||
// the language-select generator must return [] so selectEnglish never fires.
|
||||
func TestSpecDrivesLanguageThenMobile(t *testing.T) {
|
||||
languageXML, err := os.ReadFile("/tmp/live-dump.xml")
|
||||
if err != nil {
|
||||
t.Skip("live-dump.xml not present")
|
||||
}
|
||||
mobileXML, err := os.ReadFile("/tmp/mobile-real.xml")
|
||||
if err != nil {
|
||||
t.Skip("mobile-real.xml not present")
|
||||
}
|
||||
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>
|
||||
</node>
|
||||
</hierarchy>`
|
||||
|
||||
specPath, err := filepath.Abs("../../examples/specs/merchant-ledger.ts")
|
||||
// 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)
|
||||
}
|
||||
@@ -34,111 +34,93 @@ func TestSpecDrivesLanguageThenMobile(t *testing.T) {
|
||||
}
|
||||
bundle, err := bundler.Bundle(bundler.Options{
|
||||
EntryFile: specPath,
|
||||
Defines: map[string]string{"UATU_TEST_PHONE": "7509657590", "UATU_TEST_OTP": "000000"},
|
||||
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(string(bundle.JavaScript)); err != nil {
|
||||
if err := v.Load(bundleSampleAppSpec(t)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
languageTree, err := hierarchy.Parse(string(languageXML))
|
||||
tree, err := hierarchy.Parse(sampleAppHierarchyXML)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := v.PushSnapshot(Snapshots{"screen": json.RawMessage(`"customer_ledger"`)}, languageTree); err != nil {
|
||||
t.Fatal(err)
|
||||
snapshots := Snapshots{
|
||||
"app_state": json.RawMessage(`"running"`),
|
||||
"click_count": json.RawMessage(`0`),
|
||||
}
|
||||
langCounts := map[string]int{}
|
||||
for range 200 {
|
||||
action, err := v.NextAction()
|
||||
if err != nil {
|
||||
langCounts["noAction"]++
|
||||
continue
|
||||
}
|
||||
key := string(action.Kind) + ":" + action.On
|
||||
langCounts[key]++
|
||||
}
|
||||
t.Logf("language-screen action counts: %+v", langCounts)
|
||||
if langCounts["Tap:text:English"] == 0 {
|
||||
t.Fatal("selectEnglish never fired on language screen")
|
||||
}
|
||||
|
||||
mobileTree, err := hierarchy.Parse(string(mobileXML))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := v.PushSnapshot(Snapshots{"screen": json.RawMessage(`"enter_mobile"`)}, mobileTree); err != nil {
|
||||
if err := v.PushSnapshot(snapshots, tree); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Fire NextAction many times and confirm selectEnglish is never picked.
|
||||
englishHits := 0
|
||||
tapHits := 0
|
||||
for range 200 {
|
||||
action, err := v.NextAction()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if action.Kind == ActionKindTap && action.On == "text:English" {
|
||||
englishHits++
|
||||
t.Logf("unexpected Tap text:English on mobile screen, action=%+v", action)
|
||||
if action.Kind == ActionKindTap && action.On == "text:Click me" {
|
||||
tapHits++
|
||||
}
|
||||
}
|
||||
if englishHits > 0 {
|
||||
t.Fatalf("selectEnglish leaked into mobile screen %d times", englishHits)
|
||||
if tapHits == 0 {
|
||||
t.Fatal("tapClickMe never fired on sample-app hierarchy")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSpecDismissesMultiDeviceConfirm makes sure the confirm-dialog gate
|
||||
// actually fires — the live run got stuck here.
|
||||
func TestSpecDismissesMultiDeviceConfirm(t *testing.T) {
|
||||
dialogXML, err := os.ReadFile("/tmp/confirm-dump.xml")
|
||||
if err != nil {
|
||||
t.Skip("confirm-dump.xml not present")
|
||||
}
|
||||
specPath, _ := filepath.Abs("../../examples/specs/merchant-ledger.ts")
|
||||
apiPath, _ := filepath.Abs("../../pkg/spec-api/src/index.ts")
|
||||
bundle, err := bundler.Bundle(bundler.Options{
|
||||
EntryFile: specPath,
|
||||
Defines: map[string]string{"UATU_TEST_PHONE": "7509657590", "UATU_TEST_OTP": "000000"},
|
||||
Aliases: map[string]string{"@uatu/spec": apiPath},
|
||||
})
|
||||
if err != nil {
|
||||
// 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)
|
||||
}
|
||||
|
||||
v, err := New(WithRand(rand.New(rand.NewPCG(42, 0))))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := v.Load(string(bundle.JavaScript)); err != nil {
|
||||
t.Fatal(err)
|
||||
steps := []struct {
|
||||
appState string
|
||||
clickCount int
|
||||
want map[string]ltl.Verdict
|
||||
}{
|
||||
{"running", 0, map[string]ltl.Verdict{
|
||||
"appIsRunning": ltl.VerdictHolds,
|
||||
"clickCountNonNegative": ltl.VerdictHolds,
|
||||
"clickCountNeverDecreases": ltl.VerdictHolds,
|
||||
}},
|
||||
{"running", 5, map[string]ltl.Verdict{
|
||||
"appIsRunning": ltl.VerdictHolds,
|
||||
"clickCountNonNegative": ltl.VerdictHolds,
|
||||
"clickCountNeverDecreases": ltl.VerdictHolds,
|
||||
}},
|
||||
{"running", 3, map[string]ltl.Verdict{
|
||||
"appIsRunning": ltl.VerdictHolds,
|
||||
"clickCountNonNegative": ltl.VerdictHolds,
|
||||
"clickCountNeverDecreases": ltl.VerdictViolated,
|
||||
}},
|
||||
}
|
||||
|
||||
tree, err := hierarchy.Parse(string(dialogXML))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := v.PushSnapshot(Snapshots{}, tree); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
counts := map[string]int{}
|
||||
for range 400 {
|
||||
action, err := v.NextAction()
|
||||
if err != nil {
|
||||
counts["noAction"]++
|
||||
continue
|
||||
for index, step := range steps {
|
||||
stateRaw, _ := json.Marshal(step.appState)
|
||||
countRaw, _ := json.Marshal(step.clickCount)
|
||||
if err := v.PushSnapshot(Snapshots{
|
||||
"app_state": stateRaw,
|
||||
"click_count": countRaw,
|
||||
}, 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)
|
||||
}
|
||||
}
|
||||
key := string(action.Kind) + ":" + action.On
|
||||
counts[key]++
|
||||
}
|
||||
t.Logf("dialog-screen action counts: %+v", counts)
|
||||
if counts["Tap:text:Sign Out"] == 0 {
|
||||
t.Fatal("dismissMultiDevice confirm branch never fired on dialog hierarchy")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user