appium-webdriveragent 16.12.5 → 16.12.6

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,9 @@
1
+ ## [16.12.6](https://github.com/appium/WebDriverAgent/compare/v16.12.5...v16.12.6) (2026-09-09)
2
+
3
+ ### Bug Fixes
4
+
5
+ * rescale gesture coordinates for compatibility-mode window mismatches ([#1249](https://github.com/appium/WebDriverAgent/issues/1249)) ([25fcd95](https://github.com/appium/WebDriverAgent/commit/25fcd9558314c367c755b047cd0db0949654e90b))
6
+
1
7
  ## [16.12.5](https://github.com/appium/WebDriverAgent/compare/v16.12.4...v16.12.5) (2026-09-07)
2
8
 
3
9
  ### Bug Fixes
@@ -11,6 +11,7 @@
11
11
  #if !TARGET_OS_TV
12
12
 
13
13
  #import "FBErrorBuilder.h"
14
+ #import "FBMathUtils.h"
14
15
  #import "XCUICoordinate.h"
15
16
  #import "XCUIDevice.h"
16
17
 
@@ -36,8 +37,10 @@
36
37
  } else {
37
38
  CGVector offset = CGVectorMake(relativeCoordinate.CGPointValue.x,
38
39
  relativeCoordinate.CGPointValue.y);
39
- XCUICoordinate *hitPoint = [[self coordinateWithNormalizedOffset:CGVectorMake(0, 0)]
40
- coordinateWithOffset:offset];
40
+ XCUICoordinate *hitPoint = FBCoordinateWithAnchorOffset(self, CGVectorMake(0, 0), offset, error);
41
+ if (nil == hitPoint) {
42
+ return NO;
43
+ }
41
44
  if (nil == pressure || nil == duration) {
42
45
  [hitPoint forcePress];
43
46
  } else {
@@ -25,8 +25,9 @@ static const NSTimeInterval VALUE_CHANGE_TIMEOUT = 2;
25
25
  {
26
26
  id<FBXCElementSnapshot> snapshot = [self fb_standardSnapshot];
27
27
  NSString *previousValue = snapshot.value;
28
- XCUICoordinate *startCoord = [self coordinateWithNormalizedOffset:CGVectorMake(0.5, 0.5)];
29
- XCUICoordinate *endCoord = [startCoord coordinateWithOffset:CGVectorMake(0.0, relativeHeightOffset * snapshot.frame.size.height)];
28
+ // Stay in normalized offsets end-to-end: XCTest never rescales a composed raw
29
+ // coordinateWithOffset: for compatibility-mode windows (appium/appium#16185).
30
+ XCUICoordinate *endCoord = [self coordinateWithNormalizedOffset:CGVectorMake(0.5, 0.5 + relativeHeightOffset)];
30
31
  // If picker value is reflected in its accessiblity id
31
32
  // then fetching of the next snapshot may fail with StaleElementReferenceError
32
33
  // because we bound elements by their accessbility ids by default.
@@ -16,6 +16,8 @@
16
16
  #import "FBXCElementSnapshotWrapper.h"
17
17
  #import "FBXCElementSnapshotWrapper+Helpers.h"
18
18
  #import "XCUIElement+FBCaching.h"
19
+ #import "XCUIElement+FBResolve.h"
20
+ #import "XCUIElement+FBUID.h"
19
21
  #import "XCUIApplication.h"
20
22
  #import "XCUICoordinate.h"
21
23
  #import "XCUIElement+FBIsVisible.h"
@@ -35,15 +37,37 @@ const CGFloat FBScrollTouchProportion = 0.75f;
35
37
 
36
38
  @interface FBXCElementSnapshotWrapper (FBScrolling)
37
39
 
38
- - (void)fb_scrollUpByNormalizedDistance:(CGFloat)distance inApplication:(XCUIApplication *)application;
39
- - (void)fb_scrollDownByNormalizedDistance:(CGFloat)distance inApplication:(XCUIApplication *)application;
40
- - (void)fb_scrollLeftByNormalizedDistance:(CGFloat)distance inApplication:(XCUIApplication *)application;
41
- - (void)fb_scrollRightByNormalizedDistance:(CGFloat)distance inApplication:(XCUIApplication *)application;
42
- - (BOOL)fb_scrollByNormalizedVector:(CGVector)normalizedScrollVector inApplication:(XCUIApplication *)application;
43
- - (BOOL)fb_scrollByVector:(CGVector)vector inApplication:(XCUIApplication *)application error:(NSError **)error;
40
+ - (BOOL)fb_scrollUpByNormalizedDistance:(CGFloat)distance anchorElement:(XCUIElement *)anchorElement;
41
+ - (BOOL)fb_scrollDownByNormalizedDistance:(CGFloat)distance anchorElement:(XCUIElement *)anchorElement;
42
+ - (BOOL)fb_scrollLeftByNormalizedDistance:(CGFloat)distance anchorElement:(XCUIElement *)anchorElement;
43
+ - (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;
44
46
 
45
47
  @end
46
48
 
49
+ /**
50
+ Resolves a live element for the given snapshot, so gesture coordinates can be anchored
51
+ to it (its frame gets rescaled by XCTest for compatibility-mode windows; a raw
52
+ XCUIApplication anchor never does - see appium/appium#16185). Returns nil, rather than
53
+ falling back to the application, if the snapshot can no longer be located: anchoring to
54
+ the application would silently reproduce the very bug this is fixing.
55
+ */
56
+ static XCUIElement *FBLiveElementForSnapshot(id<FBXCElementSnapshot> snapshot, XCUIApplication *application)
57
+ {
58
+ NSString *uid = [FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot];
59
+ if (nil == uid) {
60
+ return nil;
61
+ }
62
+ XCUIElement *result;
63
+ @autoreleasepool {
64
+ NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K = %@", FBStringify(FBXCElementSnapshotWrapper, fb_uid), uid];
65
+ result = [[application.fb_query descendantsMatchingType:XCUIElementTypeAny] matchingPredicate:predicate].allElementsBoundByIndex.firstObject;
66
+ }
67
+ result.fb_isResolvedNatively = @NO;
68
+ return result;
69
+ }
70
+
47
71
  @implementation XCUIElement (FBScrolling)
48
72
 
49
73
  - (BOOL)fb_nativeScrollToVisibleWithError:(NSError **)error
@@ -61,28 +85,28 @@ const CGFloat FBScrollTouchProportion = 0.75f;
61
85
  {
62
86
  id<FBXCElementSnapshot> snapshot = [self fb_customSnapshot];
63
87
  [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_scrollUpByNormalizedDistance:distance
64
- inApplication:self.application];
88
+ anchorElement:self];
65
89
  }
66
90
 
67
91
  - (void)fb_scrollDownByNormalizedDistance:(CGFloat)distance
68
92
  {
69
93
  id<FBXCElementSnapshot> snapshot = [self fb_customSnapshot];
70
94
  [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_scrollDownByNormalizedDistance:distance
71
- inApplication:self.application];
95
+ anchorElement:self];
72
96
  }
73
97
 
74
98
  - (void)fb_scrollLeftByNormalizedDistance:(CGFloat)distance
75
99
  {
76
100
  id<FBXCElementSnapshot> snapshot = [self fb_customSnapshot];
77
101
  [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_scrollLeftByNormalizedDistance:distance
78
- inApplication:self.application];
102
+ anchorElement:self];
79
103
  }
80
104
 
81
105
  - (void)fb_scrollRightByNormalizedDistance:(CGFloat)distance
82
106
  {
83
107
  id<FBXCElementSnapshot> snapshot = [self fb_customSnapshot];
84
108
  [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_scrollRightByNormalizedDistance:distance
85
- inApplication:self.application];
109
+ anchorElement:self];
86
110
  }
87
111
 
88
112
  - (BOOL)fb_scrollToVisibleWithError:(NSError **)error
@@ -176,30 +200,52 @@ const CGFloat FBScrollTouchProportion = 0.75f;
176
200
  }
