mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
docs(manual): document missing spec API in reference
This commit is contained in:
1 parent
d65728eec3
commit
eb56486d8b
1 file changed
+53
-11
@@ -4,20 +4,22 @@ title: Spec language reference
|
|||||||
|
|
||||||
# Spec language reference
|
# Spec language reference
|
||||||
|
|
||||||
|
Lookup reference for everything importable from `@sanderling/spec`. For a guided walkthrough, read [writing specs](./writing-specs/) first.
|
||||||
|
|
||||||
## Module structure
|
## Module structure
|
||||||
|
|
||||||
A spec is a TypeScript module evaluated by the Go runner each step. It must export `properties` and `actionsRoot` on `globalThis` (the bundler entry point does this automatically via the final two lines):
|
A spec is a TypeScript module evaluated by the Go runner each step. It exports `properties` and `actionsRoot`, plus an optional `setup`:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { ... } from "@sanderling/spec";
|
import { ... } from "@sanderling/spec";
|
||||||
|
|
||||||
export const properties = { ... };
|
export const properties = { ... };
|
||||||
export const actionsRoot = weighted(...);
|
export const actionsRoot = weighted(...);
|
||||||
|
export const setup = login; // optional
|
||||||
(globalThis as { actions?: unknown }).actions = actionsRoot;
|
|
||||||
(globalThis as { properties?: unknown }).properties = properties;
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`setup` is an `ActionGenerator` the runner consults before `actionsRoot` each step. While it returns actions, they run; when it returns an empty list, the runner falls through to `actionsRoot`. Use it for preconditions like login and onboarding. If the app later regresses across the precondition (a logout mid-run), `setup` re-engages on its own.
|
||||||
|
|
||||||
## State
|
## State
|
||||||
|
|
||||||
Every extractor callback receives a `State`:
|
Every extractor callback receives a `State`:
|
||||||
@@ -71,9 +73,16 @@ Every key-value pair must match. Substring and boolean rules apply per attribute
|
|||||||
|
|
||||||
Known attribute names are typed; you get autocomplete on `testTag`, `text`, `content-desc`, the boolean states (`clickable`, `enabled`, `focused`, `checked`, `selected`), and the cross-platform aliases (`identifier`, `accessibilityIdentifier`, `accessibilityText`, `accessibilityLabel`, `label`, `resource-id`, `class`, `elementType`, `package`, `placeholderValue`, `hintText`). Boolean state attributes accept a native `true` / `false`. Other attribute keys still type-check as a string-valued fallback so raw driver attributes remain reachable.
|
Known attribute names are typed; you get autocomplete on `testTag`, `text`, `content-desc`, the boolean states (`clickable`, `enabled`, `focused`, `checked`, `selected`), and the cross-platform aliases (`identifier`, `accessibilityIdentifier`, `accessibilityText`, `accessibilityLabel`, `label`, `resource-id`, `class`, `elementType`, `package`, `placeholderValue`, `hintText`). Boolean state attributes accept a native `true` / `false`. Other attribute keys still type-check as a string-valued fallback so raw driver attributes remain reachable.
|
||||||
|
|
||||||
### Path queries
|
### Path selectors
|
||||||
|
|
||||||
Chains of string selectors separated by ` > ` scope each segment to the subtree of the previous match. Path queries are only supported on the tree root (`ax.find`, `ax.findAll`), not on element-scoped `.find`/`.findAll`.
|
An array of object selectors matches a path: each segment is matched within the subtree of the previous match. Arrays work on the tree root and on element-scoped `.find`/`.findAll`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
s.ax.find([{ testTag: "LoginScreen" }, { testTag: "LoginEmail" }])
|
||||||
|
s.ax.findAll([{ testTag: "HomeScreen" }, { testTag: "AccountCard" }])
|
||||||
|
```
|
||||||
|
|
||||||
|
String selectors chain the same way with ` > `, but only on the tree root (`ax.find`, `ax.findAll`):
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
s.ax.find("id:HomeScreen > descPrefix:account_card:")
|
s.ax.find("id:HomeScreen > descPrefix:account_card:")
|
||||||
@@ -144,11 +153,13 @@ KMP apps are tested identically to native apps. An Android KMP build uses the An
|
|||||||
|
|
||||||
```ts
|
```ts
|
||||||
const loggedIn = extract((s) => !!s.ax.find("id:home-tab-bar"));
|
const loggedIn = extract((s) => !!s.ax.find("id:home-tab-bar"));
|
||||||
|
const route = extract("route", (s) => ...); // named form
|
||||||
|
|
||||||
loggedIn.current // T - value from the current step
|
loggedIn.current // T - value from the current step
|
||||||
loggedIn.previous // T | undefined - value from the previous step, undefined on first step
|
loggedIn.previous // T | undefined - value from the previous step, undefined on first step
|
||||||
```
|
```
|
||||||
|
|
||||||
Extractors are evaluated before properties and action generators. Use `.previous` to detect transitions between steps.
|
Extractors are evaluated before properties and action generators. Use `.previous` to detect transitions between steps. Named extractors appear by name in the replay UI and trace.
|
||||||
|
|
||||||
## LTL operators
|
## LTL operators
|
||||||
|
|
||||||
@@ -174,8 +185,11 @@ Extractors are evaluated before properties and action generators. Use `.previous
|
|||||||
|
|
||||||
```ts
|
```ts
|
||||||
Tap({ on: element | string })
|
Tap({ on: element | string })
|
||||||
|
DoubleTap({ on: element | string })
|
||||||
|
LongPress({ on: element | string })
|
||||||
InputText({ into: element | string, text: string })
|
InputText({ into: element | string, text: string })
|
||||||
Swipe({ from: element | Point, to: element | Point, durationMillis?: number })
|
Swipe({ from: element | Point, to: element | Point, durationMillis?: number })
|
||||||
|
Scroll({ direction: "up" | "down" | "left" | "right", in?: element | string })
|
||||||
PressKey({ key: Key })
|
PressKey({ key: Key })
|
||||||
Wait({ durationMillis: number })
|
Wait({ durationMillis: number })
|
||||||
```
|
```
|
||||||
@@ -189,6 +203,10 @@ On web, `"back"` maps to Backspace and `"home"` is not supported. All other keys
|
|||||||
| Generator | Behaviour |
|
| Generator | Behaviour |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `taps` | Random tap on a clickable element |
|
| `taps` | Random tap on a clickable element |
|
||||||
|
| `doubleTaps` | Random double tap on a clickable element |
|
||||||
|
| `longPresses` | Random long press on a clickable element |
|
||||||
|
| `typing` | Types a value from the edge-case corpus into a random editable field |
|
||||||
|
| `scrolls` | Random scroll gesture |
|
||||||
| `swipes` | Random swipe gesture |
|
| `swipes` | Random swipe gesture |
|
||||||
| `waitOnce` | Idles one step |
|
| `waitOnce` | Idles one step |
|
||||||
| `pressKeys` | Presses a random supported key |
|
| `pressKeys` | Presses a random supported key |
|
||||||
@@ -217,23 +235,47 @@ export const actionsRoot = weighted(
|
|||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
### `from(items)`
|
### `whenRoute(routeExtractor, routes, body)`
|
||||||
|
|
||||||
Returns a `Sampler<T>` that cycles through a fixed list. Use `.generate()` to pick an item.
|
Builds a generator that runs `body` only when the extractor's current value is in `routes` (a string or array of strings). Returns an empty list otherwise.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const addTxn = whenRoute(route, ["home", "ledger", "add-transaction"], () => {
|
||||||
|
...
|
||||||
|
return [Tap({ on: btn })];
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Samplers
|
||||||
|
|
||||||
|
Every sampler has `.generate()`. Draws are seeded by the run's PRNG, so a run replays identically from its seed.
|
||||||
|
|
||||||
|
| Sampler | Produces |
|
||||||
|
|---|---|
|
||||||
|
| `from(items)` | An item from a fixed list |
|
||||||
|
| `integers().between(min, max)` | An integer in the range |
|
||||||
|
| `strings().length(min, max).alpha()` | A random string; `.alpha()` restricts to letters |
|
||||||
|
| `emails().domain("example.com")` | A random email address |
|
||||||
|
| `edgeCaseText()` | A value from the adversarial input corpus (empty and whitespace strings, emoji, numeric boundary values, very long strings, injection payloads) |
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const names = from(["Checking", "Savings", "Travel"]);
|
const names = from(["Checking", "Savings", "Travel"]);
|
||||||
|
const amounts = integers().between(1, 500);
|
||||||
// inside an actions() callback:
|
// inside an actions() callback:
|
||||||
InputText({ into: nameField, text: names.generate() })
|
InputText({ into: nameField, text: names.generate() })
|
||||||
|
InputText({ into: amountField, text: String(amounts.generate()) })
|
||||||
```
|
```
|
||||||
|
|
||||||
## Default properties
|
## Defaults
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
import { defaultActions, doubleTaps } from "@sanderling/spec/defaults";
|
||||||
import { noUncaughtExceptions, noLogcatErrors } from "@sanderling/spec/defaults/properties";
|
import { noUncaughtExceptions, noLogcatErrors } from "@sanderling/spec/defaults/properties";
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`defaultActions` is a ready-made weighted tree of the built-in generators: taps and typing at weight 100, scrolls 50, swipes 25, double taps 10. Use it as a baseline pool or as one entry in your own tree.
|
||||||
|
|
||||||
| Property | Fails when |
|
| Property | Fails when |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `noUncaughtExceptions` | An uncaught exception or `Sanderling.reportError()` call is captured |
|
| `noUncaughtExceptions` | An uncaught exception or `Sanderling.reportError()` call is captured |
|
||||||
| `noLogcatErrors` | Logcat emits any error-level (`E`) lines since the previous step |
|
| `noLogcatErrors` | Logcat emits any error-level (`E`) lines since the previous step (Android only; holds elsewhere) |
|
||||||
Reference in new issue
Block a user