@devicelab/maestro-runner-darwin-arm64 1.1.29 → 1.1.31

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.
Files changed (118) hide show
  1. package/README.md +15 -4
  2. package/bin/maestro-runner +0 -0
  3. package/drivers/android/devicelab-android-driver-test.apk +0 -0
  4. package/drivers/android/devicelab-android-driver.apk +0 -0
  5. package/drivers/ios/DevicelabIOSRunner/DevicelabIOSRunnerUITests/ATTRIBUTION.md +16 -0
  6. package/drivers/ios/DevicelabIOSRunner/DevicelabIOSRunnerUITests/RunnerTests+Lifecycle.swift +33 -0
  7. package/drivers/ios/DevicelabIOSRunner/DevicelabIOSRunnerUITests/RunnerTests+Snapshot.swift +195 -88
  8. package/drivers/ios/DevicelabIOSRunner/DevicelabIOSRunnerUITests/RunnerTests.swift +3 -0
  9. package/drivers/ios/WebDriverAgent/CHANGELOG.md +202 -0
  10. package/drivers/ios/WebDriverAgent/README.md +18 -9
  11. package/drivers/ios/WebDriverAgent/Scripts/ci/build-real.sh +6 -10
  12. package/drivers/ios/WebDriverAgent/Scripts/fetch-prebuilt-wda.mjs +3 -2
  13. package/drivers/ios/WebDriverAgent/WebDriverAgent.xcodeproj/project.pbxproj +119 -0
  14. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.m +8 -0
  15. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h +3 -0
  16. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m +30 -1
  17. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.h +53 -0
  18. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.m +124 -0
  19. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h +6 -0
  20. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m +100 -23
  21. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBHinge.h +41 -0
  22. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBHinge.m +215 -0
  23. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.m +8 -3
  24. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBFind.m +20 -7
  25. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.m +110 -50
  26. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBTyping.h +7 -0
  27. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBTyping.m +20 -0
  28. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.m +14 -11
  29. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Commands/FBCustomCommands.m +86 -10
  30. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Commands/FBElementCommands.m +57 -7
  31. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Commands/FBSessionCommands.m +3 -2
  32. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Info.plist +2 -2
  33. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Routing/FBHTTPServer.m +39 -16
  34. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Routing/FBScreenRecordingRequest.m +7 -2
  35. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Routing/FBSession.h +22 -0
  36. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Routing/FBSession.m +78 -12
  37. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Routing/FBTCPSocket.h +5 -1
  38. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Routing/FBTCPSocket.m +10 -1
  39. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Routing/FBWebServer.m +4 -1
  40. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.h +2 -2
  41. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.m +20 -0
  42. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBConfiguration.h +8 -0
  43. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBConfiguration.m +2 -0
  44. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBHIDOperations.h +41 -0
  45. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBHIDOperations.m +244 -0
  46. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBImageProcessor.m +3 -1
  47. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBMathUtils.h +11 -0
  48. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBMathUtils.m +38 -0
  49. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBMjpegServer.m +5 -3
  50. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBScreen.h +17 -0
  51. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBScreen.m +76 -1
  52. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBScreenshot.h +2 -2
  53. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBScreenshot.m +6 -2
  54. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBSettings.h +2 -0
  55. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBSettings.m +2 -0
  56. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBSettingsHandler.m +48 -0
  57. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.m +121 -13
  58. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/FBXPath.m +181 -128
  59. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/LRUCache/LRUCache.m +8 -5
  60. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/Utilities/LRUCache/LRUCacheNode.h +1 -1
  61. package/drivers/ios/WebDriverAgent/WebDriverAgentLib/WebDriverAgentLib.h +1 -0
  62. package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationApp/COORDINATE_PROBE.md +3 -1
  63. package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/FBCoordinateProbeViewController.m +18 -0
  64. package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/SceneDelegate.m +33 -0
  65. package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationApp/Classes/ViewController.m +26 -7
  66. package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBConfigurationTests.m +52 -0
  67. package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBDisplayGestureTests.m +541 -0
  68. package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBElementSwipingTests.m +95 -0
  69. package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBImageProcessorTests.m +34 -0
  70. package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBScreenTests.m +392 -0
  71. package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBSessionIntegrationTests.m +97 -0
  72. package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBTypingTest.m +33 -0
  73. package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationTests/FBW3CTouchActionsIntegrationTests.m +126 -0
  74. package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIApplicationHelperTests.m +111 -0
  75. package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIDeviceHelperTests.m +97 -0
  76. package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIDeviceRotationTests.m +53 -0
  77. package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationTests/XCUIElementFBFindTests.m +113 -0
  78. package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationTests_tvOS/FBTVFocusIntegrationTests.m +21 -0
  79. package/drivers/ios/WebDriverAgent/WebDriverAgentTests/IntegrationTests_watchOS/WDAAppLifecycleIntegrationTests.swift +12 -0
  80. package/drivers/ios/WebDriverAgent/WebDriverAgentTests/UnitTests/FBCustomCommandsTests.m +61 -0
  81. package/drivers/ios/WebDriverAgent/WebDriverAgentTests/UnitTests/FBElementUtilitiesTests.m +40 -0
  82. package/drivers/ios/WebDriverAgent/WebDriverAgentTests/UnitTests/FBHTTPServerSessionTests.m +22 -4
  83. package/drivers/ios/WebDriverAgent/WebDriverAgentTests/UnitTests/FBHTTPServerTests.m +199 -5
  84. package/drivers/ios/WebDriverAgent/WebDriverAgentTests/UnitTests/FBLRUCacheTests.m +34 -0
  85. package/drivers/ios/WebDriverAgent/WebDriverAgentTests/UnitTests/FBMathUtilsTests.m +45 -0
  86. package/drivers/ios/WebDriverAgent/WebDriverAgentTests/UnitTests/FBSessionTests.m +201 -0
  87. package/drivers/ios/WebDriverAgent/WebDriverAgentTests/UnitTests/FBXPathTests.m +101 -0
  88. package/drivers/ios/WebDriverAgent/docs/hinge-angle.md +54 -0
  89. package/drivers/ios/WebDriverAgent/docs/multiple-displays.md +168 -0
  90. package/drivers/ios/WebDriverAgent/lib/webdriveragent.ts +1 -2
  91. package/drivers/ios/WebDriverAgent/lib/xcodebuild.ts +58 -10
  92. package/drivers/ios/WebDriverAgent/package.json +8 -15
  93. package/drivers/ios/WebDriverAgent/test/functional/helpers/display-settings.ts +49 -0
  94. package/drivers/ios/WebDriverAgent/test/functional/helpers/simulator.ts +7 -7
  95. package/drivers/ios/WebDriverAgent/test/functional/webdriveragent-e2e.spec.ts +4 -0
  96. package/drivers/ios/WebDriverAgent/test/unit/xcodebuild.spec.ts +230 -0
  97. package/drivers/ios/WebDriverAgent/tsconfig.json +2 -1
  98. package/drivers/ios/devicelab-ios-agent/README.md +12 -0
  99. package/drivers/ios/devicelab-ios-agent/device/DevicelabIOSAgent.app/DevicelabIOSAgent +0 -0
  100. package/drivers/ios/devicelab-ios-agent/device/DevicelabIOSAgentUITests-Runner.app/PlugIns/DevicelabIOSAgentUITests.xctest/DevicelabIOSAgentUITests +0 -0
  101. package/drivers/ios/devicelab-ios-agent/device/manifest.json +3 -3
  102. package/drivers/ios/devicelab-ios-agent/simulator/DevicelabIOSAgent.app/DevicelabIOSAgent +0 -0
  103. package/drivers/ios/devicelab-ios-agent/simulator/DevicelabIOSAgentUITests-Runner.app/PlugIns/DevicelabIOSAgentUITests.xctest/DevicelabIOSAgentUITests +0 -0
  104. package/drivers/ios/devicelab-ios-agent/simulator/manifest.json +3 -3
  105. package/package.json +1 -1
  106. package/drivers/ios/WebDriverAgent/.editorconfig +0 -11
  107. package/drivers/ios/WebDriverAgent/.github/ISSUE_TEMPLATE.md +0 -4
  108. package/drivers/ios/WebDriverAgent/.github/actions/xcode-setup/action.yml +0 -32
  109. package/drivers/ios/WebDriverAgent/.github/dependabot.yml +0 -22
  110. package/drivers/ios/WebDriverAgent/.github/workflows/functional-test.yml +0 -103
  111. package/drivers/ios/WebDriverAgent/.github/workflows/pr-title.yml +0 -10
  112. package/drivers/ios/WebDriverAgent/.github/workflows/publish.js.yml +0 -112
  113. package/drivers/ios/WebDriverAgent/.github/workflows/unit-test.yml +0 -32
  114. package/drivers/ios/WebDriverAgent/.github/workflows/wda-package.yml +0 -114
  115. package/drivers/ios/WebDriverAgent/.github/workflows/wda-tests.yml +0 -246
  116. package/drivers/ios/WebDriverAgent/oxfmt.config.mjs +0 -6
  117. package/drivers/ios/WebDriverAgent/oxlint.config.mjs +0 -6
  118. package/drivers/ios/WebDriverAgent/release.config.mjs +0 -14
