Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,11 +187,11 @@ coordinates directly.
```ts
import { connect } from "simdeck/test";

const sim = await connect();
const sim = await connect({ udid: "<udid>" });
try {
await sim.tap("<udid>", 0.5, 0.5);
await sim.waitFor("<udid>", { label: "Continue" });
await sim.screenshot("<udid>");
await sim.tap(0.5, 0.5);
await sim.waitFor({ label: "Continue" });
await sim.screenshot();
} finally {
sim.close();
}
Expand All @@ -200,6 +200,12 @@ try {
`connect()` starts the project daemon when needed, reuses it when it is already
healthy, and only stops daemons it started itself.

Run common Maestro YAML flows against the same daemon-backed iOS Simulator API:

```sh
simdeck maestro test <udid> flow.yaml --artifacts-dir artifacts/maestro
```

## NativeScript Inspector

NativeScript apps can connect directly to the running server from JS and expose
Expand Down
49 changes: 49 additions & 0 deletions docs/api/rest.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,55 @@ The response always includes:

Returns the AX-style accessibility description of the topmost element at a screen point. `x` and `y` are in UIKit screen points and must be finite, non-negative numbers.

### `POST /api/simulators/{udid}/query`

Returns compact accessibility matches for a selector:

```json
{
"selector": {
"text": "Continue",
"id": "continue-button",
"elementType": "Button",
"enabled": true,
"regex": false
},
"source": "auto",
"maxDepth": 8,
"limit": 20
}
```

Selectors can match `text`, `id`, `label`, `value`, `elementType`, `index`, `enabled`, `checked`, `focused`, and `selected`. Set `regex: true` to use regular expression matching for string fields.

### `POST /api/simulators/{udid}/wait-for`

Polls until a selector appears. `assert` is an alias with the same payload shape:

```json
{
"selector": { "text": "Welcome", "regex": true },
"timeoutMs": 5000,
"pollMs": 100
}
```

Use `POST /api/simulators/{udid}/wait-for-not` or `/assert-not` for negative assertions.

### `POST /api/simulators/{udid}/scroll-until-visible`

Scrolls and polls until the selector appears:

```json
{
"selector": { "text": "Settings" },
"direction": "down",
"timeoutMs": 10000
}
```

`direction` accepts `up`, `down`, `left`, and `right`.

## Inspector proxy

### `POST /api/simulators/{udid}/inspector/request`
Expand Down
10 changes: 10 additions & 0 deletions docs/cli/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,16 @@ simdeck batch <udid> \

Batch input can come from `--step`, `--file`, or `--stdin`. Use `wait-for` or `assert` with selector flags (`--id`, `--label`, `--value`, `--element-type`) to wait for UI state instead of fixed delays. `sleep 500` waits 500 ms; suffix seconds explicitly with `s`, as in `sleep 0.5s`. It fails fast by default; pass `--continue-on-error` for best-effort execution.

## Maestro YAML

Run common Maestro flows through SimDeck's daemon-backed iOS Simulator API:

```sh
simdeck maestro test <udid> flow.yaml --artifacts-dir artifacts/maestro
```

The compatibility runner supports the core local commands: `launchApp`, `openLink`, `tapOn`, `inputText`, `eraseText`, `pressKey`, `assertVisible`, `assertNotVisible`, `scrollUntilVisible`, `swipe`, `takeScreenshot`, and `waitForAnimationToEnd`.

## Evidence

```sh
Expand Down
59 changes: 36 additions & 23 deletions docs/guide/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,55 @@ SimDeck supports two test layers: a small JS/TS client package for app tests, an
```ts
import { connect } from "simdeck/test";

const sim = await connect();
const sim = await connect({ udid: "<udid>" });

try {
const devices = await sim.list();
await sim.launch("<udid>", "com.example.App");
await sim.tap("<udid>", 0.5, 0.5);
await sim.waitFor("<udid>", { label: "Continue" });
const png = await sim.screenshot("<udid>");
await sim.launch("com.example.App");
await sim.tap(0.5, 0.5);
await sim.waitFor({ label: "Continue" });
const png = await sim.screenshot();
} finally {
sim.close();
}
```

