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.
- package/CHANGELOG.md +484 -0
- package/CONTRIBUTING.md +42 -0
- package/FEATURES.md +112 -0
- package/LICENSE +21 -0
- package/README.md +255 -0
- package/SECURITY.md +34 -0
- package/build.zig +38 -0
- package/build.zig.zon +7 -0
- package/clients/README.md +144 -0
- package/clients/go/README.md +24 -0
- package/clients/go/examples/fake-session/main.go +93 -0
- package/clients/go/go.mod +3 -0
- package/clients/go/zmr/client.go +432 -0
- package/clients/kotlin/README.md +35 -0
- package/clients/kotlin/build.gradle.kts +35 -0
- package/clients/kotlin/settings.gradle.kts +15 -0
- package/clients/kotlin/src/main/kotlin/dev/zmr/FakeSession.kt +86 -0
- package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +67 -0
- package/clients/python/README.md +29 -0
- package/clients/python/examples/fake_session.py +48 -0
- package/clients/python/pyproject.toml +13 -0
- package/clients/python/zmr_client.py +202 -0
- package/clients/rust/Cargo.lock +107 -0
- package/clients/rust/Cargo.toml +10 -0
- package/clients/rust/README.md +19 -0
- package/clients/rust/examples/fake_session.rs +70 -0
- package/clients/rust/src/lib.rs +461 -0
- package/clients/swift/Package.swift +16 -0
- package/clients/swift/README.md +36 -0
- package/clients/swift/Sources/ZMRClient/ZMRClient.swift +114 -0
- package/clients/swift/Sources/ZMRFakeSession/main.swift +86 -0
- package/clients/typescript/README.md +34 -0
- package/clients/typescript/examples/fake-session.mjs +36 -0
- package/clients/typescript/index.d.ts +144 -0
- package/clients/typescript/index.mjs +192 -0
- package/clients/typescript/package.json +8 -0
- package/docs/adr/0001-agent-native-runner-boundary.md +31 -0
- package/docs/adr/0002-app-local-zmr-contract.md +39 -0
- package/docs/adr/0003-ios-simulator-xctest-shim.md +41 -0
- package/docs/adr/0004-benchmark-claims-and-baseline-collection.md +37 -0
- package/docs/adr/README.md +12 -0
- package/docs/ai-agents.md +156 -0
- package/docs/app-integration.md +316 -0
- package/docs/benchmarking.md +275 -0
- package/docs/client-installation.md +141 -0
- package/docs/clients.md +98 -0
- package/docs/config.md +175 -0
- package/docs/demo.md +259 -0
- package/docs/dsl.md +57 -0
- package/docs/install.md +233 -0
- package/docs/market-positioning.md +70 -0
- package/docs/npm.md +359 -0
- package/docs/protocol-fixtures/README.md +8 -0
- package/docs/protocol-fixtures/core-session.requests.jsonl +8 -0
- package/docs/protocol-fixtures/core-session.responses.jsonl +8 -0
- package/docs/protocol-versioning.md +65 -0
- package/docs/protocol.md +560 -0
- package/docs/publication.md +77 -0
- package/docs/release-audit.md +99 -0
- package/docs/release-candidate.md +111 -0
- package/docs/release-evidence.md +188 -0
- package/docs/release-notes-template.md +58 -0
- package/docs/roadmap.md +334 -0
- package/docs/scenario-authoring.md +88 -0
- package/docs/shipping.md +170 -0
- package/docs/trace-privacy.md +88 -0
- package/docs/troubleshooting.md +256 -0
- package/examples/android-app-auth-probe.json +89 -0
- package/examples/android-app-error-state.json +13 -0
- package/examples/android-app-login-smoke.json +192 -0
- package/examples/android-app-onboarding.json +12 -0
- package/examples/android-app-referral-deep-link.json +12 -0
- package/examples/android-shim-smoke.json +19 -0
- package/examples/demo-failure.json +12 -0
- package/examples/demo-fake.json +14 -0
- package/examples/ios-dev-client-open-link.json +26 -0
- package/examples/ios-dev-client-route-snapshot.json +24 -0
- package/examples/ios-shim-smoke.json +23 -0
- package/examples/ios-smoke.json +9 -0
- package/go.work +3 -0
- package/npm/agents.mjs +183 -0
- package/npm/app-config.mjs +95 -0
- package/npm/build-zmr.mjs +21 -0
- package/npm/commands.mjs +104 -0
- package/npm/generated-files.mjs +50 -0
- package/npm/index.mjs +75 -0
- package/npm/init-app.mjs +80 -0
- package/npm/package-scripts.mjs +72 -0
- package/npm/postinstall.mjs +21 -0
- package/npm/scaffold.mjs +179 -0
- package/npm/scenarios.mjs +93 -0
- package/npm/setup.mjs +69 -0
- package/npm/wizard.mjs +117 -0
- package/npm/zmr.mjs +23 -0
- package/package.json +114 -0
- 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/schemas/README.md +26 -0
- package/schemas/action-result.schema.json +27 -0
- package/schemas/capabilities-output.schema.json +98 -0
- package/schemas/devices-output.schema.json +25 -0
- package/schemas/doctor-output.schema.json +51 -0
- package/schemas/explain-output.schema.json +51 -0
- package/schemas/import-output.schema.json +23 -0
- package/schemas/init-output.schema.json +71 -0
- package/schemas/json-rpc.schema.json +55 -0
- package/schemas/release-manifest.schema.json +43 -0
- package/schemas/release-readiness-output.schema.json +127 -0
- package/schemas/run-output.schema.json +43 -0
- package/schemas/scenario.schema.json +128 -0
- package/schemas/schemas-output.schema.json +26 -0
- package/schemas/semantic-snapshot.schema.json +116 -0
- package/schemas/snapshot.schema.json +60 -0
- package/schemas/trace-event.schema.json +14 -0
- package/schemas/trace-manifest.schema.json +59 -0
- package/schemas/validate-output.schema.json +42 -0
- package/schemas/version-output.schema.json +23 -0
- package/schemas/zmr-config.schema.json +75 -0
- package/scripts/android-emulator.sh +126 -0
- package/scripts/assert-ios-physical-ready.sh +213 -0
- package/scripts/benchmark-command.sh +307 -0
- package/scripts/benchmark.sh +359 -0
- package/scripts/benchmark_gate.py +117 -0
- package/scripts/benchmark_result_row.py +88 -0
- package/scripts/compare-benchmarks.py +288 -0
- package/scripts/create-android-demo-app.sh +342 -0
- package/scripts/create-ios-demo-app.sh +261 -0
- package/scripts/demo-android-real.sh +232 -0
- package/scripts/demo-ios-real.sh +270 -0
- package/scripts/demo.sh +464 -0
- package/scripts/device-matrix.sh +338 -0
- package/scripts/ensure-ios-shim-target.rb +237 -0
- package/scripts/install-android-shim.sh +281 -0
- package/scripts/install-ios-shim.sh +589 -0
- package/scripts/pilot-gate.sh +560 -0
- package/scripts/release-readiness.py +838 -0
- package/scripts/release-readiness.sh +91 -0
- package/scripts/run-android-pilot.sh +561 -0
- package/scripts/run-ios-pilot.sh +509 -0
- package/shims/android/README.md +21 -0
- package/shims/android/ZMRShimInstrumentedTest.java +152 -0
- package/shims/android/protocol.md +18 -0
- package/shims/ios/README.md +50 -0
- package/shims/ios/ZMRShim.swift +110 -0
- package/shims/ios/ZMRShimUITestCase.swift +475 -0
- package/shims/ios/protocol.md +74 -0
- package/skills/zmr-mobile-testing/SKILL.md +127 -0
- package/src/android.zig +344 -0
- package/src/android_device_info.zig +99 -0
- package/src/android_emulator.zig +154 -0
- package/src/android_screen_recording.zig +112 -0
- package/src/android_shell.zig +112 -0
- package/src/bundle.zig +124 -0
- package/src/bundle_redaction.zig +272 -0
- package/src/bundle_tar.zig +123 -0
- package/src/cli_devices.zig +97 -0
- package/src/cli_doctor.zig +114 -0
- package/src/cli_import.zig +70 -0
- package/src/cli_info.zig +39 -0
- package/src/cli_init.zig +72 -0
- package/src/cli_output.zig +467 -0
- package/src/cli_run.zig +259 -0
- package/src/cli_serve.zig +287 -0
- package/src/cli_trace.zig +111 -0
- package/src/cli_validate.zig +41 -0
- package/src/command.zig +211 -0
- package/src/config.zig +305 -0
- package/src/config_diagnostics.zig +212 -0
- package/src/config_paths.zig +49 -0
- package/src/device_registry.zig +37 -0
- package/src/doctor.zig +412 -0
- package/src/doctor_hints.zig +52 -0
- package/src/errors.zig +55 -0
- package/src/fake_device.zig +163 -0
- package/src/health.zig +28 -0
- package/src/importer.zig +343 -0
- package/src/importer_json.zig +100 -0
- package/src/importer_model.zig +103 -0
- package/src/ios.zig +399 -0
- package/src/ios_devices.zig +219 -0
- package/src/ios_lifecycle.zig +72 -0
- package/src/ios_shim.zig +242 -0
- package/src/ios_snapshot.zig +20 -0
- package/src/json_fields.zig +80 -0
- package/src/json_rpc.zig +150 -0
- package/src/json_rpc_methods.zig +318 -0
- package/src/json_rpc_observation.zig +31 -0
- package/src/json_rpc_params.zig +52 -0
- package/src/json_rpc_protocol.zig +110 -0
- package/src/json_rpc_trace.zig +73 -0
- package/src/main.zig +135 -0
- package/src/mcp.zig +234 -0
- package/src/mcp_protocol.zig +64 -0
- package/src/mcp_trace.zig +83 -0
- package/src/report.zig +346 -0
- package/src/report_html.zig +63 -0
- package/src/report_values.zig +27 -0
- package/src/run_options.zig +152 -0
- package/src/runner.zig +280 -0
- package/src/runner_actions.zig +109 -0
- package/src/runner_config.zig +6 -0
- package/src/runner_diagnostics.zig +268 -0
- package/src/runner_events.zig +170 -0
- package/src/runner_native.zig +88 -0
- package/src/runner_waits.zig +300 -0
- package/src/scaffold.zig +472 -0
- package/src/scenario.zig +346 -0
- package/src/scenario_fields.zig +50 -0
- package/src/schema_registry.zig +53 -0
- package/src/selector.zig +84 -0
- package/src/semantic.zig +171 -0
- package/src/trace.zig +315 -0
- package/src/trace_json.zig +340 -0
- package/src/trace_summary.zig +218 -0
- package/src/trace_summary_diagnostic.zig +202 -0
- package/src/types.zig +120 -0
- package/src/uiautomator.zig +164 -0
- package/src/validation.zig +187 -0
- package/src/version.zig +22 -0
- package/viewer/app.js +373 -0
- package/viewer/index.html +126 -0
- package/viewer/parser.js +233 -0
- 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
|
+
}
|