appium-webdriveragent 16.14.1 → 16.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +9 -8
- package/WebDriverAgent.xcodeproj/project.pbxproj +4 -0
- package/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.m +110 -50
- package/WebDriverAgentLib/Commands/FBCustomCommands.m +8 -1
- package/WebDriverAgentLib/Commands/FBElementCommands.m +30 -1
- package/WebDriverAgentLib/Commands/FBSessionCommands.m +2 -1
- package/WebDriverAgentLib/Info.plist +2 -2
- package/WebDriverAgentLib/Routing/FBSession.h +22 -0
- package/WebDriverAgentLib/Routing/FBSession.m +25 -3
- 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/IntegrationTests/FBDisplayGestureTests.m +541 -0
- package/WebDriverAgentTests/IntegrationTests/FBScreenTests.m +13 -7
- package/WebDriverAgentTests/IntegrationTests/FBSessionIntegrationTests.m +71 -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,15 @@
|
|
|
1
|
+
## [16.15.0](https://github.com/appium/WebDriverAgent/compare/v16.14.2...v16.15.0) (2026-10-12)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* add asAppUnderTest flag to /wda/apps/launch ([#1289](https://github.com/appium/WebDriverAgent/issues/1289)) ([f5c0688](https://github.com/appium/WebDriverAgent/commit/f5c068873ff6dcd1ce444d26246c102cc379d5bf))
|
|
6
|
+
|
|
7
|
+
## [16.14.2](https://github.com/appium/WebDriverAgent/compare/v16.14.1...v16.14.2) (2026-10-08)
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* 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)
|
|
12
|
+
|
|
1
13
|
## [16.14.1](https://github.com/appium/WebDriverAgent/compare/v16.14.0...v16.14.1) (2026-10-07)
|
|
2
14
|
|
|
3
15
|
### Bug Fixes
|
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 */,
|
|
@@ -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
|
}
|
|
@@ -174,7 +174,14 @@
|
|
|
174
174
|
CGRectGetHeight(screen.bounds) / screen.scale);
|
|
175
175
|
#if !TARGET_OS_TV && !TARGET_OS_WATCH
|
|
176
176
|
XCUIApplication *activeApp = request.session ? request.session.activeApplication : XCUIApplication.fb_activeApplication;
|
|
177
|
-
|
|
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);
|
|
178
185
|
#endif
|
|
179
186
|
|
|
180
187
|
// SpringBoard can expose hidden status bars belonging to other displays, and
|
|
@@ -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
|
/**
|
|
@@ -131,7 +131,8 @@
|
|
|
131
131
|
[request.session launchApplicationWithBundleId:(id)request.arguments[@"bundleId"]
|
|
132
132
|
shouldWaitForQuiescence:request.arguments[@"shouldWaitForQuiescence"]
|
|
133
133
|
arguments:request.arguments[@"arguments"]
|
|
134
|
-
environment:request.arguments[@"environment"]
|
|
134
|
+
environment:request.arguments[@"environment"]
|
|
135
|
+
asApplicationUnderTest:[request.arguments[@"asAppUnderTest"] boolValue]];
|
|
135
136
|
return FBResponseWithOK();
|
|
136
137
|
}
|
|
137
138
|
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
<key>CFBundlePackageType</key>
|
|
16
16
|
<string>FMWK</string>
|
|
17
17
|
<key>CFBundleShortVersionString</key>
|
|
18
|
-
<string>16.
|
|
18
|
+
<string>16.15.0</string>
|
|
19
19
|
<key>CFBundleSignature</key>
|
|
20
20
|
<string>????</string>
|
|
21
21
|
<key>CFBundleVersion</key>
|
|
22
|
-
<string>16.
|
|
22
|
+
<string>16.15.0</string>
|
|
23
23
|
<key>NSPrincipalClass</key>
|
|
24
24
|
<string/>
|
|
25
25
|
</dict>
|
|
@@ -107,6 +107,28 @@ extern NSString* const FBSessionWasKilledNotification;
|
|
|
107
107
|
arguments:(nullable NSArray<NSString *> *)arguments
|
|
108
108
|
environment:(nullable NSDictionary <NSString *, NSString *> *)environment;
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
Launch an application with given bundle identifier in scope of current session
|
|
112
|
+
and optionally make it the application under test of this session.
|
|
113
|
+
|
|
114
|
+
@param bundleIdentifier Valid bundle identifier of the application to be launched
|
|
115
|
+
@param shouldWaitForQuiescence whether to wait for quiescence on application startup.
|
|
116
|
+
If not provided then the setting of the current application under test is inherited.
|
|
117
|
+
If there is no application under test yet then quiescence is only awaited if asApplicationUnderTest is YES
|
|
118
|
+
@param arguments The optional array of application command line arguments. The arguments are going to be applied if the application was not running before.
|
|
119
|
+
@param environment The optional dictionary of environment variables for the application, which is going to be executed. The environment variables are going to be applied if the application was not running before.
|
|
120
|
+
@param asApplicationUnderTest If YES then the launched application replaces the current application under test
|
|
121
|
+
of the session (if any), the same way as if it was launched on session startup: other applications launched later
|
|
122
|
+
inherit its quiescence setting, system alerts over it are respected, its unexpected termination is reported as a crash
|
|
123
|
+
and it gets terminated on session deletion if shouldTerminateApp is enabled
|
|
124
|
+
@return The application instance
|
|
125
|
+
*/
|
|
126
|
+
- (XCUIApplication *)launchApplicationWithBundleId:(NSString *)bundleIdentifier
|
|
127
|
+
shouldWaitForQuiescence:(nullable NSNumber *)shouldWaitForQuiescence
|
|
128
|
+
arguments:(nullable NSArray<NSString *> *)arguments
|
|
129
|
+
environment:(nullable NSDictionary <NSString *, NSString *> *)environment
|
|
130
|
+
asApplicationUnderTest:(BOOL)asApplicationUnderTest;
|
|
131
|
+
|
|
110
132
|
/**
|
|
111
133
|
Activate an application with given bundle identifier in scope of current session.
|
|
112
134
|
!This method is only available since Xcode9 SDK
|
|
@@ -43,7 +43,8 @@ static const NSTimeInterval FB_KILL_WAIT_TIMEOUT_SEC = 35.;
|
|
|
43
43
|
NSString *const FBSessionWasKilledNotification = @"FBSessionWasKilledNotification";
|
|
44
44
|
|
|
45
45
|
@interface FBSession ()
|
|
46
|
-
|
|
46
|
+
// Atomic, since /wda/apps/launch may replace it while a concurrent DELETE /session reads it in -kill
|
|
47
|
+
@property (nullable, atomic) XCUIApplication *testedApplication;
|
|
47
48
|
@property (nonatomic) BOOL isTestedApplicationExpectedToRun;
|
|
48
49
|
@property (nonatomic) BOOL shouldAppsWaitForQuiescence;
|
|
49
50
|
@property (nonatomic, nullable) FBAlertsMonitor *alertsMonitor;
|
|
@@ -386,11 +387,27 @@ static NSUInteger _committedTerminationCount = 0;
|
|
|
386
387
|
shouldWaitForQuiescence:(nullable NSNumber *)shouldWaitForQuiescence
|
|
387
388
|
arguments:(nullable NSArray<NSString *> *)arguments
|
|
388
389
|
environment:(nullable NSDictionary <NSString *, NSString *> *)environment
|
|
390
|
+
{
|
|
391
|
+
return [self launchApplicationWithBundleId:bundleIdentifier
|
|
392
|
+
shouldWaitForQuiescence:shouldWaitForQuiescence
|
|
393
|
+
arguments:arguments
|
|
394
|
+
environment:environment
|
|
395
|
+
asApplicationUnderTest:NO];
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
- (XCUIApplication *)launchApplicationWithBundleId:(NSString *)bundleIdentifier
|
|
399
|
+
shouldWaitForQuiescence:(nullable NSNumber *)shouldWaitForQuiescence
|
|
400
|
+
arguments:(nullable NSArray<NSString *> *)arguments
|
|
401
|
+
environment:(nullable NSDictionary <NSString *, NSString *> *)environment
|
|
402
|
+
asApplicationUnderTest:(BOOL)asApplicationUnderTest
|
|
389
403
|
{
|
|
390
404
|
XCUIApplication *app = [self makeApplicationWithBundleId:bundleIdentifier];
|
|
391
405
|
if (nil == shouldWaitForQuiescence) {
|
|
392
|
-
// Iherit the quiescence check setting from the main app under test by default
|
|
393
|
-
app
|
|
406
|
+
// Iherit the quiescence check setting from the main app under test by default.
|
|
407
|
+
// The first app under test of the session waits for quiescence, as it would on session startup
|
|
408
|
+
app.fb_shouldWaitForQuiescence = nil == self.testedApplication
|
|
409
|
+
? asApplicationUnderTest
|
|
410
|
+
: self.shouldAppsWaitForQuiescence;
|
|
394
411
|
} else {
|
|
395
412
|
app.fb_shouldWaitForQuiescence = [shouldWaitForQuiescence boolValue];
|
|
396
413
|
}
|
|
@@ -401,6 +418,11 @@ static NSUInteger _committedTerminationCount = 0;
|
|
|
401
418
|
} else {
|
|
402
419
|
[app activate];
|
|
403
420
|
}
|
|
421
|
+
if (asApplicationUnderTest) {
|
|
422
|
+
[FBLogger logFmt:@"The application '%@' is set as the application under test", bundleIdentifier];
|
|
423
|
+
self.testedApplication = app;
|
|
424
|
+
self.shouldAppsWaitForQuiescence = app.fb_shouldWaitForQuiescence;
|
|
425
|
+
}
|
|
404
426
|
if ([app fb_isSameAppAs:self.testedApplication]) {
|
|
405
427
|
self.isTestedApplicationExpectedToRun = YES;
|
|
406
428
|
}
|
|
@@ -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
|