zeno-mobile-runner 0.1.3 → 0.2.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 +192 -2
- package/FEATURES.md +50 -7
- package/README.md +168 -120
- package/build.zig.zon +3 -3
- package/clients/README.md +60 -3
- package/clients/go/README.md +12 -0
- package/clients/go/zmr/client.go +142 -0
- package/clients/kotlin/README.md +18 -1
- package/clients/kotlin/build.gradle.kts +1 -1
- package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +76 -1
- package/clients/python/README.md +19 -0
- package/clients/python/pyproject.toml +1 -1
- package/clients/python/zmr_client.py +33 -0
- package/clients/rust/Cargo.lock +1 -1
- package/clients/rust/Cargo.toml +1 -1
- package/clients/rust/README.md +25 -1
- package/clients/rust/src/lib.rs +201 -0
- package/clients/swift/README.md +18 -0
- package/clients/swift/Sources/ZMRClient/ZMRClient.swift +82 -0
- package/clients/typescript/README.md +16 -0
- package/clients/typescript/index.d.ts +12 -0
- package/clients/typescript/index.mjs +16 -0
- package/clients/typescript/package.json +1 -1
- package/docs/agent-discovery.md +151 -22
- package/docs/ai-agents.md +99 -11
- package/docs/benchmarking.md +49 -3
- package/docs/benchmarks/2026-06-09-android-workflow.md +73 -0
- package/docs/benchmarks/2026-06-09-android-workflow.results.jsonl +20 -0
- package/docs/benchmarks/2026-06-09-framework-baseline-status.md +32 -0
- package/docs/benchmarks/2026-06-09-ios-appium-comparison.md +115 -0
- package/docs/benchmarks/2026-06-09-ios-appium-comparison.results.jsonl +40 -0
- package/docs/benchmarks/2026-06-09-ios-demo.md +90 -0
- package/docs/benchmarks/2026-06-09-ios-demo.results.jsonl +20 -0
- package/docs/benchmarks/2026-06-09-ios-maestro-comparison.md +128 -0
- package/docs/benchmarks/2026-06-09-ios-maestro-comparison.results.jsonl +40 -0
- package/docs/benchmarks/2026-06-09-ios-workflow-comparison.md +143 -0
- package/docs/benchmarks/2026-06-09-ios-workflow-comparison.results.jsonl +40 -0
- package/docs/benchmarks/2026-06-09-ios-xctest-floor.md +106 -0
- package/docs/benchmarks/2026-06-09-ios-xctest-floor.results.jsonl +40 -0
- package/docs/benchmarks/README.md +36 -0
- package/docs/benchmarks/benchmark-lab-v1.json +155 -0
- package/docs/benchmarks/benchmark-lab-v1.md +95 -0
- package/docs/clients.md +26 -6
- package/docs/demo.md +40 -1
- package/docs/expo-smoke.md +8 -8
- package/docs/frameworks.md +10 -0
- package/docs/install.md +3 -2
- package/docs/npm.md +100 -4
- package/docs/production-readiness.md +123 -0
- package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
- package/docs/protocol.md +215 -16
- package/docs/scenario-authoring.md +18 -0
- package/docs/trace-privacy.md +9 -0
- package/docs/troubleshooting.md +7 -1
- package/examples/android-workflow.json +79 -0
- package/examples/ios-shim-workflow.json +79 -0
- package/examples/react-native-expo-workflow.json +75 -0
- package/npm/agents.mjs +16 -0
- package/npm/commands.mjs +9 -5
- package/package.json +6 -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/schemas/README.md +4 -0
- package/schemas/discover-output.schema.json +83 -0
- package/schemas/draft-output.schema.json +58 -0
- package/schemas/explore-output.schema.json +94 -0
- package/schemas/inspect-output.schema.json +88 -0
- package/schemas/run-output.schema.json +2 -0
- package/scripts/benchmark-lab.py +253 -0
- package/scripts/create-android-demo-app.sh +324 -29
- package/scripts/create-ios-demo-app.sh +174 -7
- package/scripts/create-react-native-expo-demo-app.sh +727 -0
- package/scripts/demo.sh +3 -0
- package/scripts/install-ios-shim.sh +2 -2
- package/scripts/release-readiness.py +43 -0
- package/scripts/run-android-pilot.sh +35 -9
- package/scripts/run-ios-pilot.sh +11 -4
- package/shims/ios/ZMRShim.swift +10 -0
- package/shims/ios/ZMRShimUITestCase.swift +42 -0
- package/shims/ios/protocol.md +1 -0
- package/skills/zmr-mobile-testing/SKILL.md +28 -3
- package/src/cli_discover.zig +239 -0
- package/src/cli_draft.zig +924 -0
- package/src/cli_explore.zig +136 -0
- package/src/cli_import.zig +31 -15
- package/src/cli_inspect.zig +310 -0
- package/src/cli_output.zig +26 -2
- package/src/cli_run.zig +28 -0
- package/src/cli_trace.zig +45 -15
- package/src/cli_validate.zig +12 -6
- package/src/errors.zig +9 -0
- package/src/ios.zig +49 -12
- package/src/ios_shim.zig +36 -2
- package/src/json_rpc_methods.zig +85 -11
- package/src/json_rpc_params.zig +8 -0
- package/src/json_rpc_protocol.zig +1 -1
- package/src/json_rpc_trace.zig +112 -0
- package/src/main.zig +27 -2
- package/src/mcp.zig +209 -6
- package/src/mcp_protocol.zig +29 -1
- package/src/mcp_trace.zig +126 -4
- package/src/report.zig +186 -0
- package/src/runner.zig +26 -4
- package/src/runner_actions.zig +10 -0
- package/src/runner_diagnostics.zig +31 -1
- package/src/runner_events.zig +70 -7
- package/src/runner_native.zig +17 -1
- package/src/runner_waits.zig +82 -19
- package/src/scaffold.zig +28 -12
- package/src/scenario.zig +32 -4
- package/src/schema_registry.zig +4 -0
- package/src/version.zig +1 -1
- package/viewer/app.js +23 -3
package/src/cli_trace.zig
CHANGED
|
@@ -6,6 +6,7 @@ const report = @import("report.zig");
|
|
|
6
6
|
pub const ReportArgs = struct {
|
|
7
7
|
input_path: []const u8,
|
|
8
8
|
out_path: ?[]const u8 = null,
|
|
9
|
+
junit_path: ?[]const u8 = null,
|
|
9
10
|
};
|
|
10
11
|
|
|
11
12
|
pub const ExplainArgs = struct {
|
|
@@ -21,21 +22,34 @@ pub const ExportArgs = struct {
|
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
pub fn parseReportArgs(args: []const []const u8) !ReportArgs {
|
|
24
|
-
|
|
25
|
-
var
|
|
25
|
+
var input_path: ?[]const u8 = null;
|
|
26
|
+
var out_path: ?[]const u8 = null;
|
|
27
|
+
var junit_path: ?[]const u8 = null;
|
|
26
28
|
|
|
27
|
-
var index: usize =
|
|
29
|
+
var index: usize = 0;
|
|
28
30
|
while (index < args.len) : (index += 1) {
|
|
29
31
|
const arg = args[index];
|
|
30
32
|
if (std.mem.eql(u8, arg, "--out")) {
|
|
31
33
|
index += 1;
|
|
32
|
-
|
|
34
|
+
out_path = if (index < args.len) args[index] else return error.MissingReportOutput;
|
|
35
|
+
} else if (std.mem.eql(u8, arg, "--junit")) {
|
|
36
|
+
index += 1;
|
|
37
|
+
junit_path = if (index < args.len) args[index] else return error.MissingJUnitOutput;
|
|
38
|
+
} else if (std.mem.startsWith(u8, arg, "--")) {
|
|
39
|
+
return error.UnknownFlag;
|
|
40
|
+
} else if (input_path == null) {
|
|
41
|
+
input_path = arg;
|
|
33
42
|
} else {
|
|
34
43
|
return error.UnknownFlag;
|
|
35
44
|
}
|
|
36
45
|
}
|
|
37
|
-
if (
|
|
38
|
-
return
|
|
46
|
+
if (input_path == null) return error.MissingReportInput;
|
|
47
|
+
if (out_path == null) return error.MissingReportOutput;
|
|
48
|
+
return ReportArgs{
|
|
49
|
+
.input_path = input_path.?,
|
|
50
|
+
.out_path = out_path,
|
|
51
|
+
.junit_path = junit_path,
|
|
52
|
+
};
|
|
39
53
|
}
|
|
40
54
|
|
|
41
55
|
pub fn parseExplainArgs(args: []const []const u8) !ExplainArgs {
|
|
@@ -54,26 +68,38 @@ pub fn parseExplainArgs(args: []const []const u8) !ExplainArgs {
|
|
|
54
68
|
}
|
|
55
69
|
|
|
56
70
|
pub fn parseExportArgs(args: []const []const u8) !ExportArgs {
|
|
57
|
-
|
|
58
|
-
var
|
|
71
|
+
var trace_dir: ?[]const u8 = null;
|
|
72
|
+
var out_path: ?[]const u8 = null;
|
|
73
|
+
var redact = false;
|
|
74
|
+
var omit_screenshots = false;
|
|
59
75
|
|
|
60
|
-
var index: usize =
|
|
76
|
+
var index: usize = 0;
|
|
61
77
|
while (index < args.len) : (index += 1) {
|
|
62
78
|
const arg = args[index];
|
|
63
79
|
if (std.mem.eql(u8, arg, "--out")) {
|
|
64
80
|
index += 1;
|
|
65
|
-
|
|
81
|
+
out_path = if (index < args.len) args[index] else return error.MissingTraceBundleOutput;
|
|
66
82
|
} else if (std.mem.eql(u8, arg, "--redact")) {
|
|
67
|
-
|
|
83
|
+
redact = true;
|
|
68
84
|
} else if (std.mem.eql(u8, arg, "--omit-screenshots")) {
|
|
69
|
-
|
|
70
|
-
|
|
85
|
+
redact = true;
|
|
86
|
+
omit_screenshots = true;
|
|
87
|
+
} else if (std.mem.startsWith(u8, arg, "--")) {
|
|
88
|
+
return error.UnknownFlag;
|
|
89
|
+
} else if (trace_dir == null) {
|
|
90
|
+
trace_dir = arg;
|
|
71
91
|
} else {
|
|
72
92
|
return error.UnknownFlag;
|
|
73
93
|
}
|
|
74
94
|
}
|
|
75
|
-
if (
|
|
76
|
-
return
|
|
95
|
+
if (trace_dir == null) return error.MissingTraceDir;
|
|
96
|
+
if (out_path == null) return error.MissingTraceBundleOutput;
|
|
97
|
+
return ExportArgs{
|
|
98
|
+
.trace_dir = trace_dir.?,
|
|
99
|
+
.out_path = out_path,
|
|
100
|
+
.redact = redact,
|
|
101
|
+
.omit_screenshots = omit_screenshots,
|
|
102
|
+
};
|
|
77
103
|
}
|
|
78
104
|
|
|
79
105
|
pub fn runReport(allocator: std.mem.Allocator, args: *std.process.ArgIterator) !void {
|
|
@@ -84,6 +110,10 @@ pub fn runReport(allocator: std.mem.Allocator, args: *std.process.ArgIterator) !
|
|
|
84
110
|
const parsed = try parseReportArgs(raw_args.items);
|
|
85
111
|
try report.writeHtmlReport(allocator, parsed.input_path, parsed.out_path.?);
|
|
86
112
|
try std.fs.File.stdout().deprecatedWriter().print("wrote {s}\n", .{parsed.out_path.?});
|
|
113
|
+
if (parsed.junit_path) |junit_path| {
|
|
114
|
+
try report.writeJUnitReport(allocator, parsed.input_path, junit_path);
|
|
115
|
+
try std.fs.File.stdout().deprecatedWriter().print("wrote {s}\n", .{junit_path});
|
|
116
|
+
}
|
|
87
117
|
}
|
|
88
118
|
|
|
89
119
|
pub fn runExplain(allocator: std.mem.Allocator, args: *std.process.ArgIterator) !void {
|
package/src/cli_validate.zig
CHANGED
|
@@ -9,17 +9,23 @@ pub const ParsedArgs = struct {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
pub fn parseArgs(args: []const []const u8) !ParsedArgs {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
for (args[1..]) |arg| {
|
|
12
|
+
var path: ?[]const u8 = null;
|
|
13
|
+
var json = false;
|
|
14
|
+
for (args) |arg| {
|
|
16
15
|
if (std.mem.eql(u8, arg, "--json")) {
|
|
17
|
-
|
|
16
|
+
json = true;
|
|
17
|
+
} else if (std.mem.startsWith(u8, arg, "--")) {
|
|
18
|
+
return error.UnknownFlag;
|
|
19
|
+
} else if (path == null) {
|
|
20
|
+
path = arg;
|
|
18
21
|
} else {
|
|
19
22
|
return error.UnknownFlag;
|
|
20
23
|
}
|
|
21
24
|
}
|
|
22
|
-
return
|
|
25
|
+
return ParsedArgs{
|
|
26
|
+
.path = path orelse return error.MissingScenarioPath,
|
|
27
|
+
.json = json,
|
|
28
|
+
};
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
pub fn run(allocator: std.mem.Allocator, args: *std.process.ArgIterator) !void {
|
package/src/errors.zig
CHANGED
|
@@ -10,6 +10,12 @@ pub fn classify(err: anyerror) PublicError {
|
|
|
10
10
|
error.MissingScenarioPath => .{ .code = "cli.missing_scenario", .message = "missing scenario path" },
|
|
11
11
|
error.MissingDeviceSerial => .{ .code = "cli.missing_device", .message = "missing device serial" },
|
|
12
12
|
error.MissingTraceDir => .{ .code = "cli.missing_trace_dir", .message = "missing trace directory" },
|
|
13
|
+
error.MissingReportInput => .{ .code = "cli.missing_report_input", .message = "missing report input path" },
|
|
14
|
+
error.MissingReportOutput => .{ .code = "cli.missing_report_output", .message = "missing report output path" },
|
|
15
|
+
error.MissingDraftOut => .{ .code = "cli.missing_draft_out", .message = "missing draft output path" },
|
|
16
|
+
error.MissingDiscoverOut => .{ .code = "cli.missing_discover_out", .message = "missing discover output path" },
|
|
17
|
+
error.MissingJUnitOutput => .{ .code = "cli.missing_junit_output", .message = "missing JUnit output path" },
|
|
18
|
+
error.MissingTraceBundleOutput => .{ .code = "cli.missing_trace_bundle_output", .message = "missing trace bundle output path" },
|
|
13
19
|
error.MissingAppId => .{ .code = "cli.missing_app_id", .message = "missing app id" },
|
|
14
20
|
error.MissingAdbPath => .{ .code = "cli.missing_adb_path", .message = "missing adb path" },
|
|
15
21
|
error.MissingXcrunPath => .{ .code = "cli.missing_xcrun_path", .message = "missing xcrun path" },
|
|
@@ -19,6 +25,9 @@ pub fn classify(err: anyerror) PublicError {
|
|
|
19
25
|
error.UnknownFlag => .{ .code = "cli.unknown_flag", .message = "unknown flag" },
|
|
20
26
|
error.MissingParam => .{ .code = "cli.missing_param", .message = "missing parameter" },
|
|
21
27
|
error.FileNotFound => .{ .code = "scenario.file_not_found", .message = "scenario file was not found" },
|
|
28
|
+
error.SemanticSnapshotMissing => .{ .code = "draft.no_snapshot", .message = "no semantic snapshot was found in the trace" },
|
|
29
|
+
error.InvalidTraceManifest => .{ .code = "trace.invalid_manifest", .message = "trace manifest is invalid" },
|
|
30
|
+
error.InvalidSemanticSnapshot => .{ .code = "trace.invalid_semantic_snapshot", .message = "semantic snapshot is invalid" },
|
|
22
31
|
error.UnsupportedPlatform => .{ .code = "cli.unsupported_platform", .message = "unsupported platform" },
|
|
23
32
|
error.UnsupportedTransport => .{ .code = "cli.unsupported_transport", .message = "unsupported transport" },
|
|
24
33
|
error.ScenarioMustBeObject,
|
package/src/ios.zig
CHANGED
|
@@ -10,8 +10,10 @@ 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
|
-
|
|
13
|
+
// dependency build before it can answer the first selector query. Slower CI
|
|
14
|
+
// hardware can override the default with ZMR_IOS_SHIM_TIMEOUT_MS.
|
|
15
|
+
const default_shim_timeout_ms = 5_400_000;
|
|
16
|
+
const shim_timeout_env = "ZMR_IOS_SHIM_TIMEOUT_MS";
|
|
15
17
|
const shim_best_effort_timeout_ms = 10_000;
|
|
16
18
|
const shim_command_attempts = 2;
|
|
17
19
|
const shim_bootstrap_retry_delay_ms = 500;
|
|
@@ -126,7 +128,9 @@ pub const IosDevice = struct {
|
|
|
126
128
|
const result = try self.runSimctl(&.{ "openurl", self.target(), url }, default_max_output);
|
|
127
129
|
defer result.deinit(self.allocator);
|
|
128
130
|
try result.ensureSuccess();
|
|
129
|
-
|
|
131
|
+
if (urlMayNeedOpenConfirmation(url)) {
|
|
132
|
+
self.acceptOpenURLConfirmationBestEffort();
|
|
133
|
+
}
|
|
130
134
|
}
|
|
131
135
|
|
|
132
136
|
pub fn tap(self: *IosDevice, x: i32, y: i32) !void {
|
|
@@ -212,15 +216,22 @@ pub const IosDevice = struct {
|
|
|
212
216
|
|
|
213
217
|
const active_package = try self.allocator.dupe(u8, self.app_id);
|
|
214
218
|
errdefer self.allocator.free(active_package);
|
|
215
|
-
|
|
216
|
-
|
|
219
|
+
var shim_viewport: ?types.Viewport = null;
|
|
220
|
+
const nodes = if (self.shim_path != null) blk: {
|
|
221
|
+
const shim_snapshot = self.snapshotFromShim() catch |err| {
|
|
217
222
|
if (screenshot_artifact == null) return err;
|
|
218
223
|
if (writer) |tw| try self.recordSnapshotSemanticFailure(tw, screenshot_artifact.?, err);
|
|
219
224
|
break :blk try self.allocator.alloc(types.UiNode, 0);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
225
|
+
};
|
|
226
|
+
shim_viewport = shim_snapshot.viewport;
|
|
227
|
+
break :blk shim_snapshot.nodes;
|
|
228
|
+
} else try self.allocator.alloc(types.UiNode, 0);
|
|
223
229
|
errdefer self.allocator.free(nodes);
|
|
230
|
+
if (shim_viewport) |value| {
|
|
231
|
+
if (value.width > 0 and value.height > 0) {
|
|
232
|
+
viewport = value;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
224
235
|
|
|
225
236
|
return .{
|
|
226
237
|
.id = id,
|
|
@@ -260,10 +271,10 @@ pub const IosDevice = struct {
|
|
|
260
271
|
return try self.allocator.dupe(u8, result.stdout);
|
|
261
272
|
}
|
|
262
273
|
|
|
263
|
-
fn
|
|
274
|
+
fn snapshotFromShim(self: *IosDevice) !ios_shim.SnapshotResponse {
|
|
264
275
|
const response = try self.runShim(.{ .kind = .snapshot });
|
|
265
276
|
defer self.allocator.free(response);
|
|
266
|
-
return try ios_shim.
|
|
277
|
+
return try ios_shim.parseSnapshotResponse(self.allocator, response);
|
|
267
278
|
}
|
|
268
279
|
|
|
269
280
|
fn recordSnapshotSemanticFailure(self: *IosDevice, writer: *trace.TraceWriter, screenshot_artifact: []const u8, err: anyerror) !void {
|
|
@@ -319,7 +330,7 @@ pub const IosDevice = struct {
|
|
|
319
330
|
}
|
|
320
331
|
|
|
321
332
|
fn runShim(self: *IosDevice, shim_command: ios_shim.Command) ![]u8 {
|
|
322
|
-
return self.runShimWithTimeout(shim_command,
|
|
333
|
+
return self.runShimWithTimeout(shim_command, shimTimeoutMs());
|
|
323
334
|
}
|
|
324
335
|
|
|
325
336
|
fn runShimWithTimeout(self: *IosDevice, shim_command: ios_shim.Command, timeout_ms: u64) ![]u8 {
|
|
@@ -384,6 +395,25 @@ fn isTransientShimBootstrapFailure(result: command.ExecResult) bool {
|
|
|
384
395
|
std.mem.indexOf(u8, result.stderr, "operation never finished bootstrapping") != null;
|
|
385
396
|
}
|
|
386
397
|
|
|
398
|
+
fn shimTimeoutMs() u64 {
|
|
399
|
+
return parseShimTimeoutMs(std.posix.getenv(shim_timeout_env));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
fn parseShimTimeoutMs(raw: ?[]const u8) u64 {
|
|
403
|
+
const value = raw orelse return default_shim_timeout_ms;
|
|
404
|
+
const parsed = std.fmt.parseInt(u64, value, 10) catch return default_shim_timeout_ms;
|
|
405
|
+
if (parsed == 0) return default_shim_timeout_ms;
|
|
406
|
+
return parsed;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
fn urlMayNeedOpenConfirmation(url: []const u8) bool {
|
|
410
|
+
return startsWithIgnoreCase(url, "http://") or startsWithIgnoreCase(url, "https://");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
fn startsWithIgnoreCase(value: []const u8, prefix: []const u8) bool {
|
|
414
|
+
return value.len >= prefix.len and std.ascii.eqlIgnoreCase(value[0..prefix.len], prefix);
|
|
415
|
+
}
|
|
416
|
+
|
|
387
417
|
pub fn listDevices(allocator: std.mem.Allocator, xcrun_path: []const u8) ![]types.DeviceInfo {
|
|
388
418
|
return try ios_devices.listSimulators(allocator, xcrun_path);
|
|
389
419
|
}
|
|
@@ -401,6 +431,13 @@ pub fn parsePhysicalDevicesJson(allocator: std.mem.Allocator, content: []const u
|
|
|
401
431
|
}
|
|
402
432
|
|
|
403
433
|
test "ios xctest shim timeout allows cold xcodebuild startup" {
|
|
404
|
-
try std.testing.expect(
|
|
434
|
+
try std.testing.expect(default_shim_timeout_ms >= 5_400_000);
|
|
405
435
|
try std.testing.expect(shim_best_effort_timeout_ms <= 15_000);
|
|
406
436
|
}
|
|
437
|
+
|
|
438
|
+
test "ios xctest shim timeout env override" {
|
|
439
|
+
try std.testing.expectEqual(@as(u64, default_shim_timeout_ms), parseShimTimeoutMs(null));
|
|
440
|
+
try std.testing.expectEqual(@as(u64, 600_000), parseShimTimeoutMs("600000"));
|
|
441
|
+
try std.testing.expectEqual(@as(u64, default_shim_timeout_ms), parseShimTimeoutMs("not-a-number"));
|
|
442
|
+
try std.testing.expectEqual(@as(u64, default_shim_timeout_ms), parseShimTimeoutMs("0"));
|
|
443
|
+
}
|
package/src/ios_shim.zig
CHANGED
|
@@ -32,6 +32,16 @@ pub const Command = struct {
|
|
|
32
32
|
max_chars: ?u32 = null,
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
+
pub const SnapshotResponse = struct {
|
|
36
|
+
nodes: []types.UiNode,
|
|
37
|
+
viewport: types.Viewport = .{},
|
|
38
|
+
|
|
39
|
+
pub fn deinit(self: SnapshotResponse, allocator: std.mem.Allocator) void {
|
|
40
|
+
for (self.nodes) |node| node.deinit(allocator);
|
|
41
|
+
allocator.free(self.nodes);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
35
45
|
pub fn writeCommandJson(writer: anytype, command: Command) !void {
|
|
36
46
|
try writer.writeAll("{\"cmd\":");
|
|
37
47
|
try trace.writeJsonString(writer, commandName(command.kind));
|
|
@@ -54,12 +64,13 @@ pub fn writeCommandJson(writer: anytype, command: Command) !void {
|
|
|
54
64
|
try writer.writeAll("}\n");
|
|
55
65
|
}
|
|
56
66
|
|
|
57
|
-
pub fn
|
|
67
|
+
pub fn parseSnapshotResponse(allocator: std.mem.Allocator, content: []const u8) !SnapshotResponse {
|
|
58
68
|
const parsed = try std.json.parseFromSlice(std.json.Value, allocator, content, .{});
|
|
59
69
|
defer parsed.deinit();
|
|
60
70
|
if (parsed.value != .object) return error.IosShimResponseMustBeObject;
|
|
61
71
|
const status = fieldString(parsed.value.object, "status") orelse return error.IosShimMissingStatus;
|
|
62
72
|
if (!std.mem.eql(u8, status, "ok")) return error.IosShimResponseNotOk;
|
|
73
|
+
const viewport = parseViewport(parsed.value.object.get("viewport"));
|
|
63
74
|
const nodes_value = parsed.value.object.get("nodes") orelse return error.IosShimMissingNodes;
|
|
64
75
|
if (nodes_value != .array) return error.IosShimNodesMustBeArray;
|
|
65
76
|
|
|
@@ -97,7 +108,15 @@ pub fn parseSnapshotNodes(allocator: std.mem.Allocator, content: []const u8) ![]
|
|
|
97
108
|
});
|
|
98
109
|
}
|
|
99
110
|
|
|
100
|
-
return
|
|
111
|
+
return .{
|
|
112
|
+
.nodes = try nodes.toOwnedSlice(allocator),
|
|
113
|
+
.viewport = viewport,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
pub fn parseSnapshotNodes(allocator: std.mem.Allocator, content: []const u8) ![]types.UiNode {
|
|
118
|
+
const snapshot = try parseSnapshotResponse(allocator, content);
|
|
119
|
+
return snapshot.nodes;
|
|
101
120
|
}
|
|
102
121
|
|
|
103
122
|
pub fn parseOkResponse(content: []const u8) !void {
|
|
@@ -278,6 +297,21 @@ fn intField(object: std.json.ObjectMap, key: []const u8) !i32 {
|
|
|
278
297
|
return @intCast(value.integer);
|
|
279
298
|
}
|
|
280
299
|
|
|
300
|
+
fn parseViewport(value: ?std.json.Value) types.Viewport {
|
|
301
|
+
const actual = value orelse return .{};
|
|
302
|
+
if (actual != .object) return .{};
|
|
303
|
+
const width = optionalU32Field(actual.object, "width") orelse return .{};
|
|
304
|
+
const height = optionalU32Field(actual.object, "height") orelse return .{};
|
|
305
|
+
return .{ .width = width, .height = height };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
fn optionalU32Field(object: std.json.ObjectMap, key: []const u8) ?u32 {
|
|
309
|
+
const value = object.get(key) orelse return null;
|
|
310
|
+
if (value != .integer) return null;
|
|
311
|
+
if (value.integer < 0 or value.integer > std.math.maxInt(u32)) return null;
|
|
312
|
+
return @intCast(value.integer);
|
|
313
|
+
}
|
|
314
|
+
|
|
281
315
|
fn dupeOptional(allocator: std.mem.Allocator, value: ?[]const u8) !?[]const u8 {
|
|
282
316
|
if (value) |actual| return try allocator.dupe(u8, actual);
|
|
283
317
|
return null;
|
package/src/json_rpc_methods.zig
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
const std = @import("std");
|
|
2
2
|
const bundle = @import("bundle.zig");
|
|
3
|
+
const cli_output = @import("cli_output.zig");
|
|
3
4
|
const observation = @import("json_rpc_observation.zig");
|
|
4
5
|
const params_parser = @import("json_rpc_params.zig");
|
|
5
6
|
const protocol = @import("json_rpc_protocol.zig");
|
|
6
7
|
const rpc_trace = @import("json_rpc_trace.zig");
|
|
7
8
|
const runner = @import("runner.zig");
|
|
9
|
+
const runner_events = @import("runner_events.zig");
|
|
8
10
|
const selector = @import("selector.zig");
|
|
9
11
|
const trace = @import("trace.zig");
|
|
12
|
+
const validation = @import("validation.zig");
|
|
10
13
|
|
|
11
14
|
pub fn dispatchMethod(
|
|
12
15
|
allocator: std.mem.Allocator,
|
|
@@ -18,11 +21,12 @@ pub fn dispatchMethod(
|
|
|
18
21
|
live_trace: ?*trace.TraceWriter,
|
|
19
22
|
) !void {
|
|
20
23
|
if (try dispatchCoreMethod(allocator, device, method, id, writer)) return;
|
|
21
|
-
if (try dispatchAppMethod(device, method, params, id, writer)) return;
|
|
24
|
+
if (try dispatchAppMethod(device, method, params, id, writer, live_trace)) return;
|
|
22
25
|
if (try dispatchObserveMethod(device, method, id, writer, live_trace)) return;
|
|
23
26
|
if (try dispatchUiMethod(allocator, device, method, params, id, writer, live_trace)) return;
|
|
24
27
|
if (try dispatchWaitMethod(allocator, device, method, params, id, writer, live_trace)) return;
|
|
25
28
|
if (try dispatchAssertMethod(allocator, device, method, params, id, writer, live_trace)) return;
|
|
29
|
+
if (try dispatchScenarioMethod(allocator, method, params, id, writer)) return;
|
|
26
30
|
if (try dispatchTraceMethod(allocator, method, params, id, writer, live_trace)) return;
|
|
27
31
|
|
|
28
32
|
try protocol.writeError(writer, id, -32601, "method not found");
|
|
@@ -65,37 +69,53 @@ fn dispatchAppMethod(
|
|
|
65
69
|
params: ?std.json.Value,
|
|
66
70
|
id: ?std.json.Value,
|
|
67
71
|
writer: anytype,
|
|
72
|
+
live_trace: ?*trace.TraceWriter,
|
|
68
73
|
) !bool {
|
|
69
74
|
if (std.mem.eql(u8, method, "app.install")) {
|
|
70
75
|
const path = try params_parser.requiredString(params, "path");
|
|
71
76
|
try device.install(path);
|
|
77
|
+
if (live_trace) |tw| try rpc_trace.recordSimplePayload(tw, "app.install", "path", path);
|
|
72
78
|
try protocol.writeResultRaw(writer, id, "true");
|
|
73
79
|
return true;
|
|
74
80
|
}
|
|
75
81
|
if (std.mem.eql(u8, method, "app.launch")) {
|
|
76
82
|
try device.launch();
|
|
83
|
+
if (live_trace) |tw| try tw.recordEvent("app.launch", "{\"status\":\"ok\"}");
|
|
77
84
|
try protocol.writeResultRaw(writer, id, "true");
|
|
78
85
|
return true;
|
|
79
86
|
}
|
|
80
87
|
if (std.mem.eql(u8, method, "app.stop")) {
|
|
81
88
|
try device.stop();
|
|
89
|
+
if (live_trace) |tw| try tw.recordEvent("app.stop", "{\"status\":\"ok\"}");
|
|
82
90
|
try protocol.writeResultRaw(writer, id, "true");
|
|
83
91
|
return true;
|
|
84
92
|
}
|
|
85
93
|
if (std.mem.eql(u8, method, "app.clearState")) {
|
|
86
94
|
try device.clearState();
|
|
95
|
+
if (live_trace) |tw| try tw.recordEvent("app.clearState", "{\"status\":\"ok\"}");
|
|
87
96
|
try protocol.writeResultRaw(writer, id, "true");
|
|
88
97
|
return true;
|
|
89
98
|
}
|
|
90
99
|
if (std.mem.eql(u8, method, "app.openLink")) {
|
|
91
100
|
const url = try params_parser.requiredString(params, "url");
|
|
92
101
|
try device.openLink(url);
|
|
102
|
+
if (live_trace) |tw| try recordAppOpenLink(tw, url);
|
|
93
103
|
try protocol.writeResultRaw(writer, id, "true");
|
|
94
104
|
return true;
|
|
95
105
|
}
|
|
96
106
|
return false;
|
|
97
107
|
}
|
|
98
108
|
|
|
109
|
+
fn recordAppOpenLink(tw: *trace.TraceWriter, url: []const u8) !void {
|
|
110
|
+
var payload = std.ArrayList(u8).empty;
|
|
111
|
+
defer payload.deinit(tw.allocator);
|
|
112
|
+
const writer = payload.writer(tw.allocator);
|
|
113
|
+
try writer.writeAll("{\"status\":\"ok\",\"url\":");
|
|
114
|
+
try trace.writeJsonString(writer, url);
|
|
115
|
+
try writer.writeAll("}");
|
|
116
|
+
try tw.recordEvent("app.openLink", payload.items);
|
|
117
|
+
}
|
|
118
|
+
|
|
99
119
|
fn dispatchObserveMethod(
|
|
100
120
|
device: anytype,
|
|
101
121
|
method: []const u8,
|
|
@@ -175,14 +195,13 @@ fn dispatchUiMethod(
|
|
|
175
195
|
return true;
|
|
176
196
|
}
|
|
177
197
|
if (std.mem.eql(u8, method, "ui.swipe")) {
|
|
178
|
-
try
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
);
|
|
185
|
-
if (live_trace) |tw| try tw.recordEvent("ui.swipe", "{\"status\":\"ok\"}");
|
|
198
|
+
const x1 = try params_parser.requiredI32(params, "x1");
|
|
199
|
+
const y1 = try params_parser.requiredI32(params, "y1");
|
|
200
|
+
const x2 = try params_parser.requiredI32(params, "x2");
|
|
201
|
+
const y2 = try params_parser.requiredI32(params, "y2");
|
|
202
|
+
const duration_ms = @as(u32, @intCast(try params_parser.optionalU64(params, "durationMs", 300)));
|
|
203
|
+
try device.swipe(x1, y1, x2, y2, duration_ms);
|
|
204
|
+
if (live_trace) |tw| try runner_events.recordSwipe(tw, x1, y1, x2, y2, duration_ms);
|
|
186
205
|
try protocol.writeResultRaw(writer, id, "true");
|
|
187
206
|
return true;
|
|
188
207
|
}
|
|
@@ -263,14 +282,14 @@ fn dispatchAssertMethod(
|
|
|
263
282
|
if (std.mem.eql(u8, method, "assert.visible")) {
|
|
264
283
|
const wanted = try params_parser.selectorParam(allocator, params);
|
|
265
284
|
defer wanted.deinit(allocator);
|
|
266
|
-
if (!try runner.
|
|
285
|
+
if (!try runner.assertVisible(device, wanted, try params_parser.optionalU64(params, "timeoutMs", 5000), live_trace, .{})) return error.AssertionFailed;
|
|
267
286
|
try protocol.writeResultRaw(writer, id, "true");
|
|
268
287
|
return true;
|
|
269
288
|
}
|
|
270
289
|
if (std.mem.eql(u8, method, "assert.notVisible")) {
|
|
271
290
|
const wanted = try params_parser.selectorParam(allocator, params);
|
|
272
291
|
defer wanted.deinit(allocator);
|
|
273
|
-
if (!try runner.
|
|
292
|
+
if (!try runner.assertNotVisible(device, wanted, try params_parser.optionalU64(params, "timeoutMs", 5000), live_trace, .{})) return error.AssertionFailed;
|
|
274
293
|
try protocol.writeResultRaw(writer, id, "true");
|
|
275
294
|
return true;
|
|
276
295
|
}
|
|
@@ -282,6 +301,26 @@ fn dispatchAssertMethod(
|
|
|
282
301
|
return false;
|
|
283
302
|
}
|
|
284
303
|
|
|
304
|
+
fn dispatchScenarioMethod(
|
|
305
|
+
allocator: std.mem.Allocator,
|
|
306
|
+
method: []const u8,
|
|
307
|
+
params: ?std.json.Value,
|
|
308
|
+
id: ?std.json.Value,
|
|
309
|
+
writer: anytype,
|
|
310
|
+
) !bool {
|
|
311
|
+
if (std.mem.eql(u8, method, "scenario.validate")) {
|
|
312
|
+
const path = try params_parser.requiredString(params, "path");
|
|
313
|
+
var result = try validation.validateFile(allocator, path);
|
|
314
|
+
defer result.deinit(allocator);
|
|
315
|
+
var payload = std.ArrayList(u8).empty;
|
|
316
|
+
defer payload.deinit(allocator);
|
|
317
|
+
try cli_output.writeValidationJson(payload.writer(allocator), path, result);
|
|
318
|
+
try protocol.writeResultRaw(writer, id, std.mem.trimRight(u8, payload.items, " \t\r\n"));
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
|
|
285
324
|
fn dispatchTraceMethod(
|
|
286
325
|
allocator: std.mem.Allocator,
|
|
287
326
|
method: []const u8,
|
|
@@ -296,6 +335,41 @@ fn dispatchTraceMethod(
|
|
|
296
335
|
try rpc_trace.writeEventsResult(allocator, writer, id, live_trace, after_seq, limit);
|
|
297
336
|
return true;
|
|
298
337
|
}
|
|
338
|
+
if (std.mem.eql(u8, method, "trace.discover")) {
|
|
339
|
+
try rpc_trace.writeDiscoverResult(
|
|
340
|
+
allocator,
|
|
341
|
+
writer,
|
|
342
|
+
id,
|
|
343
|
+
live_trace,
|
|
344
|
+
try params_parser.requiredString(params, "out"),
|
|
345
|
+
try params_parser.optionalBool(params, "includeActions", false),
|
|
346
|
+
try params_parser.optionalBool(params, "validate", false),
|
|
347
|
+
try params_parser.optionalBool(params, "force", false),
|
|
348
|
+
try params_parser.optionalString(params, "name"),
|
|
349
|
+
try params_parser.optionalString(params, "appId"),
|
|
350
|
+
);
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
if (std.mem.eql(u8, method, "trace.explore")) {
|
|
354
|
+
try rpc_trace.writeExploreResult(
|
|
355
|
+
allocator,
|
|
356
|
+
writer,
|
|
357
|
+
id,
|
|
358
|
+
live_trace,
|
|
359
|
+
try params_parser.requiredString(params, "out"),
|
|
360
|
+
try params_parser.requiredString(params, "goal"),
|
|
361
|
+
try params_parser.optionalBool(params, "includeActions", false),
|
|
362
|
+
try params_parser.optionalBool(params, "validate", false),
|
|
363
|
+
try params_parser.optionalBool(params, "force", false),
|
|
364
|
+
try params_parser.optionalString(params, "name"),
|
|
365
|
+
try params_parser.optionalString(params, "appId"),
|
|
366
|
+
);
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
if (std.mem.eql(u8, method, "trace.explain")) {
|
|
370
|
+
try rpc_trace.writeExplainResult(allocator, writer, id, live_trace);
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
299
373
|
if (std.mem.eql(u8, method, "trace.export")) {
|
|
300
374
|
const tw = live_trace orelse {
|
|
301
375
|
try protocol.writeTraceDisabledResult(writer, id);
|
package/src/json_rpc_params.zig
CHANGED
|
@@ -31,6 +31,14 @@ pub fn requiredString(params: ?std.json.Value, key: []const u8) ![]const u8 {
|
|
|
31
31
|
return try json_fields.requiredString(params, key, error.MissingParam, error.ParamMustBeString);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
pub fn optionalString(params: ?std.json.Value, key: []const u8) !?[]const u8 {
|
|
35
|
+
const value = field(params, key) orelse return null;
|
|
36
|
+
return switch (value) {
|
|
37
|
+
.string => |actual| actual,
|
|
38
|
+
else => error.ParamMustBeString,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
34
42
|
pub fn requiredI32(params: ?std.json.Value, key: []const u8) !i32 {
|
|
35
43
|
return try json_fields.requiredI32(params, key, error.MissingParam, error.ParamMustBeInteger);
|
|
36
44
|
}
|
|
@@ -11,7 +11,7 @@ pub const capabilities_json =
|
|
|
11
11
|
"\",\"minimumCompatibleVersion\":\"" ++ version.protocol_min_compatible_version ++
|
|
12
12
|
"\",\"stability\":\"" ++ version.protocol_stability ++
|
|
13
13
|
"\",\"breakingChangePolicy\":\"" ++ version.protocol_breaking_change_policy ++
|
|
14
|
-
"\"},\"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\"]}";
|
|
14
|
+
"\"},\"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\",\"scenario.validate\",\"trace.events\",\"trace.explore\",\"trace.discover\",\"trace.explain\",\"trace.export\"]}";
|
|
15
15
|
|
|
16
16
|
pub fn writeCapabilitiesResult(writer: anytype, id: ?std.json.Value) !void {
|
|
17
17
|
try writeResultRaw(writer, id, capabilities_json);
|