agent-device 0.17.1 → 0.17.3
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 +3 -3
- package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.17.1.apk → agent-device-android-multitouch-helper-0.17.3.apk} +0 -0
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.17.3.apk.sha256 +1 -0
- package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.17.1.manifest.json → agent-device-android-multitouch-helper-0.17.3.manifest.json} +4 -4
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.17.1.apk → agent-device-android-snapshot-helper-0.17.3.apk} +0 -0
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.17.3.apk.sha256 +1 -0
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.17.1.manifest.json → agent-device-android-snapshot-helper-0.17.3.manifest.json} +6 -6
- package/dist/src/123.js +1 -0
- package/dist/src/1231.js +1 -1
- package/dist/src/1352.js +1 -1
- package/dist/src/221.js +2 -2
- package/dist/src/2403.js +3 -0
- package/dist/src/2415.js +36 -30
- package/dist/src/4778.js +1 -1
- package/dist/src/5611.js +1 -0
- package/dist/src/5827.js +1 -0
- package/dist/src/6088.js +1 -0
- package/dist/src/6133.js +1 -0
- package/dist/src/7556.js +1 -1
- package/dist/src/7847.js +1 -1
- package/dist/src/8806.js +3 -3
- package/dist/src/9010.js +1 -0
- package/dist/src/9238.js +3 -3
- package/dist/src/9471.js +1 -1
- package/dist/src/9542.js +6 -3
- package/dist/src/9616.js +2 -0
- package/dist/src/9818.js +1 -1
- package/dist/src/android-adb.d.ts +12 -6
- package/dist/src/android-adb.js +1 -1
- package/dist/src/android-snapshot-helper.d.ts +15 -8
- package/dist/src/android.js +5 -5
- package/dist/src/apple.js +1 -1
- package/dist/src/apps.js +10 -10
- package/dist/src/args.js +53 -14
- package/dist/src/batch.d.ts +16 -4
- package/dist/src/batch.js +1 -1
- package/dist/src/cli-output.js +29 -0
- package/dist/src/cli.js +17 -42
- package/dist/src/command-surface.js +1 -1
- package/dist/src/contracts.d.ts +80 -4
- package/dist/src/contracts.js +1 -1
- package/dist/src/find.js +1 -1
- package/dist/src/finders.d.ts +3 -1
- package/dist/src/generic.js +7 -7
- package/dist/src/index.d.ts +250 -79
- package/dist/src/index.js +1 -1
- package/dist/src/input-actions.js +1 -1
- package/dist/src/interaction.js +1 -1
- package/dist/src/internal/png-worker.d.ts +26 -0
- package/dist/src/internal/png-worker.js +1 -0
- package/dist/src/metro.d.ts +3 -1
- package/dist/src/react-native.js +1 -1
- package/dist/src/record-trace-recording.js +3 -3
- package/dist/src/remote-config.d.ts +33 -7
- package/dist/src/selector-runtime.js +1 -1
- package/dist/src/selectors.d.ts +17 -3
- package/dist/src/selectors.js +1 -1
- package/dist/src/server.js +2 -2
- package/dist/src/session.js +8 -11
- package/dist/src/snapshot.js +2 -2
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +1 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.h +14 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.m +297 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +328 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +121 -172
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +153 -5
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +171 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +0 -10
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +64 -18
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +200 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SequenceExecution.swift +406 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +82 -169
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +425 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +77 -6
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +10 -1
- package/ios-runner/README.md +5 -5
- package/ios-runner/RUNNER_PROTOCOL.md +0 -1
- package/package.json +8 -5
- package/server.json +2 -2
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.17.1.apk.sha256 +0 -1
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.17.1.apk.sha256 +0 -1
- package/dist/src/1998.js +0 -1
- package/dist/src/2805.js +0 -1
- package/dist/src/4057.js +0 -1
- package/dist/src/5310.js +0 -1
- package/dist/src/5792.js +0 -1
- package/dist/src/6085.js +0 -1
- package/dist/src/6232.js +0 -1
- package/dist/src/7455.js +0 -1
- package/dist/src/8020.js +0 -1
- package/dist/src/8502.js +0 -1
- package/dist/src/8699.js +0 -1
- package/dist/src/940.js +0 -1
- package/dist/src/9404.js +0 -1
- package/dist/src/9533.js +0 -1
- package/dist/src/command-metadata.js +0 -1
- /package/dist/src/{1393.js → 3675.js} +0 -0
package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
|
|
3
|
+
extension RunnerTests {
|
|
4
|
+
private static let privateAXSnapshotMaxNodes = 5_000
|
|
5
|
+
/// Deep React Native trees make the AX server reject bulk snapshot requests outright with
|
|
6
|
+
/// kAXErrorIllegalArgument once the requested depth crosses a tree-size-dependent limit
|
|
7
|
+
/// (observed between depth 56 and 64 on the Bluesky Home feed; the limit moves with live
|
|
8
|
+
/// content). Retrying the same request at a shallower depth succeeds, so on failure we walk
|
|
9
|
+
/// this ladder instead of giving up. Capped at 4 attempts to bound worst-case latency on
|
|
10
|
+
/// apps where the AX surface is genuinely unavailable.
|
|
11
|
+
static let privateAXSnapshotDepthLadder = [56, 40, 24, 12]
|
|
12
|
+
|
|
13
|
+
func privateAXSnapshotCapture(
|
|
14
|
+
app: XCUIApplication,
|
|
15
|
+
options: SnapshotOptions
|
|
16
|
+
) -> SnapshotBackendCapture? {
|
|
17
|
+
#if os(iOS) && targetEnvironment(simulator)
|
|
18
|
+
let requestedDepth = options.depth ?? 64
|
|
19
|
+
var attemptDepths = [requestedDepth]
|
|
20
|
+
attemptDepths.append(
|
|
21
|
+
contentsOf: Self.privateAXSnapshotDepthLadder.filter { $0 < requestedDepth }
|
|
22
|
+
)
|
|
23
|
+
var response: [String: Any] = [:]
|
|
24
|
+
var effectiveDepth = requestedDepth
|
|
25
|
+
var lastError = "unknown private AX snapshot failure"
|
|
26
|
+
for depth in attemptDepths {
|
|
27
|
+
response = RunnerAXSnapshotBridge.snapshotTree(
|
|
28
|
+
for: app,
|
|
29
|
+
maxDepth: depth,
|
|
30
|
+
maxNodes: Self.privateAXSnapshotMaxNodes
|
|
31
|
+
)
|
|
32
|
+
if response["ok"] as? Bool == true {
|
|
33
|
+
effectiveDepth = depth
|
|
34
|
+
break
|
|
35
|
+
}
|
|
36
|
+
lastError = response["error"] as? String ?? lastError
|
|
37
|
+
NSLog(
|
|
38
|
+
"AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_DEPTH_RETRY depth=%ld error=%@",
|
|
39
|
+
depth,
|
|
40
|
+
lastError
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
guard response["ok"] as? Bool == true else {
|
|
44
|
+
NSLog("AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_FAILED=%@", lastError)
|
|
45
|
+
return nil
|
|
46
|
+
}
|
|
47
|
+
guard let root = response["root"] as? [String: Any] else {
|
|
48
|
+
NSLog("AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_FAILED=missing root")
|
|
49
|
+
return nil
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The public windows query backing safeSnapshotViewport can fail on the same apps that
|
|
53
|
+
// need this fallback, degrading to an infinite viewport that marks off-screen content
|
|
54
|
+
// (e.g. closed drawer menus at negative x) as visible and tappable. The private root's
|
|
55
|
+
// own frame is the reliable screen rect here.
|
|
56
|
+
var viewport = safeSnapshotViewport(app: app)
|
|
57
|
+
let rootFrame = privateAXRect(root["frame"])
|
|
58
|
+
if viewport.isInfinite || viewport.isNull || viewport.isEmpty, !rootFrame.isEmpty {
|
|
59
|
+
viewport = rootFrame
|
|
60
|
+
}
|
|
61
|
+
var nodes: [SnapshotNode] = []
|
|
62
|
+
appendPrivateAXNode(
|
|
63
|
+
root,
|
|
64
|
+
to: &nodes,
|
|
65
|
+
options: options,
|
|
66
|
+
viewport: viewport,
|
|
67
|
+
depth: 0,
|
|
68
|
+
parentIndex: nil,
|
|
69
|
+
insideMatchedScope: false
|
|
70
|
+
)
|
|
71
|
+
if nodes.count <= 1 {
|
|
72
|
+
NSLog("AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_SPARSE=%ld", nodes.count)
|
|
73
|
+
return nil
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let depthLimited = effectiveDepth < requestedDepth
|
|
77
|
+
NSLog(
|
|
78
|
+
"AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_USED nodes=%ld depth=%ld",
|
|
79
|
+
nodes.count,
|
|
80
|
+
effectiveDepth
|
|
81
|
+
)
|
|
82
|
+
return SnapshotBackendCapture(
|
|
83
|
+
payload: DataPayload(nodes: nodes, truncated: (response["truncated"] as? Bool) == true),
|
|
84
|
+
effectiveDepth: depthLimited ? effectiveDepth : nil
|
|
85
|
+
)
|
|
86
|
+
#else
|
|
87
|
+
return nil
|
|
88
|
+
#endif
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private func appendPrivateAXNode(
|
|
92
|
+
_ rawNode: [String: Any],
|
|
93
|
+
to nodes: inout [SnapshotNode],
|
|
94
|
+
options: SnapshotOptions,
|
|
95
|
+
viewport: CGRect,
|
|
96
|
+
depth: Int,
|
|
97
|
+
parentIndex: Int?,
|
|
98
|
+
insideMatchedScope: Bool
|
|
99
|
+
) {
|
|
100
|
+
if let limit = options.depth, depth > limit { return }
|
|
101
|
+
|
|
102
|
+
let rect = privateAXRect(rawNode["frame"])
|
|
103
|
+
let label = privateAXString(rawNode["label"])
|
|
104
|
+
let identifier = privateAXString(rawNode["identifier"])
|
|
105
|
+
let value = privateAXString(rawNode["value"])
|
|
106
|
+
let rawType = privateAXInt(rawNode["type"]) ?? 0
|
|
107
|
+
let typeName = elementTypeName(rawElementType: rawType)
|
|
108
|
+
let enabled = privateAXBool(rawNode["enabled"]) ?? true
|
|
109
|
+
let visible = isVisibleInViewport(rect, viewport)
|
|
110
|
+
let interactiveCandidate = privateAXInteractiveCandidate(rawElementType: rawType)
|
|
111
|
+
let filterDecision = flatSnapshotFilterDecision(
|
|
112
|
+
FlatSnapshotFilterNode(
|
|
113
|
+
isRoot: parentIndex == nil,
|
|
114
|
+
label: label,
|
|
115
|
+
identifier: identifier,
|
|
116
|
+
valueText: value.isEmpty ? nil : value,
|
|
117
|
+
visible: visible
|
|
118
|
+
),
|
|
119
|
+
options: options,
|
|
120
|
+
insideMatchedScope: insideMatchedScope
|
|
121
|
+
)
|
|
122
|
+
let include = filterDecision.include
|
|
123
|
+
let nowInsideScope = filterDecision.insideMatchedScope
|
|
124
|
+
|
|
125
|
+
let currentIndex: Int?
|
|
126
|
+
if include {
|
|
127
|
+
currentIndex = nodes.count
|
|
128
|
+
nodes.append(
|
|
129
|
+
SnapshotNode(
|
|
130
|
+
index: nodes.count,
|
|
131
|
+
type: typeName,
|
|
132
|
+
label: label.isEmpty ? nil : label,
|
|
133
|
+
identifier: identifier.isEmpty ? nil : identifier,
|
|
134
|
+
value: value.isEmpty ? nil : value,
|
|
135
|
+
rect: snapshotRect(from: rect),
|
|
136
|
+
enabled: enabled,
|
|
137
|
+
focused: privateAXBool(rawNode["focused"]) == true ? true : nil,
|
|
138
|
+
selected: privateAXBool(rawNode["selected"]) == true ? true : nil,
|
|
139
|
+
hittable: visible && enabled && interactiveCandidate,
|
|
140
|
+
depth: depth,
|
|
141
|
+
parentIndex: parentIndex,
|
|
142
|
+
hiddenContentAbove: nil,
|
|
143
|
+
hiddenContentBelow: nil
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
} else {
|
|
147
|
+
currentIndex = parentIndex
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
guard let children = rawNode["children"] as? [[String: Any]] else {
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
for child in children {
|
|
154
|
+
appendPrivateAXNode(
|
|
155
|
+
child,
|
|
156
|
+
to: &nodes,
|
|
157
|
+
options: options,
|
|
158
|
+
viewport: viewport,
|
|
159
|
+
depth: depth + 1,
|
|
160
|
+
parentIndex: currentIndex,
|
|
161
|
+
insideMatchedScope: nowInsideScope
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private func elementTypeName(rawElementType: Int) -> String {
|
|
167
|
+
if let type = flatSnapshotElementType(rawElementType: rawElementType) {
|
|
168
|
+
return elementTypeName(type)
|
|
169
|
+
}
|
|
170
|
+
return "Element(\(rawElementType))"
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private func privateAXString(_ value: Any?) -> String {
|
|
174
|
+
guard let value else { return "" }
|
|
175
|
+
if let string = value as? String {
|
|
176
|
+
return string.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
177
|
+
}
|
|
178
|
+
return String(describing: value).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private func privateAXInt(_ value: Any?) -> Int? {
|
|
182
|
+
if let value = value as? Int { return value }
|
|
183
|
+
if let value = value as? NSNumber { return value.intValue }
|
|
184
|
+
return nil
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private func privateAXBool(_ value: Any?) -> Bool? {
|
|
188
|
+
if let value = value as? Bool { return value }
|
|
189
|
+
if let value = value as? NSNumber { return value.boolValue }
|
|
190
|
+
return nil
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private func privateAXRect(_ value: Any?) -> CGRect {
|
|
194
|
+
guard let frame = value as? [String: Any] else {
|
|
195
|
+
return .zero
|
|
196
|
+
}
|
|
197
|
+
return CGRect(
|
|
198
|
+
x: privateAXDouble(frame["x"]) ?? 0,
|
|
199
|
+
y: privateAXDouble(frame["y"]) ?? 0,
|
|
200
|
+
width: privateAXDouble(frame["width"]) ?? 0,
|
|
201
|
+
height: privateAXDouble(frame["height"]) ?? 0
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private func privateAXDouble(_ value: Any?) -> Double? {
|
|
206
|
+
if let value = value as? Double { return value }
|
|
207
|
+
if let value = value as? NSNumber { return value.doubleValue }
|
|
208
|
+
return nil
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// MARK: - In-bundle unit tests
|
|
213
|
+
|
|
214
|
+
extension RunnerTests {
|
|
215
|
+
func testPrivateAXScopeSelectsSubtreeNotMatchingLabels() {
|
|
216
|
+
let tree: [String: Any] = [
|
|
217
|
+
"type": 1, "label": "App",
|
|
218
|
+
"children": [
|
|
219
|
+
[
|
|
220
|
+
"type": 9, "identifier": "homeScreen",
|
|
221
|
+
"children": [
|
|
222
|
+
["type": 48, "label": "Post body without the scope text", "children": []]
|
|
223
|
+
],
|
|
224
|
+
],
|
|
225
|
+
["type": 9, "label": "unrelated sibling", "children": []],
|
|
226
|
+
],
|
|
227
|
+
]
|
|
228
|
+
var nodes: [SnapshotNode] = []
|
|
229
|
+
appendPrivateAXNode(
|
|
230
|
+
tree,
|
|
231
|
+
to: &nodes,
|
|
232
|
+
options: SnapshotOptions(interactiveOnly: false, depth: nil, scope: "homeScreen", raw: false),
|
|
233
|
+
viewport: .infinite,
|
|
234
|
+
depth: 0,
|
|
235
|
+
parentIndex: nil,
|
|
236
|
+
insideMatchedScope: false
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
let labels = nodes.compactMap { $0.label ?? $0.identifier }
|
|
240
|
+
XCTAssertTrue(labels.contains("homeScreen"))
|
|
241
|
+
// Descendants of the matched scope are included even when they do not contain the text.
|
|
242
|
+
XCTAssertTrue(labels.contains("Post body without the scope text"))
|
|
243
|
+
XCTAssertFalse(labels.contains("unrelated sibling"))
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
func testPrivateAXInteractiveFiltersLoginLikeHiddenDrawer() {
|
|
247
|
+
let tree: [String: Any] = [
|
|
248
|
+
"type": Int(XCUIElement.ElementType.application.rawValue),
|
|
249
|
+
"label": "Blue Sky",
|
|
250
|
+
"frame": ["x": 0, "y": 0, "width": 390, "height": 844],
|
|
251
|
+
"children": [
|
|
252
|
+
[
|
|
253
|
+
"type": Int(XCUIElement.ElementType.scrollView.rawValue),
|
|
254
|
+
"frame": ["x": 0, "y": 0, "width": 390, "height": 844],
|
|
255
|
+
"children": [
|
|
256
|
+
[
|
|
257
|
+
"type": Int(XCUIElement.ElementType.image.rawValue),
|
|
258
|
+
"label": "Callstack",
|
|
259
|
+
"frame": ["x": 145, "y": 104, "width": 100, "height": 100],
|
|
260
|
+
"children": [],
|
|
261
|
+
],
|
|
262
|
+
[
|
|
263
|
+
"type": Int(XCUIElement.ElementType.staticText.rawValue),
|
|
264
|
+
"label": "Welcome back",
|
|
265
|
+
"frame": ["x": 32, "y": 260, "width": 326, "height": 32],
|
|
266
|
+
"children": [],
|
|
267
|
+
],
|
|
268
|
+
[
|
|
269
|
+
"type": Int(XCUIElement.ElementType.textField.rawValue),
|
|
270
|
+
"label": "Email",
|
|
271
|
+
"identifier": "login.email",
|
|
272
|
+
"frame": ["x": 32, "y": 348, "width": 326, "height": 48],
|
|
273
|
+
"children": [],
|
|
274
|
+
],
|
|
275
|
+
[
|
|
276
|
+
"type": Int(XCUIElement.ElementType.secureTextField.rawValue),
|
|
277
|
+
"label": "Password",
|
|
278
|
+
"identifier": "login.password",
|
|
279
|
+
"frame": ["x": 32, "y": 412, "width": 326, "height": 48],
|
|
280
|
+
"children": [],
|
|
281
|
+
],
|
|
282
|
+
[
|
|
283
|
+
"type": Int(XCUIElement.ElementType.button.rawValue),
|
|
284
|
+
"label": "Sign in",
|
|
285
|
+
"identifier": "login.submit",
|
|
286
|
+
"frame": ["x": 32, "y": 492, "width": 326, "height": 52],
|
|
287
|
+
"children": [],
|
|
288
|
+
],
|
|
289
|
+
[
|
|
290
|
+
"type": Int(XCUIElement.ElementType.link.rawValue),
|
|
291
|
+
"label": "Forgot password?",
|
|
292
|
+
"frame": ["x": 128, "y": 568, "width": 134, "height": 32],
|
|
293
|
+
"children": [],
|
|
294
|
+
],
|
|
295
|
+
[
|
|
296
|
+
"type": Int(XCUIElement.ElementType.button.rawValue),
|
|
297
|
+
"label": "Admin settings",
|
|
298
|
+
"frame": ["x": -260, "y": 184, "width": 220, "height": 44],
|
|
299
|
+
"children": [],
|
|
300
|
+
],
|
|
301
|
+
[
|
|
302
|
+
"type": Int(XCUIElement.ElementType.other.rawValue),
|
|
303
|
+
"frame": ["x": 16, "y": 184, "width": 220, "height": 44],
|
|
304
|
+
"children": [],
|
|
305
|
+
],
|
|
306
|
+
],
|
|
307
|
+
]
|
|
308
|
+
],
|
|
309
|
+
]
|
|
310
|
+
var nodes: [SnapshotNode] = []
|
|
311
|
+
appendPrivateAXNode(
|
|
312
|
+
tree,
|
|
313
|
+
to: &nodes,
|
|
314
|
+
options: SnapshotOptions(interactiveOnly: true, depth: nil, scope: nil, raw: false),
|
|
315
|
+
viewport: CGRect(x: 0, y: 0, width: 390, height: 844),
|
|
316
|
+
depth: 0,
|
|
317
|
+
parentIndex: nil,
|
|
318
|
+
insideMatchedScope: false
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
let labels = nodes.compactMap { $0.label }
|
|
322
|
+
XCTAssertEqual(
|
|
323
|
+
labels,
|
|
324
|
+
["Blue Sky", "Callstack", "Welcome back", "Email", "Password", "Sign in", "Forgot password?"]
|
|
325
|
+
)
|
|
326
|
+
XCTAssertFalse(labels.contains("Admin settings"))
|
|
327
|
+
}
|
|
328
|
+
}
|
package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift
CHANGED
|
@@ -17,7 +17,7 @@ extension RunnerTests {
|
|
|
17
17
|
min(max((durationMs / 5.0) / 1000.0, 0.016), 0.120)
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
func coordinateDragHoldDuration() -> TimeInterval {
|
|
21
21
|
0.050
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -55,27 +55,6 @@ extension RunnerTests {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
private func performDragSeries(
|
|
59
|
-
count: Int,
|
|
60
|
-
pauseMs: Double,
|
|
61
|
-
pattern: String,
|
|
62
|
-
points: DragPoints,
|
|
63
|
-
_ drag: (_ x: Double, _ y: Double, _ x2: Double, _ y2: Double) -> RunnerInteractionOutcome
|
|
64
|
-
) -> RunnerInteractionOutcome {
|
|
65
|
-
var outcome = RunnerInteractionOutcome.performed
|
|
66
|
-
runSeries(count: count, pauseMs: pauseMs) { idx in
|
|
67
|
-
guard case .performed = outcome else {
|
|
68
|
-
return
|
|
69
|
-
}
|
|
70
|
-
let reverse = pattern == "ping-pong" && (idx % 2 == 1)
|
|
71
|
-
let startX = reverse ? points.x2 : points.x
|
|
72
|
-
let startY = reverse ? points.y2 : points.y
|
|
73
|
-
let endX = reverse ? points.x : points.x2
|
|
74
|
-
let endY = reverse ? points.y : points.y2
|
|
75
|
-
outcome = drag(startX, startY, endX, endY)
|
|
76
|
-
}
|
|
77
|
-
return outcome
|
|
78
|
-
}
|
|
79
58
|
|
|
80
59
|
/// Runs a gesture action with uniform timing capture. Touch gestures pass `idleTimeout: true`
|
|
81
60
|
/// (the default) to run inside the scroll idle-timeout + quiescence-skip wrapper; synthesis
|
|
@@ -84,7 +63,7 @@ extension RunnerTests {
|
|
|
84
63
|
///
|
|
85
64
|
/// NOTE: a new SYNTHESIS gesture must pass `idleTimeout: false` — the default `true` would wrap
|
|
86
65
|
/// it in the scroll idle-timeout/quiescence-skip path and change its runtime behavior.
|
|
87
|
-
|
|
66
|
+
func performGesture(
|
|
88
67
|
_ app: XCUIApplication,
|
|
89
68
|
idleTimeout: Bool = true,
|
|
90
69
|
_ action: () -> RunnerInteractionOutcome
|
|
@@ -244,6 +223,8 @@ extension RunnerTests {
|
|
|
244
223
|
}
|
|
245
224
|
|
|
246
225
|
func executeUptime() -> Response {
|
|
226
|
+
// Placeholder value: the transport layer (jsonResponse) overwrites currentUptimeMs with a
|
|
227
|
+
// fresher send-time stamp on every ok response; kept so direct callers still get a value.
|
|
247
228
|
Response(
|
|
248
229
|
ok: true,
|
|
249
230
|
data: DataPayload(currentUptimeMs: currentUptimeMs())
|
|
@@ -580,42 +561,6 @@ extension RunnerTests {
|
|
|
580
561
|
} catch {
|
|
581
562
|
return Response(ok: false, error: ErrorPayload(message: error.localizedDescription))
|
|
582
563
|
}
|
|
583
|
-
case .tapSeries:
|
|
584
|
-
guard let x = command.x, let y = command.y else {
|
|
585
|
-
return Response(ok: false, error: ErrorPayload(message: "tapSeries requires x and y"))
|
|
586
|
-
}
|
|
587
|
-
let count = max(Int(command.count ?? 1), 1)
|
|
588
|
-
let intervalMs = max(command.intervalMs ?? 0, 0)
|
|
589
|
-
let doubleTap = command.doubleTap ?? false
|
|
590
|
-
let touchFrame = resolvedTouchVisualizationFrame(app: activeApp, x: x, y: y)
|
|
591
|
-
if doubleTap {
|
|
592
|
-
let (timing, outcome) = performGesture(activeApp) {
|
|
593
|
-
var outcome = RunnerInteractionOutcome.performed
|
|
594
|
-
runSeries(count: count, pauseMs: intervalMs) { _ in
|
|
595
|
-
if case .performed = outcome {
|
|
596
|
-
outcome = doubleTapAt(app: activeApp, x: x, y: y)
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
return outcome
|
|
600
|
-
}
|
|
601
|
-
if let response = unsupportedResponse(for: outcome) {
|
|
602
|
-
return response
|
|
603
|
-
}
|
|
604
|
-
return gestureResponse(message: "tap series", timing: timing, frame: .touch(touchFrame))
|
|
605
|
-
}
|
|
606
|
-
let (timing, outcome) = performGesture(activeApp) {
|
|
607
|
-
var outcome = RunnerInteractionOutcome.performed
|
|
608
|
-
runSeries(count: count, pauseMs: intervalMs) { _ in
|
|
609
|
-
if case .performed = outcome {
|
|
610
|
-
outcome = tapAt(app: activeApp, x: x, y: y)
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
return outcome
|
|
614
|
-
}
|
|
615
|
-
if let response = unsupportedResponse(for: outcome) {
|
|
616
|
-
return response
|
|
617
|
-
}
|
|
618
|
-
return gestureResponse(message: "tap series", timing: timing, frame: .touch(touchFrame))
|
|
619
564
|
case .longPress:
|
|
620
565
|
guard let x = command.x, let y = command.y else {
|
|
621
566
|
return Response(ok: false, error: ErrorPayload(message: "longPress requires x and y"))
|
|
@@ -633,114 +578,63 @@ extension RunnerTests {
|
|
|
633
578
|
guard let x = command.x, let y = command.y, let x2 = command.x2, let y2 = command.y2 else {
|
|
634
579
|
return Response(ok: false, error: ErrorPayload(message: "drag requires x, y, x2, and y2"))
|
|
635
580
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
581
|
+
return executeDragGesture(
|
|
582
|
+
activeApp: activeApp,
|
|
583
|
+
x: x,
|
|
584
|
+
y: y,
|
|
585
|
+
x2: x2,
|
|
586
|
+
y2: y2,
|
|
587
|
+
durationMs: command.durationMs,
|
|
588
|
+
synthesized: command.synthesized == true,
|
|
589
|
+
message: "dragged"
|
|
643
590
|
)
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
591
|
+
case .scroll:
|
|
592
|
+
// Fused frame-resolve + drag scroll for non-tvOS. Resolves the interaction frame via
|
|
593
|
+
// resolvedTouchReferenceFrame, computes drag endpoints with the Swift port of
|
|
594
|
+
// buildScrollGesturePlan, then runs the same non-synthesized drag path scroll's drag used.
|
|
595
|
+
guard let direction = command.direction,
|
|
596
|
+
direction == "up" || direction == "down" || direction == "left" || direction == "right"
|
|
597
|
+
else {
|
|
598
|
+
return Response(
|
|
599
|
+
ok: false,
|
|
600
|
+
error: ErrorPayload(
|
|
601
|
+
code: "INVALID_ARGS",
|
|
602
|
+
message: "scroll requires direction up|down|left|right"
|
|
655
603
|
)
|
|
656
|
-
}
|
|
657
|
-
if case .performed = outcome {
|
|
658
|
-
return gestureResponse(message: "dragged", timing: timing, frame: .drag(dragFrame))
|
|
659
|
-
}
|
|
660
|
-
fallback = gestureFallback(strategy: "xctest-coordinate-drag", from: outcome)
|
|
661
|
-
}
|
|
662
|
-
let holdDuration = command.synthesized == true
|
|
663
|
-
? synthesizedSwipeFallbackHoldDuration(durationMs: command.durationMs ?? 250)
|
|
664
|
-
: coordinateDragHoldDuration()
|
|
665
|
-
let (timing, outcome) = performGesture(activeApp) {
|
|
666
|
-
dragAt(
|
|
667
|
-
app: activeApp,
|
|
668
|
-
x: dragPoints.x,
|
|
669
|
-
y: dragPoints.y,
|
|
670
|
-
x2: dragPoints.x2,
|
|
671
|
-
y2: dragPoints.y2,
|
|
672
|
-
holdDuration: holdDuration
|
|
673
604
|
)
|
|
674
605
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
frame: .drag(dragFrame),
|
|
682
|
-
fallback: fallback
|
|
683
|
-
)
|
|
684
|
-
case .dragSeries:
|
|
685
|
-
guard let x = command.x, let y = command.y, let x2 = command.x2, let y2 = command.y2 else {
|
|
686
|
-
return Response(ok: false, error: ErrorPayload(message: "dragSeries requires x, y, x2, and y2"))
|
|
687
|
-
}
|
|
688
|
-
let count = max(Int(command.count ?? 1), 1)
|
|
689
|
-
let pauseMs = max(command.pauseMs ?? 0, 0)
|
|
690
|
-
let pattern = command.pattern ?? "one-way"
|
|
691
|
-
if pattern != "one-way" && pattern != "ping-pong" {
|
|
692
|
-
return Response(ok: false, error: ErrorPayload(message: "dragSeries pattern must be one-way or ping-pong"))
|
|
693
|
-
}
|
|
694
|
-
let dragPoints = keyboardAvoidingDragPoints(app: activeApp, x: x, y: y, x2: x2, y2: y2)
|
|
695
|
-
var fallback: GestureFallback?
|
|
696
|
-
if command.synthesized == true {
|
|
697
|
-
let durationMs = min(max(command.durationMs ?? 250, 16), 10000)
|
|
698
|
-
let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
|
|
699
|
-
performDragSeries(
|
|
700
|
-
count: count,
|
|
701
|
-
pauseMs: pauseMs,
|
|
702
|
-
pattern: pattern,
|
|
703
|
-
points: dragPoints
|
|
704
|
-
) { startX, startY, endX, endY in
|
|
705
|
-
synthesizedDragAt(
|
|
706
|
-
app: activeApp,
|
|
707
|
-
x: startX,
|
|
708
|
-
y: startY,
|
|
709
|
-
x2: endX,
|
|
710
|
-
y2: endY,
|
|
711
|
-
durationMs: durationMs
|
|
712
|
-
)
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
if case .performed = outcome {
|
|
716
|
-
return gestureResponse(message: "drag series", timing: timing)
|
|
717
|
-
}
|
|
718
|
-
fallback = gestureFallback(strategy: "xctest-coordinate-drag-series", from: outcome)
|
|
606
|
+
let frame = resolvedTouchReferenceFrame(app: activeApp, appFrame: activeApp.frame)
|
|
607
|
+
guard frame.width > 0, frame.height > 0 else {
|
|
608
|
+
return Response(
|
|
609
|
+
ok: false,
|
|
610
|
+
error: ErrorPayload(message: "scroll could not resolve a usable interaction frame")
|
|
611
|
+
)
|
|
719
612
|
}
|
|
720
|
-
let
|
|
721
|
-
|
|
722
|
-
:
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
x: startX,
|
|
733
|
-
y: startY,
|
|
734
|
-
x2: endX,
|
|
735
|
-
y2: endY,
|
|
736
|
-
holdDuration: holdDuration
|
|
613
|
+
guard let plan = runnerScrollGesturePlan(
|
|
614
|
+
direction: direction,
|
|
615
|
+
amount: command.amount,
|
|
616
|
+
pixels: command.pixels,
|
|
617
|
+
referenceWidth: frame.width,
|
|
618
|
+
referenceHeight: frame.height
|
|
619
|
+
) else {
|
|
620
|
+
return Response(
|
|
621
|
+
ok: false,
|
|
622
|
+
error: ErrorPayload(
|
|
623
|
+
code: "INVALID_ARGS",
|
|
624
|
+
message: "scroll could not compute a gesture plan"
|
|
737
625
|
)
|
|
738
|
-
|
|
739
|
-
}
|
|
740
|
-
if let response = unsupportedResponse(for: outcome) {
|
|
741
|
-
return response
|
|
626
|
+
)
|
|
742
627
|
}
|
|
743
|
-
return
|
|
628
|
+
return executeDragGesture(
|
|
629
|
+
activeApp: activeApp,
|
|
630
|
+
x: frame.minX + plan.x1,
|
|
631
|
+
y: frame.minY + plan.y1,
|
|
632
|
+
x2: frame.minX + plan.x2,
|
|
633
|
+
y2: frame.minY + plan.y2,
|
|
634
|
+
durationMs: nil,
|
|
635
|
+
synthesized: false,
|
|
636
|
+
message: "scrolled"
|
|
637
|
+
)
|
|
744
638
|
case .remotePress:
|
|
745
639
|
guard let button = tvRemoteButton(from: command.remoteButton) else {
|
|
746
640
|
return Response(ok: false, error: ErrorPayload(message: "remotePress requires remoteButton"))
|
|
@@ -759,17 +653,6 @@ extension RunnerTests {
|
|
|
759
653
|
response = executeTypeCommand(activeApp: activeApp, command: command)
|
|
760
654
|
}
|
|
761
655
|
return response ?? Response(ok: false, error: ErrorPayload(message: "type produced no response"))
|
|
762
|
-
case .interactionFrame:
|
|
763
|
-
let frame = resolvedTouchReferenceFrame(app: activeApp, appFrame: activeApp.frame)
|
|
764
|
-
return Response(
|
|
765
|
-
ok: true,
|
|
766
|
-
data: DataPayload(
|
|
767
|
-
x: frame.minX,
|
|
768
|
-
y: frame.minY,
|
|
769
|
-
referenceWidth: frame.width,
|
|
770
|
-
referenceHeight: frame.height
|
|
771
|
-
)
|
|
772
|
-
)
|
|
773
656
|
case .swipe:
|
|
774
657
|
guard let direction = command.direction else {
|
|
775
658
|
return Response(ok: false, error: ErrorPayload(message: "swipe requires direction"))
|
|
@@ -808,7 +691,6 @@ extension RunnerTests {
|
|
|
808
691
|
case .snapshot:
|
|
809
692
|
let options = SnapshotOptions(
|
|
810
693
|
interactiveOnly: command.interactiveOnly ?? false,
|
|
811
|
-
compact: command.compact ?? false,
|
|
812
694
|
depth: command.depth,
|
|
813
695
|
scope: command.scope,
|
|
814
696
|
raw: command.raw ?? false
|
|
@@ -960,6 +842,8 @@ extension RunnerTests {
|
|
|
960
842
|
return response
|
|
961
843
|
}
|
|
962
844
|
return gestureResponse(message: "pinched", timing: timing)
|
|
845
|
+
case .sequence:
|
|
846
|
+
return executeSequence(command: command, activeApp: activeApp)
|
|
963
847
|
case .rotateGesture:
|
|
964
848
|
guard let degrees = command.degrees, degrees.isFinite else {
|
|
965
849
|
return Response(ok: false, error: ErrorPayload(message: "rotateGesture requires degrees"))
|
|
@@ -1023,6 +907,71 @@ extension RunnerTests {
|
|
|
1023
907
|
}
|
|
1024
908
|
}
|
|
1025
909
|
|
|
910
|
+
/// Shared drag execution for `.drag` and the fused `.scroll`. Mirrors the original `.drag` body
|
|
911
|
+
/// exactly: keyboardAvoidingDragPoints -> resolvedDragVisualizationFrame -> synthesized branch
|
|
912
|
+
/// (16-10000ms clamp) or non-synthesized dragAt with coordinateDragHoldDuration ->
|
|
913
|
+
/// gestureResponse(.drag). `.scroll` always passes synthesized: false, pinning the same
|
|
914
|
+
/// non-synthesized drag path scroll's drag used today.
|
|
915
|
+
private func executeDragGesture(
|
|
916
|
+
activeApp: XCUIApplication,
|
|
917
|
+
x: Double,
|
|
918
|
+
y: Double,
|
|
919
|
+
x2: Double,
|
|
920
|
+
y2: Double,
|
|
921
|
+
durationMs: Double?,
|
|
922
|
+
synthesized: Bool,
|
|
923
|
+
message: String
|
|
924
|
+
) -> Response {
|
|
925
|
+
let dragPoints = keyboardAvoidingDragPoints(app: activeApp, x: x, y: y, x2: x2, y2: y2)
|
|
926
|
+
let dragFrame = resolvedDragVisualizationFrame(
|
|
927
|
+
app: activeApp,
|
|
928
|
+
x: dragPoints.x,
|
|
929
|
+
y: dragPoints.y,
|
|
930
|
+
x2: dragPoints.x2,
|
|
931
|
+
y2: dragPoints.y2
|
|
932
|
+
)
|
|
933
|
+
var fallback: GestureFallback?
|
|
934
|
+
if synthesized {
|
|
935
|
+
let durationMs = min(max(durationMs ?? 250, 16), 10000)
|
|
936
|
+
let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
|
|
937
|
+
synthesizedDragAt(
|
|
938
|
+
app: activeApp,
|
|
939
|
+
x: dragPoints.x,
|
|
940
|
+
y: dragPoints.y,
|
|
941
|
+
x2: dragPoints.x2,
|
|
942
|
+
y2: dragPoints.y2,
|
|
943
|
+
durationMs: durationMs
|
|
944
|
+
)
|
|
945
|
+
}
|
|
946
|
+
if case .performed = outcome {
|
|
947
|
+
return gestureResponse(message: message, timing: timing, frame: .drag(dragFrame))
|
|
948
|
+
}
|
|
949
|
+
fallback = gestureFallback(strategy: "xctest-coordinate-drag", from: outcome)
|
|
950
|
+
}
|
|
951
|
+
let holdDuration = synthesized
|
|
952
|
+
? synthesizedSwipeFallbackHoldDuration(durationMs: durationMs ?? 250)
|
|
953
|
+
: coordinateDragHoldDuration()
|
|
954
|
+
let (timing, outcome) = performGesture(activeApp) {
|
|
955
|
+
dragAt(
|
|
956
|
+
app: activeApp,
|
|
957
|
+
x: dragPoints.x,
|
|
958
|
+
y: dragPoints.y,
|
|
959
|
+
x2: dragPoints.x2,
|
|
960
|
+
y2: dragPoints.y2,
|
|
961
|
+
holdDuration: holdDuration
|
|
962
|
+
)
|
|
963
|
+
}
|
|
964
|
+
if let response = unsupportedResponse(for: outcome) {
|
|
965
|
+
return response
|
|
966
|
+
}
|
|
967
|
+
return gestureResponse(
|
|
968
|
+
message: message,
|
|
969
|
+
timing: timing,
|
|
970
|
+
frame: .drag(dragFrame),
|
|
971
|
+
fallback: fallback
|
|
972
|
+
)
|
|
973
|
+
}
|
|
974
|
+
|
|
1026
975
|
private func currentXCTestFailureCount() -> Int {
|
|
1027
976
|
return testRun?.failureCount ?? 0
|
|
1028
977
|
}
|