agent-device 0.19.0 → 0.19.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/README.md +5 -1
- package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.19.0.apk → agent-device-android-multitouch-helper-0.19.1.apk} +0 -0
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.1.apk.sha256 +1 -0
- package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.19.0.manifest.json → agent-device-android-multitouch-helper-0.19.1.manifest.json} +4 -4
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.19.0.apk → agent-device-android-snapshot-helper-0.19.1.apk} +0 -0
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.19.1.apk.sha256 +1 -0
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.19.0.manifest.json → agent-device-android-snapshot-helper-0.19.1.manifest.json} +6 -6
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +24 -7
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +506 -119
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +153 -48
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift +172 -0
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SequenceExecution.swift +76 -5
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +60 -52
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +159 -56
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift +142 -0
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +16 -14
- package/dist/src/android-adb.d.ts +1 -2
- package/dist/src/android-adb.js +1 -1
- package/dist/src/android-input-ownership.js +6 -0
- package/dist/src/android.js +1 -1
- package/dist/src/app-lifecycle.js +3 -3
- package/dist/src/apple-runner-options.js +1 -1
- package/dist/src/apps.js +5 -5
- package/dist/src/cli-help.js +131 -34
- package/dist/src/cli-runner.js +1 -1
- package/dist/src/cli.js +11 -11
- package/dist/src/client.js +2 -2
- package/dist/src/command-metadata.js +1 -1
- package/dist/src/command-schema.js +3 -3
- package/dist/src/device-input-state.js +2 -2
- package/dist/src/devices2.js +1 -1
- package/dist/src/find.js +1 -1
- package/dist/src/generic.js +1 -1
- package/dist/src/index.d.ts +199 -20
- package/dist/src/input-actions.js +3 -3
- package/dist/src/interaction-snapshot.js +1 -1
- package/dist/src/interaction.js +1 -1
- package/dist/src/interactor.js +2 -2
- package/dist/src/internal/bin.js +1 -1
- package/dist/src/internal/daemon.js +17 -15
- package/dist/src/lease.js +1 -1
- package/dist/src/linux.js +1 -1
- package/dist/src/metro.d.ts +0 -1
- package/dist/src/notifications.js +1 -1
- package/dist/src/points.js +1 -1
- package/dist/src/project-runtime.js +1 -1
- package/dist/src/providers.js +1 -1
- package/dist/src/react-native.js +1 -1
- package/dist/src/record-trace-recording.js +2 -2
- package/dist/src/record-trace.js +1 -1
- package/dist/src/registry.js +24 -23
- package/dist/src/reporting.js +3 -3
- package/dist/src/runner-client.js +1 -1
- package/dist/src/runtime.js +2 -2
- package/dist/src/sdk-android-adb.d.ts +2 -2
- package/dist/src/sdk-batch-runner.d.ts +0 -1
- package/dist/src/sdk-batch.js +2 -2
- package/dist/src/sdk-finders.d.ts +1 -2
- package/dist/src/sdk-finders.js +1 -1
- package/dist/src/sdk-io.d.ts +0 -1
- package/dist/src/sdk-remote-config.d.ts +1 -2
- package/dist/src/sdk-remote-config.js +1 -1
- package/dist/src/sdk-selectors.js +1 -1
- package/dist/src/selector-runtime.js +1 -1
- package/dist/src/selectors.d.ts +0 -1
- package/dist/src/selectors.js +1 -1
- package/dist/src/selectors2.js +1 -1
- package/dist/src/server.js +3 -3
- package/dist/src/session.js +10 -10
- package/dist/src/simctl.js +1 -1
- package/dist/src/simulator.js +1 -1
- package/dist/src/snapshot-diagnostics.js +2 -2
- package/dist/src/snapshot-helper.js +4 -4
- package/dist/src/snapshot-quality.js +1 -1
- package/dist/src/snapshot2.js +2 -2
- package/dist/src/tv-remote.js +14 -0
- package/dist/src/web.js +1 -1
- package/package.json +5 -4
- package/server.json +2 -2
- package/skills/agent-device/SKILL.md +8 -7
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.0.apk.sha256 +0 -1
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.19.0.apk.sha256 +0 -1
- package/dist/src/device-rotation.js +0 -14
- package/dist/src/logcat.js +0 -6
package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift
CHANGED
|
@@ -36,34 +36,21 @@ extension RunnerTests {
|
|
|
36
36
|
let y2: Double
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
struct SynthesizedDragPlan {
|
|
40
|
+
let points: DragPoints
|
|
41
|
+
let context: SynthesizedCoordinateContext
|
|
42
|
+
|
|
43
|
+
var referenceFrame: CGRect {
|
|
44
|
+
context.referenceFrame
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
39
48
|
struct SelectorElementMatch {
|
|
40
49
|
let element: XCUIElement?
|
|
41
50
|
let isAmbiguous: Bool
|
|
42
51
|
let usedNonHittableFallback: Bool
|
|
43
52
|
}
|
|
44
53
|
|
|
45
|
-
// MARK: - Navigation Gestures
|
|
46
|
-
|
|
47
|
-
func tapInAppBackControl(app: XCUIApplication) -> Bool {
|
|
48
|
-
#if os(macOS)
|
|
49
|
-
if let back = macOSNavigationBackElement(app: app) {
|
|
50
|
-
tapElementCenter(app: app, element: back)
|
|
51
|
-
return true
|
|
52
|
-
}
|
|
53
|
-
return false
|
|
54
|
-
#elseif os(tvOS)
|
|
55
|
-
_ = pressTvRemote(.menu)
|
|
56
|
-
return true
|
|
57
|
-
#else
|
|
58
|
-
let buttons = app.navigationBars.buttons.allElementsBoundByIndex
|
|
59
|
-
if let back = buttons.first(where: { $0.isHittable }) {
|
|
60
|
-
back.tap()
|
|
61
|
-
return true
|
|
62
|
-
}
|
|
63
|
-
return false
|
|
64
|
-
#endif
|
|
65
|
-
}
|
|
66
|
-
|
|
67
54
|
func performBackGesture(app: XCUIApplication) {
|
|
68
55
|
if pressTvRemote(.menu) {
|
|
69
56
|
return
|
|
@@ -787,11 +774,24 @@ extension RunnerTests {
|
|
|
787
774
|
y: Double,
|
|
788
775
|
x2: Double,
|
|
789
776
|
y2: Double,
|
|
790
|
-
durationMs: Double
|
|
777
|
+
durationMs: Double,
|
|
778
|
+
context: SynthesizedCoordinateContext? = nil
|
|
791
779
|
) -> RunnerInteractionOutcome {
|
|
792
780
|
#if os(iOS)
|
|
781
|
+
guard x.isFinite, y.isFinite, x2.isFinite, y2.isFinite else {
|
|
782
|
+
return .unsupported(
|
|
783
|
+
message: "synthesized coordinate drag requires finite coordinates",
|
|
784
|
+
hint: "Retry with finite x, y, x2, and y2 values."
|
|
785
|
+
)
|
|
786
|
+
}
|
|
793
787
|
let orientation = Int(RunnerSynthesizedGesture.interfaceOrientation(forApplication: app))
|
|
794
|
-
let
|
|
788
|
+
guard let context = context ?? synthesizedCoordinateContext(policy: synthesizedGesturePolicy(.synthesizedDrag)) else {
|
|
789
|
+
return .unsupported(
|
|
790
|
+
message: "synthesized coordinate drag could not resolve a finite screen frame",
|
|
791
|
+
hint: "Retry after the app is foregrounded, or use a plain screenshot to choose coordinates."
|
|
792
|
+
)
|
|
793
|
+
}
|
|
794
|
+
let frame = context.referenceFrame
|
|
795
795
|
let start = nativeSynthesizedPoint(orientedX: x, orientedY: y, in: frame, interfaceOrientation: orientation)
|
|
796
796
|
let end = nativeSynthesizedPoint(orientedX: x2, orientedY: y2, in: frame, interfaceOrientation: orientation)
|
|
797
797
|
if let message = RunnerSynthesizedGesture.synthesizeSwipe(
|
|
@@ -804,7 +804,7 @@ extension RunnerTests {
|
|
|
804
804
|
) {
|
|
805
805
|
return .unsupported(
|
|
806
806
|
message: message,
|
|
807
|
-
hint: "
|
|
807
|
+
hint: "Private XCTest event synthesis is required for AX-free coordinate drag on iOS; update Xcode if this persists."
|
|
808
808
|
)
|
|
809
809
|
}
|
|
810
810
|
return .performed
|
|
@@ -821,10 +821,28 @@ extension RunnerTests {
|
|
|
821
821
|
#endif
|
|
822
822
|
}
|
|
823
823
|
|
|
824
|
-
func synthesizedTapAt(
|
|
824
|
+
func synthesizedTapAt(
|
|
825
|
+
app: XCUIApplication,
|
|
826
|
+
x: Double,
|
|
827
|
+
y: Double,
|
|
828
|
+
context: SynthesizedCoordinateContext? = nil
|
|
829
|
+
) -> RunnerInteractionOutcome {
|
|
825
830
|
#if os(iOS)
|
|
831
|
+
guard x.isFinite, y.isFinite else {
|
|
832
|
+
return .unsupported(
|
|
833
|
+
message: "synthesized coordinate tap requires finite coordinates",
|
|
834
|
+
hint: "Retry with finite x and y values."
|
|
835
|
+
)
|
|
836
|
+
}
|
|
826
837
|
let orientation = Int(RunnerSynthesizedGesture.interfaceOrientation(forApplication: app))
|
|
827
|
-
let
|
|
838
|
+
guard let context = context ?? synthesizedCoordinateContext(policy: synthesizedGesturePolicy(.coordinateTap)) else {
|
|
839
|
+
return .unsupported(
|
|
840
|
+
message: "synthesized coordinate tap could not resolve a finite screen frame",
|
|
841
|
+
hint: "Retry after the app is foregrounded, or use a plain screenshot to choose coordinates."
|
|
842
|
+
)
|
|
843
|
+
}
|
|
844
|
+
let frame = context.referenceFrame
|
|
845
|
+
let point = nativeSynthesizedPoint(orientedX: x, orientedY: y, in: frame, interfaceOrientation: orientation)
|
|
828
846
|
if let message = RunnerSynthesizedGesture.synthesizeTap(
|
|
829
847
|
withApplication: app,
|
|
830
848
|
x: Double(point.x),
|
|
@@ -994,6 +1012,114 @@ extension RunnerTests {
|
|
|
994
1012
|
#endif
|
|
995
1013
|
}
|
|
996
1014
|
|
|
1015
|
+
func axFreeSynthesizedDragPlan(
|
|
1016
|
+
app: XCUIApplication,
|
|
1017
|
+
x: Double,
|
|
1018
|
+
y: Double,
|
|
1019
|
+
x2: Double,
|
|
1020
|
+
y2: Double,
|
|
1021
|
+
context: SynthesizedCoordinateContext? = nil
|
|
1022
|
+
) -> SynthesizedDragPlan? {
|
|
1023
|
+
#if os(iOS)
|
|
1024
|
+
let context = context ?? synthesizedCoordinateContext(policy: synthesizedGesturePolicy(.synthesizedDrag))
|
|
1025
|
+
guard x.isFinite, y.isFinite, x2.isFinite, y2.isFinite,
|
|
1026
|
+
let context
|
|
1027
|
+
else {
|
|
1028
|
+
return nil
|
|
1029
|
+
}
|
|
1030
|
+
let points = keyboardAvoidingSynthesizedDragPoints(
|
|
1031
|
+
app: app,
|
|
1032
|
+
x: x,
|
|
1033
|
+
y: y,
|
|
1034
|
+
x2: x2,
|
|
1035
|
+
y2: y2,
|
|
1036
|
+
context: context
|
|
1037
|
+
)
|
|
1038
|
+
return SynthesizedDragPlan(
|
|
1039
|
+
points: points,
|
|
1040
|
+
context: context
|
|
1041
|
+
)
|
|
1042
|
+
#else
|
|
1043
|
+
return nil
|
|
1044
|
+
#endif
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
func axFreeDragVisualizationFrame(
|
|
1048
|
+
x: Double,
|
|
1049
|
+
y: Double,
|
|
1050
|
+
x2: Double,
|
|
1051
|
+
y2: Double,
|
|
1052
|
+
referenceFrame: CGRect
|
|
1053
|
+
) -> DragVisualizationFrame {
|
|
1054
|
+
return DragVisualizationFrame(
|
|
1055
|
+
x: x,
|
|
1056
|
+
y: y,
|
|
1057
|
+
x2: x2,
|
|
1058
|
+
y2: y2,
|
|
1059
|
+
referenceWidth: Double(referenceFrame.width),
|
|
1060
|
+
referenceHeight: Double(referenceFrame.height)
|
|
1061
|
+
)
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
func synthesizedCoordinateContext(policy: SynthesizedGesturePolicy) -> SynthesizedCoordinateContext? {
|
|
1065
|
+
#if os(iOS)
|
|
1066
|
+
let health = runnerAccessibilityHealth
|
|
1067
|
+
guard let referenceFrame = synthesizedScreenshotReferenceFrame(
|
|
1068
|
+
screenshotSize: { XCUIScreen.main.screenshot().image.size }
|
|
1069
|
+
) else {
|
|
1070
|
+
return nil
|
|
1071
|
+
}
|
|
1072
|
+
return SynthesizedCoordinateContext(
|
|
1073
|
+
referenceFrame: referenceFrame,
|
|
1074
|
+
keyboardPolicy: policy.keyboardPolicy,
|
|
1075
|
+
fallbackPolicy: policy.fallbackPolicy,
|
|
1076
|
+
accessibilityHealth: health
|
|
1077
|
+
)
|
|
1078
|
+
#else
|
|
1079
|
+
return nil
|
|
1080
|
+
#endif
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
func synthesizedScreenshotReferenceFrame(screenshotSize: () -> CGSize) -> CGRect? {
|
|
1084
|
+
let screenshotSize = screenshotSize()
|
|
1085
|
+
guard screenshotSize.width.isFinite, screenshotSize.height.isFinite,
|
|
1086
|
+
screenshotSize.width > 0,
|
|
1087
|
+
screenshotSize.height > 0
|
|
1088
|
+
else {
|
|
1089
|
+
return nil
|
|
1090
|
+
}
|
|
1091
|
+
return CGRect(x: 0, y: 0, width: screenshotSize.width, height: screenshotSize.height)
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
func synthesizedFrameAvoidingKeyboardWhenAllowed(
|
|
1095
|
+
app: XCUIApplication,
|
|
1096
|
+
context: SynthesizedCoordinateContext
|
|
1097
|
+
) -> CGRect {
|
|
1098
|
+
#if os(iOS)
|
|
1099
|
+
guard context.allowsKeyboardProbe else { return context.referenceFrame }
|
|
1100
|
+
return frameAvoidingKeyboard(app: app, frame: context.referenceFrame)
|
|
1101
|
+
#else
|
|
1102
|
+
return context.referenceFrame
|
|
1103
|
+
#endif
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
func keyboardAvoidingSynthesizedDragPoints(
|
|
1107
|
+
app: XCUIApplication,
|
|
1108
|
+
x: Double,
|
|
1109
|
+
y: Double,
|
|
1110
|
+
x2: Double,
|
|
1111
|
+
y2: Double,
|
|
1112
|
+
context: SynthesizedCoordinateContext
|
|
1113
|
+
) -> DragPoints {
|
|
1114
|
+
#if os(iOS)
|
|
1115
|
+
guard context.allowsKeyboardProbe else {
|
|
1116
|
+
return DragPoints(x: x, y: y, x2: x2, y2: y2)
|
|
1117
|
+
}
|
|
1118
|
+
return keyboardAvoidingDragPoints(app: app, x: x, y: y, x2: x2, y2: y2)
|
|
1119
|
+
#else
|
|
1120
|
+
return DragPoints(x: x, y: y, x2: x2, y2: y2)
|
|
1121
|
+
#endif
|
|
1122
|
+
}
|
|
997
1123
|
|
|
998
1124
|
func swipe(app: XCUIApplication, direction: String) -> DragVisualizationFrame? {
|
|
999
1125
|
if performTvRemoteSwipeIfAvailable(direction: direction) {
|
|
@@ -1227,25 +1353,4 @@ extension RunnerTests {
|
|
|
1227
1353
|
}
|
|
1228
1354
|
#endif
|
|
1229
1355
|
|
|
1230
|
-
private func tapElementCenter(app: XCUIApplication, element: XCUIElement) {
|
|
1231
|
-
let frame = element.frame
|
|
1232
|
-
if !frame.isEmpty {
|
|
1233
|
-
_ = tapAt(app: app, x: frame.midX, y: frame.midY)
|
|
1234
|
-
return
|
|
1235
|
-
}
|
|
1236
|
-
#if !os(tvOS)
|
|
1237
|
-
element.tap()
|
|
1238
|
-
#endif
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
private func macOSNavigationBackElement(app: XCUIApplication) -> XCUIElement? {
|
|
1242
|
-
let predicate = NSPredicate(
|
|
1243
|
-
format: "identifier == %@ OR label == %@",
|
|
1244
|
-
"go back",
|
|
1245
|
-
"Back"
|
|
1246
|
-
)
|
|
1247
|
-
let element = app.descendants(matching: .any).matching(predicate).firstMatch
|
|
1248
|
-
return element.exists ? element : nil
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
1356
|
}
|
package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
|
|
3
|
+
extension RunnerTests {
|
|
4
|
+
static let navigationBackKeywords = ["back", "close", "cancel"]
|
|
5
|
+
static let navigationFallbackVerificationDelay: TimeInterval = 0.25
|
|
6
|
+
|
|
7
|
+
func tapInAppBackControl(app: XCUIApplication) -> Bool {
|
|
8
|
+
#if os(macOS)
|
|
9
|
+
if let back = macOSNavigationBackElement(app: app) {
|
|
10
|
+
tapElementCenter(app: app, element: back)
|
|
11
|
+
return true
|
|
12
|
+
}
|
|
13
|
+
return false
|
|
14
|
+
#elseif os(tvOS)
|
|
15
|
+
_ = pressTvRemote(.menu)
|
|
16
|
+
return true
|
|
17
|
+
#else
|
|
18
|
+
let buttons = app.navigationBars.buttons.allElementsBoundByIndex
|
|
19
|
+
if let back = buttons.first(where: { $0.isHittable }) {
|
|
20
|
+
back.tap()
|
|
21
|
+
return true
|
|
22
|
+
}
|
|
23
|
+
if isSnapshotXCTestChannelPenalized(bundleId: currentBundleId) {
|
|
24
|
+
NSLog("AGENT_DEVICE_RUNNER_IN_APP_BACK_SKIPPED_XCTEST_ENUMERATION bundle=%@", currentBundleId ?? "")
|
|
25
|
+
} else if let back = topNavigationBackElement(app: app) {
|
|
26
|
+
tapElementCenter(app: app, element: back)
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
return tapTopLeadingNavigationFallback(app: app)
|
|
30
|
+
#endif
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private func tapElementCenter(app: XCUIApplication, element: XCUIElement) {
|
|
34
|
+
let frame = element.frame
|
|
35
|
+
if !frame.isEmpty {
|
|
36
|
+
_ = tapAt(app: app, x: frame.midX, y: frame.midY)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
#if !os(tvOS)
|
|
40
|
+
element.tap()
|
|
41
|
+
#endif
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private func topNavigationBackElement(app: XCUIApplication) -> XCUIElement? {
|
|
45
|
+
#if os(iOS)
|
|
46
|
+
let frame = onScreenWindowFrame(app: app)
|
|
47
|
+
let candidates = app.buttons.matching(Self.navigationBackPredicate()).allElementsBoundByIndex.compactMap {
|
|
48
|
+
element -> (XCUIElement, Int)? in
|
|
49
|
+
guard element.exists, element.isHittable else { return nil }
|
|
50
|
+
guard Self.isTopNavigationControlFrame(element.frame, in: frame) else { return nil }
|
|
51
|
+
guard let rank = Self.navigationBackControlRank(
|
|
52
|
+
label: element.label,
|
|
53
|
+
identifier: element.identifier
|
|
54
|
+
) else {
|
|
55
|
+
return nil
|
|
56
|
+
}
|
|
57
|
+
return (element, rank)
|
|
58
|
+
}
|
|
59
|
+
return candidates.sorted { lhs, rhs in
|
|
60
|
+
if lhs.1 != rhs.1 { return lhs.1 < rhs.1 }
|
|
61
|
+
let leftFrame = lhs.0.frame
|
|
62
|
+
let rightFrame = rhs.0.frame
|
|
63
|
+
if leftFrame.minY != rightFrame.minY { return leftFrame.minY < rightFrame.minY }
|
|
64
|
+
return leftFrame.minX < rightFrame.minX
|
|
65
|
+
}.first?.0
|
|
66
|
+
#else
|
|
67
|
+
return nil
|
|
68
|
+
#endif
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static func navigationBackPredicate() -> NSPredicate {
|
|
72
|
+
let clauses = navigationBackKeywords.flatMap { _ in
|
|
73
|
+
["label CONTAINS[c] %@", "identifier CONTAINS[c] %@"]
|
|
74
|
+
}.joined(separator: " OR ")
|
|
75
|
+
let arguments = navigationBackKeywords.flatMap { [$0, $0] }
|
|
76
|
+
return NSPredicate(format: clauses, argumentArray: arguments)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static func navigationBackControlRank(label: String, identifier: String) -> Int? {
|
|
80
|
+
let text = "\(label) \(identifier)".lowercased()
|
|
81
|
+
return navigationBackKeywords.firstIndex { text.contains($0) }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
static func isTopNavigationControlFrame(_ candidate: CGRect, in window: CGRect) -> Bool {
|
|
85
|
+
guard
|
|
86
|
+
candidate.width.isFinite,
|
|
87
|
+
candidate.height.isFinite,
|
|
88
|
+
window.width.isFinite,
|
|
89
|
+
window.height.isFinite,
|
|
90
|
+
candidate.width > 0,
|
|
91
|
+
candidate.height > 0,
|
|
92
|
+
window.width > 0,
|
|
93
|
+
window.height > 0
|
|
94
|
+
else {
|
|
95
|
+
return false
|
|
96
|
+
}
|
|
97
|
+
// Accept the compact navigation/search header band without matching deep content controls.
|
|
98
|
+
let maxY = window.minY + min(max(window.height * 0.22, 96), 180)
|
|
99
|
+
return candidate.midY >= window.minY && candidate.midY <= maxY
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
static func topLeadingNavigationFallbackPoint(in frame: CGRect) -> CGPoint? {
|
|
103
|
+
guard frame.width.isFinite, frame.height.isFinite, frame.width > 0, frame.height > 0 else {
|
|
104
|
+
return nil
|
|
105
|
+
}
|
|
106
|
+
// Aim at the standard leading navigation slot, bounded for compact and tablet widths.
|
|
107
|
+
let xOffset = min(max(frame.width * 0.08, 28), 44)
|
|
108
|
+
// Sit below the status/dynamic-island region and inside common custom RN search headers.
|
|
109
|
+
let yOffset = min(max(frame.height * 0.155, 56), 132)
|
|
110
|
+
return CGPoint(x: frame.minX + xOffset, y: frame.minY + yOffset)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private func tapTopLeadingNavigationFallback(app: XCUIApplication) -> Bool {
|
|
114
|
+
#if os(iOS)
|
|
115
|
+
let frame = onScreenWindowFrame(app: app)
|
|
116
|
+
guard let point = Self.topLeadingNavigationFallbackPoint(in: frame) else {
|
|
117
|
+
return false
|
|
118
|
+
}
|
|
119
|
+
let before = captureNavigationFallbackVisualState()
|
|
120
|
+
let context = synthesizedCoordinateContext(
|
|
121
|
+
policy: synthesizedGesturePolicy(.coordinateTap)
|
|
122
|
+
)?.withReferenceFrame(frame)
|
|
123
|
+
let synthesized = performGesture(app, idleTimeout: false) {
|
|
124
|
+
synthesizedTapAt(app: app, x: point.x, y: point.y, context: context)
|
|
125
|
+
}
|
|
126
|
+
if case .performed = synthesized.outcome {
|
|
127
|
+
return didNavigationFallbackChangeVisualState(before: before)
|
|
128
|
+
}
|
|
129
|
+
let fallback = performGesture(app) {
|
|
130
|
+
tapAt(app: app, x: point.x, y: point.y)
|
|
131
|
+
}
|
|
132
|
+
if case .performed = fallback.outcome {
|
|
133
|
+
return didNavigationFallbackChangeVisualState(before: before)
|
|
134
|
+
}
|
|
135
|
+
#endif
|
|
136
|
+
return false
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private func captureNavigationFallbackVisualState() -> Data? {
|
|
140
|
+
#if os(iOS)
|
|
141
|
+
runnerPngData(for: XCUIScreen.main.screenshot().image)
|
|
142
|
+
#else
|
|
143
|
+
return nil
|
|
144
|
+
#endif
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private func didNavigationFallbackChangeVisualState(before: Data?) -> Bool {
|
|
148
|
+
sleepFor(Self.navigationFallbackVerificationDelay)
|
|
149
|
+
let after = captureNavigationFallbackVisualState()
|
|
150
|
+
let changed = Self.didNavigationFallbackChangeVisualState(before: before, after: after)
|
|
151
|
+
if !changed {
|
|
152
|
+
NSLog("AGENT_DEVICE_RUNNER_IN_APP_BACK_FALLBACK_NO_STATE_CHANGE")
|
|
153
|
+
}
|
|
154
|
+
return changed
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
static func didNavigationFallbackChangeVisualState(before: Data?, after: Data?) -> Bool {
|
|
158
|
+
guard let before, let after else { return false }
|
|
159
|
+
return before != after
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private func macOSNavigationBackElement(app: XCUIApplication) -> XCUIElement? {
|
|
163
|
+
let predicate = NSPredicate(
|
|
164
|
+
format: "identifier == %@ OR label == %@",
|
|
165
|
+
"go back",
|
|
166
|
+
"Back"
|
|
167
|
+
)
|
|
168
|
+
let element = app.descendants(matching: .any).matching(predicate).firstMatch
|
|
169
|
+
return element.exists ? element : nil
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
}
|
|
@@ -40,8 +40,10 @@ extension RunnerTests {
|
|
|
40
40
|
? resolvedTouchVisualizationFrame(app: activeApp, x: firstStep.x!, y: firstStep.y!)
|
|
41
41
|
: nil
|
|
42
42
|
|
|
43
|
+
let synthesizedContext = synthesizedSequenceCoordinateContext(steps: steps)
|
|
44
|
+
|
|
43
45
|
let execution = assembleSequenceExecution(steps: steps) { _, step in
|
|
44
|
-
performSequenceStep(step, activeApp: activeApp)
|
|
46
|
+
performSequenceStep(step, activeApp: activeApp, synthesizedContext: synthesizedContext)
|
|
45
47
|
}
|
|
46
48
|
return sequenceResponse(execution: execution, touchFrame: firstFrame)
|
|
47
49
|
}
|
|
@@ -141,7 +143,8 @@ extension RunnerTests {
|
|
|
141
143
|
|
|
142
144
|
private func performSequenceStep(
|
|
143
145
|
_ step: SequenceStep,
|
|
144
|
-
activeApp: XCUIApplication
|
|
146
|
+
activeApp: XCUIApplication,
|
|
147
|
+
synthesizedContext: SynthesizedCoordinateContext? = nil
|
|
145
148
|
) -> SequenceStepOutcome {
|
|
146
149
|
let x = step.x ?? 0
|
|
147
150
|
let y = step.y ?? 0
|
|
@@ -149,10 +152,26 @@ extension RunnerTests {
|
|
|
149
152
|
// a tapAt fallback when synthesis is unsupported), so fusing a jittered tap series does not
|
|
150
153
|
// change the touch mechanism for these inputs.
|
|
151
154
|
if step.kind == "tap", step.synthesized == true {
|
|
155
|
+
let policyKind = SynthesizedGesturePolicyKind.synthesizedDrag
|
|
156
|
+
#if os(iOS)
|
|
157
|
+
guard let synthesizedContext else {
|
|
158
|
+
let nowMs = ProcessInfo.processInfo.systemUptime * 1000
|
|
159
|
+
logSynthesizedGesturePolicyDecision(kind: policyKind, context: nil, fallbackAttempted: false)
|
|
160
|
+
return SequenceStepOutcome(
|
|
161
|
+
outcome: .unsupported(
|
|
162
|
+
message: "synthesized coordinate tap could not resolve a finite coordinate frame",
|
|
163
|
+
hint: "Retry after the app is foregrounded, or use a plain screenshot to choose coordinates."
|
|
164
|
+
),
|
|
165
|
+
gestureStartUptimeMs: nowMs,
|
|
166
|
+
gestureEndUptimeMs: nowMs
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
#endif
|
|
152
170
|
let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
|
|
153
|
-
synthesizedTapAt(app: activeApp, x: x, y: y)
|
|
171
|
+
synthesizedTapAt(app: activeApp, x: x, y: y, context: synthesizedContext)
|
|
154
172
|
}
|
|
155
173
|
if case .performed = outcome {
|
|
174
|
+
logSynthesizedGesturePolicyDecision(kind: policyKind, context: synthesizedContext, fallbackAttempted: false)
|
|
156
175
|
if let pauseMs = step.pauseMs, pauseMs > 0 {
|
|
157
176
|
sleepFor(min(max(pauseMs, 0), 10000) / 1000.0)
|
|
158
177
|
}
|
|
@@ -162,11 +181,50 @@ extension RunnerTests {
|
|
|
162
181
|
gestureEndUptimeMs: timing.gestureEndUptimeMs
|
|
163
182
|
)
|
|
164
183
|
}
|
|
184
|
+
#if os(iOS)
|
|
185
|
+
guard synthesizedContext.allowsXCTestCoordinateFallback else {
|
|
186
|
+
logSynthesizedGesturePolicyDecision(kind: policyKind, context: synthesizedContext, fallbackAttempted: false)
|
|
187
|
+
return SequenceStepOutcome(
|
|
188
|
+
outcome: outcome,
|
|
189
|
+
gestureStartUptimeMs: timing.gestureStartUptimeMs,
|
|
190
|
+
gestureEndUptimeMs: timing.gestureEndUptimeMs
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
logSynthesizedGesturePolicyDecision(kind: policyKind, context: synthesizedContext, fallbackAttempted: true)
|
|
194
|
+
#endif
|
|
165
195
|
// Synthesis unsupported (e.g. macOS) — fall through to the drag-based tapAt below.
|
|
166
196
|
}
|
|
167
197
|
if step.kind == "drag", step.synthesized == true {
|
|
168
|
-
let
|
|
198
|
+
let policyKind = SynthesizedGesturePolicyKind.synthesizedDrag
|
|
199
|
+
let dragPoints: DragPoints
|
|
200
|
+
let dragContext: SynthesizedCoordinateContext?
|
|
201
|
+
#if os(iOS)
|
|
202
|
+
guard let dragPlan = axFreeSynthesizedDragPlan(
|
|
203
|
+
app: activeApp,
|
|
204
|
+
x: x,
|
|
205
|
+
y: y,
|
|
206
|
+
x2: step.x2 ?? x,
|
|
207
|
+
y2: step.y2 ?? y,
|
|
208
|
+
context: synthesizedContext
|
|
209
|
+
) else {
|
|
210
|
+
let nowMs = ProcessInfo.processInfo.systemUptime * 1000
|
|
211
|
+
logSynthesizedGesturePolicyDecision(kind: policyKind, context: synthesizedContext, fallbackAttempted: false)
|
|
212
|
+
return SequenceStepOutcome(
|
|
213
|
+
outcome: .unsupported(
|
|
214
|
+
message: "synthesized coordinate drag could not resolve a finite coordinate frame",
|
|
215
|
+
hint: "Retry after the app is foregrounded, or use a plain screenshot to choose coordinates."
|
|
216
|
+
),
|
|
217
|
+
gestureStartUptimeMs: nowMs,
|
|
218
|
+
gestureEndUptimeMs: nowMs
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
dragPoints = dragPlan.points
|
|
222
|
+
dragContext = dragPlan.context
|
|
223
|
+
#else
|
|
224
|
+
dragPoints = keyboardAvoidingDragPoints(
|
|
169
225
|
app: activeApp, x: x, y: y, x2: step.x2 ?? x, y2: step.y2 ?? y)
|
|
226
|
+
dragContext = nil
|
|
227
|
+
#endif
|
|
170
228
|
let durationMs = min(max(step.durationMs ?? 250, 16), 10000)
|
|
171
229
|
let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
|
|
172
230
|
synthesizedDragAt(
|
|
@@ -175,10 +233,12 @@ extension RunnerTests {
|
|
|
175
233
|
y: dragPoints.y,
|
|
176
234
|
x2: dragPoints.x2,
|
|
177
235
|
y2: dragPoints.y2,
|
|
178
|
-
durationMs: durationMs
|
|
236
|
+
durationMs: durationMs,
|
|
237
|
+
context: dragContext
|
|
179
238
|
)
|
|
180
239
|
}
|
|
181
240
|
if case .performed = outcome {
|
|
241
|
+
logSynthesizedGesturePolicyDecision(kind: policyKind, context: dragContext, fallbackAttempted: false)
|
|
182
242
|
if let pauseMs = step.pauseMs, pauseMs > 0 {
|
|
183
243
|
sleepFor(min(max(pauseMs, 0), 10000) / 1000.0)
|
|
184
244
|
}
|
|
@@ -188,6 +248,17 @@ extension RunnerTests {
|
|
|
188
248
|
gestureEndUptimeMs: timing.gestureEndUptimeMs
|
|
189
249
|
)
|
|
190
250
|
}
|
|
251
|
+
#if os(iOS)
|
|
252
|
+
guard dragContext?.allowsXCTestCoordinateFallback == true else {
|
|
253
|
+
logSynthesizedGesturePolicyDecision(kind: policyKind, context: dragContext, fallbackAttempted: false)
|
|
254
|
+
return SequenceStepOutcome(
|
|
255
|
+
outcome: outcome,
|
|
256
|
+
gestureStartUptimeMs: timing.gestureStartUptimeMs,
|
|
257
|
+
gestureEndUptimeMs: timing.gestureEndUptimeMs
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
#endif
|
|
261
|
+
logSynthesizedGesturePolicyDecision(kind: policyKind, context: dragContext, fallbackAttempted: true)
|
|
191
262
|
let fallbackHoldDuration = synthesizedSwipeFallbackHoldDuration(durationMs: step.durationMs ?? 250)
|
|
192
263
|
let (fallbackTiming, fallbackOutcome) = performGesture(activeApp) {
|
|
193
264
|
dragAt(
|