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 CHANGED
@@ -1,3 +1,16 @@
1
+ ## [16.14.1](https://github.com/appium/WebDriverAgent/compare/v16.14.0...v16.14.1) (2026-10-07)
2
+
3
+ ### Bug Fixes
4
+
5
+ * report geometry for the selected display ([#1291](https://github.com/appium/WebDriverAgent/issues/1291)) ([43fa6da](https://github.com/appium/WebDriverAgent/commit/43fa6da647fb8adca21fdbd106d8f73d782d6717))
6
+ * skip accessibility idle checks for WebKit services ([#1293](https://github.com/appium/WebDriverAgent/issues/1293)) ([9d06611](https://github.com/appium/WebDriverAgent/commit/9d06611d1f5d342fd16458f707dca3f3e3ec00fc))
7
+
8
+ ## [16.14.0](https://github.com/appium/WebDriverAgent/compare/v16.13.6...v16.14.0) (2026-10-04)
9
+
10
+ ### Features
11
+
12
+ * support Duo hinge control and secondary display capture ([#1287](https://github.com/appium/WebDriverAgent/issues/1287)) ([76d6aa0](https://github.com/appium/WebDriverAgent/commit/76d6aa03e2423290bb05c436afc153bd32878ce0))
13
+
1
14
  ## [16.13.6](https://github.com/appium/WebDriverAgent/compare/v16.13.5...v16.13.6) (2026-09-30)
2
15
 
3
16
  ### Bug Fixes
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 */,
@@ -109,6 +109,14 @@ static id swizzledRequestSnapshotForElement(id self, SEL _cmd, id element, id at
109
109
  return original_requestSnapshotForElement(self, _cmd, element, attributes, parameters, error);
110
110
  }
111
111
 
112
+ // Remote WKWebView elements belong to a WebKit service, not the host app.
113
+ // Sending the idle-check AX action to WebContent can crash it (#1290).
114
+ // Read process metadata only: resolving UI attributes here can reenter
115
+ // snapshotting. Leave these services on the original snapshot path.
116
+ if ([application.bundleID hasPrefix:@"com.apple.WebKit."]) {
117
+ return original_requestSnapshotForElement(self, _cmd, element, attributes, parameters, error);
118
+ }
119
+
112
120
  NSNumber *pidKey = @(pid);
113
121
  @synchronized (unresponsiveApplicationPidsLock) {
114
122
  NSDate *markedUnresponsiveAt = unresponsiveApplicationPids[pidKey];
@@ -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
@@ -29,10 +29,12 @@
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"
35
36
  #import "XCUIElementQuery.h"
37
+ #import "XCUIScreen.h"
36
38
  #import "FBUnattachedAppLauncher.h"
37
39
 
38
40
  @implementation FBCustomCommands
@@ -74,6 +76,10 @@
74
76
  [[FBRoute POST:@"/wda/expectNotification"] respondWithTarget:self action:@selector(handleExpectNotification:)],
75
77
  [[FBRoute POST:@"/wda/siri/activate"] respondWithTarget:self action:@selector(handleActivateSiri:)],
76
78
  [[FBRoute POST:@"/wda/apps/launchUnattached"].withoutSession respondWithTarget:self action:@selector(handleLaunchUnattachedApp:)],
79
+ [[FBRoute GET:@"/wda/device/hingeAngle"] respondWithTarget:self action:@selector(handleGetSimulatedHingeAngle:)],
80
+ [[FBRoute GET:@"/wda/device/hingeAngle"].withoutSession respondWithTarget:self action:@selector(handleGetSimulatedHingeAngle:)],
81
+ [[FBRoute POST:@"/wda/device/hingeAngle"] respondWithTarget:self action:@selector(handleSetSimulatedHingeAngle:)],
82
+ [[FBRoute POST:@"/wda/device/hingeAngle"].withoutSession respondWithTarget:self action:@selector(handleSetSimulatedHingeAngle:)],
77
83
  [[FBRoute GET:@"/wda/device/info"] respondWithTarget:self action:@selector(handleGetDeviceInfo:)],
78
84
  [[FBRoute POST:@"/wda/resetAppAuth"] respondWithTarget:self action:@selector(handleResetAppAuth:)],
79
85
  [[FBRoute GET:@"/wda/device/info"].withoutSession respondWithTarget:self action:@selector(handleGetDeviceInfo:)],
@@ -157,17 +163,41 @@
157
163
 
158
164
  + (id<FBResponsePayload>)handleGetScreen:(FBRouteRequest *)request
159
165
  {
166
+ NSError *error = nil;
167
+ XCUIScreen *screen = [FBScreen currentScreenWithError:&error];
168
+ if (nil == screen) {
169
+ return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription
170
+ traceback:nil]);
171
+ }
160
172
  XCUIApplication *app = XCUIApplication.fb_systemApplication;
161
-
162
- XCUIElement *mainStatusBar = app.statusBars.allElementsBoundByIndex.firstObject;
163
- CGSize statusBarSize = (nil == mainStatusBar) ? CGSizeZero : mainStatusBar.frame.size;
164
-
165
- #if TARGET_OS_TV || TARGET_OS_WATCH
166
- CGSize screenSize = app.frame.size;
167
- #else
168
- CGSize screenSize = FBAdjustDimensionsForApplication(app.wdFrame.size, app.interfaceOrientation);
173
+ CGSize screenSize = CGSizeMake(CGRectGetWidth(screen.bounds) / screen.scale,
174
+ CGRectGetHeight(screen.bounds) / screen.scale);
175
+ #if !TARGET_OS_TV && !TARGET_OS_WATCH
176
+ XCUIApplication *activeApp = request.session ? request.session.activeApplication : XCUIApplication.fb_activeApplication;
177
+ screenSize = FBAdjustDimensionsForApplication(screenSize, activeApp.interfaceOrientation);
169
178
  #endif
170
179
 
180
+ // SpringBoard can expose hidden status bars belonging to other displays, and
181
+ // Duo also exposes full-screen containers for its side-mounted status UI.
182
+ // Only a visible horizontal bar at the top of this display can be subtracted
183
+ // from a rectangular viewport.
184
+ CGSize statusBarSize = CGSizeZero;
185
+ for (XCUIElement *statusBar in app.statusBars.allElementsBoundByIndex) {
186
+ // Resolve the element before reading its screen: displayID can be zero on
187
+ // an unresolved query result even when the bar belongs to the main screen.
188
+ CGRect frame = statusBar.frame;
189
+ if (statusBar.screen.displayID != screen.displayID || !statusBar.fb_isVisible) {
190
+ continue;
191
+ }
192
+ if (fabs(CGRectGetMinY(frame)) <= 1 && fabs(CGRectGetMinX(frame)) <= 1
193
+ && fabs(CGRectGetWidth(frame) - screenSize.width) <= 1
194
+ && CGRectGetHeight(frame) > 0 && CGRectGetHeight(frame) < screenSize.height
195
+ && CGRectGetHeight(frame) < CGRectGetWidth(frame)) {
196
+ statusBarSize = frame.size;
197
+ break;
198
+ }
199
+ }
200
+
171
201
  return FBResponseWithObject(
172
202
  @{
173
203
  @"screenSize":@{@"width": @(screenSize.width),
@@ -176,8 +206,8 @@
176
206
  @"statusBarSize": @{@"width": @(statusBarSize.width),
177
207
  @"height": @(statusBarSize.height),
178
208
  },
179
- @"displayId": @([FBScreen displayID]),
180
- @"scale": @([FBScreen scale]),
209
+ @"displayId": @(screen.displayID),
210
+ @"scale": @(screen.scale),
181
211
  });
182
212
  }
183
213
 
@@ -777,4 +807,35 @@
777
807
  return FBResponseWithObject(result);
778
808
  }
779
809
 
810
+ + (id<FBResponsePayload>)handleGetSimulatedHingeAngle:(FBRouteRequest *)request
811
+ {
812
+ if (!XCUIDevice.sharedDevice.fb_supportsHingeAngleReading) {
813
+ return FBResponseWithStatus([FBCommandStatus unsupportedOperationErrorWithMessage:
814
+ @"Hinge angle reading is unavailable on this device" traceback:nil]);
815
+ }
816
+ NSError *error;
817
+ NSNumber *angle = [XCUIDevice.sharedDevice fb_getSimulatedHingeAngle:&error];
818
+ return nil == angle ? FBResponseWithUnknownError(error) : FBResponseWithObject(angle);
819
+ }
820
+
821
+ + (id<FBResponsePayload>)handleSetSimulatedHingeAngle:(FBRouteRequest *)request
822
+ {
823
+ id angle = request.arguments[@"angle"];
824
+ if (![angle isKindOfClass:NSNumber.class]
825
+ || CFGetTypeID((__bridge CFTypeRef)angle) == CFBooleanGetTypeID()
826
+ || !isfinite([angle doubleValue]) || [angle doubleValue] < 0 || [angle doubleValue] > 180) {
827
+ return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:
828
+ @"'angle' must be a finite number between 0 and 180 degrees" traceback:nil]);
829
+ }
830
+ if (!XCUIDevice.sharedDevice.fb_canAttemptSimulatedHingeAngleInjection) {
831
+ return FBResponseWithStatus([FBCommandStatus unsupportedOperationErrorWithMessage:
832
+ @"Setting a simulated hinge angle requires a compatible foldable device" traceback:nil]);
833
+ }
834
+ NSError *error;
835
+ if (![XCUIDevice.sharedDevice fb_setSimulatedHingeAngle:[angle doubleValue] error:&error]) {
836
+ return FBResponseWithUnknownError(error);
837
+ }
838
+ return FBResponseWithOK();
839
+ }
840
+
780
841
  @end
@@ -15,11 +15,11 @@
15
15
  <key>CFBundlePackageType</key>
16
16
  <string>FMWK</string>
17
17
  <key>CFBundleShortVersionString</key>
18
- <string>16.13.6</string>
18
+ <string>16.14.1</string>
19
19
  <key>CFBundleSignature</key>
20
20
  <string>????</string>
21
21
  <key>CFBundleVersion</key>
22
- <string>16.13.6</string>
22
+ <string>16.14.1</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;