zeno-mobile-runner 0.1.8 → 0.2.1

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 (66) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/FEATURES.md +1 -1
  3. package/README.md +175 -238
  4. package/clients/kotlin/README.md +1 -1
  5. package/clients/kotlin/build.gradle.kts +1 -1
  6. package/clients/python/pyproject.toml +1 -1
  7. package/clients/rust/Cargo.lock +1 -1
  8. package/clients/rust/Cargo.toml +1 -1
  9. package/clients/typescript/package.json +1 -1
  10. package/docs/agent-discovery.md +10 -0
  11. package/docs/ai-agents.md +18 -0
  12. package/docs/benchmarking.md +39 -0
  13. package/docs/benchmarks/2026-06-09-android-workflow.md +73 -0
  14. package/docs/benchmarks/2026-06-09-android-workflow.results.jsonl +20 -0
  15. package/docs/benchmarks/2026-06-09-framework-baseline-status.md +32 -0
  16. package/docs/benchmarks/2026-06-09-ios-appium-comparison.md +115 -0
  17. package/docs/benchmarks/2026-06-09-ios-appium-comparison.results.jsonl +40 -0
  18. package/docs/benchmarks/2026-06-09-ios-demo.md +90 -0
  19. package/docs/benchmarks/2026-06-09-ios-demo.results.jsonl +20 -0
  20. package/docs/benchmarks/2026-06-09-ios-maestro-comparison.md +128 -0
  21. package/docs/benchmarks/2026-06-09-ios-maestro-comparison.results.jsonl +40 -0
  22. package/docs/benchmarks/2026-06-09-ios-workflow-comparison.md +143 -0
  23. package/docs/benchmarks/2026-06-09-ios-workflow-comparison.results.jsonl +40 -0
  24. package/docs/benchmarks/2026-06-09-ios-xctest-floor.md +106 -0
  25. package/docs/benchmarks/2026-06-09-ios-xctest-floor.results.jsonl +40 -0
  26. package/docs/benchmarks/README.md +36 -0
  27. package/docs/benchmarks/benchmark-lab-v1.json +155 -0
  28. package/docs/benchmarks/benchmark-lab-v1.md +95 -0
  29. package/docs/clients.md +16 -0
  30. package/docs/demo.md +36 -1
  31. package/docs/frameworks.md +10 -0
  32. package/docs/npm.md +44 -2
  33. package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
  34. package/docs/protocol.md +10 -10
  35. package/docs/scenario-authoring.md +15 -0
  36. package/docs/trace-privacy.md +9 -0
  37. package/docs/troubleshooting.md +6 -0
  38. package/examples/android-workflow.json +79 -0
  39. package/examples/ios-dev-client-open-link.json +24 -13
  40. package/examples/ios-dev-client-route-snapshot.json +33 -8
  41. package/examples/ios-shim-workflow.json +79 -0
  42. package/examples/react-native-expo-workflow.json +75 -0
  43. package/npm/scenarios.mjs +15 -8
  44. package/npm/wizard.mjs +1 -1
  45. package/package.json +6 -1
  46. package/prebuilds/darwin-arm64/zmr +0 -0
  47. package/prebuilds/darwin-x64/zmr +0 -0
  48. package/prebuilds/linux-arm64/zmr +0 -0
  49. package/prebuilds/linux-x64/zmr +0 -0
  50. package/scripts/benchmark-lab.py +253 -0
  51. package/scripts/create-android-demo-app.sh +324 -29
  52. package/scripts/create-ios-demo-app.sh +174 -7
  53. package/scripts/create-react-native-expo-demo-app.sh +727 -0
  54. package/scripts/demo.sh +3 -0
  55. package/scripts/install-ios-shim.sh +2 -2
  56. package/shims/ios/ZMRShim.swift +10 -0
  57. package/shims/ios/ZMRShimUITestCase.swift +49 -1
  58. package/shims/ios/protocol.md +1 -0
  59. package/src/cli_import.zig +31 -15
  60. package/src/cli_trace.zig +38 -16
  61. package/src/cli_validate.zig +12 -6
  62. package/src/ios.zig +44 -11
  63. package/src/ios_shim.zig +36 -2
  64. package/src/main.zig +6 -0
  65. package/src/version.zig +1 -1
  66. package/viewer/app.js +23 -3
