mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
Merge branch 'spec-npm-release' into pr-73-followups
# Conflicts: # Makefile
This commit is contained in:
commit
b2877a97b1
11 files changed
+207
-74
No files matched your search
+1
-1
@@ -50,7 +50,7 @@ internal/driver/ioscompanion/companionassets/assets/companion-*.tar.gz
|
|||||||
# before `go build -tags withcompanion`. Never commit: it's ~5 MB.
|
# before `go build -tags withcompanion`. Never commit: it's ~5 MB.
|
||||||
internal/driver/ioscompanion/runnerassets/assets/runner-*.tar.gz
|
internal/driver/ioscompanion/runnerassets/assets/runner-*.tar.gz
|
||||||
|
|
||||||
# spec-api compiled output
|
# spec package compiled output
|
||||||
pkg/spec/dist/
|
pkg/spec/dist/
|
||||||
|
|
||||||
# goreleaser local output
|
# goreleaser local output
|
||||||
|
|||||||
+4
-4
@@ -5,10 +5,10 @@ project_name: sanderling
|
|||||||
before:
|
before:
|
||||||
hooks:
|
hooks:
|
||||||
# The Go binary embeds the sidecar fat JAR via //go:embed gated by the
|
# The Go binary embeds the sidecar fat JAR via //go:embed gated by the
|
||||||
# `withsidecar` build tag. Rebuild the JAR and stage it at the embed
|
# `withsidecar` build tag. The Makefile target rebuilds the JAR and stages
|
||||||
# path so the `go build` below picks up fresh bytes.
|
# it at the embed path, so the `go build` below picks up fresh bytes and
|
||||||
- make sidecar
|
# that path stays spelled out in exactly one place.
|
||||||
- sh -c 'mkdir -p internal/sidecar/assets && cp sidecar/build/libs/sidecar-all.jar internal/sidecar/assets/sidecar-all.jar'
|
- make sidecar-embed
|
||||||
|
|
||||||
builds:
|
builds:
|
||||||
- id: sanderling
|
- id: sanderling
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ WEB_DIST := replay-ui/dist
|
|||||||
|
|
||||||
GOLINES := $(shell $(GO) env GOPATH)/bin/golines
|
GOLINES := $(shell $(GO) env GOPATH)/bin/golines
|
||||||
|
|
||||||
.PHONY: bootstrap proto sidecar sanderling sanderling-web sanderling-android sanderling-ios install test test-go test-browser test-companion test-kotlin test-spec-api test-ci-scripts spec-typecheck web-test web-typecheck web-build web-dev replay-dev docs clean release-cli release-npm-dry fmt fmt-go fmt-kotlin fmt-ts fmt-swift
|
.PHONY: bootstrap proto sidecar sidecar-embed sanderling sanderling-web sanderling-android sanderling-ios install test test-go test-browser test-companion test-kotlin test-spec-api test-ci-scripts spec-typecheck web-test web-typecheck web-build web-dev replay-dev docs clean release-cli release-npm-dry fmt fmt-go fmt-kotlin fmt-ts fmt-swift
|
||||||
|
|
||||||
bootstrap:
|
bootstrap:
|
||||||
$(GO) mod download
|
$(GO) mod download
|
||||||
@@ -42,6 +42,10 @@ proto:
|
|||||||
|
|
||||||
sidecar: $(SIDECAR_JAR)
|
sidecar: $(SIDECAR_JAR)
|
||||||
|
|
||||||
|
# Stages the JAR where //go:embed expects it. Release tooling calls this rather
|
||||||
|
# than copying the JAR itself, so the embed path is spelled out in one place.
|
||||||
|
sidecar-embed: $(SIDECAR_EMBED)
|
||||||
|
|
||||||
sanderling: $(SANDERLING_BIN)
|
sanderling: $(SANDERLING_BIN)
|
||||||
|
|
||||||
$(SANDERLING_BIN): $(SIDECAR_EMBED) $(COMPANION_EMBED) $(RUNNER_EMBED) web-build
|
$(SANDERLING_BIN): $(SIDECAR_EMBED) $(COMPANION_EMBED) $(RUNNER_EMBED) web-build
|
||||||
@@ -176,7 +180,7 @@ $(PAGE_OUT): build/site/%/index.html: docs/%.md $(DOCS_TEMPLATE)
|
|||||||
|
|
||||||
clean:
|
clean:
|
||||||
$(GO) clean
|
$(GO) clean
|
||||||
rm -rf bin dist pkg/spec-api/dist build/site
|
rm -rf bin dist pkg/spec/dist build/site
|
||||||
$(GRADLE) clean
|
$(GRADLE) clean
|
||||||
|
|
||||||
# Local release dry-runs. None of these touch remote registries.
|
# Local release dry-runs. None of these touch remote registries.
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ The spec package, in your project:
|
|||||||
npm install --save-dev @sanderling/spec
|
npm install --save-dev @sanderling/spec
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Both come from the same release tag, and the CLI bundles the package's TypeScript sources when it evaluates your spec, so upgrade them together. Pre-releases are published under npm's `next` tag; `npm install @sanderling/spec` gives you the current stable one.
|
||||||
|
|
||||||
## Check your environment
|
## Check your environment
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -350,16 +350,20 @@ func resolveRuntimeSibling(specAPIPath, userSpecPath, filename string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveSpecAPIPath returns the path to pkg/spec/src/index.ts inside
|
// resolveSpecAPIPath returns the path to the spec API's index.ts: a sanderling
|
||||||
// a sanderling source checkout, searched upward from the spec file and the cwd.
|
// source checkout first, searched upward from the spec file and the cwd, then
|
||||||
// Returns "" when not found, in which case esbuild resolves @sanderling/spec via
|
// an installed node_modules/@sanderling/spec. Aliasing the installed copy is
|
||||||
// node_modules the way a downstream user's project would.
|
// what keeps the spec and the runtime entry on one module graph; resolving the
|
||||||
|
// bare specifier through package.json "exports" would load dist/ alongside the
|
||||||
|
// runtime's src/ and give sampler-rng.ts two instances.
|
||||||
func resolveSpecAPIPath(specPath string) string {
|
func resolveSpecAPIPath(specPath string) string {
|
||||||
var candidates []string
|
var checkout, installed []string
|
||||||
if absoluteSpec, err := filepath.Abs(specPath); err == nil {
|
if absoluteSpec, err := filepath.Abs(specPath); err == nil {
|
||||||
directory := filepath.Dir(absoluteSpec)
|
directory := filepath.Dir(absoluteSpec)
|
||||||
for {
|
for {
|
||||||
candidates = append(candidates, filepath.Join(directory, "pkg/spec/src/index.ts"))
|
checkout = append(checkout, filepath.Join(directory, "pkg/spec/src/index.ts"))
|
||||||
|
installed = append(installed,
|
||||||
|
filepath.Join(directory, "node_modules/@sanderling/spec/src/index.ts"))
|
||||||
parent := filepath.Dir(directory)
|
parent := filepath.Dir(directory)
|
||||||
if parent == directory {
|
if parent == directory {
|
||||||
break
|
break
|
||||||
@@ -368,9 +372,9 @@ func resolveSpecAPIPath(specPath string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if cwd, err := os.Getwd(); err == nil {
|
if cwd, err := os.Getwd(); err == nil {
|
||||||
candidates = append(candidates, filepath.Join(cwd, "pkg/spec/src/index.ts"))
|
checkout = append(checkout, filepath.Join(cwd, "pkg/spec/src/index.ts"))
|
||||||
}
|
}
|
||||||
for _, candidate := range candidates {
|
for _, candidate := range append(checkout, installed...) {
|
||||||
if _, err := os.Stat(candidate); err == nil {
|
if _, err := os.Stat(candidate); err == nil {
|
||||||
return candidate
|
return candidate
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package testrun
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -304,3 +306,153 @@ func TestLaunchAppBoundsWedgedDriver(t *testing.T) {
|
|||||||
t.Fatal("launchApp never returned: the pre-run launch is unbounded, so a wedged driver hangs the run forever")
|
t.Fatal("launchApp never returned: the pre-run launch is unbounded, so a wedged driver hangs the run forever")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// repoFile walks up from the test's working directory and returns the absolute
|
||||||
|
// path of rel inside the sanderling checkout.
|
||||||
|
func repoFile(t *testing.T, rel string) string {
|
||||||
|
t.Helper()
|
||||||
|
directory, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
candidate := filepath.Join(directory, rel)
|
||||||
|
if _, err := os.Stat(candidate); err == nil {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
parent := filepath.Dir(directory)
|
||||||
|
if parent == directory {
|
||||||
|
t.Fatalf("%s not found above the test directory", rel)
|
||||||
|
}
|
||||||
|
directory = parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// publishedFiles returns the "files" entries of pkg/spec/package.json, the
|
||||||
|
// exact set npm ships in the @sanderling/spec tarball.
|
||||||
|
func publishedFiles(t *testing.T) []string {
|
||||||
|
t.Helper()
|
||||||
|
raw, err := os.ReadFile(repoFile(t, "pkg/spec/package.json"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var manifest struct {
|
||||||
|
Files []string `json:"files"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &manifest); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return manifest.Files
|
||||||
|
}
|
||||||
|
|
||||||
|
// installPublishedPackage reproduces what `npm install @sanderling/spec`
|
||||||
|
// unpacks into node_modules: only the paths package.json publishes.
|
||||||
|
func installPublishedPackage(t *testing.T, dest string) {
|
||||||
|
t.Helper()
|
||||||
|
specDir := filepath.Dir(repoFile(t, "pkg/spec/package.json"))
|
||||||
|
for _, entry := range publishedFiles(t) {
|
||||||
|
source := filepath.Join(specDir, entry)
|
||||||
|
if _, err := os.Stat(source); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
copyTree(t, source, filepath.Join(dest, entry))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyTree(t *testing.T, source, dest string) {
|
||||||
|
t.Helper()
|
||||||
|
err := filepath.WalkDir(source, func(path string, entry fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
relative, err := filepath.Rel(source, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
target := filepath.Join(dest, relative)
|
||||||
|
if entry.IsDir() {
|
||||||
|
return os.MkdirAll(target, 0o755)
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(target, data, 0o644)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestResolveRuntimeSibling_PublishedPackageShipsTheRuntimes pins npm's "files"
|
||||||
|
// list against the resolver that consumes it. The tarball shipped dist/ alone
|
||||||
|
// while the node_modules fallback looks for src/goja-runtime.ts, so every
|
||||||
|
// `npm install @sanderling/spec` user hit "goja-runtime.ts not found".
|
||||||
|
func TestResolveRuntimeSibling_PublishedPackageShipsTheRuntimes(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
installPublishedPackage(t, filepath.Join(root, "node_modules", "@sanderling", "spec"))
|
||||||
|
specPath := filepath.Join(root, "spec.ts")
|
||||||
|
if err := os.WriteFile(specPath, []byte(""), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, filename := range []string{"goja-runtime.ts", "web-runtime.ts"} {
|
||||||
|
if resolveRuntimeSibling("", specPath, filename) == "" {
|
||||||
|
t.Errorf("%s unreachable from a published install; package.json publishes %v",
|
||||||
|
filename, publishedFiles(t))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPrepareBundleInputs_InstalledPackageSharesOneModuleGraph pins the
|
||||||
|
// downstream case: with no sanderling checkout above the spec, the aliases and
|
||||||
|
// the runtime entry must name the SAME installed copy. An unset alias let
|
||||||
|
// esbuild resolve @sanderling/spec to dist/ while the runtime came from src/,
|
||||||
|
// which loads sampler-rng.ts twice; from(), strings(), integers() and emails()
|
||||||
|
// then read an rng the picker never set and collapse to a fixed default.
|
||||||
|
func TestPrepareBundleInputs_InstalledPackageSharesOneModuleGraph(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
installed := filepath.Join(root, "node_modules", "@sanderling", "spec")
|
||||||
|
installPublishedPackage(t, installed)
|
||||||
|
specPath := filepath.Join(root, "sanderling", "spec.ts")
|
||||||
|
if err := os.MkdirAll(filepath.Dir(specPath), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(specPath, []byte(""), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = os.Chdir(cwd) })
|
||||||
|
if err := os.Chdir(root); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
prep, err := prepareBundleInputs(Options{Spec: specPath})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
source := filepath.Join(installed, "src")
|
||||||
|
want := map[string]string{
|
||||||
|
"@sanderling/spec": filepath.Join(source, "index.ts"),
|
||||||
|
"@sanderling/spec/defaults": filepath.Join(source, "defaults/index.ts"),
|
||||||
|
"@sanderling/spec/defaults/properties": filepath.Join(source, "defaults/properties.ts"),
|
||||||
|
}
|
||||||
|
for key, wantValue := range want {
|
||||||
|
if prep.aliases[key] != wantValue {
|
||||||
|
t.Errorf("alias %q = %q, want %q", key, prep.aliases[key], wantValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got := prep.gojaRuntimePath; got != filepath.Join(source, "goja-runtime.ts") {
|
||||||
|
t.Errorf("gojaRuntimePath = %q, want it beside the aliased index.ts", got)
|
||||||
|
}
|
||||||
|
if got := resolveWebRuntimePath(prep.specAPIPath, specPath); got != filepath.Join(source, "web-runtime.ts") {
|
||||||
|
t.Errorf("webRuntimePath = %q, want it beside the aliased index.ts", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ type stateInput struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// stateObject builds the JS-side `state` object matching the State type from
|
// stateObject builds the JS-side `state` object matching the State type from
|
||||||
// pkg/spec-api. Fields beyond snapshots/ax are included when the caller
|
// pkg/spec. Fields beyond snapshots/ax are included when the caller
|
||||||
// populated them on stateInput.
|
// populated them on stateInput.
|
||||||
func stateObject(runtime *goja.Runtime, input stateInput) (*goja.Object, error) {
|
func stateObject(runtime *goja.Runtime, input stateInput) (*goja.Object, error) {
|
||||||
state := runtime.NewObject()
|
state := runtime.NewObject()
|
||||||
|
|||||||
+4
-58
@@ -1,67 +1,13 @@
|
|||||||
# @sanderling/spec
|
# @sanderling/spec
|
||||||
|
|
||||||
TypeScript spec API for [sanderling](https://github.com/priyanshujain/sanderling), a property-based UI fuzzer for mobile and web apps.
|
TypeScript spec API for [sanderling](https://github.com/priyanshujain/sanderling), a property-based UI fuzzer for Android, iOS and web apps.
|
||||||
|
|
||||||
Spec authors write properties (what the app must always or eventually do), extractors (structured state from the UI), and action generators (what sanderling is allowed to do). The `sanderling` CLI evaluates the spec in a loop against a running app.
|
A spec exports properties (what the app must always or eventually do), extractors (structured state read off the UI), and action generators (what sanderling is allowed to do). The `sanderling` CLI evaluates the spec against a running app once per step.
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm install --save-dev @sanderling/spec
|
npm install --save-dev @sanderling/spec
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
[Getting started](https://priyanshujain.github.io/sanderling/manual/getting-started/) installs the CLI and runs a first spec. The [spec language reference](https://priyanshujain.github.io/sanderling/manual/spec-language/) lists every primitive, and the [case study](https://priyanshujain.github.io/sanderling/manual/case-study/) walks a complete spec end to end.
|
||||||
|
|
||||||
```ts
|
The CLI bundles this package's TypeScript sources at run time, so keep the CLI and the package on the same release.
|
||||||
import { extract, always, eventually, actions, weighted, taps, swipes, InputText, Tap } from "@sanderling/spec";
|
|
||||||
|
|
||||||
const loggedIn = extract((s) => !!s.ax.find("id:home-tab-bar"));
|
|
||||||
const balance = extract<number>((s) => (s.snapshots.balance as number) ?? 0);
|
|
||||||
const emailField = extract((s) => s.ax.find("id:email-field"));
|
|
||||||
const submitButton = extract((s) => s.ax.find("id:sign-in-button"));
|
|
||||||
|
|
||||||
export const properties = {
|
|
||||||
balanceNeverNegative: always(() => balance.current >= 0),
|
|
||||||
loginSucceeds: eventually(() => loggedIn.current).within(30, "seconds"),
|
|
||||||
};
|
|
||||||
|
|
||||||
const doLogin = actions(() => {
|
|
||||||
if (loggedIn.current) return [];
|
|
||||||
const email = emailField.current;
|
|
||||||
const submit = submitButton.current;
|
|
||||||
if (!email || !submit) return [];
|
|
||||||
return [InputText({ into: email, text: "[email protected]" }), Tap({ on: submit })];
|
|
||||||
});
|
|
||||||
|
|
||||||
export const actionsRoot = weighted(
|
|
||||||
[50, doLogin],
|
|
||||||
[10, taps],
|
|
||||||
[2, swipes],
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Setup actions
|
|
||||||
|
|
||||||
Some action generators are not fuzz targets but preconditions: they drive the
|
|
||||||
app from a fresh state into the surface you actually want to fuzz (login,
|
|
||||||
onboarding, permission grants, seed data). Export them as `setup` instead of
|
|
||||||
mixing them into `actionsRoot`. The runner tries `setup` first; if it yields
|
|
||||||
no action, it falls through to `actionsRoot`. State regressing back across the
|
|
||||||
precondition (e.g. logout under fuzz) automatically re-engages setup.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const login = actions(() => {
|
|
||||||
if (loggedIn.current) return [];
|
|
||||||
return [InputText({ into: emailField.current!, text: "[email protected]" }), Tap({ on: submitButton.current! })];
|
|
||||||
});
|
|
||||||
|
|
||||||
export const setup = login;
|
|
||||||
export const actionsRoot = weighted([60, browse], [40, edit]);
|
|
||||||
|
|
||||||
(globalThis as { setup?: unknown }).setup = setup;
|
|
||||||
```
|
|
||||||
|
|
||||||
Setup is just an `ActionGenerator`; compose with `actions`, `weighted`, or
|
|
||||||
`whenRoute` exactly like the main pool.
|
|
||||||
|
|
||||||
Works identically across Android, iOS, and web targets.
|
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
"src",
|
||||||
"README.md"
|
"README.md"
|
||||||
],
|
],
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export type {
|
|||||||
Action,
|
Action,
|
||||||
ActionGenerator,
|
ActionGenerator,
|
||||||
AttrSelector,
|
AttrSelector,
|
||||||
|
Direction,
|
||||||
DoubleTapAction,
|
DoubleTapAction,
|
||||||
EventuallyFormula,
|
EventuallyFormula,
|
||||||
ExceptionRecord,
|
ExceptionRecord,
|
||||||
@@ -13,10 +14,12 @@ export type {
|
|||||||
Key,
|
Key,
|
||||||
KnownAttrSelectors,
|
KnownAttrSelectors,
|
||||||
LogEntry,
|
LogEntry,
|
||||||
|
LongPressAction,
|
||||||
Point,
|
Point,
|
||||||
PressKeyAction,
|
PressKeyAction,
|
||||||
RawAttrs,
|
RawAttrs,
|
||||||
Sampler,
|
Sampler,
|
||||||
|
ScrollAction,
|
||||||
SelectorPath,
|
SelectorPath,
|
||||||
Snapshots,
|
Snapshots,
|
||||||
State,
|
State,
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ import {
|
|||||||
weighted,
|
weighted,
|
||||||
whenRoute,
|
whenRoute,
|
||||||
} from "../src/index.ts";
|
} from "../src/index.ts";
|
||||||
|
import type {
|
||||||
|
Action,
|
||||||
|
Direction,
|
||||||
|
LongPressAction,
|
||||||
|
ScrollAction,
|
||||||
|
} from "../src/index.ts";
|
||||||
import { setSamplerRng } from "../src/actions.ts";
|
import { setSamplerRng } from "../src/actions.ts";
|
||||||
import { Pcg } from "../src/pcg.ts";
|
import { Pcg } from "../src/pcg.ts";
|
||||||
import type { GeneratorNode } from "../src/action-tree.ts";
|
import type { GeneratorNode } from "../src/action-tree.ts";
|
||||||
@@ -397,3 +403,18 @@ test("whenRoute body is skipped for a null route", () => {
|
|||||||
const node = whenRoute(route, ["home"], () => [Tap({ on: "id:x" })]);
|
const node = whenRoute(route, ["home"], () => [Tap({ on: "id:x" })]);
|
||||||
assert.deepEqual((node as { generate: () => unknown }).generate(), []);
|
assert.deepEqual((node as { generate: () => unknown }).generate(), []);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The package entry is the only module a spec author can import from, so every
|
||||||
|
// member of the exported Action union, and the Direction needed to build a
|
||||||
|
// Scroll, has to be reachable there rather than only from src/types.ts.
|
||||||
|
test("index exports every action type a spec author annotates with", () => {
|
||||||
|
const direction: Direction = "down";
|
||||||
|
const scroll: ScrollAction = Scroll({ direction, in: "id:list" });
|
||||||
|
const longPress: LongPressAction = LongPress({ on: "id:row" });
|
||||||
|
const built: Action[] = [scroll, longPress];
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
built.map(action => action.kind),
|
||||||
|
["Scroll", "LongPress"],
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user