177
201
  }
178
202
 
203
+ // The scroll view's own identity is stable across scroll steps, so it only needs
204
+ // 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).
206
+ XCUIElement *scrollViewElement = FBLiveElementForSnapshot(scrollView, self.application);
207
+ if (nil == scrollViewElement) {
208
+ return
209
+ [[[FBErrorBuilder builder]
210
+ withDescriptionFormat:@"Failed to resolve a live element for the scrollable parent of '%@'", self.description]
211
+ buildError:error];
212
+ }
213
+
179
214
  const NSUInteger maxScrollCount = 25;
180
215
  NSUInteger scrollCount = 0;
181
- FBXCElementSnapshotWrapper *scrollViewWrapped = [FBXCElementSnapshotWrapper ensureWrapped:scrollView];
216
+ FBXCElementSnapshotWrapper *scrollViewWrapped;
182
217
  // Scrolling till cell is visible and get current value of frames
183
218
  while (![self fb_isEquivalentElementSnapshotVisible:prescrollSnapshot] && scrollCount < maxScrollCount) {
219
+ BOOL didScroll;
184
220
  @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
+ scrollViewWrapped = [FBXCElementSnapshotWrapper ensureWrapped:[scrollViewElement fb_customSnapshot]];
185
224
  if (targetCellIndex < visibleCellIndex) {
186
- scrollDirection == FBXCUIElementScrollDirectionVertical ?
225
+ didScroll = scrollDirection == FBXCUIElementScrollDirectionVertical ?
187
226
  [scrollViewWrapped fb_scrollUpByNormalizedDistance:normalizedScrollDistance
188
- inApplication:self.application] :
227
+ anchorElement:scrollViewElement] :
189
228
  [scrollViewWrapped fb_scrollLeftByNormalizedDistance:normalizedScrollDistance
190
- inApplication:self.application];
229
+ anchorElement:scrollViewElement];
191
230
  }
192
231
  else {
193
- scrollDirection == FBXCUIElementScrollDirectionVertical ?
232
+ didScroll = scrollDirection == FBXCUIElementScrollDirectionVertical ?
194
233
  [scrollViewWrapped fb_scrollDownByNormalizedDistance:normalizedScrollDistance
195
- inApplication:self.application] :
234
+ anchorElement:scrollViewElement] :
196
235
  [scrollViewWrapped fb_scrollRightByNormalizedDistance:normalizedScrollDistance
197
- inApplication:self.application];
236
+ anchorElement:scrollViewElement];
198
237
  }
199
238
  scrollCount++;
200
239
  // Wait for scroll animation
201
240
  [self fb_waitUntilStableWithTimeout:FBConfiguration.sharedInstance.animationCoolOffTimeout];
202
241
  }
