zeno-mobile-runner 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/FEATURES.md +1 -1
  3. package/README.md +167 -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-shim-workflow.json +79 -0
  40. package/examples/react-native-expo-workflow.json +75 -0
  41. package/package.json +6 -1
  42. package/prebuilds/darwin-arm64/zmr +0 -0
  43. package/prebuilds/darwin-x64/zmr +0 -0
  44. package/prebuilds/linux-arm64/zmr +0 -0
  45. package/prebuilds/linux-x64/zmr +0 -0
  46. package/scripts/benchmark-lab.py +253 -0
  47. package/scripts/create-android-demo-app.sh +324 -29
  48. package/scripts/create-ios-demo-app.sh +174 -7
  49. package/scripts/create-react-native-expo-demo-app.sh +727 -0
  50. package/scripts/demo.sh +3 -0
  51. package/scripts/install-ios-shim.sh +2 -2
  52. package/shims/ios/ZMRShim.swift +10 -0
  53. package/shims/ios/ZMRShimUITestCase.swift +42 -0
  54. package/shims/ios/protocol.md +1 -0
  55. package/src/cli_import.zig +31 -15
  56. package/src/cli_trace.zig +38 -16
  57. package/src/cli_validate.zig +12 -6
  58. package/src/ios.zig +49 -12
  59. package/src/ios_shim.zig +36 -2
  60. package/src/main.zig +3 -0
  61. package/src/version.zig +1 -1
  62. package/viewer/app.js +23 -3
@@ -31,6 +31,7 @@ Options:
31
31
  After generation:
32
32
  adb install -r <dir>/build/app-debug.apk
33
33
  zmr run <dir>/.zmr/android-smoke.json --device emulator-5554 --app-id com.example.mobiletest --trace-dir <dir>/traces/android-demo
34
+ zmr run <dir>/.zmr/android-workflow.json --device emulator-5554 --app-id com.example.mobiletest --trace-dir <dir>/traces/android-workflow
34
35
  USAGE
35
36
  }
36
37
 
@@ -199,6 +200,29 @@ write_file "$RES_DIR/values/ids.xml" "$(cat <<'EOF'
199
200
  <item name="continue_button" type="id" />
200
201
  <item name="demo_input" type="id" />
201
202
  <item name="demo_status" type="id" />
203
+ <item name="profile_title" type="id" />
204
+ <item name="profile_name_input" type="id" />
205
+ <item name="profile_email_input" type="id" />
206
+ <item name="save_profile_button" type="id" />
207
+ <item name="catalog_title" type="id" />
208
+ <item name="catalog_list" type="id" />
209
+ <item name="catalog_item_trail_lamp" type="id" />
210
+ <item name="catalog_item_river_bottle" type="id" />
211
+ <item name="catalog_item_summit_shell" type="id" />
212
+ <item name="catalog_item_basecamp_roll" type="id" />
213
+ <item name="catalog_item_maple_organizer" type="id" />
214
+ <item name="catalog_item_canyon_sling" type="id" />
215
+ <item name="catalog_item_harbor_tote" type="id" />
216
+ <item name="catalog_item_north_ridge_pack" type="id" />
217
+ <item name="catalog_item_studio_stand" type="id" />
218
+ <item name="detail_title" type="id" />
219
+ <item name="detail_subtitle" type="id" />
220
+ <item name="detail_save_button" type="id" />
221
+ <item name="review_button" type="id" />
222
+ <item name="review_title" type="id" />
223
+ <item name="review_complete" type="id" />
224
+ <item name="review_item" type="id" />
225
+ <item name="workflow_status" type="id" />
202
226
  </resources>
203
227
  EOF
204
228
  )"
@@ -217,66 +241,252 @@ import android.content.Context;
217
241
  import android.widget.Button;
218
242
  import android.widget.EditText;
219
243
  import android.widget.LinearLayout;
244
+ import android.widget.ScrollView;
220
245
  import android.widget.TextView;
221
246
 
