Skip to content
Draft
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,12 @@ healthy, and only stops daemons it started itself. Pass `udid` to `connect()`
to make it the default for session methods; each method still accepts an
explicit UDID as the first argument when needed.

Run common Maestro YAML flows against the same daemon-backed 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
36 changes: 36 additions & 0 deletions docs/api/rest.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,10 @@ Touch, edge-touch, and multi-touch coordinates are normalized from `0.0` to `1.0
| `GET` | `/api/simulators/{udid}/accessibility-point?x=120&y=240` | Element at a point |
| `POST` | `/api/simulators/{udid}/query` | Query tree by selector |
| `POST` | `/api/simulators/{udid}/wait-for` | Wait until selector appears |
| `POST` | `/api/simulators/{udid}/wait-for-not` | Wait until selector disappears |
| `POST` | `/api/simulators/{udid}/assert` | Assert selector exists |
| `POST` | `/api/simulators/{udid}/assert-not` | Assert selector is absent |
| `POST` | `/api/simulators/{udid}/scroll-until-visible` | Scroll until selector appears |
| `POST` | `/api/simulators/{udid}/batch` | Run multiple control steps |
| `POST` | `/api/simulators/{udid}/inspector/request` | Call an in-app inspector method |

Expand All @@ -219,6 +222,39 @@ Point query parameters:

Every tree response reports the `source` used and may include a `fallbackReason`.

Selector endpoints accept compact accessibility selectors:

```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}/query` returns compact matches. `wait-for` and `assert` use the same body shape for positive checks. `wait-for-not` and `assert-not` perform negative checks.

`POST /api/simulators/{udid}/scroll-until-visible` scrolls and polls until a selector appears:

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

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

## DevTools And WebKit

| Method | Path | Purpose |
Expand Down
10 changes: 10 additions & 0 deletions docs/cli/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,16 @@ simdeck batch <udid> \

Use `wait-for` or `assert` steps instead of fixed sleeps when possible.

## 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
16 changes: 14 additions & 2 deletions docs/guide/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ try {
}
```

`connect()` starts the project daemon if needed, reuses a healthy daemon, and only stops daemons it started itself. Pass `udid` to `connect()` to make it the default for session methods; methods still accept an explicit UDID as their first argument.
`connect()` starts the project daemon if needed, reuses a healthy daemon, and only stops daemons it started itself. Pass `udid` to `connect()` to make it the default for session methods; methods still accept an explicit UDID as their first argument. Use `sim.device("<other-udid>")` to create a session bound to another simulator.

## Useful Test Methods

