appium-webdriveragent 16.13.5 → 16.14.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 CHANGED
@@ -1,3 +1,15 @@
1
+ ## [16.14.0](https://github.com/appium/WebDriverAgent/compare/v16.13.6...v16.14.0) (2026-10-04)
2
+
3
+ ### Features
4
+
5
+ * support Duo hinge control and secondary display capture ([#1287](https://github.com/appium/WebDriverAgent/issues/1287)) ([76d6aa0](https://github.com/appium/WebDriverAgent/commit/76d6aa03e2423290bb05c436afc153bd32878ce0))
6
+
7
+ ## [16.13.6](https://github.com/appium/WebDriverAgent/compare/v16.13.5...v16.13.6) (2026-09-30)
8
+
9
+ ### Bug Fixes
10
+
11
+ * restore stability wait settings when quiescence throws ([#1281](https://github.com/appium/WebDriverAgent/issues/1281)) ([bcc434c](https://github.com/appium/WebDriverAgent/commit/bcc434c08127c370c0cb05ce1dc9f4cea9f98559))
12
+
1
13
  ## [16.13.5](https://github.com/appium/WebDriverAgent/compare/v16.13.4...v16.13.5) (2026-09-30)
2
14
 
3
15
  ### Performance Improvements
package/README.md CHANGED
@@ -26,6 +26,14 @@ and start sending [requests](https://github.com/facebook/WebDriverAgent/wiki/Que
26
26
 
27
27
  More about how to start WebDriverAgent [here](https://github.com/facebook/WebDriverAgent/wiki/Starting-WebDriverAgent).
28
28
 
29
+ ## Multiple displays
30
+
31
+ See [Multiple displays](docs/multiple-displays.md) for display selection, capture,
32
+ and active-app detection.
33
+
34
+ See [Hinge angle](docs/hinge-angle.md) for reading angles, sending simulated hinge
35
+ events, runtime requirements, and physical-device testing feedback.
36
+
29
37
  ## Known Issues
30
38
  If you are having some issues please checkout [wiki](https://github.com/facebook/WebDriverAgent/wiki/Common-Issues) first.
31
39
 
@@ -245,6 +245,7 @@
245
245
  57C72DA18039FED310967F8F /* XCTSourceCodeLocation.h in Headers */ = {isa = PBXBuildFile; fileRef = 81BB54ECBFB5B7B680BB5D4F /* XCTSourceCodeLocation.h */; };
246
246
  57EAF450031647694B6491F8 /* FBConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9B76A21CF7A43900275851 /* FBConfiguration.m */; };
247
247
  583AD004FF71CAA305E6DEAF /* XCUIDevice+FBHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = AD6C26971CF2481700F8B5FF /* XCUIDevice+FBHelpers.m */; };
248
+ 0460879AD6034130AE0A3DEC /* XCUIDevice+FBHinge.m in Sources */ = {isa = PBXBuildFile; fileRef = 8C8E6FD103A84C908F5746EA /* XCUIDevice+FBHinge.m */; };
248
249
  587AA46196BC2C46555C8E44 /* XCUIButtonConsole.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E09842154A44874C4E9CA01 /* XCUIButtonConsole.h */; settings = {ATTRIBUTES = (Public, ); }; };
249
250
  588F51093FD2A5597366C282 /* XCUIElementEventTarget-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 0DC62BF635704E9C72AF533E /* XCUIElementEventTarget-Protocol.h */; };
250
251
  5967912A40E807CDBE87E4B2 /* XCTReportingSessionIssueReporter-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 525D62A58488A52AA1BFE94A /* XCTReportingSessionIssueReporter-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -327,6 +328,7 @@
327
328
  641EE60A2240C5CA00173FCB /* FBLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9B76A41CF7A43900275851 /* FBLogger.m */; };
328
329
  641EE60B2240C5CA00173FCB /* FBCustomCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7531CAEDF0C008C271F /* FBCustomCommands.m */; };
329
330
  641EE60C2240C5CA00173FCB /* XCUIDevice+FBHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = AD6C26971CF2481700F8B5FF /* XCUIDevice+FBHelpers.m */; };
331
+ 181F0DBB6D28418680D52128 /* XCUIDevice+FBHinge.m in Sources */ = {isa = PBXBuildFile; fileRef = 8C8E6FD103A84C908F5746EA /* XCUIDevice+FBHinge.m */; };
330
332
  641EE60D2240C5CA00173FCB /* XCTestPrivateSymbols.m in Sources */ = {isa = PBXBuildFile; fileRef = EE6B64FC1D0F86EF00E85F5D /* XCTestPrivateSymbols.m */; };
331
333
  641EE60E2240C5CA00173FCB /* XCUIElement+FBTyping.m in Sources */ = {isa = PBXBuildFile; fileRef = AD76723C1D6B7CC000610457 /* XCUIElement+FBTyping.m */; };
332
334
  641EE60F2240C5CA00173FCB /* XCUIElement+FBAccessibility.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7461CAEDF0C008C271F /* XCUIElement+FBAccessibility.m */; };
@@ -433,6 +435,7 @@
433
435
  641EE6BF2240C5CA00173FCB /* FBKeyboard.h in Headers */ = {isa = PBXBuildFile; fileRef = EE3A18641CDE734B00DE4205 /* FBKeyboard.h */; settings = {ATTRIBUTES = (Public, ); }; };
434
436
  641EE6C02240C5CA00173FCB /* XCUIApplication+FBHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = AD6C269A1CF2494200F8B5FF /* XCUIApplication+FBHelpers.h */; settings = {ATTRIBUTES = (Public, ); }; };
435
437
  641EE6C22240C5CA00173FCB /* XCUIDevice+FBHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = AD6C26961CF2481700F8B5FF /* XCUIDevice+FBHelpers.h */; settings = {ATTRIBUTES = (Public, ); }; };
438
+ 95EF6989BCBE4C548C26B140 /* XCUIDevice+FBHinge.h in Headers */ = {isa = PBXBuildFile; fileRef = E7536267DE094076A4EEE1D7 /* XCUIDevice+FBHinge.h */; settings = {ATTRIBUTES = (Public, ); }; };
436
439
  641EE6C32240C5CA00173FCB /* FBClassChainQueryParser.h in Headers */ = {isa = PBXBuildFile; fileRef = 71A7EAF71E224648001DA4F2 /* FBClassChainQueryParser.h */; };
437
440
  641EE6C42240C5CA00173FCB /* FBMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9B76A51CF7A43900275851 /* FBMacros.h */; settings = {ATTRIBUTES = (Public, ); }; };
438
441
  641EE6C52240C5CA00173FCB /* XCTestExpectationDelegate-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACD91E3B77D600A02D78 /* XCTestExpectationDelegate-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -887,7 +890,9 @@
887
890
  AD6C26941CF2379700F8B5FF /* FBAlert.h in Headers */ = {isa = PBXBuildFile; fileRef = AD6C26921CF2379700F8B5FF /* FBAlert.h */; settings = {ATTRIBUTES = (Public, ); }; };
