Skip to content

Commit 9ada9fd

Browse files
fix: use wdFrame instead of raw frame for scroll gesture anchor (#1256)
1 parent 6a2ae05 commit 9ada9fd

4 files changed

Lines changed: 89 additions & 13 deletions

File tree

WebDriverAgentLib/Categories/XCUIElement+FBScrolling.m

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -374,26 +374,18 @@ - (BOOL)fb_scrollAncestorScrollViewByVectorWithinScrollViewFrame:(CGVector)vecto
374374
error:(NSError **)error
375375
{
376376
CGRect scrollingFrame = self.scrollingFrame;
377-
CGRect anchorFrame = anchorElement.frame;
377+
// wdFrame matches scrollingFrame's coordinate space; raw .frame can be pre-scaled or
378+
// dimension-swapped and drift out of sync with it (appium/appium#16185).
379+
CGRect anchorFrame = anchorElement.wdFrame;
378380
if (CGRectIsEmpty(scrollingFrame) || CGRectIsEmpty(anchorFrame)) {
379381
return [[[FBErrorBuilder builder]
380382
withDescriptionFormat:@"Cannot compute a scroll gesture for '%@': its frame is empty", self.fb_description]
381383
buildError:error];
382384
}
383385

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.
389386
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);
387+
CGVector startOffset, endOffset;
388+
FBScrollGestureOffsets(scrollingFrame, anchorFrame, proportion, vector, &startOffset, &endOffset);
397389
XCUICoordinate *startCoordinate = [anchorElement coordinateWithNormalizedOffset:startOffset];
398390
XCUICoordinate *endCoordinate = [anchorElement coordinateWithNormalizedOffset:endOffset];
399391

WebDriverAgentLib/Utilities/FBMathUtils.h

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,29 @@ XCUICoordinate * _Nullable FBCoordinateWithAnchorOffset(XCUIElement *element,
6161
NSError **error);
6262
#endif
6363

64+
/*!
65+
Computes the normalized (0.0-1.0) start/end offsets of a scroll drag gesture whose
66+
touch-down/up points fall within scrollingFrame, expressed relative to anchorFrame -
67+
the coordinate space the resulting offsets get resolved against (e.g. via
68+
-[XCUIElement coordinateWithNormalizedOffset:]). scrollingFrame and anchorFrame are
69+
usually the same rect, but scrollingFrame may be clipped to a visible sub-region, and/or
70+
the two may come from frame sources XCTest doesn't keep in sync (see appium/appium#16185)
71+
- passing mismatched frames here reproduces that bug rather than fixing it.
72+
73+
@param scrollingFrame the (possibly clipped) frame to compute the touch-down/up points within
74+
@param anchorFrame the frame startOffset/endOffset get normalized against
75+
@param proportion normalized touch-down position within scrollingFrame, e.g. from
76+
-fb_normalizedHitPointOffsetForScrollingVector:
77+
@param vector the scroll vector, in scrollingFrame's coordinate space
78+
@param startOffset populated with the normalized start offset; untouched if NO is returned
79+
@param endOffset populated with the normalized end offset; untouched if NO is returned
80+
@return NO if either frame is empty
81+
*/
82+
BOOL FBScrollGestureOffsets(CGRect scrollingFrame,
83+
CGRect anchorFrame,
84+
CGVector proportion,
85+
CGVector vector,
86+
CGVector *startOffset,
87+
CGVector *endOffset);
88+
6489
NS_ASSUME_NONNULL_END

WebDriverAgentLib/Utilities/FBMathUtils.m

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,24 @@ This verification is just to make sure the bug is still there (since height is n
8686
return [element coordinateWithNormalizedOffset:normalizedOffset];
8787
}
8888
#endif
89+
90+
BOOL FBScrollGestureOffsets(CGRect scrollingFrame,
91+
CGRect anchorFrame,
92+
CGVector proportion,
93+
CGVector vector,
94+
CGVector *startOffset,
95+
CGVector *endOffset)
96+
{
97+
if (CGRectIsEmpty(scrollingFrame) || CGRectIsEmpty(anchorFrame)) {
98+
return NO;
99+
}
100+
101+
CGPoint startPoint = CGPointMake((CGFloat)floor(scrollingFrame.origin.x + scrollingFrame.size.width * proportion.dx),
102+
(CGFloat)floor(scrollingFrame.origin.y + scrollingFrame.size.height * proportion.dy));
103+
CGPoint endPoint = CGPointMake((CGFloat)floor(startPoint.x + vector.dx), (CGFloat)floor(startPoint.y + vector.dy));
104+
*startOffset = CGVectorMake((startPoint.x - anchorFrame.origin.x) / anchorFrame.size.width,
105+
(startPoint.y - anchorFrame.origin.y) / anchorFrame.size.height);
106+
*endOffset = CGVectorMake((endPoint.x - anchorFrame.origin.x) / anchorFrame.size.width,
107+
(endPoint.y - anchorFrame.origin.y) / anchorFrame.size.height);
108+
return YES;
109+
}

WebDriverAgentTests/UnitTests/FBMathUtilsTests.m

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,42 @@ - (void)testSizeInversion
110110
XCTAssertTrue(FBSizeFuzzyEqualToSize(screenSizeLandscape, FBAdjustDimensionsForApplication(screenSizeLandscape, UIInterfaceOrientationLandscapeRight), t));
111111
}
112112

