@devicelab/maestro-runner-darwin-arm64 1.1.29 → 1.1.31
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/README.md +15 -4
- package/bin/maestro-runner +0 -0
- package/drivers/android/devicelab-android-driver-test.apk +0 -0
- package/drivers/android/devicelab-android-driver.apk +0 -0
- package/drivers/ios/DevicelabIOSRunner/DevicelabIOSRunnerUITests/ATTRIBUTION.md +16 -0
- package/drivers/ios/DevicelabIOSRunner/DevicelabIOSRunnerUITests/RunnerTests+Lifecycle.swift +33 -0
- package/drivers/ios/DevicelabIOSRunner/DevicelabIOSRunnerUITests/RunnerTests+Snapshot.swift +195 -88
- package/drivers/ios/DevicelabIOSRunner/DevicelabIOSRunnerUITests/RunnerTests.swift +3 -0
- package/drivers/ios/WebDriverAgent/CHANGELOG.md +202 -0
- package/drivers/ios/WebDriverAgent/README.md +18 -9
- package/drivers/ios/WebDriverAgent/Scripts/ci/build-real.sh +6 -10
- package/drivers/ios/WebDriverAgent/Scripts/fetch-prebuilt-wda.mjs +3 -2
- package/drivers/ios/WebDriverAgent/WebDriverAgent.xcodeproj/project.pbxproj +119 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.m +8 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h +3 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m +30 -1
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.h +53 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.m +124 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h +6 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m +100 -23
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBHinge.h +41 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBHinge.m +215 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.m +8 -3
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBFind.m +20 -7
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.m +110 -50
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBTyping.h +7 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBTyping.m +20 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.m +14 -11
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Commands/FBCustomCommands.m +86 -10
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Commands/FBElementCommands.m +57 -7
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Commands/FBSessionCommands.m +3 -2
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Info.plist +2 -2
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Routing/FBHTTPServer.m +39 -16
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Routing/FBScreenRecordingRequest.m +7 -2
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Routing/FBSession.h +22 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Routing/FBSession.m +78 -12
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Routing/FBTCPSocket.h +5 -1
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Routing/FBTCPSocket.m +10 -1
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Routing/FBWebServer.m +4 -1
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.h +2 -2
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.m +20 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBConfiguration.h +8 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBConfiguration.m +2 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBHIDOperations.h +41 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBHIDOperations.m +244 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBImageProcessor.m +3 -1
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBMathUtils.h +11 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBMathUtils.m +38 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBMjpegServer.m +5 -3
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBScreen.h +17 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBScreen.m +76 -1
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBScreenshot.h +2 -2
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBScreenshot.m +6 -2
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBSettings.h +2 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBSettings.m +2 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBSettingsHandler.m +48 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.m +121 -13
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBXPath.m +181 -128
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/LRUCache/LRUCache.m +8 -5
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/LRUCache/LRUCacheNode.h +1 -1
- package/drivers/ios/WebDriverAgent/WebDriverAgentLib/WebDriverAgentLib.h +1 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationApp/COORDINATE_PROBE.md +3 -1
- package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBCoordinateProbeViewController.m +18 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/SceneDelegate.m +33 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/ViewController.m +26 -7
- package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBConfigurationTests.m +52 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBDisplayGestureTests.m +541 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBElementSwipingTests.m +95 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBImageProcessorTests.m +34 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBScreenTests.m +392 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBSessionIntegrationTests.m +97 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBTypingTest.m +33 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBW3CTouchActionsIntegrationTests.m +126 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIApplicationHelperTests.m +111 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIDeviceHelperTests.m +97 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIDeviceRotationTests.m +53 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIElementFBFindTests.m +113 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationTests_tvOS/FBTVFocusIntegrationTests.m +21 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationTests_watchOS/WDAAppLifecycleIntegrationTests.swift +12 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentTests/UnitTests/FBCustomCommandsTests.m +61 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentTests/UnitTests/FBElementUtilitiesTests.m +40 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentTests/UnitTests/FBHTTPServerSessionTests.m +22 -4
- package/drivers/ios/WebDriverAgent/WebDriverAgentTests/UnitTests/FBHTTPServerTests.m +199 -5
- package/drivers/ios/WebDriverAgent/WebDriverAgentTests/UnitTests/FBLRUCacheTests.m +34 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentTests/UnitTests/FBMathUtilsTests.m +45 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentTests/UnitTests/FBSessionTests.m +201 -0
- package/drivers/ios/WebDriverAgent/WebDriverAgentTests/UnitTests/FBXPathTests.m +101 -0
- package/drivers/ios/WebDriverAgent/docs/hinge-angle.md +54 -0
- package/drivers/ios/WebDriverAgent/docs/multiple-displays.md +168 -0
- package/drivers/ios/WebDriverAgent/lib/webdriveragent.ts +1 -2
- package/drivers/ios/WebDriverAgent/lib/xcodebuild.ts +58 -10
- package/drivers/ios/WebDriverAgent/package.json +8 -15
- package/drivers/ios/WebDriverAgent/test/functional/helpers/display-settings.ts +49 -0
- package/drivers/ios/WebDriverAgent/test/functional/helpers/simulator.ts +7 -7
- package/drivers/ios/WebDriverAgent/test/functional/webdriveragent-e2e.spec.ts +4 -0
- package/drivers/ios/WebDriverAgent/test/unit/xcodebuild.spec.ts +230 -0
- package/drivers/ios/WebDriverAgent/tsconfig.json +2 -1
- package/drivers/ios/devicelab-ios-agent/README.md +12 -0
- package/drivers/ios/devicelab-ios-agent/device/DevicelabIOSAgent.app/DevicelabIOSAgent +0 -0
- package/drivers/ios/devicelab-ios-agent/device/DevicelabIOSAgentUITests-Runner.app/PlugIns/DevicelabIOSAgentUITests.xctest/DevicelabIOSAgentUITests +0 -0
- package/drivers/ios/devicelab-ios-agent/device/manifest.json +3 -3
- package/drivers/ios/devicelab-ios-agent/simulator/DevicelabIOSAgent.app/DevicelabIOSAgent +0 -0
- package/drivers/ios/devicelab-ios-agent/simulator/DevicelabIOSAgentUITests-Runner.app/PlugIns/DevicelabIOSAgentUITests.xctest/DevicelabIOSAgentUITests +0 -0
- package/drivers/ios/devicelab-ios-agent/simulator/manifest.json +3 -3
- package/package.json +1 -1
- package/drivers/ios/WebDriverAgent/.editorconfig +0 -11
- package/drivers/ios/WebDriverAgent/.github/ISSUE_TEMPLATE.md +0 -4
- package/drivers/ios/WebDriverAgent/.github/actions/xcode-setup/action.yml +0 -32
- package/drivers/ios/WebDriverAgent/.github/dependabot.yml +0 -22
- package/drivers/ios/WebDriverAgent/.github/workflows/functional-test.yml +0 -103
- package/drivers/ios/WebDriverAgent/.github/workflows/pr-title.yml +0 -10
- package/drivers/ios/WebDriverAgent/.github/workflows/publish.js.yml +0 -112
- package/drivers/ios/WebDriverAgent/.github/workflows/unit-test.yml +0 -32
- package/drivers/ios/WebDriverAgent/.github/workflows/wda-package.yml +0 -114
- package/drivers/ios/WebDriverAgent/.github/workflows/wda-tests.yml +0 -246
- package/drivers/ios/WebDriverAgent/oxfmt.config.mjs +0 -6
- package/drivers/ios/WebDriverAgent/oxlint.config.mjs +0 -6
- package/drivers/ios/WebDriverAgent/release.config.mjs +0 -14
package/README.md
CHANGED
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
[](https://www.npmjs.com/package/maestro-runner)
|
|
21
21
|
[](https://github.com/devicelab-dev/maestro-runner/actions/workflows/ci.yml)
|
|
22
22
|
[](https://codecov.io/gh/devicelab-dev/maestro-runner)
|
|
23
|
-
[](https://github.com/devicelab-dev/maestro-runner/releases)
|
|
24
|
+
[](https://open.devicelab.dev/maestro-runner/docs/getting-started)
|
|
25
|
+
[](https://open.devicelab.dev/maestro-runner/docs/getting-started)
|
|
24
26
|
|
|
25
27
|
<b><a href="https://open.devicelab.dev/maestro-runner">Documentation</a></b> | <b><a href="#install">Get Started</a></b> | <b><a href="https://open.devicelab.dev/maestro-runner/docs/cli-reference">CLI Reference</a></b> | <b><a href="https://open.devicelab.dev/maestro-runner/docs/flow-commands">Flow Commands</a></b> | <b><a href="CONTRIBUTING.md">Contributing</a></b>
|
|
26
28
|
|
|
@@ -75,6 +77,14 @@ maestro-runner --driver appium --appium-url <server-url> test flow.yaml # Appium
|
|
|
75
77
|
maestro-runner test --parallel 3 flows/ # Parallel on 3 devices
|
|
76
78
|
```
|
|
77
79
|
|
|
80
|
+
## AI coding agents
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
maestro-runner skills install # writes ~/.claude/skills/maestro-runner/SKILL.md
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Installs a skill that tells Claude Code (and other agents that read `SKILL.md`) how to write Maestro flows, run them with maestro-runner, inspect the screen and read the reports. `--dir` writes it elsewhere (`.claude/skills/maestro-runner` in a repository), `--print` prints it. The file carries the binary's version and is replaced when you install from a newer release.
|
|
87
|
+
|
|
78
88
|
## Key Features
|
|
79
89
|
|
|
80
90
|
- **Zero migration** — Runs your existing Maestro YAML flows as-is, no changes needed
|
|
@@ -94,7 +104,7 @@ maestro-runner test --parallel 3 flows/ # Parall
|
|
|
94
104
|
- **scrollUntilVisible** — Native scroll implementation that reliably finds off-screen elements
|
|
95
105
|
- **Relative selectors** — Find elements by position: below, above, leftOf, rightOf, childOf
|
|
96
106
|
- **JavaScript scripting** — Embedded JS runtime with HTTP client for dynamic test logic, no external dependencies
|
|
97
|
-
- **Configurable timeouts** — Per-command and per-flow timeouts, `--wait-for-idle-timeout 0` to disable
|
|
107
|
+
- **Configurable timeouts** — Per-command and per-flow timeouts, `--find-timeout 3000` to make a wrong selector fail in 3 s instead of 17 s, `--wait-for-idle-timeout 0` to disable
|
|
98
108
|
- **Lightweight** — Single binary, no JVM required
|
|
99
109
|
|
|
100
110
|
## Supported Platforms & Drivers
|
|
@@ -104,7 +114,7 @@ maestro-runner test --parallel 3 flows/ # Parall
|
|
|
104
114
|
| **DeviceLab** | Android | Default since v1.1.28. On-device WebSocket driver, ~2x faster than UIAutomator2. |
|
|
105
115
|
| **UIAutomator2** | Android | `--driver uiautomator2`. Direct connection to the device, no external server needed. |
|
|
106
116
|
| **DeviceLab** | iOS | Default since v1.1.28. The DeviceLab iOS agent, on simulators and real iPhones (real iPhones need `--team-id`). `--driver devicelab-legacy` keeps the previous runner. |
|
|
107
|
-
| **WDA (WebDriverAgent)** | iOS | `--driver wda`. Supports simulators and physical devices. |
|
|
117
|
+
| **WDA (WebDriverAgent)** | iOS | `--driver wda`. Supports simulators and physical devices. `--reuse-wda` leaves WebDriverAgent running between runs and reuses it. |
|
|
108
118
|
| **Browser (CDP)** | Web | `--platform web`. Chrome/Chromium automation via Chrome DevTools Protocol. |
|
|
109
119
|
| **Appium** | Android & iOS | `--driver appium`. For cloud testing providers and existing Appium infrastructure. |
|
|
110
120
|
|
|
@@ -195,7 +205,8 @@ Turn it off with `MAESTRO_RUNNER_NO_TELEMETRY=1` or `DO_NOT_TRACK=1`. Set
|
|
|
195
205
|
`MAESTRO_RUNNER_TELEMETRY_DEBUG=1` to print the event instead of sending it.
|
|
196
206
|
[Telemetry](https://open.devicelab.dev/maestro-runner/docs/telemetry) lists every field.
|
|
197
207
|
|
|
198
|
-
The update check is separate
|
|
208
|
+
The update check is separate: `MAESTRO_RUNNER_NO_UPDATE_CHECK=1` turns it off,
|
|
209
|
+
and on CI the notice is not printed.
|
|
199
210
|
|
|
200
211
|
## Contributing
|
|
201
212
|
|
package/bin/maestro-runner
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -63,6 +63,22 @@ The only intentional local edits are:
|
|
|
63
63
|
60 → 240 chars/sec (matches WDA's `FBTypeText` default). RN inputs
|
|
64
64
|
accept this rate without dropping characters.
|
|
65
65
|
|
|
66
|
+
8. `RunnerTests+Snapshot.swift` collapsed tab fallback: ported from
|
|
67
|
+
upstream's fix for wide sliders wedging the runner (agent-device #3776,
|
|
68
|
+
commit 92db564a3, MIT), adapted to this file. The fallback reads its
|
|
69
|
+
candidates in one predicate query instead of every descendant one
|
|
70
|
+
attribute at a time, starts that query only while the snapshot's 8 s
|
|
71
|
+
processing budget still covers what the root snapshot took (otherwise the
|
|
72
|
+
snapshot reports `truncated`), and no longer treats a slider as a
|
|
73
|
+
collapsed container. Candidate `hittable` comes from geometry instead of
|
|
74
|
+
a per-element XCTest call.
|
|
75
|
+
9. `RunnerTests+Lifecycle.swift` `activateTarget`: when the target app was
|
|
76
|
+
in the background, wait (at most 2 s) for SpringBoard to go quiescent
|
|
77
|
+
after `activate()`. The idea comes from WebDriverAgent's
|
|
78
|
+
`-[XCUIApplication fb_activate]` (appium/appium-ios PR #82, Apache License
|
|
79
|
+
2.0): after a resume the next tap could report success without reaching
|
|
80
|
+
the app.
|
|
81
|
+
|
|
66
82
|
The wire-protocol contract (`RunnerTests+Models.swift` Command/Response shapes,
|
|
67
83
|
HTTP/JSON transport in `RunnerTests+Transport.swift`, dispatch behaviour in
|
|
68
84
|
`RunnerTests+CommandExecution.swift`) is **agent-device's contract** — the
|
package/drivers/ios/DevicelabIOSRunner/DevicelabIOSRunnerUITests/RunnerTests+Lifecycle.swift
CHANGED
|
@@ -237,14 +237,47 @@ extension RunnerTests {
|
|
|
237
237
|
target.state.rawValue,
|
|
238
238
|
reason
|
|
239
239
|
)
|
|
240
|
+
let resuming = isBackgrounded(target.state)
|
|
240
241
|
// activate avoids terminating and relaunching the target app
|
|
241
242
|
target.activate()
|
|
243
|
+
if resuming {
|
|
244
|
+
waitForSpringBoardAfterResume()
|
|
245
|
+
}
|
|
242
246
|
currentApp = target
|
|
243
247
|
currentBundleId = bundleId
|
|
244
248
|
needsFirstInteractionDelay = true
|
|
245
249
|
return target
|
|
246
250
|
}
|
|
247
251
|
|
|
252
|
+
/// True for an app that is running but not on screen: activating it is a
|
|
253
|
+
/// resume, not a launch.
|
|
254
|
+
func isBackgrounded(_ state: XCUIApplication.State) -> Bool {
|
|
255
|
+
#if os(iOS)
|
|
256
|
+
return state == .runningBackground || state == .runningBackgroundSuspended
|
|
257
|
+
#else
|
|
258
|
+
return false
|
|
259
|
+
#endif
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/// Waits, capped at resumeSpringBoardIdleTimeout, for SpringBoard to go
|
|
263
|
+
/// idle with its animations done. After a backgrounded app is brought back
|
|
264
|
+
/// the app itself can already look idle while SpringBoard is still
|
|
265
|
+
/// finishing the switch, and the next tap was reported as performed but
|
|
266
|
+
/// never reached the app. Waiting on SpringBoard, only on this resume path,
|
|
267
|
+
/// is the fix WebDriverAgent took (appium/appium-ios#82, Apache-2.0).
|
|
268
|
+
func waitForSpringBoardAfterResume() {
|
|
269
|
+
let started = ProcessInfo.processInfo.systemUptime
|
|
270
|
+
let outcome = RunnerXCTestTimeouts.waitForQuiescence(
|
|
271
|
+
of: springboard,
|
|
272
|
+
timeout: resumeSpringBoardIdleTimeout
|
|
273
|
+
)
|
|
274
|
+
NSLog(
|
|
275
|
+
"AGENT_DEVICE_RUNNER_RESUME_WAIT outcome=%d waited_ms=%.0f",
|
|
276
|
+
outcome.rawValue,
|
|
277
|
+
(ProcessInfo.processInfo.systemUptime - started) * 1000
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
248
281
|
func withTemporaryScrollIdleTimeoutIfSupported(
|
|
249
282
|
_ target: XCUIApplication,
|
|
250
283
|
operation: () -> Void
|
|
@@ -20,6 +20,13 @@ extension RunnerTests {
|
|
|
20
20
|
// captureRootSnapshot retries a slow root snapshot at XCTest's full
|
|
21
21
|
// timeout, so this bound cannot cut a large tree short.
|
|
22
22
|
private static let snapshotRequestTimeout: TimeInterval = 3.0
|
|
23
|
+
// How long one fast snapshot may run before the collapsed tab fallback's
|
|
24
|
+
// query no longer starts (agent-device's tree slice, #3776). XCTest cannot
|
|
25
|
+
// cancel a query once it runs on the main thread, so the check is made
|
|
26
|
+
// before it starts.
|
|
27
|
+
private static let collapsedTabProcessingBudget: TimeInterval = 8.0
|
|
28
|
+
// The least time a query is admitted with (agent-device's sweep budget).
|
|
29
|
+
private static let minimumQueryBudget: TimeInterval = 0.1
|
|
23
30
|
|
|
24
31
|
private struct SnapshotTraversalContext {
|
|
25
32
|
// The app actually read. A no-bundle snapshot can move to the app in
|
|
@@ -27,6 +34,9 @@ extension RunnerTests {
|
|
|
27
34
|
let app: XCUIApplication
|
|
28
35
|
let queryRoot: XCUIElement
|
|
29
36
|
let rootSnapshot: XCUIElementSnapshot
|
|
37
|
+
// How long reading rootSnapshot took: what one more query over the
|
|
38
|
+
// whole tree is expected to cost.
|
|
39
|
+
let rootReadDuration: TimeInterval
|
|
30
40
|
let viewport: CGRect
|
|
31
41
|
let occlusion: OcclusionIndex
|
|
32
42
|
let maxDepth: Int
|
|
@@ -173,6 +183,7 @@ extension RunnerTests {
|
|
|
173
183
|
}
|
|
174
184
|
|
|
175
185
|
func snapshotFast(app requested: XCUIApplication, options: SnapshotOptions) -> Response {
|
|
186
|
+
let processingDeadline = Date().addingTimeInterval(Self.collapsedTabProcessingBudget)
|
|
176
187
|
if let blocking = blockingSystemAlertSnapshot() {
|
|
177
188
|
return Response(ok: true, data: blocking)
|
|
178
189
|
}
|
|
@@ -192,20 +203,22 @@ extension RunnerTests {
|
|
|
192
203
|
}
|
|
193
204
|
let app = context.app
|
|
194
205
|
|
|
195
|
-
var
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
206
|
+
var nodes: [SnapshotNode] = []
|
|
207
|
+
var truncated = false
|
|
208
|
+
|
|
209
|
+
// Read at most once per snapshot, and only when a collapsed container
|
|
210
|
+
// asks. A query that could not start leaves the tree marked truncated.
|
|
211
|
+
var cachedCollapsedTabCandidates: [CollapsedTabCandidate]?
|
|
212
|
+
func collapsedTabCandidatesOnce() -> [CollapsedTabCandidate] {
|
|
213
|
+
if let cachedCollapsedTabCandidates {
|
|
214
|
+
return cachedCollapsedTabCandidates
|
|
202
215
|
}
|
|
203
|
-
|
|
204
|
-
|
|
216
|
+
let candidates = collapsedTabCandidates(context: context, processingDeadline: processingDeadline)
|
|
217
|
+
if candidates == nil { truncated = true }
|
|
218
|
+
cachedCollapsedTabCandidates = candidates ?? []
|
|
219
|
+
return candidates ?? []
|
|
205
220
|
}
|
|
206
221
|
|
|
207
|
-
var nodes: [SnapshotNode] = []
|
|
208
|
-
var truncated = false
|
|
209
222
|
let rootEvaluation = evaluateSnapshot(context.rootSnapshot, in: context)
|
|
210
223
|
nodes.append(
|
|
211
224
|
makeSnapshotNode(
|
|
@@ -220,7 +233,8 @@ extension RunnerTests {
|
|
|
220
233
|
let didTruncateFallback = appendCollapsedTabFallbackNodes(
|
|
221
234
|
to: &nodes,
|
|
222
235
|
containerSnapshot: context.rootSnapshot,
|
|
223
|
-
|
|
236
|
+
resolveCandidates: collapsedTabCandidatesOnce,
|
|
237
|
+
viewport: context.viewport,
|
|
224
238
|
depth: 1,
|
|
225
239
|
parentIndex: 0,
|
|
226
240
|
nodeLimit: fastSnapshotLimit
|
|
@@ -301,7 +315,8 @@ extension RunnerTests {
|
|
|
301
315
|
let didTruncateFallback = appendCollapsedTabFallbackNodes(
|
|
302
316
|
to: &nodes,
|
|
303
317
|
containerSnapshot: snapshot,
|
|
304
|
-
|
|
318
|
+
resolveCandidates: collapsedTabCandidatesOnce,
|
|
319
|
+
viewport: context.viewport,
|
|
305
320
|
depth: visibleDepth + 1,
|
|
306
321
|
parentIndex: index,
|
|
307
322
|
nodeLimit: nodeLimit
|
|
@@ -504,6 +519,7 @@ extension RunnerTests {
|
|
|
504
519
|
options: SnapshotOptions
|
|
505
520
|
) -> SnapshotTraversalContext? {
|
|
506
521
|
var app = requested
|
|
522
|
+
var readStarted = Date()
|
|
507
523
|
var captured = captureRootSnapshot(app: app, options: options)
|
|
508
524
|
if captured == nil, options.followsScreen, app.state != .runningForeground {
|
|
509
525
|
// The app left the screen while its snapshot was in flight. A
|
|
@@ -512,18 +528,21 @@ extension RunnerTests {
|
|
|
512
528
|
let front = foregroundTarget(app)
|
|
513
529
|
if front !== app {
|
|
514
530
|
app = front
|
|
531
|
+
readStarted = Date()
|
|
515
532
|
captured = captureRootSnapshot(app: app, options: options)
|
|
516
533
|
}
|
|
517
534
|
}
|
|
518
535
|
guard let (queryRoot, rootSnapshot) = captured else {
|
|
519
536
|
return nil
|
|
520
537
|
}
|
|
538
|
+
let rootReadDuration = Date().timeIntervalSince(readStarted)
|
|
521
539
|
|
|
522
540
|
let viewport = snapshotViewport(app: app, rootSnapshot: queryRoot === app ? rootSnapshot : nil)
|
|
523
541
|
return SnapshotTraversalContext(
|
|
524
542
|
app: app,
|
|
525
543
|
queryRoot: queryRoot,
|
|
526
544
|
rootSnapshot: rootSnapshot,
|
|
545
|
+
rootReadDuration: rootReadDuration,
|
|
527
546
|
viewport: viewport,
|
|
528
547
|
occlusion: OcclusionIndex(root: rootSnapshot, viewport: viewport, isOccluding: isOccludingType),
|
|
529
548
|
maxDepth: options.depth ?? Int.max
|
|
@@ -634,7 +653,12 @@ extension RunnerTests {
|
|
|
634
653
|
}
|
|
635
654
|
|
|
636
655
|
private func snapshotValueText(_ snapshot: XCUIElementSnapshot) -> String? {
|
|
637
|
-
|
|
656
|
+
Self.valueText(snapshot.value)
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/// An element's value as a node publishes it; blank reads as no value.
|
|
660
|
+
private static func valueText(_ value: Any?) -> String? {
|
|
661
|
+
guard let value else { return nil }
|
|
638
662
|
let text = String(describing: value).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
639
663
|
return text.isEmpty ? nil : text
|
|
640
664
|
}
|
|
@@ -687,14 +711,16 @@ extension RunnerTests {
|
|
|
687
711
|
private func appendCollapsedTabFallbackNodes(
|
|
688
712
|
to nodes: inout [SnapshotNode],
|
|
689
713
|
containerSnapshot: XCUIElementSnapshot,
|
|
690
|
-
|
|
714
|
+
resolveCandidates: () -> [CollapsedTabCandidate],
|
|
715
|
+
viewport: CGRect,
|
|
691
716
|
depth: Int,
|
|
692
717
|
parentIndex: Int,
|
|
693
718
|
nodeLimit: Int
|
|
694
719
|
) -> Bool {
|
|
695
720
|
let fallbackNodes = collapsedTabFallbackNodes(
|
|
696
721
|
for: containerSnapshot,
|
|
697
|
-
|
|
722
|
+
resolveCandidates: resolveCandidates,
|
|
723
|
+
viewport: viewport,
|
|
698
724
|
startingIndex: nodes.count,
|
|
699
725
|
depth: depth,
|
|
700
726
|
parentIndex: parentIndex
|
|
@@ -706,26 +732,131 @@ extension RunnerTests {
|
|
|
706
732
|
return fallbackNodes.count > remaining
|
|
707
733
|
}
|
|
708
734
|
|
|
735
|
+
/// One live element a collapsed tab container may adopt, as the matching
|
|
736
|
+
/// query's predicate read it.
|
|
737
|
+
private struct CollapsedTabCandidate {
|
|
738
|
+
let type: XCUIElement.ElementType
|
|
739
|
+
let frame: CGRect
|
|
740
|
+
let label: String
|
|
741
|
+
let identifier: String
|
|
742
|
+
let valueText: String?
|
|
743
|
+
let enabled: Bool
|
|
744
|
+
let focused: Bool
|
|
745
|
+
let selected: Bool
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Ported from agent-device #3776 (MIT): a wide slider qualified for this
|
|
749
|
+
// fallback, which then read every live descendant of the app one attribute
|
|
750
|
+
// at a time on the XCTest main thread — one round trip each, which nothing
|
|
751
|
+
// can cancel — so a crowded screen kept the runner busy until it was
|
|
752
|
+
// treated as wedged. Candidates now come from one query whose predicate
|
|
753
|
+
// applies the type, geometry and content rules and copies each match's
|
|
754
|
+
// attributes as XCTest evaluates it, and the query starts only while the
|
|
755
|
+
// time left before processingDeadline covers what the root snapshot took.
|
|
756
|
+
// nil means it did not start.
|
|
757
|
+
private func collapsedTabCandidates(
|
|
758
|
+
context: SnapshotTraversalContext,
|
|
759
|
+
processingDeadline: Date
|
|
760
|
+
) -> [CollapsedTabCandidate]? {
|
|
761
|
+
let containerFrames = Self.snapshotDescendants(of: context.rootSnapshot)
|
|
762
|
+
.filter { isCollapsedTabContainer($0) }
|
|
763
|
+
.map(\.frame)
|
|
764
|
+
if containerFrames.isEmpty { return [] }
|
|
765
|
+
let expectedCost = max(Self.minimumQueryBudget, context.rootReadDuration)
|
|
766
|
+
guard processingDeadline.timeIntervalSinceNow >= expectedCost else {
|
|
767
|
+
NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_TAB_FALLBACK_QUERY_NOT_STARTED")
|
|
768
|
+
return nil
|
|
769
|
+
}
|
|
770
|
+
var candidates: [CollapsedTabCandidate] = []
|
|
771
|
+
let predicate = NSPredicate { object, _ in
|
|
772
|
+
guard let attributes = object as? XCUIElementAttributes,
|
|
773
|
+
Self.hasCollapsedTabContent(attributes),
|
|
774
|
+
containerFrames.contains(where: {
|
|
775
|
+
Self.isCollapsedTabCandidate(type: attributes.elementType, frame: attributes.frame, in: $0)
|
|
776
|
+
})
|
|
777
|
+
else {
|
|
778
|
+
return false
|
|
779
|
+
}
|
|
780
|
+
candidates.append(self.collapsedTabCandidate(attributes))
|
|
781
|
+
return true
|
|
782
|
+
}
|
|
783
|
+
let matches = safeSnapshotElementsQuery {
|
|
784
|
+
context.queryRoot.descendants(matching: .any).matching(predicate).allElementsBoundByIndex
|
|
785
|
+
}
|
|
786
|
+
return matches.isEmpty ? [] : candidates
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
private func collapsedTabCandidate(_ attributes: XCUIElementAttributes) -> CollapsedTabCandidate {
|
|
790
|
+
var focused = false
|
|
791
|
+
_ = RunnerObjCExceptionCatcher.catchException({
|
|
792
|
+
if let value = (attributes as? NSObject)?.value(forKey: "hasFocus") as? Bool {
|
|
793
|
+
focused = value
|
|
794
|
+
}
|
|
795
|
+
})
|
|
796
|
+
return CollapsedTabCandidate(
|
|
797
|
+
type: attributes.elementType,
|
|
798
|
+
frame: attributes.frame,
|
|
799
|
+
label: attributes.label.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
800
|
+
identifier: attributes.identifier.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
801
|
+
valueText: Self.valueText(attributes.value),
|
|
802
|
+
enabled: attributes.isEnabled,
|
|
803
|
+
focused: focused,
|
|
804
|
+
selected: attributes.isSelected
|
|
805
|
+
)
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/// Every descendant of `root` in its captured snapshot, without `root`.
|
|
809
|
+
private static func snapshotDescendants(of root: XCUIElementSnapshot) -> [XCUIElementSnapshot] {
|
|
810
|
+
var descendants: [XCUIElementSnapshot] = []
|
|
811
|
+
var stack = Array(root.children.reversed())
|
|
812
|
+
while let snapshot = stack.popLast() {
|
|
813
|
+
descendants.append(snapshot)
|
|
814
|
+
stack.append(contentsOf: snapshot.children.reversed())
|
|
815
|
+
}
|
|
816
|
+
return descendants
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/// A collapsed tab container's item: a candidate type, smaller than the
|
|
820
|
+
/// container, and centred inside it.
|
|
821
|
+
private static func isCollapsedTabCandidate(
|
|
822
|
+
type: XCUIElement.ElementType,
|
|
823
|
+
frame: CGRect,
|
|
824
|
+
in containerFrame: CGRect
|
|
825
|
+
) -> Bool {
|
|
826
|
+
guard collapsedTabCandidateTypes.contains(type), !frame.isNull, !frame.isEmpty else {
|
|
827
|
+
return false
|
|
828
|
+
}
|
|
829
|
+
if frame.equalTo(containerFrame) { return false }
|
|
830
|
+
let area = max(CGFloat(1), frame.width * frame.height)
|
|
831
|
+
let containerArea = max(CGFloat(1), containerFrame.width * containerFrame.height)
|
|
832
|
+
return area < containerArea * 0.9 && containerFrame.contains(CGPoint(x: frame.midX, y: frame.midY))
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/// Whether an element names something a flow can address: a label,
|
|
836
|
+
/// identifier or value that is not blank.
|
|
837
|
+
private static func hasCollapsedTabContent(_ attributes: XCUIElementAttributes) -> Bool {
|
|
838
|
+
[attributes.label, attributes.identifier]
|
|
839
|
+
.contains { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
|
|
840
|
+
|| valueText(attributes.value) != nil
|
|
841
|
+
}
|
|
842
|
+
|
|
709
843
|
private func collapsedTabFallbackNodes(
|
|
710
844
|
for containerSnapshot: XCUIElementSnapshot,
|
|
711
|
-
|
|
845
|
+
resolveCandidates: () -> [CollapsedTabCandidate],
|
|
846
|
+
viewport: CGRect,
|
|
712
847
|
startingIndex: Int,
|
|
713
848
|
depth: Int,
|
|
714
849
|
parentIndex: Int
|
|
715
850
|
) -> [SnapshotNode] {
|
|
716
|
-
|
|
717
|
-
guard shouldExpandCollapsedTabContainer(containerSnapshot) else { return [] }
|
|
851
|
+
guard isCollapsedTabContainer(containerSnapshot) else { return [] }
|
|
718
852
|
let containerFrame = containerSnapshot.frame
|
|
719
|
-
if containerFrame.isNull || containerFrame.isEmpty { return [] }
|
|
720
853
|
|
|
721
|
-
|
|
722
|
-
// snapshot as a fallback for XCTest omitting the tab children from the snapshot tree.
|
|
723
|
-
let elements = resolveElements()
|
|
724
|
-
let candidates = elements.compactMap { element in
|
|
854
|
+
let candidates = resolveCandidates().compactMap { candidate in
|
|
725
855
|
collapsedTabCandidateNode(
|
|
726
|
-
|
|
856
|
+
candidate: candidate,
|
|
727
857
|
containerSnapshot: containerSnapshot,
|
|
728
|
-
containerFrame: containerFrame
|
|
858
|
+
containerFrame: containerFrame,
|
|
859
|
+
viewport: viewport
|
|
729
860
|
)
|
|
730
861
|
}
|
|
731
862
|
.sorted { left, right in
|
|
@@ -772,64 +903,41 @@ extension RunnerTests {
|
|
|
772
903
|
}
|
|
773
904
|
|
|
774
905
|
private func collapsedTabCandidateNode(
|
|
775
|
-
|
|
906
|
+
candidate: CollapsedTabCandidate,
|
|
776
907
|
containerSnapshot: XCUIElementSnapshot,
|
|
777
|
-
containerFrame: CGRect
|
|
908
|
+
containerFrame: CGRect,
|
|
909
|
+
viewport: CGRect
|
|
778
910
|
) -> SnapshotNode? {
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
let containerArea = max(CGFloat(1), containerFrame.width * containerFrame.height)
|
|
789
|
-
if area >= containerArea * 0.9 { return }
|
|
790
|
-
let center = CGPoint(x: frame.midX, y: frame.midY)
|
|
791
|
-
if !containerFrame.contains(center) { return }
|
|
792
|
-
|
|
793
|
-
let label = element.label.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
794
|
-
let identifier = element.identifier.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
795
|
-
let valueText = snapshotValueText(element)
|
|
796
|
-
let hasContent = !label.isEmpty || !identifier.isEmpty || valueText != nil
|
|
797
|
-
if !hasContent { return }
|
|
798
|
-
if sameSemanticElement(
|
|
799
|
-
containerSnapshot: containerSnapshot,
|
|
800
|
-
elementType: elementType,
|
|
801
|
-
label: label,
|
|
802
|
-
identifier: identifier
|
|
803
|
-
) {
|
|
804
|
-
return
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
node = SnapshotNode(
|
|
808
|
-
index: 0,
|
|
809
|
-
type: elementTypeName(elementType),
|
|
810
|
-
label: label.isEmpty ? nil : label,
|
|
811
|
-
identifier: identifier.isEmpty ? nil : identifier,
|
|
812
|
-
value: valueText,
|
|
813
|
-
placeholderValue: nil,
|
|
814
|
-
rect: snapshotRect(from: frame),
|
|
815
|
-
enabled: element.isEnabled,
|
|
816
|
-
focused: elementHasFocus(element) ? true : nil,
|
|
817
|
-
selected: element.isSelected ? true : nil,
|
|
818
|
-
hittable: element.isHittable,
|
|
819
|
-
depth: 0,
|
|
820
|
-
parentIndex: nil,
|
|
821
|
-
hiddenContentAbove: nil,
|
|
822
|
-
hiddenContentBelow: nil
|
|
823
|
-
)
|
|
824
|
-
})
|
|
825
|
-
if let exceptionMessage {
|
|
826
|
-
NSLog(
|
|
827
|
-
"AGENT_DEVICE_RUNNER_SNAPSHOT_TAB_FALLBACK_IGNORED_EXCEPTION=%@",
|
|
828
|
-
exceptionMessage
|
|
829
|
-
)
|
|
911
|
+
if !Self.isCollapsedTabCandidate(type: candidate.type, frame: candidate.frame, in: containerFrame) {
|
|
912
|
+
return nil
|
|
913
|
+
}
|
|
914
|
+
if sameSemanticElement(
|
|
915
|
+
containerSnapshot: containerSnapshot,
|
|
916
|
+
elementType: candidate.type,
|
|
917
|
+
label: candidate.label,
|
|
918
|
+
identifier: candidate.identifier
|
|
919
|
+
) {
|
|
830
920
|
return nil
|
|
831
921
|
}
|
|
832
|
-
return
|
|
922
|
+
return SnapshotNode(
|
|
923
|
+
index: 0,
|
|
924
|
+
type: elementTypeName(candidate.type),
|
|
925
|
+
label: candidate.label.isEmpty ? nil : candidate.label,
|
|
926
|
+
identifier: candidate.identifier.isEmpty ? nil : candidate.identifier,
|
|
927
|
+
value: candidate.valueText,
|
|
928
|
+
placeholderValue: nil,
|
|
929
|
+
rect: snapshotRect(from: candidate.frame),
|
|
930
|
+
enabled: candidate.enabled,
|
|
931
|
+
focused: candidate.focused ? true : nil,
|
|
932
|
+
selected: candidate.selected ? true : nil,
|
|
933
|
+
// Read from geometry: asking XCTest would be one more round trip per
|
|
934
|
+
// item, the cost this fallback no longer pays.
|
|
935
|
+
hittable: candidate.enabled && isVisibleInViewport(candidate.frame, viewport),
|
|
936
|
+
depth: 0,
|
|
937
|
+
parentIndex: nil,
|
|
938
|
+
hiddenContentAbove: nil,
|
|
939
|
+
hiddenContentBelow: nil
|
|
940
|
+
)
|
|
833
941
|
}
|
|
834
942
|
|
|
835
943
|
private func snapshotHasFocus(_ snapshot: XCUIElementSnapshot) -> Bool {
|
|
@@ -846,24 +954,23 @@ extension RunnerTests {
|
|
|
846
954
|
return snapshot.isSelected
|
|
847
955
|
}
|
|
848
956
|
|
|
849
|
-
|
|
957
|
+
/// A wide tab bar or segmented control that XCTest reports without
|
|
958
|
+
/// children. Sliders no longer qualify (agent-device #3776): a wide slider
|
|
959
|
+
/// has no items to adopt, and qualifying sent every screen with one
|
|
960
|
+
/// through the descendant query.
|
|
961
|
+
private func isCollapsedTabContainer(_ snapshot: XCUIElementSnapshot) -> Bool {
|
|
962
|
+
if !snapshot.children.isEmpty { return false }
|
|
850
963
|
let frame = snapshot.frame
|
|
851
964
|
if frame.isNull || frame.isEmpty { return false }
|
|
852
965
|
if frame.width < max(CGFloat(160), frame.height * 1.75) { return false }
|
|
853
966
|
switch snapshot.elementType {
|
|
854
|
-
case .tabBar, .segmentedControl
|
|
967
|
+
case .tabBar, .segmentedControl:
|
|
855
968
|
return true
|
|
856
969
|
default:
|
|
857
970
|
return false
|
|
858
971
|
}
|
|
859
972
|
}
|
|
860
973
|
|
|
861
|
-
private func snapshotValueText(_ element: XCUIElement) -> String? {
|
|
862
|
-
let text = String(describing: element.value ?? "")
|
|
863
|
-
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
864
|
-
return text.isEmpty ? nil : text
|
|
865
|
-
}
|
|
866
|
-
|
|
867
974
|
private func sameSemanticElement(
|
|
868
975
|
containerSnapshot: XCUIElementSnapshot,
|
|
869
976
|
elementType: XCUIElement.ElementType,
|
|
@@ -58,6 +58,9 @@ final class RunnerTests: XCTestCase {
|
|
|
58
58
|
// a caller polling snapshots during settle paid it on each following
|
|
59
59
|
// interaction.
|
|
60
60
|
let firstInteractionAfterActivateDelay: TimeInterval = 0.1
|
|
61
|
+
// The most a resume (activating a backgrounded app) waits for SpringBoard
|
|
62
|
+
// to go idle. WebDriverAgent saw lost taps with no wait and none with 2 s.
|
|
63
|
+
let resumeSpringBoardIdleTimeout: TimeInterval = 2.0
|
|
61
64
|
// The idle command's cap when the caller gives none, and the most it may
|
|
62
65
|
// ask for: well under mainThreadExecutionTimeout, so a wait can never be
|
|
63
66
|
// mistaken for a hung main thread.
|