XCUIDevice+FBHelpers.m 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  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 "XCUIDevice+FBHelpers.h"
  10. #import <arpa/inet.h>
  11. #import <ifaddrs.h>
  12. #include <notify.h>
  13. #import <objc/runtime.h>
  14. #import "FBErrorBuilder.h"
  15. #import "FBImageUtils.h"
  16. #import "FBMacros.h"
  17. #import "FBMathUtils.h"
  18. #import "FBScreenshot.h"
  19. #import "FBXCDeviceEvent.h"
  20. #import "FBXCodeCompatibility.h"
  21. #import "FBXCTestDaemonsProxy.h"
  22. #import "XCUIDevice.h"
  23. static const NSTimeInterval FBHomeButtonCoolOffTime = 1.;
  24. static const NSTimeInterval FBScreenLockTimeout = 5.;
  25. @implementation XCUIDevice (FBHelpers)
  26. static bool fb_isLocked;
  27. #pragma clang diagnostic push
  28. #pragma clang diagnostic ignored "-Wobjc-load-method"
  29. + (void)load
  30. {
  31. [self fb_registerAppforDetectLockState];
  32. }
  33. #pragma clang diagnostic pop
  34. + (void)fb_registerAppforDetectLockState
  35. {
  36. int notify_token;
  37. #pragma clang diagnostic push
  38. #pragma clang diagnostic ignored "-Wstrict-prototypes"
  39. notify_register_dispatch("com.apple.springboard.lockstate", &notify_token, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(int token) {
  40. uint64_t state = UINT64_MAX;
  41. notify_get_state(token, &state);
  42. fb_isLocked = state != 0;
  43. });
  44. #pragma clang diagnostic pop
  45. }
  46. - (BOOL)fb_goToHomescreenWithError:(NSError **)error
  47. {
  48. return [XCUIApplication fb_switchToSystemApplicationWithError:error];
  49. }
  50. - (BOOL)fb_lockScreen:(NSError **)error
  51. {
  52. if (fb_isLocked) {
  53. return YES;
  54. }
  55. [self pressLockButton];
  56. return [[[[FBRunLoopSpinner new]
  57. timeout:FBScreenLockTimeout]
  58. timeoutErrorMessage:@"Timed out while waiting until the screen gets locked"]
  59. spinUntilTrue:^BOOL{
  60. return fb_isLocked;
  61. } error:error];
  62. }
  63. - (BOOL)fb_isScreenLocked
  64. {
  65. return fb_isLocked;
  66. }
  67. - (BOOL)fb_unlockScreen:(NSError **)error
  68. {
  69. if (!fb_isLocked) {
  70. return YES;
  71. }
  72. [self pressButton:XCUIDeviceButtonHome];
  73. [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:FBHomeButtonCoolOffTime]];
  74. #if !TARGET_OS_TV
  75. [self pressButton:XCUIDeviceButtonHome];
  76. #else
  77. [self pressButton:XCUIDeviceButtonHome];
  78. #endif
  79. [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:FBHomeButtonCoolOffTime]];
  80. return [[[[FBRunLoopSpinner new]
  81. timeout:FBScreenLockTimeout]
  82. timeoutErrorMessage:@"Timed out while waiting until the screen gets unlocked"]
  83. spinUntilTrue:^BOOL{
  84. return !fb_isLocked;
  85. } error:error];
  86. }
  87. - (NSData *)fb_screenshotWithError:(NSError*__autoreleasing*)error
  88. {
  89. return [FBScreenshot takeInOriginalResolutionWithQuality:FBConfiguration.screenshotQuality
  90. error:error];
  91. }
  92. - (BOOL)fb_fingerTouchShouldMatch:(BOOL)shouldMatch
  93. {
  94. const char *name;
  95. if (shouldMatch) {
  96. name = "com.apple.BiometricKit_Sim.fingerTouch.match";
  97. } else {
  98. name = "com.apple.BiometricKit_Sim.fingerTouch.nomatch";
  99. }
  100. return notify_post(name) == NOTIFY_STATUS_OK;
  101. }
  102. - (NSString *)fb_wifiIPAddress
  103. {
  104. struct ifaddrs *interfaces = NULL;
  105. struct ifaddrs *temp_addr = NULL;
  106. int success = getifaddrs(&interfaces);
  107. if (success != 0) {
  108. freeifaddrs(interfaces);
  109. return nil;
  110. }
  111. NSString *address = nil;
  112. temp_addr = interfaces;
  113. while(temp_addr != NULL) {
  114. if(temp_addr->ifa_addr->sa_family != AF_INET) {
  115. temp_addr = temp_addr->ifa_next;
  116. continue;
  117. }
  118. NSString *interfaceName = [NSString stringWithUTF8String:temp_addr->ifa_name];
  119. if(![interfaceName isEqualToString:@"en0"]) {
  120. temp_addr = temp_addr->ifa_next;
  121. continue;
  122. }
  123. address = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr)];
  124. break;
  125. }
  126. freeifaddrs(interfaces);
  127. return address;
  128. }
  129. - (BOOL)fb_openUrl:(NSString *)url error:(NSError **)error
  130. {
  131. NSURL *parsedUrl = [NSURL URLWithString:url];
  132. if (nil == parsedUrl) {
  133. return [[[FBErrorBuilder builder]
  134. withDescriptionFormat:@"'%@' is not a valid URL", url]
  135. buildError:error];
  136. }
  137. NSError *err;
  138. if ([FBXCTestDaemonsProxy openDefaultApplicationForURL:parsedUrl error:&err]) {
  139. return YES;
  140. }
  141. if (![err.description containsString:@"does not support"]) {
  142. if (error) {
  143. *error = err;
  144. }
  145. return NO;
  146. }
  147. id siriService = [self valueForKey:@"siriService"];
  148. if (nil != siriService) {
  149. return [self fb_activateSiriVoiceRecognitionWithText:[NSString stringWithFormat:@"Open {%@}", url] error:error];
  150. }
  151. NSString *description = [NSString stringWithFormat:@"Cannot open '%@' with the default application assigned for it. Consider upgrading to Xcode 14.3+/iOS 16.4+", url];
  152. return [[[FBErrorBuilder builder]
  153. withDescriptionFormat:@"%@", description]
  154. buildError:error];;
  155. }
  156. - (BOOL)fb_openUrl:(NSString *)url withApplication:(NSString *)bundleId error:(NSError **)error
  157. {
  158. NSURL *parsedUrl = [NSURL URLWithString:url];
  159. if (nil == parsedUrl) {
  160. return [[[FBErrorBuilder builder]
  161. withDescriptionFormat:@"'%@' is not a valid URL", url]
  162. buildError:error];
  163. }
  164. return [FBXCTestDaemonsProxy openURL:parsedUrl usingApplication:bundleId error:error];
  165. }
  166. - (BOOL)fb_activateSiriVoiceRecognitionWithText:(NSString *)text error:(NSError **)error
  167. {
  168. id siriService = [self valueForKey:@"siriService"];
  169. if (nil == siriService) {
  170. return [[[FBErrorBuilder builder]
  171. withDescription:@"Siri service is not available on the device under test"]
  172. buildError:error];
  173. }
  174. SEL selector = NSSelectorFromString(@"activateWithVoiceRecognitionText:");
  175. NSMethodSignature *signature = [siriService methodSignatureForSelector:selector];
  176. NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
  177. [invocation setSelector:selector];
  178. [invocation setArgument:&text atIndex:2];
  179. @try {
  180. [invocation invokeWithTarget:siriService];
  181. return YES;
  182. } @catch (NSException *e) {
  183. return [[[FBErrorBuilder builder]
  184. withDescriptionFormat:@"%@", e.reason]
  185. buildError:error];
  186. }
  187. }
  188. - (BOOL)fb_pressButton:(NSString *)buttonName
  189. forDuration:(nullable NSNumber *)duration
  190. error:(NSError **)error
  191. {
  192. #if !TARGET_OS_TV
  193. return [self fb_pressButton:buttonName error:error];
  194. #else
  195. NSMutableArray<NSString *> *supportedButtonNames = [NSMutableArray array];
  196. NSInteger remoteButton = -1; // no remote button
  197. if ([buttonName.lowercaseString isEqualToString:@"home"]) {
  198. // XCUIRemoteButtonHome = 7
  199. remoteButton = XCUIRemoteButtonHome;
  200. }
  201. [supportedButtonNames addObject:@"home"];
  202. // https://developer.apple.com/design/human-interface-guidelines/tvos/remote-and-controllers/remote/
  203. if ([buttonName.lowercaseString isEqualToString:@"up"]) {
  204. // XCUIRemoteButtonUp = 0,
  205. remoteButton = XCUIRemoteButtonUp;
  206. }
  207. [supportedButtonNames addObject:@"up"];
  208. if ([buttonName.lowercaseString isEqualToString:@"down"]) {
  209. // XCUIRemoteButtonDown = 1,
  210. remoteButton = XCUIRemoteButtonDown;
  211. }
  212. [supportedButtonNames addObject:@"down"];
  213. if ([buttonName.lowercaseString isEqualToString:@"left"]) {
  214. // XCUIRemoteButtonLeft = 2,
  215. remoteButton = XCUIRemoteButtonLeft;
  216. }
  217. [supportedButtonNames addObject:@"left"];
  218. if ([buttonName.lowercaseString isEqualToString:@"right"]) {
  219. // XCUIRemoteButtonRight = 3,
  220. remoteButton = XCUIRemoteButtonRight;
  221. }
  222. [supportedButtonNames addObject:@"right"];
  223. if ([buttonName.lowercaseString isEqualToString:@"menu"]) {
  224. // XCUIRemoteButtonMenu = 5,
  225. remoteButton = XCUIRemoteButtonMenu;
  226. }
  227. [supportedButtonNames addObject:@"menu"];
  228. if ([buttonName.lowercaseString isEqualToString:@"playpause"]) {
  229. // XCUIRemoteButtonPlayPause = 6,
  230. remoteButton = XCUIRemoteButtonPlayPause;
  231. }
  232. [supportedButtonNames addObject:@"playpause"];
  233. if ([buttonName.lowercaseString isEqualToString:@"select"]) {
  234. // XCUIRemoteButtonSelect = 4,
  235. remoteButton = XCUIRemoteButtonSelect;
  236. }
  237. [supportedButtonNames addObject:@"select"];
  238. if (remoteButton == -1) {
  239. return [[[FBErrorBuilder builder]
  240. withDescriptionFormat:@"The button '%@' is unknown. Only the following button names are supported: %@", buttonName, supportedButtonNames]
  241. buildError:error];
  242. }
  243. if (duration) {
  244. // https://developer.apple.com/documentation/xctest/xcuiremote/1627475-pressbutton
  245. [[XCUIRemote sharedRemote] pressButton:remoteButton forDuration:duration.doubleValue];
  246. } else {
  247. // https://developer.apple.com/documentation/xctest/xcuiremote/1627476-pressbutton
  248. [[XCUIRemote sharedRemote] pressButton:remoteButton];
  249. }
  250. return YES;
  251. #endif
  252. }
  253. #if !TARGET_OS_TV
  254. - (BOOL)fb_pressButton:(NSString *)buttonName
  255. error:(NSError **)error
  256. {
  257. NSMutableArray<NSString *> *supportedButtonNames = [NSMutableArray array];
  258. XCUIDeviceButton dstButton = 0;
  259. if ([buttonName.lowercaseString isEqualToString:@"home"]) {
  260. dstButton = XCUIDeviceButtonHome;
  261. }
  262. [supportedButtonNames addObject:@"home"];
  263. #if !TARGET_OS_SIMULATOR
  264. if ([buttonName.lowercaseString isEqualToString:@"volumeup"]) {
  265. dstButton = XCUIDeviceButtonVolumeUp;
  266. }
  267. if ([buttonName.lowercaseString isEqualToString:@"volumedown"]) {
  268. dstButton = XCUIDeviceButtonVolumeDown;
  269. }
  270. [supportedButtonNames addObject:@"volumeUp"];
  271. [supportedButtonNames addObject:@"volumeDown"];
  272. #endif
  273. if (dstButton == 0) {
  274. return [[[FBErrorBuilder builder]
  275. withDescriptionFormat:@"The button '%@' is unknown. Only the following button names are supported: %@", buttonName, supportedButtonNames]
  276. buildError:error];
  277. }
  278. [self pressButton:dstButton];
  279. return YES;
  280. }
  281. #endif
  282. - (BOOL)fb_performIOHIDEventWithPage:(unsigned int)page
  283. usage:(unsigned int)usage
  284. duration:(NSTimeInterval)duration
  285. error:(NSError **)error
  286. {
  287. id<FBXCDeviceEvent> event = FBCreateXCDeviceEvent(page, usage, duration, error);
  288. return nil == event ? NO : [self performDeviceEvent:event error:error];
  289. }
  290. - (BOOL)fb_setAppearance:(FBUIInterfaceAppearance)appearance error:(NSError **)error
  291. {
  292. SEL selector = NSSelectorFromString(@"setAppearanceMode:");
  293. if (nil != selector && [self respondsToSelector:selector]) {
  294. NSMethodSignature *signature = [self methodSignatureForSelector:selector];
  295. NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
  296. [invocation setSelector:selector];
  297. [invocation setTarget:self];
  298. [invocation setArgument:&appearance atIndex:2];
  299. [invocation invoke];
  300. return YES;
  301. }
  302. #if __clang_major__ >= 15 || (__clang_major__ >= 14 && __clang_minor__ >= 0 && __clang_patchlevel__ >= 3)
  303. // Xcode 14.3.1 can build these values.
  304. // For iOS 17+
  305. if ([self respondsToSelector:NSSelectorFromString(@"appearance")]) {
  306. self.appearance = (XCUIDeviceAppearance) appearance;
  307. return YES;
  308. }
  309. #endif
  310. return [[[FBErrorBuilder builder]
  311. withDescriptionFormat:@"Current Xcode SDK does not support appearance changing"]
  312. buildError:error];
  313. }
  314. - (NSNumber *)fb_getAppearance
  315. {
  316. #if __clang_major__ >= 15 || (__clang_major__ >= 14 && __clang_minor__ >= 0 && __clang_patchlevel__ >= 3)
  317. // Xcode 14.3.1 can build these values.
  318. // For iOS 17+
  319. if ([self respondsToSelector:NSSelectorFromString(@"appearance")]) {
  320. return [NSNumber numberWithLongLong:[self appearance]];
  321. }
  322. #endif
  323. return [self respondsToSelector:@selector(appearanceMode)]
  324. ? [NSNumber numberWithLongLong:[self appearanceMode]]
  325. : nil;
  326. }
  327. #if !TARGET_OS_TV
  328. - (BOOL)fb_setSimulatedLocation:(CLLocation *)location error:(NSError **)error
  329. {
  330. return [FBXCTestDaemonsProxy setSimulatedLocation:location error:error];
  331. }
  332. - (nullable CLLocation *)fb_getSimulatedLocation:(NSError **)error
  333. {
  334. return [FBXCTestDaemonsProxy getSimulatedLocation:error];
  335. }
  336. - (BOOL)fb_clearSimulatedLocation:(NSError **)error
  337. {
  338. return [FBXCTestDaemonsProxy clearSimulatedLocation:error];
  339. }
  340. #endif
  341. @end