888
891
  AD6C26951CF2379700F8B5FF /* FBAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = AD6C26931CF2379700F8B5FF /* FBAlert.m */; };
889
892
  AD6C26981CF2481700F8B5FF /* XCUIDevice+FBHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = AD6C26961CF2481700F8B5FF /* XCUIDevice+FBHelpers.h */; settings = {ATTRIBUTES = (Public, ); }; };
893
+ 210AB789D14D40308B764DD0 /* XCUIDevice+FBHinge.h in Headers */ = {isa = PBXBuildFile; fileRef = E7536267DE094076A4EEE1D7 /* XCUIDevice+FBHinge.h */; settings = {ATTRIBUTES = (Public, ); }; };
890
894
  AD6C26991CF2481700F8B5FF /* XCUIDevice+FBHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = AD6C26971CF2481700F8B5FF /* XCUIDevice+FBHelpers.m */; };
895
+ 25C9BDFC65564D78A70231B4 /* XCUIDevice+FBHinge.m in Sources */ = {isa = PBXBuildFile; fileRef = 8C8E6FD103A84C908F5746EA /* XCUIDevice+FBHinge.m */; };
891
896
  AD6C269C1CF2494200F8B5FF /* XCUIApplication+FBHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = AD6C269A1CF2494200F8B5FF /* XCUIApplication+FBHelpers.h */; settings = {ATTRIBUTES = (Public, ); }; };
892
897
  AD6C269D1CF2494200F8B5FF /* XCUIApplication+FBHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = AD6C269B1CF2494200F8B5FF /* XCUIApplication+FBHelpers.m */; };
893
898
  AD764F10E2F00090AEB802F2 /* XCUIElement+FBIsVisible.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7481CAEDF0C008C271F /* XCUIElement+FBIsVisible.m */; };
@@ -987,6 +992,7 @@
987
992
  D1172A7F53F89B78D8324A13 /* RouteRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 374BB11AE4E4243BCA444CF8 /* RouteRequest.m */; };
988
993
  D1212325FDE77754EE503755 /* FBRouteRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7881CAEDF0C008C271F /* FBRouteRequest.m */; };
989
994
  D142183578036F5FD5B9880B /* XCUIDevice+FBHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = AD6C26961CF2481700F8B5FF /* XCUIDevice+FBHelpers.h */; };
995
+ 2E71304F39B64EDC98EE3974 /* XCUIDevice+FBHinge.h in Headers */ = {isa = PBXBuildFile; fileRef = E7536267DE094076A4EEE1D7 /* XCUIDevice+FBHinge.h */; };
990
996
  D14718959F7D46949859EEE6 /* FBDebugLogDelegateDecorator.m in Sources */ = {isa = PBXBuildFile; fileRef = EE7E27191D06C69F001BEC7B /* FBDebugLogDelegateDecorator.m */; };
991
997
  D1DE06DF54156F744E364B5F /* FBDebugLogDelegateDecorator.h in Headers */ = {isa = PBXBuildFile; fileRef = EE7E27181D06C69F001BEC7B /* FBDebugLogDelegateDecorator.h */; };
992
998
  D203F622C7C0D968F3BD2AAF /* XCUIPlatformApplicationServicesProviding-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 3BA71BCF1FDA482419CA8596 /* XCUIPlatformApplicationServicesProviding-Protocol.h */; };
@@ -1760,7 +1766,9 @@
1760
1766
  AD6C26921CF2379700F8B5FF /* FBAlert.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FBAlert.h; path = WebDriverAgentLib/FBAlert.h; sourceTree = SOURCE_ROOT; };
1761
1767
  AD6C26931CF2379700F8B5FF /* FBAlert.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; name = FBAlert.m; path = WebDriverAgentLib/FBAlert.m; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.objc; };
1762
1768
  AD6C26961CF2481700F8B5FF /* XCUIDevice+FBHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIDevice+FBHelpers.h"; sourceTree = "<group>"; };
1769
+ E7536267DE094076A4EEE1D7 /* XCUIDevice+FBHinge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIDevice+FBHinge.h"; sourceTree = "<group>"; };
1763
1770
  AD6C26971CF2481700F8B5FF /* XCUIDevice+FBHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCUIDevice+FBHelpers.m"; sourceTree = "<group>"; };
1771
+ 8C8E6FD103A84C908F5746EA /* XCUIDevice+FBHinge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCUIDevice+FBHinge.m"; sourceTree = "<group>"; };
1764
1772
  AD6C269A1CF2494200F8B5FF /* XCUIApplication+FBHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIApplication+FBHelpers.h"; sourceTree = "<group>"; };
1765
1773
  AD6C269B1CF2494200F8B5FF /* XCUIApplication+FBHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCUIApplication+FBHelpers.m"; sourceTree = "<group>"; };
1766
1774
  AD76723B1D6B7CC000610457 /* XCUIElement+FBTyping.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBTyping.h"; sourceTree = "<group>"; };
@@ -2415,7 +2423,9 @@
2415
2423
  EEDFE11F1D9C06F800E6FFE5 /* XCUIDevice+FBHealthCheck.h */,
2416
2424
  EEDFE1201D9C06F800E6FFE5 /* XCUIDevice+FBHealthCheck.m */,
2417
2425
  AD6C26961CF2481700F8B5FF /* XCUIDevice+FBHelpers.h */,
2426
+ E7536267DE094076A4EEE1D7 /* XCUIDevice+FBHinge.h */,
2418
2427
  AD6C26971CF2481700F8B5FF /* XCUIDevice+FBHelpers.m */,
2428
+ 8C8E6FD103A84C908F5746EA /* XCUIDevice+FBHinge.m */,
2419
2429
  A1B2C3D41F001A00A1B0001 /* XCUIDevice+FBVoiceOver.h */,
2420
2430
  A1B2C3D41F001A00A1B0002 /* XCUIDevice+FBVoiceOver.m */,
2421
2431
  EEE3763D1D59F81400ED88DD /* XCUIDevice+FBRotation.h */,
@@ -3178,6 +3188,7 @@
3178
3188
  641EE6C02240C5CA00173FCB /* XCUIApplication+FBHelpers.h in Headers */,
3179
3189
  714EAA0E2673FDFE005C5B47 /* FBCapabilities.h in Headers */,
3180
3190
  641EE6C22240C5CA00173FCB /* XCUIDevice+FBHelpers.h in Headers */,
3191
+ 95EF6989BCBE4C548C26B140 /* XCUIDevice+FBHinge.h in Headers */,
3181
3192
  71D3B3D6267FC7260076473D /* XCUIElement+FBResolve.h in Headers */,
3182
3193
  641EE6C32240C5CA00173FCB /* FBClassChainQueryParser.h in Headers */,
3183
3194
  641EE6C42240C5CA00173FCB /* FBMacros.h in Headers */,
@@ -3489,6 +3500,7 @@
3489
3500
  81231F08969740B0C8C67A30 /* XCUIApplication+FBHelpers.h in Headers */,
3490
3501
  9AA0E22033281B4EAA78BC51 /* FBCapabilities.h in Headers */,
3491
3502
  D142183578036F5FD5B9880B /* XCUIDevice+FBHelpers.h in Headers */,