@@ -0,0 +1,727 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ OUT=""
5
+ APP_NAME="ZenoExpoDemo"
6
+ APP_ID="com.example.mobiletest"
7
+ IOS_BUNDLE_ID="com.example.mobiletest"
8
+ SCHEME="zenoexpodemo"
9
+
10
+ usage() {
11
+ cat <<'USAGE'
12
+ Usage:
13
+ scripts/create-react-native-expo-demo-app.sh --out <dir> [options]
14
+
15
+ Creates a small public React Native / Expo demo app and matching ZMR workflow
16
+ scenarios. The generated app is intentionally generic and contains no private
17
+ app references. Generation does not install dependencies or require network
18
+ access.
19
+
20
+ Options:
21
+ --out <dir> Output app repository directory. Required.
22
+ --name <name> Expo app display name. Default: ZenoExpoDemo.
23
+ --app-id <id> Android application id. Default: com.example.mobiletest.
24
+ --ios-bundle-id <id> iOS bundle id. Default: com.example.mobiletest.
25
+ --scheme <scheme> App deep-link scheme. Default: zenoexpodemo.
26
+ -h, --help Show this help.
27
+
28
+ After generation:
29
+ cd <dir>
30
+ bun install
31
+ bunx expo start
32
+ zmr run .zmr/react-native-expo-android-workflow.json --device emulator-5554 --app-id com.example.mobiletest --trace-dir traces/zmr-rn-expo-android
33
+ zmr run .zmr/react-native-expo-ios-workflow.json --platform ios --device booted --app-id com.example.mobiletest --trace-dir traces/zmr-rn-expo-ios
34
+ USAGE
35
+ }
36
+
37
+ die() {
38
+ echo "error: $*" >&2
39
+ exit 2
40
+ }
41
+
42
+ require_value() {
43
+ local flag="$1"
44
+ local value="${2-}"
45
+ if [[ -z "$value" || "$value" == --* ]]; then
46
+ die "$flag requires a value"
47
+ fi
48
+ printf '%s\n' "$value"
49
+ }
50
+
51
+ write_file() {
52
+ local path="$1"
53
+ local content="$2"
54
+ mkdir -p "$(dirname "$path")"
55
+ printf '%s\n' "$content" > "$path"
56
+ }
57
+
58
+ while [[ $# -gt 0 ]]; do
59
+ case "$1" in
60
+ --out)
61
+ OUT="$(require_value "$1" "${2-}")"
62
+ shift 2
63
+ ;;
64
+ --name)
65
+ APP_NAME="$(require_value "$1" "${2-}")"
66
+ shift 2
67
+ ;;
68
+ --app-id)
69
+ APP_ID="$(require_value "$1" "${2-}")"
70
+ shift 2
71
+ ;;
72
+ --ios-bundle-id)
73
+ IOS_BUNDLE_ID="$(require_value "$1" "${2-}")"
74
+ shift 2
75
+ ;;
76
+ --scheme)
77
+ SCHEME="$(require_value "$1" "${2-}")"
78
+ shift 2
79
+ ;;
80
+ -h|--help)
81
+ usage
82
+ exit 0
83
+ ;;
84
+ *)
85
+ die "unknown argument: $1"
86
+ ;;
87
+ esac
88
+ done
89
+
90
+ [[ -n "$OUT" ]] || die "--out is required"
91
+ [[ "$APP_NAME" =~ ^[A-Za-z][A-Za-z0-9_-]*$ ]] || die "--name must start with a letter and contain only letters, numbers, underscores, or hyphens"
92
+ [[ "$APP_ID" =~ ^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$ ]] || die "--app-id must be a Java-style package id"
93
+ [[ "$IOS_BUNDLE_ID" =~ ^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_-]*)+$ ]] || die "--ios-bundle-id must be a bundle identifier"
94
+ [[ "$SCHEME" =~ ^[a-z][a-z0-9+.-]*$ ]] || die "--scheme must be a lower-case URL scheme"
95
+
96
+ if [[ "$OUT" != /* ]]; then
97
+ OUT="$(pwd -P)/$OUT"
98
+ fi
99
+
100
+ PACKAGE_NAME="$(printf '%s' "$APP_NAME" | tr '[:upper:]' '[:lower:]' | tr '_' '-' | tr -cd 'a-z0-9-')"
101
+ [[ -n "$PACKAGE_NAME" ]] || PACKAGE_NAME="zeno-expo-demo"
102
+
103
+ mkdir -p "$OUT/.zmr"
104
+
105
+ write_file "$OUT/package.json" "$(cat <<EOF
106
+ {
107
+ "private": true,
108
+ "name": "$PACKAGE_NAME",
109
+ "version": "0.0.0",
110
+ "main": "index.js",
111
+ "scripts": {
112
+ "start": "expo start",
113
+ "android": "expo run:android",
114
+ "ios": "expo run:ios",
115
+ "zmr:android": "zmr run .zmr/react-native-expo-android-workflow.json --device emulator-5554 --app-id $APP_ID --trace-dir traces/zmr-rn-expo-android",
116
+ "zmr:ios": "zmr run .zmr/react-native-expo-ios-workflow.json --platform ios --device booted --app-id $IOS_BUNDLE_ID --trace-dir traces/zmr-rn-expo-ios"
117
+ },
118
+ "dependencies": {
119
+ "expo": "~55.0.0",
120
+ "expo-dev-client": "~55.0.0",
121
+ "react": "^19.2.0",
122
+ "react-native": "~0.83.0"
123
+ },
124
+ "devDependencies": {
125
+ "@types/react": "^19.1.1",
126
+ "typescript": "^5.9.0"
127
+ }
128
+ }
129
+ EOF
130
+ )"
131
+
132
+ write_file "$OUT/app.json" "$(cat <<EOF
133
+ {
134
+ "expo": {
135
+ "name": "$APP_NAME",
136
+ "slug": "$PACKAGE_NAME",
137
+ "scheme": "$SCHEME",
138
+ "version": "0.0.0",
139
+ "orientation": "portrait",
140
+ "userInterfaceStyle": "automatic",
141
+ "ios": {
142
+ "bundleIdentifier": "$IOS_BUNDLE_ID"
143
+ },
144
+ "android": {
145
+ "package": "$APP_ID"
146
+ }
147
+ }
148
+ }
149
+ EOF
150
+ )"
151
+
152
+ write_file "$OUT/index.js" "$(cat <<'EOF'
153
+ import { registerRootComponent } from "expo";
154
+
155
+ import App from "./App";
156
+
157
+ registerRootComponent(App);
158
+ EOF
159
+ )"
160
+
161
+ write_file "$OUT/tsconfig.json" "$(cat <<'EOF'
162
+ {
163
+ "extends": "expo/tsconfig.base",
164
+ "compilerOptions": {
165
+ "strict": true
166
+ }
167
+ }
168
+ EOF
169
+ )"
170
+
171
+ write_file "$OUT/App.tsx" "$(cat <<EOF
172
+ import React, { useEffect, useMemo, useState } from "react";
173
+ import {
174
+ Linking,
175
+ Pressable,
176
+ SafeAreaView,
177
+ ScrollView,
178
+ StyleSheet,
179
+ Text,
180
+ TextInput,
181
+ View,
182
+ } from "react-native";
183
+
184
+ type Screen = "welcome" | "profile" | "catalog" | "detail" | "review";
185
+
186
+ type CatalogItem = {
187
+ id: string;
188
+ title: string;
189
+ subtitle: string;
190
+ };
191
+
192
+ const catalogItems: CatalogItem[] = [
193
+ { id: "trail_lamp", title: "Trail Lamp", subtitle: "Compact campsite light" },
194
+ { id: "river_bottle", title: "River Bottle", subtitle: "Insulated hydration bottle" },
195
+ { id: "summit_shell", title: "Summit Shell", subtitle: "Lightweight rain layer" },
196
+ { id: "basecamp_roll", title: "Basecamp Roll", subtitle: "Modular storage roll" },
197
+ { id: "maple_organizer", title: "Maple Organizer", subtitle: "Cable and tool pouch" },
198
+ { id: "canyon_sling", title: "Canyon Sling", subtitle: "Cross-body field bag" },
199
+ { id: "harbor_tote", title: "Harbor Tote", subtitle: "Daily carry tote" },
200
+ { id: "north_ridge_pack", title: "North Ridge Pack", subtitle: "Weatherproof day pack" },
201
+ { id: "studio_stand", title: "Studio Stand", subtitle: "Fold-flat work stand" },
202
+ ];
203
+
204
+ const defaultItem = catalogItems.find((item) => item.id === "north_ridge_pack") ?? catalogItems[0];
205
+
206
+ export default function App() {
207
+ const [screen, setScreen] = useState<Screen>("welcome");
208
+ const [status, setStatus] = useState("Ready");
209
+ const [profileName, setProfileName] = useState("");
210
+ const [profileEmail, setProfileEmail] = useState("");
211
+ const [selectedItem, setSelectedItem] = useState<CatalogItem>(defaultItem);
212
+
213
+ useEffect(() => {
214
+ const openBenchmark = (url: string | null) => {
215
+ if (!url || !url.startsWith("$SCHEME://")) return;
216
+ setScreen("welcome");
217
+ setStatus("Deep link opened");
218
+ };
219
+
220
+ Linking.getInitialURL().then(openBenchmark).catch(() => undefined);
221
+ const subscription = Linking.addEventListener("url", (event) => openBenchmark(event.url));
222
+ return () => subscription.remove();
223
+ }, []);
224
+
225
+ const savedStatus = useMemo(() => \`Saved \${selectedItem.title}\`, [selectedItem.title]);
226
+
227
+ return (
228
+ <SafeAreaView style={styles.safeArea}>
229
+ <View style={styles.shell}>
230
+ {screen === "welcome" ? (
231
+ <View style={styles.centered}>
232
+ <Text style={styles.title} testID="demo_title" accessibilityLabel="demo_title">
233
+ Zeno Expo Demo
234
+ </Text>
235
+ <Text style={styles.copy}>A generated React Native and Expo workflow surface.</Text>
236
+ <PrimaryButton
237
+ label="Continue"
238
+ testID="continue_button"
239
+ onPress={() => {
240
+ setStatus("Continue tapped");
241
+ setScreen("profile");
242
+ }}
243
+ />
244
+ </View>
245
+ ) : null}
246
+
247
+ {screen === "profile" ? (
248
+ <View style={styles.form}>
249
+ <Text style={styles.heading} testID="profile_title" accessibilityLabel="profile_title">
250
+ Profile
251
+ </Text>
252
+ <TextInput
253
+ value={profileName}
254
+ onChangeText={setProfileName}
255
+ placeholder="Name"
256
+ autoCapitalize="none"
257
+ autoCorrect={false}
258
+ style={styles.input}
259
+ testID="profile_name_input"
260
+ accessibilityLabel="profile_name_input"
261
+ />
262
+ <TextInput
263
+ value={profileEmail}
264
+ onChangeText={setProfileEmail}
265
+ placeholder="Email"
266
+ autoCapitalize="none"
267
+ autoCorrect={false}
268
+ keyboardType="email-address"
269
+ style={styles.input}
270
+ testID="profile_email_input"
271
+ accessibilityLabel="profile_email_input"
272
+ />
273
+ <PrimaryButton
274
+ label="Save profile"
275
+ testID="save_profile_button"
276
+ onPress={() => {
277
+ setStatus("Profile saved");
278
+ setScreen("catalog");
279
+ }}
280
+ />
281
+ </View>
282
+ ) : null}
283
+
284
+ {screen === "catalog" ? (
285
+ <View style={styles.flex}>
286
+ <Text style={styles.heading} testID="catalog_title" accessibilityLabel="catalog_title">
287
+ Catalog
288
+ </Text>
289
+ <ScrollView
290
+ style={styles.list}
291
+ contentContainerStyle={styles.listContent}
292
+ testID="catalog_list"
293
+ accessibilityLabel="catalog_list"
294
+ >
295
+ {catalogItems.map((item) => (
296
+ <Pressable
297
+ key={item.id}
298
+ testID={\`catalog_item_\${item.id}\`}
299
+ accessibilityLabel={\`catalog_item_\${item.id}\`}
300
+ accessibilityRole="button"
301
+ style={styles.row}
302
+ onPress={() => {
303
+ setSelectedItem(item);
304
+ setStatus(\`Selected \${item.title}\`);
305
+ setScreen("detail");
306
+ }}
307
+ >
308
+ <Text style={styles.rowTitle}>{item.title}</Text>
309
+ <Text style={styles.rowCopy}>{item.subtitle}</Text>
310
+ </Pressable>
311
+ ))}
312
+ </ScrollView>
313
+ </View>
314
+ ) : null}
315
+
316
+ {screen === "detail" ? (
317
+ <View style={styles.form}>
318
+ <Text style={styles.heading} testID="detail_title" accessibilityLabel="detail_title">
319
+ {selectedItem.title}
320
+ </Text>
321
+ <Text style={styles.copy} testID="detail_subtitle" accessibilityLabel="detail_subtitle">
322
+ {selectedItem.subtitle}
323
+ </Text>
324
+ <PrimaryButton
325
+ label="Save item"
326
+ testID="detail_save_button"
327
+ onPress={() => {
328
+ setStatus(savedStatus);
329
+ setScreen("review");
330
+ }}
331
+ />
332
+ </View>
333
+ ) : null}
334
+
335
+ {screen === "review" ? (
336
+ <View style={styles.form}>
337
+ <Text style={styles.heading} testID="review_title" accessibilityLabel="review_title">
338
+ Review
339
+ </Text>
340
+ <Text style={styles.copy} testID="review_summary" accessibilityLabel="review_summary">
341
+ {profileName || "Riley"} saved {selectedItem.title}
342
+ </Text>
343
+ <PrimaryButton
344
+ label="Complete review"
345
+ testID="review_button"
346
+ onPress={() => setStatus("Workflow complete")}
347
+ />
348
+ </View>
349
+ ) : null}
350
+
351
+ <Text style={styles.status} testID="workflow_status" accessibilityLabel="workflow_status">
352
+ {status}
353
+ </Text>
354
+ </View>
355
+ </SafeAreaView>
356
+ );
357
+ }
358
+
359
+ function PrimaryButton({
360
+ label,
361
+ testID,
362
+ onPress,
363
+ }: {
364
+ label: string;
365
+ testID: string;
366
+ onPress: () => void;
367
+ }) {
368
+ return (
369
+ <Pressable
370
+ accessibilityRole="button"
371
+ accessibilityLabel={testID}
372
+ testID={testID}
373
+ onPress={onPress}
374
+ style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}
375
+ >
376
+ <Text style={styles.buttonText}>{label}</Text>
377
+ </Pressable>
378
+ );
379
+ }
380
+
381
+ const styles = StyleSheet.create({
382
+ safeArea: {
383
+ flex: 1,
384
+ backgroundColor: "#F8FAFC",
385
+ },
386
+ shell: {
387
+ flex: 1,
388
+ padding: 20,
389
+ gap: 16,
390
+ },
391
+ flex: {
392
+ flex: 1,
393
+ },
394
+ centered: {
395
+ flex: 1,
396
+ justifyContent: "center",
397
+ gap: 18,
398
+ },
399
+ form: {
400
+ flex: 1,
401
+ justifyContent: "center",
402
+ gap: 14,
403
+ },
404
+ title: {
405
+ color: "#111827",
406
+ fontSize: 34,
407
+ fontWeight: "700",
408
+ },
409
+ heading: {
410
+ color: "#111827",
411
+ fontSize: 28,
412
+ fontWeight: "700",
413
+ },
414
+ copy: {
415
+ color: "#475569",
416
+ fontSize: 16,
417
+ lineHeight: 22,
418
+ },
419
+ input: {
420
+ minHeight: 52,
421
+ borderWidth: 1,
422
+ borderColor: "#CBD5E1",
423
+ borderRadius: 8,
424
+ paddingHorizontal: 14,
425
+ backgroundColor: "#FFFFFF",
426
+ color: "#111827",
427
+ fontSize: 16,
428
+ },
429
+ button: {
430
+ minHeight: 52,
431
+ alignItems: "center",
432
+ justifyContent: "center",
433
+ borderRadius: 8,
434
+ backgroundColor: "#2563EB",
435
+ paddingHorizontal: 18,
436
+ },
437
+ buttonPressed: {
438
+ backgroundColor: "#1D4ED8",
439
+ },
440
+ buttonText: {
441
+ color: "#FFFFFF",
442
+ fontSize: 16,
443
+ fontWeight: "700",
444
+ },
445
+ list: {
446
+ flex: 1,
447
+ marginTop: 14,
448
+ },
449
+ listContent: {
450
+ gap: 10,
451
+ paddingBottom: 24,
452
+ },
453
+ row: {
454
+ borderWidth: 1,
455
+ borderColor: "#CBD5E1",
456
+ borderRadius: 8,
457
+ backgroundColor: "#FFFFFF",
458
+ padding: 16,
459
+ gap: 6,
460
+ },
461
+ rowTitle: {
462
+ color: "#111827",
463
+ fontSize: 18,
464
+ fontWeight: "700",
465
+ },
466
+ rowCopy: {
467
+ color: "#64748B",
468
+ fontSize: 14,
469
+ },
470
+ status: {
471
+ color: "#334155",
472
+ fontSize: 14,
473
+ textAlign: "center",
474
+ },
475
+ });
476
+ EOF
477
+ )"
478
+
479
+ write_file "$OUT/.zmr/react-native-expo-workflow.json" "$(cat <<EOF
480
+ {
481
+ "name": "ZMR React Native Expo workflow demo",
482
+ "appId": "$APP_ID",
483
+ "steps": [
484
+ { "action": "openLink", "url": "$SCHEME://benchmark" },
485
+ {
486
+ "action": "waitVisible",
487
+ "selector": { "text": "Zeno Expo Demo" },
488
+ "timeoutMs": 30000
489
+ },
490
+ {
491
+ "action": "tap",
492
+ "selector": { "contentDesc": "continue_button" }
493
+ },
494
+ {
495
+ "action": "waitVisible",
496
+ "selector": { "text": "Profile" },
497
+ "timeoutMs": 10000
498
+ },
499
+ {
500
+ "action": "typeText",
501
+ "selector": { "contentDesc": "profile_name_input" },
502
+ "text": "Riley"
503
+ },
504
+ {
505
+ "action": "typeText",
506
+ "selector": { "contentDesc": "profile_email_input" },
507
+ "text": "riley@example.test"
508
+ },
509
+ { "action": "hideKeyboard" },
510
+ {
511
+ "action": "tap",
512
+ "selector": { "contentDesc": "save_profile_button" }
513
+ },
514
+ {
515
+ "action": "waitVisible",
516
+ "selector": { "text": "Catalog" },
517
+ "timeoutMs": 10000
518
+ },
519
+ {
520
+ "action": "scrollUntilVisible",
521
+ "selector": { "contentDesc": "catalog_item_north_ridge_pack" },
522
+ "direction": "down",
523
+ "timeoutMs": 10000
524
+ },
525
+ {
526
+ "action": "tap",
527
+ "selector": { "contentDesc": "catalog_item_north_ridge_pack" }
528
+ },
529
+ {
530
+ "action": "waitVisible",
531
+ "selector": { "text": "North Ridge Pack" },
532
+ "timeoutMs": 10000
533
+ },
534
+ {
535
+ "action": "tap",
536
+ "selector": { "contentDesc": "detail_save_button" }
537
+ },
538
+ {
539
+ "action": "waitVisible",
540
+ "selector": { "text": "Saved North Ridge Pack" },
541
+ "timeoutMs": 10000
542
+ },
543
+ {
544
+ "action": "tap",
545
+ "selector": { "contentDesc": "review_button" }
546
+ },
547
+ {
548
+ "action": "assertVisible",
549
+ "selector": { "text": "Workflow complete" },
550
+ "timeoutMs": 10000
551
+ },
552
+ { "action": "snapshot" }
553
+ ]
554
+ }
555
+ EOF
556
+ )"
557
+
558
+ write_file "$OUT/.zmr/react-native-expo-android-workflow.json" "$(cat <<EOF
559
+ {
560
+ "name": "ZMR React Native Expo Android workflow demo",
561
+ "appId": "$APP_ID",
562
+ "steps": [
563
+ { "action": "openLink", "url": "$SCHEME://benchmark" },
564
+ {
565
+ "action": "waitVisible",
566
+ "selector": { "text": "Zeno Expo Demo" },
567
+ "timeoutMs": 30000
568
+ },
569
+ {
570
+ "action": "tap",
571
+ "selector": { "resourceId": "$APP_ID:id/continue_button" }
572
+ },
573
+ {
574
+ "action": "waitVisible",
575
+ "selector": { "text": "Profile" },
576
+ "timeoutMs": 10000
577
+ },
578
+ {
579
+ "action": "typeText",
580
+ "selector": { "resourceId": "$APP_ID:id/profile_name_input" },
581
+ "text": "Riley"
582
+ },
583
+ {
584
+ "action": "typeText",
585
+ "selector": { "resourceId": "$APP_ID:id/profile_email_input" },
586
+ "text": "riley@example.test"
587
+ },
588
+ { "action": "hideKeyboard" },
589
+ {
590
+ "action": "tap",
591
+ "selector": { "resourceId": "$APP_ID:id/save_profile_button" }
592
+ },
593
+ {
594
+ "action": "waitVisible",
595
+ "selector": { "text": "Catalog" },
596
+ "timeoutMs": 10000
597
+ },
598
+ {
599
+ "action": "scrollUntilVisible",
600
+ "selector": { "resourceId": "$APP_ID:id/catalog_item_north_ridge_pack" },
601
+ "direction": "down",
602
+ "timeoutMs": 10000
603
+ },
604
+ {
605
+ "action": "tap",
606
+ "selector": { "resourceId": "$APP_ID:id/catalog_item_north_ridge_pack" }
607
+ },
608
+ {
609
+ "action": "waitVisible",
610
+ "selector": { "text": "North Ridge Pack" },
611
+ "timeoutMs": 10000
612
+ },
613
+ {
614
+ "action": "tap",
615
+ "selector": { "resourceId": "$APP_ID:id/detail_save_button" }
616
+ },
617
+ {
618
+ "action": "waitVisible",
619
+ "selector": { "text": "Saved North Ridge Pack" },
620
+ "timeoutMs": 10000
621
+ },
622
+ {
623
+ "action": "tap",
624
+ "selector": { "resourceId": "$APP_ID:id/review_button" }
625
+ },
626
+ {
627
+ "action": "assertVisible",
628
+ "selector": {
629
+ "resourceId": "$APP_ID:id/workflow_status",
630
+ "text": "Workflow complete"
631
+ },
632
+ "timeoutMs": 10000
633
+ },
634
+ { "action": "snapshot" }
635
+ ]
636
+ }
637
+ EOF
638
+ )"
639
+
640
+ write_file "$OUT/.zmr/react-native-expo-ios-workflow.json" "$(cat <<EOF
641
+ {
642
+ "name": "ZMR React Native Expo iOS workflow demo",
643
+ "appId": "$IOS_BUNDLE_ID",
644
+ "steps": [
645
+ { "action": "openLink", "url": "$SCHEME://benchmark" },
646
+ {
647
+ "action": "waitVisible",
648
+ "selector": { "text": "Zeno Expo Demo" },
649
+ "timeoutMs": 30000
650
+ },
651
+ {
652
+ "action": "tap",
653
+ "selector": { "resourceId": "continue_button" }
654
+ },
655
+ {
656
+ "action": "waitVisible",
657
+ "selector": { "text": "Profile" },
658
+ "timeoutMs": 10000
659
+ },
660
+ {
661
+ "action": "typeText",
662
+ "selector": { "resourceId": "profile_name_input" },
663
+ "text": "Riley"
664
+ },
665
+ {
666
+ "action": "typeText",
667
+ "selector": { "resourceId": "profile_email_input" },
668
+ "text": "riley@example.test"
669
+ },
670
+ { "action": "hideKeyboard" },
671
+ {
672
+ "action": "tap",
673
+ "selector": { "resourceId": "save_profile_button" }
674
+ },
675
+ {
676
+ "action": "waitVisible",
677
+ "selector": { "text": "Catalog" },
678
+ "timeoutMs": 10000
679
+ },
680
+ {
681
+ "action": "scrollUntilVisible",
682
+ "selector": { "resourceId": "catalog_item_north_ridge_pack" },
683
+ "direction": "down",
684
+ "timeoutMs": 10000
685
+ },
686
+ {
687
+ "action": "tap",
688
+ "selector": { "resourceId": "catalog_item_north_ridge_pack" }
689
+ },
690
+ {
691
+ "action": "waitVisible",
692
+ "selector": { "text": "North Ridge Pack" },
693
+ "timeoutMs": 10000
694
+ },
695
+ {
696
+ "action": "tap",
697
+ "selector": { "resourceId": "detail_save_button" }
698
+ },
699
+ {
700
+ "action": "waitVisible",
701
+ "selector": { "text": "Saved North Ridge Pack" },
702
+ "timeoutMs": 10000
703
+ },
704
+ {
705
+ "action": "tap",
706
+ "selector": { "resourceId": "review_button" }
707
+ },
708
+ {
709
+ "action": "assertVisible",
710
+ "selector": {
711
+ "resourceId": "workflow_status",
712
+ "text": "Workflow complete"
713
+ },
714
+ "timeoutMs": 10000
715
+ },
716
+ { "action": "snapshot" }
717
+ ]
718
+ }
719
+ EOF
720
+ )"
721
+
722
+ echo "React Native / Expo demo app: $OUT"
723
+ echo "Deep link scheme: $SCHEME"
724
+ echo "ZMR scenarios:"
725
+ echo " $OUT/.zmr/react-native-expo-workflow.json"
726
+ echo " $OUT/.zmr/react-native-expo-android-workflow.json"
727
+ echo " $OUT/.zmr/react-native-expo-ios-workflow.json"