@@ -9,17 +9,67 @@
9
9
 
10
10
  #import "XCUIApplication+FBTouchAction.h"
11
11
 
12
+ #import <dlfcn.h>
13
+
12
14
  #import "FBBaseActionsSynthesizer.h"
13
15
  #import "FBConfiguration.h"
16
+ #import "FBErrorBuilder.h"
14
17
  #import "FBExceptions.h"
15
18
  #import "FBLogger.h"
19
+ #import "FBMathUtils.h"
16
20
  #import "FBRunLoopSpinner.h"
17
21
  #import "FBW3CActionsSynthesizer.h"
18
22
  #import "FBXCTestDaemonsProxy.h"
23
+ #import "XCPointerEventPath.h"
24
+ #import "XCSynthesizedEventRecord.h"
25
+ #import "XCUIElement.h"
19
26
  #import "XCUIElement+FBUtilities.h"
20
27
 
21
28
  #if !TARGET_OS_TV && !TARGET_OS_WATCH
22
29
 
30
+ /**
31
+ Maps a drag XCUIGestureVelocity to the speed XCTest drags at. XCTest calls the result "pixels per
32
+ second", but divides the distance between the two screen points (which are in points) by it.
33
+ Prefers XCTest's own (exported, but undeclared) mapping function, so any future change to its
34
+ presets is picked up; falls back to the values it returns now.
35
+
36
+ @throws FBInvalidArgumentException if the velocity is neither one of the XCUIGestureVelocity
37
+ presets nor a positive finite number
38
+ */
39
+ static CGFloat FBPointsPerSecondForDragVelocity(XCUIGestureVelocity velocity)
40
+ {
41
+ BOOL isDefault = FBFloatFuzzyEqualToFloat(velocity, XCUIGestureVelocityDefault, 0);
42
+ BOOL isSlow = FBFloatFuzzyEqualToFloat(velocity, XCUIGestureVelocitySlow, 0);
43
+ BOOL isFast = FBFloatFuzzyEqualToFloat(velocity, XCUIGestureVelocityFast, 0);
44
+ if (!isDefault && !isSlow && !isFast && !(isfinite(velocity) && velocity > 0)) {
45
+ NSString *reason = [NSString stringWithFormat:@"%@ is an invalid drag velocity. It must be greater than 0", @(velocity)];
46
+ @throw [NSException exceptionWithName:FBInvalidArgumentException reason:reason userInfo:nil];
47
+ }
48
+ static double (*xctestMapping)(double) = NULL;
49
+ static dispatch_once_t onceToken;
50
+ dispatch_once(&onceToken, ^{
51
+ xctestMapping = (double (*)(double))dlsym(RTLD_DEFAULT, "XCUIPixelsPerSecondForDragGestureVelocity");
52
+ if (NULL == xctestMapping) {
53
+ [FBLogger log:@"Could not find XCUIPixelsPerSecondForDragGestureVelocity. Using built-in drag velocity presets instead"];
54
+ }
55
+ });
56
+ if (NULL != xctestMapping) {
57
+ return (CGFloat)xctestMapping(velocity);
58
+ }
59
+ if (isSlow) {
60
+ return 250;
61
+ }
62
+ if (isFast) {
63
+ return 750;
64
+ }
65
+ return isDefault ? 500 : velocity;
66
+ }
67
+
68
+ static BOOL FBIsFinitePoint(CGPoint point)
69
+ {
70
+ return isfinite(point.x) && isfinite(point.y);
71
+ }
72
+
23
73
  @implementation XCUIApplication (FBTouchAction)