3503
+ 2E71304F39B64EDC98EE3974 /* XCUIDevice+FBHinge.h in Headers */,
3492
3504
  09D97BD46764FD8149747723 /* XCUIElement+FBResolve.h in Headers */,
3493
3505
  6BF3F57247F59B81B8BCEE7B /* FBClassChainQueryParser.h in Headers */,
3494
3506
  8AAA33A5B1943B667B0DB05E /* FBMacros.h in Headers */,
@@ -3756,6 +3768,7 @@
3756
3768
  AD6C269C1CF2494200F8B5FF /* XCUIApplication+FBHelpers.h in Headers */,
3757
3769
  714D88CC2733FB970074A925 /* FBXMLGenerationOptions.h in Headers */,
3758
3770
  AD6C26981CF2481700F8B5FF /* XCUIDevice+FBHelpers.h in Headers */,
3771
+ 210AB789D14D40308B764DD0 /* XCUIDevice+FBHinge.h in Headers */,
3759
3772
  71A7EAF91E224648001DA4F2 /* FBClassChainQueryParser.h in Headers */,
3760
3773
  EE9B76AA1CF7A43900275851 /* FBMacros.h in Headers */,
3761
3774
  C8FB547922D4C1FC00B69954 /* FBUnattachedAppLauncher.h in Headers */,
@@ -4513,6 +4526,7 @@
4513
4526
  641EE60B2240C5CA00173FCB /* FBCustomCommands.m in Sources */,
4514
4527
  71BB58E42B9631F100CB9BFE /* FBScreenRecordingPromise.m in Sources */,
4515
4528
  641EE60C2240C5CA00173FCB /* XCUIDevice+FBHelpers.m in Sources */,
4529
+ 181F0DBB6D28418680D52128 /* XCUIDevice+FBHinge.m in Sources */,
4516
4530
  641EE60D2240C5CA00173FCB /* XCTestPrivateSymbols.m in Sources */,
4517
4531
  641EE60E2240C5CA00173FCB /* XCUIElement+FBTyping.m in Sources */,
4518
4532
  641EE60F2240C5CA00173FCB /* XCUIElement+FBAccessibility.m in Sources */,
@@ -4668,6 +4682,7 @@
4668
4682
  EDC993E0858F89F73D8E9DF8 /* FBCustomCommands.m in Sources */,
4669
4683
  45033CF6BD35C16E34BF76CC /* FBScreenRecordingPromise.m in Sources */,
4670
4684
  583AD004FF71CAA305E6DEAF /* XCUIDevice+FBHelpers.m in Sources */,
4685
+ 0460879AD6034130AE0A3DEC /* XCUIDevice+FBHinge.m in Sources */,
4671
4686
  192CACF49EC3825BA6D2EF4F /* XCTestPrivateSymbols.m in Sources */,
4672
4687
  53E0F0F38E7847065810FF41 /* XCUIElement+FBTyping.m in Sources */,
4673
4688
  A5353D849D394BC630839E59 /* XCUIElement+FBAccessibility.m in Sources */,
@@ -4787,6 +4802,7 @@
4787
4802
  EE9B76A91CF7A43900275851 /* FBLogger.m in Sources */,
4788
4803
  EE158ABB1CBD456F00A3E3F0 /* FBCustomCommands.m in Sources */,
4789
4804
  AD6C26991CF2481700F8B5FF /* XCUIDevice+FBHelpers.m in Sources */,
4805
+ 25C9BDFC65564D78A70231B4 /* XCUIDevice+FBHinge.m in Sources */,
4790
4806
  716C9E0227315EFF005AD475 /* XCUIApplication+FBUIInterruptions.m in Sources */,
4791
4807
  EE6B64FE1D0F86EF00E85F5D /* XCTestPrivateSymbols.m in Sources */,
