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.
- package/CHANGELOG.md +49 -0
- package/FEATURES.md +1 -1
- package/README.md +167 -238
- 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/agent-discovery.md +10 -0
- package/docs/ai-agents.md +18 -0
- package/docs/benchmarking.md +39 -0
- package/docs/benchmarks/2026-06-09-android-workflow.md +73 -0
- package/docs/benchmarks/2026-06-09-android-workflow.results.jsonl +20 -0
- package/docs/benchmarks/2026-06-09-framework-baseline-status.md +32 -0
- package/docs/benchmarks/2026-06-09-ios-appium-comparison.md +115 -0
- package/docs/benchmarks/2026-06-09-ios-appium-comparison.results.jsonl +40 -0
- package/docs/benchmarks/2026-06-09-ios-demo.md +90 -0
- package/docs/benchmarks/2026-06-09-ios-demo.results.jsonl +20 -0
- package/docs/benchmarks/2026-06-09-ios-maestro-comparison.md +128 -0
- package/docs/benchmarks/2026-06-09-ios-maestro-comparison.results.jsonl +40 -0
- package/docs/benchmarks/2026-06-09-ios-workflow-comparison.md +143 -0
- package/docs/benchmarks/2026-06-09-ios-workflow-comparison.results.jsonl +40 -0
- package/docs/benchmarks/2026-06-09-ios-xctest-floor.md +106 -0
- package/docs/benchmarks/2026-06-09-ios-xctest-floor.results.jsonl +40 -0
- package/docs/benchmarks/README.md +36 -0
- package/docs/benchmarks/benchmark-lab-v1.json +155 -0
- package/docs/benchmarks/benchmark-lab-v1.md +95 -0
- package/docs/clients.md +16 -0
- package/docs/demo.md +36 -1
- package/docs/frameworks.md +10 -0
- package/docs/npm.md +44 -2
- package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
- package/docs/protocol.md +10 -10
- package/docs/scenario-authoring.md +15 -0
- package/docs/trace-privacy.md +9 -0
- package/docs/troubleshooting.md +6 -0
- package/examples/android-workflow.json +79 -0
- package/examples/ios-shim-workflow.json +79 -0
- package/examples/react-native-expo-workflow.json +75 -0
- package/package.json +6 -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/scripts/benchmark-lab.py +253 -0
- package/scripts/create-android-demo-app.sh +324 -29
- package/scripts/create-ios-demo-app.sh +174 -7
- package/scripts/create-react-native-expo-demo-app.sh +727 -0
- package/scripts/demo.sh +3 -0
- package/scripts/install-ios-shim.sh +2 -2
- package/shims/ios/ZMRShim.swift +10 -0
- package/shims/ios/ZMRShimUITestCase.swift +42 -0
- package/shims/ios/protocol.md +1 -0
- package/src/cli_import.zig +31 -15
- package/src/cli_trace.zig +38 -16
- package/src/cli_validate.zig +12 -6
- package/src/ios.zig +49 -12
- package/src/ios_shim.zig +36 -2
- package/src/main.zig +3 -0
- package/src/version.zig +1 -1
- 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
|
|
224
|
-
private
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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(
|
|
238
|
-
title.setText(
|
|
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
|
-
|
|
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
|
-
|
|
361
|
+
root.addView(button, new LinearLayout.LayoutParams(-1, dp(56)));
|
|
362
|
+
addStatusViews();
|
|
248
363
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
364
|
+
button.setOnClickListener(new View.OnClickListener() {
|
|
365
|
+
@Override
|
|
366
|
+
public void onClick(View view) {
|
|
367
|
+
showProfile("Continue tapped");
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
}
|
|
254
371
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
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.
|
|
401
|
+
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
|
270
402
|
}
|
|
403
|
+
showCatalog("Profile saved");
|
|
271
404
|
}
|
|
272
405
|
});
|
|
406
|
+
}
|
|
273
407
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
.
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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"
|