mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
test(verifier): author specs through the shared picker path
This commit is contained in:
1 parent
f8e3baf439
commit
aee5d8110a
3 files changed
+271
-73
No files matched your search
@@ -33,7 +33,10 @@ func pushTree(t *testing.T, v *Verifier, treeJSON string) {
|
|||||||
// the result lands on its center regardless of seed.
|
// the result lands on its center regardless of seed.
|
||||||
func TestTaps_ExcludeOffAppPackage(t *testing.T) {
|
func TestTaps_ExcludeOffAppPackage(t *testing.T) {
|
||||||
verifier := newVerifier(t, WithAppPackage("com.folio"))
|
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)
|
pushTree(t, verifier, scopedTreeJSON)
|
||||||
|
|
||||||
action, err := verifier.NextAction()
|
action, err := verifier.NextAction()
|
||||||
@@ -56,7 +59,10 @@ func TestTyping_ExcludeOffAppPackage(t *testing.T) {
|
|||||||
]
|
]
|
||||||
}`
|
}`
|
||||||
verifier := newVerifier(t, WithAppPackage("com.folio"))
|
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)
|
pushTree(t, verifier, treeJSON)
|
||||||
|
|
||||||
action, err := verifier.NextAction()
|
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.
|
// exploration never scrolls the keyboard's emoji list instead of the app.
|
||||||
func TestSwipes_ExcludeOffAppPackage(t *testing.T) {
|
func TestSwipes_ExcludeOffAppPackage(t *testing.T) {
|
||||||
verifier := newVerifier(t, WithAppPackage("com.folio"))
|
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)
|
pushTree(t, verifier, scopedTreeJSON)
|
||||||
|
|
||||||
// Both the root and SubmitButton (com.folio) are valid anchors; only the
|
// 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"))
|
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)
|
pushTree(t, verifier, treeJSON)
|
||||||
|
|
||||||
if _, err := verifier.NextAction(); !errors.Is(err, ErrNoAction) {
|
if _, err := verifier.NextAction(); !errors.Is(err, ErrNoAction) {
|
||||||
@@ -121,7 +133,10 @@ func TestTaps_UnsetAppPackageKeepsAllNodes(t *testing.T) {
|
|||||||
]
|
]
|
||||||
}`
|
}`
|
||||||
verifier := newVerifier(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)
|
pushTree(t, verifier, treeJSON)
|
||||||
|
|
||||||
action, err := verifier.NextAction()
|
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.
|
// mirrors taps: it yields a LongPress on the only clickable in-app node.
|
||||||
func TestLongPresses_TargetsClickableElement(t *testing.T) {
|
func TestLongPresses_TargetsClickableElement(t *testing.T) {
|
||||||
verifier := newVerifier(t, WithAppPackage("com.folio"))
|
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)
|
pushTree(t, verifier, scopedTreeJSON)
|
||||||
|
|
||||||
action, err := verifier.NextAction()
|
action, err := verifier.NextAction()
|
||||||
@@ -163,7 +181,10 @@ func TestScrolls_TargetsScrollableContainer(t *testing.T) {
|
|||||||
]
|
]
|
||||||
}`
|
}`
|
||||||
verifier := newVerifier(t, WithAppPackage("com.folio"))
|
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)
|
pushTree(t, verifier, treeJSON)
|
||||||
|
|
||||||
for range 50 {
|
for range 50 {
|
||||||
@@ -190,8 +211,8 @@ func TestScrolls_TargetsScrollableContainer(t *testing.T) {
|
|||||||
default:
|
default:
|
||||||
t.Fatalf("unexpected direction %q", action.Direction)
|
t.Fatalf("unexpected direction %q", action.Direction)
|
||||||
}
|
}
|
||||||
if action.DurationMillis != 300 {
|
if action.DurationMillis != 250 {
|
||||||
t.Fatalf("durationMillis = %d, want 300", action.DurationMillis)
|
t.Fatalf("durationMillis = %d, want 250", action.DurationMillis)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,7 +221,10 @@ func TestScrolls_TargetsScrollableContainer(t *testing.T) {
|
|||||||
// no scrollable container is present.
|
// no scrollable container is present.
|
||||||
func TestScrolls_NoScrollableYieldsErrNoAction(t *testing.T) {
|
func TestScrolls_NoScrollableYieldsErrNoAction(t *testing.T) {
|
||||||
verifier := newVerifier(t, WithAppPackage("com.folio"))
|
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)
|
pushTree(t, verifier, scopedTreeJSON)
|
||||||
|
|
||||||
if _, err := verifier.NextAction(); !errors.Is(err, ErrNoAction) {
|
if _, err := verifier.NextAction(); !errors.Is(err, ErrNoAction) {
|
||||||
@@ -219,7 +243,10 @@ func TestTaps_EmptyPackageNodeStaysInScope(t *testing.T) {
|
|||||||
]
|
]
|
||||||
}`
|
}`
|
||||||
verifier := newVerifier(t, WithAppPackage("com.folio"))
|
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)
|
pushTree(t, verifier, treeJSON)
|
||||||
|
|
||||||
action, err := verifier.NextAction()
|
action, err := verifier.NextAction()
|
||||||
|
|||||||
@@ -23,12 +23,17 @@ func bundleIntegrationSpec(t *testing.T) string {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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")
|
defaultsPath, err := filepath.Abs("../../pkg/spec/src/defaults/properties.ts")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
bundle, err := bundler.Bundle(bundler.Options{
|
bundle, err := bundler.Bundle(bundler.Options{
|
||||||
EntryFile: specPath,
|
EntryFile: specPath,
|
||||||
|
RuntimeFile: runtimePath,
|
||||||
Aliases: map[string]string{
|
Aliases: map[string]string{
|
||||||
"@sanderling/spec": apiPath,
|
"@sanderling/spec": apiPath,
|
||||||
"@sanderling/spec/defaults/properties": defaultsPath,
|
"@sanderling/spec/defaults/properties": defaultsPath,
|
||||||
|
|||||||
@@ -3,13 +3,15 @@ package verifier
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"math/rand/v2"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
|
|
||||||
|
"github.com/priyanshujain/sanderling/internal/bundler"
|
||||||
"github.com/priyanshujain/sanderling/internal/hierarchy"
|
"github.com/priyanshujain/sanderling/internal/hierarchy"
|
||||||
"github.com/priyanshujain/sanderling/internal/ltl"
|
"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 helloSpec = `
|
||||||
const screen = __sanderling__.extract(state => state.snapshots.screen ?? "");
|
const screen = __sanderling__.extract(state => state.snapshots.screen ?? "");
|
||||||
const balance = __sanderling__.extract(state => state.snapshots["ledger.balance"] ?? 0);
|
const balance = __sanderling__.extract(state => state.snapshots["ledger.balance"] ?? 0);
|
||||||
@@ -40,10 +79,6 @@ globalThis.balance = balance;
|
|||||||
globalThis.properties = {
|
globalThis.properties = {
|
||||||
balanceNonNegative: __sanderling__.always(() => balance.current >= 0),
|
balanceNonNegative: __sanderling__.always(() => balance.current >= 0),
|
||||||
};
|
};
|
||||||
|
|
||||||
globalThis.actions = __sanderling__.actions(() => [
|
|
||||||
__sanderling__.tap({ on: "id:home_button" }),
|
|
||||||
]);
|
|
||||||
`
|
`
|
||||||
|
|
||||||
func TestLoad_ExposesRuntimeBindings(t *testing.T) {
|
func TestLoad_ExposesRuntimeBindings(t *testing.T) {
|
||||||
@@ -231,9 +266,25 @@ globalThis.properties = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNextAction_FromActionsGenerator(t *testing.T) {
|
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)
|
verifier := newVerifier(t)
|
||||||
mustLoad(t, verifier, helloSpec)
|
loadActionSpec(t, verifier, `
|
||||||
_ = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}})
|
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()
|
action, err := verifier.NextAction()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -248,16 +299,31 @@ func TestNextAction_FromActionsGenerator(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNextAction_WeightedSelectsByWeight(t *testing.T) {
|
func TestNextAction_WeightedSelectsByWeight(t *testing.T) {
|
||||||
verifier := newVerifier(t, WithRand(rand.New(rand.NewPCG(42, 0))))
|
const treeJSON = `{
|
||||||
mustLoad(t, verifier, `
|
"attributes": {"resource-id": "root", "bounds": "[0,0,100,100]"},
|
||||||
const tapHome = __sanderling__.actions(() => [__sanderling__.tap({ on: "id:home" })]);
|
"children": [
|
||||||
const tapAway = __sanderling__.actions(() => [__sanderling__.tap({ on: "id:away" })]);
|
{"attributes": {"resource-id": "home", "bounds": "[0,0,100,40]"}, "clickable": true, "enabled": true, "children": []},
|
||||||
globalThis.actions = __sanderling__.weighted(
|
{"attributes": {"resource-id": "away", "bounds": "[0,40,100,80]"}, "clickable": true, "enabled": true, "children": []}
|
||||||
[1, tapHome],
|
]
|
||||||
[99, tapAway],
|
}`
|
||||||
);
|
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
|
awayCount := 0
|
||||||
homeCount := 0
|
homeCount := 0
|
||||||
@@ -280,8 +346,9 @@ func TestNextAction_WeightedSelectsByWeight(t *testing.T) {
|
|||||||
|
|
||||||
func TestNextAction_EmptyGeneratorReturnsErrNoAction(t *testing.T) {
|
func TestNextAction_EmptyGeneratorReturnsErrNoAction(t *testing.T) {
|
||||||
verifier := newVerifier(t)
|
verifier := newVerifier(t)
|
||||||
mustLoad(t, verifier, `
|
loadActionSpec(t, verifier, `
|
||||||
globalThis.actions = __sanderling__.actions(() => []);
|
import { actions } from "@sanderling/spec";
|
||||||
|
globalThis.actions = actions(() => []);
|
||||||
`)
|
`)
|
||||||
_ = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}})
|
_ = 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) {
|
func TestNextAction_SetupTakesPrecedenceWhenYielding(t *testing.T) {
|
||||||
verifier := newVerifier(t)
|
verifier := newVerifier(t)
|
||||||
mustLoad(t, verifier, `
|
loadActionSpec(t, verifier, `
|
||||||
globalThis.setup = __sanderling__.actions(() => [__sanderling__.tap({ on: "id:setup" })]);
|
import { actions, Tap } from "@sanderling/spec";
|
||||||
globalThis.actions = __sanderling__.actions(() => [__sanderling__.tap({ on: "id:main" })]);
|
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()
|
action, err := verifier.NextAction()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -310,11 +398,19 @@ func TestNextAction_SetupTakesPrecedenceWhenYielding(t *testing.T) {
|
|||||||
|
|
||||||
func TestNextAction_FallsThroughToActionsWhenSetupEmpty(t *testing.T) {
|
func TestNextAction_FallsThroughToActionsWhenSetupEmpty(t *testing.T) {
|
||||||
verifier := newVerifier(t)
|
verifier := newVerifier(t)
|
||||||
mustLoad(t, verifier, `
|
loadActionSpec(t, verifier, `
|
||||||
globalThis.setup = __sanderling__.actions(() => []);
|
import { actions, Tap } from "@sanderling/spec";
|
||||||
globalThis.actions = __sanderling__.actions(() => [__sanderling__.tap({ on: "id:main" })]);
|
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()
|
action, err := verifier.NextAction()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -327,21 +423,30 @@ func TestNextAction_FallsThroughToActionsWhenSetupEmpty(t *testing.T) {
|
|||||||
|
|
||||||
func TestNextAction_SetupReengagesAfterRegression(t *testing.T) {
|
func TestNextAction_SetupReengagesAfterRegression(t *testing.T) {
|
||||||
verifier := newVerifier(t)
|
verifier := newVerifier(t)
|
||||||
mustLoad(t, verifier, `
|
loadActionSpec(t, verifier, `
|
||||||
globalThis.loggedIn = __sanderling__.extract(state => state.snapshots["loggedIn"] === true);
|
import { actions, extract, Tap } from "@sanderling/spec";
|
||||||
globalThis.setup = __sanderling__.actions(() => {
|
const loggedIn = extract(state => state.snapshots["loggedIn"] === true);
|
||||||
|
globalThis.setup = actions(() => {
|
||||||
if (loggedIn.current) return [];
|
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) {
|
push := func(loggedIn bool) {
|
||||||
raw := json.RawMessage(`false`)
|
raw := json.RawMessage(`false`)
|
||||||
if loggedIn {
|
if loggedIn {
|
||||||
raw = json.RawMessage(`true`)
|
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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -376,14 +481,19 @@ func TestNextAction_SetupReengagesAfterRegression(t *testing.T) {
|
|||||||
|
|
||||||
func TestNextAction_NoSetupRegistered(t *testing.T) {
|
func TestNextAction_NoSetupRegistered(t *testing.T) {
|
||||||
verifier := newVerifier(t)
|
verifier := newVerifier(t)
|
||||||
mustLoad(t, verifier, `
|
loadActionSpec(t, verifier, `
|
||||||
globalThis.actions = __sanderling__.actions(() => [__sanderling__.tap({ on: "id:main" })]);
|
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{}})
|
tree, err := hierarchy.Parse(setupTree)
|
||||||
|
if err != nil {
|
||||||
if verifier.setupGenerator != nil {
|
t.Fatal(err)
|
||||||
t.Errorf("setupGenerator should be nil when spec does not export setup")
|
|
||||||
}
|
}
|
||||||
|
_ = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}, Tree: tree})
|
||||||
|
|
||||||
action, err := verifier.NextAction()
|
action, err := verifier.NextAction()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -403,7 +513,10 @@ func TestDoubleTapsBuiltin_TargetsClickable(t *testing.T) {
|
|||||||
]
|
]
|
||||||
}`
|
}`
|
||||||
verifier := newVerifier(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)
|
tree, err := hierarchy.Parse(treeJSON)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -442,13 +555,25 @@ func TestDoubleTapsBuiltin_TargetsClickable(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDoubleTap_RoundTrip(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)
|
verifier := newVerifier(t)
|
||||||
mustLoad(t, verifier, `
|
loadActionSpec(t, verifier, `
|
||||||
globalThis.actions = __sanderling__.actions(() => [
|
import { actions, DoubleTap } from "@sanderling/spec";
|
||||||
__sanderling__.doubleTap({ on: "id:save" }),
|
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()
|
action, err := verifier.NextAction()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -463,13 +588,25 @@ func TestDoubleTap_RoundTrip(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestInputText_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)
|
verifier := newVerifier(t)
|
||||||
mustLoad(t, verifier, `
|
loadActionSpec(t, verifier, `
|
||||||
globalThis.actions = __sanderling__.actions(() => [
|
import { actions, InputText } from "@sanderling/spec";
|
||||||
__sanderling__.inputText({ into: "id:phone", text: "+919876543210" }),
|
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()
|
action, err := verifier.NextAction()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -495,7 +632,10 @@ func TestTypingBuiltin_TargetsEditableField(t *testing.T) {
|
|||||||
]
|
]
|
||||||
}`
|
}`
|
||||||
verifier := newVerifier(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)
|
tree, err := hierarchy.Parse(treeJSON)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -513,11 +653,21 @@ func TestTypingBuiltin_TargetsEditableField(t *testing.T) {
|
|||||||
if action.X != 50 || action.Y != 20 {
|
if action.X != 50 || action.Y != 20 {
|
||||||
t.Errorf("coords = (%d,%d), want (50,20) at EmailField center", action.X, action.Y)
|
t.Errorf("coords = (%d,%d), want (50,20) at EmailField center", action.X, action.Y)
|
||||||
}
|
}
|
||||||
if !slices.Contains(inputCorpus, action.Text) {
|
// The typing builtin draws from corpus.ts INPUT_CORPUS; the single editable
|
||||||
t.Errorf("text %q not drawn from inputCorpus", action.Text)
|
// 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--",
|
||||||
|
"<script>alert(1)</script>", "../../etc/passwd", "%s%n", "NaN",
|
||||||
|
}
|
||||||
|
|
||||||
// TestTypingBuiltin_NoEditableYieldsErrNoAction verifies the typing generator
|
// TestTypingBuiltin_NoEditableYieldsErrNoAction verifies the typing generator
|
||||||
// declines (ErrNoAction) when no editable element is present, so a weighted
|
// declines (ErrNoAction) when no editable element is present, so a weighted
|
||||||
// layer falls through to another generator.
|
// layer falls through to another generator.
|
||||||
@@ -529,7 +679,10 @@ func TestTypingBuiltin_NoEditableYieldsErrNoAction(t *testing.T) {
|
|||||||
]
|
]
|
||||||
}`
|
}`
|
||||||
verifier := newVerifier(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)
|
tree, err := hierarchy.Parse(treeJSON)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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
|
// 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.
|
// folio spec relies on this to replace Math.random() in account-card taps.
|
||||||
func TestFrom_SeededReplayIsDeterministic(t *testing.T) {
|
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 {
|
pickedSequence := func(seed uint64) []string {
|
||||||
verifier := newVerifier(t, WithRand(rand.New(rand.NewPCG(seed, 0))))
|
verifier := newVerifier(t, WithSeed(seed))
|
||||||
mustLoad(t, verifier, `
|
loadActionSpec(t, verifier, `
|
||||||
globalThis.actions = __sanderling__.actions(() => {
|
import { actions, from, Tap } from "@sanderling/spec";
|
||||||
const cards = ["card_a", "card_b", "card_c", "card_d"];
|
const cards = ["id:card_a", "id:card_b", "id:card_c", "id:card_d"];
|
||||||
return [__sanderling__.tap({ on: __sanderling__.from(cards).generate() })];
|
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
|
var picks []string
|
||||||
for range 20 {
|
for range 20 {
|
||||||
action, err := verifier.NextAction()
|
action, err := verifier.NextAction()
|
||||||
@@ -892,8 +1060,6 @@ globalThis.card = card;
|
|||||||
globalThis.properties = {
|
globalThis.properties = {
|
||||||
hasTestTag: __sanderling__.always(() => typeof card.current.attrs.testTag === "string"),
|
hasTestTag: __sanderling__.always(() => typeof card.current.attrs.testTag === "string"),
|
||||||
};
|
};
|
||||||
|
|
||||||
globalThis.actions = __sanderling__.actions(() => []);
|
|
||||||
`
|
`
|
||||||
|
|
||||||
// TestSelectorStringFromJS_CanonicalGrammar guarantees the selector tag stamped
|
// TestSelectorStringFromJS_CanonicalGrammar guarantees the selector tag stamped
|
||||||
|
|||||||
Reference in new issue
Block a user