zeno-mobile-runner 0.1.2

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 (213) hide show
  1. package/CHANGELOG.md +497 -0
  2. package/CONTRIBUTING.md +42 -0
  3. package/FEATURES.md +111 -0
  4. package/LICENSE +21 -0
  5. package/README.md +176 -0
  6. package/SECURITY.md +34 -0
  7. package/build.zig +38 -0
  8. package/build.zig.zon +7 -0
  9. package/clients/README.md +149 -0
  10. package/clients/go/README.md +24 -0
  11. package/clients/go/examples/fake-session/main.go +93 -0
  12. package/clients/go/go.mod +3 -0
  13. package/clients/go/zmr/client.go +432 -0
  14. package/clients/kotlin/README.md +35 -0
  15. package/clients/kotlin/build.gradle.kts +35 -0
  16. package/clients/kotlin/settings.gradle.kts +15 -0
  17. package/clients/kotlin/src/main/kotlin/dev/zmr/FakeSession.kt +86 -0
  18. package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +67 -0
  19. package/clients/python/README.md +29 -0
  20. package/clients/python/examples/fake_session.py +48 -0
  21. package/clients/python/pyproject.toml +13 -0
  22. package/clients/python/zmr_client.py +202 -0
  23. package/clients/rust/Cargo.lock +107 -0
  24. package/clients/rust/Cargo.toml +10 -0
  25. package/clients/rust/README.md +19 -0
  26. package/clients/rust/examples/fake_session.rs +70 -0
  27. package/clients/rust/src/lib.rs +461 -0
  28. package/clients/swift/Package.swift +16 -0
  29. package/clients/swift/README.md +36 -0
  30. package/clients/swift/Sources/ZMRClient/ZMRClient.swift +114 -0
  31. package/clients/swift/Sources/ZMRFakeSession/main.swift +86 -0
  32. package/clients/typescript/README.md +34 -0
  33. package/clients/typescript/examples/fake-session.mjs +36 -0
  34. package/clients/typescript/index.d.ts +144 -0
  35. package/clients/typescript/index.mjs +192 -0
  36. package/clients/typescript/package.json +8 -0
  37. package/docs/adr/0001-agent-native-runner-boundary.md +31 -0
  38. package/docs/adr/0002-app-local-zmr-contract.md +39 -0
  39. package/docs/adr/0003-ios-simulator-xctest-shim.md +41 -0
  40. package/docs/adr/0004-benchmark-claims-and-baseline-collection.md +37 -0
  41. package/docs/adr/README.md +12 -0
  42. package/docs/ai-agents.md +154 -0
  43. package/docs/app-integration.md +330 -0
  44. package/docs/benchmarking.md +273 -0
  45. package/docs/client-installation.md +133 -0
  46. package/docs/clients.md +98 -0
  47. package/docs/config.md +175 -0
  48. package/docs/demo.md +259 -0
  49. package/docs/frameworks.md +72 -0
  50. package/docs/install.md +95 -0
  51. package/docs/npm.md +356 -0
  52. package/docs/protocol-fixtures/README.md +8 -0
  53. package/docs/protocol-fixtures/core-session.requests.jsonl +8 -0
  54. package/docs/protocol-fixtures/core-session.responses.jsonl +8 -0
  55. package/docs/protocol-versioning.md +65 -0
  56. package/docs/protocol.md +560 -0
  57. package/docs/scenario-authoring.md +88 -0
  58. package/docs/trace-privacy.md +88 -0
  59. package/docs/troubleshooting.md +256 -0
  60. package/examples/android-app-auth-probe.json +89 -0
  61. package/examples/android-app-error-state.json +13 -0
  62. package/examples/android-app-login-smoke.json +192 -0
  63. package/examples/android-app-onboarding.json +12 -0
  64. package/examples/android-app-referral-deep-link.json +12 -0
  65. package/examples/android-shim-smoke.json +19 -0
  66. package/examples/demo-failure.json +12 -0
  67. package/examples/demo-fake.json +14 -0
  68. package/examples/ios-dev-client-open-link.json +26 -0
  69. package/examples/ios-dev-client-route-snapshot.json +24 -0
  70. package/examples/ios-shim-smoke.json +23 -0
  71. package/examples/ios-smoke.json +9 -0
  72. package/go.work +3 -0
  73. package/npm/agents.mjs +183 -0
  74. package/npm/app-config.mjs +95 -0
  75. package/npm/build-zmr.mjs +21 -0
  76. package/npm/commands.mjs +104 -0
  77. package/npm/generated-files.mjs +50 -0
  78. package/npm/index.mjs +75 -0
  79. package/npm/init-app.mjs +80 -0
  80. package/npm/package-scripts.mjs +72 -0
  81. package/npm/postinstall.mjs +21 -0
  82. package/npm/scaffold.mjs +179 -0
  83. package/npm/scenarios.mjs +93 -0
  84. package/npm/setup.mjs +69 -0
  85. package/npm/wizard.mjs +117 -0
  86. package/npm/zmr.mjs +23 -0
  87. package/package.json +118 -0
  88. package/schemas/README.md +26 -0
  89. package/schemas/action-result.schema.json +27 -0
  90. package/schemas/capabilities-output.schema.json +98 -0
  91. package/schemas/devices-output.schema.json +25 -0
  92. package/schemas/doctor-output.schema.json +51 -0
  93. package/schemas/explain-output.schema.json +51 -0
  94. package/schemas/import-output.schema.json +23 -0
  95. package/schemas/init-output.schema.json +71 -0
  96. package/schemas/json-rpc.schema.json +55 -0
  97. package/schemas/release-manifest.schema.json +43 -0
  98. package/schemas/release-readiness-output.schema.json +127 -0
  99. package/schemas/run-output.schema.json +43 -0
  100. package/schemas/scenario.schema.json +128 -0
  101. package/schemas/schemas-output.schema.json +26 -0
  102. package/schemas/semantic-snapshot.schema.json +116 -0
  103. package/schemas/snapshot.schema.json +60 -0
  104. package/schemas/trace-event.schema.json +14 -0
  105. package/schemas/trace-manifest.schema.json +59 -0
  106. package/schemas/validate-output.schema.json +42 -0
  107. package/schemas/version-output.schema.json +23 -0
  108. package/schemas/zmr-config.schema.json +75 -0
  109. package/scripts/android-emulator.sh +126 -0
  110. package/scripts/assert-ios-physical-ready.sh +213 -0
  111. package/scripts/benchmark-command.sh +307 -0
  112. package/scripts/benchmark.sh +359 -0
  113. package/scripts/benchmark_gate.py +117 -0
  114. package/scripts/benchmark_result_row.py +88 -0
  115. package/scripts/compare-benchmarks.py +288 -0
  116. package/scripts/create-android-demo-app.sh +342 -0
  117. package/scripts/create-ios-demo-app.sh +261 -0
  118. package/scripts/demo-android-real.sh +232 -0
  119. package/scripts/demo-ios-real.sh +270 -0
  120. package/scripts/demo.sh +464 -0
  121. package/scripts/device-matrix.sh +338 -0
  122. package/scripts/ensure-ios-shim-target.rb +237 -0
  123. package/scripts/install-android-shim.sh +281 -0
  124. package/scripts/install-ios-shim.sh +589 -0
  125. package/scripts/pilot-gate.sh +560 -0
  126. package/scripts/release-readiness.py +838 -0
  127. package/scripts/release-readiness.sh +91 -0
  128. package/scripts/run-android-pilot.sh +561 -0
  129. package/scripts/run-ios-pilot.sh +509 -0
  130. package/shims/android/README.md +21 -0
  131. package/shims/android/ZMRShimInstrumentedTest.java +152 -0
  132. package/shims/android/protocol.md +18 -0
  133. package/shims/ios/README.md +50 -0
  134. package/shims/ios/ZMRShim.swift +110 -0
  135. package/shims/ios/ZMRShimUITestCase.swift +518 -0
  136. package/shims/ios/protocol.md +74 -0
  137. package/skills/zmr-mobile-testing/SKILL.md +127 -0
  138. package/src/android.zig +344 -0
  139. package/src/android_device_info.zig +99 -0
  140. package/src/android_emulator.zig +154 -0
  141. package/src/android_screen_recording.zig +112 -0
  142. package/src/android_shell.zig +112 -0
  143. package/src/bundle.zig +124 -0
  144. package/src/bundle_redaction.zig +272 -0
  145. package/src/bundle_tar.zig +123 -0
  146. package/src/cli_devices.zig +97 -0
  147. package/src/cli_doctor.zig +114 -0
  148. package/src/cli_import.zig +70 -0
  149. package/src/cli_info.zig +39 -0
  150. package/src/cli_init.zig +72 -0
  151. package/src/cli_output.zig +467 -0
  152. package/src/cli_run.zig +259 -0
  153. package/src/cli_serve.zig +287 -0
  154. package/src/cli_trace.zig +111 -0
  155. package/src/cli_validate.zig +41 -0
  156. package/src/command.zig +211 -0
  157. package/src/config.zig +305 -0
  158. package/src/config_diagnostics.zig +212 -0
  159. package/src/config_paths.zig +49 -0
  160. package/src/device_registry.zig +37 -0
  161. package/src/doctor.zig +412 -0
  162. package/src/doctor_hints.zig +52 -0
  163. package/src/errors.zig +55 -0
  164. package/src/fake_device.zig +163 -0
  165. package/src/health.zig +28 -0
  166. package/src/importer.zig +343 -0
  167. package/src/importer_json.zig +100 -0
  168. package/src/importer_model.zig +103 -0
  169. package/src/ios.zig +399 -0
  170. package/src/ios_devices.zig +219 -0
  171. package/src/ios_lifecycle.zig +72 -0
  172. package/src/ios_shim.zig +242 -0
  173. package/src/ios_snapshot.zig +20 -0
  174. package/src/json_fields.zig +80 -0
  175. package/src/json_rpc.zig +150 -0
  176. package/src/json_rpc_methods.zig +318 -0
  177. package/src/json_rpc_observation.zig +31 -0
  178. package/src/json_rpc_params.zig +52 -0
  179. package/src/json_rpc_protocol.zig +110 -0
  180. package/src/json_rpc_trace.zig +73 -0
  181. package/src/main.zig +131 -0
  182. package/src/mcp.zig +234 -0
  183. package/src/mcp_protocol.zig +64 -0
  184. package/src/mcp_trace.zig +83 -0
  185. package/src/report.zig +346 -0
  186. package/src/report_html.zig +63 -0
  187. package/src/report_values.zig +27 -0
  188. package/src/run_options.zig +152 -0
  189. package/src/runner.zig +280 -0
  190. package/src/runner_actions.zig +109 -0
  191. package/src/runner_config.zig +6 -0
  192. package/src/runner_diagnostics.zig +268 -0
  193. package/src/runner_events.zig +170 -0
  194. package/src/runner_native.zig +88 -0
  195. package/src/runner_waits.zig +300 -0
  196. package/src/scaffold.zig +472 -0
  197. package/src/scenario.zig +346 -0
  198. package/src/scenario_fields.zig +50 -0
  199. package/src/schema_registry.zig +53 -0
  200. package/src/selector.zig +84 -0
  201. package/src/semantic.zig +171 -0
  202. package/src/trace.zig +315 -0
  203. package/src/trace_json.zig +340 -0
  204. package/src/trace_summary.zig +218 -0
  205. package/src/trace_summary_diagnostic.zig +202 -0
  206. package/src/types.zig +120 -0
  207. package/src/uiautomator.zig +164 -0
  208. package/src/validation.zig +187 -0
  209. package/src/version.zig +22 -0
  210. package/viewer/app.js +373 -0
  211. package/viewer/index.html +126 -0
  212. package/viewer/parser.js +233 -0
  213. package/viewer/styles.css +585 -0