113+
- (void)testScrollGestureOffsetsWithMatchingFrames
114+
{
115+
CGRect frame = CGRectMake(20, 200, 300, 400);
116+
CGVector proportion = CGVectorMake(0.5, 0.75);
117+
CGVector vector = CGVectorMake(0, -200);
118+
CGVector startOffset, endOffset;
119+
XCTAssertTrue(FBScrollGestureOffsets(frame, frame, proportion, vector, &startOffset, &endOffset));
120+
XCTAssertTrue(FBVectorFuzzyEqualToVector(startOffset, CGVectorMake(0.5, 0.75), 0.01));
121+
XCTAssertTrue(FBVectorFuzzyEqualToVector(endOffset, CGVectorMake(0.5, 0.25), 0.01));
122+
}
123+
124+
- (void)testScrollGestureOffsetsWithRescaledAnchorFrame
125+
{
126+
// Simulates a compatibility-mode window: anchorFrame is scrollingFrame scaled by ~2.19x,
127+
// same origin - offsets should still land within [0, 1] instead of drifting outside it.
128+
CGRect scrollingFrame = CGRectMake(20, 202, 335, 420);
129+
CGRect anchorFrame = CGRectMake(20, 202, 733, 920);
130+
CGVector proportion = CGVectorMake(0.5, 0.75);
131+
CGVector vector = CGVectorMake(0, -250);
132+
CGVector startOffset, endOffset;
133+
XCTAssertTrue(FBScrollGestureOffsets(scrollingFrame, anchorFrame, proportion, vector, &startOffset, &endOffset));
134+
XCTAssertTrue(startOffset.dx >= 0 && startOffset.dx <= 1);
135+
XCTAssertTrue(startOffset.dy >= 0 && startOffset.dy <= 1);
136+
XCTAssertTrue(endOffset.dx >= 0 && endOffset.dx <= 1);
137+
XCTAssertTrue(endOffset.dy >= 0 && endOffset.dy <= 1);
138+
}
139+
140+
- (void)testScrollGestureOffsetsWithEmptyFrame
141+
{
142+
CGVector startOffset = CGVectorMake(-1, -1);
143+
CGVector endOffset = CGVectorMake(-1, -1);
144+
XCTAssertFalse(FBScrollGestureOffsets(CGRectZero, CGRectMake(0, 0, 100, 100), CGVectorMake(0.5, 0.5), CGVectorMake(0, -50), &startOffset, &endOffset));
145+
XCTAssertFalse(FBScrollGestureOffsets(CGRectMake(0, 0, 100, 100), CGRectZero, CGVectorMake(0.5, 0.5), CGVectorMake(0, -50), &startOffset, &endOffset));
146+
// Untouched on failure
147+
XCTAssertTrue(startOffset.dx == -1 && startOffset.dy == -1);
148+
XCTAssertTrue(endOffset.dx == -1 && endOffset.dy == -1);
149+
}
150+
113151
@end

0 commit comments

Comments
 (0)