zig-mobile-runner 0.1.0

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.
Files changed (225) hide show
  1. package/CHANGELOG.md +484 -0
  2. package/CONTRIBUTING.md +42 -0
  3. package/FEATURES.md +112 -0
  4. package/LICENSE +21 -0
  5. package/README.md +255 -0
  6. package/SECURITY.md +34 -0
  7. package/build.zig +38 -0
  8. package/build.zig.zon +7 -0
  9. package/clients/README.md +144 -0
  10. package/clients/go/README.md +24 -0
  11. package/clients/go/examples/fake-session/main.go +93 -0
  12. package/clients/go/go.mod +3 -0
  13. package/clients/go/zmr/client.go +432 -0
  14. package/clients/kotlin/README.md +35 -0
  15. package/clients/kotlin/build.gradle.kts +35 -0
  16. package/clients/kotlin/settings.gradle.kts +15 -0
  17. package/clients/kotlin/src/main/kotlin/dev/zmr/FakeSession.kt +86 -0
  18. package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +67 -0
  19. package/clients/python/README.md +29 -0
  20. package/clients/python/examples/fake_session.py +48 -0
  21. package/clients/python/pyproject.toml +13 -0
  22. package/clients/python/zmr_client.py +202 -0
  23. package/clients/rust/Cargo.lock +107 -0
  24. package/clients/rust/Cargo.toml +10 -0
  25. package/clients/rust/README.md +19 -0
  26. package/clients/rust/examples/fake_session.rs +70 -0
  27. package/clients/rust/src/lib.rs +461 -0
  28. package/clients/swift/Package.swift +16 -0
  29. package/clients/swift/README.md +36 -0
  30. package/clients/swift/Sources/ZMRClient/ZMRClient.swift +114 -0
  31. package/clients/swift/Sources/ZMRFakeSession/main.swift +86 -0
  32. package/clients/typescript/README.md +34 -0
  33. package/clients/typescript/examples/fake-session.mjs +36 -0
  34. package/clients/typescript/index.d.ts +144 -0
  35. package/clients/typescript/index.mjs +192 -0
  36. package/clients/typescript/package.json +8 -0
  37. package/docs/adr/0001-agent-native-runner-boundary.md +31 -0
  38. package/docs/adr/0002-app-local-zmr-contract.md +39 -0
  39. package/docs/adr/0003-ios-simulator-xctest-shim.md +41 -0
  40. package/docs/adr/0004-benchmark-claims-and-baseline-collection.md +37 -0
  41. package/docs/adr/README.md +12 -0
  42. package/docs/ai-agents.md +156 -0
  43. package/docs/app-integration.md +316 -0
  44. package/docs/benchmarking.md +275 -0
  45. package/docs/client-installation.md +141 -0
  46. package/docs/clients.md +98 -0
  47. package/docs/config.md +175 -0
  48. package/docs/demo.md +259 -0
  49. package/docs/dsl.md +57 -0
  50. package/docs/install.md +233 -0
  51. package/docs/market-positioning.md +70 -0
  52. package/docs/npm.md +359 -0
  53. package/docs/protocol-fixtures/README.md +8 -0
  54. package/docs/protocol-fixtures/core-session.requests.jsonl +8 -0
  55. package/docs/protocol-fixtures/core-session.responses.jsonl +8 -0
  56. package/docs/protocol-versioning.md +65 -0
  57. package/docs/protocol.md +560 -0
  58. package/docs/publication.md +77 -0
  59. package/docs/release-audit.md +99 -0
  60. package/docs/release-candidate.md +111 -0
  61. package/docs/release-evidence.md +188 -0
  62. package/docs/release-notes-template.md +58 -0
  63. package/docs/roadmap.md +334 -0
  64. package/docs/scenario-authoring.md +88 -0
  65. package/docs/shipping.md +170 -0
  66. package/docs/trace-privacy.md +88 -0
  67. package/docs/troubleshooting.md +256 -0
  68. package/examples/android-app-auth-probe.json +89 -0
  69. package/examples/android-app-error-state.json +13 -0
  70. package/examples/android-app-login-smoke.json +192 -0
  71. package/examples/android-app-onboarding.json +12 -0
  72. package/examples/android-app-referral-deep-link.json +12 -0
  73. package/examples/android-shim-smoke.json +19 -0
  74. package/examples/demo-failure.json +12 -0
  75. package/examples/demo-fake.json +14 -0
  76. package/examples/ios-dev-client-open-link.json +26 -0
  77. package/examples/ios-dev-client-route-snapshot.json +24 -0
  78. package/examples/ios-shim-smoke.json +23 -0
  79. package/examples/ios-smoke.json +9 -0
  80. package/go.work +3 -0
  81. package/npm/agents.mjs +183 -0
  82. package/npm/app-config.mjs +95 -0
  83. package/npm/build-zmr.mjs +21 -0
  84. package/npm/commands.mjs +104 -0
  85. package/npm/generated-files.mjs +50 -0
  86. package/npm/index.mjs +75 -0
  87. package/npm/init-app.mjs +80 -0
  88. package/npm/package-scripts.mjs +72 -0
  89. package/npm/postinstall.mjs +21 -0
  90. package/npm/scaffold.mjs +179 -0
  91. package/npm/scenarios.mjs +93 -0
  92. package/npm/setup.mjs +69 -0
  93. package/npm/wizard.mjs +117 -0
  94. package/npm/zmr.mjs +23 -0
  95. package/package.json +114 -0
  96. package/prebuilds/darwin-arm64/zmr +0 -0
  97. package/prebuilds/darwin-x64/zmr +0 -0
  98. package/prebuilds/linux-arm64/zmr +0 -0
  99. package/prebuilds/linux-x64/zmr +0 -0
  100. package/schemas/README.md +26 -0
  101. package/schemas/action-result.schema.json +27 -0
  102. package/schemas/capabilities-output.schema.json +98 -0
  103. package/schemas/devices-output.schema.json +25 -0
  104. package/schemas/doctor-output.schema.json +51 -0
  105. package/schemas/explain-output.schema.json +51 -0
  106. package/schemas/import-output.schema.json +23 -0
  107. package/schemas/init-output.schema.json +71 -0
  108. package/schemas/json-rpc.schema.json +55 -0
  109. package/schemas/release-manifest.schema.json +43 -0
  110. package/schemas/release-readiness-output.schema.json +127 -0
  111. package/schemas/run-output.schema.json +43 -0
  112. package/schemas/scenario.schema.json +128 -0
  113. package/schemas/schemas-output.schema.json +26 -0
  114. package/schemas/semantic-snapshot.schema.json +116 -0
  115. package/schemas/snapshot.schema.json +60 -0
  116. package/schemas/trace-event.schema.json +14 -0
  117. package/schemas/trace-manifest.schema.json +59 -0
  118. package/schemas/validate-output.schema.json +42 -0
  119. package/schemas/version-output.schema.json +23 -0
  120. package/schemas/zmr-config.schema.json +75 -0
  121. package/scripts/android-emulator.sh +126 -0
  122. package/scripts/assert-ios-physical-ready.sh +213 -0
  123. package/scripts/benchmark-command.sh +307 -0
  124. package/scripts/benchmark.sh +359 -0
  125. package/scripts/benchmark_gate.py +117 -0
  126. package/scripts/benchmark_result_row.py +88 -0
  127. package/scripts/compare-benchmarks.py +288 -0
  128. package/scripts/create-android-demo-app.sh +342 -0
  129. package/scripts/create-ios-demo-app.sh +261 -0
  130. package/scripts/demo-android-real.sh +232 -0
  131. package/scripts/demo-ios-real.sh +270 -0
  132. package/scripts/demo.sh +464 -0
  133. package/scripts/device-matrix.sh +338 -0
  134. package/scripts/ensure-ios-shim-target.rb +237 -0
  135. package/scripts/install-android-shim.sh +281 -0
  136. package/scripts/install-ios-shim.sh +589 -0
  137. package/scripts/pilot-gate.sh +560 -0
  138. package/scripts/release-readiness.py +838 -0
  139. package/scripts/release-readiness.sh +91 -0
  140. package/scripts/run-android-pilot.sh +561 -0
  141. package/scripts/run-ios-pilot.sh +509 -0
  142. package/shims/android/README.md +21 -0
  143. package/shims/android/ZMRShimInstrumentedTest.java +152 -0
  144. package/shims/android/protocol.md +18 -0
  145. package/shims/ios/README.md +50 -0
  146. package/shims/ios/ZMRShim.swift +110 -0
  147. package/shims/ios/ZMRShimUITestCase.swift +475 -0
  148. package/shims/ios/protocol.md +74 -0
  149. package/skills/zmr-mobile-testing/SKILL.md +127 -0
  150. package/src/android.zig +344 -0
  151. package/src/android_device_info.zig +99 -0
  152. package/src/android_emulator.zig +154 -0
  153. package/src/android_screen_recording.zig +112 -0
  154. package/src/android_shell.zig +112 -0
  155. package/src/bundle.zig +124 -0
  156. package/src/bundle_redaction.zig +272 -0
  157. package/src/bundle_tar.zig +123 -0
  158. package/src/cli_devices.zig +97 -0
  159. package/src/cli_doctor.zig +114 -0
  160. package/src/cli_import.zig +70 -0
  161. package/src/cli_info.zig +39 -0
  162. package/src/cli_init.zig +72 -0
  163. package/src/cli_output.zig +467 -0
  164. package/src/cli_run.zig +259 -0
  165. package/src/cli_serve.zig +287 -0
  166. package/src/cli_trace.zig +111 -0
  167. package/src/cli_validate.zig +41 -0
  168. package/src/command.zig +211 -0
  169. package/src/config.zig +305 -0
  170. package/src/config_diagnostics.zig +212 -0
  171. package/src/config_paths.zig +49 -0
  172. package/src/device_registry.zig +37 -0
  173. package/src/doctor.zig +412 -0
  174. package/src/doctor_hints.zig +52 -0
  175. package/src/errors.zig +55 -0
  176. package/src/fake_device.zig +163 -0
  177. package/src/health.zig +28 -0
  178. package/src/importer.zig +343 -0
  179. package/src/importer_json.zig +100 -0
  180. package/src/importer_model.zig +103 -0
  181. package/src/ios.zig +399 -0
  182. package/src/ios_devices.zig +219 -0
  183. package/src/ios_lifecycle.zig +72 -0
  184. package/src/ios_shim.zig +242 -0
  185. package/src/ios_snapshot.zig +20 -0
  186. package/src/json_fields.zig +80 -0
  187. package/src/json_rpc.zig +150 -0
  188. package/src/json_rpc_methods.zig +318 -0
  189. package/src/json_rpc_observation.zig +31 -0
  190. package/src/json_rpc_params.zig +52 -0
  191. package/src/json_rpc_protocol.zig +110 -0
  192. package/src/json_rpc_trace.zig +73 -0
  193. package/src/main.zig +135 -0
  194. package/src/mcp.zig +234 -0
  195. package/src/mcp_protocol.zig +64 -0
  196. package/src/mcp_trace.zig +83 -0
  197. package/src/report.zig +346 -0
  198. package/src/report_html.zig +63 -0
  199. package/src/report_values.zig +27 -0
  200. package/src/run_options.zig +152 -0
  201. package/src/runner.zig +280 -0
  202. package/src/runner_actions.zig +109 -0
  203. package/src/runner_config.zig +6 -0
  204. package/src/runner_diagnostics.zig +268 -0
  205. package/src/runner_events.zig +170 -0
  206. package/src/runner_native.zig +88 -0
  207. package/src/runner_waits.zig +300 -0
  208. package/src/scaffold.zig +472 -0
  209. package/src/scenario.zig +346 -0
  210. package/src/scenario_fields.zig +50 -0
  211. package/src/schema_registry.zig +53 -0
  212. package/src/selector.zig +84 -0
  213. package/src/semantic.zig +171 -0
  214. package/src/trace.zig +315 -0
  215. package/src/trace_json.zig +340 -0
  216. package/src/trace_summary.zig +218 -0
  217. package/src/trace_summary_diagnostic.zig +202 -0
  218. package/src/types.zig +120 -0
  219. package/src/uiautomator.zig +164 -0
  220. package/src/validation.zig +187 -0
  221. package/src/version.zig +22 -0
  222. package/viewer/app.js +373 -0
  223. package/viewer/index.html +126 -0
  224. package/viewer/parser.js +233 -0
  225. package/viewer/styles.css +585 -0
