zig-mobile-runner 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/CHANGELOG.md +484 -0
  2. package/CONTRIBUTING.md +42 -0
  3. package/FEATURES.md +112 -0
  4. package/LICENSE +21 -0
  5. package/README.md +255 -0
  6. package/SECURITY.md +34 -0
  7. package/build.zig +38 -0
  8. package/build.zig.zon +7 -0
  9. package/clients/README.md +144 -0
  10. package/clients/go/README.md +24 -0
  11. package/clients/go/examples/fake-session/main.go +93 -0
  12. package/clients/go/go.mod +3 -0
  13. package/clients/go/zmr/client.go +432 -0
  14. package/clients/kotlin/README.md +35 -0
  15. package/clients/kotlin/build.gradle.kts +35 -0
  16. package/clients/kotlin/settings.gradle.kts +15 -0
  17. package/clients/kotlin/src/main/kotlin/dev/zmr/FakeSession.kt +86 -0
  18. package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +67 -0
  19. package/clients/python/README.md +29 -0
  20. package/clients/python/examples/fake_session.py +48 -0
  21. package/clients/python/pyproject.toml +13 -0
  22. package/clients/python/zmr_client.py +202 -0
  23. package/clients/rust/Cargo.lock +107 -0
  24. package/clients/rust/Cargo.toml +10 -0
  25. package/clients/rust/README.md +19 -0
  26. package/clients/rust/examples/fake_session.rs +70 -0
  27. package/clients/rust/src/lib.rs +461 -0
  28. package/clients/swift/Package.swift +16 -0
  29. package/clients/swift/README.md +36 -0
  30. package/clients/swift/Sources/ZMRClient/ZMRClient.swift +114 -0
  31. package/clients/swift/Sources/ZMRFakeSession/main.swift +86 -0
  32. package/clients/typescript/README.md +34 -0
  33. package/clients/typescript/examples/fake-session.mjs +36 -0
  34. package/clients/typescript/index.d.ts +144 -0
  35. package/clients/typescript/index.mjs +192 -0
  36. package/clients/typescript/package.json +8 -0
  37. package/docs/adr/0001-agent-native-runner-boundary.md +31 -0
  38. package/docs/adr/0002-app-local-zmr-contract.md +39 -0
  39. package/docs/adr/0003-ios-simulator-xctest-shim.md +41 -0
  40. package/docs/adr/0004-benchmark-claims-and-baseline-collection.md +37 -0
  41. package/docs/adr/README.md +12 -0
  42. package/docs/ai-agents.md +156 -0
  43. package/docs/app-integration.md +316 -0
  44. package/docs/benchmarking.md +275 -0
  45. package/docs/client-installation.md +141 -0
  46. package/docs/clients.md +98 -0
  47. package/docs/config.md +175 -0
  48. package/docs/demo.md +259 -0
  49. package/docs/dsl.md +57 -0
  50. package/docs/install.md +233 -0
  51. package/docs/market-positioning.md +70 -0
  52. package/docs/npm.md +359 -0
  53. package/docs/protocol-fixtures/README.md +8 -0
  54. package/docs/protocol-fixtures/core-session.requests.jsonl +8 -0
  55. package/docs/protocol-fixtures/core-session.responses.jsonl +8 -0
  56. package/docs/protocol-versioning.md +65 -0
  57. package/docs/protocol.md +560 -0
  58. package/docs/publication.md +77 -0
  59. package/docs/release-audit.md +99 -0
  60. package/docs/release-candidate.md +111 -0
  61. package/docs/release-evidence.md +188 -0
  62. package/docs/release-notes-template.md +58 -0
  63. package/docs/roadmap.md +334 -0
  64. package/docs/scenario-authoring.md +88 -0
  65. package/docs/shipping.md +170 -0
  66. package/docs/trace-privacy.md +88 -0
  67. package/docs/troubleshooting.md +256 -0
  68. package/examples/android-app-auth-probe.json +89 -0
  69. package/examples/android-app-error-state.json +13 -0
  70. package/examples/android-app-login-smoke.json +192 -0
  71. package/examples/android-app-onboarding.json +12 -0
  72. package/examples/android-app-referral-deep-link.json +12 -0
  73. package/examples/android-shim-smoke.json +19 -0
  74. package/examples/demo-failure.json +12 -0
  75. package/examples/demo-fake.json +14 -0
  76. package/examples/ios-dev-client-open-link.json +26 -0
  77. package/examples/ios-dev-client-route-snapshot.json +24 -0
  78. package/examples/ios-shim-smoke.json +23 -0
  79. package/examples/ios-smoke.json +9 -0
  80. package/go.work +3 -0
  81. package/npm/agents.mjs +183 -0
  82. package/npm/app-config.mjs +95 -0
  83. package/npm/build-zmr.mjs +21 -0
  84. package/npm/commands.mjs +104 -0
  85. package/npm/generated-files.mjs +50 -0
  86. package/npm/index.mjs +75 -0
  87. package/npm/init-app.mjs +80 -0
  88. package/npm/package-scripts.mjs +72 -0
  89. package/npm/postinstall.mjs +21 -0
  90. package/npm/scaffold.mjs +179 -0
  91. package/npm/scenarios.mjs +93 -0
  92. package/npm/setup.mjs +69 -0
  93. package/npm/wizard.mjs +117 -0
  94. package/npm/zmr.mjs +23 -0
  95. package/package.json +114 -0
  96. package/prebuilds/darwin-arm64/zmr +0 -0
  97. package/prebuilds/darwin-x64/zmr +0 -0
  98. package/prebuilds/linux-arm64/zmr +0 -0
  99. package/prebuilds/linux-x64/zmr +0 -0
  100. package/schemas/README.md +26 -0
  101. package/schemas/action-result.schema.json +27 -0
  102. package/schemas/capabilities-output.schema.json +98 -0
  103. package/schemas/devices-output.schema.json +25 -0
  104. package/schemas/doctor-output.schema.json +51 -0
  105. package/schemas/explain-output.schema.json +51 -0
  106. package/schemas/import-output.schema.json +23 -0
  107. package/schemas/init-output.schema.json +71 -0
  108. package/schemas/json-rpc.schema.json +55 -0
  109. package/schemas/release-manifest.schema.json +43 -0
  110. package/schemas/release-readiness-output.schema.json +127 -0
  111. package/schemas/run-output.schema.json +43 -0
  112. package/schemas/scenario.schema.json +128 -0
  113. package/schemas/schemas-output.schema.json +26 -0
  114. package/schemas/semantic-snapshot.schema.json +116 -0
  115. package/schemas/snapshot.schema.json +60 -0
  116. package/schemas/trace-event.schema.json +14 -0
  117. package/schemas/trace-manifest.schema.json +59 -0
  118. package/schemas/validate-output.schema.json +42 -0
  119. package/schemas/version-output.schema.json +23 -0
  120. package/schemas/zmr-config.schema.json +75 -0
  121. package/scripts/android-emulator.sh +126 -0
  122. package/scripts/assert-ios-physical-ready.sh +213 -0
  123. package/scripts/benchmark-command.sh +307 -0
  124. package/scripts/benchmark.sh +359 -0
  125. package/scripts/benchmark_gate.py +117 -0
  126. package/scripts/benchmark_result_row.py +88 -0
  127. package/scripts/compare-benchmarks.py +288 -0
  128. package/scripts/create-android-demo-app.sh +342 -0
  129. package/scripts/create-ios-demo-app.sh +261 -0
  130. package/scripts/demo-android-real.sh +232 -0
  131. package/scripts/demo-ios-real.sh +270 -0
  132. package/scripts/demo.sh +464 -0
  133. package/scripts/device-matrix.sh +338 -0
  134. package/scripts/ensure-ios-shim-target.rb +237 -0
  135. package/scripts/install-android-shim.sh +281 -0
  136. package/scripts/install-ios-shim.sh +589 -0
  137. package/scripts/pilot-gate.sh +560 -0
  138. package/scripts/release-readiness.py +838 -0
  139. package/scripts/release-readiness.sh +91 -0
  140. package/scripts/run-android-pilot.sh +561 -0
  141. package/scripts/run-ios-pilot.sh +509 -0
  142. package/shims/android/README.md +21 -0
  143. package/shims/android/ZMRShimInstrumentedTest.java +152 -0
  144. package/shims/android/protocol.md +18 -0
  145. package/shims/ios/README.md +50 -0
  146. package/shims/ios/ZMRShim.swift +110 -0
  147. package/shims/ios/ZMRShimUITestCase.swift +475 -0
  148. package/shims/ios/protocol.md +74 -0
  149. package/skills/zmr-mobile-testing/SKILL.md +127 -0
  150. package/src/android.zig +344 -0
  151. package/src/android_device_info.zig +99 -0
  152. package/src/android_emulator.zig +154 -0
  153. package/src/android_screen_recording.zig +112 -0
  154. package/src/android_shell.zig +112 -0
  155. package/src/bundle.zig +124 -0
  156. package/src/bundle_redaction.zig +272 -0
  157. package/src/bundle_tar.zig +123 -0
  158. package/src/cli_devices.zig +97 -0
  159. package/src/cli_doctor.zig +114 -0
  160. package/src/cli_import.zig +70 -0
  161. package/src/cli_info.zig +39 -0
  162. package/src/cli_init.zig +72 -0
  163. package/src/cli_output.zig +467 -0
  164. package/src/cli_run.zig +259 -0
  165. package/src/cli_serve.zig +287 -0
  166. package/src/cli_trace.zig +111 -0
  167. package/src/cli_validate.zig +41 -0
  168. package/src/command.zig +211 -0
  169. package/src/config.zig +305 -0
  170. package/src/config_diagnostics.zig +212 -0
  171. package/src/config_paths.zig +49 -0
  172. package/src/device_registry.zig +37 -0
  173. package/src/doctor.zig +412 -0
  174. package/src/doctor_hints.zig +52 -0
  175. package/src/errors.zig +55 -0
  176. package/src/fake_device.zig +163 -0
  177. package/src/health.zig +28 -0
  178. package/src/importer.zig +343 -0
  179. package/src/importer_json.zig +100 -0
  180. package/src/importer_model.zig +103 -0
  181. package/src/ios.zig +399 -0
  182. package/src/ios_devices.zig +219 -0
  183. package/src/ios_lifecycle.zig +72 -0
  184. package/src/ios_shim.zig +242 -0
  185. package/src/ios_snapshot.zig +20 -0
  186. package/src/json_fields.zig +80 -0
  187. package/src/json_rpc.zig +150 -0
  188. package/src/json_rpc_methods.zig +318 -0
  189. package/src/json_rpc_observation.zig +31 -0
  190. package/src/json_rpc_params.zig +52 -0
  191. package/src/json_rpc_protocol.zig +110 -0
  192. package/src/json_rpc_trace.zig +73 -0
  193. package/src/main.zig +135 -0
  194. package/src/mcp.zig +234 -0
  195. package/src/mcp_protocol.zig +64 -0
  196. package/src/mcp_trace.zig +83 -0
  197. package/src/report.zig +346 -0
  198. package/src/report_html.zig +63 -0
  199. package/src/report_values.zig +27 -0
  200. package/src/run_options.zig +152 -0
  201. package/src/runner.zig +280 -0
  202. package/src/runner_actions.zig +109 -0
  203. package/src/runner_config.zig +6 -0
  204. package/src/runner_diagnostics.zig +268 -0
  205. package/src/runner_events.zig +170 -0
  206. package/src/runner_native.zig +88 -0
  207. package/src/runner_waits.zig +300 -0
  208. package/src/scaffold.zig +472 -0
  209. package/src/scenario.zig +346 -0
  210. package/src/scenario_fields.zig +50 -0
  211. package/src/schema_registry.zig +53 -0
  212. package/src/selector.zig +84 -0
  213. package/src/semantic.zig +171 -0
  214. package/src/trace.zig +315 -0
  215. package/src/trace_json.zig +340 -0
  216. package/src/trace_summary.zig +218 -0
  217. package/src/trace_summary_diagnostic.zig +202 -0
  218. package/src/types.zig +120 -0
  219. package/src/uiautomator.zig +164 -0
  220. package/src/validation.zig +187 -0
  221. package/src/version.zig +22 -0
  222. package/viewer/app.js +373 -0
  223. package/viewer/index.html +126 -0
  224. package/viewer/parser.js +233 -0
  225. package/viewer/styles.css +585 -0
