zeno-mobile-runner 0.2.12 → 0.2.14
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 +24 -0
- package/FEATURES.md +1 -1
- package/README.md +1 -1
- package/clients/kotlin/README.md +1 -1
- package/clients/kotlin/build.gradle.kts +1 -1
- package/clients/python/pyproject.toml +1 -1
- package/clients/rust/Cargo.lock +1 -1
- package/clients/rust/Cargo.toml +1 -1
- package/clients/typescript/package.json +1 -1
- package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
- package/docs/protocol.md +11 -10
- package/docs/scenario-authoring.md +12 -0
- package/package.json +1 -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/scenario.schema.json +7 -0
- package/shims/ios/ZMRShim.swift +2 -0
- package/shims/ios/ZMRShimUITestCase.swift +112 -40
- package/shims/ios/protocol.md +8 -0
- package/src/android.zig +20 -0
- package/src/errors.zig +8 -0
- package/src/fake_device.zig +15 -0
- package/src/ios.zig +104 -5
- package/src/ios_shim.zig +24 -0
- package/src/json_fields.zig +13 -0
- package/src/runner.zig +55 -0
- package/src/runner_events.zig +31 -0
- package/src/runner_waits.zig +173 -54
- package/src/scenario.zig +46 -0
- package/src/scenario_fields.zig +4 -0
- package/src/validation.zig +8 -0
- package/src/version.zig +1 -1
package/src/fake_device.zig
CHANGED
|
@@ -20,6 +20,8 @@ pub const FakeDevice = struct {
|
|
|
20
20
|
opened_link: ?[]const u8 = null,
|
|
21
21
|
settles: usize = 0,
|
|
22
22
|
last_settle_timeout_ms: u64 = 0,
|
|
23
|
+
location_sets: usize = 0,
|
|
24
|
+
last_location: ?LocationRecord = null,
|
|
23
25
|
|
|
24
26
|
pub fn init(allocator: std.mem.Allocator, snapshots: []types.ObservationSnapshot) FakeDevice {
|
|
25
27
|
return .{
|
|
@@ -71,6 +73,14 @@ pub const FakeDevice = struct {
|
|
|
71
73
|
self.opened_link = try self.allocator.dupe(u8, url);
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
pub fn setLocation(self: *FakeDevice, latitude: f64, longitude: f64) !void {
|
|
77
|
+
self.location_sets += 1;
|
|
78
|
+
self.last_location = .{
|
|
79
|
+
.latitude = latitude,
|
|
80
|
+
.longitude = longitude,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
74
84
|
pub fn tap(self: *FakeDevice, x: i32, y: i32) !void {
|
|
75
85
|
_ = x;
|
|
76
86
|
_ = y;
|
|
@@ -127,6 +137,11 @@ pub const SwipeRecord = struct {
|
|
|
127
137
|
duration_ms: u32,
|
|
128
138
|
};
|
|
129
139
|
|
|
140
|
+
pub const LocationRecord = struct {
|
|
141
|
+
latitude: f64,
|
|
142
|
+
longitude: f64,
|
|
143
|
+
};
|
|
144
|
+
|
|
130
145
|
pub fn cloneSnapshot(allocator: std.mem.Allocator, source: types.ObservationSnapshot) !types.ObservationSnapshot {
|
|
131
146
|
var nodes = try allocator.alloc(types.UiNode, source.nodes.len);
|
|
132
147
|
errdefer allocator.free(nodes);
|
package/src/ios.zig
CHANGED
|
@@ -33,6 +33,7 @@ pub const IosDevice = struct {
|
|
|
33
33
|
app_id: []const u8,
|
|
34
34
|
shim_path: ?[]const u8 = null,
|
|
35
35
|
target_kind: TargetKind = .simulator,
|
|
36
|
+
expo_dev_client_open_link_mode: bool = false,
|
|
36
37
|
|
|
37
38
|
pub fn init(
|
|
38
39
|
allocator: std.mem.Allocator,
|
|
@@ -131,13 +132,31 @@ pub const IosDevice = struct {
|
|
|
131
132
|
const result = try self.runSimctl(&.{ "openurl", self.target(), url }, default_max_output);
|
|
132
133
|
defer result.deinit(self.allocator);
|
|
133
134
|
try result.ensureSuccess();
|
|
135
|
+
if (isExpoDevClientOpenLink(url)) {
|
|
136
|
+
self.expo_dev_client_open_link_mode = true;
|
|
137
|
+
}
|
|
134
138
|
// Opening a URL on the simulator can raise a SpringBoard "Open in <App>?"
|
|
135
139
|
// confirmation for universal links (http/https) and, just as often, for
|
|
136
140
|
// custom schemes — the common Expo dev-client case
|
|
137
141
|
// (exp+scheme://expo-development-client/...). Attempt a best-effort accept
|
|
138
142
|
// whenever a shim is configured; the shim probes briefly and returns fast
|
|
139
143
|
// when no dialog is present, so this stays cheap on the no-prompt path.
|
|
140
|
-
self.acceptOpenURLConfirmationBestEffort();
|
|
144
|
+
self.acceptOpenURLConfirmationBestEffort(url);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
pub fn setLocation(self: *IosDevice, latitude: f64, longitude: f64) !void {
|
|
148
|
+
if (self.target_kind == .physical) return error.UnsupportedDeviceCapability;
|
|
149
|
+
|
|
150
|
+
const coordinate = try std.fmt.allocPrint(self.allocator, "{d:.6},{d:.6}", .{ latitude, longitude });
|
|
151
|
+
defer self.allocator.free(coordinate);
|
|
152
|
+
|
|
153
|
+
const grant = try self.runSimctl(&.{ "privacy", self.target(), "grant", "location", self.app_id }, default_max_output);
|
|
154
|
+
defer grant.deinit(self.allocator);
|
|
155
|
+
try grant.ensureSuccess();
|
|
156
|
+
|
|
157
|
+
const result = try self.runSimctl(&.{ "location", self.target(), "set", coordinate }, default_max_output);
|
|
158
|
+
defer result.deinit(self.allocator);
|
|
159
|
+
try result.ensureSuccess();
|
|
141
160
|
}
|
|
142
161
|
|
|
143
162
|
pub fn tap(self: *IosDevice, x: i32, y: i32) !void {
|
|
@@ -189,6 +208,12 @@ pub const IosDevice = struct {
|
|
|
189
208
|
try self.runShimAction(.{ .kind = .swipe, .x1 = x1, .y1 = y1, .x2 = x2, .y2 = y2, .duration_ms = duration_ms });
|
|
190
209
|
}
|
|
191
210
|
|
|
211
|
+
pub fn scrollViewport(self: *IosDevice) !types.Viewport {
|
|
212
|
+
const response = try self.runShim(.{ .kind = .viewport });
|
|
213
|
+
defer self.allocator.free(response);
|
|
214
|
+
return try ios_shim.parseViewportResponse(response);
|
|
215
|
+
}
|
|
216
|
+
|
|
192
217
|
pub fn pressBack(self: *IosDevice) !void {
|
|
193
218
|
try self.runShimAction(.{ .kind = .press_back });
|
|
194
219
|
}
|
|
@@ -322,20 +347,25 @@ pub const IosDevice = struct {
|
|
|
322
347
|
try ios_shim.parseOkResponse(response);
|
|
323
348
|
}
|
|
324
349
|
|
|
325
|
-
fn acceptOpenURLConfirmationBestEffort(self: *IosDevice) void {
|
|
350
|
+
fn acceptOpenURLConfirmationBestEffort(self: *IosDevice, url: []const u8) void {
|
|
326
351
|
if (self.shim_path == null) return;
|
|
327
352
|
var attempt: usize = 0;
|
|
328
353
|
while (attempt < open_link_interruption_attempts) {
|
|
329
354
|
attempt += 1;
|
|
330
|
-
if (self.acceptOpenURLConfirmationOnce() catch return) return;
|
|
355
|
+
if (self.acceptOpenURLConfirmationOnce(url) catch return) return;
|
|
331
356
|
if (attempt < open_link_interruption_attempts) {
|
|
332
357
|
stdio.sleepNs(open_link_interruption_retry_delay_ms * std.time.ns_per_ms);
|
|
333
358
|
}
|
|
334
359
|
}
|
|
335
360
|
}
|
|
336
361
|
|
|
337
|
-
fn acceptOpenURLConfirmationOnce(self: *IosDevice) !bool {
|
|
338
|
-
const response = try self.runShimWithTimeout(.{
|
|
362
|
+
fn acceptOpenURLConfirmationOnce(self: *IosDevice, url: []const u8) !bool {
|
|
363
|
+
const response = try self.runShimWithTimeout(.{
|
|
364
|
+
.kind = .accept_system_alert,
|
|
365
|
+
.text = "Open",
|
|
366
|
+
.url = url,
|
|
367
|
+
.expo_dev_client_fallback = self.expo_dev_client_open_link_mode,
|
|
368
|
+
}, shim_best_effort_timeout_ms);
|
|
339
369
|
defer self.allocator.free(response);
|
|
340
370
|
return try ios_shim.parseAcceptSystemAlertResponse(response);
|
|
341
371
|
}
|
|
@@ -460,6 +490,11 @@ fn parseShimTimeoutMs(raw: ?[]const u8) u64 {
|
|
|
460
490
|
return parsed;
|
|
461
491
|
}
|
|
462
492
|
|
|
493
|
+
fn isExpoDevClientOpenLink(url: []const u8) bool {
|
|
494
|
+
return std.mem.startsWith(u8, url, "exp+") and
|
|
495
|
+
std.mem.indexOf(u8, url, "://expo-development-client/") != null;
|
|
496
|
+
}
|
|
497
|
+
|
|
463
498
|
test "ios simulator openLink keeps sweeping delayed XCTest interruptions until accepted" {
|
|
464
499
|
const allocator = std.heap.page_allocator;
|
|
465
500
|
const argv = [_][*:0]const u8{"zmr-ios-test"};
|
|
@@ -523,6 +558,70 @@ test "ios simulator openLink keeps sweeping delayed XCTest interruptions until a
|
|
|
523
558
|
try std.testing.expectEqual(@as(u8, 6), count);
|
|
524
559
|
}
|
|
525
560
|
|
|
561
|
+
test "ios simulator setLocation grants app location permission and sets coordinates" {
|
|
562
|
+
const allocator = std.heap.page_allocator;
|
|
563
|
+
const argv = [_][*:0]const u8{"zmr-ios-test"};
|
|
564
|
+
stdio.initProcess(.{
|
|
565
|
+
.args = .{ .vector = argv[0..] },
|
|
566
|
+
.environ = .empty,
|
|
567
|
+
}, allocator);
|
|
568
|
+
defer stdio.deinitProcess();
|
|
569
|
+
|
|
570
|
+
var tmp = std.testing.tmpDir(.{});
|
|
571
|
+
defer tmp.cleanup();
|
|
572
|
+
|
|
573
|
+
var xcrun = try tmp.dir.createFile(stdio.io(), "fake-xcrun-location.sh", .{ .truncate = true });
|
|
574
|
+
{
|
|
575
|
+
var buffer: [4096]u8 = undefined;
|
|
576
|
+
var writer = xcrun.writerStreaming(stdio.io(), &buffer);
|
|
577
|
+
const script = try std.fmt.allocPrint(allocator,
|
|
578
|
+
\\#!/usr/bin/env bash
|
|
579
|
+
\\set -euo pipefail
|
|
580
|
+
\\printf '%s\n' "$*" >> ".zig-cache/tmp/{s}/xcrun.log"
|
|
581
|
+
\\if [[ "${{1:-}}" != "simctl" ]]; then
|
|
582
|
+
\\ echo "expected simctl" >&2
|
|
583
|
+
\\ exit 2
|
|
584
|
+
\\fi
|
|
585
|
+
\\shift
|
|
586
|
+
\\case "${{1:-}}" in
|
|
587
|
+
\\ privacy)
|
|
588
|
+
\\ [[ "${{2:-}}" == "fake-ios-1" && "${{3:-}}" == "grant" && "${{4:-}}" == "location" && "${{5:-}}" == "com.example.mobiletest" ]] || exit 2
|
|
589
|
+
\\ ;;
|
|
590
|
+
\\ location)
|
|
591
|
+
\\ [[ "${{2:-}}" == "fake-ios-1" && "${{3:-}}" == "set" && "${{4:-}}" == "51.507400,-0.127800" ]] || exit 2
|
|
592
|
+
\\ ;;
|
|
593
|
+
\\ *)
|
|
594
|
+
\\ echo "unsupported simctl command: $*" >&2
|
|
595
|
+
\\ exit 2
|
|
596
|
+
\\ ;;
|
|
597
|
+
\\esac
|
|
598
|
+
\\
|
|
599
|
+
, .{tmp.sub_path});
|
|
600
|
+
defer allocator.free(script);
|
|
601
|
+
try writer.interface.writeAll(script);
|
|
602
|
+
try writer.interface.flush();
|
|
603
|
+
}
|
|
604
|
+
xcrun.close(stdio.io());
|
|
605
|
+
|
|
606
|
+
const xcrun_path = try std.fmt.allocPrint(allocator, ".zig-cache/tmp/{s}/fake-xcrun-location.sh", .{tmp.sub_path});
|
|
607
|
+
defer allocator.free(xcrun_path);
|
|
608
|
+
const xcrun_path_z = try allocator.dupeZ(u8, xcrun_path);
|
|
609
|
+
defer allocator.free(xcrun_path_z);
|
|
610
|
+
if (std.c.chmod(xcrun_path_z, 0o755) != 0) return error.ChmodFailed;
|
|
611
|
+
|
|
612
|
+
var device = try IosDevice.initWithShim(allocator, xcrun_path, "fake-ios-1", "com.example.mobiletest", null);
|
|
613
|
+
defer device.deinit();
|
|
614
|
+
|
|
615
|
+
try device.setLocation(51.5074, -0.1278);
|
|
616
|
+
|
|
617
|
+
const log_path = try std.fmt.allocPrint(allocator, ".zig-cache/tmp/{s}/xcrun.log", .{tmp.sub_path});
|
|
618
|
+
defer allocator.free(log_path);
|
|
619
|
+
const log = try stdio.readFileAlloc(allocator, log_path, 1024);
|
|
620
|
+
defer allocator.free(log);
|
|
621
|
+
try std.testing.expect(std.mem.indexOf(u8, log, "simctl privacy fake-ios-1 grant location com.example.mobiletest") != null);
|
|
622
|
+
try std.testing.expect(std.mem.indexOf(u8, log, "simctl location fake-ios-1 set 51.507400,-0.127800") != null);
|
|
623
|
+
}
|
|
624
|
+
|
|
526
625
|
pub fn listDevices(allocator: std.mem.Allocator, xcrun_path: []const u8) ![]types.DeviceInfo {
|
|
527
626
|
return try ios_devices.listSimulators(allocator, xcrun_path);
|
|
528
627
|
}
|
package/src/ios_shim.zig
CHANGED
|
@@ -5,6 +5,7 @@ const types = @import("types.zig");
|
|
|
5
5
|
|
|
6
6
|
pub const CommandKind = enum {
|
|
7
7
|
snapshot,
|
|
8
|
+
viewport,
|
|
8
9
|
screenshot,
|
|
9
10
|
tap,
|
|
10
11
|
type_text,
|
|
@@ -22,6 +23,8 @@ pub const Command = struct {
|
|
|
22
23
|
kind: CommandKind,
|
|
23
24
|
selector: ?[]const u8 = null,
|
|
24
25
|
text: ?[]const u8 = null,
|
|
26
|
+
url: ?[]const u8 = null,
|
|
27
|
+
expo_dev_client_fallback: bool = false,
|
|
25
28
|
x: ?i32 = null,
|
|
26
29
|
y: ?i32 = null,
|
|
27
30
|
x1: ?i32 = null,
|
|
@@ -42,6 +45,19 @@ pub const SnapshotResponse = struct {
|
|
|
42
45
|
}
|
|
43
46
|
};
|
|
44
47
|
|
|
48
|
+
pub fn parseViewportResponse(content: []const u8) !types.Viewport {
|
|
49
|
+
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
|
50
|
+
defer arena.deinit();
|
|
51
|
+
const parsed = try std.json.parseFromSlice(std.json.Value, arena.allocator(), content, .{});
|
|
52
|
+
if (parsed.value != .object) return error.IosShimResponseMustBeObject;
|
|
53
|
+
const status = fieldString(parsed.value.object, "status") orelse return error.IosShimMissingStatus;
|
|
54
|
+
if (!std.mem.eql(u8, status, "ok")) return error.IosShimResponseNotOk;
|
|
55
|
+
const viewport_value = parsed.value.object.get("viewport") orelse return error.IosShimMissingViewport;
|
|
56
|
+
const viewport = parseViewport(viewport_value);
|
|
57
|
+
if (viewport.width == 0 or viewport.height == 0) return error.IosShimInvalidViewport;
|
|
58
|
+
return viewport;
|
|
59
|
+
}
|
|
60
|
+
|
|
45
61
|
pub fn writeCommandJson(writer: anytype, command: Command) !void {
|
|
46
62
|
try writer.writeAll("{\"cmd\":");
|
|
47
63
|
try trace.writeJsonString(writer, commandName(command.kind));
|
|
@@ -53,6 +69,13 @@ pub fn writeCommandJson(writer: anytype, command: Command) !void {
|
|
|
53
69
|
try writer.writeAll(",\"text\":");
|
|
54
70
|
try trace.writeJsonString(writer, text);
|
|
55
71
|
}
|
|
72
|
+
if (command.url) |url| {
|
|
73
|
+
try writer.writeAll(",\"url\":");
|
|
74
|
+
try trace.writeJsonString(writer, url);
|
|
75
|
+
}
|
|
76
|
+
if (command.expo_dev_client_fallback) {
|
|
77
|
+
try writer.writeAll(",\"expoDevClientFallback\":true");
|
|
78
|
+
}
|
|
56
79
|
if (command.x) |value| try writer.print(",\"x\":{d}", .{value});
|
|
57
80
|
if (command.y) |value| try writer.print(",\"y\":{d}", .{value});
|
|
58
81
|
if (command.x1) |value| try writer.print(",\"x1\":{d}", .{value});
|
|
@@ -270,6 +293,7 @@ pub fn selectorString(allocator: std.mem.Allocator, wanted: selectors.Selector)
|
|
|
270
293
|
fn commandName(kind: CommandKind) []const u8 {
|
|
271
294
|
return switch (kind) {
|
|
272
295
|
.snapshot => "snapshot",
|
|
296
|
+
.viewport => "viewport",
|
|
273
297
|
.screenshot => "screenshot",
|
|
274
298
|
.tap => "tap",
|
|
275
299
|
.type_text => "type",
|
package/src/json_fields.zig
CHANGED
|
@@ -31,6 +31,11 @@ pub fn requiredI32FromObject(object: std.json.ObjectMap, key: []const u8, missin
|
|
|
31
31
|
return i32Value(value, type_error);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
pub fn requiredF64FromObject(object: std.json.ObjectMap, key: []const u8, missing_error: anyerror, type_error: anyerror) !f64 {
|
|
35
|
+
const value = object.get(key) orelse return missing_error;
|
|
36
|
+
return f64Value(value, type_error);
|
|
37
|
+
}
|
|
38
|
+
|
|
34
39
|
pub fn optionalU64(params: ?std.json.Value, key: []const u8, default_value: u64, type_error: anyerror) !u64 {
|
|
35
40
|
const value = field(params, key) orelse return default_value;
|
|
36
41
|
return u64Value(value, type_error);
|
|
@@ -72,6 +77,14 @@ fn u64Value(value: std.json.Value, type_error: anyerror) !u64 {
|
|
|
72
77
|
};
|
|
73
78
|
}
|
|
74
79
|
|
|
80
|
+
fn f64Value(value: std.json.Value, type_error: anyerror) !f64 {
|
|
81
|
+
return switch (value) {
|
|
82
|
+
.float => |actual| actual,
|
|
83
|
+
.integer => |actual| @as(f64, @floatFromInt(actual)),
|
|
84
|
+
else => type_error,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
75
88
|
fn boolValue(value: std.json.Value, type_error: anyerror) !bool {
|
|
76
89
|
return switch (value) {
|
|
77
90
|
.bool => |actual| actual,
|
package/src/runner.zig
CHANGED
|
@@ -8,6 +8,7 @@ const scenario = @import("scenario.zig");
|
|
|
8
8
|
const selector = @import("selector.zig");
|
|
9
9
|
const trace = @import("trace.zig");
|
|
10
10
|
const types = @import("types.zig");
|
|
11
|
+
const fake_device = @import("fake_device.zig");
|
|
11
12
|
|
|
12
13
|
pub const RunOptions = runner_config.RunOptions;
|
|
13
14
|
|
|
@@ -86,6 +87,14 @@ pub fn executeStep(
|
|
|
86
87
|
if (writer) |tw| try runner_events.recordActionStatus(tw, "app.openLink", "ok", null, url);
|
|
87
88
|
try settleDevice(device, options);
|
|
88
89
|
},
|
|
90
|
+
.set_location => |location| {
|
|
91
|
+
device.setLocation(location.latitude, location.longitude) catch |err| {
|
|
92
|
+
if (writer) |tw| try runner_events.recordSetLocation(tw, "failed", err, location.latitude, location.longitude);
|
|
93
|
+
return err;
|
|
94
|
+
};
|
|
95
|
+
if (writer) |tw| try runner_events.recordSetLocation(tw, "ok", null, location.latitude, location.longitude);
|
|
96
|
+
try settleDevice(device, options);
|
|
97
|
+
},
|
|
89
98
|
.tap => |wanted| try tapSelector(device, wanted, writer, options),
|
|
90
99
|
.type_text => |input| {
|
|
91
100
|
if (input.selector) |wanted| return try typeTextSelector(device, wanted, input.text, writer, options);
|
|
@@ -309,6 +318,46 @@ fn settleDevice(device: anytype, options: RunOptions) !void {
|
|
|
309
318
|
try device.settle(options.settle_ms);
|
|
310
319
|
}
|
|
311
320
|
|
|
321
|
+
test "setLocation dispatches through the device, records trace evidence, and settles" {
|
|
322
|
+
const allocator = std.testing.allocator;
|
|
323
|
+
const dir = "zig-cache-test-runner-set-location";
|
|
324
|
+
std.Io.Dir.cwd().deleteTree(stdio.io(), dir) catch {};
|
|
325
|
+
defer std.Io.Dir.cwd().deleteTree(stdio.io(), dir) catch {};
|
|
326
|
+
|
|
327
|
+
const script_json =
|
|
328
|
+
\\{
|
|
329
|
+
\\ "name": "set location",
|
|
330
|
+
\\ "steps": [
|
|
331
|
+
\\ {"action": "setLocation", "latitude": 51.5074, "longitude": -0.1278}
|
|
332
|
+
\\ ]
|
|
333
|
+
\\}
|
|
334
|
+
;
|
|
335
|
+
const script = try scenario.parseSlice(allocator, script_json);
|
|
336
|
+
defer script.deinit(allocator);
|
|
337
|
+
|
|
338
|
+
var device = fake_device.FakeDevice.init(allocator, &.{});
|
|
339
|
+
defer device.deinit();
|
|
340
|
+
var tw = try trace.TraceWriter.init(allocator, dir);
|
|
341
|
+
defer tw.deinit();
|
|
342
|
+
|
|
343
|
+
try runScenario(allocator, &device, script, &tw, .{ .settle_ms = 25 });
|
|
344
|
+
|
|
345
|
+
try std.testing.expectEqual(@as(usize, 1), device.location_sets);
|
|
346
|
+
try std.testing.expectApproxEqAbs(@as(f64, 51.5074), device.last_location.?.latitude, 0.000001);
|
|
347
|
+
try std.testing.expectApproxEqAbs(@as(f64, -0.1278), device.last_location.?.longitude, 0.000001);
|
|
348
|
+
try std.testing.expectEqual(@as(usize, 1), device.settles);
|
|
349
|
+
try std.testing.expectEqual(@as(u64, 25), device.last_settle_timeout_ms);
|
|
350
|
+
|
|
351
|
+
const events_path = try std.fs.path.join(allocator, &.{ dir, "events.jsonl" });
|
|
352
|
+
defer allocator.free(events_path);
|
|
353
|
+
const events = try stdio.readFileAlloc(allocator, events_path, 1024 * 1024);
|
|
354
|
+
defer allocator.free(events);
|
|
355
|
+
try std.testing.expect(std.mem.indexOf(u8, events, "\"kind\":\"device.setLocation\"") != null);
|
|
356
|
+
try std.testing.expect(std.mem.indexOf(u8, events, "\"status\":\"ok\"") != null);
|
|
357
|
+
try std.testing.expect(std.mem.indexOf(u8, events, "\"latitude\":51.5074") != null);
|
|
358
|
+
try std.testing.expect(std.mem.indexOf(u8, events, "\"longitude\":-0.1278") != null);
|
|
359
|
+
}
|
|
360
|
+
|
|
312
361
|
test "whenVisible skips the conditional block when the visibility probe command fails" {
|
|
313
362
|
const allocator = std.testing.allocator;
|
|
314
363
|
const dir = "zig-cache-test-runner-when-visible-command-failed";
|
|
@@ -336,6 +385,12 @@ test "whenVisible skips the conditional block when the visibility probe command
|
|
|
336
385
|
_ = url;
|
|
337
386
|
}
|
|
338
387
|
|
|
388
|
+
pub fn setLocation(self: *@This(), latitude: f64, longitude: f64) !void {
|
|
389
|
+
_ = self;
|
|
390
|
+
_ = latitude;
|
|
391
|
+
_ = longitude;
|
|
392
|
+
}
|
|
393
|
+
|
|
339
394
|
pub fn tap(self: *@This(), x: i32, y: i32) !void {
|
|
340
395
|
_ = self;
|
|
341
396
|
_ = x;
|
package/src/runner_events.zig
CHANGED
|
@@ -26,6 +26,23 @@ pub fn recordNativeWait(tw: *trace.TraceWriter, kind: []const u8, wanted: select
|
|
|
26
26
|
try tw.recordEvent(kind, writer.buffered());
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
pub fn recordNativeScrollUntilVisible(
|
|
30
|
+
tw: *trace.TraceWriter,
|
|
31
|
+
wanted: selector.Selector,
|
|
32
|
+
direction: []const u8,
|
|
33
|
+
timeout_ms: u64,
|
|
34
|
+
) !void {
|
|
35
|
+
var payload: std.Io.Writer.Allocating = .init(tw.allocator);
|
|
36
|
+
defer payload.deinit();
|
|
37
|
+
const writer = &payload.writer;
|
|
38
|
+
try writer.writeAll("{\"status\":\"ok\",\"strategy\":\"nativeSelector\",\"selector\":");
|
|
39
|
+
try trace.writeSelectorJson(writer, wanted);
|
|
40
|
+
try writer.writeAll(",\"direction\":");
|
|
41
|
+
try trace.writeJsonString(writer, direction);
|
|
42
|
+
try writer.print(",\"timeoutMs\":{d}}}", .{timeout_ms});
|
|
43
|
+
try tw.recordEvent("ui.scrollUntilVisible", writer.buffered());
|
|
44
|
+
}
|
|
45
|
+
|
|
29
46
|
pub fn recordNativeWaitTimeout(tw: *trace.TraceWriter, kind: []const u8, selectors: []const selector.Selector, timeout_ms: u64) !void {
|
|
30
47
|
var payload: std.Io.Writer.Allocating = .init(tw.allocator);
|
|
31
48
|
defer payload.deinit();
|
|
@@ -120,6 +137,20 @@ pub fn recordActionStatus(tw: *trace.TraceWriter, kind: []const u8, status: []co
|
|
|
120
137
|
try tw.recordEvent(kind, out.buffered());
|
|
121
138
|
}
|
|
122
139
|
|
|
140
|
+
pub fn recordSetLocation(tw: *trace.TraceWriter, status: []const u8, err: ?anyerror, latitude: f64, longitude: f64) !void {
|
|
141
|
+
var payload: std.Io.Writer.Allocating = .init(tw.allocator);
|
|
142
|
+
defer payload.deinit();
|
|
143
|
+
const out = &payload.writer;
|
|
144
|
+
try out.writeAll("{\"status\":");
|
|
145
|
+
try trace.writeJsonString(out, status);
|
|
146
|
+
if (err) |actual| {
|
|
147
|
+
try out.writeAll(",\"error\":");
|
|
148
|
+
try trace.writeJsonString(out, @errorName(actual));
|
|
149
|
+
}
|
|
150
|
+
try out.print(",\"latitude\":{d:.6},\"longitude\":{d:.6}}}", .{ latitude, longitude });
|
|
151
|
+
try tw.recordEvent("device.setLocation", out.buffered());
|
|
152
|
+
}
|
|
153
|
+
|
|
123
154
|
pub fn recordSwipe(tw: *trace.TraceWriter, x1: i32, y1: i32, x2: i32, y2: i32, duration_ms: u32) !void {
|
|
124
155
|
const payload = try std.fmt.allocPrint(
|
|
125
156
|
tw.allocator,
|