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.
@@ -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
- NSData *screenshotData = [FBScreenshot takeInOriginalResolutionWithScreenID:self.mainScreenID
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 = [XCUIDevice.sharedDevice screensOrError:error];
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
- + (nullable XCUIScreen *)screenWithDisplayID:(long long)displayID error:(NSError **)error
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
- NSArray<XCUIScreen *> *screens = [XCUIDevice.sharedDevice screensOrError:error];
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
- [[FBErrorBuilder.builder withDescriptionFormat:@"No display with id %lld is available. Available display ids: [%@]",
61
- displayID, [availableIDs componentsJoinedByString:@", "]]
62
- buildError:error];
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
- XCUIScreen *screen = [FBScreen screenWithDisplayID:[FBScreen displayID] error:&error];
66
- XCTAssertNil(error);
67
- XCTAssertEqual(screen.displayID, [FBScreen displayID]);
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appium-webdriveragent",
3
- "version": "16.13.6",
3
+ "version": "16.14.1",
4
4
  "description": "Package bundling WebDriverAgent",
5
5
  "keywords": [
6
6
  "Appium",