appium-webdriveragent 16.14.0 → 16.14.2

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/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## [16.14.2](https://github.com/appium/WebDriverAgent/compare/v16.14.1...v16.14.2) (2026-10-08)
2
+
3
+ ### Bug Fixes
4
+
5
+ * align Duo W3C actions and scrolling ([#1288](https://github.com/appium/WebDriverAgent/issues/1288)) ([104a31c](https://github.com/appium/WebDriverAgent/commit/104a31ce9aca53222bb312103f485b59f52f9894)), closes [#1294](https://github.com/appium/WebDriverAgent/issues/1294) [#1292](https://github.com/appium/WebDriverAgent/issues/1292)
6
+
7
+ ## [16.14.1](https://github.com/appium/WebDriverAgent/compare/v16.14.0...v16.14.1) (2026-10-07)
8
+
9
+ ### Bug Fixes
10
+
11
+ * report geometry for the selected display ([#1291](https://github.com/appium/WebDriverAgent/issues/1291)) ([43fa6da](https://github.com/appium/WebDriverAgent/commit/43fa6da647fb8adca21fdbd106d8f73d782d6717))
12
+ * skip accessibility idle checks for WebKit services ([#1293](https://github.com/appium/WebDriverAgent/issues/1293)) ([9d06611](https://github.com/appium/WebDriverAgent/commit/9d06611d1f5d342fd16458f707dca3f3e3ec00fc))
13
+
1
14
  ## [16.14.0](https://github.com/appium/WebDriverAgent/compare/v16.13.6...v16.14.0) (2026-10-04)
2
15
 
3
16
  ### Features
package/README.md CHANGED
@@ -10,12 +10,12 @@
10
10
  WebDriverAgent is a [WebDriver server](https://w3c.github.io/webdriver/webdriver-spec.html) implementation for iOS that can be used to remote control iOS devices. It allows you to launch & kill applications, tap & scroll views or confirm view presence on a screen. This makes it a perfect tool for application end-to-end testing or general purpose device automation. It works by linking `XCTest.framework` and calling Apple's API to execute commands directly on a device. WebDriverAgent is developed for end-to-end testing and is successfully adopted by [Appium](http://appium.io) via [XCUITest driver](https://github.com/appium/appium-xcuitest-driver).
11
11
 
12
12
  ## Features
13
- * Both iOS and tvOS platforms are supported with devices & simulators
14
- * Implements most of [WebDriver Spec](https://w3c.github.io/webdriver/webdriver-spec.html)
15
- * Implements part of [Mobile JSON Wire Protocol Spec](https://github.com/SeleniumHQ/mobile-spec/blob/master/spec-draft.md)
16
- * USB support for devices is implemented via [appium-ios-device](https://github.com/appium/appium-ios-device) library and has zero dependencies on third-party tools.
17
- * Easy development cycle as it can be launched & debugged directly via Xcode
18
- * Use [Mac2Driver](https://github.com/appium/appium-mac2-driver) to automate macOS apps
13
+ * Both iOS and tvOS platforms are supported with devices & simulators
14
+ * Implements most of [WebDriver Spec](https://w3c.github.io/webdriver/webdriver-spec.html)
15
+ * Implements part of [Mobile JSON Wire Protocol Spec](https://github.com/SeleniumHQ/mobile-spec/blob/master/spec-draft.md)
16
+ * USB support for devices is implemented via [appium-ios-device](https://github.com/appium/appium-ios-device) library and has zero dependencies on third-party tools.
17
+ * Easy development cycle as it can be launched & debugged directly via Xcode
18
+ * Use [Mac2Driver](https://github.com/appium/appium-mac2-driver) to automate macOS apps
19
19
 
20
20
  ## Getting Started On This Repository
21
21
 
@@ -35,20 +35,21 @@ See [Hinge angle](docs/hinge-angle.md) for reading angles, sending simulated hin
35
35
  events, runtime requirements, and physical-device testing feedback.
36
36
 
37
37
  ## Known Issues
38
+
38
39
  If you are having some issues please checkout [wiki](https://github.com/facebook/WebDriverAgent/wiki/Common-Issues) first.
39
40
 
40
41
  ## For Contributors
42
+
41
43
  If you want to help us out, you are more than welcome to. However please make sure you have followed the guidelines in [CONTRIBUTING](CONTRIBUTING.md).
42
44
 
43
45
  ## Creating Bundles
44
46
 
45
47
  `npm run bundle`
46
48
 
47
- Then, you find `WebDriverAgentRunner-Runner-sim-<version>.zip` for iOS and `WebDriverAgentRunner-Runner-tv_sim-<version>.zip` for tvOS files in the current directory.
49
+ Then, you find `WebDriverAgentRunner-Runner-sim-<version>.zip` for iOS and `WebDriverAgentRunner-Runner-tv_sim-<version>.zip` for tvOS files in the current directory.
48
50
 
49
51
  ## License
50
52
 
51
53
  [`WebDriverAgent` is BSD-licensed](LICENSE).
52
54
 
53
-
54
55
  Have fun!
@@ -587,6 +587,7 @@
587
587
  715AFAC11FFA29180053896D /* FBScreen.h in Headers */ = {isa = PBXBuildFile; fileRef = 715AFABF1FFA29180053896D /* FBScreen.h */; };
588
588
  715AFAC21FFA29180053896D /* FBScreen.m in Sources */ = {isa = PBXBuildFile; fileRef = 715AFAC01FFA29180053896D /* FBScreen.m */; };
589
589
  715AFAC41FFA2AAF0053896D /* FBScreenTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 715AFAC31FFA2AAF0053896D /* FBScreenTests.m */; };
590
+ 8EB95DF348A9438BBEAB4501 /* FBDisplayGestureTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8EB95DF348A9438BBEAB4502 /* FBDisplayGestureTests.m */; };
590
591
  715D554B2229891B00524509 /* FBExceptionHandlerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 715D554A2229891B00524509 /* FBExceptionHandlerTests.m */; };
591
592
  715D5773224DE02E00DA2D99 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 716C9345224D540C004B8542 /* libxml2.tbd */; };
592
593
  715D5774224DE05400DA2D99 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 716C9345224D540C004B8542 /* libxml2.tbd */; };
@@ -1627,6 +1628,7 @@
1627
1628
  715AFABF1FFA29180053896D /* FBScreen.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBScreen.h; sourceTree = "<group>"; };
1628
1629
  715AFAC01FFA29180053896D /* FBScreen.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBScreen.m; sourceTree = "<group>"; };
1629
1630
  715AFAC31FFA2AAF0053896D /* FBScreenTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBScreenTests.m; sourceTree = "<group>"; };
1631
+ 8EB95DF348A9438BBEAB4502 /* FBDisplayGestureTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBDisplayGestureTests.m; sourceTree = "<group>"; };
1630
1632
  715D554A2229891B00524509 /* FBExceptionHandlerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBExceptionHandlerTests.m; sourceTree = "<group>"; };
1631
1633
  71649EC82518C19C0087F212 /* IOSTestSettings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = IOSTestSettings.xcconfig; sourceTree = "<group>"; };
1632
1634
  716C9342224D53A1004B8542 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/AppleTVOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; };
@@ -2704,6 +2706,7 @@
2704
2706
  7119E1EB1E891F8600D0B125 /* FBPickerWheelSelectTests.m */,
2705
2707
  71ACF5B7242F2FDC00F0AAD4 /* FBSafariAlertTests.m */,
2706
2708
  715AFAC31FFA2AAF0053896D /* FBScreenTests.m */,
2709
+ 8EB95DF348A9438BBEAB4502 /* FBDisplayGestureTests.m */,
2707
2710
  EE55B3261D1D54CF003AAAEC /* FBScrollingTests.m */,
2708
2711
  7152EB2F1F41F9960047EEFF /* FBSessionIntegrationTests.m */,
2709
2712
  EE26409A1D0EB5E8009BE6B0 /* FBTapTest.m */,
@@ -4861,6 +4864,7 @@
4861
4864
  719CD8FF2126C90200C7D0C2 /* FBAutoAlertsHandlerTests.m in Sources */,
4862
4865
  EE2202131ECC612200A29571 /* FBIntegrationTestCase.m in Sources */,
4863
4866
  715AFAC41FFA2AAF0053896D /* FBScreenTests.m in Sources */,
4867
+ 8EB95DF348A9438BBEAB4501 /* FBDisplayGestureTests.m in Sources */,
4864
4868
  71BB58EC2B96328700CB9BFE /* FBScreenRecordingRequest.m in Sources */,
4865
4869
  EE22021E1ECC618900A29571 /* FBTapTest.m in Sources */,
4866
4870
  71930C472066434000D3AFEC /* FBPasteboardTests.m in Sources */,
@@ -109,6 +109,14 @@ static id swizzledRequestSnapshotForElement(id self, SEL _cmd, id element, id at
109
109
  return original_requestSnapshotForElement(self, _cmd, element, attributes, parameters, error);
110
110
  }
111
111
 
112
+ // Remote WKWebView elements belong to a WebKit service, not the host app.
113
+ // Sending the idle-check AX action to WebContent can crash it (#1290).
114
+ // Read process metadata only: resolving UI attributes here can reenter
115
+ // snapshotting. Leave these services on the original snapshot path.
116
+ if ([application.bundleID hasPrefix:@"com.apple.WebKit."]) {
117
+ return original_requestSnapshotForElement(self, _cmd, element, attributes, parameters, error);
118
+ }
119
+
112
120
  NSNumber *pidKey = @(pid);
113
121
  @synchronized (unresponsiveApplicationPidsLock) {
114
122
  NSDate *markedUnresponsiveAt = unresponsiveApplicationPids[pidKey];
@@ -9,7 +9,9 @@
9
9
  #import "XCUIElement+FBScrolling.h"
10
10
 
11
11
  #import "FBErrorBuilder.h"
12
+ #import "FBExceptions.h"
12
13
  #import "FBLogger.h"
14
+ #import "FBScreen.h"
13
15
  #import "FBMacros.h"
14
16
  #import "FBMathUtils.h"
15
17
  #import "FBXCodeCompatibility.h"
@@ -20,6 +22,7 @@
20
22
  #import "XCUIElement+FBUID.h"
21
23
  #import "XCUIApplication.h"
22
24
  #import "XCUICoordinate.h"
25
+ #import "XCUIScreen.h"
23
26
  #import "XCUIElement+FBIsVisible.h"
24
27
  #import "XCUIElement+FBVisibleFrame.h"
25
28
  #import "XCUIElement.h"
@@ -35,14 +38,40 @@ const CGFloat FBScrollTouchProportion = 0.75f;
35
38
 
36
39
  #if !TARGET_OS_TV
37
40
 
41
+ static void FBValidateScreenForScrolling(void)
42
+ {
43
+ NSError *error = nil;
44
+ XCUIScreen *screen = [FBScreen currentScreenWithError:&error];
45
+ if (nil == screen) {
46
+ // Match active-app detection: never continue on a different coordinate space
47
+ // when the selected display is unavailable, including for void scroll APIs.
48
+ @throw [NSException exceptionWithName:FBInvalidArgumentException
49
+ reason:error.localizedDescription
50
+ userInfo:nil];
51
+ }
52
+ }
53
+
54
+ static CGRect FBScrollFrameInApplication(CGRect frame, XCUIScreen *screen,
55
+ XCUIApplication *application)
56
+ {
57
+ #if !TARGET_OS_WATCH
58
+ // XCTest's visibleFrame is in portrait screen coordinates on both main and
59
+ // secondary displays. The live anchor's frame uses the app's orientation.
60
+ CGSize size = CGSizeMake(CGRectGetWidth(screen.bounds) / screen.scale,
61
+ CGRectGetHeight(screen.bounds) / screen.scale);
62
+ frame = FBRectFromPortraitCoordinates(frame, size, application.interfaceOrientation);
63
+ #endif
64
+ return frame;
65
+ }
66
+
38
67
  @interface FBXCElementSnapshotWrapper (FBScrolling)
39
68
 
40
69
  - (BOOL)fb_scrollUpByNormalizedDistance:(CGFloat)distance anchorElement:(XCUIElement *)anchorElement;
41
70
  - (BOOL)fb_scrollDownByNormalizedDistance:(CGFloat)distance anchorElement:(XCUIElement *)anchorElement;
42
71
  - (BOOL)fb_scrollLeftByNormalizedDistance:(CGFloat)distance anchorElement:(XCUIElement *)anchorElement;
43
72
  - (BOOL)fb_scrollRightByNormalizedDistance:(CGFloat)distance anchorElement:(XCUIElement *)anchorElement;
44
- - (BOOL)fb_scrollByNormalizedVector:(CGVector)normalizedScrollVector anchorElement:(XCUIElement *)anchorElement;
45
- - (BOOL)fb_scrollByVector:(CGVector)vector anchorElement:(XCUIElement *)anchorElement error:(NSError **)error;
73
+ - (BOOL)fb_scrollByNormalizedVector:(CGVector)normalizedScrollVector anchorElement:(XCUIElement *)anchorElement error:(NSError **)error;
74
+ - (BOOL)fb_scrollByVector:(CGVector)vector inFrame:(CGRect)frame anchorElement:(XCUIElement *)anchorElement velocity:(CGFloat)velocity error:(NSError **)error;
46
75
 
47
76
  @end
48
77
 
@@ -126,6 +155,9 @@ static XCUIElement *FBLiveElementForSnapshot(id<FBXCElementSnapshot> snapshot, X
126
155
  scrollDirection:(FBXCUIElementScrollDirection)scrollDirection
127
156
  error:(NSError **)error
128
157
  {
158
+ if (nil == [FBScreen currentScreenWithError:error]) {
159
+ return NO;
160
+ }
129
161
  FBXCElementSnapshotWrapper *prescrollSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:[self fb_customSnapshot]];
130
162
 
131
163
  if (prescrollSnapshot.isWDVisible) {
@@ -202,7 +234,7 @@ static XCUIElement *FBLiveElementForSnapshot(id<FBXCElementSnapshot> snapshot, X
202
234
 
203
235
  // The scroll view's own identity is stable across scroll steps, so it only needs
204
236
  // to be resolved to a live element once, up front; its frame does not, since it can
205
- // change across scroll steps (rotation, keyboard, dynamic layout).
237
+ // change across scroll steps (keyboard or dynamic layout).
206
238
  XCUIElement *scrollViewElement = FBLiveElementForSnapshot(scrollView, self.application);
207
239
  if (nil == scrollViewElement) {
208
240
  return
@@ -217,34 +249,27 @@ static XCUIElement *FBLiveElementForSnapshot(id<FBXCElementSnapshot> snapshot, X
217
249
  // Scrolling till cell is visible and get current value of frames
218
250
  while (![self fb_isEquivalentElementSnapshotVisible:prescrollSnapshot] && scrollCount < maxScrollCount) {
219
251
  BOOL didScroll;
252
+ __strong NSError *scrollError = nil;
220
253
  @autoreleasepool {
221
- // Re-snapshotting the scroll view every step keeps its frame from drifting too far
222
- // out of sync with the live anchor element's frame used to resolve touch points.
223
254
  scrollViewWrapped = [FBXCElementSnapshotWrapper ensureWrapped:[scrollViewElement fb_customSnapshot]];
224
- if (targetCellIndex < visibleCellIndex) {
225
- didScroll = scrollDirection == FBXCUIElementScrollDirectionVertical ?
226
- [scrollViewWrapped fb_scrollUpByNormalizedDistance:normalizedScrollDistance
227
- anchorElement:scrollViewElement] :
228
- [scrollViewWrapped fb_scrollLeftByNormalizedDistance:normalizedScrollDistance
229
- anchorElement:scrollViewElement];
230
- }
231
- else {
232
- didScroll = scrollDirection == FBXCUIElementScrollDirectionVertical ?
233
- [scrollViewWrapped fb_scrollDownByNormalizedDistance:normalizedScrollDistance
234
- anchorElement:scrollViewElement] :
235
- [scrollViewWrapped fb_scrollRightByNormalizedDistance:normalizedScrollDistance
236
- anchorElement:scrollViewElement];
237
- }
255
+ CGFloat distance = targetCellIndex < visibleCellIndex ? normalizedScrollDistance : -normalizedScrollDistance;
256
+ CGVector vector = scrollDirection == FBXCUIElementScrollDirectionVertical
257
+ ? CGVectorMake(0, distance) : CGVectorMake(distance, 0);
258
+ NSError *stepError = nil;
259
+ didScroll = [scrollViewWrapped fb_scrollByNormalizedVector:vector
260
+ anchorElement:scrollViewElement
261
+ error:&stepError];
262
+ scrollError = stepError;
238
263
  scrollCount++;
239
264
  // Wait for scroll animation
240
265
  [self fb_waitUntilStableWithTimeout:FBConfiguration.sharedInstance.animationCoolOffTimeout];
241
266
  }
242
267
  // The `error` out-param must not be written from inside the autorelease pool above.
243
268
  if (!didScroll) {
244
- return
245
- [[[FBErrorBuilder builder]
246
- withDescriptionFormat:@"Failed to scroll '%@': its frame is empty", self.description]
247
- buildError:error];
269
+ if (error) {
270
+ *error = scrollError;
271
+ }
272
+ return NO;
248
273
  }
249
274
  }
250
275
 
@@ -260,14 +285,39 @@ static XCUIElement *FBLiveElementForSnapshot(id<FBXCElementSnapshot> snapshot, X
260
285
  // Trying fb_cachedSnapshot first
261
286
  FBXCElementSnapshotWrapper *targetCellSnapshotWrapped = [FBXCElementSnapshotWrapper ensureWrapped:[self fb_customSnapshot]];
262
287
  targetCellSnapshot = [targetCellSnapshotWrapped fb_parentCellSnapshot];
263
- CGRect visibleFrame = [FBXCElementSnapshotWrapper ensureWrapped:targetCellSnapshot].fb_visibleFrame;
264
-
265
- CGVector scrollVector = CGVectorMake(visibleFrame.size.width - targetCellSnapshot.frame.size.width,
266
- visibleFrame.size.height - targetCellSnapshot.frame.size.height
267
- );
288
+ XCUIScreen *screen = [FBScreen currentScreenWithError:error];
289
+ if (nil == screen) {
290
+ return NO;
291
+ }
292
+ CGRect visibleFrame = FBScrollFrameInApplication(
293
+ [FBXCElementSnapshotWrapper ensureWrapped:targetCellSnapshot].fb_visibleFrame, screen, self.application);
294
+
295
+ CGRect targetFrame = targetCellSnapshot.frame;
296
+ // A cell can be clipped at the leading edge after a drag. Move back toward
297
+ // that edge instead of always scrolling down/right and hiding it further.
298
+ CGVector scrollVector = CGVectorMake(
299
+ CGRectGetMinX(visibleFrame) > CGRectGetMinX(targetFrame)
300
+ ? CGRectGetMinX(visibleFrame) - CGRectGetMinX(targetFrame)
301
+ : CGRectGetMaxX(visibleFrame) - CGRectGetMaxX(targetFrame),
302
+ CGRectGetMinY(visibleFrame) > CGRectGetMinY(targetFrame)
303
+ ? CGRectGetMinY(visibleFrame) - CGRectGetMinY(targetFrame)
304
+ : CGRectGetMaxY(visibleFrame) - CGRectGetMaxY(targetFrame));
305
+ // UIKit consumes the start of a drag before recognizing a pan. Include that
306
+ // margin so a small clipped edge is actually moved into view.
307
+ if (fabs(scrollVector.dx) > 1) {
308
+ scrollVector.dx += copysign(FBFuzzyPointThreshold, scrollVector.dx);
309
+ }
310
+ if (fabs(scrollVector.dy) > 1) {
311
+ scrollVector.dy += copysign(FBFuzzyPointThreshold, scrollVector.dy);
312
+ }
313
+ // Short fast corrections can turn into a flick and overshoot the target.
314
+ CGFloat velocity = MIN(FBTouchVelocity, MAX(fabs(scrollVector.dx), fabs(scrollVector.dy)) / FBTouchEventDelay);
268
315
  scrollViewWrapped = [FBXCElementSnapshotWrapper ensureWrapped:[scrollViewElement fb_customSnapshot]];
316
+ CGRect scrollingFrame = FBScrollFrameInApplication(scrollViewWrapped.visibleFrame, screen, self.application);
269
317
  return [scrollViewWrapped fb_scrollByVector:scrollVector
318
+ inFrame:scrollingFrame
270
319
  anchorElement:scrollViewElement
320
+ velocity:velocity
271
321
  error:error];
272
322
  }
273
323
 
@@ -296,64 +346,73 @@ static XCUIElement *FBLiveElementForSnapshot(id<FBXCElementSnapshot> snapshot, X
296
346
 
297
347
  @implementation FBXCElementSnapshotWrapper (FBScrolling)
298
348
 
299
- - (CGRect)scrollingFrame
300
- {
301
- return self.visibleFrame;
302
- }
303
-
304
349
  - (BOOL)fb_scrollUpByNormalizedDistance:(CGFloat)distance
305
350
  anchorElement:(XCUIElement *)anchorElement
306
351
  {
307
- return [self fb_scrollByNormalizedVector:CGVectorMake(0.0, distance) anchorElement:anchorElement];
352
+ FBValidateScreenForScrolling();
353
+ return [self fb_scrollByNormalizedVector:CGVectorMake(0.0, distance) anchorElement:anchorElement error:nil];
308
354
  }
309
355
 
310
356
  - (BOOL)fb_scrollDownByNormalizedDistance:(CGFloat)distance
311
357
  anchorElement:(XCUIElement *)anchorElement
312
358
  {
313
- return [self fb_scrollByNormalizedVector:CGVectorMake(0.0, -distance) anchorElement:anchorElement];
359
+ FBValidateScreenForScrolling();
360
+ return [self fb_scrollByNormalizedVector:CGVectorMake(0.0, -distance) anchorElement:anchorElement error:nil];
314
361
  }
315
362
 
316
363
  - (BOOL)fb_scrollLeftByNormalizedDistance:(CGFloat)distance
317
364
  anchorElement:(XCUIElement *)anchorElement
318
365
  {
319
- return [self fb_scrollByNormalizedVector:CGVectorMake(distance, 0.0) anchorElement:anchorElement];
366
+ FBValidateScreenForScrolling();
367
+ return [self fb_scrollByNormalizedVector:CGVectorMake(distance, 0.0) anchorElement:anchorElement error:nil];
320
368
  }
321
369
 
322
370
  - (BOOL)fb_scrollRightByNormalizedDistance:(CGFloat)distance
323
371
  anchorElement:(XCUIElement *)anchorElement
324
372
  {
325
- return [self fb_scrollByNormalizedVector:CGVectorMake(-distance, 0.0) anchorElement:anchorElement];
373
+ FBValidateScreenForScrolling();
374
+ return [self fb_scrollByNormalizedVector:CGVectorMake(-distance, 0.0) anchorElement:anchorElement error:nil];
326
375
  }
327
376
 
328
377
  - (BOOL)fb_scrollByNormalizedVector:(CGVector)normalizedScrollVector
329
378
  anchorElement:(XCUIElement *)anchorElement
379
+ error:(NSError **)error
330
380
  {
331
- CGVector scrollVector = CGVectorMake(CGRectGetWidth(self.scrollingFrame) * normalizedScrollVector.dx,
332
- CGRectGetHeight(self.scrollingFrame) * normalizedScrollVector.dy
381
+ XCUIScreen *screen = [FBScreen currentScreenWithError:error];
382
+ if (nil == screen) {
383
+ return NO;
384
+ }
385
+ CGRect frame = FBScrollFrameInApplication(self.visibleFrame, screen, anchorElement.application);
386
+ CGVector scrollVector = CGVectorMake(CGRectGetWidth(frame) * normalizedScrollVector.dx,
387
+ CGRectGetHeight(frame) * normalizedScrollVector.dy
333
388
  );
334
- return [self fb_scrollByVector:scrollVector anchorElement:anchorElement error:nil];
389
+ return [self fb_scrollByVector:scrollVector inFrame:frame anchorElement:anchorElement velocity:FBTouchVelocity error:error];
335
390
  }
336
391
 
337
392
  - (BOOL)fb_scrollByVector:(CGVector)vector
393
+ inFrame:(CGRect)frame
338
394
  anchorElement:(XCUIElement *)anchorElement
395
+ velocity:(CGFloat)velocity
339
396
  error:(NSError **)error
340
397
  {
341
- CGVector scrollBoundingVector = CGVectorMake(
342
- CGRectGetWidth(self.scrollingFrame) * FBScrollTouchProportion,
343
- CGRectGetHeight(self.scrollingFrame) * FBScrollTouchProportion
344
- );
345
- scrollBoundingVector.dx = (CGFloat)floor(copysign(scrollBoundingVector.dx, vector.dx));
346
- scrollBoundingVector.dy = (CGFloat)floor(copysign(scrollBoundingVector.dy, vector.dy));
347
-
398
+ // Keep the vector and clipped frame in the same coordinate space throughout
399
+ // this multi-drag gesture. Rotation or folding during a gesture is unsupported.
348
400
  NSInteger preciseScrollAttemptsCount = 20;
349
401
  CGVector CGZeroVector = CGVectorMake(0, 0);
350
402
  BOOL shouldFinishScrolling = NO;
351
403
  while (!shouldFinishScrolling) {
404
+ CGVector scrollBoundingVector = CGVectorMake(
405
+ (CGFloat)floor(copysign(CGRectGetWidth(frame) * FBScrollTouchProportion, vector.dx)),
406
+ (CGFloat)floor(copysign(CGRectGetHeight(frame) * FBScrollTouchProportion, vector.dy)));
352
407
  CGVector scrollVector = CGVectorMake(fabs(vector.dx) > fabs(scrollBoundingVector.dx) ? scrollBoundingVector.dx : vector.dx,
353
408
  fabs(vector.dy) > fabs(scrollBoundingVector.dy) ? scrollBoundingVector.dy : vector.dy);
354
409
  vector = CGVectorMake(vector.dx - scrollVector.dx, vector.dy - scrollVector.dy);
355
410
  shouldFinishScrolling = FBVectorFuzzyEqualToVector(vector, CGZeroVector, 1) || --preciseScrollAttemptsCount <= 0;
356
- if (![self fb_scrollAncestorScrollViewByVectorWithinScrollViewFrame:scrollVector anchorElement:anchorElement error:error]){
411
+ if (![self fb_scrollAncestorScrollViewByVectorWithinScrollViewFrame:scrollVector
412
+ inFrame:frame
413
+ anchorElement:anchorElement
414
+ velocity:velocity
415
+ error:error]) {
357
416
  return NO;
358
417
  }
359
418
  }
@@ -370,10 +429,11 @@ static XCUIElement *FBLiveElementForSnapshot(id<FBXCElementSnapshot> snapshot, X
370
429
  }
371
430
 
372
431
  - (BOOL)fb_scrollAncestorScrollViewByVectorWithinScrollViewFrame:(CGVector)vector
432
+ inFrame:(CGRect)scrollingFrame
373
433
  anchorElement:(XCUIElement *)anchorElement
434
+ velocity:(CGFloat)velocity
374
435
  error:(NSError **)error
375
436
  {
376
- CGRect scrollingFrame = self.scrollingFrame;
377
437
  // wdFrame matches scrollingFrame's coordinate space; raw .frame can be pre-scaled or
378
438
  // dimension-swapped and drift out of sync with it (appium/appium#16185).
379
439
  CGRect anchorFrame = anchorElement.wdFrame;
@@ -395,7 +455,7 @@ static XCUIElement *FBLiveElementForSnapshot(id<FBXCElementSnapshot> snapshot, X
395
455
 
396
456
  [startCoordinate pressForDuration:FBTouchEventDelay
397
457
  thenDragToCoordinate:endCoordinate
398
- withVelocity:FBTouchVelocity
458
+ withVelocity:velocity
399
459
  thenHoldForDuration:FBTouchEventDelay];
400
460
  return YES;
401
461
  }
@@ -34,6 +34,7 @@
34
34
  #import "XCUIElement.h"
35
35
  #import "XCUIElement+FBIsVisible.h"
36
36
  #import "XCUIElementQuery.h"
37
+ #import "XCUIScreen.h"
37
38
  #import "FBUnattachedAppLauncher.h"
38
39
 
39
40
  @implementation FBCustomCommands
@@ -162,17 +163,48 @@
162
163
 
163
164
  + (id<FBResponsePayload>)handleGetScreen:(FBRouteRequest *)request
164
165
  {
166
+ NSError *error = nil;
167
+ XCUIScreen *screen = [FBScreen currentScreenWithError:&error];
168
+ if (nil == screen) {
169
+ return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription
170
+ traceback:nil]);
171
+ }
165
172
  XCUIApplication *app = XCUIApplication.fb_systemApplication;
166
-
167
- XCUIElement *mainStatusBar = app.statusBars.allElementsBoundByIndex.firstObject;
168
- CGSize statusBarSize = (nil == mainStatusBar) ? CGSizeZero : mainStatusBar.frame.size;
169
-
170
- #if TARGET_OS_TV || TARGET_OS_WATCH
171
- CGSize screenSize = app.frame.size;
172
- #else
173
- CGSize screenSize = FBAdjustDimensionsForApplication(app.wdFrame.size, app.interfaceOrientation);
173
+ CGSize screenSize = CGSizeMake(CGRectGetWidth(screen.bounds) / screen.scale,
174
+ CGRectGetHeight(screen.bounds) / screen.scale);
175
+ #if !TARGET_OS_TV && !TARGET_OS_WATCH
176
+ XCUIApplication *activeApp = request.session ? request.session.activeApplication : XCUIApplication.fb_activeApplication;
177
+ UIInterfaceOrientation orientation = activeApp.interfaceOrientation;
178
+ // Screen bounds describe the panel's native orientation. Some physical iPads
179
+ // expose landscape-native bounds even while the application is in portrait.
180
+ // Normalize the full screen separately from compatibility-mode app frames.
181
+ if (UIInterfaceOrientationIsPortrait(orientation) && screenSize.width > screenSize.height) {
182
+ screenSize = CGSizeMake(screenSize.height, screenSize.width);
183
+ }
184
+ screenSize = FBAdjustDimensionsForApplication(screenSize, orientation);
174
185
  #endif
175
186
 
187
+ // SpringBoard can expose hidden status bars belonging to other displays, and
188
+ // Duo also exposes full-screen containers for its side-mounted status UI.
189
+ // Only a visible horizontal bar at the top of this display can be subtracted
190
+ // from a rectangular viewport.
191
+ CGSize statusBarSize = CGSizeZero;
192
+ for (XCUIElement *statusBar in app.statusBars.allElementsBoundByIndex) {
193
+ // Resolve the element before reading its screen: displayID can be zero on
194
+ // an unresolved query result even when the bar belongs to the main screen.
195
+ CGRect frame = statusBar.frame;
196
+ if (statusBar.screen.displayID != screen.displayID || !statusBar.fb_isVisible) {
197
+ continue;
198
+ }
199
+ if (fabs(CGRectGetMinY(frame)) <= 1 && fabs(CGRectGetMinX(frame)) <= 1
200
+ && fabs(CGRectGetWidth(frame) - screenSize.width) <= 1
201
+ && CGRectGetHeight(frame) > 0 && CGRectGetHeight(frame) < screenSize.height
202
+ && CGRectGetHeight(frame) < CGRectGetWidth(frame)) {
203
+ statusBarSize = frame.size;
204
+ break;
205
+ }
206
+ }
207
+
176
208
  return FBResponseWithObject(
177
209
  @{
178
210
  @"screenSize":@{@"width": @(screenSize.width),
@@ -181,8 +213,8 @@
181
213
  @"statusBarSize": @{@"width": @(statusBarSize.width),
182
214
  @"height": @(statusBarSize.height),
183
215
  },
184
- @"displayId": @([FBScreen displayID]),
185
- @"scale": @([FBScreen scale]),
216
+ @"displayId": @(screen.displayID),
217
+ @"scale": @(screen.scale),
186
218
  });
187
219
  }
188
220
 
@@ -9,6 +9,8 @@
9
9
  #import "FBElementCommands.h"
10
10
 
11
11
  #import "FBConfiguration.h"
12
+ #import "FBScreen.h"
13
+ #import "XCUIScreen.h"
12
14
  #import "FBKeyboard.h"
13
15
  #import "FBRoute.h"
14
16
  #import "FBRouteRequest.h"
@@ -698,7 +700,34 @@ static const NSInteger DEFAULT_MAX_PICKER_ATTEMPTS = 25;
698
700
  element:(XCUIElement *)element
699
701
  error:(NSError **)error
700
702
  {
701
- return FBCoordinateWithAnchorOffset(element, CGVectorMake(0, 0), offset, error);
703
+ if (element.elementType != XCUIElementTypeApplication) {
704
+ return FBCoordinateWithAnchorOffset(element, CGVectorMake(0, 0), offset, error);
705
+ }
706
+ XCUIScreen *screen = [FBScreen currentScreenWithError:error];
707
+ if (nil == screen) {
708
+ return nil;
709
+ }
710
+ if (screen.displayID == XCUIScreen.mainScreen.displayID) {
711
+ return FBCoordinateWithAnchorOffset(element, CGVectorMake(0, 0), offset, error);
712
+ }
713
+ // Application-root coordinates are rotated using the main display on Duo.
714
+ // A window coordinate keeps XCTest's display affinity and rotation intact.
715
+ // It is only a coordinate anchor, not a hit-test boundary. Preserve edge and
716
+ // outside offsets just as on the main display; XCTest handles their delivery.
717
+ CGPoint point = CGPointMake(offset.dx, offset.dy);
718
+ for (XCUIElement *window in [element childrenMatchingType:XCUIElementTypeWindow].allElementsBoundByIndex) {
719
+ // Resolve the window before reading its display affinity: an unresolved
720
+ // element may report displayID == 0.
721
+ (void)window.frame;
722
+ CGRect frame = window.wdFrame;
723
+ if (window.screen.displayID == screen.displayID && !CGRectIsEmpty(frame)) {
724
+ return FBCoordinateWithAnchorOffset(window, CGVectorMake(0, 0),
725
+ CGVectorMake(point.x - frame.origin.x, point.y - frame.origin.y), error);
726
+ }
727
+ }
728
+ [[[FBErrorBuilder builder] withDescriptionFormat:@"No non-empty application window on display %lld is available as a coordinate anchor", screen.displayID]
729
+ buildError:error];
730
+ return nil;
702
731
  }
703
732
 
704
733
  /**
@@ -15,11 +15,11 @@
15
15
  <key>CFBundlePackageType</key>
16
16
  <string>FMWK</string>
17
17
  <key>CFBundleShortVersionString</key>
18
- <string>16.14.0</string>
18
+ <string>16.14.2</string>
19
19
  <key>CFBundleSignature</key>
20
20
  <string>????</string>
21
21
  <key>CFBundleVersion</key>
22
- <string>16.14.0</string>
22
+ <string>16.14.2</string>
23
23
  <key>NSPrincipalClass</key>
24
24
  <string/>
25
25
  </dict>
@@ -35,8 +35,19 @@ BOOL FBSizeFuzzyEqualToSize(CGSize size1, CGSize size2, CGFloat threshold);
35
35
  BOOL FBRectFuzzyEqualToRect(CGRect rect1, CGRect rect2, CGFloat threshold);
36
36
 
37
37
  #if !TARGET_OS_TV && !TARGET_OS_WATCH
38
+ /*! Converts a portrait-screen rect into the application's orientation, preserving its clipping. */
39
+ CGRect FBRectFromPortraitCoordinates(CGRect frame, CGSize screenSize, UIInterfaceOrientation orientation);
40
+
38
41
  /*! Inverts size if necessary to match current screen orientation */
39
42
  CGSize FBAdjustDimensionsForApplication(CGSize actualSize, UIInterfaceOrientation orientation);
43
+
44
+ /*!
45
+ Offsets XCTest screenPoint coordinates for an explicitly selected display.
46
+ XCTest rotates these coordinates using the main screen's dimensions even when
47
+ the synthesized event record targets another display. Sizes are in points.
48
+ */
49
+ CGVector FBDisplayCoordinateOffset(CGSize mainSize, CGSize displaySize,
50
+ UIInterfaceOrientation orientation);
40
51
  #endif
41
52
 
42
53
  #if !TARGET_OS_TV
@@ -50,6 +50,27 @@ BOOL FBRectFuzzyEqualToRect(CGRect rect1, CGRect rect2, CGFloat threshold)
50
50
 
51
51
  #if !TARGET_OS_TV && !TARGET_OS_WATCH
52
52
 
53
+ CGRect FBRectFromPortraitCoordinates(CGRect frame, CGSize screenSize, UIInterfaceOrientation orientation)
54
+ {
55
+ if (CGRectIsEmpty(frame)) {
56
+ return frame;
57
+ }
58
+ CGFloat width = MIN(screenSize.width, screenSize.height);
59
+ CGFloat height = MAX(screenSize.width, screenSize.height);
60
+ switch (orientation) {
61
+ case UIInterfaceOrientationLandscapeLeft:
62
+ return CGRectMake(height - CGRectGetMaxY(frame), CGRectGetMinX(frame), frame.size.height, frame.size.width);
63
+ case UIInterfaceOrientationLandscapeRight:
64
+ return CGRectMake(CGRectGetMinY(frame), width - CGRectGetMaxX(frame), frame.size.height, frame.size.width);
65
+ case UIInterfaceOrientationPortraitUpsideDown:
66
+ return CGRectMake(width - CGRectGetMaxX(frame), height - CGRectGetMaxY(frame), frame.size.width, frame.size.height);
67
+ case UIInterfaceOrientationUnknown:
68
+ case UIInterfaceOrientationPortrait:
69
+ default:
70
+ return frame;
71
+ }
72
+ }
73
+
53
74
  CGSize FBAdjustDimensionsForApplication(CGSize actualSize, UIInterfaceOrientation orientation)
54
75
  {
55
76
  if (orientation == UIInterfaceOrientationLandscapeLeft || orientation == UIInterfaceOrientationLandscapeRight) {
@@ -107,3 +128,20 @@ BOOL FBScrollGestureOffsets(CGRect scrollingFrame,
107
128
  (endPoint.y - anchorFrame.origin.y) / anchorFrame.size.height);
108
129
  return YES;
109
130
  }
131
+
132
+ #if !TARGET_OS_TV && !TARGET_OS_WATCH
133
+ CGVector FBDisplayCoordinateOffset(CGSize mainSize, CGSize displaySize,
134
+ UIInterfaceOrientation orientation)
135
+ {
136
+ CGVector offset = CGVectorMake(0, 0);
137
+ if (orientation == UIInterfaceOrientationLandscapeRight
138
+ || orientation == UIInterfaceOrientationPortraitUpsideDown) {
139
+ offset.dx = displaySize.width - mainSize.width;
140
+ }
141
+ if (orientation == UIInterfaceOrientationLandscapeLeft
142
+ || orientation == UIInterfaceOrientationPortraitUpsideDown) {
143
+ offset.dy = displaySize.height - mainSize.height;
144
+ }
145
+ return offset;
146
+ }
147
+ #endif