zeno-mobile-runner 0.2.0 → 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.
- package/CHANGELOG.md +23 -0
- package/FEATURES.md +1 -1
- package/README.md +9 -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/examples/ios-dev-client-open-link.json +24 -13
- package/examples/ios-dev-client-route-snapshot.json +33 -8
- package/npm/scenarios.mjs +15 -8
- package/npm/wizard.mjs +1 -1
- 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/shims/ios/ZMRShimUITestCase.swift +7 -1
- package/src/ios.zig +7 -11
- package/src/main.zig +3 -0
- 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.1 (2026-06-10)
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- iOS simulator `openLink` now asks the XCTest shim to accept the SpringBoard
|
|
12
|
+
"Open in <App>?" confirmation for custom URL schemes too, not just
|
|
13
|
+
http/https universal links. Custom schemes are the common Expo dev-client
|
|
14
|
+
deep-link case (`exp+scheme://expo-development-client/...`), and the
|
|
15
|
+
unaccepted dialog previously blocked navigation entirely. The shim's
|
|
16
|
+
`acceptSystemAlert` also gained a single alert-existence probe so the
|
|
17
|
+
best-effort accept stays fast when no dialog appears.
|
|
18
|
+
- The generated Expo dev-client scenarios no longer pass when only the Expo
|
|
19
|
+
dev launcher rendered. The old `waitAny` markers also matched launcher
|
|
20
|
+
chrome ("Home", "Continue", "Sign in"), so runs exited green even though
|
|
21
|
+
the app's JS bundle never loaded. The scenarios now wait for the launcher's
|
|
22
|
+
persistent marker to be gone (`waitNotVisible` on "evelopment servers",
|
|
23
|
+
covering both case-sensitive spellings) — passing immediately when the deep
|
|
24
|
+
link navigates, and failing when the launcher is stuck — then assert no
|
|
25
|
+
bundle-error screen ("Unable to load" / "There was a problem loading") is
|
|
26
|
+
visible before `assertHealthy` and `snapshot`. Verified both directions
|
|
27
|
+
against a real Expo SDK 56 app: passes in ~24s with Metro serving, fails
|
|
28
|
+
with a wait timeout when the bundler is down.
|
|
29
|
+
|
|
7
30
|
## 0.2.0 (2026-06-10)
|
|
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.1`, 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
|
@@ -86,6 +86,14 @@ Hook it up to your coding agent (Claude Code shown; any MCP client works):
|
|
|
86
86
|
claude mcp add zmr -- npx zmr mcp --config .zmr/config.json --trace-dir traces/zmr-agent
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
Claude Code users can instead install the plugin, which bundles the MCP server
|
|
90
|
+
and a mobile-testing skill:
|
|
91
|
+
|
|
92
|
+
```text
|
|
93
|
+
/plugin marketplace add johnmikel/zeno-mobile-runner
|
|
94
|
+
/plugin install zmr@zmr-marketplace
|
|
95
|
+
```
|
|
96
|
+
|
|
89
97
|
Or in an `.mcp.json` / MCP client config:
|
|
90
98
|
|
|
91
99
|
```json
|
|
@@ -189,7 +197,7 @@ comparisons against your current E2E tool, and multi-device matrices, see
|
|
|
189
197
|
| Cloud device farms | Not included | ZMR focuses on local and self-managed device targets in this preview |
|
|
190
198
|
|
|
191
199
|
Slow CI hardware can extend the iOS shim cold-build timeout with
|
|
192
|
-
`ZMR_IOS_SHIM_TIMEOUT_MS`. Current release: `0.2.
|
|
200
|
+
`ZMR_IOS_SHIM_TIMEOUT_MS`. Current release: `0.2.1` developer preview.
|
|
193
201
|
Protocol version: `2026-04-28`.
|
|
194
202
|
|
|
195
203
|
## 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.1.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.1","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.1`.
|
|
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.1","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.1","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.1","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.1","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.1","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.1","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
|
|
@@ -432,7 +432,7 @@ Request:
|
|
|
432
432
|
Response:
|
|
433
433
|
|
|
434
434
|
```json
|
|
435
|
-
{"jsonrpc":"2.0","id":1,"result":{"name":"zmr","version":"0.2.
|
|
435
|
+
{"jsonrpc":"2.0","id":1,"result":{"name":"zmr","version":"0.2.1","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"]}}
|
|
436
436
|
```
|
|
437
437
|
|
|
438
438
|
### `trace.events`
|
|
@@ -514,7 +514,7 @@ Request:
|
|
|
514
514
|
Response:
|
|
515
515
|
|
|
516
516
|
```json
|
|
517
|
-
{"jsonrpc":"2.0","id":25,"result":{"ok":true,"mode":"discover","schemaVersion":1,"runnerVersion":"0.2.
|
|
517
|
+
{"jsonrpc":"2.0","id":25,"result":{"ok":true,"mode":"discover","schemaVersion":1,"runnerVersion":"0.2.1","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"]}}
|
|
518
518
|
```
|
|
519
519
|
|
|
520
520
|
Without `--trace-dir`, it returns `ok: false` with `traceDir: null`. Generated
|
|
@@ -537,7 +537,7 @@ Request:
|
|
|
537
537
|
Response:
|
|
538
538
|
|
|
539
539
|
```json
|
|
540
|
-
{"jsonrpc":"2.0","id":27,"result":{"ok":true,"mode":"explore","schemaVersion":1,"runnerVersion":"0.2.
|
|
540
|
+
{"jsonrpc":"2.0","id":27,"result":{"ok":true,"mode":"explore","schemaVersion":1,"runnerVersion":"0.2.1","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"]}}
|
|
541
541
|
```
|
|
542
542
|
|
|
543
543
|
Without `--trace-dir`, it returns `ok: false` with `traceDir: null`.
|
|
@@ -2,25 +2,36 @@
|
|
|
2
2
|
"name": "iOS Expo dev-client open-link smoke",
|
|
3
3
|
"appId": "com.example.mobiletest",
|
|
4
4
|
"steps": [
|
|
5
|
-
{
|
|
5
|
+
{
|
|
6
|
+
"action": "stop"
|
|
7
|
+
},
|
|
6
8
|
{
|
|
7
9
|
"action": "openLink",
|
|
8
10
|
"url": "exp+mobiletest://expo-development-client/?url=http%3A%2F%2F127.0.0.1%3A8081"
|
|
9
11
|
},
|
|
10
12
|
{
|
|
11
|
-
"action": "
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
{ "textContains": "Reload" },
|
|
16
|
-
{ "textContains": "Continue" },
|
|
17
|
-
{ "textContains": "Sign in" },
|
|
18
|
-
{ "textContains": "Home" },
|
|
19
|
-
{ "textContains": "Unable to load" }
|
|
20
|
-
],
|
|
13
|
+
"action": "waitNotVisible",
|
|
14
|
+
"selector": {
|
|
15
|
+
"textContains": "evelopment servers"
|
|
16
|
+
},
|
|
21
17
|
"timeoutMs": 120000
|
|
22
18
|
},
|
|
23
|
-
{
|
|
24
|
-
|
|
19
|
+
{
|
|
20
|
+
"action": "assertNoneVisible",
|
|
21
|
+
"selectors": [
|
|
22
|
+
{
|
|
23
|
+
"textContains": "Unable to load"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"textContains": "There was a problem loading"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"action": "assertHealthy"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"action": "snapshot"
|
|
35
|
+
}
|
|
25
36
|
]
|
|
26
37
|
}
|
|
@@ -2,23 +2,48 @@
|
|
|
2
2
|
"name": "iOS Expo dev-client route snapshot",
|
|
3
3
|
"appId": "com.example.mobiletest",
|
|
4
4
|
"steps": [
|
|
5
|
-
{
|
|
5
|
+
{
|
|
6
|
+
"action": "stop"
|
|
7
|
+
},
|
|
6
8
|
{
|
|
7
9
|
"action": "openLink",
|
|
8
10
|
"url": "exampleapp:///settings?mode=manage&source=zmr"
|
|
9
11
|
},
|
|
12
|
+
{
|
|
13
|
+
"action": "waitNotVisible",
|
|
14
|
+
"selector": {
|
|
15
|
+
"textContains": "evelopment servers"
|
|
16
|
+
},
|
|
17
|
+
"timeoutMs": 60000
|
|
18
|
+
},
|
|
10
19
|
{
|
|
11
20
|
"action": "waitAny",
|
|
12
21
|
"selectors": [
|
|
13
|
-
{
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
{
|
|
17
|
-
|
|
22
|
+
{
|
|
23
|
+
"textContains": "Settings"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"textContains": "Manage"
|
|
27
|
+
}
|
|
18
28
|
],
|
|
19
29
|
"timeoutMs": 60000
|
|
20
30
|
},
|
|
21
|
-
{
|
|
22
|
-
|
|
31
|
+
{
|
|
32
|
+
"action": "assertNoneVisible",
|
|
33
|
+
"selectors": [
|
|
34
|
+
{
|
|
35
|
+
"textContains": "Unable to load"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"textContains": "There was a problem loading"
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"action": "assertHealthy"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"action": "snapshot"
|
|
47
|
+
}
|
|
23
48
|
]
|
|
24
49
|
}
|
package/npm/scenarios.mjs
CHANGED
|
@@ -64,6 +64,14 @@ export function scenarioFiles(appId, { android = true, ios = true, expoDevClient
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
export function devClientScenario(name, appId, scheme, metroUrl) {
|
|
67
|
+
// The Expo dev-launcher chrome shares tokens with common app copy ("Home",
|
|
68
|
+
// "Continue", "Sign in"), and bundle-loading overlays ("Downloading") can
|
|
69
|
+
// flash faster than a snapshot poll, so neither can prove the app's JS
|
|
70
|
+
// bundle loaded. Instead wait for the launcher's persistent marker to be
|
|
71
|
+
// GONE: "evelopment servers" matches both "Development servers" and "No
|
|
72
|
+
// development servers found" (matching is case-sensitive), passes
|
|
73
|
+
// immediately when the deep link navigates without showing the launcher,
|
|
74
|
+
// and times out — failing the run — when the launcher is stuck on screen.
|
|
67
75
|
return {
|
|
68
76
|
name,
|
|
69
77
|
appId,
|
|
@@ -74,17 +82,16 @@ export function devClientScenario(name, appId, scheme, metroUrl) {
|
|
|
74
82
|
url: `exp+${scheme}://expo-development-client/?url=${encodeURIComponent(metroUrl)}`,
|
|
75
83
|
},
|
|
76
84
|
{
|
|
77
|
-
action: "
|
|
85
|
+
action: "waitNotVisible",
|
|
86
|
+
selector: { textContains: "evelopment servers" },
|
|
87
|
+
timeoutMs: 120000,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
action: "assertNoneVisible",
|
|
78
91
|
selectors: [
|
|
79
|
-
{ textContains: "Downloading" },
|
|
80
|
-
{ textContains: "Connected to:" },
|
|
81
|
-
{ textContains: "Reload" },
|
|
82
|
-
{ textContains: "Continue" },
|
|
83
|
-
{ textContains: "Sign in" },
|
|
84
|
-
{ textContains: "Home" },
|
|
85
92
|
{ textContains: "Unable to load" },
|
|
93
|
+
{ textContains: "There was a problem loading" },
|
|
86
94
|
],
|
|
87
|
-
timeoutMs: 120000,
|
|
88
95
|
},
|
|
89
96
|
{ action: "assertHealthy" },
|
|
90
97
|
{ action: "snapshot" },
|
package/npm/wizard.mjs
CHANGED
package/package.json
CHANGED
|
Binary file
|
package/prebuilds/darwin-x64/zmr
CHANGED
|
Binary file
|
|
Binary file
|
package/prebuilds/linux-x64/zmr
CHANGED
|
Binary file
|
|
@@ -241,10 +241,16 @@ final class ZMRShimUITestCase: XCTestCase {
|
|
|
241
241
|
var acceptedCount = 0
|
|
242
242
|
var lastAcceptedLabel = ""
|
|
243
243
|
for _ in 0..<3 {
|
|
244
|
+
// One existence probe on the alert container keeps the no-dialog
|
|
245
|
+
// path to a single short wait instead of a per-label wait, so the
|
|
246
|
+
// best-effort accept after every openLink stays cheap.
|
|
247
|
+
guard springboard.alerts.firstMatch.waitForExistence(timeout: 2) else {
|
|
248
|
+
break
|
|
249
|
+
}
|
|
244
250
|
var tapped = false
|
|
245
251
|
for label in labels {
|
|
246
252
|
let button = springboard.buttons[label].firstMatch
|
|
247
|
-
if button.
|
|
253
|
+
if button.exists, button.isHittable {
|
|
248
254
|
button.tap()
|
|
249
255
|
acceptedCount += 1
|
|
250
256
|
lastAcceptedLabel = label
|
package/src/ios.zig
CHANGED
|
@@ -128,9 +128,13 @@ pub const IosDevice = struct {
|
|
|
128
128
|
const result = try self.runSimctl(&.{ "openurl", self.target(), url }, default_max_output);
|
|
129
129
|
defer result.deinit(self.allocator);
|
|
130
130
|
try result.ensureSuccess();
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
// Opening a URL on the simulator can raise a SpringBoard "Open in <App>?"
|
|
132
|
+
// confirmation for universal links (http/https) and, just as often, for
|
|
133
|
+
// custom schemes — the common Expo dev-client case
|
|
134
|
+
// (exp+scheme://expo-development-client/...). Attempt a best-effort accept
|
|
135
|
+
// whenever a shim is configured; the shim probes briefly and returns fast
|
|
136
|
+
// when no dialog is present, so this stays cheap on the no-prompt path.
|
|
137
|
+
self.acceptOpenURLConfirmationBestEffort();
|
|
134
138
|
}
|
|
135
139
|
|
|
136
140
|
pub fn tap(self: *IosDevice, x: i32, y: i32) !void {
|
|
@@ -406,14 +410,6 @@ fn parseShimTimeoutMs(raw: ?[]const u8) u64 {
|
|
|
406
410
|
return parsed;
|
|
407
411
|
}
|
|
408
412
|
|
|
409
|
-
fn urlMayNeedOpenConfirmation(url: []const u8) bool {
|
|
410
|
-
return startsWithIgnoreCase(url, "http://") or startsWithIgnoreCase(url, "https://");
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
fn startsWithIgnoreCase(value: []const u8, prefix: []const u8) bool {
|
|
414
|
-
return value.len >= prefix.len and std.ascii.eqlIgnoreCase(value[0..prefix.len], prefix);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
413
|
pub fn listDevices(allocator: std.mem.Allocator, xcrun_path: []const u8) ![]types.DeviceInfo {
|
|
418
414
|
return try ios_devices.listSimulators(allocator, xcrun_path);
|
|
419
415
|
}
|
package/src/main.zig
CHANGED
|
@@ -16,6 +16,9 @@ const errors = @import("errors.zig");
|
|
|
16
16
|
|
|
17
17
|
pub fn main() void {
|
|
18
18
|
mainInner() catch |err| {
|
|
19
|
+
// stdout's consumer went away (e.g. `zmr ... | head`); exit quietly
|
|
20
|
+
// with the conventional SIGPIPE status instead of reporting an error.
|
|
21
|
+
if (err == error.BrokenPipe) std.process.exit(141);
|
|
19
22
|
writeTopLevelError(err);
|
|
20
23
|
std.process.exit(exitCodeForError(err));
|
|
21
24
|
};
|
package/src/version.zig
CHANGED