242
+ // The `error` out-param must not be written from inside the autorelease pool above.
243
+ if (!didScroll) {
244
+ return
245
+ [[[FBErrorBuilder builder]
246
+ withDescriptionFormat:@"Failed to scroll '%@': its frame is empty", self.description]
247
+ buildError:error];
248
+ }
203
249
  }
204
250
 
205
251
  if (scrollCount >= maxScrollCount) {
@@ -215,12 +261,13 @@ const CGFloat FBScrollTouchProportion = 0.75f;
215
261
  FBXCElementSnapshotWrapper *targetCellSnapshotWrapped = [FBXCElementSnapshotWrapper ensureWrapped:[self fb_customSnapshot]];
216
262
  targetCellSnapshot = [targetCellSnapshotWrapped fb_parentCellSnapshot];
217
263
  CGRect visibleFrame = [FBXCElementSnapshotWrapper ensureWrapped:targetCellSnapshot].fb_visibleFrame;
218
-
264
+
219
265
  CGVector scrollVector = CGVectorMake(visibleFrame.size.width - targetCellSnapshot.frame.size.width,
220
266
  visibleFrame.size.height - targetCellSnapshot.frame.size.height
221
267
  );
268
+ scrollViewWrapped = [FBXCElementSnapshotWrapper ensureWrapped:[scrollViewElement fb_customSnapshot]];
222
269
  return [scrollViewWrapped fb_scrollByVector:scrollVector
223
- inApplication:self.application
270
+ anchorElement:scrollViewElement
224
271
  error:error];
225
272
  }
226
273
 
@@ -254,42 +301,42 @@ const CGFloat FBScrollTouchProportion = 0.75f;
254
301
  return self.visibleFrame;
255
302
  }
256
303
 
257
- - (void)fb_scrollUpByNormalizedDistance:(CGFloat)distance
258
- inApplication:(XCUIApplication *)application
304
+ - (BOOL)fb_scrollUpByNormalizedDistance:(CGFloat)distance
305
+ anchorElement:(XCUIElement *)anchorElement
259
306
  {
260
- [self fb_scrollByNormalizedVector:CGVectorMake(0.0, distance) inApplication:application];
307
+ return [self fb_scrollByNormalizedVector:CGVectorMake(0.0, distance) anchorElement:anchorElement];
261
308
  }
262
309
 
263
- - (void)fb_scrollDownByNormalizedDistance:(CGFloat)distance
264
- inApplication:(XCUIApplication *)application
310
+ - (BOOL)fb_scrollDownByNormalizedDistance:(CGFloat)distance
311
+ anchorElement:(XCUIElement *)anchorElement
265
312
  {
266
- [self fb_scrollByNormalizedVector:CGVectorMake(0.0, -distance) inApplication:application];
313
+ return [self fb_scrollByNormalizedVector:CGVectorMake(0.0, -distance) anchorElement:anchorElement];
267
314
  }
268
315
 
269
- - (void)fb_scrollLeftByNormalizedDistance:(CGFloat)distance
270
- inApplication:(XCUIApplication *)application
316
+ - (BOOL)fb_scrollLeftByNormalizedDistance:(CGFloat)distance
317
+ anchorElement:(XCUIElement *)anchorElement
271
318
  {
272
- [self fb_scrollByNormalizedVector:CGVectorMake(distance, 0.0) inApplication:application];
319
+ return [self fb_scrollByNormalizedVector:CGVectorMake(distance, 0.0) anchorElement:anchorElement];
273
320
  }
274
321
 
275
- - (void)fb_scrollRightByNormalizedDistance:(CGFloat)distance
276
- inApplication:(XCUIApplication *)application
322
+ - (BOOL)fb_scrollRightByNormalizedDistance:(CGFloat)distance
323
+ anchorElement:(XCUIElement *)anchorElement
277
324
  {
278
- [self fb_scrollByNormalizedVector:CGVectorMake(-distance, 0.0) inApplication:application];
325
+ return [self fb_scrollByNormalizedVector:CGVectorMake(-distance, 0.0) anchorElement:anchorElement];
279
326
  }
280
327
 
281
328
  - (BOOL)fb_scrollByNormalizedVector:(CGVector)normalizedScrollVector
282
- inApplication:(XCUIApplication *)application
329
+ anchorElement:(XCUIElement *)anchorElement
283
330
  {
284
331
  CGVector scrollVector = CGVectorMake(CGRectGetWidth(self.scrollingFrame) * normalizedScrollVector.dx,
285
332
  CGRectGetHeight(self.scrollingFrame) * normalizedScrollVector.dy
286
333
  );
287
- return [self fb_scrollByVector:scrollVector inApplication:application error:nil];
334
+ return [self fb_scrollByVector:scrollVector anchorElement:anchorElement error:nil];
288
335
  }
289
336
 
290
337
  - (BOOL)fb_scrollByVector:(CGVector)vector
291
- inApplication:(XCUIApplication *)application
292
- error:(NSError **)error
338
+ anchorElement:(XCUIElement *)anchorElement
339
+ error:(NSError **)error
293
340
  {
294
341
  CGVector scrollBoundingVector = CGVectorMake(
295
342
  CGRectGetWidth(self.scrollingFrame) * FBScrollTouchProportion,
@@ -306,29 +353,49 @@ const CGFloat FBScrollTouchProportion = 0.75f;
306
353
  fabs(vector.dy) > fabs(scrollBoundingVector.dy) ? scrollBoundingVector.dy : vector.dy);
307
354
  vector = CGVectorMake(vector.dx - scrollVector.dx, vector.dy - scrollVector.dy);
308
355
  shouldFinishScrolling = FBVectorFuzzyEqualToVector(vector, CGZeroVector, 1) || --preciseScrollAttemptsCount <= 0;
309
- if (![self fb_scrollAncestorScrollViewByVectorWithinScrollViewFrame:scrollVector inApplication:application error:error]){
356
+ if (![self fb_scrollAncestorScrollViewByVectorWithinScrollViewFrame:scrollVector anchorElement:anchorElement error:error]){
310
357
  return NO;
311
358
  }
312
359
  }
313
360
  return YES;
314
361
  }