4792
4808
  AD76723E1D6B7CC000610457 /* XCUIElement+FBTyping.m in Sources */,
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Copyright (c) 2015-present, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ #import <XCTest/XCTest.h>
10
+
11
+ NS_ASSUME_NONNULL_BEGIN
12
+
13
+ @interface XCUIDevice (FBHinge)
14
+
15
+ /** Whether a hinge and the CoreMotion angle-reading APIs are available. */
16
+ - (BOOL)fb_supportsHingeAngleReading;
17
+
18
+ /**
19
+ Whether a hinge and the IOKit APIs needed to attempt injection are available.
20
+ This does not guarantee event acceptance, particularly on physical devices.
21
+ */
22
+ - (BOOL)fb_canAttemptSimulatedHingeAngleInjection;
23
+
24
+ /**
25
+ Reads the current hinge angle in degrees, including changes made outside WDA.
26
+ Waits up to 5 seconds for a valid reading. Does not change the hinge angle.
27
+ @return The angle, or nil with an error if unavailable or reading times out.
28
+ */
29
+ - (nullable NSNumber *)fb_getSimulatedHingeAngle:(NSError **)error;
30
+
31
+ /**
32
+ Sends a simulated hinge angle in degrees (0 = closed, 180 = fully open).
33
+ This dispatches the event; the display transition completes asynchronously.
34
+ Verified on the iPhone Duo simulator; physical-device behavior is unverified.
35
+ Does not change currentDisplayId.
36
+ */
37
+ - (BOOL)fb_setSimulatedHingeAngle:(double)angle error:(NSError **)error;
38
+
39
+ @end
40
+
41
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Copyright (c) 2015-present, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ #import "XCUIDevice+FBHinge.h"
10
+
11
+ #import <dlfcn.h>
12
+ #import <math.h>
13
+ #import <mach/mach_time.h>
14
+
15
+ #import "FBErrorBuilder.h"
16
+
17
+ #if !TARGET_OS_TV && !TARGET_OS_WATCH
18
+ static const NSTimeInterval FBHingeAngleReadingTimeout = 5.0;
19
+
20
+ typedef CFDataRef (*FBIOCFSerialize)(CFTypeRef object, CFOptionFlags options);
21
+ typedef CFTypeRef (*FBIOHIDEventCreateVendorDefinedEvent)(CFAllocatorRef allocator,
22
+ uint64_t timestamp,
23
+ uint32_t usagePage,
24
+ uint32_t usage,
25
+ uint32_t version,
26
+ uint8_t *data,
27
+ CFIndex length,
28
+ uint32_t options);
29
+ typedef CFTypeRef (*FBIOHIDEventSystemClientCreate)(CFAllocatorRef allocator);
30
+ typedef void (*FBIOHIDEventSystemClientDispatchEvent)(CFTypeRef client, CFTypeRef event);
31
+
32
+ @protocol FBAngle <NSObject>
33
+ - (BOOL)isAngleValid;
34
+ - (float)angleDegrees;
35
+ @end
36
+
37
+ @protocol FBAngleManager <NSObject>
38
+ + (instancetype)new;
39
+ + (BOOL)isAvailable;
40
+ + (BOOL)instancesRespondToSelector:(SEL)selector;
41
+ - (void)startAngleUpdatesToQueue:(NSOperationQueue *)queue handler:(void (^)(id<FBAngle>))handler;
42
+ - (void)stopAngleUpdates;
43
+ @end
44
+
45
+ static Class<FBAngleManager> FBAngleManagerClass(void)
46
+ {
47
+ static Class<FBAngleManager> angleManagerClass;
48
+ static dispatch_once_t onceToken;
49
+ dispatch_once(&onceToken, ^{
50
+ // This path also loads on physical devices (verified on an iPad mini).
51
+ // Framework/class presence does not imply that a hinge sensor is available.
52
+ if (NULL != dlopen("/System/Library/Frameworks/CoreMotion.framework/CoreMotion", RTLD_LAZY)) {
53
+ angleManagerClass = NSClassFromString(@"CMAngleManager");
54
+ }
55
+ });
56
+ return angleManagerClass;
57
+ }
58
+
59
+ static BOOL FBHasAvailableHinge(void)
60
+ {
61
+ Class<FBAngleManager> managerClass = FBAngleManagerClass();
62
+ return [managerClass respondsToSelector:@selector(isAvailable)] && [managerClass isAvailable];
63
+ }
64
+
65
+ typedef struct {
66
+ FBIOCFSerialize serialize;
67
+ FBIOHIDEventCreateVendorDefinedEvent createEvent;
68
+ FBIOHIDEventSystemClientCreate createClient;
69
+ FBIOHIDEventSystemClientDispatchEvent dispatchEvent;
70
+ } FBHingeInjectionAPI;
71
+
72
+ // Loads and caches the injection APIs; NULL means at least one is unavailable.
73
+ // Resolving these functions does not dispatch an event or confirm its acceptance.
74
+ static const FBHingeInjectionAPI *FBHingeInjectionFunctions(void)
75
+ {
76
+ static FBHingeInjectionAPI api;
77
+ static dispatch_once_t onceToken;
78
+ dispatch_once(&onceToken, ^{
79
+ void *handle = dlopen("/System/Library/Frameworks/IOKit.framework/IOKit", RTLD_LAZY);
80
+ if (NULL != handle) {
81
+ api.serialize = (FBIOCFSerialize)dlsym(handle, "IOCFSerialize");
82
+ api.createEvent = (FBIOHIDEventCreateVendorDefinedEvent)dlsym(handle, "IOHIDEventCreateVendorDefinedEvent");
83
+ api.createClient = (FBIOHIDEventSystemClientCreate)dlsym(handle, "IOHIDEventSystemClientCreate");
84
+ api.dispatchEvent = (FBIOHIDEventSystemClientDispatchEvent)dlsym(handle, "IOHIDEventSystemClientDispatchEvent");
85
+ }
86
+ });
87
+ return NULL != api.serialize && NULL != api.createEvent
88
+ && NULL != api.createClient && NULL != api.dispatchEvent ? &api : NULL;
89
+ }
90
+ #endif
91
+
92
+ @implementation XCUIDevice (FBHinge)
93
+
94
+ - (BOOL)fb_supportsHingeAngleReading
95
+ {
96
+ #if !TARGET_OS_TV && !TARGET_OS_WATCH
97
+ Class<FBAngleManager> managerClass = FBAngleManagerClass();
98
+ return FBHasAvailableHinge()
99
+ && [managerClass instancesRespondToSelector:@selector(startAngleUpdatesToQueue:handler:)]
100
+ && [managerClass instancesRespondToSelector:@selector(stopAngleUpdates)];
101
+ #else
102
+ return NO;
103
+ #endif
104
+ }
105
+
106
+ - (BOOL)fb_canAttemptSimulatedHingeAngleInjection
107
+ {
108
+ #if !TARGET_OS_TV && !TARGET_OS_WATCH
109
+ // Sensor presence and local APIs permit an attempt, not proof that the
110
+ // device's receiver accepts the vendor event. Physical devices are unverified.
111
+ return FBHasAvailableHinge() && NULL != FBHingeInjectionFunctions();
112
+ #else
113
+ return NO;
114
+ #endif
115
+ }
116
+
117
+ - (nullable NSNumber *)fb_getSimulatedHingeAngle:(NSError **)error
118
+ {
119
+ if (!self.fb_supportsHingeAngleReading) {
120
+ [[FBErrorBuilder.builder withDescription:@"Hinge angle reading is unavailable on this device"] buildError:error];
121
+ return nil;
122
+ }
123
+ #if !TARGET_OS_TV && !TARGET_OS_WATCH
124
+ id<FBAngleManager> manager = [FBAngleManagerClass() new];
125
+ NSOperationQueue *queue = [NSOperationQueue new];
126
+ queue.maxConcurrentOperationCount = 1;
127
+ NSCondition *condition = [NSCondition new];
128
+ __block NSNumber *reading = nil;
129
+ NSNumber *result;
130
+ @try {
131
+ [manager startAngleUpdatesToQueue:queue handler:^(id<FBAngle> angle) {
132
+ if (![angle respondsToSelector:@selector(isAngleValid)]
133
+ || ![angle respondsToSelector:@selector(angleDegrees)] || !angle.isAngleValid) {
134
+ return;
135
+ }
136
+ float degrees = angle.angleDegrees;
137
+ if (!isfinite(degrees)) {
138
+ return;
139
+ }
140
+ [condition lock];
141
+ if (nil == reading) {
142
+ reading = @(degrees);
143
+ [condition signal];
144
+ }
145
+ [condition unlock];
146
+ }];
147
+ NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:FBHingeAngleReadingTimeout];
148
+ [condition lock];
149
+ while (nil == reading) {
150
+ if (![condition waitUntilDate:deadline]) {
151
+ break;
152
+ }
153
+ }
154
+ result = reading;
155
+ [condition unlock];
156
+ } @finally {
157
+ [manager stopAngleUpdates];
158
+ }
159
+ if (nil == result) {
160
+ [[FBErrorBuilder.builder withDescriptionFormat:@"Timed out after %g seconds waiting for a valid hinge angle",
161
+ FBHingeAngleReadingTimeout] buildError:error];
162
+ }
163
+ return result;
164
+ #else
165
+ return nil;
166
+ #endif
167
+ }
168
+
169
+ - (BOOL)fb_setSimulatedHingeAngle:(double)angle error:(NSError **)error
170
+ {
171
+ if (!isfinite(angle) || angle < 0 || angle > 180) {
172
+ return [[FBErrorBuilder.builder withDescription:@"Hinge angle must be a finite number between 0 and 180 degrees"] buildError:error];
173
+ }
174
+ if (!self.fb_canAttemptSimulatedHingeAngleInjection) {
175
+ return [[FBErrorBuilder.builder withDescription:@"Hinge angle injection requires an available hinge and the IOKit HID APIs"] buildError:error];
176
+ }
177
+ #if !TARGET_OS_TV && !TARGET_OS_WATCH
178
+ // WDA's fb_performIOHIDEventWithPage:usage:duration:error: uses XCDeviceEvent
179
+ // with page, usage, and duration only. The XCDeviceEvent interfaces checked
180
+ // in Xcode 27.1 provide no way to attach the vendor payload containing the
181
+ // hinge angle, so this command creates and dispatches an IOKit event directly.
182
+ const FBHingeInjectionAPI *api = FBHingeInjectionFunctions();
183
+ // Matches Device Hub's hinge-slider-control payload (Xcode 27.1). It is an
184
+ // IOCF binary serialization, not an NSPropertyListSerialization binary plist.
185
+ NSDictionary *payload = @{
186
+ @"provider": @"com.apple.Virtualization.VirtualMachines",
187
+ @"source": @"hinge-slider-control",
188
+ @"type": @"range",
189
+ @"value": @(angle),
190
+ };
191
+ CFDataRef data = api->serialize((__bridge CFTypeRef)payload, 1);
192
+ if (NULL == data) {
193
+ return [[FBErrorBuilder.builder withDescription:@"Cannot serialize the simulated hinge event"] buildError:error];
194
+ }
195
+ CFTypeRef event = api->createEvent(kCFAllocatorDefault, mach_absolute_time(), 0xff61, 0x5b, 0,
196
+ (uint8_t *)CFDataGetBytePtr(data), CFDataGetLength(data), 0);
197
+ CFRelease(data);
198
+ CFTypeRef client = api->createClient(kCFAllocatorDefault);
199
+ BOOL canDispatch = NULL != event && NULL != client;
200
+ if (canDispatch) {
201
+ api->dispatchEvent(client, event);
202
+ }
203
+ if (NULL != event) {
204
+ CFRelease(event);
205
+ }
206
+ if (NULL != client) {
207
+ CFRelease(client);
208
+ }
209
+ return canDispatch || [[FBErrorBuilder.builder withDescription:@"Cannot create the simulated hinge HID event or client"] buildError:error];
210
+ #else
211
+ return NO;
212
+ #endif
213
+ }
214
+
215
+ @end
@@ -141,18 +141,21 @@
141
141
  return;