24
74
 
25
75
  + (BOOL)handleEventSynthesWithError:(NSError *)error
@@ -65,6 +115,80 @@
65
115
  return YES;
66
116
  }
67
117
 
118
+ - (BOOL)fb_pressAtCoordinate:(XCUICoordinate *)startCoordinate
119
+ forDuration:(NSTimeInterval)pressDuration
120
+ thenDragToCoordinate:(XCUICoordinate *)endCoordinate
121
+ withVelocity:(XCUIGestureVelocity)velocity
122
+ thenHoldForDuration:(NSTimeInterval)holdDuration
123
+ error:(NSError **)error
124
+ {
125
+ CGFloat pointsPerSecond = FBPointsPerSecondForDragVelocity(velocity);
126
+ if (!isfinite(pressDuration) || pressDuration < 0 || !isfinite(holdDuration) || holdDuration < 0) {
127
+ NSString *reason = [NSString stringWithFormat:@"Drag press and hold durations must be non-negative numbers of seconds. Got %@ and %@", @(pressDuration), @(holdDuration)];
128
+ @throw [NSException exceptionWithName:FBInvalidArgumentException reason:reason userInfo:nil];
129
+ }
130
+
131
+ // Resolved once, here, so the points validated below are exactly the ones sent. XCTest's own
132
+ // drag re-resolves them inside its event dispatch instead, after re-snapshotting the application.
133
+ CGPoint startPoint = startCoordinate.screenPoint;
134
+ CGPoint endPoint = endCoordinate.screenPoint;
135
+ if (!FBIsFinitePoint(startPoint) || !FBIsFinitePoint(endPoint)) {
136
+ return [[[FBErrorBuilder builder]
137
+ withDescriptionFormat:@"Cannot resolve the on-screen position of the drag gesture (from %@ to %@). "
138
+ "This usually means '%@' did not deliver a valid accessibility snapshot, e.g. because it is busy "
139
+ "or still transitioning after an application switch. Make sure it is idle and retry",
140
+ NSStringFromCGPoint(startPoint), NSStringFromCGPoint(endPoint), self.bundleID]
141
+ buildError:error];
142
+ }
143
+
144
+ // Like XCTest, target the display of the element the start coordinate belongs to. displayID
145
+ // only reads the element's last snapshot, which resolving screenPoint above has just refreshed.
146
+ XCUIElement *referencedElement = startCoordinate.referencedElement;
147
+ long long displayID = [referencedElement respondsToSelector:@selector(displayID)]
148
+ ? referencedElement.displayID
149
+ : 0;
150
+ XCSynthesizedEventRecord *event = [self.class fb_pressDragEventFromPoint:startPoint
151
+ forDuration:pressDuration
152
+ toPoint:endPoint
153
+ pointsPerSecond:pointsPerSecond
154
+ thenHoldForDuration:holdDuration
155
+ displayID:displayID
156
+ interfaceOrientation:self.interfaceOrientation];
157
+ if (![self fb_synthesizeEvent:event error:error]) {
158
+ return NO;
159
+ }
160
+ [self fb_waitUntilStableWithTimeout:FBConfiguration.sharedInstance.animationCoolOffTimeout];
161
+ return YES;
162
+ }
163
+
164
+ + (XCSynthesizedEventRecord *)fb_pressDragEventFromPoint:(CGPoint)startPoint
165
+ forDuration:(NSTimeInterval)pressDuration
166
+ toPoint:(CGPoint)endPoint
167
+ pointsPerSecond:(CGFloat)pointsPerSecond
168
+ thenHoldForDuration:(NSTimeInterval)holdDuration
169
+ displayID:(long long)displayID
170
+ interfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
171
+ {
172
+ // Mirrors XCTest's own press-hold-drag event builder
173
+ XCPointerEventPath *path = [[XCPointerEventPath alloc] initForTouchAtPoint:startPoint offset:0];
174
+ if (pressDuration > 0) {
175
+ [path moveToPoint:startPoint atOffset:pressDuration];
176
+ }
177
+ CGFloat distance = (CGFloat)hypot(endPoint.x - startPoint.x, endPoint.y - startPoint.y);
178
+ NSTimeInterval dragEndOffset = pressDuration + distance / pointsPerSecond;
179
+ [path moveToPoint:endPoint atOffset:dragEndOffset];
180
+ [path liftUpAtOffset:dragEndOffset + holdDuration];
181
+
182
+ NSString *name = @"Press, drag and hold";
183
+ XCSynthesizedEventRecord *event = [XCSynthesizedEventRecord instancesRespondToSelector:@selector(initWithName:displayID:interfaceOrientation:)]
184
+ ? [[XCSynthesizedEventRecord alloc] initWithName:name
185
+ displayID:(unsigned long long)displayID
186
+ interfaceOrientation:interfaceOrientation]
187
+ : [[XCSynthesizedEventRecord alloc] initWithName:name interfaceOrientation:interfaceOrientation];
188
+ [event addPointerEventPath:path];
189
+ return event;
190
+ }
191
+
68
192
  - (BOOL)fb_synthesizeEvent:(XCSynthesizedEventRecord *)event error:(NSError *__autoreleasing*)error
