diff --git a/internal/verifier/scope_test.go b/internal/verifier/scope_test.go index 265d5c9..aa469d8 100644 --- a/internal/verifier/scope_test.go +++ b/internal/verifier/scope_test.go @@ -33,7 +33,10 @@ func pushTree(t *testing.T, v *Verifier, treeJSON string) { // the result lands on its center regardless of seed. func TestTaps_ExcludeOffAppPackage(t *testing.T) { verifier := newVerifier(t, WithAppPackage("com.folio")) - mustLoad(t, verifier, `globalThis.actions = __sanderling__.taps;`) + loadActionSpec(t, verifier, ` + import { taps } from "@sanderling/spec"; + globalThis.actions = taps; + `) pushTree(t, verifier, scopedTreeJSON) action, err := verifier.NextAction() @@ -56,7 +59,10 @@ func TestTyping_ExcludeOffAppPackage(t *testing.T) { ] }` verifier := newVerifier(t, WithAppPackage("com.folio")) - mustLoad(t, verifier, `globalThis.actions = __sanderling__.typing;`) + loadActionSpec(t, verifier, ` + import { typing } from "@sanderling/spec"; + globalThis.actions = typing; + `) pushTree(t, verifier, treeJSON) action, err := verifier.NextAction() @@ -72,7 +78,10 @@ func TestTyping_ExcludeOffAppPackage(t *testing.T) { // exploration never scrolls the keyboard's emoji list instead of the app. func TestSwipes_ExcludeOffAppPackage(t *testing.T) { verifier := newVerifier(t, WithAppPackage("com.folio")) - mustLoad(t, verifier, `globalThis.actions = __sanderling__.swipes;`) + loadActionSpec(t, verifier, ` + import { swipes } from "@sanderling/spec"; + globalThis.actions = swipes; + `) pushTree(t, verifier, scopedTreeJSON) // Both the root and SubmitButton (com.folio) are valid anchors; only the @@ -103,7 +112,10 @@ func TestTaps_AllOffAppYieldsErrNoAction(t *testing.T) { ] }` verifier := newVerifier(t, WithAppPackage("com.folio")) - mustLoad(t, verifier, `globalThis.actions = __sanderling__.taps;`) + loadActionSpec(t, verifier, ` + import { taps } from "@sanderling/spec"; + globalThis.actions = taps; + `) pushTree(t, verifier, treeJSON) if _, err := verifier.NextAction(); !errors.Is(err, ErrNoAction) { @@ -121,7 +133,10 @@ func TestTaps_UnsetAppPackageKeepsAllNodes(t *testing.T) { ] }` verifier := newVerifier(t) - mustLoad(t, verifier, `globalThis.actions = __sanderling__.taps;`) + loadActionSpec(t, verifier, ` + import { taps } from "@sanderling/spec"; + globalThis.actions = taps; + `) pushTree(t, verifier, treeJSON) action, err := verifier.NextAction() @@ -137,7 +152,10 @@ func TestTaps_UnsetAppPackageKeepsAllNodes(t *testing.T) { // mirrors taps: it yields a LongPress on the only clickable in-app node. func TestLongPresses_TargetsClickableElement(t *testing.T) { verifier := newVerifier(t, WithAppPackage("com.folio")) - mustLoad(t, verifier, `globalThis.actions = __sanderling__.longPresses;`) + loadActionSpec(t, verifier, ` + import { longPresses } from "@sanderling/spec"; + globalThis.actions = longPresses; + `) pushTree(t, verifier, scopedTreeJSON) action, err := verifier.NextAction() @@ -163,7 +181,10 @@ func TestScrolls_TargetsScrollableContainer(t *testing.T) { ] }` verifier := newVerifier(t, WithAppPackage("com.folio")) - mustLoad(t, verifier, `globalThis.actions = __sanderling__.scrolls;`) + loadActionSpec(t, verifier, ` + import { scrolls } from "@sanderling/spec"; + globalThis.actions = scrolls; + `) pushTree(t, verifier, treeJSON) for range 50 { @@ -190,8 +211,8 @@ func TestScrolls_TargetsScrollableContainer(t *testing.T) { default: t.Fatalf("unexpected direction %q", action.Direction) } - if action.DurationMillis != 300 { - t.Fatalf("durationMillis = %d, want 300", action.DurationMillis) + if action.DurationMillis != 250 { + t.Fatalf("durationMillis = %d, want 250", action.DurationMillis) } } } @@ -200,7 +221,10 @@ func TestScrolls_TargetsScrollableContainer(t *testing.T) { // no scrollable container is present. func TestScrolls_NoScrollableYieldsErrNoAction(t *testing.T) { verifier := newVerifier(t, WithAppPackage("com.folio")) - mustLoad(t, verifier, `globalThis.actions = __sanderling__.scrolls;`) + loadActionSpec(t, verifier, ` + import { scrolls } from "@sanderling/spec"; + globalThis.actions = scrolls; + `) pushTree(t, verifier, scopedTreeJSON) if _, err := verifier.NextAction(); !errors.Is(err, ErrNoAction) { @@ -219,7 +243,10 @@ func TestTaps_EmptyPackageNodeStaysInScope(t *testing.T) { ] }` verifier := newVerifier(t, WithAppPackage("com.folio")) - mustLoad(t, verifier, `globalThis.actions = __sanderling__.taps;`) + loadActionSpec(t, verifier, ` + import { taps } from "@sanderling/spec"; + globalThis.actions = taps; + `) pushTree(t, verifier, treeJSON) action, err := verifier.NextAction() diff --git a/internal/verifier/spec_integration_test.go b/internal/verifier/spec_integration_test.go index 75f6530..b5dcf54 100644 --- a/internal/verifier/spec_integration_test.go +++ b/internal/verifier/spec_integration_test.go @@ -23,12 +23,17 @@ func bundleIntegrationSpec(t *testing.T) string { if err != nil { t.Fatal(err) } + runtimePath, err := filepath.Abs("../../pkg/spec/src/goja-runtime.ts") + if err != nil { + t.Fatal(err) + } defaultsPath, err := filepath.Abs("../../pkg/spec/src/defaults/properties.ts") if err != nil { t.Fatal(err) } bundle, err := bundler.Bundle(bundler.Options{ - EntryFile: specPath, + EntryFile: specPath, + RuntimeFile: runtimePath, Aliases: map[string]string{ "@sanderling/spec": apiPath, "@sanderling/spec/defaults/properties": defaultsPath, diff --git a/internal/verifier/verifier_test.go b/internal/verifier/verifier_test.go index 7f20a85..bbc9d81 100644 --- a/internal/verifier/verifier_test.go +++ b/internal/verifier/verifier_test.go @@ -3,13 +3,15 @@ package verifier import ( "encoding/json" "errors" - "math/rand/v2" + "os" + "path/filepath" "slices" "strings" "testing" "github.com/dop251/goja" + "github.com/priyanshujain/sanderling/internal/bundler" "github.com/priyanshujain/sanderling/internal/hierarchy" "github.com/priyanshujain/sanderling/internal/ltl" ) @@ -30,6 +32,43 @@ func mustLoad(t *testing.T, verifier *Verifier, source string) { } } +// bundleActionSpec bundles an inline TS spec authored against @sanderling/spec +// together with the goja runtime entry, so loading it installs +// __sanderlingNextAction__ (the shared picker). Action targets must be resolved +// ax elements (carrying x/y) or builtins; raw selector strings no longer +// resolve to coordinates in the unified contract. +func bundleActionSpec(t *testing.T, specSource string) string { + t.Helper() + dir := t.TempDir() + specPath := filepath.Join(dir, "spec.ts") + if err := os.WriteFile(specPath, []byte(specSource), 0o600); err != nil { + t.Fatal(err) + } + apiPath, err := filepath.Abs("../../pkg/spec/src/index.ts") + if err != nil { + t.Fatal(err) + } + runtimePath, err := filepath.Abs("../../pkg/spec/src/goja-runtime.ts") + if err != nil { + t.Fatal(err) + } + bundle, err := bundler.Bundle(bundler.Options{ + EntryFile: specPath, + RuntimeFile: runtimePath, + Aliases: map[string]string{"@sanderling/spec": apiPath}, + }) + if err != nil { + t.Fatal(err) + } + return string(bundle.JavaScript) +} + +// loadActionSpec bundles and loads an inline authored spec into the verifier. +func loadActionSpec(t *testing.T, verifier *Verifier, specSource string) { + t.Helper() + mustLoad(t, verifier, bundleActionSpec(t, specSource)) +} + const helloSpec = ` const screen = __sanderling__.extract(state => state.snapshots.screen ?? ""); const balance = __sanderling__.extract(state => state.snapshots["ledger.balance"] ?? 0); @@ -40,10 +79,6 @@ globalThis.balance = balance; globalThis.properties = { balanceNonNegative: __sanderling__.always(() => balance.current >= 0), }; - -globalThis.actions = __sanderling__.actions(() => [ - __sanderling__.tap({ on: "id:home_button" }), -]); ` func TestLoad_ExposesRuntimeBindings(t *testing.T) { @@ -231,9 +266,25 @@ globalThis.properties = { } func TestNextAction_FromActionsGenerator(t *testing.T) { + const treeJSON = `{ + "attributes": {"resource-id": "root", "bounds": "[0,0,100,100]"}, + "children": [ + {"attributes": {"resource-id": "home_button", "bounds": "[0,40,100,80]"}, "clickable": true, "enabled": true, "children": []} + ] + }` verifier := newVerifier(t) - mustLoad(t, verifier, helloSpec) - _ = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}}) + loadActionSpec(t, verifier, ` + import { actions, Tap } from "@sanderling/spec"; + globalThis.actions = actions(() => { + const home = state.ax.find("id:home_button"); + return home ? [Tap({ on: home })] : []; + }); + `) + tree, err := hierarchy.Parse(treeJSON) + if err != nil { + t.Fatal(err) + } + _ = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}, Tree: tree}) action, err := verifier.NextAction() if err != nil { @@ -248,16 +299,31 @@ func TestNextAction_FromActionsGenerator(t *testing.T) { } func TestNextAction_WeightedSelectsByWeight(t *testing.T) { - verifier := newVerifier(t, WithRand(rand.New(rand.NewPCG(42, 0)))) - mustLoad(t, verifier, ` - const tapHome = __sanderling__.actions(() => [__sanderling__.tap({ on: "id:home" })]); - const tapAway = __sanderling__.actions(() => [__sanderling__.tap({ on: "id:away" })]); - globalThis.actions = __sanderling__.weighted( - [1, tapHome], - [99, tapAway], - ); + const treeJSON = `{ + "attributes": {"resource-id": "root", "bounds": "[0,0,100,100]"}, + "children": [ + {"attributes": {"resource-id": "home", "bounds": "[0,0,100,40]"}, "clickable": true, "enabled": true, "children": []}, + {"attributes": {"resource-id": "away", "bounds": "[0,40,100,80]"}, "clickable": true, "enabled": true, "children": []} + ] + }` + verifier := newVerifier(t, WithSeed(42)) + loadActionSpec(t, verifier, ` + import { actions, weighted, Tap } from "@sanderling/spec"; + const tapHome = actions(() => { + const home = state.ax.find("id:home"); + return home ? [Tap({ on: home })] : []; + }); + const tapAway = actions(() => { + const away = state.ax.find("id:away"); + return away ? [Tap({ on: away })] : []; + }); + globalThis.actions = weighted([1, tapHome], [99, tapAway]); `) - _ = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}}) + tree, err := hierarchy.Parse(treeJSON) + if err != nil { + t.Fatal(err) + } + _ = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}, Tree: tree}) awayCount := 0 homeCount := 0 @@ -280,8 +346,9 @@ func TestNextAction_WeightedSelectsByWeight(t *testing.T) { func TestNextAction_EmptyGeneratorReturnsErrNoAction(t *testing.T) { verifier := newVerifier(t) - mustLoad(t, verifier, ` - globalThis.actions = __sanderling__.actions(() => []); + loadActionSpec(t, verifier, ` + import { actions } from "@sanderling/spec"; + globalThis.actions = actions(() => []); `) _ = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}}) @@ -291,13 +358,34 @@ func TestNextAction_EmptyGeneratorReturnsErrNoAction(t *testing.T) { } } +// setupTree carries the three targets the setup-precedence tests resolve. +const setupTree = `{ + "attributes": {"resource-id": "root", "bounds": "[0,0,100,120]"}, + "children": [ + {"attributes": {"resource-id": "setup", "bounds": "[0,0,100,40]"}, "clickable": true, "enabled": true, "children": []}, + {"attributes": {"resource-id": "main", "bounds": "[0,40,100,80]"}, "clickable": true, "enabled": true, "children": []}, + {"attributes": {"resource-id": "login", "bounds": "[0,80,100,120]"}, "clickable": true, "enabled": true, "children": []} + ] +}` + func TestNextAction_SetupTakesPrecedenceWhenYielding(t *testing.T) { verifier := newVerifier(t) - mustLoad(t, verifier, ` - globalThis.setup = __sanderling__.actions(() => [__sanderling__.tap({ on: "id:setup" })]); - globalThis.actions = __sanderling__.actions(() => [__sanderling__.tap({ on: "id:main" })]); + loadActionSpec(t, verifier, ` + import { actions, Tap } from "@sanderling/spec"; + globalThis.setup = actions(() => { + const target = state.ax.find("id:setup"); + return target ? [Tap({ on: target })] : []; + }); + globalThis.actions = actions(() => { + const target = state.ax.find("id:main"); + return target ? [Tap({ on: target })] : []; + }); `) - _ = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}}) + tree, err := hierarchy.Parse(setupTree) + if err != nil { + t.Fatal(err) + } + _ = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}, Tree: tree}) action, err := verifier.NextAction() if err != nil { @@ -310,11 +398,19 @@ func TestNextAction_SetupTakesPrecedenceWhenYielding(t *testing.T) { func TestNextAction_FallsThroughToActionsWhenSetupEmpty(t *testing.T) { verifier := newVerifier(t) - mustLoad(t, verifier, ` - globalThis.setup = __sanderling__.actions(() => []); - globalThis.actions = __sanderling__.actions(() => [__sanderling__.tap({ on: "id:main" })]); + loadActionSpec(t, verifier, ` + import { actions, Tap } from "@sanderling/spec"; + globalThis.setup = actions(() => []); + globalThis.actions = actions(() => { + const target = state.ax.find("id:main"); + return target ? [Tap({ on: target })] : []; + }); `) - _ = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}}) + tree, err := hierarchy.Parse(setupTree) + if err != nil { + t.Fatal(err) + } + _ = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}, Tree: tree}) action, err := verifier.NextAction() if err != nil { @@ -327,21 +423,30 @@ func TestNextAction_FallsThroughToActionsWhenSetupEmpty(t *testing.T) { func TestNextAction_SetupReengagesAfterRegression(t *testing.T) { verifier := newVerifier(t) - mustLoad(t, verifier, ` - globalThis.loggedIn = __sanderling__.extract(state => state.snapshots["loggedIn"] === true); - globalThis.setup = __sanderling__.actions(() => { + loadActionSpec(t, verifier, ` + import { actions, extract, Tap } from "@sanderling/spec"; + const loggedIn = extract(state => state.snapshots["loggedIn"] === true); + globalThis.setup = actions(() => { if (loggedIn.current) return []; - return [__sanderling__.tap({ on: "id:login" })]; + const target = state.ax.find("id:login"); + return target ? [Tap({ on: target })] : []; + }); + globalThis.actions = actions(() => { + const target = state.ax.find("id:main"); + return target ? [Tap({ on: target })] : []; }); - globalThis.actions = __sanderling__.actions(() => [__sanderling__.tap({ on: "id:main" })]); `) + tree, err := hierarchy.Parse(setupTree) + if err != nil { + t.Fatal(err) + } push := func(loggedIn bool) { raw := json.RawMessage(`false`) if loggedIn { raw = json.RawMessage(`true`) } - if err := verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{"loggedIn": raw}}); err != nil { + if err := verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{"loggedIn": raw}, Tree: tree}); err != nil { t.Fatal(err) } } @@ -376,14 +481,19 @@ func TestNextAction_SetupReengagesAfterRegression(t *testing.T) { func TestNextAction_NoSetupRegistered(t *testing.T) { verifier := newVerifier(t) - mustLoad(t, verifier, ` - globalThis.actions = __sanderling__.actions(() => [__sanderling__.tap({ on: "id:main" })]); + loadActionSpec(t, verifier, ` + import { actions, Tap } from "@sanderling/spec"; + globalThis.actions = actions(() => { + const target = state.ax.find("id:main"); + return target ? [Tap({ on: target })] : []; + }); `) - _ = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}}) - - if verifier.setupGenerator != nil { - t.Errorf("setupGenerator should be nil when spec does not export setup") + tree, err := hierarchy.Parse(setupTree) + if err != nil { + t.Fatal(err) } + _ = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}, Tree: tree}) + action, err := verifier.NextAction() if err != nil { t.Fatal(err) @@ -403,7 +513,10 @@ func TestDoubleTapsBuiltin_TargetsClickable(t *testing.T) { ] }` verifier := newVerifier(t) - mustLoad(t, verifier, `globalThis.actions = __sanderling__.doubleTaps;`) + loadActionSpec(t, verifier, ` + import { doubleTaps } from "@sanderling/spec"; + globalThis.actions = doubleTaps; + `) tree, err := hierarchy.Parse(treeJSON) if err != nil { t.Fatal(err) @@ -442,13 +555,25 @@ func TestDoubleTapsBuiltin_TargetsClickable(t *testing.T) { } func TestDoubleTap_RoundTrip(t *testing.T) { + const treeJSON = `{ + "attributes": {"resource-id": "root", "bounds": "[0,0,100,100]"}, + "children": [ + {"attributes": {"resource-id": "save", "bounds": "[0,0,100,40]"}, "clickable": true, "enabled": true, "children": []} + ] + }` verifier := newVerifier(t) - mustLoad(t, verifier, ` - globalThis.actions = __sanderling__.actions(() => [ - __sanderling__.doubleTap({ on: "id:save" }), - ]); + loadActionSpec(t, verifier, ` + import { actions, DoubleTap } from "@sanderling/spec"; + globalThis.actions = actions(() => { + const save = state.ax.find("id:save"); + return save ? [DoubleTap({ on: save })] : []; + }); `) - _ = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}}) + tree, err := hierarchy.Parse(treeJSON) + if err != nil { + t.Fatal(err) + } + _ = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}, Tree: tree}) action, err := verifier.NextAction() if err != nil { @@ -463,13 +588,25 @@ func TestDoubleTap_RoundTrip(t *testing.T) { } func TestInputText_RoundTrip(t *testing.T) { + const treeJSON = `{ + "attributes": {"resource-id": "root", "bounds": "[0,0,100,100]"}, + "children": [ + {"attributes": {"resource-id": "phone", "bounds": "[0,0,100,40]"}, "editable": true, "enabled": true, "children": []} + ] + }` verifier := newVerifier(t) - mustLoad(t, verifier, ` - globalThis.actions = __sanderling__.actions(() => [ - __sanderling__.inputText({ into: "id:phone", text: "+919876543210" }), - ]); + loadActionSpec(t, verifier, ` + import { actions, InputText } from "@sanderling/spec"; + globalThis.actions = actions(() => { + const phone = state.ax.find("id:phone"); + return phone ? [InputText({ into: phone, text: "+919876543210" })] : []; + }); `) - _ = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}}) + tree, err := hierarchy.Parse(treeJSON) + if err != nil { + t.Fatal(err) + } + _ = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}, Tree: tree}) action, err := verifier.NextAction() if err != nil { @@ -495,7 +632,10 @@ func TestTypingBuiltin_TargetsEditableField(t *testing.T) { ] }` verifier := newVerifier(t) - mustLoad(t, verifier, `globalThis.actions = __sanderling__.typing;`) + loadActionSpec(t, verifier, ` + import { typing } from "@sanderling/spec"; + globalThis.actions = typing; + `) tree, err := hierarchy.Parse(treeJSON) if err != nil { t.Fatal(err) @@ -513,11 +653,21 @@ func TestTypingBuiltin_TargetsEditableField(t *testing.T) { if action.X != 50 || action.Y != 20 { t.Errorf("coords = (%d,%d), want (50,20) at EmailField center", action.X, action.Y) } - if !slices.Contains(inputCorpus, action.Text) { - t.Errorf("text %q not drawn from inputCorpus", action.Text) + // The typing builtin draws from corpus.ts INPUT_CORPUS; the single editable + // field means any draw lands here, so the text must be a corpus member. + if !slices.Contains(testInputCorpus, action.Text) { + t.Errorf("text %q not drawn from the input corpus", action.Text) } } +// testInputCorpus mirrors pkg/spec/src/corpus.ts INPUT_CORPUS so the typing +// builtin's emitted text can be asserted to come from the shared pool. +var testInputCorpus = []string{ + "", "a", strings.Repeat("a", 4096), "🙂🔥💸", " ", "\t\n", "-1", + "999999999999999999999", "0.0000001", "1e10", "'; DROP TABLE--", + "", "../../etc/passwd", "%s%n", "NaN", +} + // TestTypingBuiltin_NoEditableYieldsErrNoAction verifies the typing generator // declines (ErrNoAction) when no editable element is present, so a weighted // layer falls through to another generator. @@ -529,7 +679,10 @@ func TestTypingBuiltin_NoEditableYieldsErrNoAction(t *testing.T) { ] }` verifier := newVerifier(t) - mustLoad(t, verifier, `globalThis.actions = __sanderling__.typing;`) + loadActionSpec(t, verifier, ` + import { typing } from "@sanderling/spec"; + globalThis.actions = typing; + `) tree, err := hierarchy.Parse(treeJSON) if err != nil { t.Fatal(err) @@ -740,15 +893,30 @@ func TestSelector_BooleanValue(t *testing.T) { // dynamic array picks the same element under the same seed across runs. The // folio spec relies on this to replace Math.random() in account-card taps. func TestFrom_SeededReplayIsDeterministic(t *testing.T) { + const treeJSON = `{ + "attributes": {"resource-id": "root", "bounds": "[0,0,100,200]"}, + "children": [ + {"attributes": {"resource-id": "card_a", "bounds": "[0,0,100,40]"}, "clickable": true, "enabled": true, "children": []}, + {"attributes": {"resource-id": "card_b", "bounds": "[0,40,100,80]"}, "clickable": true, "enabled": true, "children": []}, + {"attributes": {"resource-id": "card_c", "bounds": "[0,80,100,120]"}, "clickable": true, "enabled": true, "children": []}, + {"attributes": {"resource-id": "card_d", "bounds": "[0,120,100,160]"}, "clickable": true, "enabled": true, "children": []} + ] + }` + tree, err := hierarchy.Parse(treeJSON) + if err != nil { + t.Fatal(err) + } pickedSequence := func(seed uint64) []string { - verifier := newVerifier(t, WithRand(rand.New(rand.NewPCG(seed, 0)))) - mustLoad(t, verifier, ` - globalThis.actions = __sanderling__.actions(() => { - const cards = ["card_a", "card_b", "card_c", "card_d"]; - return [__sanderling__.tap({ on: __sanderling__.from(cards).generate() })]; + verifier := newVerifier(t, WithSeed(seed)) + loadActionSpec(t, verifier, ` + import { actions, from, Tap } from "@sanderling/spec"; + const cards = ["id:card_a", "id:card_b", "id:card_c", "id:card_d"]; + globalThis.actions = actions(() => { + const target = state.ax.find(from(cards).generate()); + return target ? [Tap({ on: target })] : []; }); `) - _ = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}}) + _ = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}, Tree: tree}) var picks []string for range 20 { action, err := verifier.NextAction() @@ -892,8 +1060,6 @@ globalThis.card = card; globalThis.properties = { hasTestTag: __sanderling__.always(() => typeof card.current.attrs.testTag === "string"), }; - -globalThis.actions = __sanderling__.actions(() => []); ` // TestSelectorStringFromJS_CanonicalGrammar guarantees the selector tag stamped