@@ -0,0 +1,461 @@
1
+ use serde::{Deserialize, Serialize};
2
+ use serde_json::{json, Value};
3
+ use std::collections::HashMap;
4
+ use std::fmt;
5
+ use std::io::{BufRead, BufReader, Write};
6
+ use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
7
+
8
+ #[derive(Debug)]
9
+ pub enum Error {
10
+ Io(std::io::Error),
11
+ Json(serde_json::Error),
12
+ Rpc(RpcError),
13
+ UnexpectedResponseId(i64),
14
+ MissingPipe(&'static str),
15
+ }
16
+
17
+ impl fmt::Display for Error {
18
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19
+ match self {
20
+ Error::Io(err) => write!(f, "{err}"),
21
+ Error::Json(err) => write!(f, "{err}"),
22
+ Error::Rpc(err) => write!(f, "{err}"),
23
+ Error::UnexpectedResponseId(id) => write!(f, "unexpected JSON-RPC response id {id}"),
24
+ Error::MissingPipe(name) => write!(f, "missing child process {name} pipe"),
25
+ }
26
+ }
27
+ }
28
+
29
+ impl std::error::Error for Error {}
30
+
31
+ impl From<std::io::Error> for Error {
32
+ fn from(value: std::io::Error) -> Self {
33
+ Error::Io(value)
34
+ }
35
+ }
36
+
37
+ impl From<serde_json::Error> for Error {
38
+ fn from(value: serde_json::Error) -> Self {
39
+ Error::Json(value)
40
+ }
41
+ }
42
+
43
+ #[derive(Debug, Deserialize)]
44
+ pub struct RpcError {
45
+ pub code: i64,
46
+ pub message: String,
47
+ #[serde(rename = "publicCode")]
48
+ pub public_code: Option<String>,
49
+ pub data: Option<Value>,
50
+ }
51
+
52
+ impl fmt::Display for RpcError {
53
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54
+ write!(f, "{}", self.message)
55
+ }
56
+ }
57
+
58
+ impl std::error::Error for RpcError {}
59
+
60
+ #[derive(Debug, Deserialize)]
61
+ struct RpcResponse {
62
+ id: i64,
63
+ result: Option<Value>,
64
+ error: Option<RpcError>,
65
+ }
66
+
67
+ pub struct Client {
68
+ child: Child,
69
+ stdin: ChildStdin,
70
+ stdout: BufReader<ChildStdout>,
71
+ next_id: i64,
72
+ }
73
+
74
+ #[derive(Debug, Deserialize)]
75
+ pub struct Capabilities {
76
+ pub name: String,
77
+ pub version: String,
78
+ #[serde(rename = "protocolVersion")]
79
+ pub protocol_version: String,
80
+ pub platforms: Vec<String>,
81
+ #[serde(rename = "platformSupport", default)]
82
+ pub platform_support: HashMap<String, PlatformSupport>,
83
+ #[serde(rename = "iosPreview", default)]
84
+ pub ios_preview: bool,
85
+ pub transports: Vec<String>,
86
+ pub methods: Vec<String>,
87
+ }
88
+
89
+ #[derive(Debug, Deserialize)]
90
+ pub struct PlatformSupport {
91
+ pub status: String,
92
+ #[serde(rename = "deviceTypes")]
93
+ pub device_types: Vec<String>,
94
+ pub automation: Vec<String>,
95
+ #[serde(rename = "physicalDevices", default)]
96
+ pub physical_devices: bool,
97
+ }
98
+
99
+ #[derive(Debug, Deserialize)]
100
+ pub struct Session {
101
+ #[serde(rename = "sessionId")]
102
+ pub session_id: String,
103
+ }
104
+
105
+ #[derive(Debug, Deserialize)]
106
+ pub struct DeviceInfo {
107
+ pub serial: String,
108
+ pub state: String,
109
+ pub ready: bool,
110
+ }
111
+
112
+ #[derive(Debug, Deserialize)]
113
+ pub struct Snapshot {
114
+ pub id: String,
115
+ #[serde(rename = "timestampMs")]
116
+ pub timestamp_ms: i64,
117
+ pub viewport: Value,
118
+ #[serde(rename = "activePackage")]
119
+ pub active_package: String,
120
+ #[serde(rename = "activeActivity")]
121
+ pub active_activity: Option<String>,
122
+ pub nodes: Vec<Node>,
123
+ }
124
+
125
+ #[derive(Debug, Deserialize)]
126
+ pub struct Node {
127
+ #[serde(rename = "stableId")]
128
+ pub stable_id: String,
129
+ #[serde(rename = "className")]
130
+ pub class_name: String,
131
+ #[serde(default, rename = "resourceId")]
132
+ pub resource_id: Option<String>,
133
+ pub text: Option<String>,
134
+ #[serde(rename = "contentDesc")]
135
+ pub content_desc: Option<String>,
136
+ pub bounds: Value,
137
+ pub enabled: bool,
138
+ pub visible: bool,
139
+ pub selected: bool,
140
+ }
141
+
142
+ #[derive(Debug, Deserialize)]
143
+ pub struct SemanticSnapshot {
144
+ pub id: String,
145
+ #[serde(rename = "timestampMs")]
146
+ pub timestamp_ms: i64,
147
+ pub viewport: Value,
148
+ #[serde(rename = "activePackage")]
149
+ pub active_package: Option<String>,
150
+ #[serde(rename = "activeActivity")]
151
+ pub active_activity: Option<String>,
152
+ #[serde(rename = "focusedNodeId")]
153
+ pub focused_node_id: Option<String>,
154
+ pub nodes: Vec<SemanticNode>,
155
+ pub summary: SemanticSummary,
156
+ }
157
+
158
+ #[derive(Debug, Deserialize)]
159
+ pub struct SemanticSummary {
160
+ #[serde(rename = "nodeCount")]
161
+ pub node_count: usize,
162
+ #[serde(rename = "interactiveCount")]
163
+ pub interactive_count: usize,
164
+ #[serde(rename = "visibleText")]
165
+ pub visible_text: Vec<String>,
166
+ }
167
+
168
+ #[derive(Debug, Deserialize)]
169
+ pub struct SemanticNode {
170
+ pub id: String,
171
+ pub role: String,
172
+ pub name: String,
173
+ pub selector: Value,
174
+ pub source: Value,
175
+ pub bounds: Value,
176
+ pub enabled: bool,
177
+ pub visible: bool,
178
+ pub selected: bool,
179
+ pub interactive: bool,
180
+ #[serde(rename = "recommendedAction")]
181
+ pub recommended_action: Option<String>,
182
+ }
183
+
184
+ #[derive(Debug, Deserialize)]
185
+ pub struct TraceExport {
186
+ #[serde(rename = "traceDir")]
187
+ pub trace_dir: String,
188
+ pub out: String,
189
+ pub redacted: bool,
190
+ #[serde(rename = "omitScreenshots")]
191
+ pub omit_screenshots: bool,
192
+ }
193
+
194
+ #[derive(Debug, Deserialize)]
195
+ pub struct TraceEvents {
196
+ #[serde(rename = "traceDir")]
197
+ pub trace_dir: String,
198
+ #[serde(rename = "afterSeq")]
199
+ pub after_seq: i64,
200
+ #[serde(rename = "nextSeq")]
201
+ pub next_seq: i64,
202
+ #[serde(rename = "latestSeq")]
203
+ pub latest_seq: i64,
204
+ pub events: Vec<Value>,
205
+ }
206
+
207
+ impl Client {
208
+ pub fn start<I, S>(command: &str, args: I) -> Result<Self, Error>
209
+ where
210
+ I: IntoIterator<Item = S>,
211
+ S: AsRef<std::ffi::OsStr>,
212
+ {
213
+ let mut child = Command::new(command)
214
+ .args(args)
215
+ .stdin(Stdio::piped())
216
+ .stdout(Stdio::piped())
217
+ .spawn()?;
218
+ let stdin = child.stdin.take().ok_or(Error::MissingPipe("stdin"))?;
219
+ let stdout = child.stdout.take().ok_or(Error::MissingPipe("stdout"))?;
220
+ Ok(Self {
221
+ child,
222
+ stdin,
223
+ stdout: BufReader::new(stdout),
224
+ next_id: 1,
225
+ })
226
+ }
227
+
228
+ pub fn request<T: for<'de> Deserialize<'de>>(
229
+ &mut self,
230
+ method: &str,
231
+ params: Value,
232
+ ) -> Result<T, Error> {
233
+ let id = self.next_id;
234
+ self.next_id += 1;
235
+ let request = json!({
236
+ "jsonrpc": "2.0",
237
+ "id": id,
238
+ "method": method,
239
+ "params": params,
240
+ });
241
+ writeln!(self.stdin, "{}", serde_json::to_string(&request)?)?;
242
+ self.stdin.flush()?;
243
+
244
+ let mut line = String::new();
245
+ self.stdout.read_line(&mut line)?;
246
+ let response: RpcResponse = serde_json::from_str(&line)?;
247
+ if response.id != id {
248
+ return Err(Error::UnexpectedResponseId(response.id));
249
+ }
250
+ if let Some(error) = response.error {
251
+ return Err(Error::Rpc(error));
252
+ }
253
+ let result = response.result.unwrap_or(Value::Null);
254
+ Ok(serde_json::from_value(result)?)
255
+ }
256
+
257
+ pub fn capabilities(&mut self) -> Result<Capabilities, Error> {
258
+ self.request("runner.capabilities", json!({}))
259
+ }
260
+
261
+ pub fn create_session(&mut self) -> Result<Session, Error> {
262
+ self.request("session.create", json!({}))
263
+ }
264
+
265
+ pub fn close_session(&mut self) -> Result<bool, Error> {
266
+ self.request("session.close", json!({}))
267
+ }
268
+
269
+ pub fn devices(&mut self) -> Result<Vec<DeviceInfo>, Error> {
270
+ self.request("device.list", json!({}))
271
+ }
272
+
273
+ pub fn launch(&mut self) -> Result<bool, Error> {
274
+ self.request("app.launch", json!({}))
275
+ }
276
+
277
+ pub fn stop(&mut self) -> Result<bool, Error> {
278
+ self.request("app.stop", json!({}))
279
+ }
280
+
281
+ pub fn clear_state(&mut self) -> Result<bool, Error> {
282
+ self.request("app.clearState", json!({}))
283
+ }
284
+
285
+ pub fn open_link(&mut self, url: &str) -> Result<bool, Error> {
286
+ self.request("app.openLink", json!({ "url": url }))
287
+ }
288
+
289
+ pub fn snapshot(&mut self) -> Result<Snapshot, Error> {
290
+ self.request("observe.snapshot", json!({}))
291
+ }
292
+
293
+ pub fn semantic_snapshot(&mut self) -> Result<SemanticSnapshot, Error> {
294
+ self.request("observe.semanticSnapshot", json!({}))
295
+ }
296
+
297
+ pub fn tap(&mut self, selector: Value) -> Result<bool, Error> {
298
+ self.request("ui.tap", json!({ "selector": selector }))
299
+ }
300
+
301
+ pub fn type_text(&mut self, text: &str, selector: Option<Value>) -> Result<bool, Error> {
302
+ let mut params = json!({ "text": text });
303
+ if let Some(selector) = selector {
304
+ params["selector"] = selector;
305
+ }
306
+ self.request("ui.type", params)
307
+ }
308
+
309
+ pub fn erase_text(
310
+ &mut self,
311
+ selector: Option<Value>,
312
+ max_chars: Option<i64>,
313
+ ) -> Result<bool, Error> {
314
+ let mut params = json!({});
315
+ if let Some(selector) = selector {
316
+ params["selector"] = selector;
317
+ }
318
+ if let Some(max_chars) = max_chars {
319
+ params["maxChars"] = json!(max_chars);
320
+ }
321
+ self.request("ui.eraseText", params)
322
+ }
323
+
324
+ pub fn hide_keyboard(&mut self) -> Result<bool, Error> {
325
+ self.request("ui.hideKeyboard", json!({}))
326
+ }
327
+
328
+ pub fn swipe(
329
+ &mut self,
330
+ x1: i64,
331
+ y1: i64,
332
+ x2: i64,
333
+ y2: i64,
334
+ duration_ms: Option<i64>,
335
+ ) -> Result<bool, Error> {
336
+ let mut params = json!({ "x1": x1, "y1": y1, "x2": x2, "y2": y2 });
337
+ if let Some(duration_ms) = duration_ms {
338
+ params["durationMs"] = json!(duration_ms);
339
+ }
340
+ self.request("ui.swipe", params)
341
+ }
342
+
343
+ pub fn press_back(&mut self) -> Result<bool, Error> {
344
+ self.request("ui.pressBack", json!({}))
345
+ }
346
+
347
+ pub fn scroll_until_visible(
348
+ &mut self,
349
+ selector: Value,
350
+ direction: Option<&str>,
351
+ timeout_ms: Option<i64>,
352
+ ) -> Result<bool, Error> {
353
+ let mut params = json!({ "selector": selector });
354
+ if let Some(direction) = direction {
355
+ params["direction"] = json!(direction);
356
+ }
357
+ if let Some(timeout_ms) = timeout_ms {
358
+ params["timeoutMs"] = json!(timeout_ms);
359
+ }
360
+ self.request("ui.scrollUntilVisible", params)
361
+ }
362
+
363
+ pub fn wait_until(&mut self, selector: Value, timeout_ms: Option<i64>) -> Result<bool, Error> {
364
+ let mut params = json!({ "visible": selector });
365
+ if let Some(timeout_ms) = timeout_ms {
366
+ params["timeoutMs"] = json!(timeout_ms);
367
+ }
368
+ self.request("wait.until", params)
369
+ }
370
+
371
+ pub fn wait_any(
372
+ &mut self,
373
+ selectors: Vec<Value>,
374
+ timeout_ms: Option<i64>,
375
+ ) -> Result<bool, Error> {
376
+ let mut params = json!({ "selectors": selectors });
377
+ if let Some(timeout_ms) = timeout_ms {
378
+ params["timeoutMs"] = json!(timeout_ms);
379
+ }
380
+ self.request("wait.any", params)
381
+ }
382
+
383
+ pub fn wait_gone(&mut self, selector: Value, timeout_ms: Option<i64>) -> Result<bool, Error> {
384
+ let mut params = json!({ "selector": selector });
385
+ if let Some(timeout_ms) = timeout_ms {
386
+ params["timeoutMs"] = json!(timeout_ms);
387
+ }
388
+ self.request("wait.gone", params)
389
+ }
390
+
391
+ pub fn assert_visible(
392
+ &mut self,
393
+ selector: Value,
394
+ timeout_ms: Option<i64>,
395
+ ) -> Result<bool, Error> {
396
+ let mut params = json!({ "selector": selector });
397
+ if let Some(timeout_ms) = timeout_ms {
398
+ params["timeoutMs"] = json!(timeout_ms);
399
+ }
400
+ self.request("assert.visible", params)
401
+ }
402
+
403
+ pub fn assert_not_visible(
404
+ &mut self,
405
+ selector: Value,
406
+ timeout_ms: Option<i64>,
407
+ ) -> Result<bool, Error> {
408
+ let mut params = json!({ "selector": selector });
409
+ if let Some(timeout_ms) = timeout_ms {
410
+ params["timeoutMs"] = json!(timeout_ms);
411
+ }
412
+ self.request("assert.notVisible", params)
413
+ }
414
+
415
+ pub fn assert_healthy(&mut self, timeout_ms: Option<i64>) -> Result<bool, Error> {
416
+ let mut params = json!({});
417
+ if let Some(timeout_ms) = timeout_ms {
418
+ params["timeoutMs"] = json!(timeout_ms);
419
+ }
420
+ self.request("assert.healthy", params)
421
+ }
422
+
423
+ pub fn export_trace(
424
+ &mut self,
425
+ out: &str,
426
+ redact: bool,
427
+ omit_screenshots: bool,
428
+ ) -> Result<TraceExport, Error> {
429
+ self.request(
430
+ "trace.export",
431
+ json!({ "out": out, "redact": redact, "omitScreenshots": omit_screenshots }),
432
+ )
433
+ }
434
+
435
+ pub fn trace_events(
436
+ &mut self,
437
+ after_seq: i64,
438
+ limit: Option<i64>,
439
+ ) -> Result<TraceEvents, Error> {
440
+ let mut params = json!({ "afterSeq": after_seq });
441
+ if let Some(limit) = limit {
442
+ params["limit"] = json!(limit);
443
+ }
444
+ self.request("trace.events", params)
445
+ }
446
+ }
447
+
448
+ impl Drop for Client {
449
+ fn drop(&mut self) {
450
+ let _ = self.child.kill();
451
+ let _ = self.child.wait();
452
+ }
453
+ }
454
+
455
+ #[derive(Debug, Serialize)]
456
+ pub struct Selector {
457
+ #[serde(skip_serializing_if = "Option::is_none")]
458
+ pub text: Option<String>,
459
+ #[serde(skip_serializing_if = "Option::is_none", rename = "resourceId")]
460
+ pub resource_id: Option<String>,
461
+ }
@@ -0,0 +1,16 @@
1
+ // swift-tools-version: 5.9
2
+ import PackageDescription
3
+
4
+ let package = Package(
5
+ name: "ZMRClient",
6
+ platforms: [.macOS(.v13)],
7
+ products: [
8
+ .library(name: "ZMRClient", targets: ["ZMRClient"]),
9
+ .executable(name: "ZMRFakeSession", targets: ["ZMRFakeSession"])
10
+ ],
11
+ targets: [
12
+ .target(name: "ZMRClient"),
13
+ .executableTarget(name: "ZMRFakeSession", dependencies: ["ZMRClient"]),
14
+ .testTarget(name: "ZMRClientTests", dependencies: ["ZMRClient"])
15
+ ]
16
+ )
@@ -0,0 +1,36 @@
1
+ # ZMR Swift Client
2
+
3
+ Small Foundation-based client for macOS test harnesses and agents that drive
4
+ `zmr serve --transport stdio`.
5
+
6
+ Add it to a Swift package. Until this client is published as a standalone Swift
7
+ package, consume it from a local checkout:
8
+
9
+ ```bash
10
+ git submodule add https://github.com/johnmikel/zig-mobile-runner.git vendor/zig-mobile-runner
11
+ ```
12
+
13
+ ```swift
14
+ .package(path: "vendor/zig-mobile-runner/clients/swift")
15
+ ```
16
+
17
+ Then depend on the `ZMRClient` product from `clients/swift`.
18
+
19
+ Run the package test from this directory:
20
+
21
+ ```bash
22
+ swift test
23
+ ```
24
+
25
+ Run the fake-session example against a local checkout:
26
+
27
+ ```bash
28
+ swift run ZMRFakeSession \
29
+ --zmr ../../zig-out/bin/zmr \
30
+ --adb ../../tests/fake-adb.sh \
31
+ --trace-dir ../../traces/demo-swift-client \
32
+ --trace-out ../../traces/demo-swift-client-redacted.zmrtrace
33
+ ```
34
+
35
+ The Swift client is host-side. It is for macOS automation code, not code that
36
+ runs inside the iOS app.
@@ -0,0 +1,114 @@
1
+ import Foundation
2
+
3
+ public enum ZMRError: Error {
4
+ case processNotStarted
5
+ case invalidResponse
6
+ case rpcError([String: Any])
7
+ }
8
+
9
+ public final class ZMRClient {
10
+ private let process: Process
11
+ private let input: FileHandle
12
+ private let output: FileHandle
13
+ private var nextID = 1
14
+
15
+ public init(executable: String = "zmr", arguments: [String] = ["serve", "--transport", "stdio"]) {
16
+ let process = Process()
17
+ if executable.contains("/") {
18
+ process.executableURL = URL(fileURLWithPath: executable)
19
+ process.arguments = arguments
20
+ } else {
21
+ process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
22
+ process.arguments = [executable] + arguments
23
+ }
24
+
25
+ let stdinPipe = Pipe()
26
+ let stdoutPipe = Pipe()
27
+ process.standardInput = stdinPipe
28
+ process.standardOutput = stdoutPipe
29
+ process.standardError = FileHandle.standardError
30
+
31
+ self.process = process
32
+ self.input = stdinPipe.fileHandleForWriting
33
+ self.output = stdoutPipe.fileHandleForReading
34
+ }
35
+
36
+ public func start() throws {
37
+ try process.run()
38
+ }
39
+
40
+ public func close() {
41
+ _ = try? call("session.close")
42
+ input.closeFile()
43
+ if process.isRunning {
44
+ process.terminate()
45
+ }
46
+ }
47
+
48
+ @discardableResult
49
+ public func call(_ method: String, params: [String: Any]? = nil) throws -> Any {
50
+ guard process.isRunning else { throw ZMRError.processNotStarted }
51
+ let id = nextID
52
+ nextID += 1
53
+
54
+ var request: [String: Any] = ["jsonrpc": "2.0", "id": id, "method": method]
55
+ if let params {
56
+ request["params"] = params
57
+ }
58
+ let data = try JSONSerialization.data(withJSONObject: request, options: [])
59
+ input.write(data)
60
+ input.write(Data([0x0a]))
61
+
62
+ let line = try readLineData()
63
+ let object = try JSONSerialization.jsonObject(with: line, options: [])
64
+ guard let response = object as? [String: Any] else { throw ZMRError.invalidResponse }
65
+ if let error = response["error"] as? [String: Any] {
66
+ throw ZMRError.rpcError(error)
67
+ }
68
+ guard let result = response["result"] else { throw ZMRError.invalidResponse }
69
+ return result
70
+ }
71
+
72
+ public func createSession() throws {
73
+ _ = try call("session.create")
74
+ }
75
+
76
+ public func snapshot() throws -> [String: Any] {
77
+ guard let result = try call("observe.snapshot") as? [String: Any] else {
78
+ throw ZMRError.invalidResponse
79
+ }
80
+ return result
81
+ }
82
+
83
+ public func semanticSnapshot() throws -> [String: Any] {
84
+ guard let result = try call("observe.semanticSnapshot") as? [String: Any] else {
85
+ throw ZMRError.invalidResponse
86
+ }
87
+ return result
88
+ }
89
+
90
+ public func assertHealthy(timeoutMs: Int? = nil) throws -> Bool {
91
+ var params: [String: Any] = [:]
92
+ if let timeoutMs {
93
+ params["timeoutMs"] = timeoutMs
94
+ }
95
+ guard let result = try call("assert.healthy", params: params) as? Bool else {
96
+ throw ZMRError.invalidResponse
97
+ }
98
+ return result
99
+ }
100
+
101
+ private func readLineData() throws -> Data {
102
+ var data = Data()
103
+ while true {
104
+ let byte = output.readData(ofLength: 1)
105
+ if byte.isEmpty {
106
+ throw ZMRError.invalidResponse
107
+ }
108
+ if byte[0] == 0x0a {
109
+ return data
110
+ }
111
+ data.append(byte)
112
+ }
113
+ }
114
+ }