FBAlert.m 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  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. An additional grant
  7. * of patent rights can be found in the PATENTS file in the same directory.
  8. */
  9. #import "FBAlert.h"
  10. #import "FBConfiguration.h"
  11. #import "FBErrorBuilder.h"
  12. #import "FBLogger.h"
  13. #import "FBXCElementSnapshotWrapper+Helpers.h"
  14. #import "FBXCodeCompatibility.h"
  15. #import "XCUIApplication.h"
  16. #import "XCUIApplication+FBAlert.h"
  17. #import "XCUIElement+FBClassChain.h"
  18. #import "XCUIElement+FBTyping.h"
  19. #import "XCUIElement+FBUtilities.h"
  20. #import "XCUIElement+FBWebDriverAttributes.h"
  21. @interface FBAlert ()
  22. @property (nonatomic, strong) XCUIApplication *application;
  23. @property (nonatomic, strong, nullable) XCUIElement *element;
  24. @end
  25. @implementation FBAlert
  26. + (instancetype)alertWithApplication:(XCUIApplication *)application
  27. {
  28. FBAlert *alert = [FBAlert new];
  29. alert.application = application;
  30. return alert;
  31. }
  32. + (instancetype)alertWithElement:(XCUIElement *)element
  33. {
  34. FBAlert *alert = [FBAlert new];
  35. alert.element = element;
  36. alert.application = element.application;
  37. return alert;
  38. }
  39. - (BOOL)isPresent
  40. {
  41. @try {
  42. if (nil == self.alertElement) {
  43. return NO;
  44. }
  45. [self.alertElement fb_customSnapshot];
  46. return YES;
  47. } @catch (NSException *) {
  48. return NO;
  49. }
  50. }
  51. - (BOOL)notPresentWithError:(NSError **)error
  52. {
  53. return [[[FBErrorBuilder builder]
  54. withDescriptionFormat:@"No alert is open"]
  55. buildError:error];
  56. }
  57. + (BOOL)isSafariWebAlertWithSnapshot:(id<FBXCElementSnapshot>)snapshot
  58. {
  59. if (snapshot.elementType != XCUIElementTypeOther) {
  60. return NO;
  61. }
  62. FBXCElementSnapshotWrapper *snapshotWrapper = [FBXCElementSnapshotWrapper ensureWrapped:snapshot];
  63. id<FBXCElementSnapshot> application = [snapshotWrapper fb_parentMatchingType:XCUIElementTypeApplication];
  64. return nil != application && [application.label isEqualToString:FB_SAFARI_APP_NAME];
  65. }
  66. - (NSString *)text
  67. {
  68. if (!self.isPresent) {
  69. return nil;
  70. }
  71. NSMutableArray<NSString *> *resultText = [NSMutableArray array];
  72. id<FBXCElementSnapshot> snapshot = self.alertElement.lastSnapshot ?: [self.alertElement fb_customSnapshot];
  73. BOOL isSafariAlert = [self.class isSafariWebAlertWithSnapshot:snapshot];
  74. [snapshot enumerateDescendantsUsingBlock:^(id<FBXCElementSnapshot> descendant) {
  75. XCUIElementType elementType = descendant.elementType;
  76. if (!(elementType == XCUIElementTypeTextView || elementType == XCUIElementTypeStaticText)) {
  77. return;
  78. }
  79. FBXCElementSnapshotWrapper *descendantWrapper = [FBXCElementSnapshotWrapper ensureWrapped:descendant];
  80. if (elementType == XCUIElementTypeStaticText
  81. && nil != [descendantWrapper fb_parentMatchingType:XCUIElementTypeButton]) {
  82. return;
  83. }
  84. NSString *text = descendantWrapper.wdLabel ?: descendantWrapper.wdValue;
  85. if (isSafariAlert && nil != descendant.parent) {
  86. FBXCElementSnapshotWrapper *descendantParentWrapper = [FBXCElementSnapshotWrapper ensureWrapped:descendant.parent];
  87. NSString *parentText = descendantParentWrapper.wdLabel ?: descendantParentWrapper.wdValue;
  88. if ([parentText isEqualToString:text]) {
  89. // Avoid duplicated texts on Safari alerts
  90. return;
  91. }
  92. }
  93. if (nil != text) {
  94. [resultText addObject:[NSString stringWithFormat:@"%@", text]];
  95. }
  96. }];
  97. return [resultText componentsJoinedByString:@"\n"];
  98. }
  99. - (BOOL)typeText:(NSString *)text error:(NSError **)error
  100. {
  101. if (!self.isPresent) {
  102. return [self notPresentWithError:error];
  103. }
  104. NSPredicate *textCollectorPredicate = [NSPredicate predicateWithFormat:@"elementType IN {%lu,%lu}",
  105. XCUIElementTypeTextField, XCUIElementTypeSecureTextField];
  106. NSArray<XCUIElement *> *dstFields = [[self.alertElement descendantsMatchingType:XCUIElementTypeAny]
  107. matchingPredicate:textCollectorPredicate].allElementsBoundByIndex;
  108. if (dstFields.count > 1) {
  109. return [[[FBErrorBuilder builder]
  110. withDescriptionFormat:@"The alert contains more than one input field"]
  111. buildError:error];
  112. }
  113. if (0 == dstFields.count) {
  114. return [[[FBErrorBuilder builder]
  115. withDescriptionFormat:@"The alert contains no input fields"]
  116. buildError:error];
  117. }
  118. return [dstFields.firstObject fb_typeText:text
  119. shouldClear:YES
  120. error:error];
  121. }
  122. - (NSArray *)buttonLabels
  123. {
  124. if (!self.isPresent) {
  125. return nil;
  126. }
  127. NSMutableArray<NSString *> *labels = [NSMutableArray array];
  128. id<FBXCElementSnapshot> alertSnapshot = self.alertElement.lastSnapshot ?: [self.alertElement fb_customSnapshot];
  129. [alertSnapshot enumerateDescendantsUsingBlock:^(id<FBXCElementSnapshot> descendant) {
  130. if (descendant.elementType != XCUIElementTypeButton) {
  131. return;
  132. }
  133. NSString *label = [FBXCElementSnapshotWrapper ensureWrapped:descendant].wdLabel;
  134. if (nil != label) {
  135. [labels addObject:[NSString stringWithFormat:@"%@", label]];
  136. }
  137. }];
  138. return labels.copy;
  139. }
  140. - (BOOL)acceptWithError:(NSError **)error
  141. {
  142. if (!self.isPresent) {
  143. return [self notPresentWithError:error];
  144. }
  145. id<FBXCElementSnapshot> alertSnapshot = self.alertElement.lastSnapshot ?: [self.alertElement fb_customSnapshot];
  146. XCUIElement *acceptButton = nil;
  147. if (FBConfiguration.acceptAlertButtonSelector.length) {
  148. NSString *errorReason = nil;
  149. @try {
  150. acceptButton = [[self.alertElement fb_descendantsMatchingClassChain:FBConfiguration.acceptAlertButtonSelector
  151. shouldReturnAfterFirstMatch:YES] firstObject];
  152. } @catch (NSException *ex) {
  153. errorReason = ex.reason;
  154. }
  155. if (nil == acceptButton) {
  156. [FBLogger logFmt:@"Cannot find any match for Accept alert button using the class chain selector '%@'",
  157. FBConfiguration.acceptAlertButtonSelector];
  158. if (nil != errorReason) {
  159. [FBLogger logFmt:@"Original error: %@", errorReason];
  160. }
  161. [FBLogger log:@"Will fallback to the default button location algorithm"];
  162. }
  163. }
  164. if (nil == acceptButton) {
  165. NSArray<XCUIElement *> *buttons = [self.alertElement.fb_query
  166. descendantsMatchingType:XCUIElementTypeButton].allElementsBoundByIndex;
  167. acceptButton = (alertSnapshot.elementType == XCUIElementTypeAlert || [self.class isSafariWebAlertWithSnapshot:alertSnapshot])
  168. ? buttons.lastObject
  169. : buttons.firstObject;
  170. }
  171. if (nil == acceptButton) {
  172. return [[[FBErrorBuilder builder]
  173. withDescriptionFormat:@"Failed to find accept button for alert: %@", self.alertElement]
  174. buildError:error];
  175. }
  176. [acceptButton tap];
  177. return YES;
  178. }
  179. - (BOOL)dismissWithError:(NSError **)error
  180. {
  181. if (!self.isPresent) {
  182. return [self notPresentWithError:error];
  183. }
  184. id<FBXCElementSnapshot> alertSnapshot = self.alertElement.lastSnapshot ?: [self.alertElement fb_customSnapshot];
  185. XCUIElement *dismissButton = nil;
  186. if (FBConfiguration.dismissAlertButtonSelector.length) {
  187. NSString *errorReason = nil;
  188. @try {
  189. dismissButton = [[self.alertElement fb_descendantsMatchingClassChain:FBConfiguration.dismissAlertButtonSelector
  190. shouldReturnAfterFirstMatch:YES] firstObject];
  191. } @catch (NSException *ex) {
  192. errorReason = ex.reason;
  193. }
  194. if (nil == dismissButton) {
  195. [FBLogger logFmt:@"Cannot find any match for Dismiss alert button using the class chain selector '%@'",
  196. FBConfiguration.dismissAlertButtonSelector];
  197. if (nil != errorReason) {
  198. [FBLogger logFmt:@"Original error: %@", errorReason];
  199. }
  200. [FBLogger log:@"Will fallback to the default button location algorithm"];
  201. }
  202. }
  203. if (nil == dismissButton) {
  204. NSArray<XCUIElement *> *buttons = [self.alertElement.fb_query
  205. descendantsMatchingType:XCUIElementTypeButton].allElementsBoundByIndex;
  206. dismissButton = (alertSnapshot.elementType == XCUIElementTypeAlert || [self.class isSafariWebAlertWithSnapshot:alertSnapshot])
  207. ? buttons.firstObject
  208. : buttons.lastObject;
  209. }
  210. if (nil == dismissButton) {
  211. return [[[FBErrorBuilder builder]
  212. withDescriptionFormat:@"Failed to find dismiss button for alert: %@", self.alertElement]
  213. buildError:error];
  214. }
  215. [dismissButton tap];
  216. return YES;
  217. }
  218. - (BOOL)clickAlertButton:(NSString *)label error:(NSError **)error
  219. {
  220. if (!self.isPresent) {
  221. return [self notPresentWithError:error];
  222. }
  223. NSPredicate *predicate = [NSPredicate predicateWithFormat:@"label == %@", label];
  224. XCUIElement *requestedButton = [[self.alertElement descendantsMatchingType:XCUIElementTypeButton]
  225. matchingPredicate:predicate].allElementsBoundByIndex.firstObject;
  226. if (!requestedButton) {
  227. return [[[FBErrorBuilder builder]
  228. withDescriptionFormat:@"Failed to find button with label '%@' for alert: %@", label, self.alertElement]
  229. buildError:error];
  230. }
  231. [requestedButton tap];
  232. return YES;
  233. }
  234. - (XCUIElement *)alertElement
  235. {
  236. if (nil == self.element) {
  237. XCUIApplication *systemApp = XCUIApplication.fb_systemApplication;
  238. if ([systemApp fb_isSameAppAs:self.application]) {
  239. self.element = systemApp.fb_alertElement;
  240. } else {
  241. self.element = systemApp.fb_alertElement ?: self.application.fb_alertElement;
  242. }
  243. }
  244. return self.element;
  245. }
  246. @end