`connect()` starts the daemon when needed, reuses it when healthy, and only stops daemons it started itself unless `keepDaemon` is set.
`connect()` starts the daemon when needed, reuses it when healthy, and only stops daemons it started itself unless `keepDaemon` is set. Pass `udid` to bind the session to one simulator; existing calls that pass `udid` as the first method argument still work, and `sim.device("<other-udid>")` returns a session bound to another simulator.

## Session API

The current session object exposes:

| Method | Purpose |
| -------------- | ----------------------------------------------------------------- |
| `list()` | Fetch simulator inventory from `GET /api/simulators`. |
| `launch()` | Launch an installed bundle ID. |
| `openUrl()` | Open a URL or deep link. |
| `tap()` | Tap normalized screen coordinates. |
| `key()` | Send one HID key code. |
| `button()` | Press a hardware button. |
| `tree()` | Fetch an accessibility hierarchy. |
| `query()` | Return compact matches for a selector. |
| `waitFor()` | Poll until a selector appears. |
| `assert()` | Assert a selector is present. |
| `batch()` | Run multiple REST actions through `/api/simulators/{udid}/batch`. |
| `screenshot()` | Return a PNG buffer. |
| `close()` | Stop the daemon if this session started it. |

Selectors can match `id`, `label`, `value`, or `type`. Query options accept `source`, `maxDepth`, and `includeHidden`.
| Method | Purpose |
| ---------------------- | ----------------------------------------------------------------- |
| `list()` | Fetch simulator inventory from `GET /api/simulators`. |
| `launch()` | Launch an installed bundle ID. |
| `openUrl()` | Open a URL or deep link. |
| `tap()` | Tap normalized screen coordinates. |
| `key()` | Send one HID key code. |
| `button()` | Press a hardware button. |
| `tree()` | Fetch an accessibility hierarchy. |
| `query()` | Return compact matches for a selector. |
| `waitFor()` | Poll until a selector appears. |
| `waitForNot()` | Poll until a selector disappears. |
| `assert()` | Assert a selector is present. |
| `assertNot()` | Assert a selector is absent. |
| `scrollUntilVisible()` | Scroll until a selector appears or the timeout expires. |
| `batch()` | Run multiple REST actions through `/api/simulators/{udid}/batch`. |
| `screenshot()` | Return a PNG buffer. |
| `close()` | Stop the daemon if this session started it. |

Selectors can match `text`, `id`, `label`, `value`, `type`, `index`, `enabled`, `checked`, `focused`, and `selected`. Set `regex: true` to treat string selector fields as regular expressions. Query options accept `source`, `maxDepth`, and `includeHidden`.

## Maestro-Compatible YAML

The CLI includes a compatibility runner for common Maestro YAML flows:

```sh
simdeck maestro test <udid> flow.yaml --artifacts-dir artifacts/maestro
```

Supported commands include `launchApp`, `openLink`, `tapOn`, `inputText`, `eraseText`, `pressKey`, `assertVisible`, `assertNotVisible`, `scrollUntilVisible`, `swipe`, `takeScreenshot`, and `waitForAnimationToEnd`. Unsupported Maestro commands fail clearly so the flow can be adjusted or the compatibility layer can be expanded.

## Repository Integration Suite