69
193
  {
70
194
  return [FBXCTestDaemonsProxy synthesizeEventWithRecord:event error:error];
@@ -233,4 +233,10 @@ typedef NS_ENUM(NSUInteger, FBUIInterfaceAppearance) {
233
233
 
234
234
  @end
235
235
 
236
+ /**
237
+ Returns the map of lowercase button names to their XCTest enum values
238
+ supported on the current platform (cached after the first call).
239
+ */
240
+ extern NSDictionary<NSString *, NSNumber *> *FBAvailableButtonNames(void);
241
+
236
242
  NS_ASSUME_NONNULL_END
@@ -13,6 +13,7 @@
13
13
  #include <notify.h>
14
14
  #import <objc/runtime.h>
15
15
 
16
+ #import "FBHIDOperations.h"
16
17
  #import "FBErrorBuilder.h"
17
18
  #import "FBImageUtils.h"
18
19
  #import "FBMacros.h"
@@ -22,12 +23,17 @@
22
23
  #import "FBXCodeCompatibility.h"
23
24
  #import "FBXCTestDaemonsProxy.h"
24
25
  #import "XCUIDevice.h"
26
+ #import "XCUIApplication.h"
25
27
 
26
28
  static const NSTimeInterval FBHomeButtonCoolOffTime = 1.;
27
29
  static const NSTimeInterval FBScreenLockTimeout = 5.;
28
30
 
31
+ #if TARGET_OS_WATCH
32
+ static NSString *const FBClockFaceBundleId = @"com.apple.clockface";
33
+ #endif
34
+
29
35
  #if TARGET_OS_TV
30
- NSDictionary<NSString *, NSNumber *> *fb_availableButtonNames(void) {
36
+ NSDictionary<NSString *, NSNumber *> *FBAvailableButtonNames(void) {
31
37
  static dispatch_once_t onceToken;
32
38
  static NSDictionary *result;
33
39
  dispatch_once(&onceToken, ^{
@@ -58,7 +64,7 @@ NSDictionary<NSString *, NSNumber *> *fb_availableButtonNames(void) {
58
64
  return result;
59
65
  }
60
66
  #else
61
- NSDictionary<NSString *, NSNumber *> *fb_availableButtonNames(void) {
67
+ NSDictionary<NSString *, NSNumber *> *FBAvailableButtonNames(void) {
62
68
  static dispatch_once_t onceToken;
63
69
  static NSDictionary *result;
64
70
  dispatch_once(&onceToken, ^{
@@ -136,29 +142,90 @@ static bool fb_isLocked;
136
142
 
137
143
  - (BOOL)fb_goToHomescreenWithError:(NSError **)error
138
144
  {
145
+ #if TARGET_OS_TV
146
+ return [self fb_goToHomescreenOnTVWithError:error];
147
+ #elif TARGET_OS_WATCH
148
+ return [self fb_goToHomescreenOnWatchWithError:error];
149
+ #else
139
150
  return [XCUIApplication fb_switchToSystemApplicationWithError:error];
151
+ #endif
140
152
  }
141
153
 
154
+ #if TARGET_OS_WATCH
155
+ - (BOOL)fb_goToHomescreenOnWatchWithError:(NSError **)error
156
+ {
157
+ // Carousel rejects launch/activate requests on watchOS 27.0.
158
+ XCUIApplication *foregroundApp = XCUIApplication.fb_activeApplication;
159
+ // The watch face is a separate foreground application from Carousel.
160
+ if ([foregroundApp.bundleID isEqualToString:FBClockFaceBundleId]
161
+ || [foregroundApp fb_isSameAppAs:XCUIApplication.fb_systemApplication]) {
162
+ return YES;
163
+ }
164
+ @try {
165
+ [self pressButton:XCUIDeviceButtonHome];
166
+ } @catch (NSException *exception) {
167
+ return [[[FBErrorBuilder alloc] withDescription:exception.reason] buildError:error];
168
+ }
169
+ return [[[[FBRunLoopSpinner new] timeout:5]
170
+ timeoutErrorMessage:@"Timed out while waiting for the home screen"]
171
+ spinUntilTrue:^BOOL{
172
+ return foregroundApp.state != XCUIApplicationStateRunningForeground;
173
+ } error:error];
174
+ }
175
+ #endif
176
+
177
+ #if TARGET_OS_TV
178
+ - (BOOL)fb_goToHomescreenOnTVWithError:(NSError **)error
179
+ {
180
+ // PineBoard rejects launch/activate requests on tvOS 26.5 and 27.0.
181
+ XCUIApplication *foregroundApp = XCUIApplication.fb_activeApplication;
182
+ if ([foregroundApp fb_isSameAppAs:XCUIApplication.fb_systemApplication]) {
183
+ return YES;
184
+ }
185
+ @try {
186
+ [XCUIRemote.sharedRemote pressButton:XCUIRemoteButtonHome];
187
+ } @catch (NSException *exception) {
188
+ return [[[FBErrorBuilder alloc] withDescription:exception.reason] buildError:error];
189
+ }
190
+ return [[[[FBRunLoopSpinner new] timeout:5]
191
+ timeoutErrorMessage:@"Timed out while waiting for the home screen"]
192
+ spinUntilTrue:^BOOL{
193
+ return foregroundApp.state != XCUIApplicationStateRunningForeground;
194
+ } error:error];
195
+ }
196
+ #endif
197
+
142
198
  - (BOOL)fb_lockScreen:(NSError **)error
143
199
  {
144
200
  if (fb_isLocked) {
145
201
  return YES;
146
202
  }
147
- #if TARGET_OS_SIMULATOR
148
- [self pressLockButton];
149
- #else
150
- if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"27.0")) {
151
- // iOS 27: pressLockButton no longer locks; use a direct IOHID Power press (0x0C/0x30, ~0.5s hold).
152
- if (![self fb_performIOHIDEventWithPage:0x0C
153
- usage:0x30
154
- duration:0.5
155
- error:error]) {
203
+ #if !TARGET_OS_TV && !TARGET_OS_WATCH
204
+ if ([FBHIDOperations appliesToDevice:self]) {
205
+ if (![FBHIDOperations pressLockWithError:error]) {
156
206
  return NO;
157
207
  }
158
- } else {
208
+ } else
209
+ #endif
210
+ {
211
+ #if TARGET_OS_SIMULATOR
159
212
  [self pressLockButton];
160
- }
213
+ #else
214
+ if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"27.0")) {
215
+ // iOS 27: pressLockButton no longer locks; use a short IOHID Power press.
216
+ // A 0.5s hold can invoke Siri instead of locking the screen.
217
+ const NSTimeInterval duration = 0.2;
218
+ if (![self fb_performIOHIDEventWithPage:0x0C
219
+ usage:0x30
220
+ duration:duration
221
+ error:error]) {
222
+ return NO;
223
+ }
224
+ } else {
225
+ [self pressLockButton];
226
+ }
161
227
  #endif
228
+ }
162
229
  return [[[[FBRunLoopSpinner new]
163
230
  timeout:FBScreenLockTimeout]
164
231
  timeoutErrorMessage:@"Timed out while waiting until the screen gets locked"]
@@ -177,14 +244,19 @@ static bool fb_isLocked;
177
244
  if (!fb_isLocked) {
178
245
  return YES;
179
246
  }
180
- [self pressButton:XCUIDeviceButtonHome];
181
- [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:FBHomeButtonCoolOffTime]];
182
- #if !TARGET_OS_TV
183
- [self pressButton:XCUIDeviceButtonHome];
184
- #else
185
- [self pressButton:XCUIDeviceButtonHome];
247
+ for (NSUInteger attempt = 0; attempt < 2; attempt++) {
248
+ #if !TARGET_OS_TV && !TARGET_OS_WATCH
249
+ if ([FBHIDOperations appliesToDevice:self]) {
250
+ if (![FBHIDOperations pressHomeWithError:error]) {
251
+ return NO;
252
+ }
253
+ } else
186
254
  #endif
187
- [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:FBHomeButtonCoolOffTime]];
255
+ {
256
+ [self pressButton:XCUIDeviceButtonHome];
257
+ }
258
+ [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:FBHomeButtonCoolOffTime]];
259
+ }
188
260
  return [[[[FBRunLoopSpinner new]
189
261
  timeout:FBScreenLockTimeout]
190
262
  timeoutErrorMessage:@"Timed out while waiting until the screen gets unlocked"]
@@ -307,7 +379,7 @@ static bool fb_isLocked;
307
379
 
308
380
  - (BOOL)fb_hasButton:(NSString *)buttonName
309
381
  {
310
- return fb_availableButtonNames()[buttonName.lowercaseString] != nil;
382
+ return FBAvailableButtonNames()[buttonName.lowercaseString] != nil;
311
383
  }
312
384
 
313
385
  - (BOOL)fb_pressButton:(NSString *)buttonName
@@ -318,7 +390,7 @@ static bool fb_isLocked;
318
390
  return [self fb_pressButton:buttonName error:error];
319
391
  #else
320
392
 
321
- NSDictionary<NSString *, NSNumber *> *availableButtons = fb_availableButtonNames();
393
+ NSDictionary<NSString *, NSNumber *> *availableButtons = FBAvailableButtonNames();
322
394
  NSNumber *buttonValue = availableButtons[buttonName.lowercaseString];
323
395
 
324
396
  if (!buttonValue) {
@@ -342,7 +414,7 @@ static bool fb_isLocked;
342
414
  - (BOOL)fb_pressButton:(NSString *)buttonName
343
415
  error:(NSError **)error
344
416
  {
345
- NSDictionary<NSString *, NSNumber *> *availableButtons = fb_availableButtonNames();
417
+ NSDictionary<NSString *, NSNumber *> *availableButtons = FBAvailableButtonNames();
346
418
  NSNumber *buttonValue = availableButtons[buttonName.lowercaseString];
347
419
 
348
420
  if (!buttonValue) {
@@ -351,6 +423,11 @@ static bool fb_isLocked;
351
423
  withDescriptionFormat:@"The button '%@' is not supported. The device under test only supports the following buttons: %@", buttonName, sortedKeys]
352
424
  buildError:error];
353
425
  }
426
+ #if !TARGET_OS_TV && !TARGET_OS_WATCH
427
+ if (buttonValue.unsignedIntegerValue == XCUIDeviceButtonHome && [FBHIDOperations appliesToDevice:self]) {
428
+ return [FBHIDOperations pressHomeWithError:error];
429
+ }
430
+ #endif
354
431
  [self pressButton:(XCUIDeviceButton)[buttonValue unsignedIntegerValue]];
355
432
  return YES;
356
433
  }
@@ -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
@@ -9,16 +9,23 @@
9
9
  #import "XCUIDevice+FBRotation.h"
10
10
 
11
11
  #import "FBConfiguration.h"
12
+ #import "FBHIDOperations.h"
12
13
  #import "XCUIApplication.h"
13
14
  #import "XCUIApplication+FBHelpers.h"
14
15
  #import "XCUIElement+FBUtilities.h"
15
16
 
16
17
  # if !TARGET_OS_TV && !TARGET_OS_WATCH
17
18
 
19
+ static UIInterfaceOrientation FBInterfaceOrientationFromDeviceOrientation(UIDeviceOrientation orientation);
20
+
18
21
  @implementation XCUIDevice (FBRotation)
19
22
 
20
23
  - (BOOL)fb_setDeviceInterfaceOrientation:(UIDeviceOrientation)orientation
21
24
  {
25
+ if ([FBHIDOperations appliesToDevice:self]) {
26
+ return [FBHIDOperations rotateToOrientation:orientation
27
+ interfaceOrientation:FBInterfaceOrientationFromDeviceOrientation(orientation)];
28
+ }
22
29
  XCUIApplication *application = XCUIApplication.fb_activeApplication;
23
30
  [XCUIDevice sharedDevice].orientation = orientation;
24
31
  return [self waitUntilInterfaceIsAtOrientation:orientation application:application];
@@ -31,9 +38,7 @@
31
38
  return NO;
32
39
  }
33
40
  UIDeviceOrientation orientation = (UIDeviceOrientation)keysForRotationObj.firstObject.integerValue;
34
- XCUIApplication *application = XCUIApplication.fb_activeApplication;
35
- [XCUIDevice sharedDevice].orientation = orientation;
36
- return [self waitUntilInterfaceIsAtOrientation:orientation application:application];
41
+ return [self fb_setDeviceInterfaceOrientation:orientation];
37
42
  }
38
43
 
39
44
  static UIInterfaceOrientation FBInterfaceOrientationFromDeviceOrientation(UIDeviceOrientation orientation)