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.
Files changed (115) hide show
  1. package/CHANGELOG.md +192 -2
  2. package/FEATURES.md +50 -7
  3. package/README.md +168 -120
  4. package/build.zig.zon +3 -3
  5. package/clients/README.md +60 -3
  6. package/clients/go/README.md +12 -0
  7. package/clients/go/zmr/client.go +142 -0
  8. package/clients/kotlin/README.md +18 -1
  9. package/clients/kotlin/build.gradle.kts +1 -1
  10. package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +76 -1
  11. package/clients/python/README.md +19 -0
  12. package/clients/python/pyproject.toml +1 -1
  13. package/clients/python/zmr_client.py +33 -0
  14. package/clients/rust/Cargo.lock +1 -1
  15. package/clients/rust/Cargo.toml +1 -1
  16. package/clients/rust/README.md +25 -1
  17. package/clients/rust/src/lib.rs +201 -0
  18. package/clients/swift/README.md +18 -0
  19. package/clients/swift/Sources/ZMRClient/ZMRClient.swift +82 -0
  20. package/clients/typescript/README.md +16 -0
  21. package/clients/typescript/index.d.ts +12 -0
  22. package/clients/typescript/index.mjs +16 -0
  23. package/clients/typescript/package.json +1 -1
  24. package/docs/agent-discovery.md +151 -22
  25. package/docs/ai-agents.md +99 -11
  26. package/docs/benchmarking.md +49 -3
  27. package/docs/benchmarks/2026-06-09-android-workflow.md +73 -0
  28. package/docs/benchmarks/2026-06-09-android-workflow.results.jsonl +20 -0
  29. package/docs/benchmarks/2026-06-09-framework-baseline-status.md +32 -0
  30. package/docs/benchmarks/2026-06-09-ios-appium-comparison.md +115 -0
  31. package/docs/benchmarks/2026-06-09-ios-appium-comparison.results.jsonl +40 -0
  32. package/docs/benchmarks/2026-06-09-ios-demo.md +90 -0
  33. package/docs/benchmarks/2026-06-09-ios-demo.results.jsonl +20 -0
  34. package/docs/benchmarks/2026-06-09-ios-maestro-comparison.md +128 -0
  35. package/docs/benchmarks/2026-06-09-ios-maestro-comparison.results.jsonl +40 -0
  36. package/docs/benchmarks/2026-06-09-ios-workflow-comparison.md +143 -0
  37. package/docs/benchmarks/2026-06-09-ios-workflow-comparison.results.jsonl +40 -0
  38. package/docs/benchmarks/2026-06-09-ios-xctest-floor.md +106 -0
  39. package/docs/benchmarks/2026-06-09-ios-xctest-floor.results.jsonl +40 -0
  40. package/docs/benchmarks/README.md +36 -0
  41. package/docs/benchmarks/benchmark-lab-v1.json +155 -0
  42. package/docs/benchmarks/benchmark-lab-v1.md +95 -0
  43. package/docs/clients.md +26 -6
  44. package/docs/demo.md +40 -1
  45. package/docs/expo-smoke.md +8 -8
  46. package/docs/frameworks.md +10 -0
  47. package/docs/install.md +3 -2
  48. package/docs/npm.md +100 -4
  49. package/docs/production-readiness.md +123 -0
  50. package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
  51. package/docs/protocol.md +215 -16
  52. package/docs/scenario-authoring.md +18 -0
  53. package/docs/trace-privacy.md +9 -0
  54. package/docs/troubleshooting.md +7 -1
  55. package/examples/android-workflow.json +79 -0
  56. package/examples/ios-shim-workflow.json +79 -0
  57. package/examples/react-native-expo-workflow.json +75 -0
  58. package/npm/agents.mjs +16 -0
  59. package/npm/commands.mjs +9 -5
  60. package/package.json +6 -1
  61. package/prebuilds/darwin-arm64/zmr +0 -0
  62. package/prebuilds/darwin-x64/zmr +0 -0
  63. package/prebuilds/linux-arm64/zmr +0 -0
  64. package/prebuilds/linux-x64/zmr +0 -0
  65. package/schemas/README.md +4 -0
  66. package/schemas/discover-output.schema.json +83 -0
  67. package/schemas/draft-output.schema.json +58 -0
  68. package/schemas/explore-output.schema.json +94 -0
  69. package/schemas/inspect-output.schema.json +88 -0
  70. package/schemas/run-output.schema.json +2 -0
  71. package/scripts/benchmark-lab.py +253 -0
  72. package/scripts/create-android-demo-app.sh +324 -29
  73. package/scripts/create-ios-demo-app.sh +174 -7
  74. package/scripts/create-react-native-expo-demo-app.sh +727 -0
  75. package/scripts/demo.sh +3 -0
  76. package/scripts/install-ios-shim.sh +2 -2
  77. package/scripts/release-readiness.py +43 -0
  78. package/scripts/run-android-pilot.sh +35 -9
  79. package/scripts/run-ios-pilot.sh +11 -4
  80. package/shims/ios/ZMRShim.swift +10 -0
  81. package/shims/ios/ZMRShimUITestCase.swift +42 -0
  82. package/shims/ios/protocol.md +1 -0
  83. package/skills/zmr-mobile-testing/SKILL.md +28 -3
  84. package/src/cli_discover.zig +239 -0
  85. package/src/cli_draft.zig +924 -0
  86. package/src/cli_explore.zig +136 -0
  87. package/src/cli_import.zig +31 -15
  88. package/src/cli_inspect.zig +310 -0
  89. package/src/cli_output.zig +26 -2
  90. package/src/cli_run.zig +28 -0
  91. package/src/cli_trace.zig +45 -15
  92. package/src/cli_validate.zig +12 -6
  93. package/src/errors.zig +9 -0
  94. package/src/ios.zig +49 -12
  95. package/src/ios_shim.zig +36 -2
  96. package/src/json_rpc_methods.zig +85 -11
  97. package/src/json_rpc_params.zig +8 -0
  98. package/src/json_rpc_protocol.zig +1 -1
  99. package/src/json_rpc_trace.zig +112 -0
  100. package/src/main.zig +27 -2
  101. package/src/mcp.zig +209 -6
  102. package/src/mcp_protocol.zig +29 -1
  103. package/src/mcp_trace.zig +126 -4
  104. package/src/report.zig +186 -0
  105. package/src/runner.zig +26 -4
  106. package/src/runner_actions.zig +10 -0
  107. package/src/runner_diagnostics.zig +31 -1
  108. package/src/runner_events.zig +70 -7
  109. package/src/runner_native.zig +17 -1
  110. package/src/runner_waits.zig +82 -19
  111. package/src/scaffold.zig +28 -12
  112. package/src/scenario.zig +32 -4
  113. package/src/schema_registry.zig +4 -0
  114. package/src/version.zig +1 -1
  115. package/viewer/app.js +23 -3
