zeno-mobile-runner 0.2.14 → 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 +23 -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/app-integration.md +4 -0
- package/docs/npm.md +4 -1
- package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
- package/docs/protocol.md +10 -10
- package/docs/troubleshooting.md +4 -2
- package/package.json +2 -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 +65 -3
- package/shims/ios/README.md +5 -1
- package/shims/ios/ZMRShimUITestCase.swift +58 -17
- package/src/version.zig +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ 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
|
+
|
|
19
|
+
## 0.2.15 (2026-06-24)
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Generated iOS shim commands now remove app-local ZMR-owned derived data paths
|
|
24
|
+
ending in `ZMRDerivedData` before `build-for-testing` refreshes. This avoids
|
|
25
|
+
reusing stale Xcode absolute paths when app checkouts are copied, while
|
|
26
|
+
refusing to delete arbitrary shared DerivedData locations.
|
|
27
|
+
- Release gates now verify that `ZMR_VERSION`, `package.json`, `src/version.zig`,
|
|
28
|
+
archive names, and release-smoked binaries agree before publishing.
|
|
29
|
+
|
|
7
30
|
## 0.2.14 (2026-06-23)
|
|
8
31
|
|
|
9
32
|
### Added
|
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
package/docs/app-integration.md
CHANGED
|
@@ -101,6 +101,10 @@ with `--ios-shim`. It caches `build-for-testing` output and uses
|
|
|
101
101
|
`test-without-building` for selector commands through `.zmr/ios-shim-state/`.
|
|
102
102
|
Set `ZMR_IOS_SHIM_FORCE_REBUILD=1` after app-side target changes, or
|
|
103
103
|
`ZMR_IOS_SHIM_ONESHOT=1` when you need to debug the slower cold-start path.
|
|
104
|
+
When `--derived-data-path` points at a ZMR-owned path ending in
|
|
105
|
+
`ZMRDerivedData`, the generated shim removes that directory before each
|
|
106
|
+
`build-for-testing` refresh so copied app checkouts do not reuse stale absolute
|
|
107
|
+
Xcode paths. It refuses to delete arbitrary shared DerivedData locations.
|
|
104
108
|
|
|
105
109
|
## Recommended App Repo Layout
|
|
106
110
|
|
package/docs/npm.md
CHANGED
|
@@ -333,7 +333,10 @@ The generated command caches `build-for-testing` output under
|
|
|
333
333
|
prints the last Xcode log lines when XCTest fails. Set
|
|
334
334
|
`ZMR_IOS_SHIM_FORCE_REBUILD=1` after app-side target changes, or
|
|
335
335
|
`ZMR_IOS_SHIM_ONESHOT=1` for a cold-start fallback while debugging app-side Xcode
|
|
336
|
-
wiring.
|
|
336
|
+
wiring. When `--derived-data-path` points at a ZMR-owned path ending in
|
|
337
|
+
`ZMRDerivedData`, the generated command removes that directory before each
|
|
338
|
+
`build-for-testing` refresh so copied app checkouts do not reuse stale absolute
|
|
339
|
+
Xcode paths. It refuses to delete arbitrary shared DerivedData locations.
|
|
337
340
|
|
|
338
341
|
## Native Binary Resolution
|
|
339
342
|
|
|
@@ -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/docs/troubleshooting.md
CHANGED
|
@@ -208,8 +208,10 @@ This avoids failing the whole demo when one local simulator cannot start
|
|
|
208
208
|
`launchd_sim`, while still surfacing a setup error if every available simulator
|
|
209
209
|
fails to boot.
|
|
210
210
|
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
The generated shim now removes app-local ZMR derived data paths ending in
|
|
212
|
+
`ZMRDerivedData` before each `build-for-testing` refresh. If you intentionally
|
|
213
|
+
configured a different shared DerivedData path, clean only the app-local ZMR
|
|
214
|
+
cache you own, then rerun the shim once to prewarm it:
|
|
213
215
|
|
|
214
216
|
```bash
|
|
215
217
|
rm -rf ios/build/ZMRDerivedData
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zeno-mobile-runner",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
4
4
|
"description": "Agent-native mobile app test runner for React Native, Expo, Flutter, and native Android/iOS.",
|
|
5
5
|
"main": "npm/index.mjs",
|
|
6
6
|
"repository": {
|
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
"!scripts/release-candidate.sh",
|
|
83
83
|
"!scripts/release-gate.sh",
|
|
84
84
|
"!scripts/release-smoke.sh",
|
|
85
|
+
"!scripts/verify-release-version.sh",
|
|
85
86
|
"!scripts/sign-macos-release.sh",
|
|
86
87
|
"!scripts/verify-release-artifacts.sh",
|
|
87
88
|
"!scripts/__pycache__/",
|
|
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,7 +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"
|
|
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"
|
|
226
252
|
STDIN_FILE="\$(mktemp)"
|
|
227
253
|
trap 'rm -f "\$STDIN_FILE"' EXIT
|
|
228
254
|
|
|
@@ -332,6 +358,40 @@ destination_spec() {
|
|
|
332
358
|
printf 'platform=%s,id=%s' "\$platform_name" "\$destination_id"
|
|
333
359
|
}
|
|
334
360
|
|
|
361
|
+
clean_zmr_derived_data() {
|
|
362
|
+
if [[ -z "\$DERIVED_DATA_PATH_VALUE" ]]; then
|
|
363
|
+
return 0
|
|
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
|
|
368
|
+
|
|
369
|
+
local derived_data_abs
|
|
370
|
+
if [[ "\$DERIVED_DATA_PATH_VALUE" == /* ]]; then
|
|
371
|
+
derived_data_abs="\$DERIVED_DATA_PATH_VALUE"
|
|
372
|
+
else
|
|
373
|
+
derived_data_abs="$APP_ROOT/\$DERIVED_DATA_PATH_VALUE"
|
|
374
|
+
fi
|
|
375
|
+
while [[ "\$derived_data_abs" == */ && "\$derived_data_abs" != "/" ]]; do
|
|
376
|
+
derived_data_abs="\${derived_data_abs%/}"
|
|
377
|
+
done
|
|
378
|
+
|
|
379
|
+
case "\$derived_data_abs" in
|
|
380
|
+
"$APP_ROOT/ZMRDerivedData"|"$APP_ROOT"/*/ZMRDerivedData)
|
|
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"
|
|
388
|
+
;;
|
|
389
|
+
*)
|
|
390
|
+
echo "warning: refusing to delete non-ZMR derived data path: \$DERIVED_DATA_PATH_VALUE" >&2
|
|
391
|
+
;;
|
|
392
|
+
esac
|
|
393
|
+
}
|
|
394
|
+
|
|
335
395
|
is_server_running() {
|
|
336
396
|
if [[ ! -f "\$PID_FILE" ]]; then
|
|
337
397
|
return 1
|
|
@@ -392,13 +452,14 @@ wait_for_ready() {
|
|
|
392
452
|
}
|
|
393
453
|
|
|
394
454
|
build_for_testing() {
|
|
395
|
-
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
|
|
396
456
|
return 0
|
|
397
457
|
fi
|
|
398
458
|
|
|
399
459
|
local destination_id build_log
|
|
400
460
|
destination_id="\$(destination_spec)"
|
|
401
461
|
build_log="\$STATE_DIR/xcodebuild.build.log"
|
|
462
|
+
clean_zmr_derived_data
|
|
402
463
|
|
|
403
464
|
run_xcodebuild_with_timeout "iOS shim build-for-testing" "\${ZMR_IOS_SHIM_BUILD_TIMEOUT_SECONDS:-5400}" "\$build_log" \\
|
|
404
465
|
xcodebuild build-for-testing \\
|
|
@@ -410,6 +471,7 @@ build_for_testing() {
|
|
|
410
471
|
ZMR_SHIM_SERVER_DIR="\$SERVER_DIR" \\
|
|
411
472
|
ZMR_APP_BUNDLE_ID="$BUNDLE_ID"
|
|
412
473
|
|
|
474
|
+
printf '%s\\n' "\$INSTALL_FINGERPRINT_VALUE" > "\$BUILD_FINGERPRINT_FILE"
|
|
413
475
|
touch "\$BUILD_READY_FILE"
|
|
414
476
|
}
|
|
415
477
|
|
package/shims/ios/README.md
CHANGED
|
@@ -19,7 +19,11 @@ Current status:
|
|
|
19
19
|
selector commands through `test-without-building`, exchanging per-command
|
|
20
20
|
files under `.zmr/ios-shim-state/`. Set `ZMR_IOS_SHIM_FORCE_REBUILD=1` to
|
|
21
21
|
refresh the cached test bundle, or `ZMR_IOS_SHIM_ONESHOT=1` to force the
|
|
22
|
-
slower one-command XCTest fallback for debugging.
|
|
22
|
+
slower one-command XCTest fallback for debugging. When configured with a
|
|
23
|
+
ZMR-owned derived data path ending in `ZMRDerivedData`, the command removes
|
|
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.
|
|
23
27
|
- The iOS adapter still uses `xcrun simctl` for simulator install, launch,
|
|
24
28
|
terminate, open link, screenshots, and logs. It uses `xcrun devicectl` for
|
|
25
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