agent-device 0.18.2 → 0.19.0
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/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.18.2.apk → agent-device-android-multitouch-helper-0.19.0.apk} +0 -0
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.0.apk.sha256 +1 -0
- package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.18.2.manifest.json → agent-device-android-multitouch-helper-0.19.0.manifest.json} +4 -4
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.18.2.apk → agent-device-android-snapshot-helper-0.19.0.apk} +0 -0
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.19.0.apk.sha256 +1 -0
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.18.2.manifest.json → agent-device-android-snapshot-helper-0.19.0.manifest.json} +6 -6
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTapPointPolicy.swift +28 -0
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +9 -120
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +132 -66
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +155 -0
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +67 -0
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +20 -74
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +64 -0
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SequenceExecution.swift +0 -177
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +81 -48
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +102 -129
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +208 -46
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +82 -11
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +72 -0
- package/dist/src/agent-browser-tool.js +1 -0
- package/dist/src/android-adb.d.ts +64 -234
- package/dist/src/android-adb.js +1 -1
- package/dist/src/android.js +1 -5
- package/dist/src/app-lifecycle.js +7 -0
- package/dist/src/apple-runner-options.js +1 -0
- package/dist/src/apps.js +5 -11
- package/dist/src/artifacts.d.ts +4 -3
- package/dist/src/artifacts.js +1 -1
- package/dist/src/audio-probe.js +1 -1
- package/dist/src/batch.d.ts +2 -177
- package/dist/src/batch.js +1 -1
- package/dist/src/cli-help.js +64 -38
- package/dist/src/cli-output.js +1 -1
- package/dist/src/cli-runner.js +2 -0
- package/dist/src/cli.js +12 -41
- package/dist/src/client-metro-companion.js +1 -0
- package/dist/src/client.js +3 -0
- package/dist/src/command-metadata.js +1 -0
- package/dist/src/command-schema.js +22 -0
- package/dist/src/command-surface.js +1 -1
- package/dist/src/contracts.d.ts +2 -213
- package/dist/src/contracts.js +1 -1
- package/dist/src/debug-symbols.js +5 -3
- package/dist/src/device-input-state.js +2 -0
- package/dist/src/device-rotation.js +14 -0
- package/dist/src/devices.js +1 -1
- package/dist/src/devices2.js +1 -0
- package/dist/src/devices3.js +1 -0
- package/dist/src/find.js +1 -1
- package/dist/src/finders.d.ts +2 -81
- package/dist/src/finders.js +1 -1
- package/dist/src/generic.js +1 -1
- package/dist/src/handler-utils.js +1 -1
- package/dist/src/helper.js +2 -0
- package/dist/src/host-process.js +2 -0
- package/dist/src/index.d.ts +1265 -1768
- package/dist/src/index.js +1 -1
- package/dist/src/input-actions.js +3 -1
- package/dist/src/input-actions2.js +1 -0
- package/dist/src/install-source.d.ts +13 -14
- package/dist/src/install-source.js +1 -1
- package/dist/src/install-source2.js +1 -0
- package/dist/src/interaction-snapshot.js +1 -0
- package/dist/src/interaction.js +1 -1
- package/dist/src/interactor.js +2 -1
- package/dist/src/internal/bin.d.ts +1 -1
- package/dist/src/internal/bin.js +2 -5
- package/dist/src/internal/companion-tunnel.d.ts +1 -1
- package/dist/src/internal/companion-tunnel.js +1 -1
- package/dist/src/internal/daemon.d.ts +1 -1
- package/dist/src/internal/daemon.js +16 -1
- package/dist/src/internal/png-worker.d.ts +29 -26
- package/dist/src/internal/png-worker.js +1 -1
- package/dist/src/internal/update-check-entry.d.ts +1 -1
- package/dist/src/internal/update-check-entry.js +1 -1
- package/dist/src/io.d.ts +2 -85
- package/dist/src/io.js +1 -1
- package/dist/src/lease.js +1 -1
- package/dist/src/linux.js +1 -1
- package/dist/src/logcat.js +6 -0
- package/dist/src/manifest.js +1 -0
- package/dist/src/metro.d.ts +70 -113
- package/dist/src/metro.js +1 -1
- package/dist/src/metro2.js +1 -0
- package/dist/src/notifications.js +1 -1
- package/dist/src/output-path.js +1 -0
- package/dist/src/path-resolution.js +1 -0
- package/dist/src/png-worker-contract.js +1 -0
- package/dist/src/points.js +4 -0
- package/dist/src/project-runtime.js +1 -0
- package/dist/src/provider.js +1 -1
- package/dist/src/providers.js +1 -0
- package/dist/src/react-native.js +1 -1
- package/dist/src/record-trace-recording.js +2 -5
- package/dist/src/record-trace.js +1 -1
- package/dist/src/recording-provider.js +1 -1
- package/dist/src/registry.js +37 -0
- package/dist/src/remote-config.d.ts +2 -83
- package/dist/src/reporting.js +9 -0
- package/dist/src/rolldown-runtime.js +1 -0
- package/dist/src/runner-client.js +1 -0
- package/dist/src/runner-provider.js +1 -0
- package/dist/src/runner-sequence.js +1 -1
- package/dist/src/runtime-transport.js +1 -0
- package/dist/src/runtime.js +3 -0
- package/dist/src/scoped-provider.js +1 -0
- package/dist/src/sdk-android-adb.d.ts +144 -0
- package/dist/src/sdk-android-adb.js +4 -0
- package/dist/src/sdk-app-inventory.d.ts +4 -0
- package/dist/src/sdk-app-inventory.js +1 -0
- package/dist/src/sdk-batch-runner.d.ts +40 -0
- package/dist/src/sdk-batch-runner.js +2 -0
- package/dist/src/sdk-batch.js +2 -0
- package/dist/src/sdk-contracts.d.ts +386 -0
- package/dist/src/sdk-contracts.js +2 -0
- package/dist/src/sdk-finders.d.ts +61 -0
- package/dist/src/sdk-finders.js +1 -0
- package/dist/src/sdk-io.d.ts +79 -0
- package/dist/src/sdk-io.js +1 -0
- package/dist/src/sdk-remote-config.d.ts +140 -0
- package/dist/src/sdk-remote-config.js +1 -0
- package/dist/src/sdk-selectors.js +1 -0
- package/dist/src/selector-runtime.js +1 -1
- package/dist/src/selectors.d.ts +39 -137
- package/dist/src/selectors.js +1 -1
- package/dist/src/selectors2.js +1 -0
- package/dist/src/server.js +3 -2
- package/dist/src/session.js +10 -11
- package/dist/src/setting-state.js +1 -0
- package/dist/src/simctl.js +1 -0
- package/dist/src/simulator.js +1 -2
- package/dist/src/snapshot-diagnostics.js +3 -0
- package/dist/src/snapshot-helper.js +5 -0
- package/dist/src/snapshot-quality.js +1 -0
- package/dist/src/snapshot.js +1 -2
- package/dist/src/snapshot2.js +2 -0
- package/dist/src/{5560.js → timeouts.js} +1 -1
- package/dist/src/tool-provider.js +3 -0
- package/dist/src/tool-provider2.js +1 -0
- package/dist/src/ttl-memo.js +1 -0
- package/dist/src/update-check.js +1 -0
- package/dist/src/url.js +1 -0
- package/dist/src/version.js +1 -0
- package/dist/src/web.js +1 -1
- package/dist/src/xml.js +2 -0
- package/package.json +12 -19
- package/server.json +2 -2
- package/skills/agent-device/SKILL.md +2 -0
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.18.2.apk.sha256 +0 -1
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.18.2.apk.sha256 +0 -1
- package/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +0 -443
- package/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +0 -169
- package/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +0 -220
- package/dist/src/1404.js +0 -1
- package/dist/src/1986.js +0 -1
- package/dist/src/2748.js +0 -1
- package/dist/src/2948.js +0 -30
- package/dist/src/3006.js +0 -1
- package/dist/src/314.js +0 -1
- package/dist/src/3340.js +0 -8
- package/dist/src/4418.js +0 -1
- package/dist/src/4428.js +0 -3
- package/dist/src/485.js +0 -1
- package/dist/src/4917.js +0 -1
- package/dist/src/495.js +0 -5
- package/dist/src/5448.js +0 -3
- package/dist/src/5484.js +0 -1
- package/dist/src/5643.js +0 -1
- package/dist/src/587.js +0 -1
- package/dist/src/5913.js +0 -4
- package/dist/src/6849.js +0 -1
- package/dist/src/6918.js +0 -1
- package/dist/src/6967.js +0 -1
- package/dist/src/760.js +0 -1
- package/dist/src/7871.js +0 -1
- package/dist/src/8184.js +0 -1
- package/dist/src/8225.js +0 -1
- package/dist/src/8391.js +0 -1
- package/dist/src/8404.js +0 -2
- package/dist/src/8414.js +0 -2
- package/dist/src/8875.js +0 -4
- package/dist/src/893.js +0 -1
- package/dist/src/8936.js +0 -1
- package/dist/src/9414.js +0 -1
- package/dist/src/9581.js +0 -22
- package/dist/src/9612.js +0 -1
- package/dist/src/9624.js +0 -1
- package/dist/src/9722.js +0 -38
- package/dist/src/devices~1.js +0 -1
- package/dist/src/devices~2.js +0 -1
- package/dist/src/input-actions~1.js +0 -1
- package/dist/src/rslib-runtime.js +0 -1
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.m +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/xcshareddata/xcschemes/AgentDeviceRunner.xcscheme +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests.entitlements +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.h +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.m +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerObjCExceptionCatcher.h +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerObjCExceptionCatcher.m +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Alert.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Environment.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Exceptions.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenRecorder.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TvRemote.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests.xctestplan +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/RecordingScripts/recording-overlay.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/RecordingScripts/recording-resize.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/RecordingScripts/recording-trim.swift +0 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
|
|
3
|
+
struct FlatSnapshotFilterNode {
|
|
4
|
+
let isRoot: Bool
|
|
5
|
+
let label: String
|
|
6
|
+
let identifier: String
|
|
7
|
+
let valueText: String?
|
|
8
|
+
let visible: Bool
|
|
9
|
+
|
|
10
|
+
func matchesScope(_ scope: String) -> Bool {
|
|
11
|
+
let haystack = [label, identifier, valueText ?? ""].joined(separator: "\n")
|
|
12
|
+
return haystack.localizedCaseInsensitiveContains(scope)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
struct FlatSnapshotFilterDecision {
|
|
17
|
+
let include: Bool
|
|
18
|
+
let insideMatchedScope: Bool
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
extension RunnerTests {
|
|
22
|
+
func flatSnapshotFilterDecision(
|
|
23
|
+
_ node: FlatSnapshotFilterNode,
|
|
24
|
+
options: SnapshotOptions,
|
|
25
|
+
insideMatchedScope: Bool
|
|
26
|
+
) -> FlatSnapshotFilterDecision {
|
|
27
|
+
let scope = options.scope?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
28
|
+
let scopeActive = scope?.isEmpty == false
|
|
29
|
+
let matchesScope: Bool
|
|
30
|
+
if scopeActive, let scope {
|
|
31
|
+
matchesScope = node.matchesScope(scope)
|
|
32
|
+
} else {
|
|
33
|
+
matchesScope = false
|
|
34
|
+
}
|
|
35
|
+
let nowInsideScope = insideMatchedScope || matchesScope
|
|
36
|
+
|
|
37
|
+
let include: Bool
|
|
38
|
+
if node.isRoot {
|
|
39
|
+
include = true
|
|
40
|
+
} else if scopeActive && !nowInsideScope {
|
|
41
|
+
include = false
|
|
42
|
+
} else if options.interactiveOnly && !node.visible {
|
|
43
|
+
include = false
|
|
44
|
+
} else {
|
|
45
|
+
include = true
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return FlatSnapshotFilterDecision(include: include, insideMatchedScope: nowInsideScope)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
func privateAXInteractiveCandidate(rawElementType: Int) -> Bool {
|
|
52
|
+
guard let type = flatSnapshotElementType(rawElementType: rawElementType) else {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
return interactiveTypes.contains(type) || Self.scrollContainerTypes.contains(type)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func flatSnapshotElementType(rawElementType: Int) -> XCUIElement.ElementType? {
|
|
59
|
+
guard let raw = UInt(exactly: rawElementType),
|
|
60
|
+
let type = XCUIElement.ElementType(rawValue: raw)
|
|
61
|
+
else {
|
|
62
|
+
return nil
|
|
63
|
+
}
|
|
64
|
+
return type
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
}
|
|
@@ -200,16 +200,32 @@ extension RunnerTests {
|
|
|
200
200
|
)
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
// Maestro-compat gate for the non-hittable coordinate fallback: an element
|
|
204
|
+
// with no frame at all cannot be coordinate-tapped, otherwise the decision
|
|
205
|
+
// is the shared TapPointPolicy center-in-frame rule (golden parity table
|
|
206
|
+
// with the TS twin). app.frame is the frame source here — replay taps
|
|
207
|
+
// resolved bounds Maestro-style, so the union frame is intentional.
|
|
203
208
|
private func hasTappableFrame(app: XCUIApplication, element: XCUIElement) -> Bool {
|
|
204
209
|
let frame = element.frame
|
|
205
210
|
if frame.isEmpty {
|
|
206
211
|
return false
|
|
207
212
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
213
|
+
return TapPointPolicy.isAllowed(elementFrame: frame, windowFrame: app.frame)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// The tappable on-screen viewport. app.frame is unsuitable: it unions
|
|
217
|
+
// transformed subtrees, so a closed drawer at negative x inflates it and
|
|
218
|
+
// out-of-window coordinates still pass containment. Falls back to app.frame
|
|
219
|
+
// when no window frame is readable.
|
|
220
|
+
func onScreenWindowFrame(app: XCUIApplication) -> CGRect {
|
|
221
|
+
let window = app.windows.element(boundBy: 0)
|
|
222
|
+
if window.exists {
|
|
223
|
+
let frame = window.frame
|
|
224
|
+
if !frame.isEmpty {
|
|
225
|
+
return frame
|
|
226
|
+
}
|
|
211
227
|
}
|
|
212
|
-
return
|
|
228
|
+
return app.frame
|
|
213
229
|
}
|
|
214
230
|
|
|
215
231
|
func queryElement(app: XCUIApplication, selectorKey: String, selectorValue: String) -> Response {
|
|
@@ -1232,74 +1248,4 @@ extension RunnerTests {
|
|
|
1232
1248
|
return element.exists ? element : nil
|
|
1233
1249
|
}
|
|
1234
1250
|
|
|
1235
|
-
#if AGENT_DEVICE_RUNNER_UNIT_TESTS
|
|
1236
|
-
// Identity in portrait/unknown, 90° per landscape, 180° upside-down.
|
|
1237
|
-
func testNativeSynthesizedPointRotatesByInterfaceOrientation() {
|
|
1238
|
-
let portrait = CGRect(x: 0, y: 0, width: 834, height: 1210)
|
|
1239
|
-
let landscape = CGRect(x: 0, y: 0, width: 1210, height: 834)
|
|
1240
|
-
let offsetLandscape = CGRect(x: 10, y: 20, width: 1210, height: 834)
|
|
1241
|
-
// (frame, UIInterfaceOrientation, expected native point) for a tap at (170, 268).
|
|
1242
|
-
let cases: [(CGRect, Int, CGPoint)] = [
|
|
1243
|
-
(portrait, RunnerInterfaceOrientation.portrait, CGPoint(x: 170, y: 268)),
|
|
1244
|
-
(landscape, RunnerInterfaceOrientation.landscapeRight, CGPoint(x: 566, y: 170)),
|
|
1245
|
-
(landscape, RunnerInterfaceOrientation.landscapeLeft, CGPoint(x: 268, y: 1040)),
|
|
1246
|
-
(portrait, RunnerInterfaceOrientation.portraitUpsideDown, CGPoint(x: 664, y: 942)),
|
|
1247
|
-
(portrait, RunnerInterfaceOrientation.unknown, CGPoint(x: 170, y: 268)),
|
|
1248
|
-
]
|
|
1249
|
-
for (frame, orientation, expected) in cases {
|
|
1250
|
-
XCTAssertEqual(
|
|
1251
|
-
nativeSynthesizedPoint(orientedX: 170, orientedY: 268, in: frame, interfaceOrientation: orientation),
|
|
1252
|
-
expected,
|
|
1253
|
-
"interfaceOrientation \(orientation)"
|
|
1254
|
-
)
|
|
1255
|
-
}
|
|
1256
|
-
XCTAssertEqual(
|
|
1257
|
-
nativeSynthesizedPoint(
|
|
1258
|
-
orientedX: 180,
|
|
1259
|
-
orientedY: 288,
|
|
1260
|
-
in: offsetLandscape,
|
|
1261
|
-
interfaceOrientation: RunnerInterfaceOrientation.landscapeLeft
|
|
1262
|
-
),
|
|
1263
|
-
CGPoint(x: 268, y: 1040),
|
|
1264
|
-
"non-zero frame origin is localized before rotation"
|
|
1265
|
-
)
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
func testNativeSynthesizedVectorRotatesByInterfaceOrientation() {
|
|
1269
|
-
let cases: [(Int, CGVector)] = [
|
|
1270
|
-
(RunnerInterfaceOrientation.portrait, CGVector(dx: 40, dy: -20)),
|
|
1271
|
-
(RunnerInterfaceOrientation.landscapeRight, CGVector(dx: 20, dy: 40)),
|
|
1272
|
-
(RunnerInterfaceOrientation.landscapeLeft, CGVector(dx: -20, dy: -40)),
|
|
1273
|
-
(RunnerInterfaceOrientation.portraitUpsideDown, CGVector(dx: -40, dy: 20)),
|
|
1274
|
-
(RunnerInterfaceOrientation.unknown, CGVector(dx: 40, dy: -20)),
|
|
1275
|
-
]
|
|
1276
|
-
for (orientation, expected) in cases {
|
|
1277
|
-
let vector = nativeSynthesizedVector(orientedDx: 40, orientedDy: -20, interfaceOrientation: orientation)
|
|
1278
|
-
XCTAssertEqual(vector.dx, expected.dx, "dx interfaceOrientation \(orientation)")
|
|
1279
|
-
XCTAssertEqual(vector.dy, expected.dy, "dy interfaceOrientation \(orientation)")
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
func testDesktopScrollWheelDeltasMapDirections() throws {
|
|
1284
|
-
XCTAssertEqual(try XCTUnwrap(desktopScrollWheelDeltas(direction: "up", pixels: 120)).vertical, 120)
|
|
1285
|
-
XCTAssertEqual(try XCTUnwrap(desktopScrollWheelDeltas(direction: "down", pixels: 120)).vertical, -120)
|
|
1286
|
-
XCTAssertEqual(try XCTUnwrap(desktopScrollWheelDeltas(direction: "left", pixels: 120)).horizontal, 120)
|
|
1287
|
-
XCTAssertEqual(try XCTUnwrap(desktopScrollWheelDeltas(direction: "right", pixels: 120)).horizontal, -120)
|
|
1288
|
-
XCTAssertNil(desktopScrollWheelDeltas(direction: "diagonal", pixels: 120))
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
func testDesktopScrollWheelDeltaEventsHonorDurationAndPreservePixels() throws {
|
|
1292
|
-
let events = try XCTUnwrap(desktopScrollWheelDeltaEvents(direction: "down", pixels: 200, durationMs: 50))
|
|
1293
|
-
XCTAssertEqual(events.count, 4)
|
|
1294
|
-
XCTAssertEqual(events.map(\.vertical).reduce(0, +), -200)
|
|
1295
|
-
XCTAssertEqual(events.map(\.horizontal).reduce(0, +), 0)
|
|
1296
|
-
XCTAssertEqual(desktopScrollEventIntervalSeconds(durationMs: 50, eventCount: events.count), 0.05 / 3.0)
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
func testDesktopScrollWheelDeltaEventsKeepInstantScrollSingleEvent() throws {
|
|
1300
|
-
let events = try XCTUnwrap(desktopScrollWheelDeltaEvents(direction: "down", pixels: 200, durationMs: 0))
|
|
1301
|
-
XCTAssertEqual(events.count, 1)
|
|
1302
|
-
XCTAssertEqual(events.first?.vertical, -200)
|
|
1303
|
-
}
|
|
1304
|
-
#endif
|
|
1305
1251
|
}
|
package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
|
|
3
|
+
// Swift port of buildScrollGesturePlan from src/core/scroll-gesture.ts.
|
|
4
|
+
//
|
|
5
|
+
// This is a deliberate two-place invariant: the daemon keeps the TS implementation (for Android,
|
|
6
|
+
// recording, and reported-pixels), and the runner places the gesture with this Swift copy. The
|
|
7
|
+
// parity test vectors at the bottom of this file mirror src/core/__tests__/scroll-gesture.test.ts —
|
|
8
|
+
// if you change the math in either language, update the other and both vector sets.
|
|
9
|
+
//
|
|
10
|
+
// All inputs here are positive (reference dims, travel, center), so Swift's `.rounded()`
|
|
11
|
+
// (half away from zero) matches JS `Math.round` (half up) on every value computed below.
|
|
12
|
+
|
|
13
|
+
struct RunnerScrollGesturePlan {
|
|
14
|
+
let x1: Double
|
|
15
|
+
let y1: Double
|
|
16
|
+
let x2: Double
|
|
17
|
+
let y2: Double
|
|
18
|
+
let travelPixels: Double
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private let runnerDefaultScrollAmount = 0.6
|
|
22
|
+
private let runnerDefaultEdgePaddingFraction = 0.05
|
|
23
|
+
|
|
24
|
+
func runnerScrollGesturePlan(
|
|
25
|
+
direction: String,
|
|
26
|
+
amount: Double?,
|
|
27
|
+
pixels: Double?,
|
|
28
|
+
referenceWidth: Double,
|
|
29
|
+
referenceHeight: Double
|
|
30
|
+
) -> RunnerScrollGesturePlan? {
|
|
31
|
+
// Mirror the TS INVALID_ARGS contract: non-positive or non-finite amount/pixels are rejected
|
|
32
|
+
// rather than clamped into a journaled 1px scroll. The daemon validates before sending, so
|
|
33
|
+
// this only triggers for non-daemon wire clients.
|
|
34
|
+
if let amount, !(amount.isFinite && amount > 0) { return nil }
|
|
35
|
+
if let pixels, !(pixels.isFinite && pixels > 0) { return nil }
|
|
36
|
+
let axisLength = (direction == "up" || direction == "down") ? referenceHeight : referenceWidth
|
|
37
|
+
let requestedAmount = amount ?? runnerDefaultScrollAmount
|
|
38
|
+
let requestedPixels: Double =
|
|
39
|
+
pixels.map { max(1, $0.rounded()) } ?? (axisLength * requestedAmount).rounded()
|
|
40
|
+
let edgePadding = max(1, (axisLength * runnerDefaultEdgePaddingFraction).rounded())
|
|
41
|
+
let maxTravelPixels = max(1, axisLength - edgePadding * 2)
|
|
42
|
+
let travelPixels = max(1, min(requestedPixels, maxTravelPixels))
|
|
43
|
+
let halfTravel = (travelPixels / 2).rounded()
|
|
44
|
+
let centerX = (referenceWidth / 2).rounded()
|
|
45
|
+
let centerY = (referenceHeight / 2).rounded()
|
|
46
|
+
|
|
47
|
+
func plan(_ x1: Double, _ y1: Double, _ x2: Double, _ y2: Double) -> RunnerScrollGesturePlan {
|
|
48
|
+
RunnerScrollGesturePlan(x1: x1, y1: y1, x2: x2, y2: y2, travelPixels: travelPixels)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
switch direction {
|
|
52
|
+
case "up":
|
|
53
|
+
return plan(centerX, centerY - halfTravel, centerX, centerY + halfTravel)
|
|
54
|
+
case "down":
|
|
55
|
+
return plan(centerX, centerY + halfTravel, centerX, centerY - halfTravel)
|
|
56
|
+
case "left":
|
|
57
|
+
return plan(centerX - halfTravel, centerY, centerX + halfTravel, centerY)
|
|
58
|
+
case "right":
|
|
59
|
+
return plan(centerX + halfTravel, centerY, centerX - halfTravel, centerY)
|
|
60
|
+
default:
|
|
61
|
+
return nil
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
@@ -273,180 +273,3 @@ extension RunnerTests {
|
|
|
273
273
|
}
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
-
#if AGENT_DEVICE_RUNNER_UNIT_TESTS
|
|
277
|
-
// MARK: - In-bundle unit tests (device-free)
|
|
278
|
-
|
|
279
|
-
extension RunnerTests {
|
|
280
|
-
func testSequenceDecodesStepsFromWire() throws {
|
|
281
|
-
let json = """
|
|
282
|
-
{"command":"sequence","commandId":"seq-1","steps":[
|
|
283
|
-
{"kind":"tap","x":100,"y":200},
|
|
284
|
-
{"kind":"doubleTap","x":101,"y":200},
|
|
285
|
-
{"kind":"longPress","x":102,"y":200,"durationMs":300},
|
|
286
|
-
{"kind":"drag","x":10,"y":600,"x2":10,"y2":200,"durationMs":250,"pauseMs":50}
|
|
287
|
-
]}
|
|
288
|
-
"""
|
|
289
|
-
let command = try JSONDecoder().decode(Command.self, from: Data(json.utf8))
|
|
290
|
-
XCTAssertEqual(command.command, .sequence)
|
|
291
|
-
XCTAssertEqual(command.steps?.count, 4)
|
|
292
|
-
XCTAssertEqual(command.steps?[0].kind, "tap")
|
|
293
|
-
XCTAssertEqual(command.steps?[1].kind, "doubleTap")
|
|
294
|
-
XCTAssertEqual(command.steps?[3].x2, 10)
|
|
295
|
-
XCTAssertEqual(command.steps?[3].pauseMs, 50)
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
func testSequenceAcceptsDoubleTapKind() {
|
|
299
|
-
// A doubleTap step missing coords must fail on the coords check, not the kind allowlist —
|
|
300
|
-
// proving "doubleTap" passes validateSequenceStep without needing a device to execute on.
|
|
301
|
-
let response = executeSequenceForTest(steps: [
|
|
302
|
-
sequenceStep(kind: "doubleTap", x: nil)
|
|
303
|
-
])
|
|
304
|
-
XCTAssertEqual(response.ok, false)
|
|
305
|
-
XCTAssertEqual(response.error?.code, "INVALID_ARGS")
|
|
306
|
-
XCTAssertTrue(response.error?.message.contains("requires finite x and y") ?? false)
|
|
307
|
-
XCTAssertFalse(response.error?.message.contains("unsupported kind") ?? true)
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
func testSequenceRejectsUnknownKind() throws {
|
|
311
|
-
let response = executeSequenceForTest(steps: [
|
|
312
|
-
sequenceStep(kind: "tap", x: 1, y: 2),
|
|
313
|
-
sequenceStep(kind: "pinch", x: 3, y: 4),
|
|
314
|
-
])
|
|
315
|
-
XCTAssertEqual(response.ok, false)
|
|
316
|
-
XCTAssertEqual(response.error?.code, "INVALID_ARGS")
|
|
317
|
-
XCTAssertTrue(response.error?.message.contains("step 1") ?? false)
|
|
318
|
-
XCTAssertTrue(response.error?.message.contains("pinch") ?? false)
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
func testSequenceRejectsEmpty() {
|
|
322
|
-
let response = executeSequenceForTest(steps: [])
|
|
323
|
-
XCTAssertEqual(response.ok, false)
|
|
324
|
-
XCTAssertEqual(response.error?.code, "INVALID_ARGS")
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
func testSequenceRejectsTooManySteps() {
|
|
328
|
-
let steps = (0..<21).map { _ in sequenceStep(kind: "tap", x: 1, y: 2) }
|
|
329
|
-
let response = executeSequenceForTest(steps: steps)
|
|
330
|
-
XCTAssertEqual(response.ok, false)
|
|
331
|
-
XCTAssertEqual(response.error?.code, "INVALID_ARGS")
|
|
332
|
-
XCTAssertTrue(response.error?.message.contains("at most 20") ?? false)
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
func testSequenceRejectsDragMissingSecondPoint() {
|
|
336
|
-
let response = executeSequenceForTest(steps: [
|
|
337
|
-
sequenceStep(kind: "tap", x: 1, y: 2),
|
|
338
|
-
sequenceStep(kind: "drag", x: 3, y: 4),
|
|
339
|
-
])
|
|
340
|
-
XCTAssertEqual(response.ok, false)
|
|
341
|
-
XCTAssertEqual(response.error?.code, "INVALID_ARGS")
|
|
342
|
-
XCTAssertTrue(response.error?.message.contains("step 1") ?? false)
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
func testAssembleSequencePreservesOrderOnSuccess() {
|
|
346
|
-
let steps = [
|
|
347
|
-
sequenceStep(kind: "tap", x: 1, y: 1),
|
|
348
|
-
sequenceStep(kind: "longPress", x: 2, y: 2),
|
|
349
|
-
sequenceStep(kind: "tap", x: 3, y: 3),
|
|
350
|
-
]
|
|
351
|
-
var calls: [Int] = []
|
|
352
|
-
let execution = assembleSequenceExecution(steps: steps) { index, _ in
|
|
353
|
-
calls.append(index)
|
|
354
|
-
return SequenceStepOutcome(
|
|
355
|
-
outcome: .performed,
|
|
356
|
-
gestureStartUptimeMs: Double(index * 10),
|
|
357
|
-
gestureEndUptimeMs: Double(index * 10 + 5)
|
|
358
|
-
)
|
|
359
|
-
}
|
|
360
|
-
XCTAssertEqual(calls, [0, 1, 2])
|
|
361
|
-
XCTAssertEqual(execution.completedSteps, 3)
|
|
362
|
-
XCTAssertNil(execution.failedStepIndex)
|
|
363
|
-
XCTAssertEqual(execution.results.map { $0.kind }, ["tap", "longPress", "tap"])
|
|
364
|
-
XCTAssertEqual(execution.gestureStartUptimeMs, 0)
|
|
365
|
-
XCTAssertEqual(execution.gestureEndUptimeMs, 25)
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
func testAssembleSequenceStopsAtFirstFailure() {
|
|
369
|
-
let steps = [
|
|
370
|
-
sequenceStep(kind: "tap", x: 1, y: 1),
|
|
371
|
-
sequenceStep(kind: "drag", x: 2, y: 2),
|
|
372
|
-
sequenceStep(kind: "tap", x: 3, y: 3),
|
|
373
|
-
]
|
|
374
|
-
var calls: [Int] = []
|
|
375
|
-
let execution = assembleSequenceExecution(steps: steps) { index, _ in
|
|
376
|
-
calls.append(index)
|
|
377
|
-
if index == 1 {
|
|
378
|
-
return SequenceStepOutcome(
|
|
379
|
-
outcome: .unsupported(message: "drag unsupported", hint: nil),
|
|
380
|
-
gestureStartUptimeMs: 10,
|
|
381
|
-
gestureEndUptimeMs: 15
|
|
382
|
-
)
|
|
383
|
-
}
|
|
384
|
-
return SequenceStepOutcome(outcome: .performed, gestureStartUptimeMs: 0, gestureEndUptimeMs: 5)
|
|
385
|
-
}
|
|
386
|
-
// Step 2 is never invoked.
|
|
387
|
-
XCTAssertEqual(calls, [0, 1])
|
|
388
|
-
XCTAssertEqual(execution.completedSteps, 1)
|
|
389
|
-
XCTAssertEqual(execution.failedStepIndex, 1)
|
|
390
|
-
// results.count == completedSteps + 1 (the failed step).
|
|
391
|
-
XCTAssertEqual(execution.results.count, 2)
|
|
392
|
-
XCTAssertEqual(execution.results[1].ok, false)
|
|
393
|
-
XCTAssertEqual(execution.results[1].errorCode, "UNSUPPORTED_OPERATION")
|
|
394
|
-
XCTAssertEqual(execution.results[1].errorMessage, "drag unsupported")
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
func testSequenceWorstCaseResponseStaysUnderJournalCap() throws {
|
|
398
|
-
let longMessage = String(repeating: "e", count: 200)
|
|
399
|
-
let results = (0..<20).map { index in
|
|
400
|
-
SequenceStepResult(
|
|
401
|
-
ok: index < 19,
|
|
402
|
-
kind: "drag",
|
|
403
|
-
errorCode: index < 19 ? nil : "UNSUPPORTED_OPERATION",
|
|
404
|
-
errorMessage: index < 19 ? nil : longMessage,
|
|
405
|
-
gestureStartUptimeMs: 123456.789,
|
|
406
|
-
gestureEndUptimeMs: 123466.789
|
|
407
|
-
)
|
|
408
|
-
}
|
|
409
|
-
let response = Response(
|
|
410
|
-
ok: true,
|
|
411
|
-
data: DataPayload(
|
|
412
|
-
message: "sequence",
|
|
413
|
-
completedSteps: 19,
|
|
414
|
-
failedStepIndex: 19,
|
|
415
|
-
sequenceResults: results
|
|
416
|
-
)
|
|
417
|
-
)
|
|
418
|
-
let encoded = try JSONEncoder().encode(response)
|
|
419
|
-
XCTAssertLessThan(encoded.count, 16 * 1024)
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
private func sequenceStep(
|
|
423
|
-
kind: String,
|
|
424
|
-
x: Double?,
|
|
425
|
-
y: Double? = nil,
|
|
426
|
-
x2: Double? = nil,
|
|
427
|
-
y2: Double? = nil
|
|
428
|
-
) -> SequenceStep {
|
|
429
|
-
SequenceStep(
|
|
430
|
-
kind: kind, x: x, y: y, x2: x2, y2: y2, durationMs: nil, pauseMs: nil, synthesized: nil)
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
/// Validation runs before any executor call, so the INVALID_ARGS paths are exercised without
|
|
434
|
-
/// reaching the device executor (which is never invoked when validation rejects).
|
|
435
|
-
private func executeSequenceForTest(steps: [SequenceStep]) -> Response {
|
|
436
|
-
let command = makeSequenceCommand(steps: steps)
|
|
437
|
-
return executeSequence(command: command, activeApp: app)
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
/// Build a sequence Command via JSON so the test does not depend on the memberwise init's
|
|
441
|
-
/// parameter order.
|
|
442
|
-
private func makeSequenceCommand(steps: [SequenceStep]) -> Command {
|
|
443
|
-
struct SequenceCommandFixture: Encodable {
|
|
444
|
-
let command = "sequence"
|
|
445
|
-
let commandId = "seq-test"
|
|
446
|
-
let steps: [SequenceStep]
|
|
447
|
-
}
|
|
448
|
-
let data = try! JSONEncoder().encode(SequenceCommandFixture(steps: steps))
|
|
449
|
-
return try! JSONDecoder().decode(Command.self, from: data)
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
#endif
|
|
@@ -302,7 +302,11 @@ extension RunnerTests {
|
|
|
302
302
|
return DataPayload(nodes: nodes, truncated: false)
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
func snapshotFlatInteractive(
|
|
305
|
+
func snapshotFlatInteractive(
|
|
306
|
+
app: XCUIApplication,
|
|
307
|
+
options: SnapshotOptions,
|
|
308
|
+
planDeadline: Date = .distantFuture
|
|
309
|
+
) -> DataPayload {
|
|
306
310
|
var nodes: [SnapshotNode] = [
|
|
307
311
|
interactiveRootNode(rect: .zero)
|
|
308
312
|
]
|
|
@@ -310,9 +314,12 @@ extension RunnerTests {
|
|
|
310
314
|
return DataPayload(nodes: nodes, truncated: false)
|
|
311
315
|
}
|
|
312
316
|
|
|
313
|
-
|
|
317
|
+
// Bounded by both its own sweep budget and the umbrella capture-plan deadline, so a
|
|
318
|
+
// chained recovery tier can never push the plan past the main-thread watchdog (#1105).
|
|
319
|
+
let sweepDeadline = options.interactiveOnly
|
|
314
320
|
? Date().addingTimeInterval(Self.flatInteractiveFallbackBudget)
|
|
315
321
|
: Date.distantFuture
|
|
322
|
+
let deadline = min(sweepDeadline, planDeadline)
|
|
316
323
|
let viewport = safeSnapshotViewport(app: app)
|
|
317
324
|
var seen = Set<String>()
|
|
318
325
|
var candidates: [SnapshotNode] = []
|
|
@@ -426,50 +433,6 @@ extension RunnerTests {
|
|
|
426
433
|
)
|
|
427
434
|
}
|
|
428
435
|
|
|
429
|
-
#if AGENT_DEVICE_RUNNER_UNIT_TESTS
|
|
430
|
-
func testSnapshotAccessibilityUnavailableMarksSparseSnapshotRunnerFatal() {
|
|
431
|
-
currentApp = app
|
|
432
|
-
currentBundleId = "com.example.app"
|
|
433
|
-
|
|
434
|
-
let payload = snapshotAccessibilityUnavailable(
|
|
435
|
-
failure: SnapshotCaptureFailure(
|
|
436
|
-
code: Self.axSnapshotErrorCode,
|
|
437
|
-
message: Self.axSnapshotFailureMessage,
|
|
438
|
-
hint: Self.axSnapshotHint
|
|
439
|
-
)
|
|
440
|
-
)
|
|
441
|
-
|
|
442
|
-
XCTAssertEqual(payload.message, "\(Self.axSnapshotFailureMessage) Hint: \(Self.axSnapshotHint)")
|
|
443
|
-
XCTAssertEqual(payload.nodes?.count, 1)
|
|
444
|
-
XCTAssertEqual(payload.nodes?.first?.type, "Application")
|
|
445
|
-
XCTAssertEqual(payload.truncated, true)
|
|
446
|
-
XCTAssertEqual(payload.runnerFatal, true)
|
|
447
|
-
XCTAssertEqual(payload.runnerFatalReason, Self.axSnapshotUnavailableReason)
|
|
448
|
-
XCTAssertNil(currentApp)
|
|
449
|
-
XCTAssertNil(currentBundleId)
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
func testRecoveredSnapshotMessagePreservesHint() {
|
|
453
|
-
let message = recoveredSnapshotMessage(
|
|
454
|
-
SnapshotCaptureFailure(
|
|
455
|
-
code: Self.axSnapshotErrorCode,
|
|
456
|
-
message: Self.axSnapshotFailureMessage,
|
|
457
|
-
hint: Self.axSnapshotHint
|
|
458
|
-
)
|
|
459
|
-
)
|
|
460
|
-
|
|
461
|
-
XCTAssertTrue(message.contains(Self.axSnapshotFailureMessage))
|
|
462
|
-
XCTAssertTrue(message.contains(Self.axSnapshotHint))
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
func testRawSnapshotTooLargeFailureIsStructured() {
|
|
466
|
-
let failure = rawSnapshotTooLargeFailure(nodeCount: Self.rawSnapshotMaxNodes + 1)
|
|
467
|
-
|
|
468
|
-
XCTAssertEqual(failure.code, Self.rawSnapshotTooLargeCode)
|
|
469
|
-
XCTAssertTrue(failure.message.contains("\(Self.rawSnapshotMaxNodes) nodes"))
|
|
470
|
-
XCTAssertEqual(failure.hint, Self.rawSnapshotTooLargeHint)
|
|
471
|
-
}
|
|
472
|
-
#endif
|
|
473
436
|
|
|
474
437
|
private func interactiveRootNode(rect: CGRect) -> SnapshotNode {
|
|
475
438
|
SnapshotNode(
|
|
@@ -562,12 +525,14 @@ extension RunnerTests {
|
|
|
562
525
|
|
|
563
526
|
func makeSnapshotTraversalContext(
|
|
564
527
|
app: XCUIApplication,
|
|
565
|
-
options: SnapshotOptions
|
|
528
|
+
options: SnapshotOptions,
|
|
529
|
+
captureDeadline: Date = .distantFuture
|
|
566
530
|
) throws -> SnapshotTraversalContext? {
|
|
567
531
|
let viewport = safeSnapshotViewport(app: app)
|
|
568
532
|
let queryRoot = options.scope.flatMap { findScopeElement(app: app, scope: $0) } ?? app
|
|
569
533
|
|
|
570
|
-
|
|
534
|
+
let slice = min(treeCaptureSliceBudget, max(0.5, captureDeadline.timeIntervalSinceNow))
|
|
535
|
+
guard let rootSnapshot = try captureSnapshotRootBounded(queryRoot, sliceSeconds: slice) else {
|
|
571
536
|
return nil
|
|
572
537
|
}
|
|
573
538
|
|
|
@@ -582,6 +547,74 @@ extension RunnerTests {
|
|
|
582
547
|
)
|
|
583
548
|
}
|
|
584
549
|
|
|
550
|
+
static let treeCaptureTimeoutCode = "IOS_TREE_CAPTURE_TIMEOUT"
|
|
551
|
+
|
|
552
|
+
func hasAbandonedTreeCapture() -> Bool {
|
|
553
|
+
treeCaptureLock.lock()
|
|
554
|
+
defer { treeCaptureLock.unlock() }
|
|
555
|
+
return abandonedTreeCaptureCount > 0
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/// Runs the blocking tree-snapshot XPC on a worker thread bounded by `sliceSeconds`. On
|
|
559
|
+
/// timeout the XPC keeps running on its worker (it cannot be cancelled); the capture is
|
|
560
|
+
/// marked abandoned so plans avoid XCTest-backed tiers until it drains, the tree backend is
|
|
561
|
+
/// penalized for this bundle, and the plan moves on to the private AX backend (#1105).
|
|
562
|
+
private func captureSnapshotRootBounded(
|
|
563
|
+
_ element: XCUIElement,
|
|
564
|
+
sliceSeconds: TimeInterval
|
|
565
|
+
) throws -> XCUIElementSnapshot? {
|
|
566
|
+
final class TreeCaptureBox {
|
|
567
|
+
var abandoned = false
|
|
568
|
+
var outcome: Result<XCUIElementSnapshot?, Error>?
|
|
569
|
+
}
|
|
570
|
+
let box = TreeCaptureBox()
|
|
571
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
572
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
573
|
+
var result: Result<XCUIElementSnapshot?, Error>
|
|
574
|
+
do {
|
|
575
|
+
result = .success(try self.captureSnapshotRoot(element))
|
|
576
|
+
} catch {
|
|
577
|
+
result = .failure(error)
|
|
578
|
+
}
|
|
579
|
+
self.treeCaptureLock.lock()
|
|
580
|
+
if box.abandoned {
|
|
581
|
+
self.abandonedTreeCaptureCount -= 1
|
|
582
|
+
self.treeCaptureLock.unlock()
|
|
583
|
+
NSLog("AGENT_DEVICE_RUNNER_TREE_CAPTURE_DRAINED")
|
|
584
|
+
} else {
|
|
585
|
+
box.outcome = result
|
|
586
|
+
self.treeCaptureLock.unlock()
|
|
587
|
+
}
|
|
588
|
+
semaphore.signal()
|
|
589
|
+
}
|
|
590
|
+
if semaphore.wait(timeout: .now() + sliceSeconds) == .timedOut {
|
|
591
|
+
treeCaptureLock.lock()
|
|
592
|
+
let timedOut = box.outcome == nil
|
|
593
|
+
if timedOut {
|
|
594
|
+
box.abandoned = true
|
|
595
|
+
abandonedTreeCaptureCount += 1
|
|
596
|
+
}
|
|
597
|
+
treeCaptureLock.unlock()
|
|
598
|
+
if timedOut {
|
|
599
|
+
NSLog("AGENT_DEVICE_RUNNER_TREE_CAPTURE_SLICE_TIMEOUT slice=%.1f", sliceSeconds)
|
|
600
|
+
penalizeSnapshotTreeBackend(bundleId: currentBundleId, reason: "tree_capture_slice_timeout")
|
|
601
|
+
throw SnapshotCaptureFailure(
|
|
602
|
+
code: Self.treeCaptureTimeoutCode,
|
|
603
|
+
message: "the XCTest tree capture exceeded its \(Int(sliceSeconds))s time slice",
|
|
604
|
+
hint: "The capture plan recovers through the private AX backend on this screen."
|
|
605
|
+
)
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
switch box.outcome {
|
|
609
|
+
case .success(let snapshot):
|
|
610
|
+
return snapshot
|
|
611
|
+
case .failure(let error):
|
|
612
|
+
throw error
|
|
613
|
+
case .none:
|
|
614
|
+
return nil
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
585
618
|
private func captureSnapshotRoot(_ element: XCUIElement) throws -> XCUIElementSnapshot? {
|
|
586
619
|
var rootSnapshot: XCUIElementSnapshot?
|
|
587
620
|
var swiftErrorMessage: String?
|