package/src/mcp_trace.zig CHANGED
@@ -1,6 +1,10 @@
1
1
  const std = @import("std");
2
2
  const bundle = @import("bundle.zig");
3
+ const cli_discover = @import("cli_discover.zig");
4
+ const cli_explore = @import("cli_explore.zig");
3
5
  const mcp_protocol = @import("mcp_protocol.zig");
6
+ const report = @import("report.zig");
7
+ const runner_events = @import("runner_events.zig");
4
8
  const trace = @import("trace.zig");
5
9
 
6
10
  pub fn writeEventsToolResult(
@@ -12,7 +16,10 @@ pub fn writeEventsToolResult(
12
16
  limit: u64,
13
17
  ) !void {
14
18
  const tw = live_trace orelse {
15
- try mcp_protocol.writeToolTextResult(writer, id, "{\"traceDir\":null,\"events\":[]}");
19
+ var no_trace_payload = std.ArrayList(u8).empty;
20
+ defer no_trace_payload.deinit(allocator);
21
+ try no_trace_payload.writer(allocator).print("{{\"traceDir\":null,\"afterSeq\":{d},\"nextSeq\":{d},\"latestSeq\":0,\"events\":[]}}", .{ after_seq, after_seq });
22
+ try mcp_protocol.writeToolTextResult(writer, id, no_trace_payload.items);
16
23
  return;
17
24
  };
18
25
 
@@ -29,7 +36,11 @@ pub fn writeEventsToolResult(
29
36
  const payload_writer = payload.writer(allocator);
30
37
  try payload_writer.writeAll("{\"traceDir\":");
31
38
  try trace.writeJsonString(payload_writer, tw.root_dir);
32
- try payload_writer.print(",\"afterSeq\":{d},\"events\":[", .{after_seq});
39
+ try payload_writer.print(",\"afterSeq\":{d},\"nextSeq\":", .{after_seq});
40
+ var events_json = std.ArrayList(u8).empty;
41
+ defer events_json.deinit(allocator);
42
+ const events_writer = events_json.writer(allocator);
43
+ var next_seq = after_seq;
33
44
  var emitted: u64 = 0;
34
45
  var lines = std.mem.splitScalar(u8, content, '\n');
35
46
  while (lines.next()) |raw_line| {
@@ -43,10 +54,13 @@ pub fn writeEventsToolResult(
43
54
  if (seq_value != .integer or seq_value.integer <= 0) continue;
44
55
  const seq = @as(u64, @intCast(seq_value.integer));
45
56
  if (seq <= after_seq) continue;
46
- if (emitted > 0) try payload_writer.writeAll(",");
47
- try payload_writer.writeAll(line);
57
+ if (emitted > 0) try events_writer.writeAll(",");
58
+ try events_writer.writeAll(line);
59
+ next_seq = seq;
48
60
  emitted += 1;
49
61
  }
62
+ try payload_writer.print("{d},\"latestSeq\":{d},\"events\":[", .{ next_seq, tw.event_count });
63
+ try payload_writer.writeAll(events_json.items);
50
64
  try payload_writer.writeAll("]}");
51
65
  try mcp_protocol.writeToolTextResult(writer, id, payload.items);
52
66
  }
@@ -81,3 +95,111 @@ pub fn writeExportToolResult(
81
95
  try payload_writer.print(",\"redacted\":{},\"omitScreenshots\":{}}}", .{ redact, omit_screenshots });
82
96
  try mcp_protocol.writeToolTextResult(writer, id, payload.items);
83
97
  }
98
+
99
+ pub fn writeExplainToolResult(
100
+ allocator: std.mem.Allocator,
101
+ writer: anytype,
102
+ id: ?std.json.Value,
103
+ live_trace: ?*trace.TraceWriter,
104
+ ) !void {
105
+ const tw = live_trace orelse {
106
+ try mcp_protocol.writeToolTextResult(writer, id, "{\"traceDir\":null,\"message\":\"start zmr mcp with --trace-dir to enable live trace explanation\"}");
107
+ return;
108
+ };
109
+
110
+ try tw.flushManifest();
111
+ var payload = std.ArrayList(u8).empty;
112
+ defer payload.deinit(allocator);
113
+ try report.writeTraceExplanationJson(allocator, tw.root_dir, payload.writer(allocator));
114
+ try mcp_protocol.writeToolTextResult(writer, id, std.mem.trimRight(u8, payload.items, " \t\r\n"));
115
+ try tw.recordEvent("trace.explain", "{\"status\":\"ok\"}");
116
+ }
117
+
118
+ pub fn writeDiscoverToolResult(
119
+ allocator: std.mem.Allocator,
120
+ writer: anytype,
121
+ id: ?std.json.Value,
122
+ live_trace: ?*trace.TraceWriter,
123
+ out_path: []const u8,
124
+ include_actions: bool,
125
+ validate: bool,
126
+ force: bool,
127
+ name: ?[]const u8,
128
+ app_id: ?[]const u8,
129
+ ) !void {
130
+ const tw = live_trace orelse {
131
+ try mcp_protocol.writeToolTextResult(writer, id, "{\"ok\":false,\"mode\":\"discover\",\"traceDir\":null,\"message\":\"start zmr mcp with --trace-dir to enable live trace discovery\"}");
132
+ return;
133
+ };
134
+
135
+ try tw.flushManifest();
136
+ var discovered = try cli_discover.discoverFromTrace(allocator, .{
137
+ .from_trace = tw.root_dir,
138
+ .out_path = out_path,
139
+ .name = name,
140
+ .app_id = app_id,
141
+ .include_actions = include_actions,
142
+ .validate = validate,
143
+ .force = force,
144
+ .json = true,
145
+ });
146
+ defer discovered.deinit(allocator);
147
+ try runner_events.recordTraceDiscover(
148
+ tw,
149
+ if (discovered.summary.ok) "ok" else "failed",
150
+ discovered.summary.draft.out_path,
151
+ include_actions,
152
+ discovered.summary.validated,
153
+ );
154
+
155
+ var payload = std.ArrayList(u8).empty;
156
+ defer payload.deinit(allocator);
157
+ try cli_discover.writeJson(payload.writer(allocator), discovered.summary, discovered.validation);
158
+ try mcp_protocol.writeToolTextResult(writer, id, std.mem.trimRight(u8, payload.items, " \t\r\n"));
159
+ }
160
+
161
+ pub fn writeExploreToolResult(
162
+ allocator: std.mem.Allocator,
163
+ writer: anytype,
164
+ id: ?std.json.Value,
165
+ live_trace: ?*trace.TraceWriter,
166
+ out_path: []const u8,
167
+ goal: []const u8,
168
+ include_actions: bool,
169
+ validate: bool,
170
+ force: bool,
171
+ name: ?[]const u8,
172
+ app_id: ?[]const u8,
173
+ ) !void {
174
+ const tw = live_trace orelse {
175
+ try mcp_protocol.writeToolTextResult(writer, id, "{\"ok\":false,\"mode\":\"explore\",\"traceDir\":null,\"message\":\"start zmr mcp with --trace-dir to enable live trace exploration\"}");
176
+ return;
177
+ };
178
+
179
+ try tw.flushManifest();
180
+ var explored = try cli_explore.exploreFromTrace(allocator, .{
181
+ .from_trace = tw.root_dir,
182
+ .out_path = out_path,
183
+ .goal = goal,
184
+ .name = name,
185
+ .app_id = app_id,
186
+ .include_actions = include_actions,
187
+ .validate = validate,
188
+ .force = force,
189
+ .json = true,
190
+ });
191
+ defer explored.deinit(allocator);
192
+ try runner_events.recordTraceExplore(
193
+ tw,
194
+ if (explored.summary.ok) "ok" else "failed",
195
+ explored.discovered.summary.draft.out_path,
196
+ goal,
197
+ include_actions,
198
+ explored.discovered.summary.validated,
199
+ );
200
+
201
+ var payload = std.ArrayList(u8).empty;
202
+ defer payload.deinit(allocator);
203
+ try cli_explore.writeJson(payload.writer(allocator), explored.summary, explored.discovered.summary, explored.discovered.validation);
204
+ try mcp_protocol.writeToolTextResult(writer, id, std.mem.trimRight(u8, payload.items, " \t\r\n"));
205
+ }
package/src/report.zig CHANGED
@@ -22,6 +22,23 @@ pub fn writeHtmlReport(
22
22
  }
23
23
  }
24
24
 
25
+ pub fn writeJUnitReport(
26
+ allocator: std.mem.Allocator,
27
+ input_path: []const u8,
28
+ out_path: []const u8,
29
+ ) !void {
30
+ const results_path = try std.fs.path.join(allocator, &.{ input_path, "results.jsonl" });
31
+ defer allocator.free(results_path);
32
+
33
+ if (std.fs.cwd().openFile(results_path, .{})) |file| {
34
+ file.close();
35
+ return try writeBenchmarkJUnitReport(allocator, input_path, results_path, out_path);
36
+ } else |err| switch (err) {
37
+ error.FileNotFound => return try writeTraceJUnitReport(allocator, input_path, out_path),
38
+ else => return err,
39
+ }
40
+ }
41
+
25
42
  pub fn writeTraceExplanation(
26
43
  allocator: std.mem.Allocator,
27
44
  trace_dir: []const u8,
@@ -151,6 +168,8 @@ fn writeTraceExplanationNextCommandsJson(writer: anytype, trace_dir: []const u8)
151
168
  try cli_output.writeShellArgJsonContent(writer, trace_dir);
152
169
  try writer.writeAll(" --out ");
153
170
  try cli_output.writeJoinedPathShellArgJsonContent(writer, trace_dir, "report.html");
171
+ try writer.writeAll(" --junit ");
172
+ try cli_output.writeJoinedPathShellArgJsonContent(writer, trace_dir, "junit.xml");
154
173
  try writer.writeAll("\",\"zmr export ");
155
174
  try cli_output.writeShellArgJsonContent(writer, trace_dir);
156
175
  try writer.writeAll(" --out ");
@@ -260,6 +279,89 @@ fn writeBenchmarkReport(
260
279
  try report_html.writeFile(out_path, html.items);
261
280
  }
262
281
 
282
+ fn writeBenchmarkJUnitReport(
283
+ allocator: std.mem.Allocator,
284
+ input_path: []const u8,
285
+ results_path: []const u8,
286
+ out_path: []const u8,
287
+ ) !void {
288
+ const content = try std.fs.cwd().readFileAlloc(allocator, results_path, 64 * 1024 * 1024);
289
+ defer allocator.free(content);
290
+
291
+ var cases_xml = std.ArrayList(u8).empty;
292
+ defer cases_xml.deinit(allocator);
293
+
294
+ var total: usize = 0;
295
+ var failed: usize = 0;
296
+ var total_duration_ms: i64 = 0;
297
+
298
+ var lines = std.mem.splitScalar(u8, content, '\n');
299
+ while (lines.next()) |raw_line| {
300
+ const line = std.mem.trim(u8, raw_line, " \t\r\n");
301
+ if (line.len == 0) continue;
302
+
303
+ const parsed = try std.json.parseFromSlice(std.json.Value, allocator, line, .{});
304
+ defer parsed.deinit();
305
+ if (parsed.value != .object) continue;
306
+ const object = parsed.value.object;
307
+
308
+ const tool = report_values.stringField(object, "tool") orelse "zmr";
309
+ const status = report_values.stringField(object, "status") orelse "";
310
+ const trace_status = report_values.stringField(object, "traceStatus") orelse "";
311
+ const trace_error = report_values.stringField(object, "traceError") orelse "";
312
+ const trace_dir = report_values.stringField(object, "traceDir") orelse "";
313
+ const run = report_values.intField(object, "run") orelse @as(i64, @intCast(total + 1));
314
+ const duration_ms = report_values.intField(object, "durationMs") orelse 0;
315
+ const failed_step = report_values.intField(object, "failedStepIndex");
316
+ const row_passed = std.mem.eql(u8, status, "ok") and (trace_status.len == 0 or std.mem.eql(u8, trace_status, "passed"));
317
+
318
+ total += 1;
319
+ if (!row_passed) failed += 1;
320
+ if (duration_ms > 0) total_duration_ms += duration_ms;
321
+
322
+ const writer = cases_xml.writer(allocator);
323
+ try writer.writeAll(" <testcase classname=\"");
324
+ try report_html.escape(writer, tool);
325
+ try writer.writeAll("\" name=\"run ");
326
+ try writer.print("{d}", .{run});
327
+ try writer.writeAll("\" time=\"");
328
+ try writeSeconds(writer, duration_ms);
329
+ try writer.writeAll("\">");
330
+ if (!row_passed) {
331
+ const message = if (trace_error.len > 0) trace_error else if (trace_status.len > 0) trace_status else status;
332
+ try writer.writeAll("<failure message=\"");
333
+ try report_html.escape(writer, message);
334
+ try writer.writeAll("\" type=\"ZMRFailure\">");
335
+ if (failed_step) |index| try writer.print("failedStepIndex={d}", .{index});
336
+ if (trace_dir.len > 0) {
337
+ if (failed_step != null) try writer.writeAll(" ");
338
+ try writer.writeAll("traceDir=");
339
+ try report_html.escape(writer, trace_dir);
340
+ }
341
+ try writer.writeAll("</failure>");
342
+ }
343
+ try writer.writeAll("</testcase>\n");
344
+ }
345
+
346
+ var xml = std.ArrayList(u8).empty;
347
+ defer xml.deinit(allocator);
348
+ const writer = xml.writer(allocator);
349
+ try writeJUnitHeader(writer);
350
+ try writer.writeAll("<testsuite name=\"ZMR Benchmark\" tests=\"");
351
+ try writer.print("{d}", .{total});
352
+ try writer.writeAll("\" failures=\"");
353
+ try writer.print("{d}", .{failed});
354
+ try writer.writeAll("\" errors=\"0\" skipped=\"0\" time=\"");
355
+ try writeSeconds(writer, total_duration_ms);
356
+ try writer.writeAll("\">\n");
357
+ try writer.writeAll(" <properties>\n");
358
+ try writeJUnitProperty(writer, "source", input_path);
359
+ try writer.writeAll(" </properties>\n");
360
+ try writer.writeAll(cases_xml.items);
361
+ try writer.writeAll("</testsuite>\n");
362
+ try report_html.writeFile(out_path, xml.items);
363
+ }
364
+
263
365
  fn writeTraceReport(
264
366
  allocator: std.mem.Allocator,
265
367
  input_path: []const u8,
@@ -344,3 +446,87 @@ fn writeTraceReport(
344
446
  defer allocator.free(relative_report_path);
345
447
  try trace.attachReportPath(allocator, input_path, relative_report_path);
346
448
  }
449
+
450
+ fn writeTraceJUnitReport(
451
+ allocator: std.mem.Allocator,
452
+ input_path: []const u8,
453
+ out_path: []const u8,
454
+ ) !void {
455
+ var summary = try trace_summary.read(allocator, input_path);
456
+ defer summary.deinit(allocator);
457
+
458
+ const failed = !isPassedStatus(summary.status);
459
+ var xml = std.ArrayList(u8).empty;
460
+ defer xml.deinit(allocator);
461
+ const writer = xml.writer(allocator);
462
+
463
+ try writeJUnitHeader(writer);
464
+ try writer.writeAll("<testsuite name=\"ZMR\" tests=\"1\" failures=\"");
465
+ try writer.writeAll(if (failed) "1" else "0");
466
+ try writer.writeAll("\" errors=\"0\" skipped=\"0\" time=\"");
467
+ try writeSeconds(writer, summary.duration_ms orelse 0);
468
+ try writer.writeAll("\">\n");
469
+ try writer.writeAll(" <properties>\n");
470
+ try writeJUnitProperty(writer, "traceDir", input_path);
471
+ try writeJUnitProperty(writer, "status", summary.status);
472
+ if (summary.event_count) |value| try writeJUnitIntProperty(writer, "eventCount", value);
473
+ if (summary.snapshot_count) |value| try writeJUnitIntProperty(writer, "snapshotCount", value);
474
+ if (summary.failed_step_index) |value| try writeJUnitIntProperty(writer, "failedStepIndex", value);
475
+ try writer.writeAll(" </properties>\n");
476
+
477
+ try writer.writeAll(" <testcase classname=\"");
478
+ try report_html.escape(writer, summary.app_id orelse "zmr");
479
+ try writer.writeAll("\" name=\"");
480
+ try report_html.escape(writer, summary.scenario_name);
481
+ try writer.writeAll("\" time=\"");
482
+ try writeSeconds(writer, summary.duration_ms orelse 0);
483
+ try writer.writeAll("\">");
484
+ if (failed) {
485
+ const message = summary.error_name orelse summary.status;
486
+ const failure_type = summary.error_name orelse "ZMRFailure";
487
+ try writer.writeAll("<failure message=\"");
488
+ try report_html.escape(writer, message);
489
+ try writer.writeAll("\" type=\"");
490
+ try report_html.escape(writer, failure_type);
491
+ try writer.writeAll("\">");
492
+ if (summary.failed_step_index) |index| {
493
+ try writer.print("failedStepIndex={d}", .{index});
494
+ } else {
495
+ try writer.writeAll("status=");
496
+ try report_html.escape(writer, summary.status);
497
+ }
498
+ try writer.writeAll("</failure>");
499
+ }
500
+ try writer.writeAll("</testcase>\n");
501
+ try writer.writeAll("</testsuite>\n");
502
+ try report_html.writeFile(out_path, xml.items);
503
+ }
504
+
505
+ fn isPassedStatus(status: []const u8) bool {
506
+ return std.mem.eql(u8, status, "passed");
507
+ }
508
+
509
+ fn writeJUnitHeader(writer: anytype) !void {
510
+ try writer.writeAll("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
511
+ }
512
+
513
+ fn writeJUnitProperty(writer: anytype, name: []const u8, value: []const u8) !void {
514
+ try writer.writeAll(" <property name=\"");
515
+ try report_html.escape(writer, name);
516
+ try writer.writeAll("\" value=\"");
517
+ try report_html.escape(writer, value);
518
+ try writer.writeAll("\"/>\n");
519
+ }
520
+
521
+ fn writeJUnitIntProperty(writer: anytype, name: []const u8, value: i64) !void {
522
+ try writer.writeAll(" <property name=\"");
523
+ try report_html.escape(writer, name);
524
+ try writer.writeAll("\" value=\"");
525
+ try writer.print("{d}", .{value});
526
+ try writer.writeAll("\"/>\n");
527
+ }
528
+
529
+ fn writeSeconds(writer: anytype, duration_ms: i64) !void {
530
+ const safe_ms = @max(duration_ms, 0);
531
+ try writer.print("{d}.{d:0>3}", .{ @divTrunc(safe_ms, 1000), @mod(safe_ms, 1000) });
532
+ }
package/src/runner.zig CHANGED
@@ -102,6 +102,7 @@ pub fn executeStep(
102
102
  },
103
103
  .press_back => {
104
104
  try device.pressBack();
105
+ if (writer) |tw| try tw.recordEvent("ui.pressBack", "{\"status\":\"ok\"}");
105
106
  try settleDevice(device, options);
106
107
  },
107
108
  .hide_keyboard => {
@@ -111,6 +112,7 @@ pub fn executeStep(
111
112
  },
112
113
  .swipe => |swipe| {
113
114
  try device.swipe(swipe.x1, swipe.y1, swipe.x2, swipe.y2, swipe.duration_ms);
115
+ if (writer) |tw| try runner_events.recordSwipe(tw, swipe.x1, swipe.y1, swipe.x2, swipe.y2, swipe.duration_ms);
114
116
  try settleDevice(device, options);
115
117
  },
116
118
  .wait_visible => |wait| {
@@ -122,11 +124,11 @@ pub fn executeStep(
122
124
  .wait_any => |wait| {
123
125
  if (try waitUntilAnyVisible(device, wait.selectors, wait.timeout_ms, writer, options) == null) return error.WaitTimeout;
124
126
  },
125
- .assert_visible => |wanted| {
126
- if (!try waitUntilVisible(device, wanted, options.default_timeout_ms, writer, options)) return error.AssertionFailed;
127
+ .assert_visible => |assertion| {
128
+ if (!try assertVisible(device, assertion.selector, assertion.timeout_ms orelse options.default_timeout_ms, writer, options)) return error.AssertionFailed;
127
129
  },
128
- .assert_not_visible => |wanted| {
129
- if (!try waitUntilNotVisible(device, wanted, options.default_timeout_ms, writer, options)) return error.AssertionFailed;
130
+ .assert_not_visible => |assertion| {
131
+ if (!try assertNotVisible(device, assertion.selector, assertion.timeout_ms orelse options.default_timeout_ms, writer, options)) return error.AssertionFailed;
130
132
  },
131
133
  .assert_none_visible => |assertion| {
132
134
  if (!try assertNoneVisible(device, assertion.selectors, assertion.timeout_ms, writer, options)) return error.AssertionFailed;
@@ -231,6 +233,26 @@ pub fn waitUntilAnyVisible(
231
233
  return try runner_waits.waitUntilAnyVisible(device, selectors, timeout_ms, writer, options);
232
234
  }
233
235
 
236
+ pub fn assertVisible(
237
+ device: anytype,
238
+ wanted: selector.Selector,
239
+ timeout_ms: u64,
240
+ writer: ?*trace.TraceWriter,
241
+ options: RunOptions,
242
+ ) !bool {
243
+ return try runner_waits.assertVisible(device, wanted, timeout_ms, writer, options);
244
+ }
245
+
246
+ pub fn assertNotVisible(
247
+ device: anytype,
248
+ wanted: selector.Selector,
249
+ timeout_ms: u64,
250
+ writer: ?*trace.TraceWriter,
251
+ options: RunOptions,
252
+ ) !bool {
253
+ return try runner_waits.assertNotVisible(device, wanted, timeout_ms, writer, options);
254
+ }
255
+
234
256
  pub fn assertNoneVisible(
235
257
  device: anytype,
236
258
  selectors: []const selector.Selector,
@@ -61,6 +61,16 @@ pub fn typeTextSelector(
61
61
  if (try runner_native.tryTypeTextSelector(device, wanted, text, writer, options.settle_ms)) return;
62
62
  try tapSelector(device, wanted, writer, options);
63
63
  try device.typeText(text);
64
+ if (writer) |tw| {
65
+ var payload = std.ArrayList(u8).empty;
66
+ defer payload.deinit(tw.allocator);
67
+ try payload.writer(tw.allocator).writeAll("{\"status\":\"ok\",\"selector\":");
68
+ try trace.writeSelectorJson(payload.writer(tw.allocator), wanted);
69
+ try payload.writer(tw.allocator).writeAll(",\"text\":");
70
+ try trace.writeJsonString(payload.writer(tw.allocator), text);
71
+ try payload.writer(tw.allocator).writeAll("}");
72
+ try tw.recordEvent("ui.type", payload.items);
73
+ }
64
74
  try settleDevice(device, options);
65
75
  }
66
76
 
@@ -10,10 +10,26 @@ pub fn record(
10
10
  strategy: ?[]const u8,
11
11
  selectors: []const selector.Selector,
12
12
  snap: types.ObservationSnapshot,
13
+ ) !void {
14
+ try recordWithOptions(tw, kind, status, strategy, selectors, snap, .{});
15
+ }
16
+
17
+ pub const DiagnosticOptions = struct {
18
+ timeout_ms: ?u64 = null,
19
+ };
20
+
21
+ pub fn recordWithOptions(
22
+ tw: *trace.TraceWriter,
23
+ kind: []const u8,
24
+ status: []const u8,
25
+ strategy: ?[]const u8,
26
+ selectors: []const selector.Selector,
27
+ snap: types.ObservationSnapshot,
28
+ options: DiagnosticOptions,
13
29
  ) !void {
14
30
  var payload = std.ArrayList(u8).empty;
15
31
  defer payload.deinit(tw.allocator);
16
- try writeSelectorDiagnosticJson(payload.writer(tw.allocator), status, strategy, selectors, snap);
32
+ try writeSelectorDiagnosticJsonWithOptions(payload.writer(tw.allocator), status, strategy, selectors, snap, options);
17
33
  try tw.recordEvent(kind, payload.items);
18
34
  }
19
35
 
@@ -23,12 +39,26 @@ pub fn writeSelectorDiagnosticJson(
23
39
  strategy: ?[]const u8,
24
40
  selectors: []const selector.Selector,
25
41
  snap: types.ObservationSnapshot,
42
+ ) !void {
43
+ try writeSelectorDiagnosticJsonWithOptions(writer, status, strategy, selectors, snap, .{});
44
+ }
45
+
46
+ pub fn writeSelectorDiagnosticJsonWithOptions(
47
+ writer: anytype,
48
+ status: []const u8,
49
+ strategy: ?[]const u8,
50
+ selectors: []const selector.Selector,
51
+ snap: types.ObservationSnapshot,
52
+ options: DiagnosticOptions,
26
53
  ) !void {
27
54
  try writer.print("{{\"status\":\"{s}\"", .{status});
28
55
  if (strategy) |value| {
29
56
  try writer.writeAll(",\"strategy\":");
30
57
  try trace.writeJsonString(writer, value);
31
58
  }
59
+ if (options.timeout_ms) |timeout_ms| {
60
+ try writer.print(",\"timeoutMs\":{d}", .{timeout_ms});
61
+ }
32
62
  try writer.print(",\"snapshotId\":\"{s}\",\"selectors\":[", .{snap.id});
33
63
  for (selectors, 0..) |wanted, index| {
34
64
  if (index > 0) try writer.writeAll(",");
@@ -13,21 +13,21 @@ pub fn eventString(allocator: std.mem.Allocator, value: []const u8) ![]const u8
13
13
  return try buffer.toOwnedSlice(allocator);
14
14
  }
15
15
 
16
- pub fn recordNativeWait(tw: *trace.TraceWriter, kind: []const u8, wanted: selector.Selector, matched_index: ?usize) !void {
16
+ pub fn recordNativeWait(tw: *trace.TraceWriter, kind: []const u8, wanted: selector.Selector, matched_index: ?usize, timeout_ms: u64) !void {
17
17
  var payload = std.ArrayList(u8).empty;
18
18
  defer payload.deinit(tw.allocator);
19
19
  try payload.writer(tw.allocator).writeAll("{\"status\":\"ok\",\"strategy\":\"nativeSelector\"");
20
20
  if (matched_index) |index| try payload.writer(tw.allocator).print(",\"matchedIndex\":{d}", .{index});
21
21
  try payload.writer(tw.allocator).writeAll(",\"selector\":");
22
22
  try trace.writeSelectorJson(payload.writer(tw.allocator), wanted);
23
- try payload.writer(tw.allocator).writeAll("}");
23
+ try payload.writer(tw.allocator).print(",\"timeoutMs\":{d}}}", .{timeout_ms});
24
24
  try tw.recordEvent(kind, payload.items);
25
25
  }
26
26
 
27
- pub fn recordNativeWaitTimeout(tw: *trace.TraceWriter, kind: []const u8, selectors: []const selector.Selector) !void {
27
+ pub fn recordNativeWaitTimeout(tw: *trace.TraceWriter, kind: []const u8, selectors: []const selector.Selector, timeout_ms: u64) !void {
28
28
  var payload = std.ArrayList(u8).empty;
29
29
  defer payload.deinit(tw.allocator);
30
- try payload.writer(tw.allocator).writeAll("{\"status\":\"timeout\",\"strategy\":\"nativeSelector\",\"selectors\":[");
30
+ try payload.writer(tw.allocator).print("{{\"status\":\"timeout\",\"strategy\":\"nativeSelector\",\"timeoutMs\":{d},\"selectors\":[", .{timeout_ms});
31
31
  for (selectors, 0..) |wanted, index| {
32
32
  if (index > 0) try payload.writer(tw.allocator).writeAll(",");
33
33
  try trace.writeSelectorJson(payload.writer(tw.allocator), wanted);
@@ -36,13 +36,28 @@ pub fn recordNativeWaitTimeout(tw: *trace.TraceWriter, kind: []const u8, selecto
36
36
  try tw.recordEvent(kind, payload.items);
37
37
  }
38
38
 
39
- pub fn recordNativeWaitTimeoutWithDiagnostics(device: anytype, tw: *trace.TraceWriter, kind: []const u8, selectors: []const selector.Selector) !void {
39
+ pub fn recordNativeWaitTimeoutWithDiagnostics(device: anytype, tw: *trace.TraceWriter, kind: []const u8, selectors: []const selector.Selector, timeout_ms: u64) !void {
40
40
  var snap = device.snapshot(tw) catch {
41
- try recordNativeWaitTimeout(tw, kind, selectors);
41
+ try recordNativeWaitTimeout(tw, kind, selectors, timeout_ms);
42
42
  return;
43
43
  };
44
44
  defer snap.deinit(device.allocator);
45
- try recordDiagnosticWithStrategy(tw, kind, "timeout", "nativeSelector", selectors, snap);
45
+ try recordDiagnosticWithStrategyAndTimeout(tw, kind, "timeout", "nativeSelector", selectors, snap, timeout_ms);
46
+ }
47
+
48
+ pub fn recordSelectorArrayStatus(tw: *trace.TraceWriter, kind: []const u8, status: []const u8, selectors: []const selector.Selector, timeout_ms: u64) !void {
49
+ var payload = std.ArrayList(u8).empty;
50
+ defer payload.deinit(tw.allocator);
51
+ const out = payload.writer(tw.allocator);
52
+ try out.writeAll("{\"status\":");
53
+ try trace.writeJsonString(out, status);
54
+ try out.writeAll(",\"selectors\":[");
55
+ for (selectors, 0..) |wanted, index| {
56
+ if (index > 0) try out.writeAll(",");
57
+ try trace.writeSelectorJson(out, wanted);
58
+ }
59
+ try out.print("],\"timeoutMs\":{d}}}", .{timeout_ms});
60
+ try tw.recordEvent(kind, payload.items);
46
61
  }
47
62
 
48
63
  pub fn recordSelectorEvent(tw: *trace.TraceWriter, kind: []const u8, wanted: selector.Selector) !void {
@@ -72,6 +87,42 @@ pub fn recordActionStatus(tw: *trace.TraceWriter, kind: []const u8, status: []co
72
87
  try tw.recordEvent(kind, payload.items);
73
88
  }
74
89
 
90
+ pub fn recordSwipe(tw: *trace.TraceWriter, x1: i32, y1: i32, x2: i32, y2: i32, duration_ms: u32) !void {
91
+ const payload = try std.fmt.allocPrint(
92
+ tw.allocator,
93
+ "{{\"status\":\"ok\",\"x1\":{d},\"y1\":{d},\"x2\":{d},\"y2\":{d},\"durationMs\":{d}}}",
94
+ .{ x1, y1, x2, y2, duration_ms },
95
+ );
96
+ defer tw.allocator.free(payload);
97
+ try tw.recordEvent("ui.swipe", payload);
98
+ }
99
+
100
+ pub fn recordTraceDiscover(tw: *trace.TraceWriter, status: []const u8, out_path: []const u8, include_actions: bool, validated: bool) !void {
101
+ var payload = std.ArrayList(u8).empty;
102
+ defer payload.deinit(tw.allocator);
103
+ const out = payload.writer(tw.allocator);
104
+ try out.writeAll("{\"status\":");
105
+ try trace.writeJsonString(out, status);
106
+ try out.writeAll(",\"out\":");
107
+ try trace.writeJsonString(out, out_path);
108
+ try out.print(",\"includeActions\":{},\"validated\":{}}}", .{ include_actions, validated });
109
+ try tw.recordEvent("trace.discover", payload.items);
110
+ }
111
+
112
+ pub fn recordTraceExplore(tw: *trace.TraceWriter, status: []const u8, out_path: []const u8, goal: []const u8, include_actions: bool, validated: bool) !void {
113
+ var payload = std.ArrayList(u8).empty;
114
+ defer payload.deinit(tw.allocator);
115
+ const out = payload.writer(tw.allocator);
116
+ try out.writeAll("{\"status\":");
117
+ try trace.writeJsonString(out, status);
118
+ try out.writeAll(",\"out\":");
119
+ try trace.writeJsonString(out, out_path);
120
+ try out.writeAll(",\"goal\":");
121
+ try trace.writeJsonString(out, goal);
122
+ try out.print(",\"includeActions\":{},\"validated\":{}}}", .{ include_actions, validated });
123
+ try tw.recordEvent("trace.explore", payload.items);
124
+ }
125
+
75
126
  pub fn recordStepError(tw: *trace.TraceWriter, index: usize, err: anyerror) !void {
76
127
  const payload = try std.fmt.allocPrint(
77
128
  tw.allocator,
@@ -159,6 +210,18 @@ pub fn recordDiagnosticWithStrategy(
159
210
  try runner_diagnostics.record(tw, kind, status, strategy, selectors, snap);
160
211
  }
161
212
 
213
+ pub fn recordDiagnosticWithStrategyAndTimeout(
214
+ tw: *trace.TraceWriter,
215
+ kind: []const u8,
216
+ status: []const u8,
217
+ strategy: ?[]const u8,
218
+ selectors: []const selector.Selector,
219
+ snap: types.ObservationSnapshot,
220
+ timeout_ms: u64,
221
+ ) !void {
222
+ try runner_diagnostics.recordWithOptions(tw, kind, status, strategy, selectors, snap, .{ .timeout_ms = timeout_ms });
223
+ }
224
+
162
225
  pub fn writeSelectorDiagnosticJson(
163
226
  writer: anytype,
164
227
  status: []const u8,
@@ -32,7 +32,7 @@ pub fn tryTypeTextSelector(
32
32
  return err;
33
33
  };
34
34
  if (!typed) return false;
35
- if (writer) |tw| try recordSelectorAction(tw, "ui.type", wanted, null);
35
+ if (writer) |tw| try recordSelectorTextAction(tw, "ui.type", wanted, text);
36
36
  try device.settle(settle_ms);
37
37
  return true;
38
38
  }
@@ -70,6 +70,22 @@ fn recordSelectorAction(
70
70
  try tw.recordEvent(kind, payload.items);
71
71
  }
72
72
 
73
+ fn recordSelectorTextAction(
74
+ tw: *trace.TraceWriter,
75
+ kind: []const u8,
76
+ wanted: selector.Selector,
77
+ text: []const u8,
78
+ ) !void {
79
+ var payload = std.ArrayList(u8).empty;
80
+ defer payload.deinit(tw.allocator);
81
+ try payload.writer(tw.allocator).writeAll("{\"status\":\"ok\",\"strategy\":\"nativeSelector\",\"selector\":");
82
+ try trace.writeSelectorJson(payload.writer(tw.allocator), wanted);
83
+ try payload.writer(tw.allocator).writeAll(",\"text\":");
84
+ try trace.writeJsonString(payload.writer(tw.allocator), text);
85
+ try payload.writer(tw.allocator).writeAll("}");
86
+ try tw.recordEvent(kind, payload.items);
87
+ }
88
+
73
89
  fn recordSelectorActionFailure(
74
90
  tw: *trace.TraceWriter,
75
91
  kind: []const u8,