appium-webdriveragent 16.13.6 → 16.14.1
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 +8 -0
- package/WebDriverAgent.xcodeproj/project.pbxproj +16 -0
- package/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.m +8 -0
- package/WebDriverAgentLib/Categories/XCUIDevice+FBHinge.h +41 -0
- package/WebDriverAgentLib/Categories/XCUIDevice+FBHinge.m +215 -0
- package/WebDriverAgentLib/Commands/FBCustomCommands.m +71 -10
- package/WebDriverAgentLib/Info.plist +2 -2
- package/WebDriverAgentLib/Routing/FBScreenRecordingRequest.m +7 -2
- package/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.h +2 -2
- package/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.m +20 -0
- package/WebDriverAgentLib/Utilities/FBConfiguration.h +1 -1
- package/WebDriverAgentLib/Utilities/FBMjpegServer.m +3 -3
- package/WebDriverAgentLib/Utilities/FBScreen.m +53 -10
- package/WebDriverAgentLib/WebDriverAgentLib.h +1 -0
- package/WebDriverAgentTests/IntegrationApp/Classes/ViewController.m +15 -0
- package/WebDriverAgentTests/IntegrationTests/FBConfigurationTests.m +52 -0
- package/WebDriverAgentTests/IntegrationTests/FBScreenTests.m +322 -3
- package/WebDriverAgentTests/UnitTests/FBCustomCommandsTests.m +61 -0
- package/package.json +1 -1
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
#import "FBConfiguration.h"
|
|
17
17
|
#import "FBLogger.h"
|
|
18
18
|
#import "FBScreenshot.h"
|
|
19
|
+
#import "FBScreen.h"
|
|
19
20
|
#import "FBImageProcessor.h"
|
|
20
21
|
#import "FBImageUtils.h"
|
|
21
22
|
#import "XCUIScreen.h"
|
|
@@ -42,7 +43,6 @@ static NSUInteger FBNormalizedMjpegFramerate(NSUInteger framerate)
|
|
|
42
43
|
@property (nonatomic, readonly) dispatch_queue_t backgroundQueue;
|
|
43
44
|
@property (nonatomic, readonly) NSMutableArray<nw_connection_t> *listeningClients;
|
|
44
45
|
@property (nonatomic, readonly) FBImageProcessor *imageProcessor;
|
|
45
|
-
@property (nonatomic, readonly) long long mainScreenID;
|
|
46
46
|
@property (nonatomic, assign) NSUInteger consecutiveScreenshotFailures;
|
|
47
47
|
@property (atomic, assign) BOOL isStreaming;
|
|
48
48
|
@property (nonatomic, assign) NSUInteger sentFramesCount;
|
|
@@ -67,7 +67,6 @@ static NSUInteger FBNormalizedMjpegFramerate(NSUInteger framerate)
|
|
|
67
67
|
_pendingFrameCounts = [NSMapTable mapTableWithKeyOptions:(NSPointerFunctionsOptions)(NSMapTableObjectPointerPersonality | NSMapTableStrongMemory)
|
|
68
68
|
valueOptions:(NSPointerFunctionsOptions)NSMapTableStrongMemory];
|
|
69
69
|
_imageProcessor = [[FBImageProcessor alloc] init];
|
|
70
|
-
_mainScreenID = [XCUIScreen.mainScreen displayID];
|
|
71
70
|
dispatch_queue_attr_t queueAttributes = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, 0);
|
|
72
71
|
_backgroundQueue = dispatch_queue_create(QUEUE_NAME, queueAttributes);
|
|
73
72
|
__weak typeof(self) weakSelf = self;
|
|
@@ -116,7 +115,8 @@ static NSUInteger FBNormalizedMjpegFramerate(NSUInteger framerate)
|
|
|
116
115
|
NSError *error;
|
|
117
116
|
CGFloat compressionQuality = MAX(FBMinCompressionQuality,
|
|
118
117
|
MIN(FBMaxCompressionQuality, (double)FBConfiguration.sharedInstance.mjpegServerScreenshotQuality / 100.0));
|
|
119
|
-
|
|
118
|
+
XCUIScreen *screen = [FBScreen currentScreenWithError:&error];
|
|
119
|
+
NSData *screenshotData = nil == screen ? nil : [FBScreenshot takeInOriginalResolutionWithScreenID:screen.displayID
|
|
120
120
|
compressionQuality:compressionQuality
|
|
121
121
|
uti:UTTypeJPEG
|
|
122
122
|
timeout:FRAME_TIMEOUT
|
|
@@ -13,12 +13,26 @@
|
|
|
13
13
|
#import "FBXCodeCompatibility.h"
|
|
14
14
|
#import "XCUIDevice.h"
|
|
15
15
|
#import "XCUIScreen.h"
|
|
16
|
+
#import <objc/message.h>
|
|
17
|
+
|
|
18
|
+
static NSArray<XCUIScreen *> *FBScreensForDevice(id device, NSError **error)
|
|
19
|
+
{
|
|
20
|
+
SEL selector = NSSelectorFromString(@"screensOrError:");
|
|
21
|
+
if ([device respondsToSelector:selector]) {
|
|
22
|
+
return ((NSArray<XCUIScreen *> *(*)(id, SEL, NSError **))objc_msgSend)(device, selector, error);
|
|
23
|
+
}
|
|
24
|
+
// Older XCTest versions expose screens on XCUIScreen instead of XCUIDevice.
|
|
25
|
+
if ([XCUIScreen respondsToSelector:NSSelectorFromString(@"screens")]) {
|
|
26
|
+
return XCUIScreen.screens;
|
|
27
|
+
}
|
|
28
|
+
return @[XCUIScreen.mainScreen];
|
|
29
|
+
}
|
|
16
30
|
|
|
17
31
|
@implementation FBScreen
|
|
18
32
|
|
|
19
33
|
+ (nullable NSArray<NSDictionary<NSString *, id> *> *)screensWithError:(NSError **)error
|
|
20
34
|
{
|
|
21
|
-
NSArray<XCUIScreen *> *screens =
|
|
35
|
+
NSArray<XCUIScreen *> *screens = FBScreensForDevice(XCUIDevice.sharedDevice, error);
|
|
22
36
|
if (nil == screens) {
|
|
23
37
|
return nil;
|
|
24
38
|
}
|
|
@@ -42,27 +56,56 @@
|
|
|
42
56
|
return result.copy;
|
|
43
57
|
}
|
|
44
58
|
|
|
45
|
-
|
|
59
|
+
// Kept separate from sharedDevice so legacy lookup behavior can be tested.
|
|
60
|
+
+ (nullable XCUIScreen *)screenWithDisplayID:(long long)displayID
|
|
61
|
+
device:(XCUIDevice *)device
|
|
62
|
+
error:(NSError **)error
|
|
46
63
|
{
|
|
47
|
-
|
|
64
|
+
SEL lookupSelector = NSSelectorFromString(@"screenWithDisplayID:orError:");
|
|
65
|
+
BOOL hasNativeLookup = [device respondsToSelector:lookupSelector];
|
|
66
|
+
BOOL needsEnumeratedLookup = !hasNativeLookup;
|
|
67
|
+
NSError *lookupError = nil;
|
|
68
|
+
if (hasNativeLookup) {
|
|
69
|
+
XCUIScreen *screen = ((XCUIScreen *(*)(id, SEL, long long, NSError **))objc_msgSend)(
|
|
70
|
+
device, lookupSelector, displayID, &lookupError);
|
|
71
|
+
if ([screen respondsToSelector:NSSelectorFromString(@"bounds")] && !CGRectIsEmpty(screen.bounds)) {
|
|
72
|
+
return screen;
|
|
73
|
+
}
|
|
74
|
+
// XCTest may return a zero-sized placeholder for an inactive wireless
|
|
75
|
+
// display which screensOrError: omits. Confirm its availability by listing.
|
|
76
|
+
needsEnumeratedLookup = nil != screen;
|
|
77
|
+
}
|
|
78
|
+
// Enumerate only for legacy/placeholder lookups or to enrich a lookup error.
|
|
79
|
+
NSError *enumerationError = nil;
|
|
80
|
+
NSArray<XCUIScreen *> *screens = FBScreensForDevice(device, &enumerationError);
|
|
48
81
|
if (nil == screens) {
|
|
82
|
+
if (NULL != error) {
|
|
83
|
+
*error = lookupError ?: enumerationError;
|
|
84
|
+
}
|
|
49
85
|
return nil;
|
|
50
86
|
}
|
|
87
|
+
NSMutableArray<NSNumber *> *availableIDs = [NSMutableArray arrayWithCapacity:screens.count];
|
|
51
88
|
for (XCUIScreen *screen in screens) {
|
|
52
|
-
if (screen.displayID == displayID) {
|
|
89
|
+
if (needsEnumeratedLookup && screen.displayID == displayID) {
|
|
53
90
|
return screen;
|
|
54
91
|
}
|
|
55
|
-
}
|
|
56
|
-
NSMutableArray<NSNumber *> *availableIDs = [NSMutableArray arrayWithCapacity:screens.count];
|
|
57
|
-
for (XCUIScreen *screen in screens) {
|
|
58
92
|
[availableIDs addObject:@(screen.displayID)];
|
|
59
93
|
}
|
|
60
|
-
[
|
|
61
|
-
|
|
62
|
-
|
|
94
|
+
FBErrorBuilder *builder = [FBErrorBuilder.builder withDescriptionFormat:
|
|
95
|
+
@"No display with id %lld is available. Available display ids: [%@]",
|
|
96
|
+
displayID, [availableIDs componentsJoinedByString:@", "]];
|
|
97
|
+
if (nil != lookupError) {
|
|
98
|
+
[builder withInnerError:lookupError];
|
|
99
|
+
}
|
|
100
|
+
[builder buildError:error];
|
|
63
101
|
return nil;
|
|
64
102
|
}
|
|
65
103
|
|
|
104
|
+
+ (nullable XCUIScreen *)screenWithDisplayID:(long long)displayID error:(NSError **)error
|
|
105
|
+
{
|
|
106
|
+
return [self screenWithDisplayID:displayID device:XCUIDevice.sharedDevice error:error];
|
|
107
|
+
}
|
|
108
|
+
|
|
66
109
|
+ (nullable XCUIScreen *)currentScreenWithError:(NSError **)error
|
|
67
110
|
{
|
|
68
111
|
NSNumber *displayID = FBConfiguration.sharedInstance.currentDisplayId;
|
|
@@ -47,6 +47,7 @@ FOUNDATION_EXPORT const unsigned char WebDriverAgentLib_VersionString[];
|
|
|
47
47
|
#import <WebDriverAgentLib/XCTIssue+FBPatcher.h>
|
|
48
48
|
#import <WebDriverAgentLib/XCUIApplication+FBHelpers.h>
|
|
49
49
|
#import <WebDriverAgentLib/XCUIDevice+FBHelpers.h>
|
|
50
|
+
#import <WebDriverAgentLib/XCUIDevice+FBHinge.h>
|
|
50
51
|
#import <WebDriverAgentLib/XCUIDevice+FBRotation.h>
|
|
51
52
|
#import <WebDriverAgentLib/XCUIDevice+FBVoiceOver.h>
|
|
52
53
|
#import <WebDriverAgentLib/XCUIElement.h>
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
#import "ViewController.h"
|
|
10
10
|
#import "FBCoordinateProbeViewController.h"
|
|
11
11
|
|
|
12
|
+
#if !TARGET_OS_TV
|
|
13
|
+
#import <WebKit/WebKit.h>
|
|
14
|
+
#endif
|
|
15
|
+
|
|
12
16
|
@interface ViewController ()
|
|
13
17
|
@property (weak, nonatomic) IBOutlet UILabel *orentationLabel;
|
|
14
18
|
@property (weak, nonatomic) IBOutlet UIButton *button;
|
|
@@ -19,6 +23,17 @@
|
|
|
19
23
|
- (void)viewDidLoad
|
|
20
24
|
{
|
|
21
25
|
[super viewDidLoad];
|
|
26
|
+
|
|
27
|
+
#if !TARGET_OS_TV
|
|
28
|
+
if ([NSProcessInfo.processInfo.arguments containsObject:@"--webview-fixture"]) {
|
|
29
|
+
WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds];
|
|
30
|
+
webView.autoresizingMask = UIViewAutoresizingFlexibleWidth
|
|
31
|
+
| UIViewAutoresizingFlexibleHeight;
|
|
32
|
+
[self.view addSubview:webView];
|
|
33
|
+
[webView loadHTMLString:@"<!doctype html><html><body>"
|
|
34
|
+
"<p>WebContent snapshot fixture</p></body></html>" baseURL:nil];
|
|
35
|
+
}
|
|
36
|
+
#endif
|
|
22
37
|
|
|
23
38
|
UIAccessibilityCustomAction *action1 =
|
|
24
39
|
[[UIAccessibilityCustomAction alloc] initWithName:@"Custom Action 1"
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
#import "FBConfiguration.h"
|
|
13
13
|
#import "FBRuntimeUtils.h"
|
|
14
14
|
#import "FBTestMacros.h"
|
|
15
|
+
#import "FBXCAccessibilityElement.h"
|
|
16
|
+
#import "FBXCAXClientProxy.h"
|
|
17
|
+
#import "XCUIApplication.h"
|
|
15
18
|
#import "XCUIElement.h"
|
|
16
19
|
#import "XCUIElement+FBIsVisible.h"
|
|
17
20
|
|
|
@@ -68,4 +71,53 @@
|
|
|
68
71
|
}
|
|
69
72
|
}
|
|
70
73
|
|
|
74
|
+
- (void)testAccessibilityDeadlineAllowsResponsiveNativeAppSnapshots
|
|
75
|
+
{
|
|
76
|
+
[self launchApplication];
|
|
77
|
+
NSTimeInterval previousDeadline = FBConfiguration.sharedInstance.accessibilityDeadline;
|
|
78
|
+
@try {
|
|
79
|
+
FBConfiguration.sharedInstance.accessibilityDeadline = 3;
|
|
80
|
+
NSError *error = nil;
|
|
81
|
+
XCTAssertNotNil([self.testedApplication snapshotWithError:&error]);
|
|
82
|
+
XCTAssertNil(error);
|
|
83
|
+
} @finally {
|
|
84
|
+
FBConfiguration.sharedInstance.accessibilityDeadline = previousDeadline;
|
|
85
|
+
[self.testedApplication terminate];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
- (void)testAccessibilityDeadlineAllowsWebContentSnapshots
|
|
90
|
+
{
|
|
91
|
+
NSTimeInterval previousDeadline = FBConfiguration.sharedInstance.accessibilityDeadline;
|
|
92
|
+
@try {
|
|
93
|
+
// Resolve the remote AX element before enabling the guard so the test
|
|
94
|
+
// exercises a WebContent-owned snapshot, not just the host app's tree.
|
|
95
|
+
FBConfiguration.sharedInstance.accessibilityDeadline = 0;
|
|
96
|
+
self.testedApplication.launchArguments = @[@"--webview-fixture"];
|
|
97
|
+
[self.testedApplication launch];
|
|
98
|
+
XCUIElement *text = self.testedApplication.webViews.staticTexts[
|
|
99
|
+
@"WebContent snapshot fixture"];
|
|
100
|
+
XCTAssertTrue([text waitForExistenceWithTimeout:15]);
|
|
101
|
+
NSError *error = nil;
|
|
102
|
+
id<FBXCElementSnapshot> initial = [text snapshotWithError:&error];
|
|
103
|
+
XCTAssertNotNil(initial);
|
|
104
|
+
XCTAssertNil(error);
|
|
105
|
+
id<FBXCAccessibilityElement> element = initial.accessibilityElement;
|
|
106
|
+
XCTAssertNotEqual(element.processIdentifier, self.testedApplication.processID);
|
|
107
|
+
|
|
108
|
+
FBConfiguration.sharedInstance.accessibilityDeadline = 3;
|
|
109
|
+
for (NSUInteger i = 0; i < 3; i++) {
|
|
110
|
+
error = nil;
|
|
111
|
+
id<FBXCElementSnapshot> snapshot = [FBXCAXClientProxy.sharedClient
|
|
112
|
+
snapshotForElement:element attributes:@[] inDepth:YES error:&error];
|
|
113
|
+
XCTAssertNotNil(snapshot);
|
|
114
|
+
XCTAssertNil(error);
|
|
115
|
+
}
|
|
116
|
+
XCTAssertTrue(text.exists);
|
|
117
|
+
} @finally {
|
|
118
|
+
FBConfiguration.sharedInstance.accessibilityDeadline = previousDeadline;
|
|
119
|
+
[self.testedApplication terminate];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
71
123
|
@end
|
|
@@ -11,8 +11,69 @@
|
|
|
11
11
|
#import "FBIntegrationTestCase.h"
|
|
12
12
|
#import "FBConfiguration.h"
|
|
13
13
|
#import "FBScreen.h"
|
|
14
|
+
#import "FBRunLoopSpinner.h"
|
|
15
|
+
#import "XCUIDevice+FBRotation.h"
|
|
16
|
+
#import "XCUIDevice+FBHinge.h"
|
|
17
|
+
#import "XCUIElement.h"
|
|
18
|
+
#import "FBActiveAppDetectionPoint.h"
|
|
19
|
+
#import "FBExceptions.h"
|
|
20
|
+
#import "FBScreenRecordingRequest.h"
|
|
14
21
|
#import "FBScreenshot.h"
|
|
15
22
|
#import "XCUIScreen.h"
|
|
23
|
+
#import "FBCustomCommands.h"
|
|
24
|
+
#import "FBRouteRequest.h"
|
|
25
|
+
#import "FBResponsePayload.h"
|
|
26
|
+
#import "RouteResponse.h"
|
|
27
|
+
|
|
28
|
+
@interface FBCustomCommands (FBScreenInfoTesting)
|
|
29
|
+
+ (id<FBResponsePayload>)handleGetScreen:(FBRouteRequest *)request;
|
|
30
|
+
@end
|
|
31
|
+
|
|
32
|
+
@interface FBScreen (FBLookupTesting)
|
|
33
|
+
+ (nullable XCUIScreen *)screenWithDisplayID:(long long)displayID device:(id)device error:(NSError **)error;
|
|
34
|
+
@end
|
|
35
|
+
|
|
36
|
+
@interface FBEmptyScreenDouble : NSObject
|
|
37
|
+
@property (nonatomic) CGRect bounds;
|
|
38
|
+
@end
|
|
39
|
+
@implementation FBEmptyScreenDouble
|
|
40
|
+
@end
|
|
41
|
+
|
|
42
|
+
@interface FBScreenDeviceDouble : NSObject
|
|
43
|
+
@property (nonatomic) BOOL nativeLookupAvailable;
|
|
44
|
+
@property (nonatomic) BOOL enumerationUnavailable;
|
|
45
|
+
@property (nonatomic) NSUInteger enumerationCount;
|
|
46
|
+
@property (nonatomic, strong) XCUIScreen *nativeScreen;
|
|
47
|
+
@property (nonatomic, strong) NSArray<XCUIScreen *> *screens;
|
|
48
|
+
@property (nonatomic, strong) NSError *lookupError;
|
|
49
|
+
@property (nonatomic, strong) NSError *enumerationError;
|
|
50
|
+
@end
|
|
51
|
+
|
|
52
|
+
@implementation FBScreenDeviceDouble
|
|
53
|
+
- (BOOL)respondsToSelector:(SEL)selector
|
|
54
|
+
{
|
|
55
|
+
if (selector == NSSelectorFromString(@"screensOrError:") && self.enumerationUnavailable) {
|
|
56
|
+
return NO;
|
|
57
|
+
}
|
|
58
|
+
return selector == @selector(screenWithDisplayID:orError:)
|
|
59
|
+
? self.nativeLookupAvailable : [super respondsToSelector:selector];
|
|
60
|
+
}
|
|
61
|
+
- (XCUIScreen *)screenWithDisplayID:(long long)displayID orError:(NSError **)error
|
|
62
|
+
{
|
|
63
|
+
if (NULL != error) {
|
|
64
|
+
*error = self.lookupError;
|
|
65
|
+
}
|
|
66
|
+
return self.nativeScreen;
|
|
67
|
+
}
|
|
68
|
+
- (NSArray<XCUIScreen *> *)screensOrError:(NSError **)error
|
|
69
|
+
{
|
|
70
|
+
self.enumerationCount++;
|
|
71
|
+
if (NULL != error) {
|
|
72
|
+
*error = self.enumerationError;
|
|
73
|
+
}
|
|
74
|
+
return self.screens;
|
|
75
|
+
}
|
|
76
|
+
@end
|
|
16
77
|
|
|
17
78
|
@interface FBScreenTests : FBIntegrationTestCase
|
|
18
79
|
@end
|
|
@@ -62,9 +123,13 @@
|
|
|
62
123
|
- (void)testScreenWithDisplayID
|
|
63
124
|
{
|
|
64
125
|
NSError *error = nil;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
126
|
+
for (NSDictionary<NSString *, id> *entry in [FBScreen screensWithError:&error]) {
|
|
127
|
+
long long displayID = [entry[@"displayId"] longLongValue];
|
|
128
|
+
XCUIScreen *screen = [FBScreen screenWithDisplayID:displayID error:&error];
|
|
129
|
+
XCTAssertNotNil(screen);
|
|
130
|
+
XCTAssertNil(error);
|
|
131
|
+
XCTAssertEqual(screen.displayID, displayID);
|
|
132
|
+
}
|
|
68
133
|
|
|
69
134
|
XCTAssertNil([FBScreen screenWithDisplayID:[self unknownDisplayID] error:&error]);
|
|
70
135
|
XCTAssertNotNil(error);
|
|
@@ -77,6 +142,84 @@
|
|
|
77
142
|
XCTAssertTrue([error.localizedDescription containsString:availableDisplays]);
|
|
78
143
|
}
|
|
79
144
|
|
|
145
|
+
- (void)testNativeScreenLookupDoesNotEnumerateOnSuccess
|
|
146
|
+
{
|
|
147
|
+
FBScreenDeviceDouble *device = [FBScreenDeviceDouble new];
|
|
148
|
+
device.nativeLookupAvailable = YES;
|
|
149
|
+
device.nativeScreen = XCUIScreen.mainScreen;
|
|
150
|
+
NSError *error = nil;
|
|
151
|
+
XCTAssertEqualObjects([FBScreen screenWithDisplayID:device.nativeScreen.displayID device:device error:&error],
|
|
152
|
+
device.nativeScreen);
|
|
153
|
+
XCTAssertNil(error);
|
|
154
|
+
XCTAssertEqual(device.enumerationCount, 0UL);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
- (void)testNativeScreenPlaceholderMustBeAvailable
|
|
158
|
+
{
|
|
159
|
+
FBScreenDeviceDouble *device = [FBScreenDeviceDouble new];
|
|
160
|
+
device.nativeLookupAvailable = YES;
|
|
161
|
+
device.nativeScreen = (id)[FBEmptyScreenDouble new];
|
|
162
|
+
device.screens = @[XCUIScreen.mainScreen];
|
|
163
|
+
NSError *error = nil;
|
|
164
|
+
XCTAssertNil([FBScreen screenWithDisplayID:-1 device:device error:&error]);
|
|
165
|
+
XCTAssertNotNil(error);
|
|
166
|
+
XCTAssertEqual(device.enumerationCount, 1UL);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
- (void)testNativeScreenLookupFailureIncludesAvailableIDs
|
|
170
|
+
{
|
|
171
|
+
FBScreenDeviceDouble *device = [FBScreenDeviceDouble new];
|
|
172
|
+
device.nativeLookupAvailable = YES;
|
|
173
|
+
device.screens = @[XCUIScreen.mainScreen];
|
|
174
|
+
device.lookupError = [NSError errorWithDomain:@"NativeLookup" code:1 userInfo:nil];
|
|
175
|
+
NSError *error = nil;
|
|
176
|
+
XCTAssertNil([FBScreen screenWithDisplayID:-1 device:device error:&error]);
|
|
177
|
+
XCTAssertEqual(device.enumerationCount, 1UL);
|
|
178
|
+
XCTAssertTrue([error.localizedDescription containsString:@"Available display ids"]);
|
|
179
|
+
XCTAssertEqualObjects(error.userInfo[NSUnderlyingErrorKey], device.lookupError);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
- (void)testNativeScreenLookupErrorSurvivesEnumerationFailure
|
|
183
|
+
{
|
|
184
|
+
FBScreenDeviceDouble *device = [FBScreenDeviceDouble new];
|
|
185
|
+
device.nativeLookupAvailable = YES;
|
|
186
|
+
device.lookupError = [NSError errorWithDomain:@"NativeLookup" code:1 userInfo:nil];
|
|
187
|
+
device.enumerationError = [NSError errorWithDomain:@"Enumeration" code:2 userInfo:nil];
|
|
188
|
+
NSError *error = nil;
|
|
189
|
+
XCTAssertNil([FBScreen screenWithDisplayID:-1 device:device error:&error]);
|
|
190
|
+
XCTAssertEqualObjects(error, device.lookupError);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
- (void)testScreenLookupWithoutNativeSelector
|
|
194
|
+
{
|
|
195
|
+
FBScreenDeviceDouble *device = [FBScreenDeviceDouble new];
|
|
196
|
+
device.screens = @[XCUIScreen.mainScreen];
|
|
197
|
+
NSError *error = nil;
|
|
198
|
+
XCTAssertEqualObjects([FBScreen screenWithDisplayID:XCUIScreen.mainScreen.displayID device:device error:&error],
|
|
199
|
+
XCUIScreen.mainScreen);
|
|
200
|
+
XCTAssertNil(error);
|
|
201
|
+
XCTAssertEqual(device.enumerationCount, 1UL);
|
|
202
|
+
XCTAssertNil([FBScreen screenWithDisplayID:-1 device:device error:&error]);
|
|
203
|
+
XCTAssertTrue([error.localizedDescription containsString:@"Available display ids"]);
|
|
204
|
+
device.screens = nil;
|
|
205
|
+
device.enumerationError = [NSError errorWithDomain:@"Enumeration" code:2 userInfo:nil];
|
|
206
|
+
XCTAssertNil([FBScreen screenWithDisplayID:-1 device:device error:&error]);
|
|
207
|
+
XCTAssertEqualObjects(error, device.enumerationError);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
- (void)testScreenLookupWithoutDeviceScreenSelectors
|
|
211
|
+
{
|
|
212
|
+
FBScreenDeviceDouble *device = [FBScreenDeviceDouble new];
|
|
213
|
+
device.enumerationUnavailable = YES;
|
|
214
|
+
NSError *error = nil;
|
|
215
|
+
XCTAssertEqualObjects([FBScreen screenWithDisplayID:XCUIScreen.mainScreen.displayID device:device error:&error],
|
|
216
|
+
XCUIScreen.mainScreen);
|
|
217
|
+
XCTAssertNil(error);
|
|
218
|
+
XCTAssertNil([FBScreen screenWithDisplayID:-1 device:device error:&error]);
|
|
219
|
+
XCTAssertNotNil(error);
|
|
220
|
+
XCTAssertEqual(device.enumerationCount, 0UL);
|
|
221
|
+
}
|
|
222
|
+
|
|
80
223
|
- (void)testCurrentScreenDefaultsToMainScreen
|
|
81
224
|
{
|
|
82
225
|
NSError *error = nil;
|
|
@@ -108,6 +251,52 @@
|
|
|
108
251
|
XCTAssertNil(FBConfiguration.sharedInstance.currentDisplayId);
|
|
109
252
|
}
|
|
110
253
|
|
|
254
|
+
- (void)testSecondaryDisplayDoesNotHitTestMainDisplay
|
|
255
|
+
{
|
|
256
|
+
NSArray<NSDictionary<NSString *, id> *> *screens = [FBScreen screensWithError:nil];
|
|
257
|
+
BOOL testedSecondaryDisplay = NO;
|
|
258
|
+
for (NSDictionary<NSString *, id> *screen in screens) {
|
|
259
|
+
if ([screen[@"isMain"] boolValue]) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
testedSecondaryDisplay = YES;
|
|
263
|
+
FBConfiguration.sharedInstance.currentDisplayId = screen[@"displayId"];
|
|
264
|
+
XCTAssertNil(FBActiveAppDetectionPoint.sharedInstance.axElement);
|
|
265
|
+
}
|
|
266
|
+
XCTSkipIf(!testedSecondaryDisplay, @"Requires a secondary display");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
- (void)testActiveAppDetectionRejectsUnavailableDisplay
|
|
270
|
+
{
|
|
271
|
+
FBConfiguration.sharedInstance.currentDisplayId = @([self unknownDisplayID]);
|
|
272
|
+
XCTAssertThrowsSpecificNamed(FBActiveAppDetectionPoint.sharedInstance.axElement,
|
|
273
|
+
NSException, FBInvalidArgumentException);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
- (void)testRecordingUsesSelectedDisplay
|
|
277
|
+
{
|
|
278
|
+
XCTSkipIf(nil == NSClassFromString(@"XCTScreenRecordingRequest"), @"Screen recording is unavailable");
|
|
279
|
+
FBScreenRecordingRequest *request = [[FBScreenRecordingRequest alloc] initWithFps:10 codec:0];
|
|
280
|
+
NSError *error = nil;
|
|
281
|
+
id defaultRequest = [request toNativeRequestWithError:&error];
|
|
282
|
+
XCTAssertNotNil(defaultRequest);
|
|
283
|
+
XCTAssertNil(error);
|
|
284
|
+
XCTAssertEqualObjects([defaultRequest valueForKey:@"screenID"], @([FBScreen displayID]));
|
|
285
|
+
|
|
286
|
+
for (NSDictionary<NSString *, id> *screen in [FBScreen screensWithError:nil]) {
|
|
287
|
+
FBConfiguration.sharedInstance.currentDisplayId = screen[@"displayId"];
|
|
288
|
+
id nativeRequest = [request toNativeRequestWithError:&error];
|
|
289
|
+
XCTAssertNotNil(nativeRequest);
|
|
290
|
+
XCTAssertNil(error);
|
|
291
|
+
XCTAssertEqualObjects([nativeRequest valueForKey:@"screenID"], screen[@"displayId"]);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
FBConfiguration.sharedInstance.currentDisplayId = @([self unknownDisplayID]);
|
|
295
|
+
XCTAssertNil([request toNativeRequestWithError:&error]);
|
|
296
|
+
XCTAssertNotNil(error);
|
|
297
|
+
XCTAssertTrue([error.localizedDescription containsString:@"Available display ids"]);
|
|
298
|
+
}
|
|
299
|
+
|
|
111
300
|
- (long long)unknownDisplayID
|
|
112
301
|
{
|
|
113
302
|
long long maxID = 0;
|
|
@@ -122,4 +311,134 @@
|
|
|
122
311
|
XCTAssertTrue([FBScreen scale] >= 2);
|
|
123
312
|
}
|
|
124
313
|
|
|
314
|
+
- (NSDictionary *)screenInfoResponse
|
|
315
|
+
{
|
|
316
|
+
FBRouteRequest *request = [FBRouteRequest routeRequestWithURL:[NSURL URLWithString:@"http://localhost:8100/wda/screen"]
|
|
317
|
+
parameters:@{}
|
|
318
|
+
arguments:@{}];
|
|
319
|
+
RouteResponse *response = [RouteResponse new];
|
|
320
|
+
[[FBCustomCommands handleGetScreen:request] dispatchWithResponse:response];
|
|
321
|
+
return [NSJSONSerialization JSONObjectWithData:response.responseData options:0 error:nil][@"value"];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
- (void)testScreenInfoFollowsDisplaySelection
|
|
325
|
+
{
|
|
326
|
+
for (NSDictionary *screen in [FBScreen screensWithError:nil]) {
|
|
327
|
+
FBConfiguration.sharedInstance.currentDisplayId = screen[@"displayId"];
|
|
328
|
+
NSDictionary *info = [self screenInfoResponse];
|
|
329
|
+
XCTAssertNil(info[@"error"]);
|
|
330
|
+
XCTAssertEqualObjects(info[@"displayId"], screen[@"displayId"]);
|
|
331
|
+
XCTAssertEqualObjects(info[@"scale"], screen[@"scale"]);
|
|
332
|
+
double width = [info[@"screenSize"][@"width"] doubleValue];
|
|
333
|
+
double height = [info[@"screenSize"][@"height"] doubleValue];
|
|
334
|
+
double scale = [screen[@"scale"] doubleValue];
|
|
335
|
+
double expectedWidth = [screen[@"bounds"][@"width"] doubleValue] / scale;
|
|
336
|
+
double expectedHeight = [screen[@"bounds"][@"height"] doubleValue] / scale;
|
|
337
|
+
XCTAssertEqualWithAccuracy(MIN(width, height), MIN(expectedWidth, expectedHeight), 0.01);
|
|
338
|
+
XCTAssertEqualWithAccuracy(MAX(width, height), MAX(expectedWidth, expectedHeight), 0.01);
|
|
339
|
+
double barHeight = [info[@"statusBarSize"][@"height"] doubleValue];
|
|
340
|
+
XCTAssertGreaterThanOrEqual(barHeight, 0);
|
|
341
|
+
XCTAssertLessThan(barHeight, height);
|
|
342
|
+
if (barHeight > 0) {
|
|
343
|
+
XCTAssertEqualWithAccuracy([info[@"statusBarSize"][@"width"] doubleValue], width, 1);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// tearDown restores the default display even when an assertion fails.
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
- (void)testScreenInfoFollowsDisplayReset
|
|
350
|
+
{
|
|
351
|
+
// Exercise reset independently of the display-selection assertions.
|
|
352
|
+
FBConfiguration.sharedInstance.currentDisplayId = @([self unknownDisplayID]);
|
|
353
|
+
FBConfiguration.sharedInstance.currentDisplayId = nil;
|
|
354
|
+
XCTAssertEqualObjects([self screenInfoResponse][@"displayId"], @([FBScreen displayID]));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
- (void)testScreenInfoRejectsUnavailableDisplay
|
|
358
|
+
{
|
|
359
|
+
FBConfiguration.sharedInstance.currentDisplayId = @([self unknownDisplayID]);
|
|
360
|
+
NSDictionary *info = [self screenInfoResponse];
|
|
361
|
+
XCTAssertEqualObjects(info[@"error"], @"invalid argument");
|
|
362
|
+
XCTAssertTrue([info[@"message"] containsString:@"Available display ids"]);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
- (void)testScreenInfoInBothLandscapeOrientations
|
|
366
|
+
{
|
|
367
|
+
XCTSkipIf(XCUIDevice.sharedDevice.fb_canAttemptSimulatedHingeAngleInjection,
|
|
368
|
+
@"Foldable orientations are covered by testScreenInfoAcrossFoldStates");
|
|
369
|
+
XCUIElement *window = self.testedApplication.windows.firstMatch;
|
|
370
|
+
(void)window.frame;
|
|
371
|
+
XCTSkipIf(window.screen.displayID != XCUIScreen.mainScreen.displayID,
|
|
372
|
+
@"Requires the fixture on the main display");
|
|
373
|
+
[self addTeardownBlock:^{
|
|
374
|
+
[[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:UIDeviceOrientationPortrait];
|
|
375
|
+
}];
|
|
376
|
+
BOOL expectsVisibleBar = UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad;
|
|
377
|
+
for (NSNumber *orientation in @[@(UIDeviceOrientationLandscapeLeft), @(UIDeviceOrientationLandscapeRight)]) {
|
|
378
|
+
XCTAssertTrue([[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:orientation.integerValue]);
|
|
379
|
+
__block NSDictionary *info = nil;
|
|
380
|
+
// iPad keeps its top status bar in landscape. Wait for rotation to finish
|
|
381
|
+
// rather than accepting a transient zero crop as a successful result.
|
|
382
|
+
XCTAssertTrue([[[FBRunLoopSpinner new] timeout:5] spinUntilTrue:^BOOL {
|
|
383
|
+
info = [self screenInfoResponse];
|
|
384
|
+
double width = [info[@"screenSize"][@"width"] doubleValue];
|
|
385
|
+
double height = [info[@"screenSize"][@"height"] doubleValue];
|
|
386
|
+
double barWidth = [info[@"statusBarSize"][@"width"] doubleValue];
|
|
387
|
+
double barHeight = [info[@"statusBarSize"][@"height"] doubleValue];
|
|
388
|
+
if (nil != info[@"error"] || width <= height || height <= 0) {
|
|
389
|
+
return NO;
|
|
390
|
+
}
|
|
391
|
+
if (barHeight == 0) {
|
|
392
|
+
return !expectsVisibleBar && barWidth == 0;
|
|
393
|
+
}
|
|
394
|
+
return barHeight > 0 && barHeight < height && fabs(barWidth - width) <= 1;
|
|
395
|
+
}], @"Unexpected landscape screen metadata: %@", info);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
- (void)testScreenInfoAcrossFoldStates
|
|
400
|
+
{
|
|
401
|
+
XCUIDevice *device = XCUIDevice.sharedDevice;
|
|
402
|
+
XCTSkipIf(!device.fb_canAttemptSimulatedHingeAngleInjection, @"Requires a foldable simulator");
|
|
403
|
+
NSError *error = nil;
|
|
404
|
+
NSNumber *originalAngle = [device fb_getSimulatedHingeAngle:&error];
|
|
405
|
+
XCTAssertNotNil(originalAngle, @"%@", error);
|
|
406
|
+
[self addTeardownBlock:^{
|
|
407
|
+
if (nil != originalAngle) {
|
|
408
|
+
[device fb_setSimulatedHingeAngle:originalAngle.doubleValue error:nil];
|
|
409
|
+
}
|
|
410
|
+
}];
|
|
411
|
+
for (NSNumber *angle in @[@0, @90, @180, @0]) {
|
|
412
|
+
[self.testedApplication terminate];
|
|
413
|
+
XCTAssertTrue([device fb_setSimulatedHingeAngle:angle.doubleValue error:&error], @"%@", error);
|
|
414
|
+
[self launchApplication];
|
|
415
|
+
XCUIElement *window = self.testedApplication.windows.firstMatch;
|
|
416
|
+
CGRect frame = window.frame;
|
|
417
|
+
FBConfiguration.sharedInstance.currentDisplayId = @(window.screen.displayID);
|
|
418
|
+
__block NSDictionary *info = nil;
|
|
419
|
+
XCTAssertTrue([[[FBRunLoopSpinner new] timeout:5] spinUntilTrue:^BOOL {
|
|
420
|
+
info = [self screenInfoResponse];
|
|
421
|
+
double width = [info[@"screenSize"][@"width"] doubleValue];
|
|
422
|
+
double height = [info[@"screenSize"][@"height"] doubleValue];
|
|
423
|
+
double barHeight = [info[@"statusBarSize"][@"height"] doubleValue];
|
|
424
|
+
return nil == info[@"error"] && fabs(width - frame.size.width) <= 1
|
|
425
|
+
&& fabs(height - frame.size.height) <= 1
|
|
426
|
+
&& barHeight >= 0 && barHeight < height
|
|
427
|
+
&& (barHeight == 0 || fabs([info[@"statusBarSize"][@"width"] doubleValue] - width) <= 1)
|
|
428
|
+
&& (angle.doubleValue == 0 || barHeight == 0);
|
|
429
|
+
}], @"Unexpected screen metadata at hinge angle %@: %@", angle, info);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
- (void)testScreenInfoIncludesTopStatusBarOnSingleDisplay
|
|
434
|
+
{
|
|
435
|
+
XCTSkipIf([FBScreen screensWithError:nil].count != 1, @"Requires a single-display device");
|
|
436
|
+
// The integration fixture uses the ordinary visible portrait status bar.
|
|
437
|
+
// An unresolved XCUIElement has displayID == 0; it must not be filtered out.
|
|
438
|
+
NSDictionary *info = [self screenInfoResponse];
|
|
439
|
+
XCTAssertGreaterThan([info[@"statusBarSize"][@"height"] doubleValue], 0);
|
|
440
|
+
XCTAssertEqualWithAccuracy([info[@"statusBarSize"][@"width"] doubleValue],
|
|
441
|
+
[info[@"screenSize"][@"width"] doubleValue], 1);
|
|
442
|
+
}
|
|
443
|
+
|
|
125
444
|
@end
|
|
@@ -12,12 +12,17 @@
|
|
|
12
12
|
#import "FBElementCache.h"
|
|
13
13
|
#import "FBRouteRequest-Private.h"
|
|
14
14
|
#import "FBSession.h"
|
|
15
|
+
#import "FBResponsePayload.h"
|
|
16
|
+
#import "RouteResponse.h"
|
|
17
|
+
#import "XCUIDevice+FBHinge.h"
|
|
15
18
|
#import "Doubles/XCUIElementDouble.h"
|
|
16
19
|
|
|
17
20
|
#if !TARGET_OS_TV && __clang_major__ >= 15
|
|
18
21
|
|
|
19
22
|
@interface FBCustomCommands (FBWDATestable)
|
|
20
23
|
+ (id<FBResponsePayload>)handleKeyboardInput:(FBRouteRequest *)request;
|
|
24
|
+
+ (id<FBResponsePayload>)handleGetSimulatedHingeAngle:(FBRouteRequest *)request;
|
|
25
|
+
+ (id<FBResponsePayload>)handleSetSimulatedHingeAngle:(FBRouteRequest *)request;
|
|
21
26
|
@end
|
|
22
27
|
|
|
23
28
|
@interface FBCustomCommandsTests : XCTestCase
|
|
@@ -78,6 +83,62 @@
|
|
|
78
83
|
XCTAssertEqual(element.lastTypedModifierFlags, 2);
|
|
79
84
|
}
|
|
80
85
|
|
|
86
|
+
- (NSDictionary *)hingeResponseWithArguments:(NSDictionary *)arguments
|
|
87
|
+
{
|
|
88
|
+
FBRouteRequest *request = [FBRouteRequest routeRequestWithURL:[NSURL URLWithString:@"http://localhost:8100/"]
|
|
89
|
+
parameters:@{}
|
|
90
|
+
arguments:arguments];
|
|
91
|
+
RouteResponse *response = [RouteResponse new];
|
|
92
|
+
[[FBCustomCommands handleSetSimulatedHingeAngle:request] dispatchWithResponse:response];
|
|
93
|
+
return [NSJSONSerialization JSONObjectWithData:response.responseData options:0 error:nil];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
- (void)testHingeAngleReading
|
|
97
|
+
{
|
|
98
|
+
NSError *error = nil;
|
|
99
|
+
NSNumber *angle = [XCUIDevice.sharedDevice fb_getSimulatedHingeAngle:&error];
|
|
100
|
+
BOOL available = XCUIDevice.sharedDevice.fb_supportsHingeAngleReading;
|
|
101
|
+
if (available) {
|
|
102
|
+
XCTAssertNotNil(angle);
|
|
103
|
+
XCTAssertNil(error);
|
|
104
|
+
XCTAssertTrue(isfinite(angle.doubleValue));
|
|
105
|
+
} else {
|
|
106
|
+
XCTAssertNil(angle);
|
|
107
|
+
XCTAssertNotNil(error);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
FBRouteRequest *request = [FBRouteRequest routeRequestWithURL:[NSURL URLWithString:@"http://localhost:8100/"]
|
|
111
|
+
parameters:@{}
|
|
112
|
+
arguments:@{}];
|
|
113
|
+
RouteResponse *response = [RouteResponse new];
|
|
114
|
+
[[FBCustomCommands handleGetSimulatedHingeAngle:request] dispatchWithResponse:response];
|
|
115
|
+
NSDictionary *body = [NSJSONSerialization JSONObjectWithData:response.responseData options:0 error:nil];
|
|
116
|
+
if (available) {
|
|
117
|
+
XCTAssertTrue([body[@"value"] isKindOfClass:NSNumber.class]);
|
|
118
|
+
XCTAssertTrue(isfinite([body[@"value"] doubleValue]));
|
|
119
|
+
} else {
|
|
120
|
+
XCTAssertEqualObjects(body[@"value"][@"error"], @"unsupported operation");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
- (void)testHingeAngleRejectsInvalidArguments
|
|
125
|
+
{
|
|
126
|
+
for (id value in @[@(-1), @181, @YES, @"90", NSNull.null, @(NAN), @(INFINITY)]) {
|
|
127
|
+
NSDictionary *response = [self hingeResponseWithArguments:@{@"angle": value}];
|
|
128
|
+
XCTAssertEqualObjects(response[@"value"][@"error"], @"invalid argument");
|
|
129
|
+
}
|
|
130
|
+
XCTAssertEqualObjects([self hingeResponseWithArguments:@{}][@"value"][@"error"], @"invalid argument");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
- (void)testHingeAngleRejectsUnsupportedDevices
|
|
134
|
+
{
|
|
135
|
+
XCTSkipIf(XCUIDevice.sharedDevice.fb_canAttemptSimulatedHingeAngleInjection, @"Requires a device without an available hinge");
|
|
136
|
+
for (NSNumber *angle in @[@0, @90, @180]) {
|
|
137
|
+
XCTAssertEqualObjects([self hingeResponseWithArguments:@{@"angle": angle}][@"value"][@"error"],
|
|
138
|
+
@"unsupported operation");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
81
142
|
@end
|
|
82
143
|
|
|
83
144
|
#endif
|