315
362
 
316
- - (CGVector)fb_hitPointOffsetForScrollingVector:(CGVector)scrollingVector
363
+ // Normalized (0.0-1.0) touch-down offset within the scrolling frame, for the given
364
+ // scroll vector's direction.
365
+ - (CGVector)fb_normalizedHitPointOffsetForScrollingVector:(CGVector)scrollingVector
317
366
  {
318
- CGFloat x = CGRectGetMinX(self.scrollingFrame) + CGRectGetWidth(self.scrollingFrame) * (scrollingVector.dx < 0.0f ? FBScrollTouchProportion : (1 - FBScrollTouchProportion));
319
- CGFloat y = CGRectGetMinY(self.scrollingFrame) + CGRectGetHeight(self.scrollingFrame) * (scrollingVector.dy < 0.0f ? FBScrollTouchProportion : (1 - FBScrollTouchProportion));
320
- return CGVectorMake((CGFloat)floor(x), (CGFloat)floor(y));
367
+ CGFloat x = scrollingVector.dx < 0.0f ? FBScrollTouchProportion : (1 - FBScrollTouchProportion);
368
+ CGFloat y = scrollingVector.dy < 0.0f ? FBScrollTouchProportion : (1 - FBScrollTouchProportion);
369
+ return CGVectorMake(x, y);
321
370
  }
322
371
 
323
372
  - (BOOL)fb_scrollAncestorScrollViewByVectorWithinScrollViewFrame:(CGVector)vector
324
- inApplication:(XCUIApplication *)application
325
- error:(NSError **)error
373
+ anchorElement:(XCUIElement *)anchorElement
374
+ error:(NSError **)error
326
375
  {
327
- CGVector hitpointOffset = [self fb_hitPointOffsetForScrollingVector:vector];
376
+ CGRect scrollingFrame = self.scrollingFrame;
377
+ CGRect anchorFrame = anchorElement.frame;
378
+ if (CGRectIsEmpty(scrollingFrame) || CGRectIsEmpty(anchorFrame)) {
379
+ return [[[FBErrorBuilder builder]
380
+ withDescriptionFormat:@"Cannot compute a scroll gesture for '%@': its frame is empty", self.fb_description]
381
+ buildError:error];
382
+ }
328
383
 
329
- XCUICoordinate *appCoordinate = [[XCUICoordinate alloc] initWithElement:application normalizedOffset:CGVectorMake(0.0, 0.0)];
330
- XCUICoordinate *startCoordinate = [[XCUICoordinate alloc] initWithCoordinate:appCoordinate pointsOffset:hitpointOffset];
331
- XCUICoordinate *endCoordinate = [[XCUICoordinate alloc] initWithCoordinate:startCoordinate pointsOffset:vector];
384
+ // Compute the touch-down/up points within the (possibly clipped) scrolling frame as
385
+ // before, then express them as fractions of the anchor element's own frame instead of
386
+ // raw points, which XCTest never rescales for compatibility-mode windows
387
+ // (appium/appium#16185). When scrollingFrame == anchorFrame this resolves to the exact
388
+ // same absolute point as before; it only differs once XCTest itself rescales anchorFrame.
389
+ CGVector proportion = [self fb_normalizedHitPointOffsetForScrollingVector:vector];
390
+ CGPoint startPoint = CGPointMake((CGFloat)floor(scrollingFrame.origin.x + scrollingFrame.size.width * proportion.dx),
391
+ (CGFloat)floor(scrollingFrame.origin.y + scrollingFrame.size.height * proportion.dy));
392
+ CGPoint endPoint = CGPointMake((CGFloat)floor(startPoint.x + vector.dx), (CGFloat)floor(startPoint.y + vector.dy));
393
+ CGVector startOffset = CGVectorMake((startPoint.x - anchorFrame.origin.x) / anchorFrame.size.width,
394
+ (startPoint.y - anchorFrame.origin.y) / anchorFrame.size.height);
395
+ CGVector endOffset = CGVectorMake((endPoint.x - anchorFrame.origin.x) / anchorFrame.size.width,
396
+ (endPoint.y - anchorFrame.origin.y) / anchorFrame.size.height);
397
+ XCUICoordinate *startCoordinate = [anchorElement coordinateWithNormalizedOffset:startOffset];
398
+ XCUICoordinate *endCoordinate = [anchorElement coordinateWithNormalizedOffset:endOffset];
332
399
 
333
400
  if (FBPointFuzzyEqualToPoint(startCoordinate.screenPoint, endCoordinate.screenPoint, FBFuzzyPointThreshold)) {
334
401
  return YES;
@@ -353,14 +353,23 @@
353
353
  + (id<FBResponsePayload>)handlePressAndDragCoordinateWithVelocity:(FBRouteRequest *)request
354
354
  {
355
355
  XCUIApplication *application = request.session.activeApplication;
356
+ NSError *error;
356
357
  CGVector startOffset = CGVectorMake((CGFloat)[request.arguments[@"fromX"] doubleValue],
357
358
  (CGFloat)[request.arguments[@"fromY"] doubleValue]);
358
359
  XCUICoordinate *startCoordinate = [self.class gestureCoordinateWithOffset:startOffset
359
- element:application];
360
+ element:application
361
+ error:&error];
362
+ if (nil == startCoordinate) {
363
+ return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]);
364
+ }
360
365
  CGVector endOffset = CGVectorMake((CGFloat)[request.arguments[@"toX"] doubleValue],
361
366
  (CGFloat)[request.arguments[@"toY"] doubleValue]);
