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/runner_waits.zig
CHANGED
|
@@ -9,6 +9,7 @@ const trace = @import("trace.zig");
|
|
|
9
9
|
|
|
10
10
|
const RunOptions = runner_config.RunOptions;
|
|
11
11
|
const native_health_probe_timeout_ms: u64 = 1000;
|
|
12
|
+
const native_selector_transient_retry_limit: usize = 1;
|
|
12
13
|
|
|
13
14
|
pub fn waitUntilVisible(
|
|
14
15
|
device: anytype,
|
|
@@ -39,23 +40,36 @@ fn untilVisibleKind(
|
|
|
39
40
|
kind: []const u8,
|
|
40
41
|
) !bool {
|
|
41
42
|
const deadline = stdio.nowMs() + @as(i64, @intCast(timeout_ms));
|
|
43
|
+
var native_query_failures: usize = 0;
|
|
42
44
|
while (true) {
|
|
43
|
-
if (nativeSelectorQueryTimeoutMs(deadline)) |query_timeout_ms| {
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
if (nativeSelectorQueryTimeoutMs(deadline, options)) |query_timeout_ms| {
|
|
46
|
+
var native_query_failed = false;
|
|
47
|
+
const native_result = nativeVisibleBySelector(device, wanted, query_timeout_ms) catch |err| blk: {
|
|
48
|
+
if (try recordTransientNativeSelectorObservation(err, kind, writer)) {
|
|
49
|
+
native_query_failures += 1;
|
|
50
|
+
if (native_query_failures <= native_selector_transient_retry_limit and stdio.nowMs() < deadline) {
|
|
51
|
+
try sleepMs(options.poll_ms);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
native_query_failed = true;
|
|
55
|
+
break :blk null;
|
|
56
|
+
}
|
|
46
57
|
return err;
|
|
47
58
|
};
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
if (!native_query_failed) native_query_failures = 0;
|
|
60
|
+
if (!native_query_failed) {
|
|
61
|
+
if (native_result) |visible| {
|
|
62
|
+
if (visible) {
|
|
63
|
+
if (writer) |tw| try runner_events.recordNativeWait(tw, kind, wanted, null, timeout_ms);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
if (stdio.nowMs() >= deadline) {
|
|
67
|
+
if (writer) |tw| try runner_events.recordNativeWaitTimeoutWithDiagnostics(device, tw, kind, &[_]selector.Selector{wanted}, timeout_ms);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
try sleepMs(options.poll_ms);
|
|
71
|
+
continue;
|
|
56
72
|
}
|
|
57
|
-
try sleepMs(options.poll_ms);
|
|
58
|
-
continue;
|
|
59
73
|
}
|
|
60
74
|
} else if (hasNativeSelectorQuery(device)) {
|
|
61
75
|
if (writer) |tw| try runner_events.recordNativeWaitTimeoutWithDiagnostics(device, tw, kind, &[_]selector.Selector{wanted}, timeout_ms);
|
|
@@ -119,23 +133,36 @@ fn untilNotVisibleKind(
|
|
|
119
133
|
kind: []const u8,
|
|
120
134
|
) !bool {
|
|
121
135
|
const deadline = stdio.nowMs() + @as(i64, @intCast(timeout_ms));
|
|
136
|
+
var native_query_failures: usize = 0;
|
|
122
137
|
while (true) {
|
|
123
|
-
if (nativeSelectorQueryTimeoutMs(deadline)) |query_timeout_ms| {
|
|
124
|
-
|
|
125
|
-
|
|
138
|
+
if (nativeSelectorQueryTimeoutMs(deadline, options)) |query_timeout_ms| {
|
|
139
|
+
var native_query_failed = false;
|
|
140
|
+
const native_result = nativeVisibleBySelector(device, wanted, query_timeout_ms) catch |err| blk: {
|
|
141
|
+
if (try recordTransientNativeSelectorObservation(err, kind, writer)) {
|
|
142
|
+
native_query_failures += 1;
|
|
143
|
+
if (native_query_failures <= native_selector_transient_retry_limit and stdio.nowMs() < deadline) {
|
|
144
|
+
try sleepMs(options.poll_ms);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
native_query_failed = true;
|
|
148
|
+
break :blk null;
|
|
149
|
+
}
|
|
126
150
|
return err;
|
|
127
151
|
};
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
152
|
+
if (!native_query_failed) native_query_failures = 0;
|
|
153
|
+
if (!native_query_failed) {
|
|
154
|
+
if (native_result) |visible| {
|
|
155
|
+
if (!visible) {
|
|
156
|
+
if (writer) |tw| try runner_events.recordNativeWait(tw, kind, wanted, null, timeout_ms);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
if (stdio.nowMs() >= deadline) {
|
|
160
|
+
if (writer) |tw| try runner_events.recordNativeWaitTimeoutWithDiagnostics(device, tw, kind, &[_]selector.Selector{wanted}, timeout_ms);
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
try sleepMs(options.poll_ms);
|
|
164
|
+
continue;
|
|
136
165
|
}
|
|
137
|
-
try sleepMs(options.poll_ms);
|
|
138
|
-
continue;
|
|
139
166
|
}
|
|
140
167
|
} else if (hasNativeSelectorQuery(device)) {
|
|
141
168
|
if (writer) |tw| try runner_events.recordNativeWaitTimeoutWithDiagnostics(device, tw, kind, &[_]selector.Selector{wanted}, timeout_ms);
|
|
@@ -178,10 +205,11 @@ pub fn waitUntilAnyVisible(
|
|
|
178
205
|
options: RunOptions,
|
|
179
206
|
) !?usize {
|
|
180
207
|
const deadline = stdio.nowMs() + @as(i64, @intCast(timeout_ms));
|
|
181
|
-
|
|
208
|
+
var native_query_failures: usize = 0;
|
|
209
|
+
native_poll: while (true) {
|
|
182
210
|
var all_native = true;
|
|
183
211
|
for (selectors, 0..) |wanted, index| {
|
|
184
|
-
const query_timeout_ms = nativeSelectorQueryTimeoutMs(deadline) orelse {
|
|
212
|
+
const query_timeout_ms = nativeSelectorQueryTimeoutMs(deadline, options) orelse {
|
|
185
213
|
if (hasNativeSelectorQuery(device)) {
|
|
186
214
|
if (writer) |tw| try runner_events.recordNativeWaitTimeoutWithDiagnostics(device, tw, "wait.any", selectors, timeout_ms);
|
|
187
215
|
return null;
|
|
@@ -190,9 +218,18 @@ pub fn waitUntilAnyVisible(
|
|
|
190
218
|
break;
|
|
191
219
|
};
|
|
192
220
|
const native_result = nativeVisibleBySelector(device, wanted, query_timeout_ms) catch |err| {
|
|
193
|
-
if (try
|
|
221
|
+
if (try recordTransientNativeSelectorObservation(err, "wait.any", writer)) {
|
|
222
|
+
native_query_failures += 1;
|
|
223
|
+
if (native_query_failures <= native_selector_transient_retry_limit and stdio.nowMs() < deadline) {
|
|
224
|
+
try sleepMs(options.poll_ms);
|
|
225
|
+
continue :native_poll;
|
|
226
|
+
}
|
|
227
|
+
all_native = false;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
194
230
|
return err;
|
|
195
231
|
};
|
|
232
|
+
native_query_failures = 0;
|
|
196
233
|
if (native_result) |visible| {
|
|
197
234
|
if (visible) {
|
|
198
235
|
if (writer) |tw| try runner_events.recordNativeWait(tw, "wait.any", wanted, index, timeout_ms);
|
|
@@ -322,7 +359,7 @@ fn nativeAssertHealthy(
|
|
|
322
359
|
|
|
323
360
|
const deadline = stdio.nowMs() + @as(i64, @intCast(timeout_ms));
|
|
324
361
|
native_probe: while (true) {
|
|
325
|
-
const remaining_ms =
|
|
362
|
+
const remaining_ms = nativeSelectorRemainingTimeoutMs(deadline) orelse return null;
|
|
326
363
|
const query_timeout_ms = @min(remaining_ms, nativeHealthProbeTimeoutMs(timeout_ms, options));
|
|
327
364
|
|
|
328
365
|
for (health_selectors, 0..) |wanted, index| {
|
|
@@ -356,7 +393,47 @@ pub fn scrollUntilVisible(
|
|
|
356
393
|
options: RunOptions,
|
|
357
394
|
) !bool {
|
|
358
395
|
const deadline = stdio.nowMs() + @as(i64, @intCast(timeout_ms));
|
|
396
|
+
var native_query_failures: usize = 0;
|
|
359
397
|
while (true) {
|
|
398
|
+
if (nativeSelectorQueryTimeoutMs(deadline, options)) |query_timeout_ms| {
|
|
399
|
+
var native_query_failed = false;
|
|
400
|
+
const native_result = nativeVisibleBySelector(device, wanted, query_timeout_ms) catch |err| blk: {
|
|
401
|
+
if (try recordTransientNativeSelectorObservation(err, "ui.scrollUntilVisible", writer)) {
|
|
402
|
+
native_query_failures += 1;
|
|
403
|
+
if (native_query_failures <= native_selector_transient_retry_limit and stdio.nowMs() < deadline) {
|
|
404
|
+
try sleepMs(options.poll_ms);
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
native_query_failed = true;
|
|
408
|
+
break :blk null;
|
|
409
|
+
}
|
|
410
|
+
return err;
|
|
411
|
+
};
|
|
412
|
+
if (!native_query_failed) native_query_failures = 0;
|
|
413
|
+
if (!native_query_failed) {
|
|
414
|
+
if (native_result) |visible| {
|
|
415
|
+
if (visible) {
|
|
416
|
+
if (writer) |tw| try runner_events.recordNativeScrollUntilVisible(
|
|
417
|
+
tw,
|
|
418
|
+
wanted,
|
|
419
|
+
if (direction == .down) "down" else "up",
|
|
420
|
+
timeout_ms,
|
|
421
|
+
);
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
if (stdio.nowMs() >= deadline) {
|
|
425
|
+
if (writer) |tw| try runner_events.recordNativeWaitTimeoutWithDiagnostics(device, tw, "ui.scrollUntilVisible", &[_]selector.Selector{wanted}, timeout_ms);
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
try scrollDevice(device, direction, writer, options);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
} else if (hasNativeSelectorQuery(device)) {
|
|
433
|
+
if (writer) |tw| try runner_events.recordNativeWaitTimeoutWithDiagnostics(device, tw, "ui.scrollUntilVisible", &[_]selector.Selector{wanted}, timeout_ms);
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
|
|
360
437
|
var snap = device.snapshot(writer) catch |err| {
|
|
361
438
|
if (try retryTransientObservation(err, "ui.scrollUntilVisible", writer, deadline, options)) continue;
|
|
362
439
|
return err;
|
|
@@ -385,29 +462,7 @@ pub fn scrollUntilVisible(
|
|
|
385
462
|
return false;
|
|
386
463
|
}
|
|
387
464
|
|
|
388
|
-
|
|
389
|
-
const height = if (snap.viewport.height == 0) @as(i32, 1280) else @as(i32, @intCast(snap.viewport.height));
|
|
390
|
-
const x = @divTrunc(width, 2);
|
|
391
|
-
const start_y = switch (direction) {
|
|
392
|
-
.down => @divTrunc(height * 4, 5),
|
|
393
|
-
.up => @divTrunc(height * 3, 10),
|
|
394
|
-
};
|
|
395
|
-
const end_y = switch (direction) {
|
|
396
|
-
.down => @divTrunc(height * 3, 10),
|
|
397
|
-
.up => @divTrunc(height * 4, 5),
|
|
398
|
-
};
|
|
399
|
-
try device.swipe(x, start_y, x, end_y, 350);
|
|
400
|
-
if (writer) |tw| {
|
|
401
|
-
const payload = try std.fmt.allocPrint(tw.allocator, "{{\"direction\":\"{s}\",\"x\":{d},\"y1\":{d},\"y2\":{d}}}", .{
|
|
402
|
-
if (direction == .down) "down" else "up",
|
|
403
|
-
x,
|
|
404
|
-
start_y,
|
|
405
|
-
end_y,
|
|
406
|
-
});
|
|
407
|
-
defer tw.allocator.free(payload);
|
|
408
|
-
try tw.recordEvent("ui.scroll", payload);
|
|
409
|
-
}
|
|
410
|
-
try settleDevice(device, options);
|
|
465
|
+
try scrollDeviceWithViewport(device, direction, writer, options, snap.viewport);
|
|
411
466
|
}
|
|
412
467
|
}
|
|
413
468
|
|
|
@@ -421,7 +476,13 @@ fn nativeVisibleBySelector(device: anytype, wanted: selector.Selector, timeout_m
|
|
|
421
476
|
return try device.visibleBySelector(wanted);
|
|
422
477
|
}
|
|
423
478
|
|
|
424
|
-
fn nativeSelectorQueryTimeoutMs(deadline: i64) ?u64 {
|
|
479
|
+
fn nativeSelectorQueryTimeoutMs(deadline: i64, options: RunOptions) ?u64 {
|
|
480
|
+
const remaining_ms = nativeSelectorRemainingTimeoutMs(deadline) orelse return null;
|
|
481
|
+
if (options.action_timeout_ms == 0) return remaining_ms;
|
|
482
|
+
return @max(@as(u64, 1), @min(remaining_ms, options.action_timeout_ms));
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
fn nativeSelectorRemainingTimeoutMs(deadline: i64) ?u64 {
|
|
425
486
|
const now = stdio.nowMs();
|
|
426
487
|
if (now >= deadline) return null;
|
|
427
488
|
return @as(u64, @intCast(deadline - now));
|
|
@@ -434,6 +495,16 @@ fn nativeHealthProbeTimeoutMs(timeout_ms: u64, options: RunOptions) u64 {
|
|
|
434
495
|
return @max(probe_timeout_ms, 1);
|
|
435
496
|
}
|
|
436
497
|
|
|
498
|
+
fn recordTransientNativeSelectorObservation(
|
|
499
|
+
err: anyerror,
|
|
500
|
+
kind: []const u8,
|
|
501
|
+
writer: ?*trace.TraceWriter,
|
|
502
|
+
) !bool {
|
|
503
|
+
if (err != error.CommandTimedOut and err != error.CommandFailed) return false;
|
|
504
|
+
if (writer) |tw| try runner_events.recordObservationRetry(tw, kind, err);
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
|
|
437
508
|
fn retryTransientObservation(
|
|
438
509
|
err: anyerror,
|
|
439
510
|
kind: []const u8,
|
|
@@ -448,6 +519,54 @@ fn retryTransientObservation(
|
|
|
448
519
|
return true;
|
|
449
520
|
}
|
|
450
521
|
|
|
522
|
+
fn scrollDevice(
|
|
523
|
+
device: anytype,
|
|
524
|
+
direction: scenario.ScrollDirection,
|
|
525
|
+
writer: ?*trace.TraceWriter,
|
|
526
|
+
options: RunOptions,
|
|
527
|
+
) !void {
|
|
528
|
+
try scrollDeviceWithViewport(device, direction, writer, options, try scrollViewport(device));
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
fn scrollViewport(device: anytype) !@import("types.zig").Viewport {
|
|
532
|
+
if (@hasDecl(@TypeOf(device.*), "scrollViewport")) {
|
|
533
|
+
return try device.scrollViewport();
|
|
534
|
+
}
|
|
535
|
+
return .{};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
fn scrollDeviceWithViewport(
|
|
539
|
+
device: anytype,
|
|
540
|
+
direction: scenario.ScrollDirection,
|
|
541
|
+
writer: ?*trace.TraceWriter,
|
|
542
|
+
options: RunOptions,
|
|
543
|
+
viewport: @import("types.zig").Viewport,
|
|
544
|
+
) !void {
|
|
545
|
+
const width = if (viewport.width == 0) @as(i32, 720) else @as(i32, @intCast(viewport.width));
|
|
546
|
+
const height = if (viewport.height == 0) @as(i32, 1280) else @as(i32, @intCast(viewport.height));
|
|
547
|
+
const x = @divTrunc(width, 2);
|
|
548
|
+
const start_y = switch (direction) {
|
|
549
|
+
.down => @divTrunc(height * 4, 5),
|
|
550
|
+
.up => @divTrunc(height * 3, 10),
|
|
551
|
+
};
|
|
552
|
+
const end_y = switch (direction) {
|
|
553
|
+
.down => @divTrunc(height * 3, 10),
|
|
554
|
+
.up => @divTrunc(height * 4, 5),
|
|
555
|
+
};
|
|
556
|
+
try device.swipe(x, start_y, x, end_y, 350);
|
|
557
|
+
if (writer) |tw| {
|
|
558
|
+
const payload = try std.fmt.allocPrint(tw.allocator, "{{\"direction\":\"{s}\",\"x\":{d},\"y1\":{d},\"y2\":{d}}}", .{
|
|
559
|
+
if (direction == .down) "down" else "up",
|
|
560
|
+
x,
|
|
561
|
+
start_y,
|
|
562
|
+
end_y,
|
|
563
|
+
});
|
|
564
|
+
defer tw.allocator.free(payload);
|
|
565
|
+
try tw.recordEvent("ui.scroll", payload);
|
|
566
|
+
}
|
|
567
|
+
try settleDevice(device, options);
|
|
568
|
+
}
|
|
569
|
+
|
|
451
570
|
fn settleDevice(device: anytype, options: RunOptions) !void {
|
|
452
571
|
try device.settle(options.settle_ms);
|
|
453
572
|
}
|
package/src/scenario.zig
CHANGED
|
@@ -11,6 +11,11 @@ pub const Swipe = struct {
|
|
|
11
11
|
duration_ms: u32 = 300,
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
pub const Location = struct {
|
|
15
|
+
latitude: f64,
|
|
16
|
+
longitude: f64,
|
|
17
|
+
};
|
|
18
|
+
|
|
14
19
|
pub const WaitVisible = struct {
|
|
15
20
|
selector: selector.Selector,
|
|
16
21
|
timeout_ms: u64 = 5000,
|
|
@@ -106,6 +111,7 @@ pub const Step = union(enum) {
|
|
|
106
111
|
clear_state,
|
|
107
112
|
snapshot,
|
|
108
113
|
open_link: []const u8,
|
|
114
|
+
set_location: Location,
|
|
109
115
|
tap: selector.Selector,
|
|
110
116
|
type_text: TypeText,
|
|
111
117
|
press_back,
|
|
@@ -224,6 +230,10 @@ fn parseRawStep(allocator: std.mem.Allocator, object: std.json.ObjectMap) anyerr
|
|
|
224
230
|
if (std.mem.eql(u8, action, "hideKeyboard")) return .hide_keyboard;
|
|
225
231
|
if (std.mem.eql(u8, action, "sleep")) return .{ .sleep_ms = try fields.optionalU64(object, "ms", 500) };
|
|
226
232
|
if (std.mem.eql(u8, action, "openLink")) return .{ .open_link = try fields.requiredStringOrError(allocator, object, "url", error.StepMissingUrl) };
|
|
233
|
+
if (std.mem.eql(u8, action, "setLocation")) return .{ .set_location = .{
|
|
234
|
+
.latitude = try parseLatitude(object),
|
|
235
|
+
.longitude = try parseLongitude(object),
|
|
236
|
+
} };
|
|
227
237
|
if (std.mem.eql(u8, action, "tap")) return .{ .tap = try fields.parseSelectorField(allocator, object) };
|
|
228
238
|
if (std.mem.eql(u8, action, "typeText")) {
|
|
229
239
|
const wanted = if (object.get("selector")) |selector_value| try selector.parseFromJson(allocator, selector_value) else null;
|
|
@@ -342,6 +352,18 @@ fn parseRawStep(allocator: std.mem.Allocator, object: std.json.ObjectMap) anyerr
|
|
|
342
352
|
return error.unknownScenarioAction;
|
|
343
353
|
}
|
|
344
354
|
|
|
355
|
+
fn parseLatitude(object: std.json.ObjectMap) !f64 {
|
|
356
|
+
const latitude = try fields.requiredF64OrError(object, "latitude", error.StepMissingLatitude, error.StepLatitudeMustBeNumber);
|
|
357
|
+
if (latitude < -90.0 or latitude > 90.0) return error.StepLatitudeOutOfRange;
|
|
358
|
+
return latitude;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
fn parseLongitude(object: std.json.ObjectMap) !f64 {
|
|
362
|
+
const longitude = try fields.requiredF64OrError(object, "longitude", error.StepMissingLongitude, error.StepLongitudeMustBeNumber);
|
|
363
|
+
if (longitude < -180.0 or longitude > 180.0) return error.StepLongitudeOutOfRange;
|
|
364
|
+
return longitude;
|
|
365
|
+
}
|
|
366
|
+
|
|
345
367
|
fn appendParsedSteps(allocator: std.mem.Allocator, steps: *std.ArrayList(Step), value: std.json.Value) anyerror!void {
|
|
346
368
|
if (value != .array) return error.ScenarioStepsMustBeArray;
|
|
347
369
|
for (value.array.items) |step_value| {
|
|
@@ -373,3 +395,27 @@ fn optionalTimeoutMs(object: std.json.ObjectMap) !?u64 {
|
|
|
373
395
|
if (object.get("timeoutMs") == null) return null;
|
|
374
396
|
return try fields.optionalU64(object, "timeoutMs", 0);
|
|
375
397
|
}
|
|
398
|
+
|
|
399
|
+
test "parses setLocation with latitude and longitude" {
|
|
400
|
+
const allocator = std.testing.allocator;
|
|
401
|
+
const script_json =
|
|
402
|
+
\\{
|
|
403
|
+
\\ "name": "set location smoke",
|
|
404
|
+
\\ "steps": [
|
|
405
|
+
\\ {"action": "setLocation", "latitude": 51.5074, "longitude": -0.1278}
|
|
406
|
+
\\ ]
|
|
407
|
+
\\}
|
|
408
|
+
;
|
|
409
|
+
|
|
410
|
+
const script = try parseSlice(allocator, script_json);
|
|
411
|
+
defer script.deinit(allocator);
|
|
412
|
+
|
|
413
|
+
try std.testing.expectEqual(@as(usize, 1), script.steps.len);
|
|
414
|
+
switch (script.steps[0]) {
|
|
415
|
+
.set_location => |location| {
|
|
416
|
+
try std.testing.expectApproxEqAbs(@as(f64, 51.5074), location.latitude, 0.000001);
|
|
417
|
+
try std.testing.expectApproxEqAbs(@as(f64, -0.1278), location.longitude, 0.000001);
|
|
418
|
+
},
|
|
419
|
+
else => return error.ExpectedSetLocationStep,
|
|
420
|
+
}
|
|
421
|
+
}
|
package/src/scenario_fields.zig
CHANGED
|
@@ -21,6 +21,10 @@ pub fn requiredI32OrError(object: std.json.ObjectMap, key: []const u8, missing_e
|
|
|
21
21
|
return try json_fields.requiredI32FromObject(object, key, missing_error, error.RequiredFieldMustBeInteger);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
pub fn requiredF64OrError(object: std.json.ObjectMap, key: []const u8, missing_error: anyerror, type_error: anyerror) !f64 {
|
|
25
|
+
return try json_fields.requiredF64FromObject(object, key, missing_error, type_error);
|
|
26
|
+
}
|
|
27
|
+
|
|
24
28
|
pub fn optionalU64(object: std.json.ObjectMap, key: []const u8, default_value: u64) !u64 {
|
|
25
29
|
return try json_fields.optionalU64FromObject(object, key, default_value, error.OptionalFieldMustBeInteger);
|
|
26
30
|
}
|
package/src/validation.zig
CHANGED
|
@@ -109,6 +109,14 @@ fn diagnoseFailure(allocator: std.mem.Allocator, content: []const u8, err: anyer
|
|
|
109
109
|
=> try pathDiagnostic(allocator, content, "$.steps[].x2", "x2"),
|
|
110
110
|
error.StepMissingY2,
|
|
111
111
|
=> try pathDiagnostic(allocator, content, "$.steps[].y2", "y2"),
|
|
112
|
+
error.StepMissingLatitude,
|
|
113
|
+
error.StepLatitudeMustBeNumber,
|
|
114
|
+
error.StepLatitudeOutOfRange,
|
|
115
|
+
=> try pathDiagnostic(allocator, content, "$.steps[].latitude", "latitude"),
|
|
116
|
+
error.StepMissingLongitude,
|
|
117
|
+
error.StepLongitudeMustBeNumber,
|
|
118
|
+
error.StepLongitudeOutOfRange,
|
|
119
|
+
=> try pathDiagnostic(allocator, content, "$.steps[].longitude", "longitude"),
|
|
112
120
|
error.MissingSelector,
|
|
113
121
|
error.StepMissingSelector,
|
|
114
122
|
error.SelectorMustNotBeEmpty,
|
package/src/version.zig
CHANGED