zeno-mobile-runner 0.2.15 → 0.2.16
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 +12 -0
- package/FEATURES.md +1 -1
- package/README.md +1 -1
- 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/protocol-fixtures/core-session.responses.jsonl +1 -1
- package/docs/protocol.md +10 -10
- package/package.json +1 -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/install-ios-shim.sh +39 -4
- package/shims/ios/README.md +3 -2
- package/shims/ios/ZMRShimUITestCase.swift +58 -17
- package/src/version.zig +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ All notable changes to Zeno Mobile Runner are tracked here.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.2.16 (2026-06-25)
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- iOS Expo dev-client custom-link recovery now reopens matched project entries
|
|
12
|
+
from observed launcher state instead of reporting blind coordinate taps as
|
|
13
|
+
accepted. Visible but non-hittable launcher rows are tapped by their matched
|
|
14
|
+
frame, keeping recovery selector/state based.
|
|
15
|
+
- Generated iOS shim commands now fingerprint shim inputs and preserve reusable
|
|
16
|
+
app/Pods build outputs. Reinstalls reuse unchanged `build-for-testing`
|
|
17
|
+
products, and stale cleanup removes only ZMR shim products/intermediates.
|
|
18
|
+
|
|
7
19
|
## 0.2.15 (2026-06-24)
|
|
8
20
|
|
|
9
21
|
### Fixed
|
package/FEATURES.md
CHANGED
|
@@ -142,7 +142,7 @@ state, and writes deterministic traces. It does not embed an LLM.
|
|
|
142
142
|
|
|
143
143
|
## Current Limitations
|
|
144
144
|
|
|
145
|
-
- Current release status is `0.2.
|
|
145
|
+
- Current release status is `0.2.16`, a public developer preview rather than
|
|
146
146
|
a production-stable `1.0.0`.
|
|
147
147
|
- Physical iOS log capture is still simulator-first. Physical iOS screenshots
|
|
148
148
|
are available when the XCTest/XCUIAutomation shim is configured.
|
package/README.md
CHANGED
|
@@ -199,7 +199,7 @@ comparisons against your current E2E tool, and multi-device matrices, see
|
|
|
199
199
|
Slow CI hardware can extend the generated iOS shim build timeout with
|
|
200
200
|
`ZMR_IOS_SHIM_BUILD_TIMEOUT_SECONDS`; `ZMR_IOS_SHIM_RESPONSE_TIMEOUT_SECONDS`
|
|
201
201
|
bounds each in-flight request, and `ZMR_IOS_SHIM_TIMEOUT_MS` remains the outer
|
|
202
|
-
process ceiling. Current release: `0.2.
|
|
202
|
+
process ceiling. Current release: `0.2.16` developer preview.
|
|
203
203
|
Protocol version: `2026-04-28`.
|
|
204
204
|
|
|
205
205
|
## Optional protocol clients
|
package/clients/kotlin/README.md
CHANGED
|
@@ -27,7 +27,7 @@ gradle -p clients/kotlin runFakeSession \
|
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
```kotlin
|
|
30
|
-
implementation(files("path/to/zeno-mobile-runner/clients/kotlin/build/libs/zmr-client-0.2.
|
|
30
|
+
implementation(files("path/to/zeno-mobile-runner/clients/kotlin/build/libs/zmr-client-0.2.16.jar"))
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
```kotlin
|
package/clients/rust/Cargo.lock
CHANGED
package/clients/rust/Cargo.toml
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
{"jsonrpc":"2.0","id":1,"result":{"name":"zmr","version":"0.2.
|
|
1
|
+
{"jsonrpc":"2.0","id":1,"result":{"name":"zmr","version":"0.2.16","protocolVersion":"2026-04-28","protocol":{"version":"2026-04-28","minimumCompatibleVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"},"platforms":["android","ios"],"platformSupport":{"android":{"status":"supported","deviceTypes":["emulator","physical"],"automation":["adb","uiautomator","android-shim"]},"ios":{"status":"supported","deviceTypes":["simulator","physical"],"automation":["simctl","devicectl","xctest-shim"],"physicalDevices":true}},"iosPreview":false,"transports":["stdio","tcp"],"methods":["runner.capabilities","device.list","session.create","session.close","app.install","app.launch","app.stop","app.openLink","app.clearState","observe.snapshot","observe.semanticSnapshot","ui.tap","ui.type","ui.eraseText","ui.hideKeyboard","ui.swipe","ui.pressBack","ui.scrollUntilVisible","wait.until","wait.any","wait.gone","assert.visible","assert.notVisible","assert.healthy","scenario.validate","trace.events","trace.explore","trace.discover","trace.explain","trace.export"]}}
|
|
2
2
|
{"jsonrpc":"2.0","id":2,"result":[{"serial":"fake-device-1","state":"device","ready":true}]}
|
|
3
3
|
{"jsonrpc":"2.0","id":3,"result":{"sessionId":"default"}}
|
|
4
4
|
{"jsonrpc":"2.0","id":4,"result":true}
|
package/docs/protocol.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
ZMR exposes newline-delimited JSON-RPC 2.0 over stdio or localhost TCP in v1. Each request is one JSON object followed by `\n`. Each response is one JSON object followed by `\n`.
|
|
4
4
|
|
|
5
|
-
Current runner version: `0.2.
|
|
5
|
+
Current runner version: `0.2.16`.
|
|
6
6
|
|
|
7
7
|
Current protocol version: `2026-04-28`.
|
|
8
8
|
|
|
@@ -47,7 +47,7 @@ and protocol versions. The response is covered by
|
|
|
47
47
|
`schemas/inspect-output.schema.json`:
|
|
48
48
|
|
|
49
49
|
```json
|
|
50
|
-
{"ok":true,"status":"ready","schemaVersion":1,"runnerVersion":"0.2.
|
|
50
|
+
{"ok":true,"status":"ready","schemaVersion":1,"runnerVersion":"0.2.16","protocolVersion":"2026-04-28","dir":".","configPath":".zmr/config.json","configExists":true,"agentInstructionsPath":".zmr/AGENTS.md","agentInstructionsExists":true,"platforms":[{"name":"android","enabled":true,"defaultDevice":"emulator-5554","smokeScenario":".zmr/android-smoke.json","smokeScenarioExists":true,"traceDir":"traces/zmr-android"},{"name":"ios","enabled":true,"defaultDevice":"booted","smokeScenario":".zmr/ios-smoke.json","smokeScenarioExists":true,"traceDir":"traces/zmr-ios"}],"recommendedCommands":["zmr doctor --strict --json --config .zmr/config.json","zmr schemas --json","zmr validate --json .zmr/android-smoke.json","zmr validate --json .zmr/ios-smoke.json","zmr serve --transport stdio --config .zmr/config.json --trace-dir traces/zmr-agent","zmr mcp --config .zmr/config.json --trace-dir traces/zmr-agent"],"claimsPolicy":["verify runs with trace evidence before making readiness claims","do not claim Flutter widget-tree inspection"],"limitations":["inspect is read-only and does not launch devices","autonomous crawling is not shipped; generate or edit scenarios for human review"]}
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
`zmr discover --from-trace <trace-dir> --out <scenario.json> --validate --json`
|
|
@@ -60,7 +60,7 @@ invent credentials, or commit files. The response is covered by
|
|
|
60
60
|
`schemas/discover-output.schema.json`:
|
|
61
61
|
|
|
62
62
|
```json
|
|
63
|
-
{"ok":true,"mode":"discover","schemaVersion":1,"runnerVersion":"0.2.
|
|
63
|
+
{"ok":true,"mode":"discover","schemaVersion":1,"runnerVersion":"0.2.16","protocolVersion":"2026-04-28","out":".zmr/discovered/replay-smoke.json","traceDir":"traces/zmr-agent","sourceSnapshot":"traces/zmr-agent/artifacts/snapshot-2.json","name":"draft from login smoke","appId":"com.example.mobiletest","selectorCount":2,"stepCount":6,"replay":{"enabled":true,"eventCount":4,"stepCount":3,"skippedEventCount":1},"warnings":["draft requires human review before commit"],"validated":true,"validation":{"ok":true,"path":".zmr/discovered/replay-smoke.json","name":"draft from login smoke","appId":"com.example.mobiletest","stepCount":6},"nextCommands":["zmr validate --json .zmr/discovered/replay-smoke.json","zmr run .zmr/discovered/replay-smoke.json --json --trace-dir traces/zmr-agent"]}
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
`zmr explore --from-trace <trace-dir> --out <scenario.json> --goal <goal>
|
|
@@ -71,7 +71,7 @@ launch devices, crawl the app, invent missing actions, discover credentials, or
|
|
|
71
71
|
commit files. The response is covered by `schemas/explore-output.schema.json`:
|
|
72
72
|
|
|
73
73
|
```json
|
|
74
|
-
{"ok":true,"mode":"explore","schemaVersion":1,"runnerVersion":"0.2.
|
|
74
|
+
{"ok":true,"mode":"explore","schemaVersion":1,"runnerVersion":"0.2.16","protocolVersion":"2026-04-28","goal":"find a stable login smoke","autonomous":false,"reviewRequired":true,"guardrails":["writes from existing trace evidence only","does not crawl the app","does not discover credentials or secrets","requires human review before commit"],"out":".zmr/discovered/login-smoke.json","traceDir":"traces/zmr-agent","sourceSnapshot":"traces/zmr-agent/artifacts/snapshot-2.json","name":"draft from login smoke","appId":"com.example.mobiletest","selectorCount":2,"stepCount":6,"replay":{"enabled":true,"eventCount":4,"stepCount":3,"skippedEventCount":1},"warnings":["draft requires human review before commit"],"validated":true,"validation":{"ok":true,"path":".zmr/discovered/login-smoke.json","name":"draft from login smoke","appId":"com.example.mobiletest","stepCount":6},"nextCommands":["zmr validate --json .zmr/discovered/login-smoke.json","zmr run .zmr/discovered/login-smoke.json --json --trace-dir traces/zmr-agent"]}
|
|
75
75
|
```
|
|
76
76
|
|
|
77
77
|
`zmr draft --from-trace <trace-dir> --out <scenario.json> --json` is the lower
|
|
@@ -84,7 +84,7 @@ into fields, or commit files. The response is covered by
|
|
|
84
84
|
`schemas/draft-output.schema.json`:
|
|
85
85
|
|
|
86
86
|
```json
|
|
87
|
-
{"ok":true,"mode":"draft","schemaVersion":1,"runnerVersion":"0.2.
|
|
87
|
+
{"ok":true,"mode":"draft","schemaVersion":1,"runnerVersion":"0.2.16","protocolVersion":"2026-04-28","out":".zmr/discovered/surface-smoke.json","traceDir":"traces/zmr-agent","sourceSnapshot":"traces/zmr-agent/artifacts/snapshot-2.json","name":"draft from login smoke","appId":"com.example.mobiletest","selectorCount":2,"stepCount":4,"replay":{"enabled":false,"eventCount":0,"stepCount":0,"skippedEventCount":0},"warnings":["draft requires human review before commit"],"nextCommands":["zmr validate --json .zmr/discovered/surface-smoke.json","zmr run .zmr/discovered/surface-smoke.json --json --trace-dir traces/zmr-agent"]}
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
`zmr draft --include-actions` additionally parses `events.jsonl` and prepends
|
|
@@ -214,7 +214,7 @@ installers, setup scripts, and generated clients. The response is covered by
|
|
|
214
214
|
`schemas/version-output.schema.json`:
|
|
215
215
|
|
|
216
216
|
```json
|
|
217
|
-
{"name":"zmr","version":"0.2.
|
|
217
|
+
{"name":"zmr","version":"0.2.16","protocolVersion":"2026-04-28","minimumCompatibleProtocolVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"}
|
|
218
218
|
```
|
|
219
219
|
|
|
220
220
|
## Capabilities Output Contract
|
|
@@ -226,7 +226,7 @@ and method inventory for JSON-RPC clients. The result object is covered by
|
|
|
226
226
|
iOS simulator, or physical iOS workflows are available.
|
|
227
227
|
|
|
228
228
|
```json
|
|
229
|
-
{"name":"zmr","version":"0.2.
|
|
229
|
+
{"name":"zmr","version":"0.2.16","protocolVersion":"2026-04-28","protocol":{"version":"2026-04-28","minimumCompatibleVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"},"platforms":["android","ios"],"platformSupport":{"android":{"status":"supported","deviceTypes":["emulator","physical"],"automation":["adb","uiautomator","android-shim"]},"ios":{"status":"supported","deviceTypes":["simulator","physical"],"automation":["simctl","devicectl","xctest-shim"],"physicalDevices":true}},"iosPreview":false,"transports":["stdio","tcp"],"methods":["runner.capabilities","device.list","session.create","session.close","app.install","app.launch","app.stop","app.openLink","app.clearState","observe.snapshot","observe.semanticSnapshot","ui.tap","ui.type","ui.eraseText","ui.hideKeyboard","ui.swipe","ui.pressBack","ui.scrollUntilVisible","wait.until","wait.any","wait.gone","assert.visible","assert.notVisible","assert.healthy","scenario.validate","trace.events","trace.explore","trace.discover","trace.explain","trace.export"]}
|
|
230
230
|
```
|
|
231
231
|
|
|
232
232
|
## Doctor Output Contract
|
|
@@ -433,7 +433,7 @@ Request:
|
|
|
433
433
|
Response:
|
|
434
434
|
|
|
435
435
|
```json
|
|
436
|
-
{"jsonrpc":"2.0","id":1,"result":{"name":"zmr","version":"0.2.
|
|
436
|
+
{"jsonrpc":"2.0","id":1,"result":{"name":"zmr","version":"0.2.16","protocolVersion":"2026-04-28","protocol":{"version":"2026-04-28","minimumCompatibleVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"},"platforms":["android","ios"],"platformSupport":{"android":{"status":"supported","deviceTypes":["emulator","physical"],"automation":["adb","uiautomator","android-shim"]},"ios":{"status":"supported","deviceTypes":["simulator","physical"],"automation":["simctl","devicectl","xctest-shim"],"physicalDevices":true}},"iosPreview":false,"transports":["stdio","tcp"],"methods":["runner.capabilities","device.list","session.create","session.close","app.install","app.launch","app.stop","app.openLink","app.clearState","observe.snapshot","observe.semanticSnapshot","ui.tap","ui.type","ui.eraseText","ui.hideKeyboard","ui.swipe","ui.pressBack","ui.scrollUntilVisible","wait.until","wait.any","wait.gone","assert.visible","assert.notVisible","assert.healthy","scenario.validate","trace.events","trace.explore","trace.discover","trace.explain","trace.export"]}}
|
|
437
437
|
```
|
|
438
438
|
|
|
439
439
|
### `trace.events`
|
|
@@ -515,7 +515,7 @@ Request:
|
|
|
515
515
|
Response:
|
|
516
516
|
|
|
517
517
|
```json
|
|
518
|
-
{"jsonrpc":"2.0","id":25,"result":{"ok":true,"mode":"discover","schemaVersion":1,"runnerVersion":"0.2.
|
|
518
|
+
{"jsonrpc":"2.0","id":25,"result":{"ok":true,"mode":"discover","schemaVersion":1,"runnerVersion":"0.2.16","protocolVersion":"2026-04-28","out":".zmr/discovered/agent-smoke.json","traceDir":"traces/agent-session","sourceSnapshot":"traces/agent-session/artifacts/snapshot-1.json","name":"agent smoke","appId":"com.example.mobiletest","selectorCount":1,"stepCount":4,"replay":{"enabled":true,"eventCount":2,"stepCount":1,"skippedEventCount":1},"warnings":["draft requires human review before commit"],"validated":true,"validation":{"ok":true,"path":".zmr/discovered/agent-smoke.json","name":"agent smoke","appId":"com.example.mobiletest","stepCount":4},"nextCommands":["zmr validate --json .zmr/discovered/agent-smoke.json","zmr run .zmr/discovered/agent-smoke.json --json --trace-dir traces/agent-session"]}}
|
|
519
519
|
```
|
|
520
520
|
|
|
521
521
|
Without `--trace-dir`, it returns `ok: false` with `traceDir: null`. Generated
|
|
@@ -538,7 +538,7 @@ Request:
|
|
|
538
538
|
Response:
|
|
539
539
|
|
|
540
540
|
```json
|
|
541
|
-
{"jsonrpc":"2.0","id":27,"result":{"ok":true,"mode":"explore","schemaVersion":1,"runnerVersion":"0.2.
|
|
541
|
+
{"jsonrpc":"2.0","id":27,"result":{"ok":true,"mode":"explore","schemaVersion":1,"runnerVersion":"0.2.16","protocolVersion":"2026-04-28","out":".zmr/discovered/agent-goal.json","traceDir":"traces/agent-session","sourceSnapshot":"traces/agent-session/artifacts/snapshot-1.json","name":"agent goal smoke","appId":"com.example.mobiletest","selectorCount":1,"stepCount":4,"replay":{"enabled":true,"eventCount":2,"stepCount":1,"skippedEventCount":1},"warnings":["draft requires human review before commit"],"validated":true,"validation":{"ok":true,"path":".zmr/discovered/agent-goal.json","name":"agent goal smoke","appId":"com.example.mobiletest","stepCount":4},"nextCommands":["zmr validate --json .zmr/discovered/agent-goal.json","zmr run .zmr/discovered/agent-goal.json --json --trace-dir traces/agent-session"],"goal":"find a stable login smoke","autonomous":false,"reviewRequired":true,"guardrails":["writes from existing trace evidence only","does not crawl the app","does not discover credentials or secrets","requires human review before commit"]}}
|
|
542
542
|
```
|
|
543
543
|
|
|
544
544
|
Without `--trace-dir`, it returns `ok: false` with `traceDir: null`.
|
package/package.json
CHANGED
|
Binary file
|
package/prebuilds/darwin-x64/zmr
CHANGED
|
Binary file
|
|
Binary file
|
package/prebuilds/linux-x64/zmr
CHANGED
|
Binary file
|
|
@@ -165,8 +165,28 @@ fi
|
|
|
165
165
|
|
|
166
166
|
mkdir -p "$APP_ROOT"
|
|
167
167
|
APP_ROOT="$(cd "$APP_ROOT" && pwd)"
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
SHIM_INSTALL_FINGERPRINT="$(
|
|
169
|
+
{
|
|
170
|
+
printf '%s\n' \
|
|
171
|
+
"$SCHEME" \
|
|
172
|
+
"$TEST_TARGET" \
|
|
173
|
+
"$TEST_BUNDLE_ID" \
|
|
174
|
+
"$WORKSPACE" \
|
|
175
|
+
"$PROJECT" \
|
|
176
|
+
"$APP_TARGET" \
|
|
177
|
+
"$BUNDLE_ID" \
|
|
178
|
+
"$DERIVED_DATA_PATH" \
|
|
179
|
+
"$DEVICE_TYPE" \
|
|
180
|
+
"$CONFIGURATION" \
|
|
181
|
+
"$DEPLOYMENT_TARGET"
|
|
182
|
+
shasum -a 256 \
|
|
183
|
+
"$ROOT/scripts/install-ios-shim.sh" \
|
|
184
|
+
"$ROOT/scripts/ensure-ios-shim-target.rb" \
|
|
185
|
+
"$ROOT/shims/ios/ZMRShim.swift" \
|
|
186
|
+
"$ROOT/shims/ios/ZMRShimUITestCase.swift"
|
|
187
|
+
} | shasum -a 256 | awk '{print $1}'
|
|
188
|
+
)"
|
|
189
|
+
mkdir -p "$APP_ROOT/.zmr" "$APP_ROOT/.zmr/shims/ios" "$APP_ROOT/.zmr/ios-shim-state"
|
|
170
190
|
rm -f "$APP_ROOT/.zmr/ios-shim-state/destination.id"
|
|
171
191
|
rm -f "$APP_ROOT/.zmr/ios-shim-state/xcodebuild.pid"
|
|
172
192
|
rm -f "$APP_ROOT/.zmr/ios-shim-state/xcodebuild.log"
|
|
@@ -222,8 +242,13 @@ PID_FILE="\$STATE_DIR/xcodebuild.pid"
|
|
|
222
242
|
READY_FILE="\$SERVER_DIR/ready"
|
|
223
243
|
DESTINATION_ID_FILE="\$STATE_DIR/destination.id"
|
|
224
244
|
BUILD_READY_FILE="\$STATE_DIR/build-for-testing.ready"
|
|
245
|
+
BUILD_FINGERPRINT_FILE="\$STATE_DIR/build-for-testing.fingerprint"
|
|
246
|
+
DERIVED_DATA_CLEAN_FINGERPRINT_FILE="\$STATE_DIR/derived-data-clean.fingerprint"
|
|
225
247
|
LOG_FILE="\$STATE_DIR/xcodebuild.log"
|
|
226
248
|
DERIVED_DATA_PATH_VALUE="$DERIVED_DATA_PATH"
|
|
249
|
+
INSTALL_FINGERPRINT_VALUE="$SHIM_INSTALL_FINGERPRINT"
|
|
250
|
+
ZMR_TEST_TARGET_NAME="$TEST_TARGET"
|
|
251
|
+
ZMR_SCHEME_NAME="$SCHEME"
|
|
227
252
|
STDIN_FILE="\$(mktemp)"
|
|
228
253
|
trap 'rm -f "\$STDIN_FILE"' EXIT
|
|
229
254
|
|
|
@@ -337,6 +362,9 @@ clean_zmr_derived_data() {
|
|
|
337
362
|
if [[ -z "\$DERIVED_DATA_PATH_VALUE" ]]; then
|
|
338
363
|
return 0
|
|
339
364
|
fi
|
|
365
|
+
if [[ "\${ZMR_IOS_SHIM_FORCE_REBUILD:-}" != "1" && -f "\$DERIVED_DATA_CLEAN_FINGERPRINT_FILE" ]] && [[ "\$(cat "\$DERIVED_DATA_CLEAN_FINGERPRINT_FILE" 2>/dev/null || true)" == "\$INSTALL_FINGERPRINT_VALUE" ]]; then
|
|
366
|
+
return 0
|
|
367
|
+
fi
|
|
340
368
|
|
|
341
369
|
local derived_data_abs
|
|
342
370
|
if [[ "\$DERIVED_DATA_PATH_VALUE" == /* ]]; then
|
|
@@ -350,7 +378,13 @@ clean_zmr_derived_data() {
|
|
|
350
378
|
|
|
351
379
|
case "\$derived_data_abs" in
|
|
352
380
|
"$APP_ROOT/ZMRDerivedData"|"$APP_ROOT"/*/ZMRDerivedData)
|
|
353
|
-
|
|
381
|
+
if [[ -d "\$derived_data_abs/Build/Products" ]]; then
|
|
382
|
+
find "\$derived_data_abs/Build/Products" -depth \\( -name "\$ZMR_TEST_TARGET_NAME*" -o -name "\$ZMR_SCHEME_NAME*" \\) -exec rm -rf {} +
|
|
383
|
+
fi
|
|
384
|
+
if [[ -d "\$derived_data_abs/Build/Intermediates.noindex" ]]; then
|
|
385
|
+
find "\$derived_data_abs/Build/Intermediates.noindex" -depth \\( -name "\$ZMR_TEST_TARGET_NAME.build" -o -name "\$ZMR_SCHEME_NAME.build" \\) -exec rm -rf {} +
|
|
386
|
+
fi
|
|
387
|
+
printf '%s\\n' "\$INSTALL_FINGERPRINT_VALUE" > "\$DERIVED_DATA_CLEAN_FINGERPRINT_FILE"
|
|
354
388
|
;;
|
|
355
389
|
*)
|
|
356
390
|
echo "warning: refusing to delete non-ZMR derived data path: \$DERIVED_DATA_PATH_VALUE" >&2
|
|
@@ -418,7 +452,7 @@ wait_for_ready() {
|
|
|
418
452
|
}
|
|
419
453
|
|
|
420
454
|
build_for_testing() {
|
|
421
|
-
if [[ "\${ZMR_IOS_SHIM_FORCE_REBUILD:-}" != "1" && -f "\$BUILD_READY_FILE" ]]; then
|
|
455
|
+
if [[ "\${ZMR_IOS_SHIM_FORCE_REBUILD:-}" != "1" && -f "\$BUILD_READY_FILE" && -f "\$BUILD_FINGERPRINT_FILE" ]] && [[ "\$(cat "\$BUILD_FINGERPRINT_FILE" 2>/dev/null || true)" == "\$INSTALL_FINGERPRINT_VALUE" ]]; then
|
|
422
456
|
return 0
|
|
423
457
|
fi
|
|
424
458
|
|
|
@@ -437,6 +471,7 @@ build_for_testing() {
|
|
|
437
471
|
ZMR_SHIM_SERVER_DIR="\$SERVER_DIR" \\
|
|
438
472
|
ZMR_APP_BUNDLE_ID="$BUNDLE_ID"
|
|
439
473
|
|
|
474
|
+
printf '%s\\n' "\$INSTALL_FINGERPRINT_VALUE" > "\$BUILD_FINGERPRINT_FILE"
|
|
440
475
|
touch "\$BUILD_READY_FILE"
|
|
441
476
|
}
|
|
442
477
|
|
package/shims/ios/README.md
CHANGED
|
@@ -21,8 +21,9 @@ Current status:
|
|
|
21
21
|
refresh the cached test bundle, or `ZMR_IOS_SHIM_ONESHOT=1` to force the
|
|
22
22
|
slower one-command XCTest fallback for debugging. When configured with a
|
|
23
23
|
ZMR-owned derived data path ending in `ZMRDerivedData`, the command removes
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
only stale shim target products and intermediates before a needed
|
|
25
|
+
`build-for-testing` refresh, preserves app and Pods build outputs, and
|
|
26
|
+
refuses to delete arbitrary shared DerivedData paths.
|
|
26
27
|
- The iOS adapter still uses `xcrun simctl` for simulator install, launch,
|
|
27
28
|
terminate, open link, screenshots, and logs. It uses `xcrun devicectl` for
|
|
28
29
|
physical-device lifecycle where Apple exposes a supported local command, and
|
|
@@ -2,6 +2,8 @@ import Foundation
|
|
|
2
2
|
import XCTest
|
|
3
3
|
|
|
4
4
|
final class ZMRShimUITestCase: XCTestCase {
|
|
5
|
+
private let expoDevClientRecoveryTimeout: TimeInterval = 10
|
|
6
|
+
|
|
5
7
|
func testRunZMRCommand() throws {
|
|
6
8
|
let environment = ProcessInfo.processInfo.environment
|
|
7
9
|
let app = makeApplication(bundleIdentifier: shimRuntimeValue("ZMR_APP_BUNDLE_ID", environment: environment))
|
|
@@ -318,6 +320,7 @@ final class ZMRShimUITestCase: XCTestCase {
|
|
|
318
320
|
|
|
319
321
|
if app.staticTexts["Deep link received:"].waitForExistence(timeout: 1) {
|
|
320
322
|
if tapFirstMatchingExpoCandidate(
|
|
323
|
+
app: app,
|
|
321
324
|
queries: [app.buttons, app.cells, app.staticTexts],
|
|
322
325
|
predicate: predicate
|
|
323
326
|
) {
|
|
@@ -328,12 +331,29 @@ final class ZMRShimUITestCase: XCTestCase {
|
|
|
328
331
|
if expoDevClientFallback,
|
|
329
332
|
isCustomSchemeURL(openedURL),
|
|
330
333
|
!isExpoDevClientURL(openedURL) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
334
|
+
return waitForExpoDevClientRecovery(app: app, deepLinkPredicate: predicate)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return (false, "")
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private func waitForExpoDevClientRecovery(
|
|
341
|
+
app: XCUIApplication,
|
|
342
|
+
deepLinkPredicate: NSPredicate
|
|
343
|
+
) -> (accepted: Bool, label: String) {
|
|
344
|
+
let deadline = Date().addingTimeInterval(expoDevClientRecoveryTimeout)
|
|
345
|
+
while Date() < deadline {
|
|
346
|
+
if app.staticTexts["Deep link received:"].exists,
|
|
347
|
+
tapExpoDevClientDeepLinkCandidateFallback(app: app, predicate: deepLinkPredicate) {
|
|
335
348
|
return (true, "expo-dev-client-deep-link-candidate")
|
|
336
349
|
}
|
|
350
|
+
|
|
351
|
+
let homeSelection = resumeExpoDevClientHome(app: app)
|
|
352
|
+
if homeSelection.accepted {
|
|
353
|
+
return homeSelection
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
Thread.sleep(forTimeInterval: 0.2)
|
|
337
357
|
}
|
|
338
358
|
|
|
339
359
|
return (false, "")
|
|
@@ -367,6 +387,7 @@ final class ZMRShimUITestCase: XCTestCase {
|
|
|
367
387
|
|
|
368
388
|
let predicate = NSPredicate(format: "label CONTAINS[c] %@ OR label CONTAINS[c] %@", " http://", " https://")
|
|
369
389
|
if tapFirstMatchingExpoCandidate(
|
|
390
|
+
app: app,
|
|
370
391
|
queries: [app.buttons, app.cells, app.staticTexts],
|
|
371
392
|
predicate: predicate
|
|
372
393
|
) {
|
|
@@ -399,6 +420,7 @@ final class ZMRShimUITestCase: XCTestCase {
|
|
|
399
420
|
}
|
|
400
421
|
|
|
401
422
|
private func tapFirstMatchingExpoCandidate(
|
|
423
|
+
app: XCUIApplication,
|
|
402
424
|
queries: [XCUIElementQuery],
|
|
403
425
|
predicate: NSPredicate
|
|
404
426
|
) -> Bool {
|
|
@@ -410,19 +432,44 @@ final class ZMRShimUITestCase: XCTestCase {
|
|
|
410
432
|
break
|
|
411
433
|
}
|
|
412
434
|
|
|
413
|
-
|
|
414
|
-
|
|
435
|
+
if tapMatchedExpoCandidate(element: element, app: app) {
|
|
436
|
+
return true
|
|
415
437
|
}
|
|
416
|
-
|
|
417
|
-
element.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
|
418
|
-
Thread.sleep(forTimeInterval: 1.0)
|
|
419
|
-
return true
|
|
420
438
|
}
|
|
421
439
|
}
|
|
422
440
|
|
|
423
441
|
return false
|
|
424
442
|
}
|
|
425
443
|
|
|
444
|
+
private func tapMatchedExpoCandidate(element: XCUIElement, app: XCUIApplication) -> Bool {
|
|
445
|
+
if element.isHittable {
|
|
446
|
+
element.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
|
447
|
+
Thread.sleep(forTimeInterval: 1.0)
|
|
448
|
+
return true
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
let visibleFrame = element.frame.intersection(app.frame)
|
|
452
|
+
guard !visibleFrame.isNull,
|
|
453
|
+
!visibleFrame.isEmpty,
|
|
454
|
+
app.frame.width > 0,
|
|
455
|
+
app.frame.height > 0 else {
|
|
456
|
+
return false
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
let normalizedX = (visibleFrame.midX - app.frame.minX) / app.frame.width
|
|
460
|
+
let normalizedY = (visibleFrame.midY - app.frame.minY) / app.frame.height
|
|
461
|
+
guard normalizedX >= 0,
|
|
462
|
+
normalizedX <= 1,
|
|
463
|
+
normalizedY >= 0,
|
|
464
|
+
normalizedY <= 1 else {
|
|
465
|
+
return false
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
app.coordinate(withNormalizedOffset: CGVector(dx: normalizedX, dy: normalizedY)).tap()
|
|
469
|
+
Thread.sleep(forTimeInterval: 1.0)
|
|
470
|
+
return true
|
|
471
|
+
}
|
|
472
|
+
|
|
426
473
|
private func isCustomSchemeURL(_ value: String?) -> Bool {
|
|
427
474
|
guard let value else {
|
|
428
475
|
return false
|
|
@@ -437,15 +484,9 @@ final class ZMRShimUITestCase: XCTestCase {
|
|
|
437
484
|
return value.hasPrefix("exp+") && value.contains("://expo-development-client/")
|
|
438
485
|
}
|
|
439
486
|
|
|
440
|
-
private func tapExpoDevClientDeepLinkCoordinateFallback(app: XCUIApplication) -> Bool {
|
|
441
|
-
Thread.sleep(forTimeInterval: 1.5)
|
|
442
|
-
app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.6)).tap()
|
|
443
|
-
Thread.sleep(forTimeInterval: 1.0)
|
|
444
|
-
return true
|
|
445
|
-
}
|
|
446
|
-
|
|
447
487
|
private func tapExpoDevClientDeepLinkCandidateFallback(app: XCUIApplication, predicate: NSPredicate) -> Bool {
|
|
448
488
|
tapFirstMatchingExpoCandidate(
|
|
489
|
+
app: app,
|
|
449
490
|
queries: [app.buttons, app.cells, app.staticTexts],
|
|
450
491
|
predicate: predicate
|
|
451
492
|
)
|
package/src/version.zig
CHANGED