From b248ad2e5a229701c0cbd51b83cda6cf053b06cd Mon Sep 17 00:00:00 2001 From: PJ Date: Sat, 18 Apr 2026 01:59:54 +0700 Subject: [PATCH] test(verifier): integration tests against live uiautomator dumps --- internal/verifier/ax_integration_test.go | 48 +++++++++++ internal/verifier/spec_integration_test.go | 93 ++++++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 internal/verifier/ax_integration_test.go create mode 100644 internal/verifier/spec_integration_test.go diff --git a/internal/verifier/ax_integration_test.go b/internal/verifier/ax_integration_test.go new file mode 100644 index 0000000..994c5de --- /dev/null +++ b/internal/verifier/ax_integration_test.go @@ -0,0 +1,48 @@ +package verifier + +import ( + "os" + "testing" + + "github.com/priyanshujain/uatu/internal/hierarchy" +) + +// TestStateAxFindWorks verifies that a Parse+PushSnapshot+extract round trip +// actually lets the spec resolve selectors through state.ax.find. +func TestStateAxFindWorks(t *testing.T) { + xmlText, err := os.ReadFile("/tmp/live-dump.xml") + if err != nil { + t.Skip("live-dump.xml not present") + } + tree, err := hierarchy.Parse(string(xmlText)) + if err != nil { + t.Fatal(err) + } + if tree.Find("id:select_language") == nil { + t.Fatal("Go-side parser should find id:select_language") + } + + verifier := newVerifier(t) + mustLoad(t, verifier, ` + globalThis.probe = __uatu__.extract(state => { + const element = state.ax.find("id:select_language"); + return element ? "matched:" + element.text : "miss"; + }); + globalThis.count = __uatu__.extract(state => state.ax.findAll("id:select_language").length); + `) + + if err := verifier.PushSnapshot(Snapshots{}, tree); err != nil { + t.Fatal(err) + } + + probe := verifier.runtime.GlobalObject().Get("probe").ToObject(verifier.runtime).Get("current").String() + if probe == "miss" { + t.Fatalf("state.ax.find returned undefined; got %q", probe) + } + t.Logf("probe returned %q", probe) + + count := verifier.runtime.GlobalObject().Get("count").ToObject(verifier.runtime).Get("current").ToInteger() + if count != 1 { + t.Fatalf("findAll count = %d, want 1", count) + } +} diff --git a/internal/verifier/spec_integration_test.go b/internal/verifier/spec_integration_test.go new file mode 100644 index 0000000..e096f32 --- /dev/null +++ b/internal/verifier/spec_integration_test.go @@ -0,0 +1,93 @@ +package verifier + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/priyanshujain/uatu/internal/bundler" + "github.com/priyanshujain/uatu/internal/hierarchy" +) + +// 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") + } + + specPath, err := filepath.Abs("../../examples/specs/merchant-ledger.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, + 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) + } + + v := newVerifier(t) + if err := v.Load(string(bundle.JavaScript)); err != nil { + t.Fatal(err) + } + + languageTree, err := hierarchy.Parse(string(languageXML)) + if err != nil { + t.Fatal(err) + } + if err := v.PushSnapshot(Snapshots{"screen": json.RawMessage(`"customer_ledger"`)}, languageTree); err != nil { + t.Fatal(err) + } + 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 { + t.Fatal(err) + } + + // Fire NextAction many times and confirm selectEnglish is never picked. + englishHits := 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 englishHits > 0 { + t.Fatalf("selectEnglish leaked into mobile screen %d times", englishHits) + } +}