zeno-mobile-runner 0.1.2 → 0.1.8
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 +162 -3
- package/FEATURES.md +50 -7
- package/README.md +133 -7
- 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 +202 -0
- package/docs/ai-agents.md +87 -6
- package/docs/benchmarking.md +10 -3
- package/docs/clients.md +10 -6
- package/docs/demo.md +4 -0
- package/docs/expo-smoke.md +79 -0
- package/docs/install.md +3 -2
- package/docs/npm.md +58 -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 +3 -0
- package/docs/troubleshooting.md +1 -1
- package/npm/agents.mjs +16 -0
- package/npm/build-zmr.mjs +1 -1
- package/npm/commands.mjs +9 -5
- package/npm/postinstall.mjs +28 -2
- package/npm/verify-publish.mjs +36 -0
- package/package.json +2 -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/install-ios-shim.sh +79 -14
- 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 +3 -0
- package/shims/ios/ZMRShimUITestCase.swift +41 -11
- 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_inspect.zig +310 -0
- package/src/cli_output.zig +26 -2
- package/src/cli_run.zig +28 -0
- package/src/cli_trace.zig +8 -0
- package/src/errors.zig +9 -0
- package/src/ios.zig +11 -4
- package/src/ios_lifecycle.zig +36 -0
- package/src/ios_shim.zig +42 -0
- 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 +24 -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
|
@@ -0,0 +1,924 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
|
|
3
|
+
const cli_output = @import("cli_output.zig");
|
|
4
|
+
const trace = @import("trace.zig");
|
|
5
|
+
const version = @import("version.zig");
|
|
6
|
+
|
|
7
|
+
const max_draft_selectors = 3;
|
|
8
|
+
|
|
9
|
+
pub const ReplaySummary = struct {
|
|
10
|
+
enabled: bool = false,
|
|
11
|
+
event_count: usize = 0,
|
|
12
|
+
step_count: usize = 0,
|
|
13
|
+
skipped_event_count: usize = 0,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
pub const ParsedArgs = struct {
|
|
17
|
+
from_trace: ?[]const u8 = null,
|
|
18
|
+
out_path: ?[]const u8 = null,
|
|
19
|
+
name: ?[]const u8 = null,
|
|
20
|
+
app_id: ?[]const u8 = null,
|
|
21
|
+
include_actions: bool = false,
|
|
22
|
+
force: bool = false,
|
|
23
|
+
json: bool = false,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
pub const DraftSummary = struct {
|
|
27
|
+
ok: bool = true,
|
|
28
|
+
out_path: []const u8,
|
|
29
|
+
trace_dir: []const u8,
|
|
30
|
+
source_snapshot: []const u8,
|
|
31
|
+
name: []const u8,
|
|
32
|
+
app_id: ?[]const u8 = null,
|
|
33
|
+
selector_count: usize,
|
|
34
|
+
step_count: usize,
|
|
35
|
+
replay: ReplaySummary = .{},
|
|
36
|
+
warnings: []const []const u8 = &.{},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
pub const OwnedDraft = struct {
|
|
40
|
+
summary: DraftSummary,
|
|
41
|
+
owned_strings: std.ArrayList([]const u8),
|
|
42
|
+
warnings: std.ArrayList([]const u8),
|
|
43
|
+
|
|
44
|
+
pub fn deinit(self: *OwnedDraft, allocator: std.mem.Allocator) void {
|
|
45
|
+
for (self.owned_strings.items) |value| allocator.free(value);
|
|
46
|
+
self.owned_strings.deinit(allocator);
|
|
47
|
+
self.warnings.deinit(allocator);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const TraceMetadata = struct {
|
|
52
|
+
scenario_name: []const u8,
|
|
53
|
+
app_id: ?[]const u8,
|
|
54
|
+
events_path: []const u8,
|
|
55
|
+
artifacts_dir: []const u8,
|
|
56
|
+
snapshot_count: usize,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const SelectorKind = enum {
|
|
60
|
+
resource_id,
|
|
61
|
+
content_desc,
|
|
62
|
+
text,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const DraftSelector = struct {
|
|
66
|
+
kind: SelectorKind,
|
|
67
|
+
value: []const u8,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const DraftActionStep = struct {
|
|
71
|
+
json: []const u8,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const ReplayDraft = struct {
|
|
75
|
+
steps: []DraftActionStep,
|
|
76
|
+
summary: ReplaySummary,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
pub fn parseArgs(args: []const []const u8) !ParsedArgs {
|
|
80
|
+
var parsed = ParsedArgs{};
|
|
81
|
+
var index: usize = 0;
|
|
82
|
+
while (index < args.len) : (index += 1) {
|
|
83
|
+
const arg = args[index];
|
|
84
|
+
if (std.mem.eql(u8, arg, "--from-trace")) {
|
|
85
|
+
index += 1;
|
|
86
|
+
parsed.from_trace = if (index < args.len) args[index] else return error.MissingTraceDir;
|
|
87
|
+
} else if (std.mem.eql(u8, arg, "--out")) {
|
|
88
|
+
index += 1;
|
|
89
|
+
parsed.out_path = if (index < args.len) args[index] else return error.MissingDraftOut;
|
|
90
|
+
} else if (std.mem.eql(u8, arg, "--name")) {
|
|
91
|
+
index += 1;
|
|
92
|
+
parsed.name = if (index < args.len) args[index] else return error.MissingParam;
|
|
93
|
+
} else if (std.mem.eql(u8, arg, "--app-id")) {
|
|
94
|
+
index += 1;
|
|
95
|
+
parsed.app_id = if (index < args.len) args[index] else return error.MissingAppId;
|
|
96
|
+
} else if (std.mem.eql(u8, arg, "--include-actions")) {
|
|
97
|
+
parsed.include_actions = true;
|
|
98
|
+
} else if (std.mem.eql(u8, arg, "--force")) {
|
|
99
|
+
parsed.force = true;
|
|
100
|
+
} else if (std.mem.eql(u8, arg, "--json")) {
|
|
101
|
+
parsed.json = true;
|
|
102
|
+
} else {
|
|
103
|
+
return error.UnknownFlag;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (parsed.from_trace == null) return error.MissingTraceDir;
|
|
108
|
+
if (parsed.out_path == null) return error.MissingDraftOut;
|
|
109
|
+
return parsed;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
pub fn run(allocator: std.mem.Allocator, args: *std.process.ArgIterator) !void {
|
|
113
|
+
var raw_args = std.ArrayList([]const u8).empty;
|
|
114
|
+
defer raw_args.deinit(allocator);
|
|
115
|
+
while (args.next()) |arg| try raw_args.append(allocator, arg);
|
|
116
|
+
|
|
117
|
+
const parsed = try parseArgs(raw_args.items);
|
|
118
|
+
var draft = try draftFromTrace(allocator, parsed);
|
|
119
|
+
defer draft.deinit(allocator);
|
|
120
|
+
|
|
121
|
+
const stdout = std.fs.File.stdout().deprecatedWriter();
|
|
122
|
+
if (parsed.json) {
|
|
123
|
+
try writeJson(stdout, draft.summary);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try stdout.print("wrote {s}\n", .{draft.summary.out_path});
|
|
128
|
+
try stdout.writeAll("next: zmr validate --json ");
|
|
129
|
+
try cli_output.writeShellArg(stdout, draft.summary.out_path);
|
|
130
|
+
try stdout.writeAll("\n");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
pub fn draftFromTrace(allocator: std.mem.Allocator, parsed: ParsedArgs) !OwnedDraft {
|
|
134
|
+
const from_trace = parsed.from_trace orelse return error.MissingTraceDir;
|
|
135
|
+
const out_path = parsed.out_path orelse return error.MissingDraftOut;
|
|
136
|
+
|
|
137
|
+
var owned = OwnedDraft{
|
|
138
|
+
.summary = .{
|
|
139
|
+
.out_path = undefined,
|
|
140
|
+
.trace_dir = undefined,
|
|
141
|
+
.source_snapshot = undefined,
|
|
142
|
+
.name = undefined,
|
|
143
|
+
.selector_count = 0,
|
|
144
|
+
.step_count = 0,
|
|
145
|
+
},
|
|
146
|
+
.owned_strings = .empty,
|
|
147
|
+
.warnings = .empty,
|
|
148
|
+
};
|
|
149
|
+
errdefer owned.deinit(allocator);
|
|
150
|
+
|
|
151
|
+
const manifest_path = try std.fs.path.join(allocator, &.{ from_trace, "trace.json" });
|
|
152
|
+
defer allocator.free(manifest_path);
|
|
153
|
+
const manifest_content = try std.fs.cwd().readFileAlloc(allocator, manifest_path, 1024 * 1024);
|
|
154
|
+
defer allocator.free(manifest_content);
|
|
155
|
+
|
|
156
|
+
var parsed_manifest = try std.json.parseFromSlice(std.json.Value, allocator, manifest_content, .{});
|
|
157
|
+
defer parsed_manifest.deinit();
|
|
158
|
+
if (parsed_manifest.value != .object) return error.InvalidTraceManifest;
|
|
159
|
+
const metadata = traceMetadata(parsed_manifest.value.object);
|
|
160
|
+
|
|
161
|
+
const snapshot_path = try latestSnapshotPath(allocator, &owned, from_trace, metadata);
|
|
162
|
+
const selectors = try parseSemanticSelectors(allocator, snapshot_path, &owned);
|
|
163
|
+
defer freeSelectors(allocator, selectors);
|
|
164
|
+
const replay = if (parsed.include_actions)
|
|
165
|
+
try parseReplaySteps(allocator, &owned, from_trace, metadata)
|
|
166
|
+
else
|
|
167
|
+
ReplayDraft{
|
|
168
|
+
.steps = try allocator.alloc(DraftActionStep, 0),
|
|
169
|
+
.summary = .{},
|
|
170
|
+
};
|
|
171
|
+
defer allocator.free(replay.steps);
|
|
172
|
+
|
|
173
|
+
const draft_name = if (parsed.name) |explicit|
|
|
174
|
+
try ownString(allocator, &owned, explicit)
|
|
175
|
+
else if (metadata.scenario_name.len > 0)
|
|
176
|
+
try ownFmt(allocator, &owned, "draft from {s}", .{metadata.scenario_name})
|
|
177
|
+
else
|
|
178
|
+
try ownString(allocator, &owned, "draft from trace");
|
|
179
|
+
|
|
180
|
+
const app_id = if (parsed.app_id) |explicit|
|
|
181
|
+
try ownString(allocator, &owned, explicit)
|
|
182
|
+
else if (metadata.app_id) |from_manifest|
|
|
183
|
+
try ownString(allocator, &owned, from_manifest)
|
|
184
|
+
else
|
|
185
|
+
null;
|
|
186
|
+
|
|
187
|
+
try writeScenarioFile(out_path, draft_name, app_id, replay.steps, selectors, parsed.force);
|
|
188
|
+
|
|
189
|
+
owned.summary.out_path = try ownString(allocator, &owned, out_path);
|
|
190
|
+
owned.summary.trace_dir = try ownString(allocator, &owned, from_trace);
|
|
191
|
+
owned.summary.source_snapshot = snapshot_path;
|
|
192
|
+
owned.summary.name = draft_name;
|
|
193
|
+
owned.summary.app_id = app_id;
|
|
194
|
+
owned.summary.selector_count = selectors.len;
|
|
195
|
+
owned.summary.step_count = (if (replay.steps.len > 0) replay.steps.len else 1) + 1 + selectors.len;
|
|
196
|
+
owned.summary.replay = replay.summary;
|
|
197
|
+
try appendWarning(allocator, &owned, "draft requires human review before commit");
|
|
198
|
+
if (selectors.len == 0) try appendWarning(allocator, &owned, "no stable visible selectors were found in the semantic snapshot");
|
|
199
|
+
owned.summary.warnings = owned.warnings.items;
|
|
200
|
+
|
|
201
|
+
return owned;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
fn traceMetadata(object: std.json.ObjectMap) TraceMetadata {
|
|
205
|
+
return .{
|
|
206
|
+
.scenario_name = optionalString(object, "scenarioName") orelse "",
|
|
207
|
+
.app_id = optionalString(object, "appId"),
|
|
208
|
+
.events_path = optionalString(object, "eventsPath") orelse "events.jsonl",
|
|
209
|
+
.artifacts_dir = optionalString(object, "artifactsDir") orelse "artifacts",
|
|
210
|
+
.snapshot_count = optionalUsize(object, "snapshotCount") orelse 0,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
fn parseReplaySteps(
|
|
215
|
+
allocator: std.mem.Allocator,
|
|
216
|
+
owned: *OwnedDraft,
|
|
217
|
+
trace_dir: []const u8,
|
|
218
|
+
metadata: TraceMetadata,
|
|
219
|
+
) !ReplayDraft {
|
|
220
|
+
const events_path = try std.fs.path.join(allocator, &.{ trace_dir, metadata.events_path });
|
|
221
|
+
defer allocator.free(events_path);
|
|
222
|
+
const content = std.fs.cwd().readFileAlloc(allocator, events_path, 64 * 1024 * 1024) catch |err| switch (err) {
|
|
223
|
+
error.FileNotFound => {
|
|
224
|
+
try appendWarning(allocator, owned, "trace events were not found; action replay was skipped");
|
|
225
|
+
return .{
|
|
226
|
+
.steps = try allocator.alloc(DraftActionStep, 0),
|
|
227
|
+
.summary = .{ .enabled = true },
|
|
228
|
+
};
|
|
229
|
+
},
|
|
230
|
+
else => return err,
|
|
231
|
+
};
|
|
232
|
+
defer allocator.free(content);
|
|
233
|
+
|
|
234
|
+
var steps = std.ArrayList(DraftActionStep).empty;
|
|
235
|
+
errdefer steps.deinit(allocator);
|
|
236
|
+
var replay_event_count: usize = 0;
|
|
237
|
+
var skipped_replay_event_count: usize = 0;
|
|
238
|
+
|
|
239
|
+
var lines = std.mem.splitScalar(u8, content, '\n');
|
|
240
|
+
while (lines.next()) |raw_line| {
|
|
241
|
+
const line = std.mem.trim(u8, raw_line, " \t\r\n");
|
|
242
|
+
if (line.len == 0) continue;
|
|
243
|
+
var parsed = std.json.parseFromSlice(std.json.Value, allocator, line, .{}) catch {
|
|
244
|
+
try appendWarning(allocator, owned, "invalid trace event was skipped");
|
|
245
|
+
continue;
|
|
246
|
+
};
|
|
247
|
+
defer parsed.deinit();
|
|
248
|
+
if (parsed.value != .object) continue;
|
|
249
|
+
const event = parsed.value.object;
|
|
250
|
+
const kind = optionalString(event, "kind") orelse continue;
|
|
251
|
+
const payload_value = event.get("payload") orelse continue;
|
|
252
|
+
if (payload_value != .object) continue;
|
|
253
|
+
|
|
254
|
+
const replay_event = !isControlEvent(kind);
|
|
255
|
+
if (replay_event) replay_event_count += 1;
|
|
256
|
+
if (try replayStepJson(allocator, owned, kind, payload_value.object)) |json| {
|
|
257
|
+
try steps.append(allocator, .{ .json = json });
|
|
258
|
+
} else if (replay_event) {
|
|
259
|
+
skipped_replay_event_count += 1;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const owned_steps = try steps.toOwnedSlice(allocator);
|
|
264
|
+
return .{
|
|
265
|
+
.steps = owned_steps,
|
|
266
|
+
.summary = .{
|
|
267
|
+
.enabled = true,
|
|
268
|
+
.event_count = replay_event_count,
|
|
269
|
+
.step_count = owned_steps.len,
|
|
270
|
+
.skipped_event_count = skipped_replay_event_count,
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
fn replayStepJson(
|
|
276
|
+
allocator: std.mem.Allocator,
|
|
277
|
+
owned: *OwnedDraft,
|
|
278
|
+
kind: []const u8,
|
|
279
|
+
payload: std.json.ObjectMap,
|
|
280
|
+
) !?[]const u8 {
|
|
281
|
+
if (isControlEvent(kind)) return null;
|
|
282
|
+
if (!isSuccessfulPayload(payload)) return null;
|
|
283
|
+
|
|
284
|
+
if (std.mem.eql(u8, kind, "app.launch")) {
|
|
285
|
+
return try ownString(allocator, owned, "{\"action\":\"launch\"}");
|
|
286
|
+
}
|
|
287
|
+
if (std.mem.eql(u8, kind, "app.openLink")) {
|
|
288
|
+
const url = optionalString(payload, "url") orelse return try warnMissingReplayField(allocator, owned, kind);
|
|
289
|
+
return try actionWithStringField(allocator, owned, "openLink", "url", url);
|
|
290
|
+
}
|
|
291
|
+
if (std.mem.eql(u8, kind, "ui.tap")) {
|
|
292
|
+
const selector_value = payload.get("selector") orelse return try warnMissingReplayField(allocator, owned, kind);
|
|
293
|
+
if (selector_value != .object) return try warnMissingReplayField(allocator, owned, kind);
|
|
294
|
+
return try actionWithSelector(allocator, owned, "tap", selector_value);
|
|
295
|
+
}
|
|
296
|
+
if (std.mem.eql(u8, kind, "ui.type")) {
|
|
297
|
+
const text = optionalString(payload, "text") orelse return try warnMissingReplayField(allocator, owned, kind);
|
|
298
|
+
if (isRedactedTraceText(text)) {
|
|
299
|
+
try appendWarningFmt(allocator, owned, "redacted trace text action was skipped: {s}", .{kind});
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
if (payload.get("selector")) |selector_value| {
|
|
303
|
+
if (selector_value == .object) return try actionWithSelectorAndString(allocator, owned, "typeText", selector_value, "text", text);
|
|
304
|
+
}
|
|
305
|
+
return try actionWithStringField(allocator, owned, "typeText", "text", text);
|
|
306
|
+
}
|
|
307
|
+
if (std.mem.eql(u8, kind, "ui.eraseText")) {
|
|
308
|
+
const max_chars = optionalUsize(payload, "maxChars") orelse return try warnMissingReplayField(allocator, owned, kind);
|
|
309
|
+
if (payload.get("selector")) |selector_value| {
|
|
310
|
+
if (selector_value == .object) return try actionWithSelectorAndInt(allocator, owned, "eraseText", selector_value, "maxChars", max_chars);
|
|
311
|
+
}
|
|
312
|
+
return try actionWithIntField(allocator, owned, "eraseText", "maxChars", max_chars);
|
|
313
|
+
}
|
|
314
|
+
if (std.mem.eql(u8, kind, "ui.hideKeyboard")) {
|
|
315
|
+
return try ownString(allocator, owned, "{\"action\":\"hideKeyboard\"}");
|
|
316
|
+
}
|
|
317
|
+
if (std.mem.eql(u8, kind, "ui.pressBack")) {
|
|
318
|
+
return try ownString(allocator, owned, "{\"action\":\"pressBack\"}");
|
|
319
|
+
}
|
|
320
|
+
if (std.mem.eql(u8, kind, "ui.swipe")) {
|
|
321
|
+
const x1 = optionalI32(payload, "x1") orelse return try warnMissingReplayField(allocator, owned, kind);
|
|
322
|
+
const y1 = optionalI32(payload, "y1") orelse return try warnMissingReplayField(allocator, owned, kind);
|
|
323
|
+
const x2 = optionalI32(payload, "x2") orelse return try warnMissingReplayField(allocator, owned, kind);
|
|
324
|
+
const y2 = optionalI32(payload, "y2") orelse return try warnMissingReplayField(allocator, owned, kind);
|
|
325
|
+
return try actionWithSwipe(allocator, owned, x1, y1, x2, y2, optionalUsize(payload, "durationMs"));
|
|
326
|
+
}
|
|
327
|
+
if (std.mem.eql(u8, kind, "wait.visible")) {
|
|
328
|
+
const selector_value = payload.get("selector") orelse return try warnMissingReplayField(allocator, owned, kind);
|
|
329
|
+
if (selector_value != .object) return try warnMissingReplayField(allocator, owned, kind);
|
|
330
|
+
if (optionalUsize(payload, "timeoutMs")) |timeout_ms| return try actionWithSelectorAndInt(allocator, owned, "waitVisible", selector_value, "timeoutMs", timeout_ms);
|
|
331
|
+
return try actionWithSelector(allocator, owned, "waitVisible", selector_value);
|
|
332
|
+
}
|
|
333
|
+
if (std.mem.eql(u8, kind, "wait.notVisible")) {
|
|
334
|
+
const selector_value = payload.get("selector") orelse return try warnMissingReplayField(allocator, owned, kind);
|
|
335
|
+
if (selector_value != .object) return try warnMissingReplayField(allocator, owned, kind);
|
|
336
|
+
if (optionalUsize(payload, "timeoutMs")) |timeout_ms| return try actionWithSelectorAndInt(allocator, owned, "waitNotVisible", selector_value, "timeoutMs", timeout_ms);
|
|
337
|
+
return try actionWithSelector(allocator, owned, "waitNotVisible", selector_value);
|
|
338
|
+
}
|
|
339
|
+
if (std.mem.eql(u8, kind, "assert.visible")) {
|
|
340
|
+
const selector_value = payload.get("selector") orelse return try warnMissingReplayField(allocator, owned, kind);
|
|
341
|
+
if (selector_value != .object) return try warnMissingReplayField(allocator, owned, kind);
|
|
342
|
+
if (optionalUsize(payload, "timeoutMs")) |timeout_ms| return try actionWithSelectorAndInt(allocator, owned, "assertVisible", selector_value, "timeoutMs", timeout_ms);
|
|
343
|
+
return try actionWithSelector(allocator, owned, "assertVisible", selector_value);
|
|
344
|
+
}
|
|
345
|
+
if (std.mem.eql(u8, kind, "assert.notVisible")) {
|
|
346
|
+
const selector_value = payload.get("selector") orelse return try warnMissingReplayField(allocator, owned, kind);
|
|
347
|
+
if (selector_value != .object) return try warnMissingReplayField(allocator, owned, kind);
|
|
348
|
+
if (optionalUsize(payload, "timeoutMs")) |timeout_ms| return try actionWithSelectorAndInt(allocator, owned, "assertNotVisible", selector_value, "timeoutMs", timeout_ms);
|
|
349
|
+
return try actionWithSelector(allocator, owned, "assertNotVisible", selector_value);
|
|
350
|
+
}
|
|
351
|
+
if (std.mem.eql(u8, kind, "wait.any")) {
|
|
352
|
+
const selector_value = payload.get("selector") orelse return try warnMissingReplayField(allocator, owned, kind);
|
|
353
|
+
if (selector_value != .object) return try warnMissingReplayField(allocator, owned, kind);
|
|
354
|
+
if (optionalUsize(payload, "timeoutMs")) |timeout_ms| return try actionWithSelectorAndInt(allocator, owned, "waitVisible", selector_value, "timeoutMs", timeout_ms);
|
|
355
|
+
return try actionWithSelector(allocator, owned, "waitVisible", selector_value);
|
|
356
|
+
}
|
|
357
|
+
if (std.mem.eql(u8, kind, "ui.scrollUntilVisible")) {
|
|
358
|
+
const selector_value = payload.get("selector") orelse return try warnMissingReplayField(allocator, owned, kind);
|
|
359
|
+
if (selector_value != .object) return try warnMissingReplayField(allocator, owned, kind);
|
|
360
|
+
return try actionWithScrollUntilVisible(allocator, owned, selector_value, optionalScrollDirection(payload), optionalUsize(payload, "timeoutMs"));
|
|
361
|
+
}
|
|
362
|
+
if (std.mem.eql(u8, kind, "assert.noneVisible")) {
|
|
363
|
+
const selectors_value = payload.get("selectors") orelse return try warnMissingReplayField(allocator, owned, kind);
|
|
364
|
+
if (selectors_value != .array) return try warnMissingReplayField(allocator, owned, kind);
|
|
365
|
+
return try actionWithSelectors(allocator, owned, "assertNoneVisible", selectors_value, optionalUsize(payload, "timeoutMs"));
|
|
366
|
+
}
|
|
367
|
+
if (std.mem.eql(u8, kind, "assert.healthy")) {
|
|
368
|
+
if (optionalUsize(payload, "timeoutMs")) |timeout_ms| return try actionWithIntField(allocator, owned, "assertHealthy", "timeoutMs", timeout_ms);
|
|
369
|
+
return try ownString(allocator, owned, "{\"action\":\"assertHealthy\"}");
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
try appendWarningFmt(allocator, owned, "unsupported trace action was skipped: {s}", .{kind});
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
fn latestSnapshotPath(
|
|
377
|
+
allocator: std.mem.Allocator,
|
|
378
|
+
owned: *OwnedDraft,
|
|
379
|
+
trace_dir: []const u8,
|
|
380
|
+
metadata: TraceMetadata,
|
|
381
|
+
) ![]const u8 {
|
|
382
|
+
if (metadata.snapshot_count > 0) {
|
|
383
|
+
const candidate_name = try std.fmt.allocPrint(allocator, "snapshot-{d}.json", .{metadata.snapshot_count});
|
|
384
|
+
defer allocator.free(candidate_name);
|
|
385
|
+
const candidate = try std.fs.path.join(allocator, &.{ trace_dir, metadata.artifacts_dir, candidate_name });
|
|
386
|
+
errdefer allocator.free(candidate);
|
|
387
|
+
if (pathExists(candidate)) {
|
|
388
|
+
try owned.owned_strings.append(allocator, candidate);
|
|
389
|
+
return candidate;
|
|
390
|
+
}
|
|
391
|
+
allocator.free(candidate);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const artifacts_path = try std.fs.path.join(allocator, &.{ trace_dir, metadata.artifacts_dir });
|
|
395
|
+
defer allocator.free(artifacts_path);
|
|
396
|
+
var dir = try std.fs.cwd().openDir(artifacts_path, .{ .iterate = true });
|
|
397
|
+
defer dir.close();
|
|
398
|
+
|
|
399
|
+
var iterator = dir.iterate();
|
|
400
|
+
var best_number: usize = 0;
|
|
401
|
+
var best_name: ?[]u8 = null;
|
|
402
|
+
defer if (best_name) |value| allocator.free(value);
|
|
403
|
+
while (try iterator.next()) |entry| {
|
|
404
|
+
if (entry.kind != .file) continue;
|
|
405
|
+
const number = snapshotNumber(entry.name) orelse continue;
|
|
406
|
+
if (best_name == null or number > best_number) {
|
|
407
|
+
if (best_name) |value| allocator.free(value);
|
|
408
|
+
best_name = try allocator.dupe(u8, entry.name);
|
|
409
|
+
best_number = number;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const selected_name = best_name orelse return error.SemanticSnapshotMissing;
|
|
414
|
+
const path = try std.fs.path.join(allocator, &.{ trace_dir, metadata.artifacts_dir, selected_name });
|
|
415
|
+
errdefer allocator.free(path);
|
|
416
|
+
try owned.owned_strings.append(allocator, path);
|
|
417
|
+
return path;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
fn parseSemanticSelectors(
|
|
421
|
+
allocator: std.mem.Allocator,
|
|
422
|
+
path: []const u8,
|
|
423
|
+
owned: *OwnedDraft,
|
|
424
|
+
) ![]DraftSelector {
|
|
425
|
+
const content = try std.fs.cwd().readFileAlloc(allocator, path, 8 * 1024 * 1024);
|
|
426
|
+
defer allocator.free(content);
|
|
427
|
+
|
|
428
|
+
var parsed = try std.json.parseFromSlice(std.json.Value, allocator, content, .{});
|
|
429
|
+
defer parsed.deinit();
|
|
430
|
+
if (parsed.value != .object) return error.InvalidSemanticSnapshot;
|
|
431
|
+
const nodes_value = parsed.value.object.get("nodes") orelse return error.InvalidSemanticSnapshot;
|
|
432
|
+
if (nodes_value != .array) return error.InvalidSemanticSnapshot;
|
|
433
|
+
|
|
434
|
+
var selectors = std.ArrayList(DraftSelector).empty;
|
|
435
|
+
errdefer freeSelectors(allocator, selectors.items);
|
|
436
|
+
|
|
437
|
+
var skipped_unstable = false;
|
|
438
|
+
for (nodes_value.array.items) |node_value| {
|
|
439
|
+
if (selectors.items.len >= max_draft_selectors) break;
|
|
440
|
+
if (node_value != .object) continue;
|
|
441
|
+
const node = node_value.object;
|
|
442
|
+
if (!boolField(node, "visible") or !boolField(node, "enabled")) continue;
|
|
443
|
+
|
|
444
|
+
const selected = selectNodeSelector(node) orelse {
|
|
445
|
+
skipped_unstable = true;
|
|
446
|
+
continue;
|
|
447
|
+
};
|
|
448
|
+
if (hasSelector(selectors.items, selected.kind, selected.value)) continue;
|
|
449
|
+
|
|
450
|
+
try selectors.append(allocator, .{
|
|
451
|
+
.kind = selected.kind,
|
|
452
|
+
.value = try allocator.dupe(u8, selected.value),
|
|
453
|
+
});
|
|
454
|
+
if (selected.kind == .text) {
|
|
455
|
+
try appendWarning(allocator, owned, "text selectors can be less stable than resource IDs or accessibility labels");
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (skipped_unstable) {
|
|
460
|
+
try appendWarning(allocator, owned, "visible nodes with generated or class-only selectors were skipped");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return try selectors.toOwnedSlice(allocator);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
fn freeSelectors(allocator: std.mem.Allocator, selectors: []DraftSelector) void {
|
|
467
|
+
for (selectors) |draft_selector| allocator.free(draft_selector.value);
|
|
468
|
+
allocator.free(selectors);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
fn selectNodeSelector(node: std.json.ObjectMap) ?DraftSelector {
|
|
472
|
+
if (node.get("selector")) |value| {
|
|
473
|
+
if (value == .object) {
|
|
474
|
+
if (nonEmptyString(value.object, "resourceId")) |actual| return .{ .kind = .resource_id, .value = actual };
|
|
475
|
+
if (nonEmptyString(value.object, "contentDesc")) |actual| return .{ .kind = .content_desc, .value = actual };
|
|
476
|
+
if (nonEmptyString(value.object, "text")) |actual| return .{ .kind = .text, .value = actual };
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (node.get("source")) |value| {
|
|
480
|
+
if (value == .object) {
|
|
481
|
+
if (nonEmptyString(value.object, "resourceId")) |actual| return .{ .kind = .resource_id, .value = actual };
|
|
482
|
+
if (nonEmptyString(value.object, "contentDesc")) |actual| return .{ .kind = .content_desc, .value = actual };
|
|
483
|
+
if (nonEmptyString(value.object, "text")) |actual| return .{ .kind = .text, .value = actual };
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (nonEmptyString(node, "resourceId")) |actual| return .{ .kind = .resource_id, .value = actual };
|
|
487
|
+
if (nonEmptyString(node, "contentDesc")) |actual| return .{ .kind = .content_desc, .value = actual };
|
|
488
|
+
if (nonEmptyString(node, "text")) |actual| return .{ .kind = .text, .value = actual };
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
fn hasSelector(selectors: []const DraftSelector, kind: SelectorKind, value: []const u8) bool {
|
|
493
|
+
for (selectors) |draft_selector| {
|
|
494
|
+
if (draft_selector.kind == kind and std.mem.eql(u8, draft_selector.value, value)) return true;
|
|
495
|
+
}
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
fn writeScenarioFile(
|
|
500
|
+
out_path: []const u8,
|
|
501
|
+
name: []const u8,
|
|
502
|
+
app_id: ?[]const u8,
|
|
503
|
+
action_steps: []const DraftActionStep,
|
|
504
|
+
selectors: []const DraftSelector,
|
|
505
|
+
force: bool,
|
|
506
|
+
) !void {
|
|
507
|
+
if (std.fs.path.dirname(out_path)) |dir| {
|
|
508
|
+
if (dir.len > 0) try std.fs.cwd().makePath(dir);
|
|
509
|
+
}
|
|
510
|
+
var file = if (force)
|
|
511
|
+
try std.fs.cwd().createFile(out_path, .{ .truncate = true })
|
|
512
|
+
else
|
|
513
|
+
try std.fs.cwd().createFile(out_path, .{ .exclusive = true });
|
|
514
|
+
defer file.close();
|
|
515
|
+
|
|
516
|
+
var write_buffer: [8192]u8 = undefined;
|
|
517
|
+
var file_writer = file.writer(&write_buffer);
|
|
518
|
+
try writeScenarioJson(&file_writer.interface, name, app_id, action_steps, selectors);
|
|
519
|
+
try file_writer.interface.flush();
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
fn writeScenarioJson(
|
|
523
|
+
writer: anytype,
|
|
524
|
+
name: []const u8,
|
|
525
|
+
app_id: ?[]const u8,
|
|
526
|
+
action_steps: []const DraftActionStep,
|
|
527
|
+
selectors: []const DraftSelector,
|
|
528
|
+
) !void {
|
|
529
|
+
try writer.writeAll("{\"name\":");
|
|
530
|
+
try trace.writeJsonString(writer, name);
|
|
531
|
+
if (app_id) |actual| {
|
|
532
|
+
try writer.writeAll(",\"appId\":");
|
|
533
|
+
try trace.writeJsonString(writer, actual);
|
|
534
|
+
}
|
|
535
|
+
try writer.writeAll(",\"steps\":[");
|
|
536
|
+
if (action_steps.len == 0) {
|
|
537
|
+
try writer.writeAll("{\"action\":\"launch\"}");
|
|
538
|
+
} else {
|
|
539
|
+
for (action_steps, 0..) |step, index| {
|
|
540
|
+
if (index > 0) try writer.writeAll(",");
|
|
541
|
+
try writer.writeAll(step.json);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
try writer.writeAll(",{\"action\":\"snapshot\"}");
|
|
545
|
+
for (selectors) |draft_selector| {
|
|
546
|
+
try writer.writeAll(",{\"action\":\"assertVisible\",\"selector\":{");
|
|
547
|
+
switch (draft_selector.kind) {
|
|
548
|
+
.resource_id => try writer.writeAll("\"resourceId\":"),
|
|
549
|
+
.content_desc => try writer.writeAll("\"contentDesc\":"),
|
|
550
|
+
.text => try writer.writeAll("\"text\":"),
|
|
551
|
+
}
|
|
552
|
+
try trace.writeJsonString(writer, draft_selector.value);
|
|
553
|
+
try writer.writeAll("}}");
|
|
554
|
+
}
|
|
555
|
+
try writer.writeAll("]}\n");
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
pub fn writeJson(writer: anytype, summary: DraftSummary) !void {
|
|
559
|
+
try writer.writeAll("{\"ok\":");
|
|
560
|
+
try writer.writeAll(if (summary.ok) "true" else "false");
|
|
561
|
+
try writer.writeAll(",\"mode\":\"draft\",\"schemaVersion\":1");
|
|
562
|
+
try writer.writeAll(",\"runnerVersion\":");
|
|
563
|
+
try trace.writeJsonString(writer, version.runner_version);
|
|
564
|
+
try writer.writeAll(",\"protocolVersion\":");
|
|
565
|
+
try trace.writeJsonString(writer, version.protocol_version);
|
|
566
|
+
try writer.writeAll(",\"out\":");
|
|
567
|
+
try trace.writeJsonString(writer, summary.out_path);
|
|
568
|
+
try writer.writeAll(",\"traceDir\":");
|
|
569
|
+
try trace.writeJsonString(writer, summary.trace_dir);
|
|
570
|
+
try writer.writeAll(",\"sourceSnapshot\":");
|
|
571
|
+
try trace.writeJsonString(writer, summary.source_snapshot);
|
|
572
|
+
try writer.writeAll(",\"name\":");
|
|
573
|
+
try trace.writeJsonString(writer, summary.name);
|
|
574
|
+
try writer.writeAll(",\"appId\":");
|
|
575
|
+
if (summary.app_id) |actual| {
|
|
576
|
+
try trace.writeJsonString(writer, actual);
|
|
577
|
+
} else {
|
|
578
|
+
try writer.writeAll("null");
|
|
579
|
+
}
|
|
580
|
+
try writer.print(",\"selectorCount\":{d},\"stepCount\":{d}", .{ summary.selector_count, summary.step_count });
|
|
581
|
+
try writeReplayJson(writer, summary.replay);
|
|
582
|
+
try writer.writeAll(",\"warnings\":[");
|
|
583
|
+
for (summary.warnings, 0..) |warning, index| {
|
|
584
|
+
if (index > 0) try writer.writeAll(",");
|
|
585
|
+
try trace.writeJsonString(writer, warning);
|
|
586
|
+
}
|
|
587
|
+
try writer.writeAll("],\"nextCommands\":[\"zmr validate --json ");
|
|
588
|
+
try cli_output.writeShellArgJsonContent(writer, summary.out_path);
|
|
589
|
+
try writer.writeAll("\",\"zmr run ");
|
|
590
|
+
try cli_output.writeShellArgJsonContent(writer, summary.out_path);
|
|
591
|
+
try writer.writeAll(" --json --trace-dir ");
|
|
592
|
+
try cli_output.writeShellArgJsonContent(writer, summary.trace_dir);
|
|
593
|
+
try writer.writeAll("\"]}\n");
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
pub fn writeReplayJson(writer: anytype, replay: ReplaySummary) !void {
|
|
597
|
+
try writer.writeAll(",\"replay\":{\"enabled\":");
|
|
598
|
+
try writer.writeAll(if (replay.enabled) "true" else "false");
|
|
599
|
+
try writer.print(
|
|
600
|
+
",\"eventCount\":{d},\"stepCount\":{d},\"skippedEventCount\":{d}}}",
|
|
601
|
+
.{ replay.event_count, replay.step_count, replay.skipped_event_count },
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
fn actionWithStringField(
|
|
606
|
+
allocator: std.mem.Allocator,
|
|
607
|
+
owned: *OwnedDraft,
|
|
608
|
+
action: []const u8,
|
|
609
|
+
key: []const u8,
|
|
610
|
+
value: []const u8,
|
|
611
|
+
) ![]const u8 {
|
|
612
|
+
var buffer = std.ArrayList(u8).empty;
|
|
613
|
+
defer buffer.deinit(allocator);
|
|
614
|
+
const writer = buffer.writer(allocator);
|
|
615
|
+
try writer.writeAll("{\"action\":");
|
|
616
|
+
try trace.writeJsonString(writer, action);
|
|
617
|
+
try writer.writeAll(",");
|
|
618
|
+
try trace.writeJsonString(writer, key);
|
|
619
|
+
try writer.writeAll(":");
|
|
620
|
+
try trace.writeJsonString(writer, value);
|
|
621
|
+
try writer.writeAll("}");
|
|
622
|
+
return try ownBytes(allocator, owned, buffer.items);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
fn actionWithIntField(
|
|
626
|
+
allocator: std.mem.Allocator,
|
|
627
|
+
owned: *OwnedDraft,
|
|
628
|
+
action: []const u8,
|
|
629
|
+
key: []const u8,
|
|
630
|
+
value: usize,
|
|
631
|
+
) ![]const u8 {
|
|
632
|
+
var buffer = std.ArrayList(u8).empty;
|
|
633
|
+
defer buffer.deinit(allocator);
|
|
634
|
+
const writer = buffer.writer(allocator);
|
|
635
|
+
try writer.writeAll("{\"action\":");
|
|
636
|
+
try trace.writeJsonString(writer, action);
|
|
637
|
+
try writer.writeAll(",");
|
|
638
|
+
try trace.writeJsonString(writer, key);
|
|
639
|
+
try writer.print(":{d}}}", .{value});
|
|
640
|
+
return try ownBytes(allocator, owned, buffer.items);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
fn actionWithSelector(
|
|
644
|
+
allocator: std.mem.Allocator,
|
|
645
|
+
owned: *OwnedDraft,
|
|
646
|
+
action: []const u8,
|
|
647
|
+
selector_value: std.json.Value,
|
|
648
|
+
) ![]const u8 {
|
|
649
|
+
var buffer = std.ArrayList(u8).empty;
|
|
650
|
+
defer buffer.deinit(allocator);
|
|
651
|
+
const writer = buffer.writer(allocator);
|
|
652
|
+
try writer.writeAll("{\"action\":");
|
|
653
|
+
try trace.writeJsonString(writer, action);
|
|
654
|
+
try writer.writeAll(",\"selector\":");
|
|
655
|
+
try writeJsonValue(writer, selector_value);
|
|
656
|
+
try writer.writeAll("}");
|
|
657
|
+
return try ownBytes(allocator, owned, buffer.items);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
fn actionWithSelectorAndString(
|
|
661
|
+
allocator: std.mem.Allocator,
|
|
662
|
+
owned: *OwnedDraft,
|
|
663
|
+
action: []const u8,
|
|
664
|
+
selector_value: std.json.Value,
|
|
665
|
+
key: []const u8,
|
|
666
|
+
value: []const u8,
|
|
667
|
+
) ![]const u8 {
|
|
668
|
+
var buffer = std.ArrayList(u8).empty;
|
|
669
|
+
defer buffer.deinit(allocator);
|
|
670
|
+
const writer = buffer.writer(allocator);
|
|
671
|
+
try writer.writeAll("{\"action\":");
|
|
672
|
+
try trace.writeJsonString(writer, action);
|
|
673
|
+
try writer.writeAll(",\"selector\":");
|
|
674
|
+
try writeJsonValue(writer, selector_value);
|
|
675
|
+
try writer.writeAll(",");
|
|
676
|
+
try trace.writeJsonString(writer, key);
|
|
677
|
+
try writer.writeAll(":");
|
|
678
|
+
try trace.writeJsonString(writer, value);
|
|
679
|
+
try writer.writeAll("}");
|
|
680
|
+
return try ownBytes(allocator, owned, buffer.items);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
fn actionWithSelectorAndInt(
|
|
684
|
+
allocator: std.mem.Allocator,
|
|
685
|
+
owned: *OwnedDraft,
|
|
686
|
+
action: []const u8,
|
|
687
|
+
selector_value: std.json.Value,
|
|
688
|
+
key: []const u8,
|
|
689
|
+
value: usize,
|
|
690
|
+
) ![]const u8 {
|
|
691
|
+
var buffer = std.ArrayList(u8).empty;
|
|
692
|
+
defer buffer.deinit(allocator);
|
|
693
|
+
const writer = buffer.writer(allocator);
|
|
694
|
+
try writer.writeAll("{\"action\":");
|
|
695
|
+
try trace.writeJsonString(writer, action);
|
|
696
|
+
try writer.writeAll(",\"selector\":");
|
|
697
|
+
try writeJsonValue(writer, selector_value);
|
|
698
|
+
try writer.writeAll(",");
|
|
699
|
+
try trace.writeJsonString(writer, key);
|
|
700
|
+
try writer.print(":{d}}}", .{value});
|
|
701
|
+
return try ownBytes(allocator, owned, buffer.items);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
fn actionWithSelectors(
|
|
705
|
+
allocator: std.mem.Allocator,
|
|
706
|
+
owned: *OwnedDraft,
|
|
707
|
+
action: []const u8,
|
|
708
|
+
selectors_value: std.json.Value,
|
|
709
|
+
timeout_ms: ?usize,
|
|
710
|
+
) ![]const u8 {
|
|
711
|
+
var buffer = std.ArrayList(u8).empty;
|
|
712
|
+
defer buffer.deinit(allocator);
|
|
713
|
+
const writer = buffer.writer(allocator);
|
|
714
|
+
try writer.writeAll("{\"action\":");
|
|
715
|
+
try trace.writeJsonString(writer, action);
|
|
716
|
+
try writer.writeAll(",\"selectors\":");
|
|
717
|
+
try writeJsonValue(writer, selectors_value);
|
|
718
|
+
if (timeout_ms) |actual| try writer.print(",\"timeoutMs\":{d}", .{actual});
|
|
719
|
+
try writer.writeAll("}");
|
|
720
|
+
return try ownBytes(allocator, owned, buffer.items);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
fn actionWithSwipe(
|
|
724
|
+
allocator: std.mem.Allocator,
|
|
725
|
+
owned: *OwnedDraft,
|
|
726
|
+
x1: i32,
|
|
727
|
+
y1: i32,
|
|
728
|
+
x2: i32,
|
|
729
|
+
y2: i32,
|
|
730
|
+
duration_ms: ?usize,
|
|
731
|
+
) ![]const u8 {
|
|
732
|
+
var buffer = std.ArrayList(u8).empty;
|
|
733
|
+
defer buffer.deinit(allocator);
|
|
734
|
+
const writer = buffer.writer(allocator);
|
|
735
|
+
try writer.print("{{\"action\":\"swipe\",\"x1\":{d},\"y1\":{d},\"x2\":{d},\"y2\":{d}", .{ x1, y1, x2, y2 });
|
|
736
|
+
if (duration_ms) |actual| try writer.print(",\"durationMs\":{d}", .{actual});
|
|
737
|
+
try writer.writeAll("}");
|
|
738
|
+
return try ownBytes(allocator, owned, buffer.items);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
fn actionWithScrollUntilVisible(
|
|
742
|
+
allocator: std.mem.Allocator,
|
|
743
|
+
owned: *OwnedDraft,
|
|
744
|
+
selector_value: std.json.Value,
|
|
745
|
+
direction: ?[]const u8,
|
|
746
|
+
timeout_ms: ?usize,
|
|
747
|
+
) ![]const u8 {
|
|
748
|
+
var buffer = std.ArrayList(u8).empty;
|
|
749
|
+
defer buffer.deinit(allocator);
|
|
750
|
+
const writer = buffer.writer(allocator);
|
|
751
|
+
try writer.writeAll("{\"action\":\"scrollUntilVisible\",\"selector\":");
|
|
752
|
+
try writeJsonValue(writer, selector_value);
|
|
753
|
+
if (direction) |actual| {
|
|
754
|
+
try writer.writeAll(",\"direction\":");
|
|
755
|
+
try trace.writeJsonString(writer, actual);
|
|
756
|
+
}
|
|
757
|
+
if (timeout_ms) |actual| try writer.print(",\"timeoutMs\":{d}", .{actual});
|
|
758
|
+
try writer.writeAll("}");
|
|
759
|
+
return try ownBytes(allocator, owned, buffer.items);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
fn warnMissingReplayField(
|
|
763
|
+
allocator: std.mem.Allocator,
|
|
764
|
+
owned: *OwnedDraft,
|
|
765
|
+
kind: []const u8,
|
|
766
|
+
) !?[]const u8 {
|
|
767
|
+
try appendWarningFmt(allocator, owned, "trace action was skipped because required replay fields were missing: {s}", .{kind});
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
fn isControlEvent(kind: []const u8) bool {
|
|
772
|
+
return std.mem.eql(u8, kind, "scenario.start") or
|
|
773
|
+
std.mem.eql(u8, kind, "scenario.end") or
|
|
774
|
+
std.mem.eql(u8, kind, "step.done") or
|
|
775
|
+
std.mem.eql(u8, kind, "step.error") or
|
|
776
|
+
std.mem.eql(u8, kind, "step.optional") or
|
|
777
|
+
std.mem.eql(u8, kind, "step.whenVisible.skipped") or
|
|
778
|
+
std.mem.eql(u8, kind, "step.repeat.iteration") or
|
|
779
|
+
std.mem.eql(u8, kind, "observe.snapshot") or
|
|
780
|
+
std.mem.eql(u8, kind, "observe.semanticSnapshot") or
|
|
781
|
+
std.mem.eql(u8, kind, "observe.snapshot.semanticExtraction") or
|
|
782
|
+
std.mem.eql(u8, kind, "observe.retry") or
|
|
783
|
+
std.mem.eql(u8, kind, "rpc.request") or
|
|
784
|
+
std.mem.eql(u8, kind, "rpc.response") or
|
|
785
|
+
std.mem.eql(u8, kind, "rpc.error") or
|
|
786
|
+
std.mem.eql(u8, kind, "trace.export");
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
fn isSuccessfulPayload(payload: std.json.ObjectMap) bool {
|
|
790
|
+
const status_value = payload.get("status") orelse return true;
|
|
791
|
+
return status_value == .string and std.mem.eql(u8, status_value.string, "ok");
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
fn isRedactedTraceText(value: []const u8) bool {
|
|
795
|
+
return std.mem.startsWith(u8, value, "[REDACTED:");
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
fn writeJsonValue(writer: anytype, value: std.json.Value) !void {
|
|
799
|
+
switch (value) {
|
|
800
|
+
.null => try writer.writeAll("null"),
|
|
801
|
+
.bool => |actual| try writer.writeAll(if (actual) "true" else "false"),
|
|
802
|
+
.integer => |actual| try writer.print("{d}", .{actual}),
|
|
803
|
+
.float => |actual| try writer.print("{d}", .{actual}),
|
|
804
|
+
.number_string => |actual| try writer.writeAll(actual),
|
|
805
|
+
.string => |actual| try trace.writeJsonString(writer, actual),
|
|
806
|
+
.array => |array| {
|
|
807
|
+
try writer.writeAll("[");
|
|
808
|
+
for (array.items, 0..) |item, index| {
|
|
809
|
+
if (index > 0) try writer.writeAll(",");
|
|
810
|
+
try writeJsonValue(writer, item);
|
|
811
|
+
}
|
|
812
|
+
try writer.writeAll("]");
|
|
813
|
+
},
|
|
814
|
+
.object => |object| {
|
|
815
|
+
try writer.writeAll("{");
|
|
816
|
+
var iterator = object.iterator();
|
|
817
|
+
var first = true;
|
|
818
|
+
while (iterator.next()) |entry| {
|
|
819
|
+
if (!first) try writer.writeAll(",");
|
|
820
|
+
first = false;
|
|
821
|
+
try trace.writeJsonString(writer, entry.key_ptr.*);
|
|
822
|
+
try writer.writeAll(":");
|
|
823
|
+
try writeJsonValue(writer, entry.value_ptr.*);
|
|
824
|
+
}
|
|
825
|
+
try writer.writeAll("}");
|
|
826
|
+
},
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
fn appendWarning(allocator: std.mem.Allocator, owned: *OwnedDraft, warning: []const u8) !void {
|
|
831
|
+
for (owned.warnings.items) |existing| {
|
|
832
|
+
if (std.mem.eql(u8, existing, warning)) return;
|
|
833
|
+
}
|
|
834
|
+
try owned.warnings.append(allocator, warning);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
fn appendWarningFmt(
|
|
838
|
+
allocator: std.mem.Allocator,
|
|
839
|
+
owned: *OwnedDraft,
|
|
840
|
+
comptime fmt: []const u8,
|
|
841
|
+
args: anytype,
|
|
842
|
+
) !void {
|
|
843
|
+
const warning = try std.fmt.allocPrint(allocator, fmt, args);
|
|
844
|
+
errdefer allocator.free(warning);
|
|
845
|
+
for (owned.warnings.items) |existing| {
|
|
846
|
+
if (std.mem.eql(u8, existing, warning)) {
|
|
847
|
+
allocator.free(warning);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
try owned.owned_strings.append(allocator, warning);
|
|
852
|
+
try owned.warnings.append(allocator, warning);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
fn ownString(allocator: std.mem.Allocator, owned: *OwnedDraft, value: []const u8) ![]const u8 {
|
|
856
|
+
const copy = try allocator.dupe(u8, value);
|
|
857
|
+
try owned.owned_strings.append(allocator, copy);
|
|
858
|
+
return copy;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
fn ownBytes(allocator: std.mem.Allocator, owned: *OwnedDraft, value: []const u8) ![]const u8 {
|
|
862
|
+
return try ownString(allocator, owned, value);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
fn ownFmt(allocator: std.mem.Allocator, owned: *OwnedDraft, comptime fmt: []const u8, args: anytype) ![]const u8 {
|
|
866
|
+
const copy = try std.fmt.allocPrint(allocator, fmt, args);
|
|
867
|
+
try owned.owned_strings.append(allocator, copy);
|
|
868
|
+
return copy;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
fn optionalString(object: std.json.ObjectMap, key: []const u8) ?[]const u8 {
|
|
872
|
+
const value = object.get(key) orelse return null;
|
|
873
|
+
return switch (value) {
|
|
874
|
+
.string => |actual| if (actual.len > 0) actual else null,
|
|
875
|
+
else => null,
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
fn optionalScrollDirection(object: std.json.ObjectMap) ?[]const u8 {
|
|
880
|
+
const value = optionalString(object, "direction") orelse return null;
|
|
881
|
+
if (std.mem.eql(u8, value, "down") or std.mem.eql(u8, value, "up")) return value;
|
|
882
|
+
return null;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
fn nonEmptyString(object: std.json.ObjectMap, key: []const u8) ?[]const u8 {
|
|
886
|
+
return optionalString(object, key);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
fn optionalUsize(object: std.json.ObjectMap, key: []const u8) ?usize {
|
|
890
|
+
const value = object.get(key) orelse return null;
|
|
891
|
+
return switch (value) {
|
|
892
|
+
.integer => |actual| if (actual >= 0) @as(usize, @intCast(actual)) else null,
|
|
893
|
+
else => null,
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
fn optionalI32(object: std.json.ObjectMap, key: []const u8) ?i32 {
|
|
898
|
+
const value = object.get(key) orelse return null;
|
|
899
|
+
return switch (value) {
|
|
900
|
+
.integer => |actual| if (actual >= std.math.minInt(i32) and actual <= std.math.maxInt(i32)) @as(i32, @intCast(actual)) else null,
|
|
901
|
+
else => null,
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
fn boolField(object: std.json.ObjectMap, key: []const u8) bool {
|
|
906
|
+
const value = object.get(key) orelse return false;
|
|
907
|
+
return switch (value) {
|
|
908
|
+
.bool => |actual| actual,
|
|
909
|
+
else => false,
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
fn snapshotNumber(name: []const u8) ?usize {
|
|
914
|
+
if (!std.mem.startsWith(u8, name, "snapshot-")) return null;
|
|
915
|
+
if (!std.mem.endsWith(u8, name, ".json")) return null;
|
|
916
|
+
const number = name["snapshot-".len .. name.len - ".json".len];
|
|
917
|
+
if (number.len == 0) return null;
|
|
918
|
+
return std.fmt.parseInt(usize, number, 10) catch null;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
fn pathExists(path: []const u8) bool {
|
|
922
|
+
std.fs.cwd().access(path, .{}) catch return false;
|
|
923
|
+
return true;
|
|
924
|
+
}
|