222
247
  public class MainActivity extends Activity {
223
- private TextView status;
224
- private EditText input;
248
+ private LinearLayout root;
249
+ private TextView demoStatus;
250
+ private TextView workflowStatus;
251
+ private String currentStatus = "Ready";
252
+ private CatalogItem selectedItem = new CatalogItem("north_ridge_pack", "North Ridge Pack", "Weatherproof day pack", R.id.catalog_item_north_ridge_pack);
253
+
254
+ private static class CatalogItem {
255
+ final String key;
256
+ final String title;
257
+ final String subtitle;
258
+ final int viewId;
259
+
260
+ CatalogItem(String key, String title, String subtitle, int viewId) {
261
+ this.key = key;
262
+ this.title = title;
263
+ this.subtitle = subtitle;
264
+ this.viewId = viewId;
265
+ }
266
+ }
267
+
268
+ private final CatalogItem[] catalogItems = new CatalogItem[] {
269
+ new CatalogItem("trail_lamp", "Trail Lamp", "Compact campsite light", R.id.catalog_item_trail_lamp),
270
+ new CatalogItem("river_bottle", "River Bottle", "Insulated hydration bottle", R.id.catalog_item_river_bottle),
271
+ new CatalogItem("north_ridge_pack", "North Ridge Pack", "Weatherproof day pack", R.id.catalog_item_north_ridge_pack),
272
+ new CatalogItem("summit_shell", "Summit Shell", "Lightweight rain layer", R.id.catalog_item_summit_shell),
273
+ new CatalogItem("basecamp_roll", "Basecamp Roll", "Modular storage roll", R.id.catalog_item_basecamp_roll),
274
+ new CatalogItem("maple_organizer", "Maple Organizer", "Cable and tool pouch", R.id.catalog_item_maple_organizer),
275
+ new CatalogItem("canyon_sling", "Canyon Sling", "Cross-body field bag", R.id.catalog_item_canyon_sling),
276
+ new CatalogItem("harbor_tote", "Harbor Tote", "Daily carry tote", R.id.catalog_item_harbor_tote),
277
+ new CatalogItem("studio_stand", "Studio Stand", "Fold-flat work stand", R.id.catalog_item_studio_stand)
278
+ };
225
279
 
226
280
  @Override
227
281
  protected void onCreate(Bundle savedInstanceState) {
228
282
  super.onCreate(savedInstanceState);
229
283
 
230
- LinearLayout layout = new LinearLayout(this);
231
- layout.setOrientation(LinearLayout.VERTICAL);
232
- layout.setGravity(Gravity.CENTER_HORIZONTAL);
284
+ root = new LinearLayout(this);
285
+ setContentView(root);
286
+ showWelcome();
287
+
288
+ Uri data = getIntent().getData();
289
+ if (data != null) {
290
+ setStatus("Deep link opened");
291
+ }
292
+ }
293
+
294
+ private void resetRoot() {
295
+ root.removeAllViews();
296
+ root.setOrientation(LinearLayout.VERTICAL);
297
+ root.setGravity(Gravity.CENTER_HORIZONTAL);
233
298
  int padding = dp(24);
234
- layout.setPadding(padding, padding, padding, padding);
299
+ root.setPadding(padding, padding, padding, padding);
300
+ }
235
301
 
302
+ private TextView title(String text, int id) {
236
303
  TextView title = new TextView(this);
237
- title.setId(R.id.demo_title);
238
- title.setText("ZMR Android Demo");
304
+ title.setId(id);
305
+ title.setText(text);
239
306
  title.setTextSize(24);
240
307
  title.setTextColor(Color.rgb(17, 24, 39));
241
308
  title.setGravity(Gravity.CENTER);
242
- layout.addView(title, new LinearLayout.LayoutParams(-1, -2));
309
+ return title;
310
+ }
311
+
312
+ private EditText input(String hint, int id) {
313
+ EditText input = new EditText(this);
314
+ input.setId(id);
315
+ input.setHint(hint);
316
+ input.setSingleLine(true);
317
+ return input;
318
+ }
319
+
320
+ private Button button(String text, int id) {
321
+ Button button = new Button(this);
322
+ button.setId(id);
323
+ button.setText(text);
324
+ button.setAllCaps(false);
325
+ return button;
326
+ }
327
+
328
+ private void addStatusViews() {
329
+ demoStatus = new TextView(this);
330
+ demoStatus.setId(R.id.demo_status);
331
+ demoStatus.setText(currentStatus);
332
+ demoStatus.setTextSize(16);
333
+ demoStatus.setGravity(Gravity.CENTER);
334
+ root.addView(demoStatus, new LinearLayout.LayoutParams(-1, -2));
335
+
336
+ workflowStatus = new TextView(this);
337
+ workflowStatus.setId(R.id.workflow_status);
338
+ workflowStatus.setText(currentStatus);
339
+ workflowStatus.setTextSize(16);
340
+ workflowStatus.setGravity(Gravity.CENTER);
341
+ root.addView(workflowStatus, new LinearLayout.LayoutParams(-1, -2));
342
+ }
343
+
344
+ private void setStatus(String value) {
345
+ currentStatus = value;
346
+ if (demoStatus != null) {
347
+ demoStatus.setText(value);
348
+ }
349
+ if (workflowStatus != null) {
350
+ workflowStatus.setText(value);
351
+ }
352
+ }
353
+
354
+ private void showWelcome() {
355
+ resetRoot();
356
+ root.addView(title("ZMR Android Demo", R.id.demo_title), new LinearLayout.LayoutParams(-1, -2));
243
357
 
244
358
  Button button = new Button(this);
245
359
  button.setId(R.id.continue_button);
246
360
  button.setText("Continue");
247
- layout.addView(button, new LinearLayout.LayoutParams(-1, dp(56)));
361
+ root.addView(button, new LinearLayout.LayoutParams(-1, dp(56)));
362
+ addStatusViews();
248
363
 
249
- input = new EditText(this);
250
- input.setId(R.id.demo_input);
251
- input.setHint("Type here");
252
- input.setSingleLine(true);
253
- layout.addView(input, new LinearLayout.LayoutParams(-1, dp(56)));
364
+ button.setOnClickListener(new View.OnClickListener() {
365
+ @Override
366
+ public void onClick(View view) {
367
+ showProfile("Continue tapped");
368
+ }
369
+ });
370
+ }
254
371
 
255
- status = new TextView(this);
256
- status.setId(R.id.demo_status);
257
- status.setText("Ready");
258
- status.setTextSize(18);
259
- status.setGravity(Gravity.CENTER);
260
- layout.addView(status, new LinearLayout.LayoutParams(-1, -2));
372
+ private void showProfile(String statusText) {
373
+ currentStatus = statusText;
374
+ resetRoot();
375
+ root.addView(title("Profile", R.id.profile_title), new LinearLayout.LayoutParams(-1, -2));
261
376
 
262
- button.setOnClickListener(new View.OnClickListener() {
377
+ final EditText quickInput = input("Type here", R.id.demo_input);
378
+ root.addView(quickInput, new LinearLayout.LayoutParams(-1, dp(56)));
379
+
380
+ EditText profileName = input("Name", R.id.profile_name_input);
381
+ root.addView(profileName, new LinearLayout.LayoutParams(-1, dp(56)));
382
+
383
+ EditText profileEmail = input("Email", R.id.profile_email_input);
384
+ root.addView(profileEmail, new LinearLayout.LayoutParams(-1, dp(56)));
385
+
386
+ Button save = button("Save profile", R.id.save_profile_button);
387
+ root.addView(save, new LinearLayout.LayoutParams(-1, dp(56)));
388
+ addStatusViews();
389
+
390
+ quickInput.requestFocus();
391
+ InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
392
+ if (imm != null) {
393
+ imm.showSoftInput(quickInput, InputMethodManager.SHOW_IMPLICIT);
394
+ }
395
+
396
+ save.setOnClickListener(new View.OnClickListener() {
263
397
  @Override
264
398
  public void onClick(View view) {
265
- status.setText("Continue tapped");
266
- input.requestFocus();
267
399
  InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
268
400
  if (imm != null) {
269
- imm.showSoftInput(input, InputMethodManager.SHOW_IMPLICIT);
401
+ imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
270
402
  }
403
+ showCatalog("Profile saved");
271
404
  }
272
405
  });
406
+ }
273
407
 
274
- Uri data = getIntent().getData();
275
- if (data != null) {
276
- status.setText("Deep link opened");
408
+ private void showCatalog(String statusText) {
409
+ currentStatus = statusText;
410
+ resetRoot();
411
+ root.addView(title("Catalog", R.id.catalog_title), new LinearLayout.LayoutParams(-1, -2));
412
+
413
+ ScrollView scrollView = new ScrollView(this);
414
+ scrollView.setId(R.id.catalog_list);
415
+ LinearLayout list = new LinearLayout(this);
416
+ list.setOrientation(LinearLayout.VERTICAL);
417
+ scrollView.addView(list, new ScrollView.LayoutParams(-1, -2));
418
+
419
+ for (final CatalogItem item : catalogItems) {
420
+ Button itemButton = button(item.title, item.viewId);
421
+ itemButton.setContentDescription(item.subtitle);
422
+ list.addView(itemButton, new LinearLayout.LayoutParams(-1, dp(56)));
423
+ itemButton.setOnClickListener(new View.OnClickListener() {
424
+ @Override
425
+ public void onClick(View view) {
426
+ selectedItem = item;
427
+ showDetail("Selected " + item.title);
428
+ }
429
+ });
277
430
  }
278
431
 
279
- setContentView(layout);
432
+ root.addView(scrollView, new LinearLayout.LayoutParams(-1, 0, 1));
433
+ addStatusViews();
434
+ }
435
+
436
+ private void showDetail(String statusText) {
437
+ currentStatus = statusText;
438
+ resetRoot();
439
+ root.addView(title(selectedItem.title, R.id.detail_title), new LinearLayout.LayoutParams(-1, -2));
440
+
441
+ TextView subtitle = new TextView(this);
442
+ subtitle.setId(R.id.detail_subtitle);
443
+ subtitle.setText(selectedItem.subtitle);
444
+ subtitle.setTextSize(18);
445
+ subtitle.setGravity(Gravity.CENTER);
446
+ root.addView(subtitle, new LinearLayout.LayoutParams(-1, -2));
447
+
448
+ Button save = button("Save item", R.id.detail_save_button);
449
+ root.addView(save, new LinearLayout.LayoutParams(-1, dp(56)));
450
+
451
+ Button review = button("Review order", R.id.review_button);
452
+ root.addView(review, new LinearLayout.LayoutParams(-1, dp(56)));
453
+ addStatusViews();
454
+
455
+ save.setOnClickListener(new View.OnClickListener() {
456
+ @Override
457
+ public void onClick(View view) {
458
+ setStatus("Saved " + selectedItem.title);
459
+ }
460
+ });
461
+
462
+ review.setOnClickListener(new View.OnClickListener() {
463
+ @Override
464
+ public void onClick(View view) {
465
+ showReview();
466
+ }
467
+ });
468
+ }
469
+
470
+ private void showReview() {
471
+ currentStatus = "Workflow complete";
472
+ resetRoot();
473
+ root.addView(title("Review", R.id.review_title), new LinearLayout.LayoutParams(-1, -2));
474
+
475
+ TextView complete = new TextView(this);
476
+ complete.setId(R.id.review_complete);
477
+ complete.setText("Workflow complete");
478
+ complete.setTextSize(20);
479
+ complete.setGravity(Gravity.CENTER);
480
+ root.addView(complete, new LinearLayout.LayoutParams(-1, -2));
481
+
482
+ TextView item = new TextView(this);
483
+ item.setId(R.id.review_item);
484
+ item.setText(selectedItem.title);
485
+ item.setTextSize(18);
486
+ item.setGravity(Gravity.CENTER);
487
+ root.addView(item, new LinearLayout.LayoutParams(-1, -2));
488
+
489
+ addStatusViews();
280
490
  }
281
491
 
282
492
  private int dp(int value) {
@@ -291,6 +501,7 @@ write_file "$OUT/.zmr/android-smoke.json" "$(cat <<EOF
291
501
  "name": "ZMR Android demo smoke",
292
502
  "appId": "$APP_ID",
293
503
  "steps": [
504
+ { "action": "clearState" },
294
505
  { "action": "launch" },
295
506
  { "action": "waitVisible", "selector": { "text": "ZMR Android Demo" }, "timeoutMs": 30000 },
296
507
  { "action": "tap", "selector": { "resourceId": "$APP_ID:id/continue_button" } },
@@ -302,6 +513,90 @@ write_file "$OUT/.zmr/android-smoke.json" "$(cat <<EOF
302
513
  EOF
303
514
  )"
304
515
 
516
+ write_file "$OUT/.zmr/android-workflow.json" "$(cat <<EOF
517
+ {
518
+ "name": "ZMR Android workflow demo",
519
+ "appId": "$APP_ID",
520
+ "steps": [
521
+ { "action": "stop" },
522
+ { "action": "clearState" },
523
+ { "action": "launch" },
524
+ {
525
+ "action": "waitVisible",
526
+ "selector": { "text": "ZMR Android Demo" },
527
+ "timeoutMs": 30000
528
+ },
529
+ {
530
+ "action": "tap",
531
+ "selector": { "resourceId": "$APP_ID:id/continue_button" }
532
+ },
533
+ {
534
+ "action": "waitVisible",
535
+ "selector": { "text": "Profile" },
536
+ "timeoutMs": 10000
537
+ },
538
+ {
539
+ "action": "typeText",
540
+ "selector": { "resourceId": "$APP_ID:id/profile_name_input" },
541
+ "text": "Riley"
542
+ },
543
+ {
544
+ "action": "typeText",
545
+ "selector": { "resourceId": "$APP_ID:id/profile_email_input" },
546
+ "text": "riley@example.test"
547
+ },
548
+ { "action": "hideKeyboard" },
549
+ {
550
+ "action": "tap",
551
+ "selector": { "resourceId": "$APP_ID:id/save_profile_button" }
552
+ },
553
+ {
554
+ "action": "waitVisible",
555
+ "selector": { "text": "Catalog" },
556
+ "timeoutMs": 10000
557
+ },
558
+ {
559
+ "action": "scrollUntilVisible",
560
+ "selector": { "resourceId": "$APP_ID:id/catalog_item_north_ridge_pack" },
561
+ "direction": "down",
562
+ "timeoutMs": 10000
563
+ },
564
+ {
565
+ "action": "tap",
566
+ "selector": { "resourceId": "$APP_ID:id/catalog_item_north_ridge_pack" }
567
+ },
568
+ {
569
+ "action": "waitVisible",
570
+ "selector": { "text": "North Ridge Pack" },
571
+ "timeoutMs": 10000
572
+ },
573
+ {
574
+ "action": "tap",
575
+ "selector": { "resourceId": "$APP_ID:id/detail_save_button" }
576
+ },
577
+ {
578
+ "action": "waitVisible",
579
+ "selector": { "text": "Saved North Ridge Pack" },
580
+ "timeoutMs": 10000
581
+ },
582
+ {
583
+ "action": "tap",
584
+ "selector": { "resourceId": "$APP_ID:id/review_button" }
585
+ },
586
+ {
587
+ "action": "assertVisible",
588
+ "selector": {
589
+ "resourceId": "$APP_ID:id/workflow_status",
590
+ "text": "Workflow complete"
591
+ },
592
+ "timeoutMs": 10000
593
+ },
594
+ { "action": "snapshot" }
595
+ ]
596
+ }
597
+ EOF
598
+ )"
599
+
305
600
  run "$AAPT2" compile --dir "$RES_DIR" -o "$COMPILED_RES"
306
601
  run "$AAPT2" link -o "$UNSIGNED_APK" -I "$ANDROID_JAR" --manifest "$ANDROID_DIR/AndroidManifest.xml" -R "$COMPILED_RES" --java "$GEN_DIR" --custom-package dev.zmr.demo --auto-add-overlay
307
602
  run javac -source 1.8 -target 1.8 -bootclasspath "$ANDROID_JAR" -d "$CLASSES_DIR" "$GEN_DIR/dev/zmr/demo/R.java" "$SRC_DIR/MainActivity.java"
@@ -37,6 +37,7 @@ After generation:
37
37
  xcodebuild -project ios/ZMRDemo.xcodeproj -scheme ZMRDemo -destination 'generic/platform=iOS Simulator' -derivedDataPath DerivedData build
38
38
  xcrun simctl install booted DerivedData/Build/Products/Debug-iphonesimulator/ZMRDemo.app
39
39
  zmr run .zmr/ios-shim-smoke.json --platform ios --device booted --app-id com.example.mobiletest --ios-shim ./.zmr/ios-shim --trace-dir traces/zmr-ios-demo
40
+ zmr run .zmr/ios-shim-workflow.json --platform ios --device booted --app-id com.example.mobiletest --ios-shim ./.zmr/ios-shim --trace-dir traces/zmr-ios-workflow
40
41
  USAGE
41
42
  }
42
43
 
@@ -110,12 +111,75 @@ EOF
110
111
  cat > "$SOURCE_DIR/ContentView.swift" <<'EOF'
111
112
  import SwiftUI
112
113
 
114
+ private enum DemoScreen {
115
+ case welcome
116
+ case profile
117
+ case catalog
118
+ case detail
119
+ case review
120
+ }
121
+
122
+ private struct CatalogItem: Identifiable {
123
+ let id: String
124
+ let title: String
125
+ let subtitle: String
126
+ }
127
+
113
128
  struct ContentView: View {
114
129
  @State private var input = ""
115
130
  @State private var status = "Ready"
131
+ @State private var screen: DemoScreen = .welcome
132
+ @State private var profileName = ""
133
+ @State private var profileEmail = ""
134
+ @State private var selectedItem = CatalogItem(id: "north_ridge_pack", title: "North Ridge Pack", subtitle: "Weatherproof day pack")
116
135
  @FocusState private var inputFocused: Bool
117
136
 
137
+ private let catalogItems = [
138
+ CatalogItem(id: "trail_lamp", title: "Trail Lamp", subtitle: "Compact campsite light"),
139
+ CatalogItem(id: "river_bottle", title: "River Bottle", subtitle: "Insulated hydration bottle"),
140
+ CatalogItem(id: "north_ridge_pack", title: "North Ridge Pack", subtitle: "Weatherproof day pack"),
141
+ CatalogItem(id: "summit_shell", title: "Summit Shell", subtitle: "Lightweight rain layer"),
142
+ CatalogItem(id: "basecamp_roll", title: "Basecamp Roll", subtitle: "Modular storage roll"),
143
+ CatalogItem(id: "maple_organizer", title: "Maple Organizer", subtitle: "Cable and tool pouch"),
144
+ CatalogItem(id: "canyon_sling", title: "Canyon Sling", subtitle: "Cross-body field bag"),
145
+ CatalogItem(id: "harbor_tote", title: "Harbor Tote", subtitle: "Daily carry tote"),
146
+ CatalogItem(id: "studio_stand", title: "Studio Stand", subtitle: "Fold-flat work stand")
147
+ ]
148
+
118
149
  var body: some View {
150
+ VStack(spacing: 16) {
151
+ switch screen {
152
+ case .welcome:
153
+ welcomeView
154
+ case .profile:
155
+ profileView
156
+ case .catalog:
157
+ catalogView
158
+ case .detail:
159
+ detailView
160
+ case .review:
161
+ reviewView
162
+ }
163
+
164
+ VStack(spacing: 4) {
165
+ Text(status)
166
+ .font(.footnote)
167
+ .foregroundStyle(.secondary)
168
+ .accessibilityIdentifier("demo_status")
169
+
170
+ Text(status)
171
+ .font(.footnote)
172
+ .foregroundStyle(.secondary)
173
+ .accessibilityIdentifier("workflow_status")
174
+ }
175
+ }
176
+ .padding()
177
+ .onOpenURL { _ in
178
+ status = "Deep link opened"
179
+ }
180
+ }
181
+
182
+ private var welcomeView: some View {
119
183
  VStack(spacing: 20) {
120
184
  Text("ZMR iOS Demo")
121
185
  .font(.title)
@@ -123,24 +187,126 @@ struct ContentView: View {
123
187
 
124
188
  Button("Continue") {
125
189
  status = "Continue tapped"
126
- inputFocused = true
190
+ screen = .profile
127
191
  }
128
192
  .buttonStyle(.borderedProminent)
129
193
  .accessibilityIdentifier("continue_button")
194
+ }
195
+ }
196
+
197
+ private var profileView: some View {
198
+ VStack(spacing: 14) {
199
+ Text("Profile")
200
+ .font(.title2)
201
+ .accessibilityIdentifier("profile_title")
130
202
 
131
203
  TextField("Type here", text: $input)
132
204
  .textFieldStyle(.roundedBorder)
133
205
  .focused($inputFocused)
134
206
  .accessibilityIdentifier("demo_input")
135
- .padding(.horizontal, 32)
136
207
 
137
- Text(status)
138
- .accessibilityIdentifier("demo_status")
208
+ TextField("Name", text: $profileName)
209
+ .textFieldStyle(.roundedBorder)
210
+ .textInputAutocapitalization(.never)
211
+ .autocorrectionDisabled()
212
+ .accessibilityIdentifier("profile_name_input")
213
+
214
+ TextField("Email", text: $profileEmail)
215
+ .textFieldStyle(.roundedBorder)
216
+ .keyboardType(.emailAddress)
217
+ .textInputAutocapitalization(.never)
218
+ .autocorrectionDisabled()
219
+ .accessibilityIdentifier("profile_email_input")
220
+
221
+ Button("Save profile") {
222
+ status = "Profile saved"
223
+ screen = .catalog
224
+ }
225
+ .buttonStyle(.borderedProminent)
226
+ .accessibilityIdentifier("save_profile_button")
139
227
  }
140
- .padding()
141
- .onOpenURL { _ in
142
- status = "Deep link opened"
228
+ }
229
+
230
+ private var catalogView: some View {
231
+ VStack(spacing: 14) {
232
+ Text("Catalog")
233
+ .font(.title2)
234
+ .accessibilityIdentifier("catalog_title")
235
+
236
+ ScrollView {
237
+ LazyVStack(spacing: 10) {
238
+ ForEach(catalogItems) { item in
239
+ Button {
240
+ selectedItem = item
241
+ status = "Selected \(item.title)"
242
+ screen = .detail
243
+ } label: {
244
+ VStack(alignment: .leading, spacing: 3) {
245
+ Text(item.title)
246
+ .font(.headline)
247
+ Text(item.subtitle)
248
+ .font(.subheadline)
249
+ .foregroundStyle(.secondary)
250
+ }
251
+ .frame(maxWidth: .infinity, alignment: .leading)
252
+ .padding()
253
+ }
254
+ .buttonStyle(.bordered)
255
+ .accessibilityIdentifier(catalogAccessibilityIdentifier(for: item))
256
+ }
257
+ }
258
+ }
259
+ .frame(maxHeight: 340)
260
+ .accessibilityIdentifier("catalog_list")
261
+ }
262
+ }
263
+
264
+ private var detailView: some View {
265
+ VStack(spacing: 16) {
266
+ Text(selectedItem.title)
267
+ .font(.title2)
268
+ .accessibilityIdentifier("detail_title")
269
+
270
+ Text(selectedItem.subtitle)
271
+ .foregroundStyle(.secondary)
272
+ .accessibilityIdentifier("detail_subtitle")
273
+
274
+ Button("Save item") {
275
+ status = "Saved \(selectedItem.title)"
276
+ }
277
+ .buttonStyle(.borderedProminent)
278
+ .accessibilityIdentifier("detail_save_button")
279
+
280
+ Button("Review order") {
281
+ status = "Workflow complete"
282
+ screen = .review
283
+ }
284
+ .buttonStyle(.bordered)
285
+ .accessibilityIdentifier("review_button")
286
+ }
287
+ }
288
+
289
+ private var reviewView: some View {
290
+ VStack(spacing: 16) {
291
+ Text("Review")
292
+ .font(.title2)
293
+ .accessibilityIdentifier("review_title")
294
+
295
+ Text("Workflow complete")
296
+ .font(.headline)
297
+ .accessibilityIdentifier("review_complete")
298
+
299
+ Text(selectedItem.title)
300
+ .foregroundStyle(.secondary)
301
+ .accessibilityIdentifier("review_item")
302
+ }
303
+ }
304
+
305
+ private func catalogAccessibilityIdentifier(for item: CatalogItem) -> String {
306
+ if item.id == "north_ridge_pack" {
307
+ return "catalog_item_north_ridge_pack"
143
308
  }
309
+ return "catalog_item_\(item.id)"
144
310
  }
145
311
  }
146
312
  EOF
@@ -253,6 +419,7 @@ RUBY
253
419
 
254
420
  cp "$ROOT/examples/ios-smoke.json" "$OUT/.zmr/ios-smoke.json"
255
421
  cp "$ROOT/examples/ios-shim-smoke.json" "$OUT/.zmr/ios-shim-smoke.json"
422
+ cp "$ROOT/examples/ios-shim-workflow.json" "$OUT/.zmr/ios-shim-workflow.json"
256
423
 
257
424
  echo "created iOS demo app at $OUT"
258
425
  echo "project: $PROJECT_PATH"