Expand All @@ -39,10 +39,22 @@ try {
| `typeText()`, `key()`, `keySequence()` | Text and keyboard input |
| `button()`, `home()`, `appSwitcher()` | System controls |
| `tree()`, `query()`, `waitFor()`, `assert()` | UI state checks |
| `waitForNot()`, `assertNot()` | Negative UI state checks |
| `scrollUntilVisible()` | Scroll until a selector exists |
| `screenshot()`, `record()`, `logs()` | Evidence capture |
| `batch()` | Multi-step actions |

Selectors can match `id`, `label`, `value`, or `type`.
Selectors can match `text`, `id`, `label`, `value`, `type`, `index`, `enabled`, `checked`, `focused`, or `selected`. Set `regex: true` to treat string selector fields as regular expressions.

## 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 Tests

Expand Down
255 changes: 135 additions & 120 deletions packages/simdeck-test/dist/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,147 +1,162 @@
export type SimDeckLaunchOptions = {
cliPath?: string;
projectRoot?: string;
keepDaemon?: boolean;
isolated?: boolean;
port?: number;
videoCodec?: "auto" | "hardware" | "software" | "h264-software";
udid?: string;
cliPath?: string;
projectRoot?: string;
keepDaemon?: boolean;
isolated?: boolean;
port?: number;
videoCodec?: "auto" | "hardware" | "software" | "h264-software";
udid?: string;
};
export type QueryOptions = {
source?:
| "auto"
| "nativescript"
| "react-native"
| "flutter"
| "uikit"
| "native-ax"
| "android-uiautomator";
maxDepth?: number;
includeHidden?: boolean;
source?: "auto" | "nativescript" | "react-native" | "flutter" | "swiftui" | "uikit" | "native-ax" | "android-uiautomator";
maxDepth?: number;
includeHidden?: boolean;
};
export type ElementSelector = {
id?: string;
label?: string;
value?: string;
type?: string;
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;
durationMs?: number;
waitTimeoutMs?: number;
pollMs?: number;
};
export type SwipeOptions = {
durationMs?: number;
steps?: number;
durationMs?: number;
steps?: number;
};
export type GestureOptions = SwipeOptions & {
delta?: number;
delta?: number;
};
export type TypeTextOptions = {
delayMs?: number;
delayMs?: number;
};
export type KeySequenceOptions = {
delayMs?: number;
delayMs?: number;
};
export type LogsOptions = {
backfill?: boolean;
seconds?: number;
limit?: number;
levels?: string[];
processes?: string[];
q?: string;
backfill?: boolean;
seconds?: number;
limit?: number;
levels?: string[];
processes?: string[];
q?: string;
};
export type ScreenshotOptions = {
bezel?: boolean;
withBezel?: boolean;
bezel?: boolean;
withBezel?: boolean;
};
export type ScreenRecordingOptions = {
seconds?: number;
seconds?: number;
};
type DeviceMethod<TArgs extends unknown[], TResult> = {
(udid: string, ...args: TArgs): TResult;
(...args: TArgs): TResult;
(udid: string, ...args: TArgs): TResult;
(...args: TArgs): TResult;
};
export type SimDeckSession = {
endpoint: string;
pid: number;
projectRoot: string;
list(): Promise<unknown>;
boot: DeviceMethod<[], Promise<unknown>>;
shutdown: DeviceMethod<[], Promise<unknown>>;
erase: DeviceMethod<[], Promise<unknown>>;
install: DeviceMethod<[appPath: string], Promise<void>>;
uninstall: DeviceMethod<[bundleId: string], Promise<void>>;
launch: DeviceMethod<[bundleId: string], Promise<void>>;
openUrl: DeviceMethod<[url: string], Promise<void>>;
tap: DeviceMethod<[x: number, y: number], Promise<void>>;
tapElement: DeviceMethod<
[selector: ElementSelector, options?: TapOptions],
Promise<void>
>;
touch: DeviceMethod<[x: number, y: number, phase: string], Promise<void>>;
swipe: DeviceMethod<
[
startX: number,
startY: number,
endX: number,
endY: number,
options?: SwipeOptions,
],
Promise<unknown>
>;
gesture: DeviceMethod<
[preset: string, options?: GestureOptions],
Promise<unknown>
>;
typeText: DeviceMethod<
[text: string, options?: TypeTextOptions],
Promise<unknown>
>;
key: DeviceMethod<[keyCode: number, modifiers?: number], Promise<void>>;
keySequence: DeviceMethod<
[keyCodes: number[], options?: KeySequenceOptions],
Promise<void>
>;
button: DeviceMethod<[button: string, durationMs?: number], Promise<void>>;
home: DeviceMethod<[], Promise<void>>;
dismissKeyboard: DeviceMethod<[], Promise<void>>;
appSwitcher: DeviceMethod<[], Promise<void>>;
rotateLeft: DeviceMethod<[], Promise<void>>;
rotateRight: DeviceMethod<[], Promise<void>>;
toggleAppearance: DeviceMethod<[], Promise<void>>;
pasteboardSet: DeviceMethod<[text: string], Promise<void>>;
pasteboardGet: DeviceMethod<[], Promise<string>>;
chromeProfile: DeviceMethod<[], Promise<unknown>>;
logs: DeviceMethod<[options?: LogsOptions], Promise<unknown[]>>;
tree: DeviceMethod<[options?: QueryOptions], Promise<unknown>>;
query: DeviceMethod<
[selector: ElementSelector, options?: QueryOptions],
Promise<unknown[]>
>;
assert: DeviceMethod<
[selector: ElementSelector, options?: QueryOptions],
Promise<unknown>
>;
waitFor: DeviceMethod<
[
selector: ElementSelector,
options?: QueryOptions & {
timeoutMs?: number;
pollMs?: number;
},
],
Promise<unknown>
>;
batch: DeviceMethod<
[steps: unknown[], continueOnError?: boolean],
Promise<unknown>
>;
screenshot: DeviceMethod<[options?: ScreenshotOptions], Promise<Buffer>>;
record: DeviceMethod<[options?: ScreenRecordingOptions], Promise<Buffer>>;
close(): void;
endpoint: string;
pid: number;
projectRoot: string;
udid?: string;
device(udid: string): SimDeckSession;
list(): Promise<unknown>;
boot: DeviceMethod<[], Promise<unknown>>;
shutdown: DeviceMethod<[], Promise<unknown>>;
erase: DeviceMethod<[], Promise<unknown>>;
install: DeviceMethod<[appPath: string], Promise<void>>;
uninstall: DeviceMethod<[bundleId: string], Promise<void>>;
launch: DeviceMethod<[bundleId: string], Promise<void>>;
openUrl: DeviceMethod<[url: string], Promise<void>>;
tap: DeviceMethod<[x: number, y: number], Promise<void>>;
tapElement: DeviceMethod<[
selector: ElementSelector,
options?: TapOptions
], Promise<void>>;
touch: DeviceMethod<[x: number, y: number, phase: string], Promise<void>>;
swipe: DeviceMethod<[
startX: number,
startY: number,
endX: number,
endY: number,
options?: SwipeOptions
], Promise<unknown>>;
gesture: DeviceMethod<[
preset: string,
options?: GestureOptions
], Promise<unknown>>;
typeText: DeviceMethod<[
text: string,
options?: TypeTextOptions
], Promise<unknown>>;
key: DeviceMethod<[keyCode: number, modifiers?: number], Promise<void>>;
keySequence: DeviceMethod<[
keyCodes: number[],
options?: KeySequenceOptions
], Promise<void>>;
button: DeviceMethod<[button: string, durationMs?: number], Promise<void>>;
home: DeviceMethod<[], Promise<void>>;
dismissKeyboard: DeviceMethod<[], Promise<void>>;
appSwitcher: DeviceMethod<[], Promise<void>>;
rotateLeft: DeviceMethod<[], Promise<void>>;
rotateRight: DeviceMethod<[], Promise<void>>;
toggleAppearance: DeviceMethod<[], Promise<void>>;
pasteboardSet: DeviceMethod<[text: string], Promise<void>>;
pasteboardGet: DeviceMethod<[], Promise<string>>;
chromeProfile: DeviceMethod<[], Promise<unknown>>;
logs: DeviceMethod<[options?: LogsOptions], Promise<unknown[]>>;
tree: DeviceMethod<[options?: QueryOptions], Promise<unknown>>;
query: DeviceMethod<[
selector: ElementSelector,
options?: QueryOptions
], Promise<unknown[]>>;
assert: DeviceMethod<[
selector: ElementSelector,
options?: QueryOptions
], Promise<unknown>>;
assertNot: DeviceMethod<[
selector: ElementSelector,
options?: QueryOptions
], Promise<unknown>>;
waitFor: DeviceMethod<[
selector: ElementSelector,
options?: QueryOptions & {
timeoutMs?: number;
pollMs?: number;
}
], Promise<unknown>>;
waitForNot: DeviceMethod<[
selector: ElementSelector,
options?: QueryOptions & {
timeoutMs?: number;
pollMs?: number;
}
], Promise<unknown>>;
scrollUntilVisible: DeviceMethod<[
selector: ElementSelector,
options?: QueryOptions & {
timeoutMs?: number;
pollMs?: number;
direction?: "up" | "down" | "left" | "right";
durationMs?: number;
steps?: number;
}
], Promise<unknown>>;
batch: DeviceMethod<[
steps: unknown[],
continueOnError?: boolean
], Promise<unknown>>;
screenshot: DeviceMethod<[options?: ScreenshotOptions], Promise<Buffer>>;
record: DeviceMethod<[options?: ScreenRecordingOptions], Promise<Buffer>>;
close(): void;
};
export declare function connect(
options?: SimDeckLaunchOptions,
): Promise<SimDeckSession>;
export declare function connect(options?: SimDeckLaunchOptions): Promise<SimDeckSession>;
export {};
Loading
Loading