362
367
  XCUICoordinate *endCoordinate = [self.class gestureCoordinateWithOffset:endOffset
363
- element:application];
368
+ element:application
369
+ error:&error];
370
+ if (nil == endCoordinate) {
371
+ return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]);
372
+ }
364
373
  [startCoordinate pressForDuration:[request.arguments[@"pressDuration"] doubleValue]
365
374
  thenDragToCoordinate:endCoordinate
366
375
  withVelocity:[request.arguments[@"velocity"] doubleValue]
@@ -433,12 +442,19 @@
433
442
  + (id<FBResponsePayload>)handleDrag:(FBRouteRequest *)request
434
443
  {
435
444
  XCUIElement *target = [self targetFromRequest:request];
445
+ NSError *error;
436
446
  CGVector startOffset = CGVectorMake([request.arguments[@"fromX"] doubleValue],
437
447
  [request.arguments[@"fromY"] doubleValue]);
438
- XCUICoordinate *startCoordinate = [self.class gestureCoordinateWithOffset:startOffset element:target];
448
+ XCUICoordinate *startCoordinate = [self.class gestureCoordinateWithOffset:startOffset element:target error:&error];
449
+ if (nil == startCoordinate) {
450
+ return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]);
451
+ }
439
452
  CGVector endOffset = CGVectorMake([request.arguments[@"toX"] doubleValue],
440
453
  [request.arguments[@"toY"] doubleValue]);
441
- XCUICoordinate *endCoordinate = [self.class gestureCoordinateWithOffset:endOffset element:target];
454
+ XCUICoordinate *endCoordinate = [self.class gestureCoordinateWithOffset:endOffset element:target error:&error];
455
+ if (nil == endCoordinate) {
456
+ return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]);
457
+ }
442
458
  NSTimeInterval duration = [request.arguments[@"duration"] doubleValue];
443
459
  [startCoordinate pressForDuration:duration thenDragToCoordinate:endCoordinate];
444
460
  return FBResponseWithOK();
@@ -659,12 +675,15 @@ static const NSInteger DEFAULT_MAX_PICKER_ATTEMPTS = 25;
659
675
 
660
676
  @param offset absolute screen offset for the given application
661
677
  @param element the element instance to perform the gesture on
662
- @return translated gesture coordinates ready to be passed to XCUICoordinate methods
678
+ @param error Error instance if any
679
+ @return translated gesture coordinates ready to be passed to XCUICoordinate methods, or
680
+ nil if the element is not visible on the screen
663
681
  */
664
- + (XCUICoordinate *)gestureCoordinateWithOffset:(CGVector)offset
665
- element:(XCUIElement *)element
682
+ + (nullable XCUICoordinate *)gestureCoordinateWithOffset:(CGVector)offset
683
+ element:(XCUIElement *)element
684
+ error:(NSError **)error
666
685
  {
667
- return [[element coordinateWithNormalizedOffset:CGVectorMake(0, 0)] coordinateWithOffset:offset];
686
+ return FBCoordinateWithAnchorOffset(element, CGVectorMake(0, 0), offset, error);
668
687
  }
669
688
 
670
689
  /**
@@ -688,7 +707,8 @@ static const NSInteger DEFAULT_MAX_PICKER_ATTEMPTS = 25;
688
707
  return nil;
689
708
  }
690
709
  return [self gestureCoordinateWithOffset:CGVectorMake(x.doubleValue, y.doubleValue)
691
- element:[self targetFromRequest:request]];
710
+ element:[self targetFromRequest:request]
711
+ error:error];
692
712
  }
693
713
 
694
714
  /**
@@ -15,11 +15,11 @@
15
15
  <key>CFBundlePackageType</key>
16
16
  <string>FMWK</string>
17
17
  <key>CFBundleShortVersionString</key>
18
- <string>16.12.5</string>
18
+ <string>16.12.6</string>
19
19
  <key>CFBundleSignature</key>
20
20
  <string>????</string>
21
21
  <key>CFBundleVersion</key>
22
- <string>16.12.5</string>
22
+ <string>16.12.6</string>
23
23
  <key>NSPrincipalClass</key>
24
24
  <string/>
25
25
  </dict>
@@ -74,9 +74,9 @@
74
74
  return [element coordinateWithNormalizedOffset:CGVectorMake(0.5, 0.5)];
75
75
  }
76
76
 
77
- CGVector offset = CGVectorMake(positionOffset.CGPointValue.x, positionOffset.CGPointValue.y);
78
77
  // TODO: Shall we throw an exception if hitPoint is out of the element frame?
79
- return [[element coordinateWithNormalizedOffset:CGVectorMake(0, 0)] coordinateWithOffset:offset];
78
+ CGVector offset = CGVectorMake(positionOffset.CGPointValue.x, positionOffset.CGPointValue.y);
79
+ return FBCoordinateWithAnchorOffset((XCUIElement *)element, CGVectorMake(0, 0), offset, error);
80
80
  }
81
81
 
82
82
  @end
@@ -9,6 +9,10 @@
9
9
  #import <UIKit/UIKit.h>
10
10
 
11
11
  @class XCUIApplication;
12
+ @class XCUICoordinate;
13
+ @class XCUIElement;
14
+
15
+ NS_ASSUME_NONNULL_BEGIN
12
16
 
13
17
  extern CGFloat FBDefaultFrameFuzzyThreshold;
14
18
 
@@ -34,3 +38,27 @@ BOOL FBRectFuzzyEqualToRect(CGRect rect1, CGRect rect2, CGFloat threshold);
34
38
  /*! Inverts size if necessary to match current screen orientation */
