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

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
@@ -120,14 +120,27 @@
120
120
  - (NSArray<XCUIElement *> *)fb_descendantsMatchingIdentifier:(NSString *)accessibilityId
121
121
  shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch
122
122
  {
123
- NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id<FBXCElementSnapshot> snapshot,
124
- NSDictionary<NSString *,id> * _Nullable bindings) {
125
- @autoreleasepool {
126
- return [[FBXCElementSnapshotWrapper wdNameWithSnapshot:snapshot] isEqualToString:accessibilityId];
123
+ // Standard snapshot attributes let XCTest evaluate a structured predicate
124
+ // without WDA's per-snapshot block callback, while preserving wdName semantics.
125
+ // Local benchmarks showed lower end-to-end lookup latency; see measurements
126
+ // and regression checks in https://github.com/appium/WebDriverAgent/pull/1286.
127
+ NSPredicate *predicate = [NSPredicate predicateWithFormat:
128
+ @"(identifier != nil AND identifier != '' AND identifier == %@) OR "
129
+ "((identifier == nil OR identifier == '') AND label != nil AND label != '' AND label == %@)",
130
+ accessibilityId, accessibilityId];
131
+ XCUIElementQuery *query = [[self.fb_query descendantsMatchingType:XCUIElementTypeAny]
132
+ matchingPredicate:predicate];
133
+ NSMutableArray<XCUIElement *> *result = [NSMutableArray arrayWithArray:
134
+ [self.class fb_extractMatchingElementsFromQuery:query
135
+ shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch]];
136
+ id<FBXCElementSnapshot> cachedSnapshot = [self fb_cachedSnapshotWithQuery:query];
137
+ if ([predicate evaluateWithObject:cachedSnapshot]) {
138
+ if (shouldReturnAfterFirstMatch || result.count == 0) {
139
+ return @[self];
127
140
  }
128
- }];
129
- return [self fb_descendantsMatchingPredicate:predicate
130
- shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch];
141
+ [result insertObject:self atIndex:0];
142
+ }
143
+ return result.copy;
131
144
  }
132
145
 
133
146
  @end
@@ -9,7 +9,9 @@
9
9
  #import "XCUIElement+FBScrolling.h"
10
10
 
11
11
  #import "FBErrorBuilder.h"
12
+ #import "FBExceptions.h"
12
13
  #import "FBLogger.h"
14
+ #import "FBScreen.h"
13
15
  #import "FBMacros.h"
14
16
  #import "FBMathUtils.h"
15
17
  #import "FBXCodeCompatibility.h"
@@ -20,6 +22,7 @@
20
22
  #import "XCUIElement+FBUID.h"
21
23
  #import "XCUIApplication.h"
22
24
  #import "XCUICoordinate.h"
25
+ #import "XCUIScreen.h"
23
26
  #import "XCUIElement+FBIsVisible.h"
24
27
  #import "XCUIElement+FBVisibleFrame.h"
25
28
  #import "XCUIElement.h"
@@ -35,14 +38,40 @@ const CGFloat FBScrollTouchProportion = 0.75f;
35
38
 
36
39
  #if !TARGET_OS_TV
37
40
 
41
+ static void FBValidateScreenForScrolling(void)
42
+ {
43
+ NSError *error = nil;
44
+ XCUIScreen *screen = [FBScreen currentScreenWithError:&error];
45
+ if (nil == screen) {
46
+ // Match active-app detection: never continue on a different coordinate space
47
+ // when the selected display is unavailable, including for void scroll APIs.
48
+ @throw [NSException exceptionWithName:FBInvalidArgumentException
49
+ reason:error.localizedDescription
50
+ userInfo:nil];
51
+ }
52
+ }
53
+
54
+ static CGRect FBScrollFrameInApplication(CGRect frame, XCUIScreen *screen,
55
+ XCUIApplication *application)
56
+ {
57
+ #if !TARGET_OS_WATCH
58
+ // XCTest's visibleFrame is in portrait screen coordinates on both main and
59
+ // secondary displays. The live anchor's frame uses the app's orientation.
60
+ CGSize size = CGSizeMake(CGRectGetWidth(screen.bounds) / screen.scale,
61
+ CGRectGetHeight(screen.bounds) / screen.scale);
62
+ frame = FBRectFromPortraitCoordinates(frame, size, application.interfaceOrientation);
63
+ #endif
64
+ return frame;
65
+ }
66
+
38
67
  @interface FBXCElementSnapshotWrapper (FBScrolling)
