diff --git a/internal/verifier/spec_integration_test.go b/internal/verifier/spec_integration_test.go index 5f759d8..74722bf 100644 --- a/internal/verifier/spec_integration_test.go +++ b/internal/verifier/spec_integration_test.go @@ -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 = ` + + + + + + + +` - 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") } }