35
39
  CGSize FBAdjustDimensionsForApplication(CGSize actualSize, UIInterfaceOrientation orientation);
36
40
  #endif
41
+
42
+ #if !TARGET_OS_TV
43
+ /*!
44
+ Builds a coordinate for the given element from a raw points offset measured from a
45
+ normalized anchor point within the element's own frame - e.g. (0, 0) for an offset
46
+ relative to the top-left corner, (0.5, 0.5) for one relative to the center, as W3C
47
+ actions use. The offset is normalized against the element's wdFrame (the same
48
+ WDA-reported coordinate space pointsOffset itself is measured in) instead of being
49
+ passed through as a raw points offset, which XCTest never rescales for
50
+ compatibility-mode windows (see appium/appium#16185).
51
+
52
+ @param element the element to anchor the coordinate to
53
+ @param anchorOffset normalized offset of the anchor point within the element's wdFrame
54
+ @param pointsOffset raw points offset from the anchor point, in wdFrame's coordinate space
55
+ @param error populated if the element's frame is empty (not visible on the screen)
56
+ @return the resulting coordinate, or nil if the element's frame is empty
57
+ */
58
+ XCUICoordinate * _Nullable FBCoordinateWithAnchorOffset(XCUIElement *element,
59
+ CGVector anchorOffset,
60
+ CGVector pointsOffset,
61
+ NSError **error);
62
+ #endif
63
+
64
+ NS_ASSUME_NONNULL_END
@@ -8,7 +8,11 @@
8
8
 
9
9
  #import "FBMathUtils.h"
10
10
 
11
+ #import "FBErrorBuilder.h"
11
12
  #import "FBMacros.h"
13
+ #import "XCUICoordinate.h"
14
+ #import "XCUIElement.h"
15
+ #import "XCUIElement+FBWebDriverAttributes.h"
12
16
 
13
17
  CGFloat FBDefaultFrameFuzzyThreshold = 2.0;
14
18
 