39
68
 
40
69
  - (BOOL)fb_scrollUpByNormalizedDistance:(CGFloat)distance anchorElement:(XCUIElement *)anchorElement;
41
70
  - (BOOL)fb_scrollDownByNormalizedDistance:(CGFloat)distance anchorElement:(XCUIElement *)anchorElement;
42
71
  - (BOOL)fb_scrollLeftByNormalizedDistance:(CGFloat)distance anchorElement:(XCUIElement *)anchorElement;
43
72
  - (BOOL)fb_scrollRightByNormalizedDistance:(CGFloat)distance anchorElement:(XCUIElement *)anchorElement;
44
- - (BOOL)fb_scrollByNormalizedVector:(CGVector)normalizedScrollVector anchorElement:(XCUIElement *)anchorElement;
45
- - (BOOL)fb_scrollByVector:(CGVector)vector anchorElement:(XCUIElement *)anchorElement error:(NSError **)error;
73
+ - (BOOL)fb_scrollByNormalizedVector:(CGVector)normalizedScrollVector anchorElement:(XCUIElement *)anchorElement error:(NSError **)error;
74
+ - (BOOL)fb_scrollByVector:(CGVector)vector inFrame:(CGRect)frame anchorElement:(XCUIElement *)anchorElement velocity:(CGFloat)velocity error:(NSError **)error;
46
75
 
47
76
  @end
48
77
 
