docs(spec): cut the package readme to a description and doc links

This commit is contained in:
pj committed 2026-08-15 13:32:23 +05:30
1 parent 4b22b45b68
commit e8beddfd70
1 file changed
+4 -58
+4 -58
View File
@@ -1,67 +1,13 @@
# @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.
## Install
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.
```sh
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
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.
The CLI bundles this package's TypeScript sources at run time, so keep the CLI and the package on the same release.