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 +13 -0
- package/README.md +9 -8
- package/WebDriverAgent.xcodeproj/project.pbxproj +4 -0
- package/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.m +8 -0
- package/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.m +110 -50
- package/WebDriverAgentLib/Commands/FBCustomCommands.m +42 -10
- package/WebDriverAgentLib/Commands/FBElementCommands.m +30 -1
- package/WebDriverAgentLib/Info.plist +2 -2
- package/WebDriverAgentLib/Utilities/FBMathUtils.h +11 -0
- package/WebDriverAgentLib/Utilities/FBMathUtils.m +38 -0
- package/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.m +121 -13
- package/WebDriverAgentTests/IntegrationApp/COORDINATE_PROBE.md +3 -1
- package/WebDriverAgentTests/IntegrationApp/Classes/FBCoordinateProbeViewController.m +18 -0
- package/WebDriverAgentTests/IntegrationApp/Classes/ViewController.m +15 -0
- package/WebDriverAgentTests/IntegrationTests/FBConfigurationTests.m +52 -0
- package/WebDriverAgentTests/IntegrationTests/FBDisplayGestureTests.m +541 -0
- package/WebDriverAgentTests/IntegrationTests/FBScreenTests.m +148 -0
- package/WebDriverAgentTests/IntegrationTests/FBW3CTouchActionsIntegrationTests.m +126 -0
- package/WebDriverAgentTests/UnitTests/FBMathUtilsTests.m +45 -0
- package/package.json +1 -1
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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`
|
|
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 (
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
332
|
-
|
|
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:
|
|
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
|
-
|
|
342
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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": @(
|
|
185
|
-
@"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
|
-
|
|
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.
|
|
18
|
+
<string>16.14.2</string>
|
|
19
19
|
<key>CFBundleSignature</key>
|
|
20
20
|
<string>????</string>
|
|
21
21
|
<key>CFBundleVersion</key>
|
|
22
|
-
<string>16.14.
|
|
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
|