142
142
  }
143
143
 
144
- NSTimeInterval previousTimeout = FBConfiguration.sharedInstance.waitForIdleTimeout;
145
- BOOL previousQuiescence = self.application.fb_shouldWaitForQuiescence;
146
- FBConfiguration.sharedInstance.waitForIdleTimeout = timeout;
147
- if (!previousQuiescence) {
148
- self.application.fb_shouldWaitForQuiescence = YES;
149
- }
150
- [[[self.application applicationImpl] currentProcess]
151
- fb_waitForQuiescenceIncludingAnimationsIdle:YES];
152
- if (previousQuiescence != self.application.fb_shouldWaitForQuiescence) {
153
- self.application.fb_shouldWaitForQuiescence = previousQuiescence;
144
+ XCUIApplication *application = self.application;
145
+ // Recursive synchronization also covers nested stability checks on this thread.
146
+ @synchronized (FBConfiguration.sharedInstance) {
147
+ NSTimeInterval previousTimeout = FBConfiguration.sharedInstance.waitForIdleTimeout;
148
+ BOOL previousQuiescence = application.fb_shouldWaitForQuiescence;
149
+ @try {
150
+ FBConfiguration.sharedInstance.waitForIdleTimeout = timeout;
151
+ application.fb_shouldWaitForQuiescence = YES;
152
+ [[[application applicationImpl] currentProcess]
153
+ fb_waitForQuiescenceIncludingAnimationsIdle:YES];
154
+ } @finally {
155
+ application.fb_shouldWaitForQuiescence = previousQuiescence;
156
+ FBConfiguration.sharedInstance.waitForIdleTimeout = previousTimeout;
157
+ }
154
158
  }
155
- FBConfiguration.sharedInstance.waitForIdleTimeout = previousTimeout;
156
159
  }
157
160
 
158
161
  - (void)fb_raiseStaleElementExceptionWithError:(NSError *)error __attribute__((noreturn))
@@ -29,6 +29,7 @@
29
29
  #import "XCUIApplication.h"
30
30
  #import "XCUIApplication+FBHelpers.h"
31
31
  #import "XCUIDevice+FBHelpers.h"
32
+ #import "XCUIDevice+FBHinge.h"
32
33
  #import "XCUIDevice+FBVoiceOver.h"
33
34
  #import "XCUIElement.h"
34
35
  #import "XCUIElement+FBIsVisible.h"
@@ -74,6 +75,10 @@
74
75
  [[FBRoute POST:@"/wda/expectNotification"] respondWithTarget:self action:@selector(handleExpectNotification:)],
75
76
  [[FBRoute POST:@"/wda/siri/activate"] respondWithTarget:self action:@selector(handleActivateSiri:)],
76
77
  [[FBRoute POST:@"/wda/apps/launchUnattached"].withoutSession respondWithTarget:self action:@selector(handleLaunchUnattachedApp:)],
78
+ [[FBRoute GET:@"/wda/device/hingeAngle"] respondWithTarget:self action:@selector(handleGetSimulatedHingeAngle:)],
79
+ [[FBRoute GET:@"/wda/device/hingeAngle"].withoutSession respondWithTarget:self action:@selector(handleGetSimulatedHingeAngle:)],
80
+ [[FBRoute POST:@"/wda/device/hingeAngle"] respondWithTarget:self action:@selector(handleSetSimulatedHingeAngle:)],
81
+ [[FBRoute POST:@"/wda/device/hingeAngle"].withoutSession respondWithTarget:self action:@selector(handleSetSimulatedHingeAngle:)],
77
82
  [[FBRoute GET:@"/wda/device/info"] respondWithTarget:self action:@selector(handleGetDeviceInfo:)],
78
83
  [[FBRoute POST:@"/wda/resetAppAuth"] respondWithTarget:self action:@selector(handleResetAppAuth:)],
79
84
  [[FBRoute GET:@"/wda/device/info"].withoutSession respondWithTarget:self action:@selector(handleGetDeviceInfo:)],
@@ -777,4 +782,35 @@
777
782
  return FBResponseWithObject(result);
778
783
  }
779
784
 
785
+ + (id<FBResponsePayload>)handleGetSimulatedHingeAngle:(FBRouteRequest *)request
786
+ {
787
+ if (!XCUIDevice.sharedDevice.fb_supportsHingeAngleReading) {
788
+ return FBResponseWithStatus([FBCommandStatus unsupportedOperationErrorWithMessage:
789
+ @"Hinge angle reading is unavailable on this device" traceback:nil]);
790
+ }
791
+ NSError *error;
792
+ NSNumber *angle = [XCUIDevice.sharedDevice fb_getSimulatedHingeAngle:&error];
793
+ return nil == angle ? FBResponseWithUnknownError(error) : FBResponseWithObject(angle);
794
+ }
795
+
796
+ + (id<FBResponsePayload>)handleSetSimulatedHingeAngle:(FBRouteRequest *)request
797
+ {
798
+ id angle = request.arguments[@"angle"];
799
+ if (![angle isKindOfClass:NSNumber.class]
800
+ || CFGetTypeID((__bridge CFTypeRef)angle) == CFBooleanGetTypeID()
801
+ || !isfinite([angle doubleValue]) || [angle doubleValue] < 0 || [angle doubleValue] > 180) {
802
+ return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:
803
+ @"'angle' must be a finite number between 0 and 180 degrees" traceback:nil]);
804
+ }
805
+ if (!XCUIDevice.sharedDevice.fb_canAttemptSimulatedHingeAngleInjection) {
806
+ return FBResponseWithStatus([FBCommandStatus unsupportedOperationErrorWithMessage:
807
+ @"Setting a simulated hinge angle requires a compatible foldable device" traceback:nil]);
808
+ }
809
+ NSError *error;
810
+ if (![XCUIDevice.sharedDevice fb_setSimulatedHingeAngle:[angle doubleValue] error:&error]) {
811
+ return FBResponseWithUnknownError(error);
812
+ }
813
+ return FBResponseWithOK();
814
+ }
815
+
780
816
  @end