@@ -51,7 +55,7 @@ CGSize FBAdjustDimensionsForApplication(CGSize actualSize, UIInterfaceOrientatio
51
55
  if (orientation == UIInterfaceOrientationLandscapeLeft || orientation == UIInterfaceOrientationLandscapeRight) {
52
56
  /*
53
57
  There is an XCTest bug that application.frame property returns exchanged dimensions for landscape mode.
54
- This verification is just to make sure the bug is still there (since height is never greater than width in landscape)
58
+ This verification is just to make sure the bug is still there (since height is never greater than width in landscape)
55
59
  and to make it still working properly after XCTest itself starts to respect landscape mode.
56
60
  */
57
61
  if (actualSize.height > actualSize.width) {
@@ -61,3 +65,24 @@ CGSize FBAdjustDimensionsForApplication(CGSize actualSize, UIInterfaceOrientatio
61
65
  return actualSize;
62
66
  }
63
67
  #endif
68
+
69
+ #if !TARGET_OS_TV
70
+ XCUICoordinate *FBCoordinateWithAnchorOffset(XCUIElement *element,
71
+ CGVector anchorOffset,
72
+ CGVector pointsOffset,
73
+ NSError **error)
74
+ {
75
+ // wdFrame matches the coordinate space pointsOffset was measured in; element.frame alone
76
+ // can already be pre-scaled for a compatibility-mode window mismatch, double-applying it.
77
+ CGRect frame = element.wdFrame;
78
+ if (CGRectIsEmpty(frame)) {
79
+ [[[FBErrorBuilder builder]
80
+ withDescriptionFormat:@"The element '%@' is not visible on the screen and thus is not interactable", element.description]
81
+ buildError:error];
82
+ return nil;
83
+ }
84
+ CGVector normalizedOffset = CGVectorMake(anchorOffset.dx + pointsOffset.dx / frame.size.width,
85
+ anchorOffset.dy + pointsOffset.dy / frame.size.height);
86
+ return [element coordinateWithNormalizedOffset:normalizedOffset];
87
+ }
88
+ #endif
@@ -162,7 +162,7 @@ static NSString *const FB_KEY_ACTIONS = @"actions";
162
162
  return [super hitpointWithElement:element positionOffset:positionOffset error:error];
163
163
  }
164
164
 
165
- // An offset relative to the element is defined
165
+ // An offset relative to the element is defined.
166
166
  if (CGRectIsEmpty(element.frame)) {
167
167
  [FBLogger log:self.application.fb_descriptionRepresentation];
168
168
  NSString *description = [NSString stringWithFormat:@"The element '%@' is not visible on the screen and thus is not interactable",
@@ -174,9 +174,9 @@ static NSString *const FB_KEY_ACTIONS = @"actions";
174
174
  }
175
175
 
176
176
  // W3C standard requires that relative element coordinates start at the center of the element's rectangle
177
- CGVector offset = CGVectorMake(positionOffset.CGPointValue.x, positionOffset.CGPointValue.y);
178
177
  // TODO: Shall we throw an exception if hitPoint is out of the element frame?
179
- return [[element coordinateWithNormalizedOffset:CGVectorMake(0.5, 0.5)] coordinateWithOffset:offset];
178
+ CGVector offset = CGVectorMake(positionOffset.CGPointValue.x, positionOffset.CGPointValue.y);
179
+ return FBCoordinateWithAnchorOffset((XCUIElement *)element, CGVectorMake(0.5, 0.5), offset, error);
180
180
  }
181
181
 
182
182
  @end
@@ -78,9 +78,15 @@
78
78
  {
79
79
  if (touch)
80
80
  {
81
+ CGPoint location = [touch locationInView:self];
82
+ // Exposes the last touch-down location, in this view's own bounds coordinate
83
+ // space, for tests to assert on regardless of any window-level scaling.
84
+ self.isAccessibilityElement = YES;
85
+ self.accessibilityValue = [NSString stringWithFormat:@"%.2f,%.2f", location.x, location.y];
86
+
81
87
  TouchSpotView *newView = [[TouchSpotView alloc] init];
82
88
  newView.bounds = CGRectMake(0, 0, 1, 1);
83
- newView.center = [touch locationInView:self];
89
+ newView.center = location;
84
90
  [self addSubview:newView];
85
91
  [UIView animateWithDuration:0.2 animations:^{
86
92
  newView.bounds = CGRectMake(0, 0, 100, 100);
@@ -93,10 +93,10 @@
93
93
  - (void)setUp
94
94
  {
95
95
  [super setUp];
96
- static dispatch_once_t onceToken;
97
- dispatch_once(&onceToken, ^{
98
- [self openScrollView];
99
- });
96
+ // Each test (and retry) must start at row zero, not at the previous test's
97
+ // scroll offset. A velocity-based swipe need not undo an earlier swipe.
98
+ [self resetOrientation];
99
+ [self openScrollView];
100
100
  }
101
101
 
102
102
  - (void)testSwipeUp
@@ -47,10 +47,19 @@
47
47
  NSUInteger count = 0;
48
48
  for (NSNumber *orientation in orientations) {
49
49
  [self rotateTo:orientation.integerValue];
50
+ // XCTest can report idle before the rotation animation finishes. Re-read
51
+ // the geometry until the controls have reached their on-screen layout.
52
+ FBAssertWaitTillBecomesTrue(
53
+ CGRectContainsRect(self.testedApplication.frame, canvas.frame)
54
+ && CGRectContainsRect(self.testedApplication.frame, taps.frame)
55
+ && CGRectContainsRect(self.testedApplication.frame, touches.frame));
50
56
  CGRect screen = self.testedApplication.frame;
51
- XCTAssertTrue(CGRectContainsRect(screen, canvas.frame));
52
- XCTAssertTrue(CGRectContainsRect(screen, taps.frame));
53
- XCTAssertTrue(CGRectContainsRect(screen, touches.frame));
57
+ XCTAssertTrue(CGRectContainsRect(screen, canvas.frame), @"Screen: %@; canvas: %@",
58
+ NSStringFromCGRect(screen), NSStringFromCGRect(canvas.frame));
59
+ XCTAssertTrue(CGRectContainsRect(screen, taps.frame), @"Screen: %@; taps: %@",
60
+ NSStringFromCGRect(screen), NSStringFromCGRect(taps.frame));
61
+ XCTAssertTrue(CGRectContainsRect(screen, touches.frame), @"Screen: %@; touches: %@",
62
+ NSStringFromCGRect(screen), NSStringFromCGRect(touches.frame));
54
63
  XCTAssertGreaterThan(CGRectGetHeight(canvas.frame), 0);
55
64
  [canvas tap];
56
65
  NSString *expectedTaps = [NSString stringWithFormat:@"%lu", (unsigned long)++count];
@@ -91,4 +91,11 @@ extern NSArray<NSString *> *const FBMainViewButtonLabels;
91
91
  */
92
92
  - (void)resetOrientation;
93
93
 
94
+ /**
95
+ appium/appium#16185: skips the current test unless the app's window size actually
96
+ differs from SpringBoard's, e.g. an iPhone-only app on iPad (built with
97
+ TARGETED_DEVICE_FAMILY=1).
98
+ */
99
+ - (void)skipUnlessWindowSizeMismatchesDevice;
100
+
94
101
  @end
@@ -163,4 +163,14 @@ NSArray<NSString *> *const FBMainViewButtonLabels = @[
163
163
  FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count == 0);
164
164
  }
165
165
 
166
+ - (void)skipUnlessWindowSizeMismatchesDevice
167
+ {
168
+ CGSize appSize = self.testedApplication.frame.size;
169
+ CGSize deviceSize = self.springboard.frame.size;
170
+ if (fabs(appSize.width - deviceSize.width) < 1 && fabs(appSize.height - deviceSize.height) < 1) {
171
+ XCTSkip(@"App window size matches SpringBoard's on this build/device, so it does not "
172
+ "reproduce the compatibility-mode mismatch from appium/appium#16185");
173
+ }
174
+ }
175
+
166
176
  @end
@@ -11,9 +11,13 @@
11
11
  #import "FBIntegrationTestCase.h"
12
12
 
13
13
  #import "FBElementCache.h"
14
+ #import "FBMathUtils.h"
14
15
  #import "FBTestMacros.h"
16
+ #import "XCUIApplication+FBTouchAction.h"
17
+ #import "XCUICoordinate.h"
15
18
  #import "XCUIDevice+FBRotation.h"
16
19
  #import "XCUIElement+FBIsVisible.h"
20
+ #import "XCUIElement+FBWebDriverAttributes.h"
17
21
 
18
22
  @interface FBTapTest : FBIntegrationTestCase
19
23
  @end
@@ -100,4 +104,83 @@
100
104
  [self verifyTapByCoordinatesWithOrientation:UIDeviceOrientationPortraitUpsideDown];
101
105
  }
102
106
 
107
+ // Element-less absolute offsets are never rescaled by XCTest's XCUICoordinate (verified by
108
+ // disassembling XCUIAutomation.framework), so this still fails under a window-size mismatch.
109
+ - (void)testTapAtElementRectCenterUnderWindowSizeMismatch
110
+ {
111
+ [self skipUnlessWindowSizeMismatchesDevice];
112
+
113
+ XCUIElement *dstButton = self.testedApplication.buttons[FBShowAlertButtonName];
114
+ CGRect rect = dstButton.wdFrame;
115
+ CGPoint center = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect));
116
+
117
+ // Mirrors FBBaseActionsSynthesizer's hitpointWithElement:positionOffset:
118
+ // for an absolute (x, y) offset, as used by touch/perform and W3C actions.
119
+ XCUICoordinate *appOrigin = [self.testedApplication coordinateWithNormalizedOffset:CGVectorMake(0, 0)];
120
+ XCUICoordinate *tapPoint = [appOrigin coordinateWithOffset:CGVectorMake(center.x, center.y)];
121
+ [tapPoint tap];
122
+
123
+ XCTExpectFailureInBlock(@"element-less absolute offsets are never rescaled by XCTest for a "
124
+ "compatibility-mode window (appium/appium#16185); starts failing "
125
+ "loudly here the moment XCTest fixes this itself", ^{
126
+ FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count > 0);
127
+ });
128
+ }
129
+
130
+ @end
131
+
132
+ // The Touch page's touchable view records each touch-down's location, in its own bounds
133
+ // coordinate space, as its accessibility value - a ground truth unaffected by any
134
+ // window-level scaling, letting these tests assert on exact landing position rather than
135
+ // just on whether a tap happened to land inside some (possibly large) target.
136
+ @interface FBElementOffsetTapTest : FBIntegrationTestCase
137
+ @end
138
+
139
+ @implementation FBElementOffsetTapTest
140
+
141
+ - (void)setUp
142
+ {
143
+ [super setUp];
144
+ [self launchApplication];
145
+ [self goToTouchPage];
146
+ }
147
+
148
+ - (CGPoint)lastTouchLocationOf:(XCUIElement *)touchable
149
+ {
150
+ NSString *value = touchable.value;
151
+ NSArray<NSString *> *components = [value componentsSeparatedByString:@","];
152
+ return CGPointMake(components.firstObject.doubleValue, components.lastObject.doubleValue);
153
+ }
154
+
155
+ // FBW3CActionsSynthesizer normalizes element-relative offsets against the element's own
156
+ // frame, so this keeps landing at the intended point under a window-size mismatch
157
+ // (appium/appium#16185), unlike an element-less absolute offset.
158
+ - (void)testTapWithElementOffsetUnderWindowSizeMismatch
159
+ {
160
+ [self skipUnlessWindowSizeMismatchesDevice];
161
+
162
+ XCUIElement *touchable = self.testedApplication.otherElements[@"touchableView"];
163
+ CGSize size = touchable.wdFrame.size;
164
+ CGVector offset = CGVectorMake(size.width / 4, -size.height / 4);
165
+ CGPoint expectedLocation = CGPointMake(size.width / 2 + offset.dx, size.height / 2 + offset.dy);
166
+
167
+ NSArray<NSDictionary<NSString *, id> *> *gesture =
168
+ @[@{
169
+ @"type": @"pointer",
170
+ @"id": @"finger1",
171
+ @"parameters": @{@"pointerType": @"touch"},
172
+ @"actions": @[
173
+ @{@"type": @"pointerMove", @"duration": @0, @"origin": touchable, @"x": @(offset.dx), @"y": @(offset.dy)},
174
+ @{@"type": @"pointerDown"},
175
+ @{@"type": @"pause", @"duration": @50},
176
+ @{@"type": @"pointerUp"},
177
+ ],
178
+ },
179
+ ];
180
+ NSError *error;
181
+ XCTAssertTrue([self.testedApplication fb_performW3CActions:gesture elementCache:nil error:&error]);
182
+
183
+ FBAssertWaitTillBecomesTrue(FBPointFuzzyEqualToPoint([self lastTouchLocationOf:touchable], expectedLocation, 5.0));
184
+ }
185
+
103
186
  @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appium-webdriveragent",
3
- "version": "16.12.5",
3
+ "version": "16.12.6",
4
4
  "description": "Package bundling WebDriverAgent",
5
5
  "keywords": [
6
6
  "Appium",