@@ -126,6 +155,9 @@ static XCUIElement *FBLiveElementForSnapshot(id<FBXCElementSnapshot> snapshot, X
126
155
  scrollDirection:(FBXCUIElementScrollDirection)scrollDirection
127
156
  error:(NSError **)error
128
157
  {
158
+ if (nil == [FBScreen currentScreenWithError:error]) {
159
+ return NO;
160
+ }
129
161
  FBXCElementSnapshotWrapper *prescrollSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:[self fb_customSnapshot]];
130
162
 
131
163
  if (prescrollSnapshot.isWDVisible) {
@@ -202,7 +234,7 @@ static XCUIElement *FBLiveElementForSnapshot(id<FBXCElementSnapshot> snapshot, X
202
234
 
203
235
  // The scroll view's own identity is stable across scroll steps, so it only needs
204
236
  // to be resolved to a live element once, up front; its frame does not, since it can
205
- // change across scroll steps (rotation, keyboard, dynamic layout).
237
+ // change across scroll steps (keyboard or dynamic layout).
206
238
  XCUIElement *scrollViewElement = FBLiveElementForSnapshot(scrollView, self.application);
207
239
  if (nil == scrollViewElement) {
208
240
  return
@@ -217,34 +249,27 @@ static XCUIElement *FBLiveElementForSnapshot(id<FBXCElementSnapshot> snapshot, X
217
249
  // Scrolling till cell is visible and get current value of frames
218
250
  while (![self fb_isEquivalentElementSnapshotVisible:prescrollSnapshot] && scrollCount < maxScrollCount) {
219
251
  BOOL didScroll;
252
+ __strong NSError *scrollError = nil;
220
253
  @autoreleasepool {
221
- // Re-snapshotting the scroll view every step keeps its frame from drifting too far
222
- // out of sync with the live anchor element's frame used to resolve touch points.
223
254
  scrollViewWrapped = [FBXCElementSnapshotWrapper ensureWrapped:[scrollViewElement fb_customSnapshot]];
224
- if (targetCellIndex < visibleCellIndex) {
225
- didScroll = scrollDirection == FBXCUIElementScrollDirectionVertical ?
226
- [scrollViewWrapped fb_scrollUpByNormalizedDistance:normalizedScrollDistance
227
- anchorElement:scrollViewElement] :
228
- [scrollViewWrapped fb_scrollLeftByNormalizedDistance:normalizedScrollDistance
229
- anchorElement:scrollViewElement];
230
- }
231
- else {
232
- didScroll = scrollDirection == FBXCUIElementScrollDirectionVertical ?
233
- [scrollViewWrapped fb_scrollDownByNormalizedDistance:normalizedScrollDistance
234
- anchorElement:scrollViewElement] :
235
- [scrollViewWrapped fb_scrollRightByNormalizedDistance:normalizedScrollDistance
236
- anchorElement:scrollViewElement];
237
- }
255
+ CGFloat distance = targetCellIndex < visibleCellIndex ? normalizedScrollDistance : -normalizedScrollDistance;
256
+ CGVector vector = scrollDirection == FBXCUIElementScrollDirectionVertical
257
+ ? CGVectorMake(0, distance) : CGVectorMake(distance, 0);
258
+ NSError *stepError = nil;
259
+ didScroll = [scrollViewWrapped fb_scrollByNormalizedVector:vector
260
+ anchorElement:scrollViewElement
261
+ error:&stepError];
262
+ scrollError = stepError;
238
263
  scrollCount++;
239
264
  // Wait for scroll animation
240
265
  [self fb_waitUntilStableWithTimeout:FBConfiguration.sharedInstance.animationCoolOffTimeout];
241
266
  }
242
267
  // The `error` out-param must not be written from inside the autorelease pool above.
243
268
  if (!didScroll) {
244
- return
245
- [[[FBErrorBuilder builder]
246
- withDescriptionFormat:@"Failed to scroll '%@': its frame is empty", self.description]
247
- buildError:error];
269
+ if (error) {
270
+ *error = scrollError;
271
+ }
272
+ return NO;
248
273
  }
249
274
  }
250
275
 
@@ -260,14 +285,39 @@ static XCUIElement *FBLiveElementForSnapshot(id<FBXCElementSnapshot> snapshot, X
260
285
  // Trying fb_cachedSnapshot first
261
286
  FBXCElementSnapshotWrapper *targetCellSnapshotWrapped = [FBXCElementSnapshotWrapper ensureWrapped:[self fb_customSnapshot]];
262
287
  targetCellSnapshot = [targetCellSnapshotWrapped fb_parentCellSnapshot];
263
- CGRect visibleFrame = [FBXCElementSnapshotWrapper ensureWrapped:targetCellSnapshot].fb_visibleFrame;
264
-
265
- CGVector scrollVector = CGVectorMake(visibleFrame.size.width - targetCellSnapshot.frame.size.width,
266
- visibleFrame.size.height - targetCellSnapshot.frame.size.height
267
- );
288
+ XCUIScreen *screen = [FBScreen currentScreenWithError:error];
289
+ if (nil == screen) {
290
+ return NO;
291
+ }
292
+ CGRect visibleFrame = FBScrollFrameInApplication(
293
+ [FBXCElementSnapshotWrapper ensureWrapped:targetCellSnapshot].fb_visibleFrame, screen, self.application);
294
+
295
+ CGRect targetFrame = targetCellSnapshot.frame;
296
+ // A cell can be clipped at the leading edge after a drag. Move back toward
297
+ // that edge instead of always scrolling down/right and hiding it further.
298
+ CGVector scrollVector = CGVectorMake(
299
+ CGRectGetMinX(visibleFrame) > CGRectGetMinX(targetFrame)
300
+ ? CGRectGetMinX(visibleFrame) - CGRectGetMinX(targetFrame)
301
+ : CGRectGetMaxX(visibleFrame) - CGRectGetMaxX(targetFrame),
302
+ CGRectGetMinY(visibleFrame) > CGRectGetMinY(targetFrame)
303
+ ? CGRectGetMinY(visibleFrame) - CGRectGetMinY(targetFrame)
304
+ : CGRectGetMaxY(visibleFrame) - CGRectGetMaxY(targetFrame));
305
+ // UIKit consumes the start of a drag before recognizing a pan. Include that
306
+ // margin so a small clipped edge is actually moved into view.
307
+ if (fabs(scrollVector.dx) > 1) {
308
+ scrollVector.dx += copysign(FBFuzzyPointThreshold, scrollVector.dx);
309
+ }
310
+ if (fabs(scrollVector.dy) > 1) {
311
+ scrollVector.dy += copysign(FBFuzzyPointThreshold, scrollVector.dy);
312
+ }
313
+ // Short fast corrections can turn into a flick and overshoot the target.
314
+ CGFloat velocity = MIN(FBTouchVelocity, MAX(fabs(scrollVector.dx), fabs(scrollVector.dy)) / FBTouchEventDelay);
268
315
  scrollViewWrapped = [FBXCElementSnapshotWrapper ensureWrapped:[scrollViewElement fb_customSnapshot]];
316
+ CGRect scrollingFrame = FBScrollFrameInApplication(scrollViewWrapped.visibleFrame, screen, self.application);
269
317
  return [scrollViewWrapped fb_scrollByVector:scrollVector
318
+ inFrame:scrollingFrame
270
319
  anchorElement:scrollViewElement
320
+ velocity:velocity
271
321
  error:error];
272
322
  }
273
323
 
@@ -296,64 +346,73 @@ static XCUIElement *FBLiveElementForSnapshot(id<FBXCElementSnapshot> snapshot, X
296
346
 
297
347
  @implementation FBXCElementSnapshotWrapper (FBScrolling)
298
348
 
299
- - (CGRect)scrollingFrame
300
- {
301
- return self.visibleFrame;
302
- }
303
-
304
349
  - (BOOL)fb_scrollUpByNormalizedDistance:(CGFloat)distance
305
350
  anchorElement:(XCUIElement *)anchorElement
306
351
  {
307
- return [self fb_scrollByNormalizedVector:CGVectorMake(0.0, distance) anchorElement:anchorElement];
352
+ FBValidateScreenForScrolling();
353
+ return [self fb_scrollByNormalizedVector:CGVectorMake(0.0, distance) anchorElement:anchorElement error:nil];
308
354
  }
309
355
 
310
356
  - (BOOL)fb_scrollDownByNormalizedDistance:(CGFloat)distance
311
357
  anchorElement:(XCUIElement *)anchorElement
312
358
  {
313
- return [self fb_scrollByNormalizedVector:CGVectorMake(0.0, -distance) anchorElement:anchorElement];
359
+ FBValidateScreenForScrolling();
360
+ return [self fb_scrollByNormalizedVector:CGVectorMake(0.0, -distance) anchorElement:anchorElement error:nil];
314
361
  }
315
362
 
316
363
  - (BOOL)fb_scrollLeftByNormalizedDistance:(CGFloat)distance
317
364
  anchorElement:(XCUIElement *)anchorElement
318
365
  {
319
- return [self fb_scrollByNormalizedVector:CGVectorMake(distance, 0.0) anchorElement:anchorElement];
366
+ FBValidateScreenForScrolling();
367
+ return [self fb_scrollByNormalizedVector:CGVectorMake(distance, 0.0) anchorElement:anchorElement error:nil];
320
368
  }
321
369
 
322
370
  - (BOOL)fb_scrollRightByNormalizedDistance:(CGFloat)distance
323
371
  anchorElement:(XCUIElement *)anchorElement
324
372
  {
325
- return [self fb_scrollByNormalizedVector:CGVectorMake(-distance, 0.0) anchorElement:anchorElement];
373
+ FBValidateScreenForScrolling();
374
+ return [self fb_scrollByNormalizedVector:CGVectorMake(-distance, 0.0) anchorElement:anchorElement error:nil];
326
375
  }
327
376
 
328
377
  - (BOOL)fb_scrollByNormalizedVector:(CGVector)normalizedScrollVector
329
378
  anchorElement:(XCUIElement *)anchorElement
379
+ error:(NSError **)error
330
380
  {
331
- CGVector scrollVector = CGVectorMake(CGRectGetWidth(self.scrollingFrame) * normalizedScrollVector.dx,
332
- CGRectGetHeight(self.scrollingFrame) * normalizedScrollVector.dy
381
+ XCUIScreen *screen = [FBScreen currentScreenWithError:error];
382
+ if (nil == screen) {
383
+ return NO;
384
+ }
385
+ CGRect frame = FBScrollFrameInApplication(self.visibleFrame, screen, anchorElement.application);
386
+ CGVector scrollVector = CGVectorMake(CGRectGetWidth(frame) * normalizedScrollVector.dx,
387
+ CGRectGetHeight(frame) * normalizedScrollVector.dy
333
388
  );
334
- return [self fb_scrollByVector:scrollVector anchorElement:anchorElement error:nil];
389
+ return [self fb_scrollByVector:scrollVector inFrame:frame anchorElement:anchorElement velocity:FBTouchVelocity error:error];
335
390
  }
336
391
 
337
392
  - (BOOL)fb_scrollByVector:(CGVector)vector
393
+ inFrame:(CGRect)frame
338
394
  anchorElement:(XCUIElement *)anchorElement
395
+ velocity:(CGFloat)velocity
339
396
  error:(NSError **)error
340
397
  {
341
- CGVector scrollBoundingVector = CGVectorMake(
342
- CGRectGetWidth(self.scrollingFrame) * FBScrollTouchProportion,
343
- CGRectGetHeight(self.scrollingFrame) * FBScrollTouchProportion
344
- );
345
- scrollBoundingVector.dx = (CGFloat)floor(copysign(scrollBoundingVector.dx, vector.dx));
346
- scrollBoundingVector.dy = (CGFloat)floor(copysign(scrollBoundingVector.dy, vector.dy));
347
-
398
+ // Keep the vector and clipped frame in the same coordinate space throughout
399
+ // this multi-drag gesture. Rotation or folding during a gesture is unsupported.
348
400
  NSInteger preciseScrollAttemptsCount = 20;
349
401
  CGVector CGZeroVector = CGVectorMake(0, 0);
350
402
  BOOL shouldFinishScrolling = NO;
351
403
  while (!shouldFinishScrolling) {
404
+ CGVector scrollBoundingVector = CGVectorMake(
405
+ (CGFloat)floor(copysign(CGRectGetWidth(frame) * FBScrollTouchProportion, vector.dx)),
406
+ (CGFloat)floor(copysign(CGRectGetHeight(frame) * FBScrollTouchProportion, vector.dy)));
352
407
  CGVector scrollVector = CGVectorMake(fabs(vector.dx) > fabs(scrollBoundingVector.dx) ? scrollBoundingVector.dx : vector.dx,
353
408
  fabs(vector.dy) > fabs(scrollBoundingVector.dy) ? scrollBoundingVector.dy : vector.dy);
354
409
  vector = CGVectorMake(vector.dx - scrollVector.dx, vector.dy - scrollVector.dy);
355
410
  shouldFinishScrolling = FBVectorFuzzyEqualToVector(vector, CGZeroVector, 1) || --preciseScrollAttemptsCount <= 0;
356
- if (![self fb_scrollAncestorScrollViewByVectorWithinScrollViewFrame:scrollVector anchorElement:anchorElement error:error]){
411
+ if (![self fb_scrollAncestorScrollViewByVectorWithinScrollViewFrame:scrollVector
412
+ inFrame:frame
413
+ anchorElement:anchorElement
414
+ velocity:velocity
415
+ error:error]) {
357
416
  return NO;
358
417
  }
359
418
  }
@@ -370,10 +429,11 @@ static XCUIElement *FBLiveElementForSnapshot(id<FBXCElementSnapshot> snapshot, X
370
429
  }
371
430
 
372
431
  - (BOOL)fb_scrollAncestorScrollViewByVectorWithinScrollViewFrame:(CGVector)vector
432
+ inFrame:(CGRect)scrollingFrame
373
433
  anchorElement:(XCUIElement *)anchorElement
434
+ velocity:(CGFloat)velocity
374
435
  error:(NSError **)error
375
436
  {
376
- CGRect scrollingFrame = self.scrollingFrame;
377
437
  // wdFrame matches scrollingFrame's coordinate space; raw .frame can be pre-scaled or
378
438
  // dimension-swapped and drift out of sync with it (appium/appium#16185).
379
439
  CGRect anchorFrame = anchorElement.wdFrame;
@@ -395,7 +455,7 @@ static XCUIElement *FBLiveElementForSnapshot(id<FBXCElementSnapshot> snapshot, X
395
455
 
396
456
  [startCoordinate pressForDuration:FBTouchEventDelay
397
457
  thenDragToCoordinate:endCoordinate
398
- withVelocity:FBTouchVelocity
458
+ withVelocity:velocity
399
459
  thenHoldForDuration:FBTouchEventDelay];
400
460
  return YES;
401
461
  }
@@ -21,6 +21,13 @@ NS_ASSUME_NONNULL_BEGIN
21
21
  */
22
22
  BOOL FBTypeText(NSString *text, NSUInteger typingSpeed, NSError **error);
23
23
 
24
+ #if TARGET_OS_IOS
25
+ /**
26
+ Waits up to `timeout` seconds for a hittable key. A miss does not fail the caller.
27
+ */
28
+ void FBWaitForHittableKey(XCUIApplication * _Nullable application, NSTimeInterval timeout);
29
+ #endif
30
+
24
31
  @interface XCUIElement (FBTyping)
25
32
 
26
33
  /**
@@ -10,6 +10,7 @@
10
10
 
11
11
  #import "FBConfiguration.h"
12
12
  #import "FBErrorBuilder.h"
13
+ #import "FBKeyboard.h"
13
14
  #import "FBXCElementSnapshotWrapper.h"
14
15
  #import "FBXCElementSnapshotWrapper+Helpers.h"
15
16
  #import "FBXCodeCompatibility.h"
@@ -39,6 +40,19 @@ BOOL FBTypeText(NSString *text, NSUInteger typingSpeed, NSError **error)
39
40
  return [FBXCTestDaemonsProxy synthesizeEventWithRecord:eventRecord error:error];
40
41
  }
41
42
 
43
+ #if TARGET_OS_IOS
44
+ void FBWaitForHittableKey(XCUIApplication *application, NSTimeInterval timeout)
45
+ {
46
+ if (nil == application) {
47
+ return;
48
+ }
49
+ NSError *visibilityError = nil;
50
+ [FBKeyboard waitUntilVisibleForApplication:application
51
+ timeout:timeout
52
+ error:&visibilityError];
53
+ }
54
+ #endif
55
+
42
56
  @interface NSString (FBRepeat)
43
57
 
44
58
  - (NSString *)fb_repeatTimes:(NSUInteger)times;
@@ -139,6 +153,12 @@ BOOL FBTypeText(NSString *text, NSUInteger typingSpeed, NSError **error)
139
153
  // partial workaround (keystrokes still don't always land - a known limitation).
140
154
  [self typeText:text];
141
155
  return YES;
156
+ #elif TARGET_OS_IOS
157
+ NSTimeInterval keyboardWait = FBConfiguration.sharedInstance.keyboardWaitTimeout;
158
+ if (keyboardWait > 0) {
159
+ FBWaitForHittableKey(self.application, keyboardWait);
160
+ }
161
+ return FBTypeText(text, frequency, error);
142
162
  #else
143
163
  return FBTypeText(text, frequency, error);
144
164
  #endif
@@ -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,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
@@ -65,6 +67,8 @@
65
67
  [[FBRoute GET:@"/wda/batteryInfo"] respondWithTarget:self action:@selector(handleGetBatteryInfo:)],
66
68
  #endif
67
69
  [[FBRoute POST:@"/wda/pressButton"] respondWithTarget:self action:@selector(handlePressButtonCommand:)],
70
+ [[FBRoute GET:@"/wda/pressableButtons"] respondWithTarget:self action:@selector(handleGetPressableButtons:)],
71
+ [[FBRoute GET:@"/wda/pressableButtons"].withoutSession respondWithTarget:self action:@selector(handleGetPressableButtons:)],
68
72
  #if TARGET_OS_WATCH
69
73
  [[FBRoute POST:@"/wda/rotateDigitalCrown"] respondWithTarget:self action:@selector(handleRotateDigitalCrownCommand:)],
70
74
  [[FBRoute POST:@"/wda/performHandGesture"] respondWithTarget:self action:@selector(handlePerformHandGestureCommand:)],
@@ -74,6 +78,10 @@
74
78
  [[FBRoute POST:@"/wda/expectNotification"] respondWithTarget:self action:@selector(handleExpectNotification:)],
75
79
  [[FBRoute POST:@"/wda/siri/activate"] respondWithTarget:self action:@selector(handleActivateSiri:)],
76
80
  [[FBRoute POST:@"/wda/apps/launchUnattached"].withoutSession respondWithTarget:self action:@selector(handleLaunchUnattachedApp:)],
81
+ [[FBRoute GET:@"/wda/device/hingeAngle"] respondWithTarget:self action:@selector(handleGetSimulatedHingeAngle:)],
82
+ [[FBRoute GET:@"/wda/device/hingeAngle"].withoutSession respondWithTarget:self action:@selector(handleGetSimulatedHingeAngle:)],
83
+ [[FBRoute POST:@"/wda/device/hingeAngle"] respondWithTarget:self action:@selector(handleSetSimulatedHingeAngle:)],
84
+ [[FBRoute POST:@"/wda/device/hingeAngle"].withoutSession respondWithTarget:self action:@selector(handleSetSimulatedHingeAngle:)],
77
85
  [[FBRoute GET:@"/wda/device/info"] respondWithTarget:self action:@selector(handleGetDeviceInfo:)],
78
86
  [[FBRoute POST:@"/wda/resetAppAuth"] respondWithTarget:self action:@selector(handleResetAppAuth:)],
79
87
  [[FBRoute GET:@"/wda/device/info"].withoutSession respondWithTarget:self action:@selector(handleGetDeviceInfo:)],
@@ -157,17 +165,48 @@
157
165
 
158
166
  + (id<FBResponsePayload>)handleGetScreen:(FBRouteRequest *)request
159
167
  {
168
+ NSError *error = nil;
169
+ XCUIScreen *screen = [FBScreen currentScreenWithError:&error];
170
+ if (nil == screen) {
171
+ return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription
172
+ traceback:nil]);
173
+ }
160
174
  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);
175
+ CGSize screenSize = CGSizeMake(CGRectGetWidth(screen.bounds) / screen.scale,
176
+ CGRectGetHeight(screen.bounds) / screen.scale);
177
+ #if !TARGET_OS_TV && !TARGET_OS_WATCH
178
+ XCUIApplication *activeApp = request.session ? request.session.activeApplication : XCUIApplication.fb_activeApplication;
179
+ UIInterfaceOrientation orientation = activeApp.interfaceOrientation;
180
+ // Screen bounds describe the panel's native orientation. Some physical iPads
181
+ // expose landscape-native bounds even while the application is in portrait.
182
+ // Normalize the full screen separately from compatibility-mode app frames.
183
+ if (UIInterfaceOrientationIsPortrait(orientation) && screenSize.width > screenSize.height) {
184
+ screenSize = CGSizeMake(screenSize.height, screenSize.width);
185
+ }
186
+ screenSize = FBAdjustDimensionsForApplication(screenSize, orientation);
169
187
  #endif
170
188
 
189
+ // SpringBoard can expose hidden status bars belonging to other displays, and
190
+ // Duo also exposes full-screen containers for its side-mounted status UI.
191
+ // Only a visible horizontal bar at the top of this display can be subtracted
192
+ // from a rectangular viewport.
193
+ CGSize statusBarSize = CGSizeZero;
194
+ for (XCUIElement *statusBar in app.statusBars.allElementsBoundByIndex) {
195
+ // Resolve the element before reading its screen: displayID can be zero on
196
+ // an unresolved query result even when the bar belongs to the main screen.
197
+ CGRect frame = statusBar.frame;
198
+ if (statusBar.screen.displayID != screen.displayID || !statusBar.fb_isVisible) {
199
+ continue;
200
+ }
201
+ if (fabs(CGRectGetMinY(frame)) <= 1 && fabs(CGRectGetMinX(frame)) <= 1
202
+ && fabs(CGRectGetWidth(frame) - screenSize.width) <= 1
203
+ && CGRectGetHeight(frame) > 0 && CGRectGetHeight(frame) < screenSize.height
204
+ && CGRectGetHeight(frame) < CGRectGetWidth(frame)) {
205
+ statusBarSize = frame.size;
206
+ break;
207
+ }
208
+ }
209
+
171
210
  return FBResponseWithObject(
172
211
  @{
173
212
  @"screenSize":@{@"width": @(screenSize.width),
@@ -176,8 +215,8 @@
176
215
  @"statusBarSize": @{@"width": @(statusBarSize.width),
177
216
  @"height": @(statusBarSize.height),
178
217
  },
179
- @"displayId": @([FBScreen displayID]),
180
- @"scale": @([FBScreen scale]),
218
+ @"displayId": @(screen.displayID),
219
+ @"scale": @(screen.scale),
181
220
  });
182
221
  }
183
222
 
@@ -315,6 +354,12 @@
315
354
  return FBResponseWithOK();
316
355
  }
317
356
 
357
+ + (id<FBResponsePayload>)handleGetPressableButtons:(FBRouteRequest *)request
358
+ {
359
+ NSArray *xctestButtonNames = [FBAvailableButtonNames().allKeys sortedArrayUsingSelector:@selector(compare:)];
360
+ return FBResponseWithObject(@{@"xctest": xctestButtonNames});
361
+ }
362
+
318
363
  #if TARGET_OS_WATCH
319
364
  + (id<FBResponsePayload>)handleRotateDigitalCrownCommand:(FBRouteRequest *)request
320
365
  {
@@ -777,4 +822,35 @@
777
822
  return FBResponseWithObject(result);
778
823
  }
779
824
 
825
+ + (id<FBResponsePayload>)handleGetSimulatedHingeAngle:(FBRouteRequest *)request
826
+ {
827
+ if (!XCUIDevice.sharedDevice.fb_supportsHingeAngleReading) {
828
+ return FBResponseWithStatus([FBCommandStatus unsupportedOperationErrorWithMessage:
829
+ @"Hinge angle reading is unavailable on this device" traceback:nil]);
830
+ }
831
+ NSError *error;
832
+ NSNumber *angle = [XCUIDevice.sharedDevice fb_getSimulatedHingeAngle:&error];
833
+ return nil == angle ? FBResponseWithUnknownError(error) : FBResponseWithObject(angle);
834
+ }
835
+
836
+ + (id<FBResponsePayload>)handleSetSimulatedHingeAngle:(FBRouteRequest *)request
837
+ {
838
+ id angle = request.arguments[@"angle"];
839
+ if (![angle isKindOfClass:NSNumber.class]
840
+ || CFGetTypeID((__bridge CFTypeRef)angle) == CFBooleanGetTypeID()
841
+ || !isfinite([angle doubleValue]) || [angle doubleValue] < 0 || [angle doubleValue] > 180) {
842
+ return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:
843
+ @"'angle' must be a finite number between 0 and 180 degrees" traceback:nil]);
844
+ }
845
+ if (!XCUIDevice.sharedDevice.fb_canAttemptSimulatedHingeAngleInjection) {
846
+ return FBResponseWithStatus([FBCommandStatus unsupportedOperationErrorWithMessage:
847
+ @"Setting a simulated hinge angle requires a compatible foldable device" traceback:nil]);
848
+ }
849
+ NSError *error;
850
+ if (![XCUIDevice.sharedDevice fb_setSimulatedHingeAngle:[angle doubleValue] error:&error]) {
851
+ return FBResponseWithUnknownError(error);
852
+ }
853
+ return FBResponseWithOK();
854
+ }
855
+
780
856
  @end