zeno-mobile-runner 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/FEATURES.md +1 -1
- package/README.md +8 -1
- package/clients/kotlin/README.md +1 -1
- package/clients/kotlin/build.gradle.kts +1 -1
- package/clients/python/pyproject.toml +1 -1
- package/clients/rust/Cargo.lock +1 -1
- package/clients/rust/Cargo.toml +1 -1
- package/clients/typescript/package.json +1 -1
- package/docs/agent-discovery.md +83 -0
- package/docs/ai-agents.md +11 -0
- package/docs/expo-smoke.md +79 -0
- package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
- package/docs/protocol.md +4 -4
- package/npm/build-zmr.mjs +1 -1
- package/npm/postinstall.mjs +28 -2
- package/npm/verify-publish.mjs +36 -0
- package/package.json +2 -1
- package/prebuilds/darwin-arm64/zmr +0 -0
- package/prebuilds/darwin-x64/zmr +0 -0
- package/prebuilds/linux-arm64/zmr +0 -0
- package/prebuilds/linux-x64/zmr +0 -0
- package/scripts/install-ios-shim.sh +79 -14
- package/shims/ios/ZMRShim.swift +3 -0
- package/shims/ios/ZMRShimUITestCase.swift +41 -11
- package/src/ios.zig +11 -4
- package/src/ios_lifecycle.zig +36 -0
- package/src/ios_shim.zig +42 -0
- package/src/version.zig +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,24 @@ All notable changes to Zeno Mobile Runner are tracked here.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.1.3 (2026-06-03)
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Hardened the iOS XCTest shim for clean Expo/RN prebuilds by giving cold
|
|
12
|
+
`build-for-testing` runs a 90-minute default timeout with explicit progress
|
|
13
|
+
logging.
|
|
14
|
+
- Made iOS simulator `app.stop` idempotent when `simctl terminate` reports the
|
|
15
|
+
app is already stopped.
|
|
16
|
+
- Improved selector actions so native `selector.not_found` and
|
|
17
|
+
`selector.not_hittable` responses return a typed unavailable result instead
|
|
18
|
+
of failing the command parser.
|
|
19
|
+
- Reused a resolved booted simulator UDID across `build-for-testing` and
|
|
20
|
+
`test-without-building`, and cleared stale shim PID/server/destination state
|
|
21
|
+
when reinstalling the app-local shim.
|
|
22
|
+
- Avoided stale XCTest snapshot elements and replaced broad selector scans with
|
|
23
|
+
native predicate-based fallback queries.
|
|
24
|
+
|
|
7
25
|
## 0.1.2 (2026-05-28)
|
|
8
26
|
|
|
9
27
|
### Fixed
|
package/FEATURES.md
CHANGED
|
@@ -99,7 +99,7 @@ state, and writes deterministic traces. It does not embed an LLM.
|
|
|
99
99
|
|
|
100
100
|
## Current Limitations
|
|
101
101
|
|
|
102
|
-
- Current release status is `0.1.
|
|
102
|
+
- Current release status is `0.1.3`, a public developer preview rather than
|
|
103
103
|
a production-stable `1.0.0`.
|
|
104
104
|
- Physical iOS log capture is still simulator-first. Physical iOS screenshots
|
|
105
105
|
are available when the XCTest/XCUIAutomation shim is configured.
|
package/README.md
CHANGED
|
@@ -121,6 +121,11 @@ zmr mcp --config .zmr/config.json --trace-dir traces/zmr-agent
|
|
|
121
121
|
The MCP server exposes mobile-specific tools such as `semantic_snapshot`, `tap`,
|
|
122
122
|
`type`, `wait_visible`, `trace_events`, and `trace_export`.
|
|
123
123
|
|
|
124
|
+
For agent-led discovery and test authoring, see
|
|
125
|
+
[docs/agent-discovery.md](docs/agent-discovery.md). ZMR supports that loop
|
|
126
|
+
through MCP and JSON-RPC today; a built-in autonomous crawler is not shipped in
|
|
127
|
+
this preview.
|
|
128
|
+
|
|
124
129
|
## Optional Protocol Clients
|
|
125
130
|
|
|
126
131
|
Clients are thin wrappers around `zmr serve --transport stdio`. They do not
|
|
@@ -153,7 +158,7 @@ and [docs/client-installation.md](docs/client-installation.md).
|
|
|
153
158
|
| iOS physical device | Supported, validate locally | `devicectl` lifecycle plus app-local XCTest/XCUIAutomation shim; run pilots on your own app/device before relying on it in CI |
|
|
154
159
|
| Cloud device farms | Not included | ZMR is focused on local and self-managed device targets in this preview |
|
|
155
160
|
|
|
156
|
-
Current release: `0.1.
|
|
161
|
+
Current release: `0.1.3` developer preview. Protocol version:
|
|
157
162
|
`2026-04-28`.
|
|
158
163
|
|
|
159
164
|
## Documentation
|
|
@@ -161,8 +166,10 @@ Current release: `0.1.2` developer preview. Protocol version:
|
|
|
161
166
|
- [FEATURES.md](FEATURES.md): complete feature list and limitations
|
|
162
167
|
- [docs/install.md](docs/install.md): source, npm, Homebrew, and app setup
|
|
163
168
|
- [docs/frameworks.md](docs/frameworks.md): React Native, Expo, Flutter, and native app guidance
|
|
169
|
+
- [docs/expo-smoke.md](docs/expo-smoke.md): reproducible Expo and iOS smoke test
|
|
164
170
|
- [docs/app-integration.md](docs/app-integration.md): app-side Android/iOS shims
|
|
165
171
|
- [docs/scenario-authoring.md](docs/scenario-authoring.md): selectors, waits, and scenario design
|
|
172
|
+
- [docs/agent-discovery.md](docs/agent-discovery.md): agent-led discovery and scenario authoring loop
|
|
166
173
|
- [docs/protocol.md](docs/protocol.md): JSON-RPC methods and schemas
|
|
167
174
|
- [docs/ai-agents.md](docs/ai-agents.md): JSON-RPC and MCP agent workflows
|
|
168
175
|
- [docs/clients.md](docs/clients.md): language client guide
|
package/clients/kotlin/README.md
CHANGED
|
@@ -27,7 +27,7 @@ gradle -p clients/kotlin runFakeSession \
|
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
```kotlin
|
|
30
|
-
implementation(files("path/to/zeno-mobile-runner/clients/kotlin/build/libs/zmr-client-0.1.
|
|
30
|
+
implementation(files("path/to/zeno-mobile-runner/clients/kotlin/build/libs/zmr-client-0.1.3.jar"))
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
The Kotlin client is host-side. It is useful for Android teams that want test
|
package/clients/rust/Cargo.lock
CHANGED
package/clients/rust/Cargo.toml
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Agent Discovery
|
|
2
|
+
|
|
3
|
+
ZMR supports agent-led discovery today through its JSON-RPC and MCP interfaces.
|
|
4
|
+
An external agent can observe the app, choose typed actions, inspect trace
|
|
5
|
+
events, and write a repeatable scenario file as it learns a flow.
|
|
6
|
+
|
|
7
|
+
ZMR does not include a built-in autonomous crawler or test writer in this
|
|
8
|
+
developer preview. Keep the planning loop in the agent, and keep ZMR as the
|
|
9
|
+
deterministic mobile control plane.
|
|
10
|
+
|
|
11
|
+
## Recommended Loop
|
|
12
|
+
|
|
13
|
+
1. Validate local setup:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
zmr doctor --json --config .zmr/config.json
|
|
17
|
+
zmr validate --json .zmr/ios-smoke.json
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
2. Start a live session:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
zmr serve --transport stdio --config .zmr/config.json --trace-dir traces/zmr-agent
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Agents that speak MCP can use:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
zmr mcp --config .zmr/config.json --trace-dir traces/zmr-agent
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
3. Call `runner.capabilities`, then `session.create`.
|
|
33
|
+
4. Call `observe.semanticSnapshot` before choosing an action.
|
|
34
|
+
5. Choose one typed action, such as `ui.tap`, `ui.type`, `app.openLink`, or
|
|
35
|
+
`wait.until`.
|
|
36
|
+
6. Observe again and inspect `trace.events`.
|
|
37
|
+
7. Write successful steps into a candidate scenario, for example
|
|
38
|
+
`.zmr/discovered/login-smoke.json`.
|
|
39
|
+
8. Validate the candidate scenario:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
zmr validate --json .zmr/discovered/login-smoke.json
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
9. Re-run it deterministically:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
zmr run .zmr/discovered/login-smoke.json \
|
|
49
|
+
--platform ios \
|
|
50
|
+
--device booted \
|
|
51
|
+
--trace-dir traces/zmr-login-smoke \
|
|
52
|
+
--json
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
10. Export a redacted bundle before sharing artifacts:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
zmr export traces/zmr-login-smoke \
|
|
59
|
+
--out traces/zmr-login-smoke-redacted.zmrtrace \
|
|
60
|
+
--redact
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Guardrails
|
|
64
|
+
|
|
65
|
+
- Set a step budget and a time budget before discovery starts.
|
|
66
|
+
- Restrict discovery to known app ids, deep-link schemes, and test accounts.
|
|
67
|
+
- Do not ask an agent to discover credentials or secrets.
|
|
68
|
+
- Prefer accessibility identifiers, resource ids, stable labels, and exact text
|
|
69
|
+
over coordinates.
|
|
70
|
+
- Require human review before committing generated tests.
|
|
71
|
+
- Redact traces before sharing them outside the local team.
|
|
72
|
+
|
|
73
|
+
## Future Shape
|
|
74
|
+
|
|
75
|
+
A future command could wrap this loop:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
zmr explore --goal "find the login flow" --out .zmr/discovered/login-smoke.json
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
That command is not shipped today. The safer product direction is to make
|
|
82
|
+
scenario discovery explicit, reviewable, and trace-backed before it becomes a
|
|
83
|
+
one-command workflow.
|
package/docs/ai-agents.md
CHANGED
|
@@ -68,6 +68,17 @@ The MCP server exposes mobile-specific tools:
|
|
|
68
68
|
Prefer `semantic_snapshot` for action planning. It avoids forcing an agent to
|
|
69
69
|
infer intent from platform-specific Android/UI Automator or XCTest class names.
|
|
70
70
|
|
|
71
|
+
## Agent-Led Discovery
|
|
72
|
+
|
|
73
|
+
Agents can use ZMR to discover flows and draft scenarios by looping over
|
|
74
|
+
`observe.semanticSnapshot`, one typed action, trace events, and scenario
|
|
75
|
+
validation. See [Agent Discovery](agent-discovery.md) for the recommended
|
|
76
|
+
reviewable loop.
|
|
77
|
+
|
|
78
|
+
ZMR does not ship a built-in autonomous crawler or test writer in this developer
|
|
79
|
+
preview. Keep autonomous planning outside the runner, then commit only reviewed
|
|
80
|
+
scenario JSON.
|
|
81
|
+
|
|
71
82
|
## Scenario File Workflow
|
|
72
83
|
|
|
73
84
|
For repeatable tests, generate or edit `.zmr/*.json` scenarios:
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Expo Smoke Test
|
|
2
|
+
|
|
3
|
+
This is the quickest public smoke path for an Expo app. It proves that the npm
|
|
4
|
+
package installs, the wizard scaffolds a scenario, ZMR can launch an iOS app,
|
|
5
|
+
and the runner can produce screenshots, traces, HTML reports, and redacted trace
|
|
6
|
+
bundles.
|
|
7
|
+
|
|
8
|
+
The flow below was verified locally with `zeno-mobile-runner@0.1.3` on an iOS
|
|
9
|
+
simulator.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx create-expo-app@latest /tmp/zmr-expo-smoke --template blank --yes
|
|
13
|
+
cd /tmp/zmr-expo-smoke
|
|
14
|
+
npm install --save-dev zeno-mobile-runner
|
|
15
|
+
|
|
16
|
+
npx zmr-wizard --yes --dir . \
|
|
17
|
+
--app-id com.example.zenoexposmoke \
|
|
18
|
+
--ios \
|
|
19
|
+
--package-json
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Boot a simulator, then build and launch the app:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
xcrun simctl boot <simulator-udid>
|
|
26
|
+
npx expo run:ios --device <simulator-name>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Run the generated ZMR scenario:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx zmr run .zmr/ios-smoke.json \
|
|
33
|
+
--platform ios \
|
|
34
|
+
--device booted \
|
|
35
|
+
--trace-dir traces/zmr-ios \
|
|
36
|
+
--json
|
|
37
|
+
|
|
38
|
+
npx zmr report traces/zmr-ios --out traces/zmr-ios/report.html
|
|
39
|
+
npx zmr export traces/zmr-ios --out traces/zmr-ios-redacted.zmrtrace --redact
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Expected result shape:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"ok": true,
|
|
47
|
+
"status": "passed",
|
|
48
|
+
"scenario": "iOS smoke",
|
|
49
|
+
"appId": "com.example.zenoexposmoke",
|
|
50
|
+
"traceDir": "traces/zmr-ios",
|
|
51
|
+
"eventCount": 8,
|
|
52
|
+
"snapshotCount": 2,
|
|
53
|
+
"partialFailureCount": 0
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This smoke validates the platform-level loop: app launch, health check,
|
|
58
|
+
screenshot capture, trace collection, report generation, and redacted export.
|
|
59
|
+
For selector-grade React Native or Expo assertions on iOS, add the XCTest shim
|
|
60
|
+
described in [app integration](app-integration.md).
|
|
61
|
+
|
|
62
|
+
Android follows the same pattern with a connected emulator or device:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx zmr-wizard --yes --dir . \
|
|
66
|
+
--app-id com.example.zenoexposmoke \
|
|
67
|
+
--android \
|
|
68
|
+
--package-json
|
|
69
|
+
|
|
70
|
+
npx zmr run .zmr/android-smoke.json \
|
|
71
|
+
--platform android \
|
|
72
|
+
--device emulator-5554 \
|
|
73
|
+
--trace-dir traces/zmr-android \
|
|
74
|
+
--json
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Do not commit traces, screenshots, bundle identifiers, private app names, or
|
|
78
|
+
credentials from a real product app. Use `zmr export --redact`, and add
|
|
79
|
+
`--omit-screenshots` when visual artifacts may contain sensitive data.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
{"jsonrpc":"2.0","id":1,"result":{"name":"zmr","version":"0.1.
|
|
1
|
+
{"jsonrpc":"2.0","id":1,"result":{"name":"zmr","version":"0.1.3","protocolVersion":"2026-04-28","protocol":{"version":"2026-04-28","minimumCompatibleVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"},"platforms":["android","ios"],"platformSupport":{"android":{"status":"supported","deviceTypes":["emulator","physical"],"automation":["adb","uiautomator","android-shim"]},"ios":{"status":"supported","deviceTypes":["simulator","physical"],"automation":["simctl","devicectl","xctest-shim"],"physicalDevices":true}},"iosPreview":false,"transports":["stdio","tcp"],"methods":["runner.capabilities","device.list","session.create","session.close","app.install","app.launch","app.stop","app.openLink","app.clearState","observe.snapshot","observe.semanticSnapshot","ui.tap","ui.type","ui.eraseText","ui.hideKeyboard","ui.swipe","ui.pressBack","ui.scrollUntilVisible","wait.until","wait.any","wait.gone","assert.visible","assert.notVisible","assert.healthy","trace.events","trace.export"]}}
|
|
2
2
|
{"jsonrpc":"2.0","id":2,"result":[{"serial":"fake-device-1","state":"device","ready":true}]}
|
|
3
3
|
{"jsonrpc":"2.0","id":3,"result":{"sessionId":"default"}}
|
|
4
4
|
{"jsonrpc":"2.0","id":4,"result":true}
|
package/docs/protocol.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
ZMR exposes newline-delimited JSON-RPC 2.0 over stdio or localhost TCP in v1. Each request is one JSON object followed by `\n`. Each response is one JSON object followed by `\n`.
|
|
4
4
|
|
|
5
|
-
Current runner version: `0.1.
|
|
5
|
+
Current runner version: `0.1.3`.
|
|
6
6
|
|
|
7
7
|
Current protocol version: `2026-04-28`.
|
|
8
8
|
|
|
@@ -133,7 +133,7 @@ installers, setup scripts, and generated clients. The response is covered by
|
|
|
133
133
|
`schemas/version-output.schema.json`:
|
|
134
134
|
|
|
135
135
|
```json
|
|
136
|
-
{"name":"zmr","version":"0.1.
|
|
136
|
+
{"name":"zmr","version":"0.1.3","protocolVersion":"2026-04-28","minimumCompatibleProtocolVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"}
|
|
137
137
|
```
|
|
138
138
|
|
|
139
139
|
## Capabilities Output Contract
|
|
@@ -145,7 +145,7 @@ and method inventory for JSON-RPC clients. The result object is covered by
|
|
|
145
145
|
iOS simulator, or physical iOS workflows are available.
|
|
146
146
|
|
|
147
147
|
```json
|
|
148
|
-
{"name":"zmr","version":"0.1.
|
|
148
|
+
{"name":"zmr","version":"0.1.3","protocolVersion":"2026-04-28","protocol":{"version":"2026-04-28","minimumCompatibleVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"},"platforms":["android","ios"],"platformSupport":{"android":{"status":"supported","deviceTypes":["emulator","physical"],"automation":["adb","uiautomator","android-shim"]},"ios":{"status":"supported","deviceTypes":["simulator","physical"],"automation":["simctl","devicectl","xctest-shim"],"physicalDevices":true}},"iosPreview":false,"transports":["stdio","tcp"],"methods":["runner.capabilities","device.list","observe.snapshot","observe.semanticSnapshot"]}
|
|
149
149
|
```
|
|
150
150
|
|
|
151
151
|
## Doctor Output Contract
|
|
@@ -347,7 +347,7 @@ Request:
|
|
|
347
347
|
Response:
|
|
348
348
|
|
|
349
349
|
```json
|
|
350
|
-
{"jsonrpc":"2.0","id":1,"result":{"name":"zmr","version":"0.1.
|
|
350
|
+
{"jsonrpc":"2.0","id":1,"result":{"name":"zmr","version":"0.1.3","protocolVersion":"2026-04-28","protocol":{"version":"2026-04-28","minimumCompatibleVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"},"platforms":["android","ios"],"platformSupport":{"android":{"status":"supported","deviceTypes":["emulator","physical"],"automation":["adb","uiautomator","android-shim"]},"ios":{"status":"supported","deviceTypes":["simulator","physical"],"automation":["simctl","devicectl","xctest-shim"],"physicalDevices":true}},"iosPreview":false,"transports":["stdio","tcp"],"methods":["runner.capabilities","device.list","session.create","session.close","app.install","app.launch","app.stop","app.openLink","app.clearState","observe.snapshot","observe.semanticSnapshot","ui.tap","ui.type","ui.eraseText","ui.hideKeyboard","ui.swipe","ui.pressBack","ui.scrollUntilVisible","wait.until","wait.any","wait.gone","assert.visible","assert.notVisible","assert.healthy","trace.events","trace.export"]}}
|
|
351
351
|
```
|
|
352
352
|
|
|
353
353
|
### `trace.events`
|
package/npm/build-zmr.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
7
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
-
const out = path.join(root, "zig-out", "bin", process.platform === "win32" ? "zmr.exe" : "zmr");
|
|
8
|
+
const out = process.env.ZMR_BUILD_OUT || path.join(root, "zig-out", "bin", process.platform === "win32" ? "zmr.exe" : "zmr");
|
|
9
9
|
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
10
10
|
|
|
11
11
|
const args = ["build-exe", "src/main.zig", "-O", "ReleaseSafe", `-femit-bin=${out}`];
|
package/npm/postinstall.mjs
CHANGED
|
@@ -1,21 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
3
4
|
import { resolveBinary } from "./index.mjs";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
7
|
+
const binary = resolveBinary();
|
|
8
|
+
|
|
9
|
+
if (binary && binaryMatchesPackageVersion(binary, pkg.version)) process.exit(0);
|
|
10
|
+
if (binary && process.env.ZMR_BIN) {
|
|
11
|
+
console.warn(`zeno-mobile-runner: ZMR_BIN points to a zmr binary that does not report package version ${pkg.version}: ${binary}`);
|
|
12
|
+
process.exit(0);
|
|
13
|
+
}
|
|
6
14
|
if (process.env.ZMR_SKIP_POSTINSTALL_BUILD === "1") process.exit(0);
|
|
7
15
|
|
|
8
16
|
const hasZig = spawnSync("zig", ["version"], { stdio: "ignore" }).status === 0;
|
|
9
17
|
if (!hasZig) {
|
|
10
|
-
|
|
18
|
+
if (binary) {
|
|
19
|
+
console.warn(`zeno-mobile-runner: prebuilt zmr binary does not report package version ${pkg.version}: ${binary}`);
|
|
20
|
+
} else {
|
|
21
|
+
console.warn("zeno-mobile-runner: no prebuilt zmr binary found and Zig is not installed.");
|
|
22
|
+
}
|
|
11
23
|
console.warn("zeno-mobile-runner: install a release package with prebuilds, install Zig and run `npm run build:zmr`, or set ZMR_BIN.");
|
|
12
24
|
process.exit(0);
|
|
13
25
|
}
|
|
14
26
|
|
|
15
27
|
const result = spawnSync(process.execPath, [new URL("./build-zmr.mjs", import.meta.url).pathname], {
|
|
16
28
|
stdio: "inherit",
|
|
29
|
+
env: {
|
|
30
|
+
...process.env,
|
|
31
|
+
...(binary ? { ZMR_BUILD_OUT: binary } : {}),
|
|
32
|
+
},
|
|
17
33
|
});
|
|
18
34
|
|
|
19
35
|
if (result.status !== 0) {
|
|
20
36
|
console.warn("zeno-mobile-runner: postinstall build failed; set ZMR_BIN or run `npm run build:zmr` after fixing Zig.");
|
|
21
37
|
}
|
|
38
|
+
|
|
39
|
+
function binaryMatchesPackageVersion(binaryPath, expectedVersion) {
|
|
40
|
+
const result = spawnSync(binaryPath, ["version", "--json"], { encoding: "utf8" });
|
|
41
|
+
if (result.status !== 0) return false;
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(result.stdout).version === expectedVersion;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
9
|
+
const exe = process.platform === "win32" ? "zmr.exe" : "zmr";
|
|
10
|
+
const hostPrebuild = path.join(root, "prebuilds", `${process.platform}-${process.arch}`, exe);
|
|
11
|
+
|
|
12
|
+
if (!fs.existsSync(hostPrebuild)) {
|
|
13
|
+
fail(`missing host prebuild for ${process.platform}-${process.arch}: ${hostPrebuild}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const version = spawnSync(hostPrebuild, ["version", "--json"], { encoding: "utf8" });
|
|
17
|
+
if (version.status !== 0) {
|
|
18
|
+
fail(`host prebuild is not runnable: ${hostPrebuild}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let actual;
|
|
22
|
+
try {
|
|
23
|
+
actual = JSON.parse(version.stdout).version;
|
|
24
|
+
} catch {
|
|
25
|
+
fail(`host prebuild did not return valid version JSON: ${hostPrebuild}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (actual !== pkg.version) {
|
|
29
|
+
fail(`host prebuild reports ${actual}, expected ${pkg.version}: ${hostPrebuild}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function fail(message) {
|
|
33
|
+
console.error(`zeno-mobile-runner: ${message}`);
|
|
34
|
+
console.error("zeno-mobile-runner: run `npm run pack:npm` before publishing so prebuilds match package.json.");
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zeno-mobile-runner",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Agent-native mobile app test runner for React Native, Expo, Flutter, and native Android/iOS.",
|
|
5
5
|
"main": "npm/index.mjs",
|
|
6
6
|
"repository": {
|
|
@@ -92,6 +92,7 @@
|
|
|
92
92
|
],
|
|
93
93
|
"scripts": {
|
|
94
94
|
"postinstall": "node npm/postinstall.mjs",
|
|
95
|
+
"prepublishOnly": "node npm/verify-publish.mjs",
|
|
95
96
|
"build:zmr": "node npm/build-zmr.mjs",
|
|
96
97
|
"pack:npm": "bash scripts/build-npm-package.sh",
|
|
97
98
|
"zmr:demo": "node npm/zmr.mjs validate examples/demo-fake.json",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -167,6 +167,10 @@ mkdir -p "$APP_ROOT"
|
|
|
167
167
|
APP_ROOT="$(cd "$APP_ROOT" && pwd)"
|
|
168
168
|
mkdir -p "$APP_ROOT/.zmr" "$APP_ROOT/.zmr/shims/ios"
|
|
169
169
|
rm -f "$APP_ROOT/.zmr/ios-shim-state/build-for-testing.ready"
|
|
170
|
+
rm -f "$APP_ROOT/.zmr/ios-shim-state/destination.id"
|
|
171
|
+
rm -f "$APP_ROOT/.zmr/ios-shim-state/xcodebuild.pid"
|
|
172
|
+
rm -f "$APP_ROOT/.zmr/ios-shim-state/xcodebuild.log"
|
|
173
|
+
rm -rf "$APP_ROOT/.zmr/ios-shim-state/server"
|
|
170
174
|
cp "$ROOT/shims/ios/ZMRShim.swift" "$APP_ROOT/.zmr/shims/ios/ZMRShim.swift"
|
|
171
175
|
cp "$ROOT/shims/ios/ZMRShimUITestCase.swift" "$APP_ROOT/.zmr/ZMRShimUITestCase.swift"
|
|
172
176
|
cp "$ROOT/scripts/ensure-ios-shim-target.rb" "$APP_ROOT/.zmr/ensure-ios-shim-target.rb"
|
|
@@ -216,6 +220,7 @@ STATE_DIR="$APP_ROOT/.zmr/ios-shim-state"
|
|
|
216
220
|
SERVER_DIR="\$STATE_DIR/server"
|
|
217
221
|
PID_FILE="\$STATE_DIR/xcodebuild.pid"
|
|
218
222
|
READY_FILE="\$SERVER_DIR/ready"
|
|
223
|
+
DESTINATION_ID_FILE="\$STATE_DIR/destination.id"
|
|
219
224
|
BUILD_READY_FILE="\$STATE_DIR/build-for-testing.ready"
|
|
220
225
|
LOG_FILE="\$STATE_DIR/xcodebuild.log"
|
|
221
226
|
STDIN_FILE="\$(mktemp)"
|
|
@@ -241,6 +246,52 @@ tail_log() {
|
|
|
241
246
|
fi
|
|
242
247
|
}
|
|
243
248
|
|
|
249
|
+
tail_log_path() {
|
|
250
|
+
local log_file="\$1"
|
|
251
|
+
if [[ -f "\$log_file" ]]; then
|
|
252
|
+
tail -120 "\$log_file" >&2
|
|
253
|
+
fi
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
run_xcodebuild_with_timeout() {
|
|
257
|
+
local label="\$1"
|
|
258
|
+
local timeout_seconds="\$2"
|
|
259
|
+
local log_file="\$3"
|
|
260
|
+
shift 3
|
|
261
|
+
|
|
262
|
+
local pid started_at next_progress progress_seconds elapsed
|
|
263
|
+
progress_seconds="\${ZMR_IOS_SHIM_PROGRESS_SECONDS:-30}"
|
|
264
|
+
"\$@" >"\$log_file" 2>&1 &
|
|
265
|
+
pid="\$!"
|
|
266
|
+
started_at="\$SECONDS"
|
|
267
|
+
next_progress=\$((started_at + progress_seconds))
|
|
268
|
+
|
|
269
|
+
while kill -0 "\$pid" 2>/dev/null; do
|
|
270
|
+
elapsed=\$((SECONDS - started_at))
|
|
271
|
+
if (( elapsed >= timeout_seconds )); then
|
|
272
|
+
echo "timed out waiting for \$label after \${timeout_seconds}s" >&2
|
|
273
|
+
kill -TERM "\$pid" 2>/dev/null || true
|
|
274
|
+
sleep 2
|
|
275
|
+
if kill -0 "\$pid" 2>/dev/null; then
|
|
276
|
+
kill -KILL "\$pid" 2>/dev/null || true
|
|
277
|
+
fi
|
|
278
|
+
wait "\$pid" 2>/dev/null || true
|
|
279
|
+
tail_log_path "\$log_file"
|
|
280
|
+
exit 1
|
|
281
|
+
fi
|
|
282
|
+
if (( SECONDS >= next_progress )); then
|
|
283
|
+
echo "still waiting for \$label after \${elapsed}s; log: \$log_file" >&2
|
|
284
|
+
next_progress=\$((SECONDS + progress_seconds))
|
|
285
|
+
fi
|
|
286
|
+
sleep 1
|
|
287
|
+
done
|
|
288
|
+
|
|
289
|
+
if ! wait "\$pid"; then
|
|
290
|
+
tail_log_path "\$log_file"
|
|
291
|
+
exit 1
|
|
292
|
+
fi
|
|
293
|
+
}
|
|
294
|
+
|
|
244
295
|
resolve_destination() {
|
|
245
296
|
local destination_id="$DEVICE"
|
|
246
297
|
if [[ "\$destination_id" == "booted" ]]; then
|
|
@@ -248,7 +299,20 @@ resolve_destination() {
|
|
|
248
299
|
echo "physical iOS shim requires an explicit --device id" >&2
|
|
249
300
|
exit 2
|
|
250
301
|
fi
|
|
302
|
+
if [[ -f "\$DESTINATION_ID_FILE" ]]; then
|
|
303
|
+
destination_id="\$(cat "\$DESTINATION_ID_FILE" 2>/dev/null || true)"
|
|
304
|
+
if [[ -n "\$destination_id" ]] && xcrun simctl list devices available | grep -F "\$destination_id" >/dev/null 2>&1; then
|
|
305
|
+
# Reuse the first resolved booted simulator so build-for-testing and
|
|
306
|
+
# test-without-building target the same simulator even if it shuts down
|
|
307
|
+
# while Xcode is compiling the shim target.
|
|
308
|
+
printf '%s' "\$destination_id"
|
|
309
|
+
return 0
|
|
310
|
+
fi
|
|
311
|
+
fi
|
|
251
312
|
destination_id="\$(xcrun simctl list devices booted | sed -n 's/.*(\([0-9A-Fa-f-][0-9A-Fa-f-]*\)) (Booted).*/\1/p' | head -n 1)"
|
|
313
|
+
if [[ -n "\$destination_id" ]]; then
|
|
314
|
+
printf '%s' "\$destination_id" > "\$DESTINATION_ID_FILE"
|
|
315
|
+
fi
|
|
252
316
|
fi
|
|
253
317
|
if [[ -z "\$destination_id" ]]; then
|
|
254
318
|
echo "no booted iOS simulator found" >&2
|
|
@@ -272,9 +336,13 @@ is_server_running() {
|
|
|
272
336
|
if [[ ! -f "\$PID_FILE" ]]; then
|
|
273
337
|
return 1
|
|
274
338
|
fi
|
|
275
|
-
local pid
|
|
339
|
+
local pid command
|
|
276
340
|
pid="\$(cat "\$PID_FILE" 2>/dev/null || true)"
|
|
277
|
-
[[ -
|
|
341
|
+
if [[ -z "\$pid" ]] || ! kill -0 "\$pid" 2>/dev/null; then
|
|
342
|
+
return 1
|
|
343
|
+
fi
|
|
344
|
+
command="\$(ps -p "\$pid" -o command= 2>/dev/null || true)"
|
|
345
|
+
[[ "\$command" == *xcodebuild* && "\$command" == *ZMRShimUITests* ]]
|
|
278
346
|
}
|
|
279
347
|
|
|
280
348
|
run_oneshot() {
|
|
@@ -332,18 +400,15 @@ build_for_testing() {
|
|
|
332
400
|
destination_id="\$(destination_spec)"
|
|
333
401
|
build_log="\$STATE_DIR/xcodebuild.build.log"
|
|
334
402
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
tail -120 "\$build_log" >&2
|
|
345
|
-
exit 1
|
|
346
|
-
fi
|
|
403
|
+
run_xcodebuild_with_timeout "iOS shim build-for-testing" "\${ZMR_IOS_SHIM_BUILD_TIMEOUT_SECONDS:-5400}" "\$build_log" \\
|
|
404
|
+
xcodebuild build-for-testing \\
|
|
405
|
+
"\${XCODEBUILD_ARGS[@]}" \\
|
|
406
|
+
-scheme "$SCHEME" \\
|
|
407
|
+
-configuration "$CONFIGURATION" \\
|
|
408
|
+
-destination "\$destination_id" \\
|
|
409
|
+
ZMR_SHIM_MODE="server" \\
|
|
410
|
+
ZMR_SHIM_SERVER_DIR="\$SERVER_DIR" \\
|
|
411
|
+
ZMR_APP_BUNDLE_ID="$BUNDLE_ID"
|
|
347
412
|
|
|
348
413
|
touch "\$BUILD_READY_FILE"
|
|
349
414
|
}
|
package/shims/ios/ZMRShim.swift
CHANGED
|
@@ -128,7 +128,7 @@ final class ZMRShimUITestCase: XCTestCase {
|
|
|
128
128
|
guard isFastQueryable(parts: parts) else {
|
|
129
129
|
return error("selector.unsupported", "unsupported query selector: \(selector)")
|
|
130
130
|
}
|
|
131
|
-
let element =
|
|
131
|
+
let element = resolveElement(selector: selector, app: app, preferredTypes: [])
|
|
132
132
|
return [
|
|
133
133
|
"status": "ok",
|
|
134
134
|
"exists": element?.exists ?? false,
|
|
@@ -328,19 +328,49 @@ final class ZMRShimUITestCase: XCTestCase {
|
|
|
328
328
|
return fast
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
331
|
+
return resolveBroadElement(selector: selector, app: app)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private func resolveBroadElement(selector: String, app: XCUIApplication) -> XCUIElement? {
|
|
335
|
+
guard let parts = selectorParts(selector) else {
|
|
336
|
+
return nil
|
|
336
337
|
}
|
|
337
|
-
|
|
338
|
-
|
|
338
|
+
|
|
339
|
+
let query: XCUIElementQuery?
|
|
340
|
+
switch parts.field {
|
|
341
|
+
case "text", "label":
|
|
342
|
+
let predicate = parts.contains
|
|
343
|
+
? NSPredicate(format: "label CONTAINS[c] %@", parts.value)
|
|
344
|
+
: NSPredicate(format: "label == %@", parts.value)
|
|
345
|
+
query = app.descendants(matching: .any).matching(predicate)
|
|
346
|
+
case "identifier", "resourceId":
|
|
347
|
+
let predicate = parts.contains
|
|
348
|
+
? NSPredicate(format: "identifier CONTAINS[c] %@", parts.value)
|
|
349
|
+
: NSPredicate(format: "identifier == %@", parts.value)
|
|
350
|
+
query = app.descendants(matching: .any).matching(predicate)
|
|
351
|
+
case "value":
|
|
352
|
+
let predicate = parts.contains
|
|
353
|
+
? NSPredicate(format: "value CONTAINS[c] %@", parts.value)
|
|
354
|
+
: NSPredicate(format: "value == %@", parts.value)
|
|
355
|
+
query = app.descendants(matching: .any).matching(predicate)
|
|
356
|
+
case "id":
|
|
357
|
+
if parts.value.hasPrefix("id:") {
|
|
358
|
+
let identifier = String(parts.value.dropFirst("id:".count))
|
|
359
|
+
query = app.descendants(matching: .any).matching(identifier: identifier)
|
|
360
|
+
} else if parts.value.hasPrefix("label:") {
|
|
361
|
+
let label = String(parts.value.dropFirst("label:".count))
|
|
362
|
+
query = app.descendants(matching: .any).matching(NSPredicate(format: "label == %@", label))
|
|
363
|
+
} else {
|
|
364
|
+
query = nil
|
|
365
|
+
}
|
|
366
|
+
default:
|
|
367
|
+
query = nil
|
|
339
368
|
}
|
|
340
|
-
|
|
341
|
-
|
|
369
|
+
|
|
370
|
+
guard let element = query?.firstMatch, element.exists else {
|
|
371
|
+
return nil
|
|
342
372
|
}
|
|
343
|
-
return
|
|
373
|
+
return element
|
|
344
374
|
}
|
|
345
375
|
|
|
346
376
|
private func resolveFastElement(selector: String, app: XCUIApplication, preferredTypes: [XCUIElement.ElementType]) -> XCUIElement? {
|
package/src/ios.zig
CHANGED
|
@@ -9,7 +9,9 @@ const trace = @import("trace.zig");
|
|
|
9
9
|
const types = @import("types.zig");
|
|
10
10
|
|
|
11
11
|
const default_max_output = 32 * 1024 * 1024;
|
|
12
|
-
|
|
12
|
+
// Clean iOS prebuilds can force the XCTest shim script through a full native
|
|
13
|
+
// dependency build before it can answer the first selector query.
|
|
14
|
+
const shim_timeout_ms = 5_400_000;
|
|
13
15
|
const shim_best_effort_timeout_ms = 10_000;
|
|
14
16
|
const shim_command_attempts = 2;
|
|
15
17
|
const shim_bootstrap_retry_delay_ms = 500;
|
|
@@ -107,6 +109,7 @@ pub const IosDevice = struct {
|
|
|
107
109
|
if (self.target_kind == .physical) return try self.stopPhysicalBestEffort();
|
|
108
110
|
const result = try self.runSimctl(&.{ "terminate", self.target(), self.app_id }, default_max_output);
|
|
109
111
|
defer result.deinit(self.allocator);
|
|
112
|
+
if (ios_lifecycle.isAppNotRunning(result)) return;
|
|
110
113
|
try result.ensureSuccess();
|
|
111
114
|
}
|
|
112
115
|
|
|
@@ -307,8 +310,12 @@ pub const IosDevice = struct {
|
|
|
307
310
|
|
|
308
311
|
var command_with_selector = shim_command;
|
|
309
312
|
command_with_selector.selector = shim_selector;
|
|
310
|
-
try self.
|
|
311
|
-
|
|
313
|
+
const response = try self.runShim(command_with_selector);
|
|
314
|
+
defer self.allocator.free(response);
|
|
315
|
+
return switch (try ios_shim.parseSelectorActionResponse(response)) {
|
|
316
|
+
.ok => true,
|
|
317
|
+
.selector_unavailable => false,
|
|
318
|
+
};
|
|
312
319
|
}
|
|
313
320
|
|
|
314
321
|
fn runShim(self: *IosDevice, shim_command: ios_shim.Command) ![]u8 {
|
|
@@ -394,6 +401,6 @@ pub fn parsePhysicalDevicesJson(allocator: std.mem.Allocator, content: []const u
|
|
|
394
401
|
}
|
|
395
402
|
|
|
396
403
|
test "ios xctest shim timeout allows cold xcodebuild startup" {
|
|
397
|
-
try std.testing.expect(shim_timeout_ms >=
|
|
404
|
+
try std.testing.expect(shim_timeout_ms >= 5_400_000);
|
|
398
405
|
try std.testing.expect(shim_best_effort_timeout_ms <= 15_000);
|
|
399
406
|
}
|
package/src/ios_lifecycle.zig
CHANGED
|
@@ -70,3 +70,39 @@ pub fn isMissingInstalledApp(result: command.ExecResult) bool {
|
|
|
70
70
|
}
|
|
71
71
|
return std.mem.indexOf(u8, result.stderr, "No installed application with bundle identifier") != null;
|
|
72
72
|
}
|
|
73
|
+
|
|
74
|
+
pub fn isAppNotRunning(result: command.ExecResult) bool {
|
|
75
|
+
switch (result.term) {
|
|
76
|
+
.Exited => |code| if (code == 0) return false,
|
|
77
|
+
else => return false,
|
|
78
|
+
}
|
|
79
|
+
return std.mem.indexOf(u8, result.stderr, "found nothing to terminate") != null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
test "simctl terminate missing running app is best-effort" {
|
|
83
|
+
const allocator = std.testing.allocator;
|
|
84
|
+
const stdout = try allocator.dupe(u8, "");
|
|
85
|
+
defer allocator.free(stdout);
|
|
86
|
+
const stderr = try allocator.dupe(u8, "The request to terminate \"com.example.mobiletest\" failed. found nothing to terminate");
|
|
87
|
+
defer allocator.free(stderr);
|
|
88
|
+
|
|
89
|
+
try std.testing.expect(isAppNotRunning(.{
|
|
90
|
+
.stdout = stdout,
|
|
91
|
+
.stderr = stderr,
|
|
92
|
+
.term = .{ .Exited = 3 },
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
test "simctl terminate success is not classified as already stopped" {
|
|
97
|
+
const allocator = std.testing.allocator;
|
|
98
|
+
const stdout = try allocator.dupe(u8, "");
|
|
99
|
+
defer allocator.free(stdout);
|
|
100
|
+
const stderr = try allocator.dupe(u8, "");
|
|
101
|
+
defer allocator.free(stderr);
|
|
102
|
+
|
|
103
|
+
try std.testing.expect(!isAppNotRunning(.{
|
|
104
|
+
.stdout = stdout,
|
|
105
|
+
.stderr = stderr,
|
|
106
|
+
.term = .{ .Exited = 0 },
|
|
107
|
+
}));
|
|
108
|
+
}
|
package/src/ios_shim.zig
CHANGED
|
@@ -109,6 +109,48 @@ pub fn parseOkResponse(content: []const u8) !void {
|
|
|
109
109
|
if (!std.mem.eql(u8, status, "ok")) return error.IosShimResponseNotOk;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
pub const SelectorActionResponse = enum {
|
|
113
|
+
ok,
|
|
114
|
+
selector_unavailable,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
pub fn parseSelectorActionResponse(content: []const u8) !SelectorActionResponse {
|
|
118
|
+
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
|
119
|
+
defer arena.deinit();
|
|
120
|
+
const parsed = try std.json.parseFromSlice(std.json.Value, arena.allocator(), content, .{});
|
|
121
|
+
if (parsed.value != .object) return error.IosShimResponseMustBeObject;
|
|
122
|
+
const status = fieldString(parsed.value.object, "status") orelse return error.IosShimMissingStatus;
|
|
123
|
+
if (std.mem.eql(u8, status, "ok")) return .ok;
|
|
124
|
+
|
|
125
|
+
const code = fieldString(parsed.value.object, "code") orelse return error.IosShimResponseNotOk;
|
|
126
|
+
if (std.mem.eql(u8, code, "selector.not_found") or
|
|
127
|
+
std.mem.eql(u8, code, "selector.not_hittable"))
|
|
128
|
+
{
|
|
129
|
+
return .selector_unavailable;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return error.IosShimResponseNotOk;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
test "selector action response maps native selector misses to unavailable" {
|
|
136
|
+
try std.testing.expectEqual(SelectorActionResponse.ok, try parseSelectorActionResponse("{\"status\":\"ok\"}"));
|
|
137
|
+
try std.testing.expectEqual(
|
|
138
|
+
SelectorActionResponse.selector_unavailable,
|
|
139
|
+
try parseSelectorActionResponse("{\"status\":\"error\",\"code\":\"selector.not_found\",\"message\":\"missing\"}"),
|
|
140
|
+
);
|
|
141
|
+
try std.testing.expectEqual(
|
|
142
|
+
SelectorActionResponse.selector_unavailable,
|
|
143
|
+
try parseSelectorActionResponse("{\"status\":\"error\",\"code\":\"selector.not_hittable\",\"message\":\"covered\"}"),
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
test "selector action response keeps unsupported selectors as hard errors" {
|
|
148
|
+
try std.testing.expectError(
|
|
149
|
+
error.IosShimResponseNotOk,
|
|
150
|
+
parseSelectorActionResponse("{\"status\":\"error\",\"code\":\"selector.unsupported\",\"message\":\"bad\"}"),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
112
154
|
pub fn parseQueryResponse(content: []const u8) !bool {
|
|
113
155
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
|
114
156
|
defer arena.deinit();
|
package/src/version.zig
CHANGED