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,253 @@
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+
8
+ FIXTURE_STATUSES = {"planned", "fixture-available", "evidence-committed"}
9
+ ADAPTER_STATUSES = {"planned", "partial", "available", "evidence-committed"}
10
+ SLICE_STATUSES = {"next", "later", "done"}
11
+
12
+
13
+ def parse_args():
14
+ parser = argparse.ArgumentParser(
15
+ description="Validate and render a ZMR benchmark lab manifest.",
16
+ )
17
+ parser.add_argument(
18
+ "--manifest",
19
+ default="docs/benchmarks/benchmark-lab-v1.json",
20
+ help="Benchmark lab manifest path.",
21
+ )
22
+ parser.add_argument(
23
+ "--format",
24
+ choices=("json", "markdown"),
25
+ default="markdown",
26
+ help="Output format.",
27
+ )
28
+ parser.add_argument("--out", help="Optional output file. Defaults to stdout.")
29
+ return parser.parse_args()
30
+
31
+
32
+ def require_object(value, path):
33
+ if not isinstance(value, dict):
34
+ raise ValueError(f"{path} must be an object")
35
+
36
+
37
+ def require_array(value, path):
38
+ if not isinstance(value, list):
39
+ raise ValueError(f"{path} must be an array")
40
+
41
+
42
+ def require_string(value, path):
43
+ if not isinstance(value, str) or not value.strip():
44
+ raise ValueError(f"{path} must be a non-empty string")
45
+ return value.strip()
46
+
47
+
48
+ def validate_unique_ids(items, path):
49
+ seen = set()
50
+ for index, item in enumerate(items):
51
+ require_object(item, f"{path}[{index}]")
52
+ item_id = require_string(item.get("id"), f"{path}[{index}].id")
53
+ if item_id in seen:
54
+ raise ValueError(f"{path}[{index}].id is duplicated: {item_id}")
55
+ seen.add(item_id)
56
+
57
+
58
+ def validate_status(value, allowed, path):
59
+ actual = require_string(value, path)
60
+ if actual not in allowed:
61
+ raise ValueError(f"{path} must be one of: {', '.join(sorted(allowed))}")
62
+
63
+
64
+ def validate_manifest(data):
65
+ require_object(data, "$")
66
+ if data.get("schemaVersion") != 1:
67
+ raise ValueError("$.schemaVersion must be 1")
68
+ require_string(data.get("name"), "$.name")
69
+ require_string(data.get("purpose"), "$.purpose")
70
+ require_object(data.get("claimPolicy"), "$.claimPolicy")
71
+
72
+ policy = data["claimPolicy"]
73
+ if int(policy.get("minimumRuns", 0)) < 1:
74
+ raise ValueError("$.claimPolicy.minimumRuns must be positive")
75
+ if float(policy.get("candidatePassRate", -1)) < 0:
76
+ raise ValueError("$.claimPolicy.candidatePassRate must be non-negative")
77
+ if int(policy.get("candidateFailures", -1)) < 0:
78
+ raise ValueError("$.claimPolicy.candidateFailures must be non-negative")
79
+ if policy.get("requiresSameContext") is not True:
80
+ raise ValueError("$.claimPolicy.requiresSameContext must be true")
81
+
82
+ modes = data.get("modes")
83
+ fixtures = data.get("fixtures")
84
+ adapters = data.get("runnerAdapters")
85
+ slices = data.get("nextSlices")
86
+ require_array(modes, "$.modes")
87
+ require_array(fixtures, "$.fixtures")
88
+ require_array(adapters, "$.runnerAdapters")
89
+ require_array(slices, "$.nextSlices")
90
+ validate_unique_ids(modes, "$.modes")
91
+ validate_unique_ids(fixtures, "$.fixtures")
92
+ validate_unique_ids(adapters, "$.runnerAdapters")
93
+ validate_unique_ids(slices, "$.nextSlices")
94
+
95
+ mode_ids = {mode["id"] for mode in modes}
96
+ for index, mode in enumerate(modes):
97
+ require_string(mode.get("label"), f"$.modes[{index}].label")
98
+ require_string(mode.get("description"), f"$.modes[{index}].description")
99
+
100
+ for index, fixture in enumerate(fixtures):
101
+ require_string(fixture.get("label"), f"$.fixtures[{index}].label")
102
+ require_string(fixture.get("framework"), f"$.fixtures[{index}].framework")
103
+ validate_status(fixture.get("status"), FIXTURE_STATUSES, f"$.fixtures[{index}].status")
104
+ require_array(fixture.get("platforms"), f"$.fixtures[{index}].platforms")
105
+ require_array(fixture.get("workflow"), f"$.fixtures[{index}].workflow")
106
+ if fixture["status"] != "planned" and not fixture.get("scenario"):
107
+ raise ValueError(f"$.fixtures[{index}].scenario is required unless status is planned")
108
+
109
+ for index, adapter in enumerate(adapters):
110
+ require_string(adapter.get("label"), f"$.runnerAdapters[{index}].label")
111
+ validate_status(adapter.get("status"), ADAPTER_STATUSES, f"$.runnerAdapters[{index}].status")
112
+ require_string(adapter.get("collector"), f"$.runnerAdapters[{index}].collector")
113
+ require_array(adapter.get("modes"), f"$.runnerAdapters[{index}].modes")
114
+ for mode in adapter["modes"]:
115
+ if mode not in mode_ids:
116
+ raise ValueError(f"$.runnerAdapters[{index}].modes references unknown mode: {mode}")
117
+
118
+ for index, next_slice in enumerate(slices):
119
+ validate_status(next_slice.get("status"), SLICE_STATUSES, f"$.nextSlices[{index}].status")
120
+ require_string(next_slice.get("description"), f"$.nextSlices[{index}].description")
121
+
122
+
123
+ def read_manifest(path):
124
+ try:
125
+ data = json.loads(Path(path).read_text(encoding="utf-8"))
126
+ except json.JSONDecodeError as exc:
127
+ raise ValueError(f"{path}:{exc.lineno}:{exc.colno}: invalid json: {exc.msg}") from exc
128
+ validate_manifest(data)
129
+ return data
130
+
131
+
132
+ def summary(data):
133
+ fixtures = data["fixtures"]
134
+ adapters = data["runnerAdapters"]
135
+ next_slices = data["nextSlices"]
136
+ return {
137
+ "ok": True,
138
+ "name": data["name"],
139
+ "schemaVersion": data["schemaVersion"],
140
+ "fixtureCount": len(fixtures),
141
+ "adapterCount": len(adapters),
142
+ "modeCount": len(data["modes"]),
143
+ "evidenceFixtures": [fixture["id"] for fixture in fixtures if fixture["status"] == "evidence-committed"],
144
+ "availableFixtures": [fixture["id"] for fixture in fixtures if fixture["status"] in ("fixture-available", "evidence-committed")],
145
+ "plannedFixtures": [fixture["id"] for fixture in fixtures if fixture["status"] == "planned"],
146
+ "nextSlices": [item["id"] for item in next_slices if item["status"] == "next"],
147
+ "minimumRuns": data["claimPolicy"]["minimumRuns"],
148
+ "candidatePassRate": data["claimPolicy"]["candidatePassRate"],
149
+ "candidateFailures": data["claimPolicy"]["candidateFailures"],
150
+ }
151
+
152
+
153
+ def markdown_table(headers, rows):
154
+ lines = [
155
+ "| " + " | ".join(headers) + " |",
156
+ "| " + " | ".join("---" for _ in headers) + " |",
157
+ ]
158
+ for row in rows:
159
+ lines.append("| " + " | ".join(row) + " |")
160
+ return "\n".join(lines)
161
+
162
+
163
+ def render_markdown(data):
164
+ fixtures = [
165
+ [
166
+ fixture["id"],
167
+ fixture["framework"],
168
+ ", ".join(fixture["platforms"]),
169
+ fixture["status"],
170
+ fixture.get("scenario", "pending"),
171
+ ]
172
+ for fixture in data["fixtures"]
173
+ ]
174
+ adapters = [
175
+ [
176
+ adapter["id"],
177
+ adapter["status"],
178
+ adapter["collector"],
179
+ ", ".join(adapter["modes"]),
180
+ ]
181
+ for adapter in data["runnerAdapters"]
182
+ ]
183
+ modes = [
184
+ [mode["id"], mode["label"], mode["description"]]
185
+ for mode in data["modes"]
186
+ ]
187
+ next_slices = [
188
+ [item["id"], item["status"], item["description"]]
189
+ for item in data["nextSlices"]
190
+ ]
191
+ policy = data["claimPolicy"]
192
+ return "\n".join(
193
+ [
194
+ f"# {data['name']}",
195
+ "",
196
+ data["purpose"],
197
+ "",
198
+ "## Claim Policy",
199
+ "",
200
+ f"- Minimum runs: {policy['minimumRuns']}",
201
+ f"- Candidate pass rate: {policy['candidatePassRate']}%",
202
+ f"- Candidate failures: {policy['candidateFailures']}",
203
+ f"- Requires same context: {'yes' if policy['requiresSameContext'] else 'no'}",
204
+ f"- Requires committed rows: {'yes' if policy['requiresCommittedRows'] else 'no'}",
205
+ f"- Forbidden claim: {policy['forbiddenClaim']}",
206
+ "",
207
+ "## Fixtures",
208
+ "",
209
+ markdown_table(["Fixture", "Framework", "Platforms", "Status", "Scenario"], fixtures),
210
+ "",
211
+ "## Runner Adapters",
212
+ "",
213
+ markdown_table(["Adapter", "Status", "Collector", "Modes"], adapters),
214
+ "",
215
+ "## Modes",
216
+ "",
217
+ markdown_table(["Mode", "Label", "Description"], modes),
218
+ "",
219
+ "## Next Slices",
220
+ "",
221
+ markdown_table(["Slice", "Status", "Description"], next_slices),
222
+ "",
223
+ ]
224
+ )
225
+
226
+
227
+ def write_output(content, path):
228
+ if path:
229
+ out = Path(path)
230
+ out.parent.mkdir(parents=True, exist_ok=True)
231
+ out.write_text(content, encoding="utf-8")
232
+ else:
233
+ sys.stdout.write(content)
234
+
235
+
236
+ def main():
237
+ args = parse_args()
238
+ try:
239
+ data = read_manifest(args.manifest)
240
+ except ValueError as exc:
241
+ print(f"error: {exc}", file=sys.stderr)
242
+ return 2
243
+
244
+ if args.format == "json":
245
+ content = json.dumps(summary(data), sort_keys=True, separators=(",", ":")) + "\n"
246
+ else:
247
+ content = render_markdown(data)
248
+ write_output(content, args.out)
249
+ return 0
250
+
251
+
252
+ if __name__ == "__main__":
253
+ raise SystemExit(main())
@@ -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"