@@ -0,0 +1,170 @@
1
+ const std = @import("std");
2
+ const runner_diagnostics = @import("runner_diagnostics.zig");
3
+ const selector = @import("selector.zig");
4
+ const trace = @import("trace.zig");
5
+ const types = @import("types.zig");
6
+
7
+ pub fn eventString(allocator: std.mem.Allocator, value: []const u8) ![]const u8 {
8
+ var buffer = std.ArrayList(u8).empty;
9
+ errdefer buffer.deinit(allocator);
10
+ try buffer.writer(allocator).writeAll("{\"value\":");
11
+ try trace.writeJsonString(buffer.writer(allocator), value);
12
+ try buffer.writer(allocator).writeAll("}");
13
+ return try buffer.toOwnedSlice(allocator);
14
+ }
15
+
16
+ pub fn recordNativeWait(tw: *trace.TraceWriter, kind: []const u8, wanted: selector.Selector, matched_index: ?usize) !void {
17
+ var payload = std.ArrayList(u8).empty;
18
+ defer payload.deinit(tw.allocator);
19
+ try payload.writer(tw.allocator).writeAll("{\"status\":\"ok\",\"strategy\":\"nativeSelector\"");
20
+ if (matched_index) |index| try payload.writer(tw.allocator).print(",\"matchedIndex\":{d}", .{index});
21
+ try payload.writer(tw.allocator).writeAll(",\"selector\":");
22
+ try trace.writeSelectorJson(payload.writer(tw.allocator), wanted);
23
+ try payload.writer(tw.allocator).writeAll("}");
24
+ try tw.recordEvent(kind, payload.items);
25
+ }
26
+
27
+ pub fn recordNativeWaitTimeout(tw: *trace.TraceWriter, kind: []const u8, selectors: []const selector.Selector) !void {
28
+ var payload = std.ArrayList(u8).empty;
29
+ defer payload.deinit(tw.allocator);
30
+ try payload.writer(tw.allocator).writeAll("{\"status\":\"timeout\",\"strategy\":\"nativeSelector\",\"selectors\":[");
31
+ for (selectors, 0..) |wanted, index| {
32
+ if (index > 0) try payload.writer(tw.allocator).writeAll(",");
33
+ try trace.writeSelectorJson(payload.writer(tw.allocator), wanted);
34
+ }
35
+ try payload.writer(tw.allocator).writeAll("]}");
36
+ try tw.recordEvent(kind, payload.items);
37
+ }
38
+
39
+ pub fn recordNativeWaitTimeoutWithDiagnostics(device: anytype, tw: *trace.TraceWriter, kind: []const u8, selectors: []const selector.Selector) !void {
40
+ var snap = device.snapshot(tw) catch {
41
+ try recordNativeWaitTimeout(tw, kind, selectors);
42
+ return;
43
+ };
44
+ defer snap.deinit(device.allocator);
45
+ try recordDiagnosticWithStrategy(tw, kind, "timeout", "nativeSelector", selectors, snap);
46
+ }
47
+
48
+ pub fn recordSelectorEvent(tw: *trace.TraceWriter, kind: []const u8, wanted: selector.Selector) !void {
49
+ var payload = std.ArrayList(u8).empty;
50
+ defer payload.deinit(tw.allocator);
51
+ try payload.writer(tw.allocator).writeAll("{\"selector\":");
52
+ try trace.writeSelectorJson(payload.writer(tw.allocator), wanted);
53
+ try payload.writer(tw.allocator).writeAll("}");
54
+ try tw.recordEvent(kind, payload.items);
55
+ }
56
+
57
+ pub fn recordActionStatus(tw: *trace.TraceWriter, kind: []const u8, status: []const u8, err: ?anyerror, url: ?[]const u8) !void {
58
+ var payload = std.ArrayList(u8).empty;
59
+ defer payload.deinit(tw.allocator);
60
+ const out = payload.writer(tw.allocator);
61
+ try out.writeAll("{\"status\":");
62
+ try trace.writeJsonString(out, status);
63
+ if (err) |actual| {
64
+ try out.writeAll(",\"error\":");
65
+ try trace.writeJsonString(out, @errorName(actual));
66
+ }
67
+ if (url) |value| {
68
+ try out.writeAll(",\"url\":");
69
+ try trace.writeJsonString(out, value);
70
+ }
71
+ try out.writeAll("}");
72
+ try tw.recordEvent(kind, payload.items);
73
+ }
74
+
75
+ pub fn recordStepError(tw: *trace.TraceWriter, index: usize, err: anyerror) !void {
76
+ const payload = try std.fmt.allocPrint(
77
+ tw.allocator,
78
+ "{{\"index\":{d},\"error\":\"{s}\"}}",
79
+ .{ index, @errorName(err) },
80
+ );
81
+ defer tw.allocator.free(payload);
82
+ try tw.recordEvent("step.error", payload);
83
+ }
84
+
85
+ pub fn recordScenarioEnd(
86
+ tw: *trace.TraceWriter,
87
+ name: []const u8,
88
+ status: []const u8,
89
+ failed_index: ?usize,
90
+ err: ?anyerror,
91
+ ) !void {
92
+ var payload = std.ArrayList(u8).empty;
93
+ defer payload.deinit(tw.allocator);
94
+ const writer = payload.writer(tw.allocator);
95
+ try writer.writeAll("{\"value\":");
96
+ try trace.writeJsonString(writer, name);
97
+ try writer.writeAll(",\"status\":");
98
+ try trace.writeJsonString(writer, status);
99
+ if (failed_index) |index| {
100
+ try writer.print(",\"failedStepIndex\":{d}", .{index});
101
+ }
102
+ if (err) |actual| {
103
+ try writer.writeAll(",\"error\":");
104
+ try trace.writeJsonString(writer, @errorName(actual));
105
+ }
106
+ try writer.writeAll("}");
107
+ try tw.recordEvent("scenario.end", payload.items);
108
+ }
109
+
110
+ pub fn recordSelectorMiss(
111
+ tw: *trace.TraceWriter,
112
+ kind: []const u8,
113
+ wanted: selector.Selector,
114
+ snap: types.ObservationSnapshot,
115
+ ) !void {
116
+ const selectors = [_]selector.Selector{wanted};
117
+ try recordDiagnostic(tw, kind, "not_found", selectors[0..], snap);
118
+ }
119
+
120
+ pub fn recordWaitTimeout(
121
+ tw: *trace.TraceWriter,
122
+ kind: []const u8,
123
+ selectors: []const selector.Selector,
124
+ snap: types.ObservationSnapshot,
125
+ ) !void {
126
+ try recordDiagnostic(tw, kind, "timeout", selectors, snap);
127
+ }
128
+
129
+ pub fn recordObservationRetry(tw: *trace.TraceWriter, kind: []const u8, err: anyerror) !void {
130
+ var payload = std.ArrayList(u8).empty;
131
+ defer payload.deinit(tw.allocator);
132
+ const writer = payload.writer(tw.allocator);
133
+ try writer.writeAll("{\"status\":\"retry\",\"kind\":");
134
+ try trace.writeJsonString(writer, kind);
135
+ try writer.writeAll(",\"error\":");
136
+ try trace.writeJsonString(writer, @errorName(err));
137
+ try writer.writeAll("}");
138
+ try tw.recordEvent("observe.retry", payload.items);
139
+ }
140
+
141
+ pub fn recordDiagnostic(
142
+ tw: *trace.TraceWriter,
143
+ kind: []const u8,
144
+ status: []const u8,
145
+ selectors: []const selector.Selector,
146
+ snap: types.ObservationSnapshot,
147
+ ) !void {
148
+ try recordDiagnosticWithStrategy(tw, kind, status, null, selectors, snap);
149
+ }
150
+
151
+ pub fn recordDiagnosticWithStrategy(
152
+ tw: *trace.TraceWriter,
153
+ kind: []const u8,
154
+ status: []const u8,
155
+ strategy: ?[]const u8,
156
+ selectors: []const selector.Selector,
157
+ snap: types.ObservationSnapshot,
158
+ ) !void {
159
+ try runner_diagnostics.record(tw, kind, status, strategy, selectors, snap);
160
+ }
161
+
162
+ pub fn writeSelectorDiagnosticJson(
163
+ writer: anytype,
164
+ status: []const u8,
165
+ strategy: ?[]const u8,
166
+ selectors: []const selector.Selector,
167
+ snap: types.ObservationSnapshot,
168
+ ) !void {
169
+ try runner_diagnostics.writeSelectorDiagnosticJson(writer, status, strategy, selectors, snap);
170
+ }
@@ -0,0 +1,88 @@
1
+ const std = @import("std");
2
+ const selector = @import("selector.zig");
3
+ const trace = @import("trace.zig");
4
+
5
+ pub fn tryTapSelector(
6
+ device: anytype,
7
+ wanted: selector.Selector,
8
+ writer: ?*trace.TraceWriter,
9
+ settle_ms: u64,
10
+ ) !bool {
11
+ if (!@hasDecl(@TypeOf(device.*), "tapBySelector")) return false;
12
+ const tapped = device.tapBySelector(wanted) catch |err| {
13
+ if (writer) |tw| try recordSelectorActionFailure(tw, "ui.tap", wanted, err);
14
+ return err;
15
+ };
16
+ if (!tapped) return false;
17
+ if (writer) |tw| try recordSelectorAction(tw, "ui.tap", wanted, null);
18
+ try device.settle(settle_ms);
19
+ return true;
20
+ }
21
+
22
+ pub fn tryTypeTextSelector(
23
+ device: anytype,
24
+ wanted: selector.Selector,
25
+ text: []const u8,
26
+ writer: ?*trace.TraceWriter,
27
+ settle_ms: u64,
28
+ ) !bool {
29
+ if (!@hasDecl(@TypeOf(device.*), "typeTextBySelector")) return false;
30
+ const typed = device.typeTextBySelector(wanted, text) catch |err| {
31
+ if (writer) |tw| try recordSelectorActionFailure(tw, "ui.type", wanted, err);
32
+ return err;
33
+ };
34
+ if (!typed) return false;
35
+ if (writer) |tw| try recordSelectorAction(tw, "ui.type", wanted, null);
36
+ try device.settle(settle_ms);
37
+ return true;
38
+ }
39
+
40
+ pub fn tryEraseTextSelector(
41
+ device: anytype,
42
+ wanted: selector.Selector,
43
+ max_chars: u32,
44
+ writer: ?*trace.TraceWriter,
45
+ settle_ms: u64,
46
+ ) !bool {
47
+ if (!@hasDecl(@TypeOf(device.*), "eraseTextBySelector")) return false;
48
+ const erased = device.eraseTextBySelector(wanted, max_chars) catch |err| {
49
+ if (writer) |tw| try recordSelectorActionFailure(tw, "ui.eraseText", wanted, err);
50
+ return err;
51
+ };
52
+ if (!erased) return false;
53
+ if (writer) |tw| try recordSelectorAction(tw, "ui.eraseText", wanted, max_chars);
54
+ try device.settle(settle_ms);
55
+ return true;
56
+ }
57
+
58
+ fn recordSelectorAction(
59
+ tw: *trace.TraceWriter,
60
+ kind: []const u8,
61
+ wanted: selector.Selector,
62
+ max_chars: ?u32,
63
+ ) !void {
64
+ var payload = std.ArrayList(u8).empty;
65
+ defer payload.deinit(tw.allocator);
66
+ try payload.writer(tw.allocator).writeAll("{\"status\":\"ok\",\"strategy\":\"nativeSelector\",\"selector\":");
67
+ try trace.writeSelectorJson(payload.writer(tw.allocator), wanted);
68
+ if (max_chars) |value| try payload.writer(tw.allocator).print(",\"maxChars\":{d}", .{value});
69
+ try payload.writer(tw.allocator).writeAll("}");
70
+ try tw.recordEvent(kind, payload.items);
71
+ }
72
+
73
+ fn recordSelectorActionFailure(
74
+ tw: *trace.TraceWriter,
75
+ kind: []const u8,
76
+ wanted: selector.Selector,
77
+ err: anyerror,
78
+ ) !void {
79
+ var payload = std.ArrayList(u8).empty;
80
+ defer payload.deinit(tw.allocator);
81
+ const out = payload.writer(tw.allocator);
82
+ try out.writeAll("{\"status\":\"failed\",\"strategy\":\"nativeSelector\",\"error\":");
83
+ try trace.writeJsonString(out, @errorName(err));
84
+ try out.writeAll(",\"selector\":");
85
+ try trace.writeSelectorJson(out, wanted);
86
+ try out.writeAll("}");
87
+ try tw.recordEvent(kind, payload.items);
88
+ }
@@ -0,0 +1,300 @@
1
+ const std = @import("std");
2
+ const health = @import("health.zig");
3
+ const runner_config = @import("runner_config.zig");
4
+ const runner_events = @import("runner_events.zig");
5
+ const runner_native = @import("runner_native.zig");
6
+ const scenario = @import("scenario.zig");
7
+ const selector = @import("selector.zig");
8
+ const trace = @import("trace.zig");
9
+
10
+ const RunOptions = runner_config.RunOptions;
11
+
12
+ pub fn waitUntilVisible(
13
+ device: anytype,
14
+ wanted: selector.Selector,
15
+ timeout_ms: u64,
16
+ writer: ?*trace.TraceWriter,
17
+ options: RunOptions,
18
+ ) !bool {
19
+ const deadline = std.time.milliTimestamp() + @as(i64, @intCast(timeout_ms));
20
+ while (true) {
21
+ if (try nativeVisibleBySelector(device, wanted)) |visible| {
22
+ if (visible) {
23
+ if (writer) |tw| try runner_events.recordNativeWait(tw, "wait.visible", wanted, null);
24
+ return true;
25
+ }
26
+ if (std.time.milliTimestamp() >= deadline) {
27
+ if (writer) |tw| try runner_events.recordNativeWaitTimeoutWithDiagnostics(device, tw, "wait.visible", &[_]selector.Selector{wanted});
28
+ return false;
29
+ }
30
+ try sleepMs(options.poll_ms);
31
+ continue;
32
+ }
33
+ var snap = device.snapshot(writer) catch |err| {
34
+ if (try retryTransientObservation(err, "wait.visible", writer, deadline, options)) continue;
35
+ return err;
36
+ };
37
+ defer snap.deinit(device.allocator);
38
+ if (selector.find(snap.nodes, wanted) != null) {
39
+ if (writer) |tw| try tw.recordEvent("wait.visible", "{\"status\":\"ok\"}");
40
+ return true;
41
+ }
42
+ if (std.time.milliTimestamp() >= deadline) {
43
+ if (writer) |tw| {
44
+ const selectors = [_]selector.Selector{wanted};
45
+ try runner_events.recordWaitTimeout(tw, "wait.visible", selectors[0..], snap);
46
+ }
47
+ return false;
48
+ }
49
+ try sleepMs(options.poll_ms);
50
+ }
51
+ }
52
+
53
+ pub fn waitUntilNotVisible(
54
+ device: anytype,
55
+ wanted: selector.Selector,
56
+ timeout_ms: u64,
57
+ writer: ?*trace.TraceWriter,
58
+ options: RunOptions,
59
+ ) !bool {
60
+ const deadline = std.time.milliTimestamp() + @as(i64, @intCast(timeout_ms));
61
+ while (true) {
62
+ if (try nativeVisibleBySelector(device, wanted)) |visible| {
63
+ if (!visible) {
64
+ if (writer) |tw| try runner_events.recordNativeWait(tw, "wait.notVisible", wanted, null);
65
+ return true;
66
+ }
67
+ if (std.time.milliTimestamp() >= deadline) {
68
+ if (writer) |tw| try runner_events.recordNativeWaitTimeoutWithDiagnostics(device, tw, "wait.notVisible", &[_]selector.Selector{wanted});
69
+ return false;
70
+ }
71
+ try sleepMs(options.poll_ms);
72
+ continue;
73
+ }
74
+ var snap = device.snapshot(writer) catch |err| {
75
+ if (try retryTransientObservation(err, "wait.notVisible", writer, deadline, options)) continue;
76
+ return err;
77
+ };
78
+ defer snap.deinit(device.allocator);
79
+ if (selector.find(snap.nodes, wanted) == null) {
80
+ if (writer) |tw| try tw.recordEvent("wait.notVisible", "{\"status\":\"ok\"}");
81
+ return true;
82
+ }
83
+ if (std.time.milliTimestamp() >= deadline) {
84
+ if (writer) |tw| {
85
+ const selectors = [_]selector.Selector{wanted};
86
+ try runner_events.recordWaitTimeout(tw, "wait.notVisible", selectors[0..], snap);
87
+ }
88
+ return false;
89
+ }
90
+ try sleepMs(options.poll_ms);
91
+ }
92
+ }
93
+
94
+ pub fn waitUntilAnyVisible(
95
+ device: anytype,
96
+ selectors: []const selector.Selector,
97
+ timeout_ms: u64,
98
+ writer: ?*trace.TraceWriter,
99
+ options: RunOptions,
100
+ ) !?usize {
101
+ const deadline = std.time.milliTimestamp() + @as(i64, @intCast(timeout_ms));
102
+ while (true) {
103
+ var all_native = true;
104
+ for (selectors, 0..) |wanted, index| {
105
+ if (try nativeVisibleBySelector(device, wanted)) |visible| {
106
+ if (visible) {
107
+ if (writer) |tw| try runner_events.recordNativeWait(tw, "wait.any", wanted, index);
108
+ return index;
109
+ }
110
+ } else {
111
+ all_native = false;
112
+ break;
113
+ }
114
+ }
115
+ if (all_native) {
116
+ if (std.time.milliTimestamp() >= deadline) {
117
+ if (writer) |tw| try runner_events.recordNativeWaitTimeoutWithDiagnostics(device, tw, "wait.any", selectors);
118
+ return null;
119
+ }
120
+ try sleepMs(options.poll_ms);
121
+ continue;
122
+ }
123
+ var snap = device.snapshot(writer) catch |err| {
124
+ if (try retryTransientObservation(err, "wait.any", writer, deadline, options)) continue;
125
+ return err;
126
+ };
127
+ defer snap.deinit(device.allocator);
128
+ for (selectors, 0..) |wanted, index| {
129
+ if (selector.find(snap.nodes, wanted)) |node| {
130
+ if (writer) |tw| {
131
+ var payload = std.ArrayList(u8).empty;
132
+ defer payload.deinit(tw.allocator);
133
+ try payload.writer(tw.allocator).print("{{\"status\":\"ok\",\"matchedIndex\":{d},\"target\":\"{s}\",\"selector\":", .{ index, node.stable_id });
134
+ try trace.writeSelectorJson(payload.writer(tw.allocator), wanted);
135
+ try payload.writer(tw.allocator).writeAll("}");
136
+ try tw.recordEvent("wait.any", payload.items);
137
+ }
138
+ return index;
139
+ }
140
+ }
141
+ if (std.time.milliTimestamp() >= deadline) {
142
+ if (writer) |tw| try runner_events.recordWaitTimeout(tw, "wait.any", selectors, snap);
143
+ return null;
144
+ }
145
+ try sleepMs(options.poll_ms);
146
+ }
147
+ }
148
+
149
+ pub fn assertNoneVisible(
150
+ device: anytype,
151
+ selectors: []const selector.Selector,
152
+ timeout_ms: u64,
153
+ writer: ?*trace.TraceWriter,
154
+ options: RunOptions,
155
+ ) !bool {
156
+ const deadline = std.time.milliTimestamp() + @as(i64, @intCast(timeout_ms));
157
+ while (true) {
158
+ var snap = device.snapshot(writer) catch |err| {
159
+ if (try retryTransientObservation(err, "assert.noneVisible", writer, deadline, options)) continue;
160
+ return err;
161
+ };
162
+ defer snap.deinit(device.allocator);
163
+
164
+ var matched = false;
165
+ for (selectors) |wanted| {
166
+ if (selector.find(snap.nodes, wanted) != null) {
167
+ matched = true;
168
+ break;
169
+ }
170
+ }
171
+
172
+ if (!matched) {
173
+ if (writer) |tw| try tw.recordEvent("assert.noneVisible", "{\"status\":\"ok\"}");
174
+ return true;
175
+ }
176
+
177
+ if (std.time.milliTimestamp() >= deadline) {
178
+ if (writer) |tw| try runner_events.recordDiagnostic(tw, "assert.noneVisible", "visible", selectors, snap);
179
+ return false;
180
+ }
181
+
182
+ try sleepMs(options.poll_ms);
183
+ }
184
+ }
185
+
186
+ pub fn assertHealthy(
187
+ device: anytype,
188
+ timeout_ms: u64,
189
+ writer: ?*trace.TraceWriter,
190
+ options: RunOptions,
191
+ ) !bool {
192
+ const health_selectors = health.defaultSelectors();
193
+ const deadline = std.time.milliTimestamp() + @as(i64, @intCast(timeout_ms));
194
+ while (true) {
195
+ var snap = device.snapshot(writer) catch |err| {
196
+ if (try retryTransientObservation(err, "assert.healthy", writer, deadline, options)) continue;
197
+ return err;
198
+ };
199
+ defer snap.deinit(device.allocator);
200
+
201
+ if (!health.hasUnhealthyOverlay(snap.nodes)) {
202
+ if (writer) |tw| try tw.recordEvent("assert.healthy", "{\"status\":\"ok\"}");
203
+ return true;
204
+ }
205
+
206
+ if (std.time.milliTimestamp() >= deadline) {
207
+ if (writer) |tw| try runner_events.recordDiagnostic(tw, "assert.healthy", "unhealthy", health_selectors, snap);
208
+ return false;
209
+ }
210
+
211
+ try sleepMs(options.poll_ms);
212
+ }
213
+ }
214
+
215
+ pub fn scrollUntilVisible(
216
+ device: anytype,
217
+ wanted: selector.Selector,
218
+ timeout_ms: u64,
219
+ direction: scenario.ScrollDirection,
220
+ writer: ?*trace.TraceWriter,
221
+ options: RunOptions,
222
+ ) !bool {
223
+ const deadline = std.time.milliTimestamp() + @as(i64, @intCast(timeout_ms));
224
+ while (true) {
225
+ var snap = device.snapshot(writer) catch |err| {
226
+ if (try retryTransientObservation(err, "ui.scrollUntilVisible", writer, deadline, options)) continue;
227
+ return err;
228
+ };
229
+ defer snap.deinit(device.allocator);
230
+ if (selector.find(snap.nodes, wanted)) |node| {
231
+ if (writer) |tw| {
232
+ var payload = std.ArrayList(u8).empty;
233
+ defer payload.deinit(tw.allocator);
234
+ try payload.writer(tw.allocator).print("{{\"status\":\"ok\",\"target\":\"{s}\",\"selector\":", .{node.stable_id});
235
+ try trace.writeSelectorJson(payload.writer(tw.allocator), wanted);
236
+ try payload.writer(tw.allocator).writeAll("}");
237
+ try tw.recordEvent("ui.scrollUntilVisible", payload.items);
238
+ }
239
+ return true;
240
+ }
241
+ if (std.time.milliTimestamp() >= deadline) {
242
+ if (writer) |tw| {
243
+ const selectors = [_]selector.Selector{wanted};
244
+ try runner_events.recordWaitTimeout(tw, "ui.scrollUntilVisible", selectors[0..], snap);
245
+ }
246
+ return false;
247
+ }
248
+
249
+ const width = if (snap.viewport.width == 0) @as(i32, 720) else @as(i32, @intCast(snap.viewport.width));
250
+ const height = if (snap.viewport.height == 0) @as(i32, 1280) else @as(i32, @intCast(snap.viewport.height));
251
+ const x = @divTrunc(width, 2);
252
+ const start_y = switch (direction) {
253
+ .down => @divTrunc(height * 4, 5),
254
+ .up => @divTrunc(height * 3, 10),
255
+ };
256
+ const end_y = switch (direction) {
257
+ .down => @divTrunc(height * 3, 10),
258
+ .up => @divTrunc(height * 4, 5),
259
+ };
260
+ try device.swipe(x, start_y, x, end_y, 350);
261
+ if (writer) |tw| {
262
+ const payload = try std.fmt.allocPrint(tw.allocator, "{{\"direction\":\"{s}\",\"x\":{d},\"y1\":{d},\"y2\":{d}}}", .{
263
+ if (direction == .down) "down" else "up",
264
+ x,
265
+ start_y,
266
+ end_y,
267
+ });
268
+ defer tw.allocator.free(payload);
269
+ try tw.recordEvent("ui.scroll", payload);
270
+ }
271
+ try settleDevice(device, options);
272
+ }
273
+ }
274
+
275
+ fn nativeVisibleBySelector(device: anytype, wanted: selector.Selector) !?bool {
276
+ if (!@hasDecl(@TypeOf(device.*), "visibleBySelector")) return null;
277
+ return try device.visibleBySelector(wanted);
278
+ }
279
+
280
+ fn retryTransientObservation(
281
+ err: anyerror,
282
+ kind: []const u8,
283
+ writer: ?*trace.TraceWriter,
284
+ deadline: i64,
285
+ options: RunOptions,
286
+ ) !bool {
287
+ if (err != error.CommandTimedOut) return false;
288
+ if (std.time.milliTimestamp() >= deadline) return false;
289
+ if (writer) |tw| try runner_events.recordObservationRetry(tw, kind, err);
290
+ try sleepMs(options.poll_ms);
291
+ return true;
292
+ }
293
+
294
+ fn settleDevice(device: anytype, options: RunOptions) !void {
295
+ try device.settle(options.settle_ms);
296
+ }
297
+
298
+ fn sleepMs(ms: u64) !void {
299
+ std.Thread.sleep(ms * std.time.ns_per_ms);
300
+ }