Expand Down
131 changes: 89 additions & 42 deletions packages/simdeck-test/dist/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,75 +1,122 @@
export type SimDeckLaunchOptions = {
cliPath?: string;
projectRoot?: string;
udid?: string;
keepDaemon?: boolean;
isolated?: boolean;
port?: number;
videoCodec?: "auto" | "hardware" | "software" | "h264-software";
};
export type QueryOptions = {
source?: "auto" | "nativescript" | "uikit" | "native-ax";
source?:
| "auto"
| "nativescript"
| "react-native"
| "swiftui"
| "uikit"
| "native-ax";
maxDepth?: number;
includeHidden?: boolean;
};
export type ElementSelector = {
text?: string;
id?: string;
label?: string;
value?: string;
type?: string;
index?: number;
enabled?: boolean;
checked?: boolean;
focused?: boolean;
selected?: boolean;
regex?: boolean;
};
export type TapOptions = QueryOptions & {
durationMs?: number;
waitTimeoutMs?: number;
pollMs?: number;
};
type DeviceMethod<TArgs extends unknown[], TResult> = {
(...args: TArgs): Promise<TResult>;
(udid: string, ...args: TArgs): Promise<TResult>;
};
type DeviceNoArgMethod<TResult> = {
(): Promise<TResult>;
(udid: string): Promise<TResult>;
};
export type SimDeckSession = {
endpoint: string;
pid: number;
projectRoot: string;
udid?: string;
device(udid: string): SimDeckSession;
list(): Promise<unknown>;
install(udid: string, appPath: string): Promise<void>;
uninstall(udid: string, bundleId: string): Promise<void>;
launch(udid: string, bundleId: string): Promise<void>;
openUrl(udid: string, url: string): Promise<void>;
tap(udid: string, x: number, y: number): Promise<void>;
tapElement(
udid: string,
selector: ElementSelector,
options?: TapOptions,
): Promise<void>;
touch(udid: string, x: number, y: number, phase: string): Promise<void>;
key(udid: string, keyCode: number, modifiers?: number): Promise<void>;
button(udid: string, button: string, durationMs?: number): Promise<void>;
pasteboardSet(udid: string, text: string): Promise<void>;
pasteboardGet(udid: string): Promise<string>;
chromeProfile(udid: string): Promise<unknown>;
tree(udid: string, options?: QueryOptions): Promise<unknown>;
query(
udid: string,
selector: ElementSelector,
options?: QueryOptions,
): Promise<unknown[]>;
assert(
udid: string,
selector: ElementSelector,
options?: QueryOptions,
): Promise<unknown>;
waitFor(
udid: string,
selector: ElementSelector,
options?: QueryOptions & {
timeoutMs?: number;
pollMs?: number;
},
): Promise<unknown>;
batch(
udid: string,
steps: unknown[],
continueOnError?: boolean,
): Promise<unknown>;
screenshot(udid: string): Promise<Buffer>;
install: DeviceMethod<[appPath: string], void>;
uninstall: DeviceMethod<[bundleId: string], void>;
launch: DeviceMethod<[bundleId: string], void>;
openUrl: DeviceMethod<[url: string], void>;
tap: DeviceMethod<[x: number, y: number], void>;
tapElement: DeviceMethod<
[selector: ElementSelector, options?: TapOptions],
void
>;
touch: DeviceMethod<[x: number, y: number, phase: string], void>;
key: DeviceMethod<[keyCode: number, modifiers?: number], void>;
button: DeviceMethod<[button: string, durationMs?: number], void>;
pasteboardSet: DeviceMethod<[text: string], void>;
pasteboardGet: DeviceNoArgMethod<string>;
chromeProfile: DeviceNoArgMethod<unknown>;
tree: DeviceMethod<[options?: QueryOptions], unknown>;
query: DeviceMethod<
[selector: ElementSelector, options?: QueryOptions],
unknown[]
>;
assert: DeviceMethod<
[selector: ElementSelector, options?: QueryOptions],
unknown
>;
assertNot: DeviceMethod<
[selector: ElementSelector, options?: QueryOptions],
unknown
>;
waitFor: DeviceMethod<
[
selector: ElementSelector,
options?: QueryOptions & {
timeoutMs?: number;
pollMs?: number;
},
],
unknown
>;
waitForNot: DeviceMethod<
[
selector: ElementSelector,
options?: QueryOptions & {
timeoutMs?: number;
pollMs?: number;
},
],
unknown
>;
scrollUntilVisible: DeviceMethod<
[
selector: ElementSelector,
options?: QueryOptions & {
timeoutMs?: number;
pollMs?: number;
direction?: "up" | "down" | "left" | "right";
durationMs?: number;
steps?: number;
},
],
unknown
>;
batch: DeviceMethod<[steps: unknown[], continueOnError?: boolean], unknown>;
screenshot: DeviceNoArgMethod<Buffer>;
close(): void;
};
export declare function connect(
options?: SimDeckLaunchOptions,
): Promise<SimDeckSession>;
export {};
Loading
Loading