@@ -0,0 +1,342 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ OUT=""
6
+ APP_ID="com.example.mobiletest"
7
+ API="35"
8
+ BUILD_TOOLS="35.0.1"
9
+ ANDROID_SDK="${ANDROID_HOME:-$HOME/Library/Android/sdk}"
10
+ DRY_RUN=0
11
+
12
+ usage() {
13
+ cat <<'USAGE'
14
+ Usage:
15
+ scripts/create-android-demo-app.sh --out <dir> [options]
16
+
17
+ Creates a small public native Android demo app and a matching .zmr smoke
18
+ scenario. The generated app is intentionally generic and contains no private
19
+ app references. It uses Android SDK command-line tools directly, so it does not
20
+ need Gradle or network access.
21
+
22
+ Options:
23
+ --out <dir> Output app repository directory. Required.
24
+ --app-id <id> Android application id. Default: com.example.mobiletest.
25
+ --api <level> Android platform API level. Default: 35.
26
+ --build-tools <ver> Android build-tools version. Default: 35.0.1.
27
+ --android-sdk <path> Android SDK root. Default: ANDROID_HOME or ~/Library/Android/sdk.
28
+ --dry-run Print commands without executing them.
29
+ -h, --help Show this help.
30
+
31
+ After generation:
32
+ adb install -r <dir>/build/app-debug.apk
33
+ zmr run <dir>/.zmr/android-smoke.json --device emulator-5554 --app-id com.example.mobiletest --trace-dir <dir>/traces/android-demo
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
+ quote_cmd() {
52
+ local quoted=()
53
+ local arg
54
+ for arg in "$@"; do
55
+ quoted+=("$(printf '%q' "$arg")")
56
+ done
57
+ printf '%s\n' "${quoted[*]}"
58
+ }
59
+
60
+ run() {
61
+ echo "+ $(quote_cmd "$@")"
62
+ if [[ "$DRY_RUN" -eq 0 ]]; then
63
+ "$@"
64
+ fi
65
+ }
66
+
67
+ write_file() {
68
+ local path="$1"
69
+ local content="$2"
70
+ echo "+ write $path"
71
+ if [[ "$DRY_RUN" -eq 0 ]]; then
72
+ mkdir -p "$(dirname "$path")"
73
+ printf '%s' "$content" > "$path"
74
+ fi
75
+ }
76
+
77
+ while [[ $# -gt 0 ]]; do
78
+ case "$1" in
79
+ --out)
80
+ OUT="$(require_value "$1" "${2-}")"
81
+ shift 2
82
+ ;;
83
+ --app-id)
84
+ APP_ID="$(require_value "$1" "${2-}")"
85
+ shift 2
86
+ ;;
87
+ --api)
88
+ API="$(require_value "$1" "${2-}")"
89
+ shift 2
90
+ ;;
91
+ --build-tools)
92
+ BUILD_TOOLS="$(require_value "$1" "${2-}")"
93
+ shift 2
94
+ ;;
95
+ --android-sdk)
96
+ ANDROID_SDK="$(require_value "$1" "${2-}")"
97
+ shift 2
98
+ ;;
99
+ --dry-run)
100
+ DRY_RUN=1
101
+ shift
102
+ ;;
103
+ -h|--help)
104
+ usage
105
+ exit 0
106
+ ;;
107
+ *)
108
+ die "unknown argument: $1"
109
+ ;;
110
+ esac
111
+ done
112
+
113
+ [[ -n "$OUT" ]] || die "--out is required"
114
+ [[ "$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"
115
+ [[ "$API" =~ ^[0-9]+$ ]] || die "--api must be an integer"
116
+ [[ -n "$BUILD_TOOLS" ]] || die "--build-tools must be non-empty"
117
+
118
+ if [[ "$OUT" != /* ]]; then
119
+ OUT="$(pwd -P)/$OUT"
120
+ fi
121
+
122
+ ANDROID_DIR="$OUT/android"
123
+ SRC_DIR="$ANDROID_DIR/src/dev/zmr/demo"
124
+ RES_DIR="$ANDROID_DIR/res"
125
+ BUILD_DIR="$OUT/build"
126
+ GEN_DIR="$BUILD_DIR/generated"
127
+ CLASSES_DIR="$BUILD_DIR/classes"
128
+ DEX_DIR="$BUILD_DIR/dex"
129
+ COMPILED_RES="$BUILD_DIR/compiled-res.zip"
130
+ UNSIGNED_APK="$BUILD_DIR/app-unsigned.apk"
131
+ SIGNED_APK="$BUILD_DIR/app-debug.apk"
132
+ KEYSTORE="$BUILD_DIR/debug.keystore"
133
+ ANDROID_JAR="$ANDROID_SDK/platforms/android-$API/android.jar"
134
+ BUILD_TOOLS_DIR="$ANDROID_SDK/build-tools/$BUILD_TOOLS"
135
+ AAPT2="$BUILD_TOOLS_DIR/aapt2"
136
+ D8="$BUILD_TOOLS_DIR/d8"
137
+ APKSIGNER="$BUILD_TOOLS_DIR/apksigner"
138
+ ZMR_BIN="${ZMR_BIN:-}"
139
+
140
+ echo "Android demo app: $OUT"
141
+ echo "Android demo APK: $SIGNED_APK"
142
+ if [[ "$DRY_RUN" -eq 1 ]]; then
143
+ echo "DRY RUN: commands will be printed but not executed"
144
+ fi
145
+
146
+ if [[ "$DRY_RUN" -eq 0 ]]; then
147
+ [[ -f "$ANDROID_JAR" ]] || die "android.jar not found: $ANDROID_JAR"
148
+ [[ -x "$AAPT2" ]] || die "aapt2 not found: $AAPT2"
149
+ [[ -x "$D8" ]] || die "d8 not found: $D8"
150
+ [[ -x "$APKSIGNER" ]] || die "apksigner not found: $APKSIGNER"
151
+ command -v javac >/dev/null 2>&1 || die "javac is required"
152
+ command -v keytool >/dev/null 2>&1 || die "keytool is required"
153
+ command -v zip >/dev/null 2>&1 || die "zip is required"
154
+ fi
155
+
156
+ if [[ "$DRY_RUN" -eq 0 ]]; then
157
+ rm -rf "$BUILD_DIR"
158
+ fi
159
+ run mkdir -p "$SRC_DIR" "$RES_DIR/values" "$BUILD_DIR" "$GEN_DIR" "$CLASSES_DIR" "$DEX_DIR" "$OUT/.zmr"
160
+
161
+ write_file "$ANDROID_DIR/AndroidManifest.xml" "$(cat <<EOF
162
+ <?xml version="1.0" encoding="utf-8"?>
163
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="$APP_ID">
164
+ <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="$API" />
165
+ <application android:theme="@style/AppTheme" android:label="ZMR Android Demo" android:allowBackup="false" android:supportsRtl="true">
166
+ <activity android:name="dev.zmr.demo.MainActivity" android:exported="true">
167
+ <intent-filter>
168
+ <action android:name="android.intent.action.MAIN" />
169
+ <category android:name="android.intent.category.LAUNCHER" />
170
+ </intent-filter>
171
+ <intent-filter>
172
+ <action android:name="android.intent.action.VIEW" />
173
+ <category android:name="android.intent.category.DEFAULT" />
174
+ <category android:name="android.intent.category.BROWSABLE" />
175
+ <data android:scheme="exampleapp" />
176
+ </intent-filter>
177
+ </activity>
178
+ </application>
179
+ </manifest>
180
+ EOF
181
+ )"
182
+
183
+ write_file "$RES_DIR/values/styles.xml" "$(cat <<'EOF'
184
+ <?xml version="1.0" encoding="utf-8"?>
185
+ <resources>
186
+ <style name="AppTheme" parent="android:style/Theme.Material.Light.NoActionBar">
187
+ <item name="android:fontFamily">sans</item>
188
+ <item name="android:windowLightStatusBar">true</item>
189
+ <item name="android:colorAccent">#2563EB</item>
190
+ </style>
191
+ </resources>
192
+ EOF
193
+ )"
194
+
195
+ write_file "$RES_DIR/values/ids.xml" "$(cat <<'EOF'
196
+ <?xml version="1.0" encoding="utf-8"?>
197
+ <resources>
198
+ <item name="demo_title" type="id" />
199
+ <item name="continue_button" type="id" />
200
+ <item name="demo_input" type="id" />
201
+ <item name="demo_status" type="id" />
202
+ </resources>
203
+ EOF
204
+ )"
205
+
206
+ write_file "$SRC_DIR/MainActivity.java" "$(cat <<EOF
207
+ package dev.zmr.demo;
208
+
209
+ import android.app.Activity;
210
+ import android.graphics.Color;
211
+ import android.net.Uri;
212
+ import android.os.Bundle;
213
+ import android.view.View;
214
+ import android.view.Gravity;
215
+ import android.view.inputmethod.InputMethodManager;
216
+ import android.content.Context;
217
+ import android.widget.Button;
218
+ import android.widget.EditText;
219
+ import android.widget.LinearLayout;
220
+ import android.widget.TextView;
221
+
222
+ public class MainActivity extends Activity {
223
+ private TextView status;
224
+ private EditText input;
225
+
226
+ @Override
227
+ protected void onCreate(Bundle savedInstanceState) {
228
+ super.onCreate(savedInstanceState);
229
+
230
+ LinearLayout layout = new LinearLayout(this);
231
+ layout.setOrientation(LinearLayout.VERTICAL);
232
+ layout.setGravity(Gravity.CENTER_HORIZONTAL);
233
+ int padding = dp(24);
234
+ layout.setPadding(padding, padding, padding, padding);
235
+
236
+ TextView title = new TextView(this);
237
+ title.setId(R.id.demo_title);
238
+ title.setText("ZMR Android Demo");
239
+ title.setTextSize(24);
240
+ title.setTextColor(Color.rgb(17, 24, 39));
241
+ title.setGravity(Gravity.CENTER);
242
+ layout.addView(title, new LinearLayout.LayoutParams(-1, -2));
243
+
244
+ Button button = new Button(this);
245
+ button.setId(R.id.continue_button);
246
+ button.setText("Continue");
247
+ layout.addView(button, new LinearLayout.LayoutParams(-1, dp(56)));
248
+
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)));
254
+
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));
261
+
262
+ button.setOnClickListener(new View.OnClickListener() {
263
+ @Override
264
+ public void onClick(View view) {
265
+ status.setText("Continue tapped");
266
+ input.requestFocus();
267
+ InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
268
+ if (imm != null) {
269
+ imm.showSoftInput(input, InputMethodManager.SHOW_IMPLICIT);
270
+ }
271
+ }
272
+ });
273
+
274
+ Uri data = getIntent().getData();
275
+ if (data != null) {
276
+ status.setText("Deep link opened");
277
+ }
278
+
279
+ setContentView(layout);
280
+ }
281
+
282
+ private int dp(int value) {
283
+ return (int) (value * getResources().getDisplayMetrics().density + 0.5f);
284
+ }
285
+ }
286
+ EOF
287
+ )"
288
+
289
+ write_file "$OUT/.zmr/android-smoke.json" "$(cat <<EOF
290
+ {
291
+ "name": "ZMR Android demo smoke",
292
+ "appId": "$APP_ID",
293
+ "steps": [
294
+ { "action": "launch" },
295
+ { "action": "waitVisible", "selector": { "text": "ZMR Android Demo" }, "timeoutMs": 30000 },
296
+ { "action": "tap", "selector": { "resourceId": "$APP_ID:id/continue_button" } },
297
+ { "action": "waitVisible", "selector": { "text": "Continue tapped" }, "timeoutMs": 10000 },
298
+ { "action": "typeText", "selector": { "resourceId": "$APP_ID:id/demo_input" }, "text": "hello from zmr" },
299
+ { "action": "snapshot" }
300
+ ]
301
+ }
302
+ EOF
303
+ )"
304
+
305
+ run "$AAPT2" compile --dir "$RES_DIR" -o "$COMPILED_RES"
306
+ 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
+ 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"
308
+ if [[ "$DRY_RUN" -eq 1 ]]; then
309
+ CLASS_FILES=(
310
+ "$CLASSES_DIR/dev/zmr/demo/R.class"
311
+ "$CLASSES_DIR/dev/zmr/demo/MainActivity.class"
312
+ "$CLASSES_DIR/dev/zmr/demo/MainActivity\$1.class"
313
+ )
314
+ else
315
+ CLASS_FILES=()
316
+ while IFS= read -r class_file; do
317
+ CLASS_FILES+=("$class_file")
318
+ done < <(find "$CLASSES_DIR" -name '*.class' -print | sort)
319
+ [[ "${#CLASS_FILES[@]}" -gt 0 ]] || die "no compiled Java classes found in $CLASSES_DIR"
320
+ fi
321
+
322
+ run "$D8" --lib "$ANDROID_JAR" --min-api 23 --output "$DEX_DIR" "${CLASS_FILES[@]}"
323
+ run zip -j "$UNSIGNED_APK" "$DEX_DIR/classes.dex"
324
+ run keytool -genkeypair -keystore "$KEYSTORE" -storepass android -keypass android -alias zmrdebug -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=ZMR Android Demo,O=ZMR,C=US"
325
+ run "$APKSIGNER" sign --ks "$KEYSTORE" --ks-key-alias zmrdebug --ks-pass pass:android --key-pass pass:android --out "$SIGNED_APK" "$UNSIGNED_APK"
326
+ if [[ -z "$ZMR_BIN" ]]; then
327
+ if [[ -x "$ROOT/zig-out/bin/zmr" ]]; then
328
+ ZMR_BIN="$ROOT/zig-out/bin/zmr"
329
+ elif command -v zmr >/dev/null 2>&1; then
330
+ ZMR_BIN="$(command -v zmr)"
331
+ fi
332
+ fi
333
+
334
+ if [[ -n "$ZMR_BIN" ]]; then
335
+ run "$ZMR_BIN" validate "$OUT/.zmr/android-smoke.json"
336
+ else
337
+ echo "warning: skipped scenario validation because zmr was not found; run 'zmr validate $OUT/.zmr/android-smoke.json' after installation" >&2
338
+ fi
339
+
340
+ echo "created Android demo app at $OUT"
341
+ echo "apk: $SIGNED_APK"
342
+ echo "scenario: $OUT/.zmr/android-smoke.json"
@@ -0,0 +1,261 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SOURCE="${BASH_SOURCE[0]}"
5
+ while [[ -h "$SOURCE" ]]; do
6
+ SOURCE_DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
7
+ SOURCE="$(readlink "$SOURCE")"
8
+ if [[ "$SOURCE" != /* ]]; then
9
+ SOURCE="$SOURCE_DIR/$SOURCE"
10
+ fi
11
+ done
12
+
13
+ ROOT="$(cd -P "$(dirname "$SOURCE")/.." && pwd)"
14
+ OUT=""
15
+ APP_NAME="ZMRDemo"
16
+ BUNDLE_ID="com.example.mobiletest"
17
+ DEPLOYMENT_TARGET="16.0"
18
+
19
+ usage() {
20
+ cat <<'USAGE'
21
+ Usage:
22
+ scripts/create-ios-demo-app.sh --out <dir> [options]
23
+
24
+ Creates a small public SwiftUI iOS simulator demo app and installs the ZMR
25
+ XCTest shim into it. The generated app is intentionally generic and contains no
26
+ private app references.
27
+
28
+ Options:
29
+ --out <dir> Output app repository directory. Required.
30
+ --name <name> App target name. Default: ZMRDemo.
31
+ --bundle-id <id> App bundle id. Default: com.example.mobiletest.
32
+ --deployment-target <ver> iOS deployment target. Default: 16.0.
33
+ -h, --help Show this help.
34
+
35
+ After generation:
36
+ cd <dir>
37
+ xcodebuild -project ios/ZMRDemo.xcodeproj -scheme ZMRDemo -destination 'generic/platform=iOS Simulator' -derivedDataPath DerivedData build
38
+ xcrun simctl install booted DerivedData/Build/Products/Debug-iphonesimulator/ZMRDemo.app
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
+ USAGE
41
+ }
42
+
43
+ die() {
44
+ echo "error: $*" >&2
45
+ exit 2
46
+ }
47
+
48
+ require_value() {
49
+ local flag="$1"
50
+ local value="${2-}"
51
+ if [[ -z "$value" || "$value" == --* ]]; then
52
+ die "$flag requires a value"
53
+ fi
54
+ printf '%s\n' "$value"
55
+ }
56
+
57
+ while [[ $# -gt 0 ]]; do
58
+ case "$1" in
59
+ --out)
60
+ OUT="$(require_value "$1" "${2-}")"
61
+ shift 2
62
+ ;;
63
+ --name)
64
+ APP_NAME="$(require_value "$1" "${2-}")"
65
+ shift 2
66
+ ;;
67
+ --bundle-id)
68
+ BUNDLE_ID="$(require_value "$1" "${2-}")"
69
+ shift 2
70
+ ;;
71
+ --deployment-target)
72
+ DEPLOYMENT_TARGET="$(require_value "$1" "${2-}")"
73
+ shift 2
74
+ ;;
75
+ -h|--help)
76
+ usage
77
+ exit 0
78
+ ;;
79
+ *)
80
+ die "unknown argument: $1"
81
+ ;;
82
+ esac
83
+ done
84
+
85
+ [[ -n "$OUT" ]] || die "--out is required"
86
+ [[ "$APP_NAME" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || die "--name must be a valid Swift identifier"
87
+ if ! ruby -e 'require "xcodeproj"' >/dev/null 2>&1; then
88
+ die "Ruby gem xcodeproj is required. Install it with: gem install xcodeproj"
89
+ fi
90
+
91
+ mkdir -p "$OUT/ios/$APP_NAME"
92
+ OUT="$(cd "$OUT" && pwd)"
93
+ PROJECT_PATH="$OUT/ios/$APP_NAME.xcodeproj"
94
+ SOURCE_DIR="$OUT/ios/$APP_NAME"
95
+ TEST_SCHEME="${APP_NAME}ZMRUITests"
96
+
97
+ cat > "$SOURCE_DIR/${APP_NAME}App.swift" <<EOF
98
+ import SwiftUI
99
+
100
+ @main
101
+ struct ${APP_NAME}App: App {
102
+ var body: some Scene {
103
+ WindowGroup {
104
+ ContentView()
105
+ }
106
+ }
107
+ }
108
+ EOF
109
+
110
+ cat > "$SOURCE_DIR/ContentView.swift" <<'EOF'
111
+ import SwiftUI
112
+
113
+ struct ContentView: View {
114
+ @State private var input = ""
115
+ @State private var status = "Ready"
116
+ @FocusState private var inputFocused: Bool
117
+
118
+ var body: some View {
119
+ VStack(spacing: 20) {
120
+ Text("ZMR iOS Demo")
121
+ .font(.title)
122
+ .accessibilityIdentifier("demo_title")
123
+
124
+ Button("Continue") {
125
+ status = "Continue tapped"
126
+ inputFocused = true
127
+ }
128
+ .buttonStyle(.borderedProminent)
129
+ .accessibilityIdentifier("continue_button")
130
+
131
+ TextField("Type here", text: $input)
132
+ .textFieldStyle(.roundedBorder)
133
+ .focused($inputFocused)
134
+ .accessibilityIdentifier("demo_input")
135
+ .padding(.horizontal, 32)
136
+
137
+ Text(status)
138
+ .accessibilityIdentifier("demo_status")
139
+ }
140
+ .padding()
141
+ .onOpenURL { _ in
142
+ status = "Deep link opened"
143
+ }
144
+ }
145
+ }
146
+ EOF
147
+
148
+ cat > "$SOURCE_DIR/Info.plist" <<EOF
149
+ <?xml version="1.0" encoding="UTF-8"?>
150
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
151
+ <plist version="1.0">
152
+ <dict>
153
+ <key>CFBundleDevelopmentRegion</key>
154
+ <string>\$(DEVELOPMENT_LANGUAGE)</string>
155
+ <key>CFBundleExecutable</key>
156
+ <string>\$(EXECUTABLE_NAME)</string>
157
+ <key>CFBundleIdentifier</key>
158
+ <string>\$(PRODUCT_BUNDLE_IDENTIFIER)</string>
159
+ <key>CFBundleInfoDictionaryVersion</key>
160
+ <string>6.0</string>
161
+ <key>CFBundleName</key>
162
+ <string>\$(PRODUCT_NAME)</string>
163
+ <key>CFBundlePackageType</key>
164
+ <string>APPL</string>
165
+ <key>CFBundleShortVersionString</key>
166
+ <string>1.0</string>
167
+ <key>CFBundleURLTypes</key>
168
+ <array>
169
+ <dict>
170
+ <key>CFBundleURLName</key>
171
+ <string>${BUNDLE_ID}</string>
172
+ <key>CFBundleURLSchemes</key>
173
+ <array>
174
+ <string>exampleapp</string>
175
+ </array>
176
+ </dict>
177
+ </array>
178
+ <key>CFBundleVersion</key>
179
+ <string>1</string>
180
+ <key>UILaunchScreen</key>
181
+ <dict/>
182
+ </dict>
183
+ </plist>
184
+ EOF
185
+
186
+ ruby - "$PROJECT_PATH" "$APP_NAME" "$BUNDLE_ID" "$DEPLOYMENT_TARGET" <<'RUBY'
187
+ require "fileutils"
188
+ require "xcodeproj"
189
+
190
+ project_path, app_name, bundle_id, deployment_target = ARGV
191
+ project = Xcodeproj::Project.new(project_path)
192
+ target = project.new_target(:application, app_name, :ios, deployment_target)
193
+ group = project.main_group.new_group(app_name, app_name)
194
+
195
+ ["#{app_name}App.swift", "ContentView.swift"].each do |source|
196
+ file_ref = group.new_file(source)
197
+ target.add_file_references([file_ref])
198
+ end
199
+ group.new_file("Info.plist")
200
+
201
+ target.build_configurations.each do |configuration|
202
+ settings = configuration.build_settings
203
+ settings["PRODUCT_BUNDLE_IDENTIFIER"] = bundle_id
204
+ settings["PRODUCT_NAME"] = "$(TARGET_NAME)"
205
+ settings["SWIFT_VERSION"] = "5.0"
206
+ settings["GENERATE_INFOPLIST_FILE"] = "NO"
207
+ settings["INFOPLIST_FILE"] = "#{app_name}/Info.plist"
208
+ settings["TARGETED_DEVICE_FAMILY"] = "1,2"
209
+ settings["CODE_SIGNING_ALLOWED"] = "NO"
210
+ end
211
+
212
+ project.save
213
+ scheme_dir = File.join(project.path, "xcshareddata/xcschemes")
214
+ FileUtils.mkdir_p(scheme_dir)
215
+ scheme_path = File.join(scheme_dir, "#{app_name}.xcscheme")
216
+ File.write(scheme_path, <<~XML)
217
+ <?xml version="1.0" encoding="UTF-8"?>
218
+ <Scheme LastUpgradeVersion = "1600" version = "1.7">
219
+ <BuildAction parallelizeBuildables = "YES" buildImplicitDependencies = "YES">
220
+ <BuildActionEntries>
221
+ <BuildActionEntry buildForTesting = "YES" buildForRunning = "YES" buildForProfiling = "YES" buildForArchiving = "YES" buildForAnalyzing = "YES">
222
+ <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "#{target.uuid}" BuildableName = "#{app_name}.app" BlueprintName = "#{app_name}" ReferencedContainer = "container:#{File.basename(project.path)}">
223
+ </BuildableReference>
224
+ </BuildActionEntry>
225
+ </BuildActionEntries>
226
+ </BuildAction>
227
+ <LaunchAction buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES">
228
+ <BuildableProductRunnable runnableDebuggingMode = "0">
229
+ <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "#{target.uuid}" BuildableName = "#{app_name}.app" BlueprintName = "#{app_name}" ReferencedContainer = "container:#{File.basename(project.path)}">
230
+ </BuildableReference>
231
+ </BuildableProductRunnable>
232
+ </LaunchAction>
233
+ <ProfileAction buildConfiguration = "Release" shouldUseLaunchSchemeArgsEnv = "YES" savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES">
234
+ <BuildableProductRunnable runnableDebuggingMode = "0">
235
+ <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "#{target.uuid}" BuildableName = "#{app_name}.app" BlueprintName = "#{app_name}" ReferencedContainer = "container:#{File.basename(project.path)}">
236
+ </BuildableReference>
237
+ </BuildableProductRunnable>
238
+ </ProfileAction>
239
+ </Scheme>
240
+ XML
241
+ RUBY
242
+
243
+ "$ROOT/scripts/install-ios-shim.sh" \
244
+ --app-root "$OUT" \
245
+ --scheme "$TEST_SCHEME" \
246
+ --test-target "$TEST_SCHEME" \
247
+ --project "ios/$APP_NAME.xcodeproj" \
248
+ --app-target "$APP_NAME" \
249
+ --bundle-id "$BUNDLE_ID" \
250
+ --test-bundle-id "$BUNDLE_ID.zmr-uitests" \
251
+ --deployment-target "$DEPLOYMENT_TARGET" \
252
+ --patch-xcodeproj
253
+
254
+ cp "$ROOT/examples/ios-smoke.json" "$OUT/.zmr/ios-smoke.json"
255
+ cp "$ROOT/examples/ios-shim-smoke.json" "$OUT/.zmr/ios-shim-smoke.json"
256
+
257
+ echo "created iOS demo app at $OUT"
258
+ echo "project: $PROJECT_PATH"
259
+ echo "app scheme: $APP_NAME"
260
+ echo "shim scheme: $TEST_SCHEME"
261
+ echo "bundle id: $BUNDLE_ID"