@@ -15,11 +15,11 @@
15
15
  <key>CFBundlePackageType</key>
16
16
  <string>FMWK</string>
17
17
  <key>CFBundleShortVersionString</key>
18
- <string>16.13.5</string>
18
+ <string>16.14.0</string>
19
19
  <key>CFBundleSignature</key>
20
20
  <string>????</string>
21
21
  <key>CFBundleVersion</key>
22
- <string>16.13.5</string>
22
+ <string>16.14.0</string>
23
23
  <key>NSPrincipalClass</key>
24
24
  <string/>
25
25
  </dict>
@@ -9,6 +9,7 @@
9
9
  #import "FBScreenRecordingRequest.h"
10
10
 
11
11
  #import "FBErrorBuilder.h"
12
+ #import "FBScreen.h"
12
13
  #import "XCUIScreen.h"
13
14
 
14
15
  @implementation FBScreenRecordingRequest
@@ -56,6 +57,10 @@
56
57
 
57
58
  - (id)toNativeRequestWithError:(NSError **)error
58
59
  {
60
+ XCUIScreen *screen = [FBScreen currentScreenWithError:error];
61
+ if (nil == screen) {
62
+ return nil;
63
+ }
59
64
  Class screenRecordingRequestClass = NSClassFromString(@"XCTScreenRecordingRequest");
60
65
  if (nil == screenRecordingRequestClass) {
61
66
  [[[FBErrorBuilder builder]
@@ -80,8 +85,8 @@
80
85
  NSMethodSignature *screenRecordingRequestContructorSignature = [screenRecordingRequestAllocated methodSignatureForSelector:screenRecordingRequestConstructorSelector];
81
86
  NSInvocation *screenRecordingRequestInitInvocation = [NSInvocation invocationWithMethodSignature:screenRecordingRequestContructorSignature];
82
87
  [screenRecordingRequestInitInvocation setSelector:screenRecordingRequestConstructorSelector];
83
- long long mainScreenId = XCUIScreen.mainScreen.displayID;
84
- [screenRecordingRequestInitInvocation setArgument:&mainScreenId atIndex:2];
88
+ long long screenId = screen.displayID;
89
+ [screenRecordingRequestInitInvocation setArgument:&screenId atIndex:2];
85
90
  CGRect fullScreenRect = CGRectNull;
86
91
  [screenRecordingRequestInitInvocation setArgument:&fullScreenRect atIndex:3];
87
92
  [screenRecordingRequestInitInvocation setArgument:&videoEncoding atIndex:4];
@@ -24,14 +24,14 @@ NS_ASSUME_NONNULL_BEGIN
24
24
  * Calculates the accessbility element which is located at the given screen coordinates
25
25
  *
26
26
  * @param point The screen coordinates
27
- * @returns The retrieved accessbility element or nil if it cannot be detected
27
+ * @returns The retrieved accessbility element or nil if it cannot be detected or a secondary display is selected
28
28
  */
29
29
  + (nullable id<FBXCAccessibilityElement>)axElementWithPoint:(CGPoint)point;
30
30
 
31
31
  /**
32
32
  * Retrieves the accessbility element for the current screen point
33
33
  *
34
- * @returns The retrieved accessbility element or nil if it cannot be detected
34
+ * @returns The retrieved accessbility element or nil if it cannot be detected or a secondary display is selected
35
35
  */
36
36
  - (nullable id<FBXCAccessibilityElement>)axElement;
37
37
 
@@ -13,6 +13,10 @@
13
13
  #endif
14
14
 
15
15
  #import "FBErrorBuilder.h"
16
+ #import "FBScreen.h"
17
+ #import "FBConfiguration.h"
18
+ #import "FBExceptions.h"
19
+ #import "XCUIScreen.h"
16
20
  #import "FBLogger.h"
17
21
  #import "FBXCTestDaemonsProxy.h"
18
22
  #import "XCTMessagingChannel_RunnerToDaemon-Protocol.h"
@@ -45,6 +49,22 @@
45
49
 
46
50
  + (id<FBXCAccessibilityElement>)axElementWithPoint:(CGPoint)point
47
51
  {
52
+ if (nil != FBConfiguration.sharedInstance.currentDisplayId) {
53
+ NSError *error;
54
+ XCUIScreen *screen = [FBScreen currentScreenWithError:&error];
55
+ if (nil == screen) {
56
+ @throw [NSException exceptionWithName:FBInvalidArgumentException
57
+ reason:error.localizedDescription
58
+ userInfo:nil];
59
+ }
60
+ // XCTest's point lookup has no display parameter and always hits the main
61
+ // display. On Duo this can select the outer SpringBoard while the app is
62
+ // foreground on the inner display. Let callers use their active-app fallback
63
+ // instead of treating an element from a different display as authoritative.
64
+ if (screen.displayID != XCUIScreen.mainScreen.displayID) {
65
+ return nil;
66
+ }
67
+ }
48
68
  __block id<FBXCAccessibilityElement> onScreenElement = nil;
49
69
  id<XCTMessagingChannel_RunnerToDaemon> proxy = [FBXCTestDaemonsProxy testRunnerProxy];
50
70
  dispatch_semaphore_t sem = dispatch_semaphore_create(0);
@@ -369,7 +369,7 @@ typedef NS_ENUM(NSInteger, FBConfigurationKeyboardPreference) {
369
369
  @property (atomic, assign) BOOL enforceCustomSnapshots;
370
370
 
371
371
  /**
372
- * The identifier of the display targeted by screenshots.
372
+ * The identifier of the display targeted by screenshots, MJPEG, and new screen recordings.
373
373
  * nil means the main display. Reset to nil for each new session.
374
374
  */
375
375
  @property (atomic, copy, nullable) NSNumber *currentDisplayId;
@@ -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>
@@ -11,9 +11,58 @@
11
11
  #import "FBIntegrationTestCase.h"
12
12
  #import "FBConfiguration.h"
13
13
  #import "FBScreen.h"
14
+ #import "FBActiveAppDetectionPoint.h"
15
+ #import "FBExceptions.h"
16
+ #import "FBScreenRecordingRequest.h"
14
17
  #import "FBScreenshot.h"
15
18
  #import "XCUIScreen.h"
16
19
 
20
+ @interface FBScreen (FBLookupTesting)
21
+ + (nullable XCUIScreen *)screenWithDisplayID:(long long)displayID device:(id)device error:(NSError **)error;
22
+ @end
23
+
24
+ @interface FBEmptyScreenDouble : NSObject
25
+ @property (nonatomic) CGRect bounds;
26
+ @end
27
+ @implementation FBEmptyScreenDouble
28
+ @end
29
+
30
+ @interface FBScreenDeviceDouble : NSObject
31
+ @property (nonatomic) BOOL nativeLookupAvailable;
32
+ @property (nonatomic) BOOL enumerationUnavailable;
33
+ @property (nonatomic) NSUInteger enumerationCount;
34
+ @property (nonatomic, strong) XCUIScreen *nativeScreen;
35
+ @property (nonatomic, strong) NSArray<XCUIScreen *> *screens;
36
+ @property (nonatomic, strong) NSError *lookupError;
37
+ @property (nonatomic, strong) NSError *enumerationError;
38
+ @end
39
+
40
+ @implementation FBScreenDeviceDouble
41
+ - (BOOL)respondsToSelector:(SEL)selector
42
+ {
43
+ if (selector == NSSelectorFromString(@"screensOrError:") && self.enumerationUnavailable) {
44
+ return NO;
45
+ }
46
+ return selector == @selector(screenWithDisplayID:orError:)
47
+ ? self.nativeLookupAvailable : [super respondsToSelector:selector];
48
+ }
49
+ - (XCUIScreen *)screenWithDisplayID:(long long)displayID orError:(NSError **)error
50
+ {
51
+ if (NULL != error) {
52
+ *error = self.lookupError;
53
+ }
54
+ return self.nativeScreen;
55
+ }
56
+ - (NSArray<XCUIScreen *> *)screensOrError:(NSError **)error
57
+ {
58
+ self.enumerationCount++;
59
+ if (NULL != error) {
60
+ *error = self.enumerationError;
61
+ }
62
+ return self.screens;
63
+ }
64
+ @end
65
+
17
66
  @interface FBScreenTests : FBIntegrationTestCase
18
67
  @end
19
68
 
@@ -62,9 +111,13 @@
62
111
  - (void)testScreenWithDisplayID
63
112
  {
64
113
  NSError *error = nil;
65
- XCUIScreen *screen = [FBScreen screenWithDisplayID:[FBScreen displayID] error:&error];
66
- XCTAssertNil(error);
67
- XCTAssertEqual(screen.displayID, [FBScreen displayID]);
114
+ for (NSDictionary<NSString *, id> *entry in [FBScreen screensWithError:&error]) {
115
+ long long displayID = [entry[@"displayId"] longLongValue];
116
+ XCUIScreen *screen = [FBScreen screenWithDisplayID:displayID error:&error];
117
+ XCTAssertNotNil(screen);
118
+ XCTAssertNil(error);
119
+ XCTAssertEqual(screen.displayID, displayID);
120
+ }
68
121
 
69
122
  XCTAssertNil([FBScreen screenWithDisplayID:[self unknownDisplayID] error:&error]);
70
123
  XCTAssertNotNil(error);
@@ -77,6 +130,84 @@
77
130
  XCTAssertTrue([error.localizedDescription containsString:availableDisplays]);
78
131
  }
79
132
 
133
+ - (void)testNativeScreenLookupDoesNotEnumerateOnSuccess
134
+ {
135
+ FBScreenDeviceDouble *device = [FBScreenDeviceDouble new];
136
+ device.nativeLookupAvailable = YES;
137
+ device.nativeScreen = XCUIScreen.mainScreen;
138
+ NSError *error = nil;
139
+ XCTAssertEqualObjects([FBScreen screenWithDisplayID:device.nativeScreen.displayID device:device error:&error],
140
+ device.nativeScreen);
141
+ XCTAssertNil(error);
142
+ XCTAssertEqual(device.enumerationCount, 0UL);
143
+ }
144
+
145
+ - (void)testNativeScreenPlaceholderMustBeAvailable
146
+ {
147
+ FBScreenDeviceDouble *device = [FBScreenDeviceDouble new];
148
+ device.nativeLookupAvailable = YES;
149
+ device.nativeScreen = (id)[FBEmptyScreenDouble new];
150
+ device.screens = @[XCUIScreen.mainScreen];
151
+ NSError *error = nil;
152
+ XCTAssertNil([FBScreen screenWithDisplayID:-1 device:device error:&error]);
153
+ XCTAssertNotNil(error);
154
+ XCTAssertEqual(device.enumerationCount, 1UL);
155
+ }
156
+
157
+ - (void)testNativeScreenLookupFailureIncludesAvailableIDs
158
+ {
159
+ FBScreenDeviceDouble *device = [FBScreenDeviceDouble new];
160
+ device.nativeLookupAvailable = YES;
161
+ device.screens = @[XCUIScreen.mainScreen];
162
+ device.lookupError = [NSError errorWithDomain:@"NativeLookup" code:1 userInfo:nil];
163
+ NSError *error = nil;
164
+ XCTAssertNil([FBScreen screenWithDisplayID:-1 device:device error:&error]);
165
+ XCTAssertEqual(device.enumerationCount, 1UL);
166
+ XCTAssertTrue([error.localizedDescription containsString:@"Available display ids"]);
167
+ XCTAssertEqualObjects(error.userInfo[NSUnderlyingErrorKey], device.lookupError);
168
+ }
169
+
170
+ - (void)testNativeScreenLookupErrorSurvivesEnumerationFailure
171
+ {
172
+ FBScreenDeviceDouble *device = [FBScreenDeviceDouble new];
173
+ device.nativeLookupAvailable = YES;
174
+ device.lookupError = [NSError errorWithDomain:@"NativeLookup" code:1 userInfo:nil];
175
+ device.enumerationError = [NSError errorWithDomain:@"Enumeration" code:2 userInfo:nil];
176
+ NSError *error = nil;
177
+ XCTAssertNil([FBScreen screenWithDisplayID:-1 device:device error:&error]);
178
+ XCTAssertEqualObjects(error, device.lookupError);
179
+ }
180
+
181
+ - (void)testScreenLookupWithoutNativeSelector
182
+ {
183
+ FBScreenDeviceDouble *device = [FBScreenDeviceDouble new];
184
+ device.screens = @[XCUIScreen.mainScreen];
185
+ NSError *error = nil;
186
+ XCTAssertEqualObjects([FBScreen screenWithDisplayID:XCUIScreen.mainScreen.displayID device:device error:&error],
187
+ XCUIScreen.mainScreen);
188
+ XCTAssertNil(error);
189
+ XCTAssertEqual(device.enumerationCount, 1UL);
190
+ XCTAssertNil([FBScreen screenWithDisplayID:-1 device:device error:&error]);
191
+ XCTAssertTrue([error.localizedDescription containsString:@"Available display ids"]);
192
+ device.screens = nil;
193
+ device.enumerationError = [NSError errorWithDomain:@"Enumeration" code:2 userInfo:nil];
194
+ XCTAssertNil([FBScreen screenWithDisplayID:-1 device:device error:&error]);
195
+ XCTAssertEqualObjects(error, device.enumerationError);
196
+ }
197
+
198
+ - (void)testScreenLookupWithoutDeviceScreenSelectors
199
+ {
200
+ FBScreenDeviceDouble *device = [FBScreenDeviceDouble new];
201
+ device.enumerationUnavailable = YES;
202
+ NSError *error = nil;
203
+ XCTAssertEqualObjects([FBScreen screenWithDisplayID:XCUIScreen.mainScreen.displayID device:device error:&error],
204
+ XCUIScreen.mainScreen);
205
+ XCTAssertNil(error);
206
+ XCTAssertNil([FBScreen screenWithDisplayID:-1 device:device error:&error]);
207
+ XCTAssertNotNil(error);
208
+ XCTAssertEqual(device.enumerationCount, 0UL);
209
+ }
210
+
80
211
  - (void)testCurrentScreenDefaultsToMainScreen
81
212
  {
82
213
  NSError *error = nil;
@@ -108,6 +239,52 @@
108
239
  XCTAssertNil(FBConfiguration.sharedInstance.currentDisplayId);
109
240
  }
110
241
 
242
+ - (void)testSecondaryDisplayDoesNotHitTestMainDisplay
243
+ {
244
+ NSArray<NSDictionary<NSString *, id> *> *screens = [FBScreen screensWithError:nil];
245
+ BOOL testedSecondaryDisplay = NO;
246
+ for (NSDictionary<NSString *, id> *screen in screens) {
247
+ if ([screen[@"isMain"] boolValue]) {
248
+ continue;
249
+ }
250
+ testedSecondaryDisplay = YES;
251
+ FBConfiguration.sharedInstance.currentDisplayId = screen[@"displayId"];
252
+ XCTAssertNil(FBActiveAppDetectionPoint.sharedInstance.axElement);
253
+ }
254
+ XCTSkipIf(!testedSecondaryDisplay, @"Requires a secondary display");
255
+ }
256
+
257
+ - (void)testActiveAppDetectionRejectsUnavailableDisplay
258
+ {
259
+ FBConfiguration.sharedInstance.currentDisplayId = @([self unknownDisplayID]);
260
+ XCTAssertThrowsSpecificNamed(FBActiveAppDetectionPoint.sharedInstance.axElement,
261
+ NSException, FBInvalidArgumentException);
262
+ }
263
+
264
+ - (void)testRecordingUsesSelectedDisplay
265
+ {
266
+ XCTSkipIf(nil == NSClassFromString(@"XCTScreenRecordingRequest"), @"Screen recording is unavailable");
267
+ FBScreenRecordingRequest *request = [[FBScreenRecordingRequest alloc] initWithFps:10 codec:0];
268
+ NSError *error = nil;
269
+ id defaultRequest = [request toNativeRequestWithError:&error];
270
+ XCTAssertNotNil(defaultRequest);
271
+ XCTAssertNil(error);
272
+ XCTAssertEqualObjects([defaultRequest valueForKey:@"screenID"], @([FBScreen displayID]));
273
+
274
+ for (NSDictionary<NSString *, id> *screen in [FBScreen screensWithError:nil]) {
275
+ FBConfiguration.sharedInstance.currentDisplayId = screen[@"displayId"];
276
+ id nativeRequest = [request toNativeRequestWithError:&error];
277
+ XCTAssertNotNil(nativeRequest);
278
+ XCTAssertNil(error);
279
+ XCTAssertEqualObjects([nativeRequest valueForKey:@"screenID"], screen[@"displayId"]);
280
+ }
281
+
282
+ FBConfiguration.sharedInstance.currentDisplayId = @([self unknownDisplayID]);
283
+ XCTAssertNil([request toNativeRequestWithError:&error]);
284
+ XCTAssertNotNil(error);
285
+ XCTAssertTrue([error.localizedDescription containsString:@"Available display ids"]);
286
+ }
287
+
111
288
  - (long long)unknownDisplayID
112
289
  {
113
290
  long long maxID = 0;
@@ -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
@@ -12,12 +12,52 @@
12
12
  #import "FBElement.h"
13
13
  #import "XCUIElementDouble.h"
14
14
  #import "FBElementUtils.h"
15
+ #import "FBConfiguration.h"
16
+ #import "XCUIElement+FBUtilities.h"
17
+ #import <objc/runtime.h>
18
+
19
+ @interface FBQuiescenceApplicationDouble : NSObject
20
+ @property (nonatomic) BOOL fb_shouldWaitForQuiescence;
21
+ @property (nonatomic) NSTimeInterval observedTimeout;
22
+ @end
23
+ @implementation FBQuiescenceApplicationDouble
24
+ - (id)applicationImpl { return self; }
25
+ - (id)currentProcess { return self; }
26
+ - (void)fb_waitForQuiescenceIncludingAnimationsIdle:(BOOL)includingAnimations
27
+ {
28
+ self.observedTimeout = FBConfiguration.sharedInstance.waitForIdleTimeout;
29
+ @throw [NSException exceptionWithName:@"QuiescenceFailure" reason:@"test" userInfo:nil];
30
+ }
31
+ @end
15
32
 
16
33
  @interface FBElementUtilitiesTests : XCTestCase
17
34
  @end
18
35
 
19
36
  @implementation FBElementUtilitiesTests
20
37
 
38
+ - (void)testStabilityWaitRestoresSettingsAfterAnException
39
+ {
40
+ NSTimeInterval originalTimeout = FBConfiguration.sharedInstance.waitForIdleTimeout;
41
+ FBQuiescenceApplicationDouble *application = [FBQuiescenceApplicationDouble new];
42
+ XCUIElementDouble *element = [XCUIElementDouble new];
43
+ element.application = (id)application;
44
+ SEL selector = @selector(fb_waitUntilStableWithTimeout:);
45
+ #pragma clang diagnostic push
46
+ #pragma clang diagnostic ignored "-Wcast-function-type-strict"
47
+ void (*wait)(id, SEL, NSTimeInterval) = (void (*)(id, SEL, NSTimeInterval))
48
+ class_getMethodImplementation(XCUIElement.class, selector);
49
+ #pragma clang diagnostic pop
50
+ @try {
51
+ FBConfiguration.sharedInstance.waitForIdleTimeout = 17;
52
+ XCTAssertThrowsSpecificNamed(wait(element, selector, 2), NSException, @"QuiescenceFailure");
53
+ XCTAssertEqual(application.observedTimeout, 2);
54
+ XCTAssertFalse(application.fb_shouldWaitForQuiescence);
55
+ XCTAssertEqual(FBConfiguration.sharedInstance.waitForIdleTimeout, 17);
56
+ } @finally {
57
+ FBConfiguration.sharedInstance.waitForIdleTimeout = originalTimeout;
58
+ }
59
+ }
60
+
21
61
  - (void)testTypesFiltering {
22
62
  NSMutableArray *elements = [NSMutableArray new];
23
63
  XCUIElementDouble *el1 = [XCUIElementDouble new];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appium-webdriveragent",
3
- "version": "16.13.5",
3
+ "version": "16.14.0",
4
4
  "description": "Package bundling WebDriverAgent",
5
5
  "keywords": [
6
6
  "Appium",