agent-device 0.16.14 → 0.17.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.16.14.apk → agent-device-android-multitouch-helper-0.17.0.apk} +0 -0
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.17.0.apk.sha256 +1 -0
- package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.16.14.manifest.json → agent-device-android-multitouch-helper-0.17.0.manifest.json} +4 -4
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.16.14.apk → agent-device-android-snapshot-helper-0.17.0.apk} +0 -0
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.17.0.apk.sha256 +1 -0
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.16.14.manifest.json → agent-device-android-snapshot-helper-0.17.0.manifest.json} +6 -6
- package/dist/src/1352.js +1 -1
- package/dist/src/221.js +4 -4
- package/dist/src/2415.js +29 -29
- package/dist/src/2805.js +1 -1
- package/dist/src/6232.js +1 -1
- package/dist/src/7599.js +4 -3
- package/dist/src/8020.js +1 -0
- package/dist/src/8699.js +1 -1
- package/dist/src/940.js +1 -1
- package/dist/src/9533.js +1 -1
- package/dist/src/android-snapshot-helper.d.ts +1 -0
- package/dist/src/apple.js +1 -1
- package/dist/src/args.js +14 -9
- package/dist/src/cli.js +9 -9
- package/dist/src/command-metadata.js +1 -1
- package/dist/src/contracts.d.ts +1 -0
- package/dist/src/find.js +1 -1
- package/dist/src/finders.d.ts +1 -0
- package/dist/src/generic.js +9 -9
- package/dist/src/index.d.ts +19 -1
- package/dist/src/selectors.d.ts +1 -0
- package/dist/src/session.js +11 -11
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +4 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +71 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Alert.swift +41 -7
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +154 -11
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +11 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Exceptions.swift +12 -4
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +26 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +8 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +7 -1
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +571 -56
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +21 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TvRemote.swift +11 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +13 -2
- package/ios-runner/README.md +13 -0
- package/package.json +1 -1
- package/server.json +2 -2
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.16.14.apk.sha256 +0 -1
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.16.14.apk.sha256 +0 -1
|
@@ -53,6 +53,10 @@ static id RunnerSwipePointerPath(
|
|
|
53
53
|
CGPoint end,
|
|
54
54
|
double durationMs
|
|
55
55
|
);
|
|
56
|
+
static id RunnerTapPointerPath(
|
|
57
|
+
const RunnerXCTestEventBridge *bridge,
|
|
58
|
+
CGPoint point
|
|
59
|
+
);
|
|
56
60
|
static CGPoint RunnerPointerPointAt(
|
|
57
61
|
double x,
|
|
58
62
|
double y,
|
|
@@ -115,6 +119,18 @@ static double RunnerSmoothStep(double t);
|
|
|
115
119
|
}
|
|
116
120
|
}
|
|
117
121
|
|
|
122
|
+
+ (NSString * _Nullable)synthesizeTapWithApplication:(id)application
|
|
123
|
+
x:(double)x
|
|
124
|
+
y:(double)y {
|
|
125
|
+
@try {
|
|
126
|
+
return [self trySynthesizeTapWithApplication:application x:x y:y];
|
|
127
|
+
} @catch (NSException *exception) {
|
|
128
|
+
NSString *name = exception.name ?: @"NSException";
|
|
129
|
+
NSString *reason = exception.reason ?: @"private XCTest event synthesis failed";
|
|
130
|
+
return [NSString stringWithFormat:@"%@: %@", name, reason];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
118
134
|
+ (NSString * _Nullable)trySynthesizeTransformWithApplication:(id)application
|
|
119
135
|
x:(double)x
|
|
120
136
|
y:(double)y
|
|
@@ -224,6 +240,48 @@ static double RunnerSmoothStep(double t);
|
|
|
224
240
|
return nil;
|
|
225
241
|
}
|
|
226
242
|
|
|
243
|
+
+ (NSString * _Nullable)trySynthesizeTapWithApplication:(id)application
|
|
244
|
+
x:(double)x
|
|
245
|
+
y:(double)y {
|
|
246
|
+
RunnerXCTestEventBridge bridge;
|
|
247
|
+
NSString *missing = RunnerResolveXCTestEventBridge(application, &bridge);
|
|
248
|
+
if (missing != nil) {
|
|
249
|
+
return missing;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
NSInteger interfaceOrientation =
|
|
253
|
+
((RunnerMsgSendInteger)objc_msgSend)(application, bridge.interfaceOrientationSelector);
|
|
254
|
+
NSInteger targetProcessID = ((RunnerMsgSendInteger)objc_msgSend)(application, bridge.processIDSelector);
|
|
255
|
+
if (targetProcessID <= 0) {
|
|
256
|
+
return @"private XCTest event synthesis unavailable: could not resolve target process ID";
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
id record = ((RunnerMsgSendInitRecord)objc_msgSend)(
|
|
260
|
+
[bridge.recordClass alloc],
|
|
261
|
+
bridge.initRecordSelector,
|
|
262
|
+
@"agent-device-tap",
|
|
263
|
+
interfaceOrientation
|
|
264
|
+
);
|
|
265
|
+
if (record == nil) {
|
|
266
|
+
return @"private XCTest event synthesis failed: could not create event record";
|
|
267
|
+
}
|
|
268
|
+
((RunnerMsgSendSetInteger)objc_msgSend)(record, bridge.setTargetProcessIDSelector, targetProcessID);
|
|
269
|
+
|
|
270
|
+
id path = RunnerTapPointerPath(&bridge, CGPointMake(x, y));
|
|
271
|
+
if (path == nil) {
|
|
272
|
+
return @"private XCTest event synthesis failed: could not create pointer path";
|
|
273
|
+
}
|
|
274
|
+
((RunnerMsgSendAddPath)objc_msgSend)(record, bridge.addPathSelector, path);
|
|
275
|
+
|
|
276
|
+
NSError *error = nil;
|
|
277
|
+
BOOL ok = ((RunnerMsgSendSynthesize)objc_msgSend)(record, bridge.synthesizeSelector, &error);
|
|
278
|
+
if (!ok) {
|
|
279
|
+
NSString *detail = error.localizedDescription ?: @"synthesizeWithError returned false";
|
|
280
|
+
return [NSString stringWithFormat:@"private XCTest event synthesis failed: %@", detail];
|
|
281
|
+
}
|
|
282
|
+
return nil;
|
|
283
|
+
}
|
|
284
|
+
|
|
227
285
|
static NSString * _Nullable RunnerResolveXCTestEventBridge(
|
|
228
286
|
id application,
|
|
229
287
|
RunnerXCTestEventBridge *bridge
|
|
@@ -368,6 +426,19 @@ static id RunnerSwipePointerPath(
|
|
|
368
426
|
return path;
|
|
369
427
|
}
|
|
370
428
|
|
|
429
|
+
static id RunnerTapPointerPath(
|
|
430
|
+
const RunnerXCTestEventBridge *bridge,
|
|
431
|
+
CGPoint point
|
|
432
|
+
) {
|
|
433
|
+
id path =
|
|
434
|
+
((RunnerMsgSendInitPath)objc_msgSend)([bridge->pathClass alloc], bridge->initPathSelector, point, 0.0);
|
|
435
|
+
if (path == nil) {
|
|
436
|
+
return nil;
|
|
437
|
+
}
|
|
438
|
+
((RunnerMsgSendPathOffset)objc_msgSend)(path, bridge->liftSelector, 0.05);
|
|
439
|
+
return path;
|
|
440
|
+
}
|
|
441
|
+
|
|
371
442
|
static CGPoint RunnerPointerPointAt(
|
|
372
443
|
double x,
|
|
373
444
|
double y,
|
|
@@ -8,20 +8,18 @@ extension RunnerTests {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
func resolveAlert(app activeApp: XCUIApplication) -> RunnerAlert? {
|
|
11
|
+
#if !os(macOS)
|
|
12
|
+
if let systemModal = firstBlockingSystemModal(in: springboard) {
|
|
13
|
+
return runnerAlert(root: systemModal, ownerApp: springboard)
|
|
14
|
+
}
|
|
15
|
+
#endif
|
|
11
16
|
if let alert = firstExistingElement(in: activeApp.alerts.allElementsBoundByIndex) {
|
|
12
17
|
return runnerAlert(root: alert, ownerApp: activeApp)
|
|
13
18
|
}
|
|
14
19
|
if let popup = firstDismissPopupWindow(in: activeApp) {
|
|
15
20
|
return runnerAlert(root: popup, ownerApp: activeApp)
|
|
16
21
|
}
|
|
17
|
-
#if os(macOS)
|
|
18
|
-
return nil
|
|
19
|
-
#else
|
|
20
|
-
if let systemModal = firstBlockingSystemModal(in: springboard) {
|
|
21
|
-
return runnerAlert(root: systemModal, ownerApp: springboard)
|
|
22
|
-
}
|
|
23
22
|
return nil
|
|
24
|
-
#endif
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
func handleAlert(_ alert: RunnerAlert, action: String) -> Response {
|
|
@@ -33,6 +31,27 @@ extension RunnerTests {
|
|
|
33
31
|
if let response = unsupportedResponse(for: outcome) {
|
|
34
32
|
return response
|
|
35
33
|
}
|
|
34
|
+
sleepFor(0.2)
|
|
35
|
+
if alertStillVisible(alert, actionButtonLabel: button.label) {
|
|
36
|
+
let frame = button.frame
|
|
37
|
+
if !frame.isNull && !frame.isEmpty {
|
|
38
|
+
let coordinateOutcome = tapAt(app: alert.ownerApp, x: frame.midX, y: frame.midY)
|
|
39
|
+
if let response = unsupportedResponse(for: coordinateOutcome) {
|
|
40
|
+
return response
|
|
41
|
+
}
|
|
42
|
+
sleepFor(0.2)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if alertStillVisible(alert, actionButtonLabel: button.label) {
|
|
46
|
+
return Response(
|
|
47
|
+
ok: false,
|
|
48
|
+
error: ErrorPayload(
|
|
49
|
+
code: "INTERACTION_FAILED",
|
|
50
|
+
message: "alert \(action) did not dismiss the visible alert",
|
|
51
|
+
hint: "The alert button was found but the system still reports the alert after tapping it."
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
}
|
|
36
55
|
return Response(ok: true, data: DataPayload(message: action == "accept" ? "accepted" : "dismissed"))
|
|
37
56
|
}
|
|
38
57
|
|
|
@@ -53,6 +72,21 @@ extension RunnerTests {
|
|
|
53
72
|
return RunnerAlert(root: root, ownerApp: ownerApp, buttons: buttons)
|
|
54
73
|
}
|
|
55
74
|
|
|
75
|
+
private func alertStillVisible(_ alert: RunnerAlert, actionButtonLabel: String) -> Bool {
|
|
76
|
+
guard let current = resolveAlert(app: alert.ownerApp) else {
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
let previousTitle = preferredAlertTitle(alert.root, buttons: alert.buttons)
|
|
80
|
+
let currentTitle = preferredAlertTitle(current.root, buttons: current.buttons)
|
|
81
|
+
if previousTitle == currentTitle {
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
84
|
+
let normalizedActionLabel = actionButtonLabel.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
85
|
+
return current.buttons.contains { button in
|
|
86
|
+
button.label.trimmingCharacters(in: .whitespacesAndNewlines) == normalizedActionLabel
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
56
90
|
private func firstExistingElement(in elements: [XCUIElement]) -> XCUIElement? {
|
|
57
91
|
elements.first { isVisibleElement($0) }
|
|
58
92
|
}
|
package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift
CHANGED
|
@@ -147,10 +147,64 @@ extension RunnerTests {
|
|
|
147
147
|
return Response(ok: true, data: data)
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
func testGestureResponseIncludesSynthesizedTapFallbackDiagnostics() {
|
|
151
|
+
let response = gestureResponse(
|
|
152
|
+
message: "tapped",
|
|
153
|
+
timing: (gestureStartUptimeMs: 1, gestureEndUptimeMs: 2),
|
|
154
|
+
fallback: GestureFallback(
|
|
155
|
+
strategy: "xctest-coordinate-tap",
|
|
156
|
+
message: "Runner synthesized coordinate tap is unavailable",
|
|
157
|
+
hint: "Using XCTest coordinate tap fallback."
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
XCTAssertEqual(response.ok, true)
|
|
162
|
+
XCTAssertEqual(response.data?.gestureFallback, "xctest-coordinate-tap")
|
|
163
|
+
XCTAssertEqual(
|
|
164
|
+
response.data?.gestureFallbackMessage,
|
|
165
|
+
"Runner synthesized coordinate tap is unavailable"
|
|
166
|
+
)
|
|
167
|
+
XCTAssertEqual(response.data?.gestureFallbackHint, "Using XCTest coordinate tap fallback.")
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
func testXCTestRecordedFailureResponseFailsMutatingSuccesses() throws {
|
|
171
|
+
let command = try runnerCommandFixture(#"{"command":"tap","commandId":"tap-1"}"#)
|
|
172
|
+
let response = Response(ok: true, data: DataPayload(message: "tapped"))
|
|
173
|
+
|
|
174
|
+
let failureResponse = xctestRecordedFailureResponse(command: command, response: response)
|
|
175
|
+
|
|
176
|
+
XCTAssertEqual(failureResponse?.ok, false)
|
|
177
|
+
XCTAssertEqual(failureResponse?.error?.code, "XCTEST_RECORDED_FAILURE")
|
|
178
|
+
XCTAssertEqual(
|
|
179
|
+
failureResponse?.error?.message,
|
|
180
|
+
"XCTest recorded a failure while executing tap; the action may not have been performed."
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
func testXCTestRecordedFailureResponseDoesNotWrapReadOnlyOrRunnerFatalResponses() throws {
|
|
185
|
+
let snapshotCommand = try runnerCommandFixture(#"{"command":"snapshot","commandId":"snapshot-1"}"#)
|
|
186
|
+
let tapCommand = try runnerCommandFixture(#"{"command":"tap","commandId":"tap-1"}"#)
|
|
187
|
+
let runnerFatalResponse = Response(
|
|
188
|
+
ok: true,
|
|
189
|
+
data: DataPayload(runnerFatal: true, runnerFatalReason: "ax_snapshot_unavailable")
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
XCTAssertNil(
|
|
193
|
+
xctestRecordedFailureResponse(
|
|
194
|
+
command: snapshotCommand,
|
|
195
|
+
response: Response(ok: true, data: DataPayload(nodes: [], truncated: false))
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
XCTAssertNil(xctestRecordedFailureResponse(command: tapCommand, response: runnerFatalResponse))
|
|
199
|
+
}
|
|
200
|
+
|
|
150
201
|
func execute(command: Command) throws -> Response {
|
|
151
202
|
if command.command == .status {
|
|
152
203
|
return executeStatus(command: command)
|
|
153
204
|
}
|
|
205
|
+
if command.command == .uptime {
|
|
206
|
+
return executeUptime()
|
|
207
|
+
}
|
|
154
208
|
commandJournal.accept(command: command)
|
|
155
209
|
return try executeAccepted(command: command)
|
|
156
210
|
}
|
|
@@ -185,6 +239,13 @@ extension RunnerTests {
|
|
|
185
239
|
return Response(ok: true, data: commandJournal.status(commandId: statusCommandId))
|
|
186
240
|
}
|
|
187
241
|
|
|
242
|
+
func executeUptime() -> Response {
|
|
243
|
+
Response(
|
|
244
|
+
ok: true,
|
|
245
|
+
data: DataPayload(currentUptimeMs: currentUptimeMs())
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
188
249
|
private func executeDispatched(command: Command) throws -> Response {
|
|
189
250
|
if Thread.isMainThread {
|
|
190
251
|
return try executeOnMainSafely(command: command)
|
|
@@ -229,6 +290,7 @@ extension RunnerTests {
|
|
|
229
290
|
while true {
|
|
230
291
|
var response: Response?
|
|
231
292
|
var swiftError: Error?
|
|
293
|
+
let failureCountBefore = currentXCTestFailureCount()
|
|
232
294
|
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
|
|
233
295
|
do {
|
|
234
296
|
response = try self.executeOnMain(command: command)
|
|
@@ -238,8 +300,7 @@ extension RunnerTests {
|
|
|
238
300
|
})
|
|
239
301
|
|
|
240
302
|
if let exceptionMessage {
|
|
241
|
-
|
|
242
|
-
currentBundleId = nil
|
|
303
|
+
invalidateCachedTarget(reason: "objc_exception")
|
|
243
304
|
if !hasRetried, shouldRetryException(command, message: exceptionMessage) {
|
|
244
305
|
NSLog(
|
|
245
306
|
"AGENT_DEVICE_RUNNER_RETRY command=%@ reason=objc_exception",
|
|
@@ -265,14 +326,19 @@ extension RunnerTests {
|
|
|
265
326
|
userInfo: [NSLocalizedDescriptionKey: "command returned no response"]
|
|
266
327
|
)
|
|
267
328
|
}
|
|
329
|
+
if didRecordXCTestFailure(since: failureCountBefore),
|
|
330
|
+
let failureResponse = xctestRecordedFailureResponse(command: command, response: response)
|
|
331
|
+
{
|
|
332
|
+
invalidateCachedTarget(reason: "xctest_recorded_failure")
|
|
333
|
+
return failureResponse
|
|
334
|
+
}
|
|
268
335
|
if !hasRetried, shouldRetryCommand(command), shouldRetryResponse(response) {
|
|
269
336
|
NSLog(
|
|
270
337
|
"AGENT_DEVICE_RUNNER_RETRY command=%@ reason=response_unavailable",
|
|
271
338
|
command.command.rawValue
|
|
272
339
|
)
|
|
273
340
|
hasRetried = true
|
|
274
|
-
|
|
275
|
-
currentBundleId = nil
|
|
341
|
+
invalidateCachedTarget(reason: "response_unavailable")
|
|
276
342
|
sleepFor(retryCooldown)
|
|
277
343
|
continue
|
|
278
344
|
}
|
|
@@ -282,7 +348,9 @@ extension RunnerTests {
|
|
|
282
348
|
|
|
283
349
|
private func executeOnMain(command: Command) throws -> Response {
|
|
284
350
|
var activeApp = currentApp ?? app
|
|
285
|
-
if
|
|
351
|
+
if shouldSkipAppActivationPreflight(command) {
|
|
352
|
+
activeApp = resolveAppWithoutActivation(command: command)
|
|
353
|
+
} else if !isRunnerLifecycleCommand(command.command) {
|
|
286
354
|
let normalizedBundleId = command.appBundleId?
|
|
287
355
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
288
356
|
let requestedBundleId = (normalizedBundleId?.isEmpty == true) ? nil : normalizedBundleId
|
|
@@ -408,10 +476,7 @@ extension RunnerTests {
|
|
|
408
476
|
return Response(ok: false, error: ErrorPayload(message: "failed to stop recording: \(error.localizedDescription)"))
|
|
409
477
|
}
|
|
410
478
|
case .uptime:
|
|
411
|
-
return
|
|
412
|
-
ok: true,
|
|
413
|
-
data: DataPayload(currentUptimeMs: currentUptimeMs())
|
|
414
|
-
)
|
|
479
|
+
return executeUptime()
|
|
415
480
|
case .tap:
|
|
416
481
|
if let selectorKey = command.selectorKey, let selectorValue = command.selectorValue {
|
|
417
482
|
let match = findElement(
|
|
@@ -425,6 +490,7 @@ extension RunnerTests {
|
|
|
425
490
|
}
|
|
426
491
|
if let element = match.element {
|
|
427
492
|
let frame = element.frame
|
|
493
|
+
let isTextEntry = isTextEntryElement(element)
|
|
428
494
|
let touchFrame = frame.isEmpty
|
|
429
495
|
? nil
|
|
430
496
|
: resolvedTouchVisualizationFrame(app: activeApp, x: frame.midX, y: frame.midY)
|
|
@@ -440,7 +506,9 @@ extension RunnerTests {
|
|
|
440
506
|
if let response = unsupportedResponse(for: outcome) {
|
|
441
507
|
return response
|
|
442
508
|
}
|
|
443
|
-
|
|
509
|
+
if isTextEntry {
|
|
510
|
+
waitForTextEntryReadinessAfterTap(app: activeApp, element: element)
|
|
511
|
+
}
|
|
444
512
|
return gestureResponse(
|
|
445
513
|
message: match.usedNonHittableFallback ? "tapped via non-hittable coordinate fallback" : "tapped",
|
|
446
514
|
timing: timing,
|
|
@@ -462,12 +530,27 @@ extension RunnerTests {
|
|
|
462
530
|
return Response(ok: false, error: ErrorPayload(message: "element not found"))
|
|
463
531
|
}
|
|
464
532
|
if let x = command.x, let y = command.y {
|
|
533
|
+
var fallback: GestureFallback?
|
|
534
|
+
if command.synthesized == true {
|
|
535
|
+
let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
|
|
536
|
+
synthesizedTapAt(app: activeApp, x: x, y: y)
|
|
537
|
+
}
|
|
538
|
+
if case .performed = outcome {
|
|
539
|
+
return gestureResponse(message: "tapped", timing: timing)
|
|
540
|
+
}
|
|
541
|
+
fallback = gestureFallback(strategy: "xctest-coordinate-tap", from: outcome)
|
|
542
|
+
}
|
|
465
543
|
let touchFrame = resolvedTouchVisualizationFrame(app: activeApp, x: x, y: y)
|
|
466
544
|
let (timing, outcome) = performGesture(activeApp) { tapAt(app: activeApp, x: x, y: y) }
|
|
467
545
|
if let response = unsupportedResponse(for: outcome) {
|
|
468
546
|
return response
|
|
469
547
|
}
|
|
470
|
-
return gestureResponse(
|
|
548
|
+
return gestureResponse(
|
|
549
|
+
message: "tapped",
|
|
550
|
+
timing: timing,
|
|
551
|
+
frame: .touch(touchFrame),
|
|
552
|
+
fallback: fallback
|
|
553
|
+
)
|
|
471
554
|
}
|
|
472
555
|
return Response(ok: false, error: ErrorPayload(message: "tap requires text or x/y"))
|
|
473
556
|
case .mouseClick:
|
|
@@ -736,6 +819,7 @@ extension RunnerTests {
|
|
|
736
819
|
needsPostSnapshotInteractionDelay = true
|
|
737
820
|
return Response(ok: true, data: payload)
|
|
738
821
|
} catch let failure as SnapshotCaptureFailure {
|
|
822
|
+
invalidateCachedTarget(reason: "ax_snapshot_failure")
|
|
739
823
|
// Other thrown errors fall through to executeOnMainSafely's generic error response.
|
|
740
824
|
return Response(
|
|
741
825
|
ok: false,
|
|
@@ -935,6 +1019,65 @@ extension RunnerTests {
|
|
|
935
1019
|
}
|
|
936
1020
|
}
|
|
937
1021
|
|
|
1022
|
+
private func currentXCTestFailureCount() -> Int {
|
|
1023
|
+
return testRun?.failureCount ?? 0
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
private func didRecordXCTestFailure(since failureCountBefore: Int) -> Bool {
|
|
1027
|
+
return currentXCTestFailureCount() > failureCountBefore
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
private func xctestRecordedFailureResponse(command: Command, response: Response) -> Response? {
|
|
1031
|
+
guard response.ok else { return nil }
|
|
1032
|
+
if response.data?.runnerFatal == true {
|
|
1033
|
+
return nil
|
|
1034
|
+
}
|
|
1035
|
+
guard !isReadOnlyCommand(command), !isRunnerLifecycleCommand(command.command) else {
|
|
1036
|
+
return nil
|
|
1037
|
+
}
|
|
1038
|
+
return Response(
|
|
1039
|
+
ok: false,
|
|
1040
|
+
error: ErrorPayload(
|
|
1041
|
+
code: "XCTEST_RECORDED_FAILURE",
|
|
1042
|
+
message: "XCTest recorded a failure while executing \(command.command.rawValue); the action may not have been performed.",
|
|
1043
|
+
hint: "The iOS runner session will be restarted. Retry after a fresh snapshot, or use screenshot plus coordinate commands when the accessibility tree is unavailable."
|
|
1044
|
+
)
|
|
1045
|
+
)
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
private func runnerCommandFixture(_ json: String) throws -> Command {
|
|
1049
|
+
try JSONDecoder().decode(Command.self, from: Data(json.utf8))
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
private func shouldSkipAppActivationPreflight(_ command: Command) -> Bool {
|
|
1053
|
+
#if os(iOS)
|
|
1054
|
+
// Coordinate-only synthesized taps can run after an AX-fatal screen because they do not need
|
|
1055
|
+
// app activation, window lookup, keyboard lookup, or element resolution. Selector/text taps
|
|
1056
|
+
// intentionally stay on the normal AX path because they need an element query.
|
|
1057
|
+
return command.command == .tap
|
|
1058
|
+
&& command.synthesized == true
|
|
1059
|
+
&& command.x != nil
|
|
1060
|
+
&& command.y != nil
|
|
1061
|
+
&& command.text == nil
|
|
1062
|
+
&& command.selectorKey == nil
|
|
1063
|
+
#else
|
|
1064
|
+
return false
|
|
1065
|
+
#endif
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
private func resolveAppWithoutActivation(command: Command) -> XCUIApplication {
|
|
1069
|
+
guard let bundleId = command.appBundleId?
|
|
1070
|
+
.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
1071
|
+
!bundleId.isEmpty
|
|
1072
|
+
else {
|
|
1073
|
+
return currentApp ?? app
|
|
1074
|
+
}
|
|
1075
|
+
if currentBundleId == bundleId, let currentApp {
|
|
1076
|
+
return currentApp
|
|
1077
|
+
}
|
|
1078
|
+
return XCUIApplication(bundleIdentifier: bundleId)
|
|
1079
|
+
}
|
|
1080
|
+
|
|
938
1081
|
private func executeTypeCommand(activeApp: XCUIApplication, command: Command) -> Response {
|
|
939
1082
|
guard let text = command.text else {
|
|
940
1083
|
return Response(ok: false, error: ErrorPayload(message: "type requires text"))
|
package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift
CHANGED
|
@@ -154,6 +154,17 @@ final class RunnerCommandJournal {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
extension RunnerTests {
|
|
157
|
+
func testUptimeBypassesCommandJournal() throws {
|
|
158
|
+
let command = runnerJournalCommand("uptime", id: "uptime-probe")
|
|
159
|
+
|
|
160
|
+
let response = try execute(command: command)
|
|
161
|
+
let status = commandJournal.status(commandId: "uptime-probe")
|
|
162
|
+
|
|
163
|
+
XCTAssertEqual(response.ok, true)
|
|
164
|
+
XCTAssertNotNil(response.data?.currentUptimeMs)
|
|
165
|
+
XCTAssertEqual(status.lifecycleState, RunnerCommandLifecycleState.notAccepted.rawValue)
|
|
166
|
+
}
|
|
167
|
+
|
|
157
168
|
func testCommandJournalRetentionPolicy() throws {
|
|
158
169
|
let journal = RunnerCommandJournal()
|
|
159
170
|
|
|
@@ -10,10 +10,7 @@ extension RunnerTests {
|
|
|
10
10
|
/// exception telemetry later. `RunnerObjCExceptionCatcher.catchException` takes a non-escaping
|
|
11
11
|
/// block, so `block` may capture `inout` state.
|
|
12
12
|
func safely<T>(_ tag: String, _ fallback: T, _ block: () -> T) -> T {
|
|
13
|
-
|
|
14
|
-
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
|
|
15
|
-
result = block()
|
|
16
|
-
})
|
|
13
|
+
let (result, exceptionMessage) = catchingObjCException(fallback: fallback, block)
|
|
17
14
|
if let exceptionMessage {
|
|
18
15
|
NSLog("AGENT_DEVICE_RUNNER_%@_IGNORED_EXCEPTION=%@", tag, exceptionMessage)
|
|
19
16
|
return fallback
|
|
@@ -21,6 +18,17 @@ extension RunnerTests {
|
|
|
21
18
|
return result
|
|
22
19
|
}
|
|
23
20
|
|
|
21
|
+
func catchingObjCException<T>(
|
|
22
|
+
fallback: T,
|
|
23
|
+
_ block: () -> T
|
|
24
|
+
) -> (result: T, exceptionMessage: String?) {
|
|
25
|
+
var result = fallback
|
|
26
|
+
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
|
|
27
|
+
result = block()
|
|
28
|
+
})
|
|
29
|
+
return (result, exceptionMessage)
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
/// Optional-returning convenience: returns `nil` on exception (matching the common
|
|
25
33
|
/// `var x: T?` + catch-and-return-nil shape).
|
|
26
34
|
func safely<T>(_ tag: String, _ block: () -> T?) -> T? {
|
|
@@ -670,6 +670,32 @@ extension RunnerTests {
|
|
|
670
670
|
#endif
|
|
671
671
|
}
|
|
672
672
|
|
|
673
|
+
func synthesizedTapAt(app: XCUIApplication, x: Double, y: Double) -> RunnerInteractionOutcome {
|
|
674
|
+
#if os(iOS)
|
|
675
|
+
if let message = RunnerSynthesizedGesture.synthesizeTap(
|
|
676
|
+
withApplication: app,
|
|
677
|
+
x: x,
|
|
678
|
+
y: y
|
|
679
|
+
) {
|
|
680
|
+
return .unsupported(
|
|
681
|
+
message: message,
|
|
682
|
+
hint: "Falling back to XCTest coordinate tap may be slower and can still need a healthy accessibility tree."
|
|
683
|
+
)
|
|
684
|
+
}
|
|
685
|
+
return .performed
|
|
686
|
+
#elseif os(tvOS)
|
|
687
|
+
return .unsupported(
|
|
688
|
+
message: "coordinate tap is not supported on tvOS; move focus with swipe or scroll, then select the focused element",
|
|
689
|
+
hint: "tvOS has no coordinate input; move focus with swipe/scroll to the target, then select it."
|
|
690
|
+
)
|
|
691
|
+
#else
|
|
692
|
+
return .unsupported(
|
|
693
|
+
message: "synthesized coordinate tap is not supported on macOS",
|
|
694
|
+
hint: "macOS automation has no touchscreen; use mouse-driven interactions instead."
|
|
695
|
+
)
|
|
696
|
+
#endif
|
|
697
|
+
}
|
|
698
|
+
|
|
673
699
|
func keyboardAvoidingDragPoints(
|
|
674
700
|
app: XCUIApplication,
|
|
675
701
|
x: Double,
|
|
@@ -87,6 +87,14 @@ extension RunnerTests {
|
|
|
87
87
|
currentBundleId = nil
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
func invalidateCachedTarget(reason: String) {
|
|
91
|
+
if currentApp != nil || currentBundleId != nil {
|
|
92
|
+
NSLog("AGENT_DEVICE_RUNNER_TARGET_CACHE_INVALIDATE reason=%@", reason)
|
|
93
|
+
}
|
|
94
|
+
currentApp = nil
|
|
95
|
+
currentBundleId = nil
|
|
96
|
+
}
|
|
97
|
+
|
|
90
98
|
func targetNeedsActivation(_ target: XCUIApplication) -> Bool {
|
|
91
99
|
let state = target.state
|
|
92
100
|
#if os(macOS)
|
|
@@ -193,6 +193,8 @@ struct DataPayload: Codable {
|
|
|
193
193
|
let gestureFallback: String?
|
|
194
194
|
let gestureFallbackMessage: String?
|
|
195
195
|
let gestureFallbackHint: String?
|
|
196
|
+
let runnerFatal: Bool?
|
|
197
|
+
let runnerFatalReason: String?
|
|
196
198
|
|
|
197
199
|
init(
|
|
198
200
|
message: String? = nil,
|
|
@@ -224,7 +226,9 @@ struct DataPayload: Codable {
|
|
|
224
226
|
orientation: String? = nil,
|
|
225
227
|
gestureFallback: String? = nil,
|
|
226
228
|
gestureFallbackMessage: String? = nil,
|
|
227
|
-
gestureFallbackHint: String? = nil
|
|
229
|
+
gestureFallbackHint: String? = nil,
|
|
230
|
+
runnerFatal: Bool? = nil,
|
|
231
|
+
runnerFatalReason: String? = nil
|
|
228
232
|
) {
|
|
229
233
|
self.message = message
|
|
230
234
|
self.text = text
|
|
@@ -256,6 +260,8 @@ struct DataPayload: Codable {
|
|
|
256
260
|
self.gestureFallback = gestureFallback
|
|
257
261
|
self.gestureFallbackMessage = gestureFallbackMessage
|
|
258
262
|
self.gestureFallbackHint = gestureFallbackHint
|
|
263
|
+
self.runnerFatal = runnerFatal
|
|
264
|
+
self.runnerFatalReason = runnerFatalReason
|
|
259
265
|
}
|
|
260
266
|
}
|
|
261
267
|
|