package verifier import ( "encoding/json" "testing" "github.com/priyanshujain/sanderling/internal/hierarchy" "github.com/priyanshujain/sanderling/internal/ltl" ) // The integration spec carries no reference to any specific example app, so // bundling it exercises the CLI's path over a neutral subject. var integrationSpec = bundleOptions{SpecFile: "testdata/integration_spec.ts"} func listSnapshots() Snapshots { return Snapshots{ "route": json.RawMessage(`"list"`), "item_count": json.RawMessage(`0`), "has_submitted": json.RawMessage(`false`), } } func formSnapshots() Snapshots { return Snapshots{ "route": json.RawMessage(`"form"`), "item_count": json.RawMessage(`0`), "has_submitted": json.RawMessage(`false`), } } // TestIntegrationSpecFiresInputActions verifies the bundled neutral spec // emits an InputText action on the text field and a Tap action on the // primary button when both are present in the hierarchy. func TestIntegrationSpecFiresInputActions(t *testing.T) { v := newVerifier(t) if err := v.Load(bundleSpec(t, integrationSpec)); err != nil { t.Fatal(err) } tree, err := hierarchy.Parse(formHierarchyJSON) if err != nil { t.Fatal(err) } if err := v.PushSnapshot(SnapshotInput{Snapshots: formSnapshots(), Tree: tree}); err != nil { t.Fatal(err) } typeFieldHits := 0 tapPrimaryHits := 0 for range 400 { action, err := v.NextAction() if err != nil { continue } switch { case action.Kind == ActionKindInputText && action.On == "desc:text_field": typeFieldHits++ case action.Kind == ActionKindTap && action.On == "desc:primary_action": tapPrimaryHits++ } } if typeFieldHits == 0 { t.Fatal("typeIntoField never typed into desc:text_field on form hierarchy") } if tapPrimaryHits == 0 { t.Fatal("tapPrimary never tapped desc:primary_action on form hierarchy") } } // TestIntegrationSpecPropertiesEvaluate checks the properties declared in // the neutral spec evaluate sensibly across a small snapshot sequence. The // spec mixes safety and liveness properties; Pending verdicts are expected // for liveness properties that haven't had time to resolve yet. func TestIntegrationSpecPropertiesEvaluate(t *testing.T) { v := newVerifier(t) if err := v.Load(bundleSpec(t, integrationSpec)); err != nil { t.Fatal(err) } tree, err := hierarchy.Parse(listHierarchyJSON) if err != nil { t.Fatal(err) } if err := v.PushSnapshot(SnapshotInput{Snapshots: listSnapshots(), Tree: tree}); err != nil { t.Fatal(err) } verdicts := v.EvaluateProperties() if verdicts["itemCountNonNegative"] != ltl.VerdictHolds { t.Errorf("itemCountNonNegative: got %v, want holds", verdicts["itemCountNonNegative"]) } if verdicts["routeIsKnown"] != ltl.VerdictHolds { t.Errorf("routeIsKnown: got %v, want holds", verdicts["routeIsKnown"]) } if verdicts["noUncaughtExceptions"] != ltl.VerdictHolds { t.Errorf("noUncaughtExceptions: got %v, want holds", verdicts["noUncaughtExceptions"]) } // Liveness: submitEventually hasn't resolved yet. if verdicts["submitEventually"] != ltl.VerdictPending { t.Errorf("submitEventually: got %v, want pending", verdicts["submitEventually"]) } } // TestIntegrationSpecActionsFireOnEachRoute pushes a hierarchy + snapshot // pair representative of each route and verifies the expected action // generators fire against that state. func TestIntegrationSpecActionsFireOnEachRoute(t *testing.T) { cases := []struct { name string hierarchy string snapshots Snapshots expectKind ActionKind expectOns []string }{ { name: "list", hierarchy: listHierarchyJSON, snapshots: listSnapshots(), expectKind: ActionKindTap, expectOns: []string{"desc:primary_action", "desc:secondary_action"}, }, { name: "form", hierarchy: formHierarchyJSON, snapshots: formSnapshots(), expectKind: ActionKindTap, expectOns: []string{"desc:primary_action", "desc:secondary_action"}, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { v := newVerifier(t) if err := v.Load(bundleSpec(t, integrationSpec)); err != nil { t.Fatal(err) } tree, err := hierarchy.Parse(tc.hierarchy) if err != nil { t.Fatal(err) } if err := v.PushSnapshot(SnapshotInput{Snapshots: tc.snapshots, Tree: tree}); err != nil { t.Fatal(err) } sawOn := map[string]bool{} sawInputText := map[string]bool{} for range 800 { action, err := v.NextAction() if err != nil { continue } if action.Kind == tc.expectKind { sawOn[action.On] = true } if action.Kind == ActionKindInputText { sawInputText[action.On] = true } } for _, on := range tc.expectOns { if !sawOn[on] { t.Errorf("%s: no %s action on %q; saw %v", tc.name, tc.expectKind, on, keysOf(sawOn)) } } if tc.name == "form" && !sawInputText["desc:text_field"] { t.Errorf("form: typeIntoField never fired; saw %v", keysOf(sawInputText)) } }) } } func keysOf(m map[string]bool) []string { out := make([]string, 0, len(m)) for k := range m { out = append(out, k) } return out }